Compare commits
17 Commits
chore/fix.
...
fix/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f4b91900e | ||
|
|
2718c7ad5f | ||
|
|
5713bb17cf | ||
|
|
b78de8dbb4 | ||
|
|
256d7cf19c | ||
|
|
b87499b56e | ||
|
|
1c7badff2f | ||
|
|
5b0eda07f2 | ||
|
|
6d05e8622c | ||
|
|
c3272736fb | ||
|
|
fedfd6a0f4 | ||
|
|
ab2e946087 | ||
|
|
cc4fe205fb | ||
|
|
36dc129328 | ||
|
|
7b4803a479 | ||
|
|
f662654afb | ||
|
|
a8e2d728c9 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Recover from `@anthropic-ai/claude-code` JSON truncation bug that caused Task Master to crash when handling large (>8 kB) structured responses. The CLI/SDK still truncates, but Task Master now detects the error, preserves buffered text, and returns a usable response instead of throwing.
|
||||
12
.changeset/claude-import-fix-new.md
Normal file
12
.changeset/claude-import-fix-new.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Prevent CLAUDE.md overwrite by using Claude Code's import feature
|
||||
|
||||
- Task Master now creates its instructions in `.taskmaster/CLAUDE.md` instead of overwriting the user's `CLAUDE.md`
|
||||
- Adds an import section to the user's CLAUDE.md that references the Task Master instructions
|
||||
- Preserves existing user content in CLAUDE.md files
|
||||
- Provides clean uninstall that only removes Task Master's additions
|
||||
|
||||
**Breaking Change**: Task Master instructions for Claude Code are now stored in `.taskmaster/CLAUDE.md` and imported into the main CLAUDE.md file. Users who previously had Task Master content directly in their CLAUDE.md will need to run `task-master rules remove claude` followed by `task-master rules add claude` to migrate to the new structure.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Updating dependency ai-sdk-provider-gemini-cli to 0.0.4 to address breaking change Google made to Gemini CLI and add better 'api-key' in addition to 'gemini-api-key' AI-SDK compatibility.
|
||||
7
.changeset/fix-show-command-complexity.md
Normal file
7
.changeset/fix-show-command-complexity.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix: show command no longer requires complexity report file to exist
|
||||
|
||||
The `tm show` command was incorrectly requiring the complexity report file to exist even when not needed. Now it only validates the complexity report path when a custom report file is explicitly provided via the -r/--report option.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add support for xAI Grok 4 model
|
||||
|
||||
- Add grok-4 model to xAI provider with $3/$15 per 1M token pricing
|
||||
- Enable main, fallback, and research roles for grok-4
|
||||
- Max tokens set to 131,072 (matching other xAI models)
|
||||
10
.changeset/groq-kimi-k2-support.md
Normal file
10
.changeset/groq-kimi-k2-support.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Complete Groq provider integration and add MoonshotAI Kimi K2 model support
|
||||
|
||||
- Fixed Groq provider registration
|
||||
- Added Groq API key validation
|
||||
- Added GROQ_API_KEY to .env.example
|
||||
- Added moonshotai/kimi-k2-instruct model with $1/$3 per 1M token pricing and 16k max output
|
||||
7
.changeset/metal-papers-stay.md
Normal file
7
.changeset/metal-papers-stay.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
feat: Add Zed editor rule profile with agent rules and MCP config
|
||||
|
||||
- Resolves #637
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"mode": "exit",
|
||||
"tag": "rc",
|
||||
"initialVersions": {
|
||||
"task-master-ai": "0.19.0"
|
||||
},
|
||||
"changesets": [
|
||||
"claude-code-json-truncation",
|
||||
"cuddly-baboons-invent",
|
||||
"grok-4-support",
|
||||
"quick-laws-cover",
|
||||
"some-lies-grin",
|
||||
"spicy-badgers-fail",
|
||||
"tender-ads-joke"
|
||||
]
|
||||
}
|
||||
5
.changeset/public-crabs-ask.md
Normal file
5
.changeset/public-crabs-ask.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add Amp rule profile with AGENT.md and MCP config
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add stricter validation and clearer feedback for task priority when adding new tasks
|
||||
|
||||
- if a task priority is invalid, it will default to medium
|
||||
- made taks priority case-insensitive, essentially making HIGH and high the same value
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add support for MCP Sampling as AI provider, requires no API key, uses the client LLM provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Unify and streamline profile system architecture for improved maintainability
|
||||
5
.changeset/swift-turtles-sit.md
Normal file
5
.changeset/swift-turtles-sit.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Add MCP configuration support to Claude Code rules
|
||||
7
.changeset/ten-glasses-feel.md
Normal file
7
.changeset/ten-glasses-feel.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fixed the comprehensive taskmaster system integration via custom slash commands with proper syntax
|
||||
|
||||
- Provide claude clode with a complete set of of commands that can trigger task master events directly within Claude Code
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Added Groq provider support
|
||||
5
.changeset/update-mcp-readme.md
Normal file
5
.changeset/update-mcp-readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Correct MCP server name and use 'Add to Cursor' button with updated placeholder keys.
|
||||
7
.changeset/yellow-showers-heal.md
Normal file
7
.changeset/yellow-showers-heal.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add OpenCode profile with AGENTS.md and MCP config
|
||||
|
||||
- Resolves #965
|
||||
5
.changeset/yummy-walls-eat.md
Normal file
5
.changeset/yummy-walls-eat.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Add missing API keys to .env.example and README.md
|
||||
@@ -1,130 +0,0 @@
|
||||
# Task Master Command Reference
|
||||
|
||||
Comprehensive command structure for Task Master integration with Claude Code.
|
||||
|
||||
## Command Organization
|
||||
|
||||
Commands are organized hierarchically to match Task Master's CLI structure while providing enhanced Claude Code integration.
|
||||
|
||||
## Project Setup & Configuration
|
||||
|
||||
### `/project:tm/init`
|
||||
- `index` - Initialize new project (handles PRD files intelligently)
|
||||
- `quick` - Quick setup with auto-confirmation (-y flag)
|
||||
|
||||
### `/project:tm/models`
|
||||
- `index` - View current AI model configuration
|
||||
- `setup` - Interactive model configuration
|
||||
- `set-main` - Set primary generation model
|
||||
- `set-research` - Set research model
|
||||
- `set-fallback` - Set fallback model
|
||||
|
||||
## Task Generation
|
||||
|
||||
### `/project:tm/parse-prd`
|
||||
- `index` - Generate tasks from PRD document
|
||||
- `with-research` - Enhanced parsing with research mode
|
||||
|
||||
### `/project:tm/generate`
|
||||
- Create individual task files from tasks.json
|
||||
|
||||
## Task Management
|
||||
|
||||
### `/project:tm/list`
|
||||
- `index` - Smart listing with natural language filters
|
||||
- `with-subtasks` - Include subtasks in hierarchical view
|
||||
- `by-status` - Filter by specific status
|
||||
|
||||
### `/project:tm/set-status`
|
||||
- `to-pending` - Reset task to pending
|
||||
- `to-in-progress` - Start working on task
|
||||
- `to-done` - Mark task complete
|
||||
- `to-review` - Submit for review
|
||||
- `to-deferred` - Defer task
|
||||
- `to-cancelled` - Cancel task
|
||||
|
||||
### `/project:tm/sync-readme`
|
||||
- Export tasks to README.md with formatting
|
||||
|
||||
### `/project:tm/update`
|
||||
- `index` - Update tasks with natural language
|
||||
- `from-id` - Update multiple tasks from a starting point
|
||||
- `single` - Update specific task
|
||||
|
||||
### `/project:tm/add-task`
|
||||
- `index` - Add new task with AI assistance
|
||||
|
||||
### `/project:tm/remove-task`
|
||||
- `index` - Remove task with confirmation
|
||||
|
||||
## Subtask Management
|
||||
|
||||
### `/project:tm/add-subtask`
|
||||
- `index` - Add new subtask to parent
|
||||
- `from-task` - Convert existing task to subtask
|
||||
|
||||
### `/project:tm/remove-subtask`
|
||||
- Remove subtask (with optional conversion)
|
||||
|
||||
### `/project:tm/clear-subtasks`
|
||||
- `index` - Clear subtasks from specific task
|
||||
- `all` - Clear all subtasks globally
|
||||
|
||||
## Task Analysis & Breakdown
|
||||
|
||||
### `/project:tm/analyze-complexity`
|
||||
- Analyze and generate expansion recommendations
|
||||
|
||||
### `/project:tm/complexity-report`
|
||||
- Display complexity analysis report
|
||||
|
||||
### `/project:tm/expand`
|
||||
- `index` - Break down specific task
|
||||
- `all` - Expand all eligible tasks
|
||||
- `with-research` - Enhanced expansion
|
||||
|
||||
## Task Navigation
|
||||
|
||||
### `/project:tm/next`
|
||||
- Intelligent next task recommendation
|
||||
|
||||
### `/project:tm/show`
|
||||
- Display detailed task information
|
||||
|
||||
### `/project:tm/status`
|
||||
- Comprehensive project dashboard
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### `/project:tm/add-dependency`
|
||||
- Add task dependency
|
||||
|
||||
### `/project:tm/remove-dependency`
|
||||
- Remove task dependency
|
||||
|
||||
### `/project:tm/validate-dependencies`
|
||||
- Check for dependency issues
|
||||
|
||||
### `/project:tm/fix-dependencies`
|
||||
- Automatically fix dependency problems
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Natural Language
|
||||
Most commands accept natural language arguments:
|
||||
```
|
||||
/project:tm/add-task create user authentication system
|
||||
/project:tm/update mark all API tasks as high priority
|
||||
/project:tm/list show blocked tasks
|
||||
```
|
||||
|
||||
### ID-Based Commands
|
||||
Commands requiring IDs intelligently parse from $ARGUMENTS:
|
||||
```
|
||||
/project:tm/show 45
|
||||
/project:tm/expand 23
|
||||
/project:tm/set-status/to-done 67
|
||||
```
|
||||
|
||||
### Smart Defaults
|
||||
Commands provide intelligent defaults and suggestions based on context.
|
||||
146
.claude/commands/tm/tm-main.md
Normal file
146
.claude/commands/tm/tm-main.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Task Master Command Reference
|
||||
|
||||
Comprehensive command structure for Task Master integration with Claude Code.
|
||||
|
||||
## Command Organization
|
||||
|
||||
Commands are organized hierarchically to match Task Master's CLI structure while providing enhanced Claude Code integration.
|
||||
|
||||
## Project Setup & Configuration
|
||||
|
||||
### `/project:tm/init`
|
||||
- `init-project` - Initialize new project (handles PRD files intelligently)
|
||||
- `init-project-quick` - Quick setup with auto-confirmation (-y flag)
|
||||
|
||||
### `/project:tm/models`
|
||||
- `view-models` - View current AI model configuration
|
||||
- `setup-models` - Interactive model configuration
|
||||
- `set-main` - Set primary generation model
|
||||
- `set-research` - Set research model
|
||||
- `set-fallback` - Set fallback model
|
||||
|
||||
## Task Generation
|
||||
|
||||
### `/project:tm/parse-prd`
|
||||
- `parse-prd` - Generate tasks from PRD document
|
||||
- `parse-prd-with-research` - Enhanced parsing with research mode
|
||||
|
||||
### `/project:tm/generate`
|
||||
- `generate-tasks` - Create individual task files from tasks.json
|
||||
|
||||
## Task Management
|
||||
|
||||
### `/project:tm/list`
|
||||
- `list-tasks` - Smart listing with natural language filters
|
||||
- `list-tasks-with-subtasks` - Include subtasks in hierarchical view
|
||||
- `list-tasks-by-status` - Filter by specific status
|
||||
|
||||
### `/project:tm/set-status`
|
||||
- `to-pending` - Reset task to pending
|
||||
- `to-in-progress` - Start working on task
|
||||
- `to-done` - Mark task complete
|
||||
- `to-review` - Submit for review
|
||||
- `to-deferred` - Defer task
|
||||
- `to-cancelled` - Cancel task
|
||||
|
||||
### `/project:tm/sync-readme`
|
||||
- `sync-readme` - Export tasks to README.md with formatting
|
||||
|
||||
### `/project:tm/update`
|
||||
- `update-task` - Update tasks with natural language
|
||||
- `update-tasks-from-id` - Update multiple tasks from a starting point
|
||||
- `update-single-task` - Update specific task
|
||||
|
||||
### `/project:tm/add-task`
|
||||
- `add-task` - Add new task with AI assistance
|
||||
|
||||
### `/project:tm/remove-task`
|
||||
- `remove-task` - Remove task with confirmation
|
||||
|
||||
## Subtask Management
|
||||
|
||||
### `/project:tm/add-subtask`
|
||||
- `add-subtask` - Add new subtask to parent
|
||||
- `convert-task-to-subtask` - Convert existing task to subtask
|
||||
|
||||
### `/project:tm/remove-subtask`
|
||||
- `remove-subtask` - Remove subtask (with optional conversion)
|
||||
|
||||
### `/project:tm/clear-subtasks`
|
||||
- `clear-subtasks` - Clear subtasks from specific task
|
||||
- `clear-all-subtasks` - Clear all subtasks globally
|
||||
|
||||
## Task Analysis & Breakdown
|
||||
|
||||
### `/project:tm/analyze-complexity`
|
||||
- `analyze-complexity` - Analyze and generate expansion recommendations
|
||||
|
||||
### `/project:tm/complexity-report`
|
||||
- `complexity-report` - Display complexity analysis report
|
||||
|
||||
### `/project:tm/expand`
|
||||
- `expand-task` - Break down specific task
|
||||
- `expand-all-tasks` - Expand all eligible tasks
|
||||
- `with-research` - Enhanced expansion
|
||||
|
||||
## Task Navigation
|
||||
|
||||
### `/project:tm/next`
|
||||
- `next-task` - Intelligent next task recommendation
|
||||
|
||||
### `/project:tm/show`
|
||||
- `show-task` - Display detailed task information
|
||||
|
||||
### `/project:tm/status`
|
||||
- `project-status` - Comprehensive project dashboard
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### `/project:tm/add-dependency`
|
||||
- `add-dependency` - Add task dependency
|
||||
|
||||
### `/project:tm/remove-dependency`
|
||||
- `remove-dependency` - Remove task dependency
|
||||
|
||||
### `/project:tm/validate-dependencies`
|
||||
- `validate-dependencies` - Check for dependency issues
|
||||
|
||||
### `/project:tm/fix-dependencies`
|
||||
- `fix-dependencies` - Automatically fix dependency problems
|
||||
|
||||
## Workflows & Automation
|
||||
|
||||
### `/project:tm/workflows`
|
||||
- `smart-workflow` - Context-aware intelligent workflow execution
|
||||
- `command-pipeline` - Chain multiple commands together
|
||||
- `auto-implement-tasks` - Advanced auto-implementation with code generation
|
||||
|
||||
## Utilities
|
||||
|
||||
### `/project:tm/utils`
|
||||
- `analyze-project` - Deep project analysis and insights
|
||||
|
||||
### `/project:tm/setup`
|
||||
- `install-taskmaster` - Comprehensive installation guide
|
||||
- `quick-install-taskmaster` - One-line global installation
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Natural Language
|
||||
Most commands accept natural language arguments:
|
||||
```
|
||||
/project:tm/add-task create user authentication system
|
||||
/project:tm/update mark all API tasks as high priority
|
||||
/project:tm/list show blocked tasks
|
||||
```
|
||||
|
||||
### ID-Based Commands
|
||||
Commands requiring IDs intelligently parse from $ARGUMENTS:
|
||||
```
|
||||
/project:tm/show 45
|
||||
/project:tm/expand 23
|
||||
/project:tm/set-status/to-done 67
|
||||
```
|
||||
|
||||
### Smart Defaults
|
||||
Commands provide intelligent defaults and suggestions based on context.
|
||||
10
.coderabbit.yaml
Normal file
10
.coderabbit.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
reviews:
|
||||
profile: assertive
|
||||
poem: false
|
||||
auto_review:
|
||||
base_branches:
|
||||
- rc
|
||||
- beta
|
||||
- alpha
|
||||
- production
|
||||
- next
|
||||
@@ -8,6 +8,7 @@ GROQ_API_KEY=YOUR_GROQ_KEY_HERE
|
||||
OPENROUTER_API_KEY=YOUR_OPENROUTER_KEY_HERE
|
||||
XAI_API_KEY=YOUR_XAI_KEY_HERE
|
||||
AZURE_OPENAI_API_KEY=YOUR_AZURE_KEY_HERE
|
||||
OLLAMA_API_KEY=YOUR_OLLAMA_API_KEY_HERE
|
||||
|
||||
# Google Vertex AI Configuration
|
||||
VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
||||
# task-master-ai
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#950](https://github.com/eyaltoledano/claude-task-master/pull/950) [`699e9ee`](https://github.com/eyaltoledano/claude-task-master/commit/699e9eefb5d687b256e9402d686bdd5e3a358b4a) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Add support for xAI Grok 4 model
|
||||
- Add grok-4 model to xAI provider with $3/$15 per 1M token pricing
|
||||
- Enable main, fallback, and research roles for grok-4
|
||||
- Max tokens set to 131,072 (matching other xAI models)
|
||||
|
||||
- [#946](https://github.com/eyaltoledano/claude-task-master/pull/946) [`5f009a5`](https://github.com/eyaltoledano/claude-task-master/commit/5f009a5e1fc10e37be26f5135df4b7f44a9c5320) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add stricter validation and clearer feedback for task priority when adding new tasks
|
||||
- if a task priority is invalid, it will default to medium
|
||||
- made taks priority case-insensitive, essentially making HIGH and high the same value
|
||||
|
||||
- [#863](https://github.com/eyaltoledano/claude-task-master/pull/863) [`b530657`](https://github.com/eyaltoledano/claude-task-master/commit/b53065713c8da0ae6f18eb2655397aa975004923) Thanks [@OrenMe](https://github.com/OrenMe)! - Add support for MCP Sampling as AI provider, requires no API key, uses the client LLM provider
|
||||
|
||||
- [#930](https://github.com/eyaltoledano/claude-task-master/pull/930) [`98d1c97`](https://github.com/eyaltoledano/claude-task-master/commit/98d1c974361a56ddbeb772b1272986b9d3913459) Thanks [@OmarElKadri](https://github.com/OmarElKadri)! - Added Groq provider support
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#958](https://github.com/eyaltoledano/claude-task-master/pull/958) [`6c88a4a`](https://github.com/eyaltoledano/claude-task-master/commit/6c88a4a749083e3bd2d073a9240799771774495a) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Recover from `@anthropic-ai/claude-code` JSON truncation bug that caused Task Master to crash when handling large (>8 kB) structured responses. The CLI/SDK still truncates, but Task Master now detects the error, preserves buffered text, and returns a usable response instead of throwing.
|
||||
|
||||
- [#958](https://github.com/eyaltoledano/claude-task-master/pull/958) [`3334e40`](https://github.com/eyaltoledano/claude-task-master/commit/3334e409ae659d5223bb136ae23fd22c5e219073) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Updating dependency ai-sdk-provider-gemini-cli to 0.0.4 to address breaking change Google made to Gemini CLI and add better 'api-key' in addition to 'gemini-api-key' AI-SDK compatibility.
|
||||
|
||||
- [#853](https://github.com/eyaltoledano/claude-task-master/pull/853) [`95c299d`](https://github.com/eyaltoledano/claude-task-master/commit/95c299df642bd8e6d75f8fa5110ac705bcc72edf) Thanks [@joedanz](https://github.com/joedanz)! - Unify and streamline profile system architecture for improved maintainability
|
||||
|
||||
## 0.20.0-rc.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
15
README.md
15
README.md
@@ -25,11 +25,7 @@ For more detailed information, check out the documentation in the `docs` directo
|
||||
|
||||
#### Quick Install for Cursor 1.0+ (One-Click)
|
||||
|
||||
📋 Click the copy button (top-right of code block) then paste into your browser:
|
||||
|
||||
```text
|
||||
cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQo=
|
||||
```
|
||||
[](https://cursor.com/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D)
|
||||
|
||||
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
||||
|
||||
@@ -73,7 +69,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
@@ -82,6 +78,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
|
||||
"GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE",
|
||||
"MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE",
|
||||
"GROQ_API_KEY": "YOUR_GROQ_KEY_HERE",
|
||||
"OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE",
|
||||
"XAI_API_KEY": "YOUR_XAI_KEY_HERE",
|
||||
"AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE",
|
||||
@@ -101,7 +98,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"taskmaster-ai": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
@@ -110,9 +107,11 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
"OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
|
||||
"GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE",
|
||||
"MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE",
|
||||
"GROQ_API_KEY": "YOUR_GROQ_KEY_HERE",
|
||||
"OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE",
|
||||
"XAI_API_KEY": "YOUR_XAI_KEY_HERE",
|
||||
"AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE"
|
||||
"AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE",
|
||||
"OLLAMA_API_KEY": "YOUR_OLLAMA_API_KEY_HERE"
|
||||
},
|
||||
"type": "stdio"
|
||||
}
|
||||
|
||||
15
apps/extension/package.json
Normal file
15
apps/extension/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "extension",
|
||||
"version": "0.20.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
1
apps/extension/src/index.ts
Normal file
1
apps/extension/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
console.log('hello world');
|
||||
113
apps/extension/tsconfig.json
Normal file
113
apps/extension/tsconfig.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Task Master AI - Claude Code Integration Guide
|
||||
# Task Master AI - Agent Integration Guide
|
||||
|
||||
## Essential Commands
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# API Keys (Required to enable respective provider)
|
||||
ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-...
|
||||
PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-...
|
||||
OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI/OpenRouter models. Format: sk-proj-...
|
||||
OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-...
|
||||
GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models.
|
||||
MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models.
|
||||
XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models.
|
||||
GROQ_API_KEY="YOUR_GROQ_KEY_HERE" # Optional, for Groq models.
|
||||
OPENROUTER_API_KEY="YOUR_OPENROUTER_KEY_HERE" # Optional, for OpenRouter models.
|
||||
AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json).
|
||||
OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication.
|
||||
GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_...
|
||||
@@ -1,4 +1,4 @@
|
||||
# Available Models as of July 10, 2025
|
||||
# Available Models as of July 16, 2025
|
||||
|
||||
## Main Models
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
| xai | grok-3 | — | 3 | 15 |
|
||||
| xai | grok-3-fast | — | 5 | 25 |
|
||||
| xai | grok-4 | — | 3 | 15 |
|
||||
| groq | moonshotai/kimi-k2-instruct | 0.66 | 1 | 3 |
|
||||
| groq | llama-3.3-70b-versatile | 0.55 | 0.59 | 0.79 |
|
||||
| groq | llama-3.1-8b-instant | 0.32 | 0.05 | 0.08 |
|
||||
| groq | llama-4-scout | 0.45 | 0.11 | 0.34 |
|
||||
@@ -144,6 +145,7 @@
|
||||
| xai | grok-3 | — | 3 | 15 |
|
||||
| xai | grok-3-fast | — | 5 | 25 |
|
||||
| xai | grok-4 | — | 3 | 15 |
|
||||
| groq | moonshotai/kimi-k2-instruct | 0.66 | 1 | 3 |
|
||||
| groq | llama-3.3-70b-versatile | 0.55 | 0.59 | 0.79 |
|
||||
| groq | llama-3.1-8b-instant | 0.32 | 0.05 | 0.08 |
|
||||
| groq | llama-4-scout | 0.45 | 0.11 | 0.34 |
|
||||
|
||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.19.0",
|
||||
"version": "0.20.0",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"."
|
||||
],
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^2.2.9",
|
||||
"@ai-sdk/anthropic": "^1.2.10",
|
||||
@@ -80,6 +84,13 @@
|
||||
"ai-sdk-provider-gemini-cli": "^0.0.4"
|
||||
}
|
||||
},
|
||||
"apps/extension": {
|
||||
"version": "0.20.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/amazon-bedrock": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-2.2.10.tgz",
|
||||
@@ -7340,6 +7351,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extension": {
|
||||
"resolved": "apps/extension",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/external-editor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
||||
@@ -12977,6 +12992,10 @@
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/task-master-ai": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/term-size": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
||||
@@ -13185,6 +13204,20 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8array-extras": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.20.0-rc.0",
|
||||
"version": "0.20.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -9,6 +9,7 @@
|
||||
"task-master-mcp": "mcp-server/server.js",
|
||||
"task-master-ai": "mcp-server/server.js"
|
||||
},
|
||||
"workspaces": ["apps/*", "."],
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||
"test:fails": "node --experimental-vm-modules node_modules/.bin/jest --onlyFailures",
|
||||
|
||||
@@ -8,47 +8,48 @@
|
||||
|
||||
// --- Core Dependencies ---
|
||||
import {
|
||||
getMainProvider,
|
||||
getMainModelId,
|
||||
getResearchProvider,
|
||||
getResearchModelId,
|
||||
getFallbackProvider,
|
||||
MODEL_MAP,
|
||||
getAzureBaseURL,
|
||||
getBaseUrlForRole,
|
||||
getBedrockBaseURL,
|
||||
getDebugFlag,
|
||||
getFallbackModelId,
|
||||
getFallbackProvider,
|
||||
getMainModelId,
|
||||
getMainProvider,
|
||||
getOllamaBaseURL,
|
||||
getParametersForRole,
|
||||
getResearchModelId,
|
||||
getResearchProvider,
|
||||
getResponseLanguage,
|
||||
getUserId,
|
||||
MODEL_MAP,
|
||||
getDebugFlag,
|
||||
getBaseUrlForRole,
|
||||
isApiKeySet,
|
||||
getOllamaBaseURL,
|
||||
getAzureBaseURL,
|
||||
getBedrockBaseURL,
|
||||
getVertexProjectId,
|
||||
getVertexLocation,
|
||||
getVertexProjectId,
|
||||
isApiKeySet,
|
||||
providersWithoutApiKeys
|
||||
} from './config-manager.js';
|
||||
import {
|
||||
log,
|
||||
findProjectRoot,
|
||||
resolveEnvVariable,
|
||||
getCurrentTag
|
||||
getCurrentTag,
|
||||
log,
|
||||
resolveEnvVariable
|
||||
} from './utils.js';
|
||||
|
||||
// Import provider classes
|
||||
import {
|
||||
AnthropicAIProvider,
|
||||
PerplexityAIProvider,
|
||||
GoogleAIProvider,
|
||||
OpenAIProvider,
|
||||
XAIProvider,
|
||||
OpenRouterAIProvider,
|
||||
OllamaAIProvider,
|
||||
BedrockAIProvider,
|
||||
AzureProvider,
|
||||
VertexAIProvider,
|
||||
BedrockAIProvider,
|
||||
ClaudeCodeProvider,
|
||||
GeminiCliProvider
|
||||
GeminiCliProvider,
|
||||
GoogleAIProvider,
|
||||
GroqProvider,
|
||||
OllamaAIProvider,
|
||||
OpenAIProvider,
|
||||
OpenRouterAIProvider,
|
||||
PerplexityAIProvider,
|
||||
VertexAIProvider,
|
||||
XAIProvider
|
||||
} from '../../src/ai-providers/index.js';
|
||||
|
||||
// Import the provider registry
|
||||
@@ -61,6 +62,7 @@ const PROVIDERS = {
|
||||
google: new GoogleAIProvider(),
|
||||
openai: new OpenAIProvider(),
|
||||
xai: new XAIProvider(),
|
||||
groq: new GroqProvider(),
|
||||
openrouter: new OpenRouterAIProvider(),
|
||||
ollama: new OllamaAIProvider(),
|
||||
bedrock: new BedrockAIProvider(),
|
||||
|
||||
@@ -2353,10 +2353,14 @@ ${result.result}
|
||||
.option('--tag <tag>', 'Specify tag context for task operations')
|
||||
.action(async (taskId, options) => {
|
||||
// Initialize TaskMaster
|
||||
const taskMaster = initTaskMaster({
|
||||
tasksPath: options.file || true,
|
||||
complexityReportPath: options.report || false
|
||||
});
|
||||
const initOptions = {
|
||||
tasksPath: options.file || true
|
||||
};
|
||||
// Only pass complexityReportPath if user provided a custom path
|
||||
if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
|
||||
initOptions.complexityReportPath = options.report;
|
||||
}
|
||||
const taskMaster = initTaskMaster(initOptions);
|
||||
|
||||
const idArg = taskId || options.id;
|
||||
const statusFilter = options.status;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import chalk from 'chalk';
|
||||
import { z } from 'zod';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { log, findProjectRoot, resolveEnvVariable, isEmpty } from './utils.js';
|
||||
import { AI_COMMAND_NAMES } from '../../src/constants/commands.js';
|
||||
import {
|
||||
LEGACY_CONFIG_FILE,
|
||||
TASKMASTER_DIR
|
||||
} from '../../src/constants/paths.js';
|
||||
import { findConfigPath } from '../../src/utils/path-utils.js';
|
||||
import {
|
||||
VALIDATED_PROVIDERS,
|
||||
ALL_PROVIDERS,
|
||||
CUSTOM_PROVIDERS,
|
||||
CUSTOM_PROVIDERS_ARRAY,
|
||||
ALL_PROVIDERS
|
||||
VALIDATED_PROVIDERS
|
||||
} from '../../src/constants/providers.js';
|
||||
import { AI_COMMAND_NAMES } from '../../src/constants/commands.js';
|
||||
import { findConfigPath } from '../../src/utils/path-utils.js';
|
||||
import { findProjectRoot, isEmpty, log, resolveEnvVariable } from './utils.js';
|
||||
|
||||
// Calculate __dirname in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -641,6 +641,7 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
||||
azure: 'AZURE_OPENAI_API_KEY',
|
||||
openrouter: 'OPENROUTER_API_KEY',
|
||||
xai: 'XAI_API_KEY',
|
||||
groq: 'GROQ_API_KEY',
|
||||
vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
|
||||
'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
|
||||
bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials
|
||||
@@ -726,6 +727,10 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
|
||||
apiKeyToCheck = mcpEnv.XAI_API_KEY;
|
||||
placeholderValue = 'YOUR_XAI_API_KEY_HERE';
|
||||
break;
|
||||
case 'groq':
|
||||
apiKeyToCheck = mcpEnv.GROQ_API_KEY;
|
||||
placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
|
||||
break;
|
||||
case 'ollama':
|
||||
return true; // No key needed
|
||||
case 'claude-code':
|
||||
|
||||
@@ -295,6 +295,16 @@
|
||||
}
|
||||
],
|
||||
"groq": [
|
||||
{
|
||||
"id": "moonshotai/kimi-k2-instruct",
|
||||
"swe_score": 0.66,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 1.0,
|
||||
"output": 3.0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"],
|
||||
"max_tokens": 16384
|
||||
},
|
||||
{
|
||||
"id": "llama-3.3-70b-versatile",
|
||||
"swe_score": 0.55,
|
||||
|
||||
@@ -190,8 +190,45 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
|
||||
throw new Error('Parsed AI response is not a valid JSON object.');
|
||||
}
|
||||
|
||||
// Preprocess the task to ensure subtasks have proper structure
|
||||
const preprocessedTask = {
|
||||
...parsedTask,
|
||||
status: parsedTask.status || 'pending',
|
||||
dependencies: Array.isArray(parsedTask.dependencies)
|
||||
? parsedTask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof parsedTask.details === 'string'
|
||||
? parsedTask.details
|
||||
: String(parsedTask.details || ''),
|
||||
testStrategy:
|
||||
typeof parsedTask.testStrategy === 'string'
|
||||
? parsedTask.testStrategy
|
||||
: String(parsedTask.testStrategy || ''),
|
||||
// Ensure subtasks is an array and each subtask has required fields
|
||||
subtasks: Array.isArray(parsedTask.subtasks)
|
||||
? parsedTask.subtasks.map((subtask) => ({
|
||||
...subtask,
|
||||
title: subtask.title || '',
|
||||
description: subtask.description || '',
|
||||
status: subtask.status || 'pending',
|
||||
dependencies: Array.isArray(subtask.dependencies)
|
||||
? subtask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof subtask.details === 'string'
|
||||
? subtask.details
|
||||
: String(subtask.details || ''),
|
||||
testStrategy:
|
||||
typeof subtask.testStrategy === 'string'
|
||||
? subtask.testStrategy
|
||||
: String(subtask.testStrategy || '')
|
||||
}))
|
||||
: []
|
||||
};
|
||||
|
||||
// Validate the parsed task object using Zod
|
||||
const validationResult = updatedTaskSchema.safeParse(parsedTask);
|
||||
const validationResult = updatedTaskSchema.safeParse(preprocessedTask);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task object failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
|
||||
@@ -196,7 +196,18 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
|
||||
);
|
||||
}
|
||||
|
||||
const validationResult = updatedTaskArraySchema.safeParse(parsedTasks);
|
||||
// Preprocess tasks to ensure required fields have proper defaults
|
||||
const preprocessedTasks = parsedTasks.map((task) => ({
|
||||
...task,
|
||||
// Ensure subtasks is always an array (not null or undefined)
|
||||
subtasks: Array.isArray(task.subtasks) ? task.subtasks : [],
|
||||
// Ensure status has a default value if missing
|
||||
status: task.status || 'pending',
|
||||
// Ensure dependencies is always an array
|
||||
dependencies: Array.isArray(task.dependencies) ? task.dependencies : []
|
||||
}));
|
||||
|
||||
const validationResult = updatedTaskArraySchema.safeParse(preprocessedTasks);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task array failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
@@ -442,7 +453,17 @@ async function updateTasks(
|
||||
data.tasks.forEach((task, index) => {
|
||||
if (updatedTasksMap.has(task.id)) {
|
||||
// Only update if the task was part of the set sent to AI
|
||||
data.tasks[index] = updatedTasksMap.get(task.id);
|
||||
const updatedTask = updatedTasksMap.get(task.id);
|
||||
// Merge the updated task with the existing one to preserve fields like subtasks
|
||||
data.tasks[index] = {
|
||||
...task, // Keep all existing fields
|
||||
...updatedTask, // Override with updated fields
|
||||
// Ensure subtasks field is preserved if not provided by AI
|
||||
subtasks:
|
||||
updatedTask.subtasks !== undefined
|
||||
? updatedTask.subtasks
|
||||
: task.subtasks
|
||||
};
|
||||
actualUpdateCount++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,14 @@ export class GroqProvider extends BaseAIProvider {
|
||||
this.name = 'Groq';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the Groq API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'GROQ_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a Groq client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'roo' | 'trae' | 'windsurf' | 'vscode'} RulesProfile
|
||||
* @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -10,15 +10,18 @@
|
||||
*
|
||||
* @type {RulesProfile[]}
|
||||
* @description Defines possible rule profile sets:
|
||||
* - amp: Amp Code integration
|
||||
* - claude: Claude Code integration
|
||||
* - cline: Cline IDE rules
|
||||
* - codex: Codex integration
|
||||
* - cursor: Cursor IDE rules
|
||||
* - gemini: Gemini integration
|
||||
* - opencode: OpenCode integration
|
||||
* - roo: Roo Code IDE rules
|
||||
* - trae: Trae IDE rules
|
||||
* - vscode: VS Code with GitHub Copilot integration
|
||||
* - windsurf: Windsurf IDE rules
|
||||
* - zed: Zed IDE rules
|
||||
*
|
||||
* To add a new rule profile:
|
||||
* 1. Add the profile name to this array
|
||||
@@ -26,15 +29,18 @@
|
||||
* 3. Export it as {profile}Profile in src/profiles/index.js
|
||||
*/
|
||||
export const RULE_PROFILES = [
|
||||
'amp',
|
||||
'claude',
|
||||
'cline',
|
||||
'codex',
|
||||
'cursor',
|
||||
'gemini',
|
||||
'opencode',
|
||||
'roo',
|
||||
'trae',
|
||||
'vscode',
|
||||
'windsurf'
|
||||
'windsurf',
|
||||
'zed'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
277
src/profiles/amp.js
Normal file
277
src/profiles/amp.js
Normal file
@@ -0,0 +1,277 @@
|
||||
// Amp profile for rule-transformer
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { isSilentMode, log } from '../../scripts/modules/utils.js';
|
||||
import { createProfile } from './base-profile.js';
|
||||
|
||||
/**
|
||||
* Transform standard MCP config format to Amp format
|
||||
* @param {Object} mcpConfig - Standard MCP configuration object
|
||||
* @returns {Object} - Transformed Amp configuration object
|
||||
*/
|
||||
function transformToAmpFormat(mcpConfig) {
|
||||
const ampConfig = {};
|
||||
|
||||
// Transform mcpServers to amp.mcpServers
|
||||
if (mcpConfig.mcpServers) {
|
||||
ampConfig['amp.mcpServers'] = mcpConfig.mcpServers;
|
||||
}
|
||||
|
||||
// Preserve any other existing settings
|
||||
for (const [key, value] of Object.entries(mcpConfig)) {
|
||||
if (key !== 'mcpServers') {
|
||||
ampConfig[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return ampConfig;
|
||||
}
|
||||
|
||||
// Lifecycle functions for Amp profile
|
||||
function onAddRulesProfile(targetDir, assetsDir) {
|
||||
// Handle AGENT.md import for non-destructive integration (Amp uses AGENT.md, copies from AGENTS.md)
|
||||
const sourceFile = path.join(assetsDir, 'AGENTS.md');
|
||||
const userAgentFile = path.join(targetDir, 'AGENT.md');
|
||||
const taskMasterAgentFile = path.join(targetDir, '.taskmaster', 'AGENT.md');
|
||||
const importLine = '@./.taskmaster/AGENT.md';
|
||||
const importSection = `\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**\n${importLine}`;
|
||||
|
||||
if (fs.existsSync(sourceFile)) {
|
||||
try {
|
||||
// Ensure .taskmaster directory exists
|
||||
const taskMasterDir = path.join(targetDir, '.taskmaster');
|
||||
if (!fs.existsSync(taskMasterDir)) {
|
||||
fs.mkdirSync(taskMasterDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy Task Master instructions to .taskmaster/AGENT.md
|
||||
fs.copyFileSync(sourceFile, taskMasterAgentFile);
|
||||
log(
|
||||
'debug',
|
||||
`[Amp] Created Task Master instructions at ${taskMasterAgentFile}`
|
||||
);
|
||||
|
||||
// Handle user's AGENT.md
|
||||
if (fs.existsSync(userAgentFile)) {
|
||||
// Check if import already exists
|
||||
const content = fs.readFileSync(userAgentFile, 'utf8');
|
||||
if (!content.includes(importLine)) {
|
||||
// Append import section at the end
|
||||
const updatedContent = content.trim() + '\n' + importSection + '\n';
|
||||
fs.writeFileSync(userAgentFile, updatedContent);
|
||||
log(
|
||||
'info',
|
||||
`[Amp] Added Task Master import to existing ${userAgentFile}`
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'info',
|
||||
`[Amp] Task Master import already present in ${userAgentFile}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create minimal AGENT.md with the import section
|
||||
const minimalContent = `# Amp Instructions\n${importSection}\n`;
|
||||
fs.writeFileSync(userAgentFile, minimalContent);
|
||||
log('info', `[Amp] Created ${userAgentFile} with Task Master import`);
|
||||
}
|
||||
} catch (err) {
|
||||
log('error', `[Amp] Failed to set up Amp instructions: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// MCP transformation will be handled in onPostConvertRulesProfile
|
||||
}
|
||||
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
// Clean up AGENT.md import (Amp uses AGENT.md, not AGENTS.md)
|
||||
const userAgentFile = path.join(targetDir, 'AGENT.md');
|
||||
const taskMasterAgentFile = path.join(targetDir, '.taskmaster', 'AGENT.md');
|
||||
const importLine = '@./.taskmaster/AGENT.md';
|
||||
|
||||
try {
|
||||
// Remove Task Master AGENT.md from .taskmaster
|
||||
if (fs.existsSync(taskMasterAgentFile)) {
|
||||
fs.rmSync(taskMasterAgentFile, { force: true });
|
||||
log('debug', `[Amp] Removed ${taskMasterAgentFile}`);
|
||||
}
|
||||
|
||||
// Clean up import from user's AGENT.md
|
||||
if (fs.existsSync(userAgentFile)) {
|
||||
const content = fs.readFileSync(userAgentFile, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const filteredLines = [];
|
||||
let skipNextLines = 0;
|
||||
|
||||
// Remove the Task Master section
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (skipNextLines > 0) {
|
||||
skipNextLines--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is the start of our Task Master section
|
||||
if (lines[i].includes('## Task Master AI Instructions')) {
|
||||
// Skip this line and the next two lines (bold text and import)
|
||||
skipNextLines = 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also remove standalone import lines (for backward compatibility)
|
||||
if (lines[i].trim() === importLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filteredLines.push(lines[i]);
|
||||
}
|
||||
|
||||
// Join back and clean up excessive newlines
|
||||
let updatedContent = filteredLines
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
|
||||
// Check if file only contained our minimal template
|
||||
if (updatedContent === '# Amp Instructions' || updatedContent === '') {
|
||||
// File only contained our import, remove it
|
||||
fs.rmSync(userAgentFile, { force: true });
|
||||
log('debug', `[Amp] Removed empty ${userAgentFile}`);
|
||||
} else {
|
||||
// Write back without the import
|
||||
fs.writeFileSync(userAgentFile, updatedContent + '\n');
|
||||
log('debug', `[Amp] Removed Task Master import from ${userAgentFile}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log('error', `[Amp] Failed to remove Amp instructions: ${err.message}`);
|
||||
}
|
||||
|
||||
// MCP Removal: Remove amp.mcpServers section
|
||||
const mcpConfigPath = path.join(targetDir, '.vscode', 'settings.json');
|
||||
|
||||
if (!fs.existsSync(mcpConfigPath)) {
|
||||
log('debug', '[Amp] No .vscode/settings.json found to clean up');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the current config
|
||||
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
|
||||
// Check if it has the amp.mcpServers section and task-master-ai server
|
||||
if (
|
||||
config['amp.mcpServers'] &&
|
||||
config['amp.mcpServers']['task-master-ai']
|
||||
) {
|
||||
// Remove task-master-ai server
|
||||
delete config['amp.mcpServers']['task-master-ai'];
|
||||
|
||||
// Check if there are other MCP servers in amp.mcpServers
|
||||
const remainingServers = Object.keys(config['amp.mcpServers']);
|
||||
|
||||
if (remainingServers.length === 0) {
|
||||
// No other servers, remove entire amp.mcpServers section
|
||||
delete config['amp.mcpServers'];
|
||||
log('debug', '[Amp] Removed empty amp.mcpServers section');
|
||||
}
|
||||
|
||||
// Check if config is now empty
|
||||
const remainingKeys = Object.keys(config);
|
||||
|
||||
if (remainingKeys.length === 0) {
|
||||
// Config is empty, remove entire file
|
||||
fs.rmSync(mcpConfigPath, { force: true });
|
||||
log('info', '[Amp] Removed empty settings.json file');
|
||||
|
||||
// Check if .vscode directory is empty
|
||||
const vscodeDirPath = path.join(targetDir, '.vscode');
|
||||
if (fs.existsSync(vscodeDirPath)) {
|
||||
const remainingContents = fs.readdirSync(vscodeDirPath);
|
||||
if (remainingContents.length === 0) {
|
||||
fs.rmSync(vscodeDirPath, { recursive: true, force: true });
|
||||
log('debug', '[Amp] Removed empty .vscode directory');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Write back the modified config
|
||||
fs.writeFileSync(
|
||||
mcpConfigPath,
|
||||
JSON.stringify(config, null, '\t') + '\n'
|
||||
);
|
||||
log(
|
||||
'info',
|
||||
'[Amp] Removed TaskMaster from settings.json, preserved other configurations'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log('debug', '[Amp] TaskMaster not found in amp.mcpServers');
|
||||
}
|
||||
} catch (error) {
|
||||
log('error', `[Amp] Failed to clean up settings.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onPostConvertRulesProfile(targetDir, assetsDir) {
|
||||
// Handle AGENT.md setup (same as onAddRulesProfile)
|
||||
onAddRulesProfile(targetDir, assetsDir);
|
||||
|
||||
// Transform MCP config to Amp format
|
||||
const mcpConfigPath = path.join(targetDir, '.vscode', 'settings.json');
|
||||
|
||||
if (!fs.existsSync(mcpConfigPath)) {
|
||||
log('debug', '[Amp] No .vscode/settings.json found to transform');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the generated standard MCP config
|
||||
const mcpConfigContent = fs.readFileSync(mcpConfigPath, 'utf8');
|
||||
const mcpConfig = JSON.parse(mcpConfigContent);
|
||||
|
||||
// Check if it's already in Amp format (has amp.mcpServers)
|
||||
if (mcpConfig['amp.mcpServers']) {
|
||||
log(
|
||||
'info',
|
||||
'[Amp] settings.json already in Amp format, skipping transformation'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform to Amp format
|
||||
const ampConfig = transformToAmpFormat(mcpConfig);
|
||||
|
||||
// Write back the transformed config with proper formatting
|
||||
fs.writeFileSync(
|
||||
mcpConfigPath,
|
||||
JSON.stringify(ampConfig, null, '\t') + '\n'
|
||||
);
|
||||
|
||||
log('info', '[Amp] Transformed settings.json to Amp format');
|
||||
log('debug', '[Amp] Renamed mcpServers to amp.mcpServers');
|
||||
} catch (error) {
|
||||
log('error', `[Amp] Failed to transform settings.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export amp profile using the base factory
|
||||
export const ampProfile = createProfile({
|
||||
name: 'amp',
|
||||
displayName: 'Amp',
|
||||
url: 'ampcode.com',
|
||||
docsUrl: 'ampcode.com/manual',
|
||||
profileDir: '.vscode',
|
||||
rulesDir: '.',
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'settings.json',
|
||||
includeDefaultRules: false,
|
||||
fileMap: {
|
||||
'AGENTS.md': '.taskmaster/AGENT.md'
|
||||
},
|
||||
onAdd: onAddRulesProfile,
|
||||
onRemove: onRemoveRulesProfile,
|
||||
onPostConvert: onPostConvertRulesProfile
|
||||
});
|
||||
|
||||
// Export lifecycle functions separately to avoid naming conflicts
|
||||
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };
|
||||
@@ -46,7 +46,9 @@ export function createProfile(editorConfig) {
|
||||
onPostConvert
|
||||
} = editorConfig;
|
||||
|
||||
const mcpConfigPath = mcpConfigName ? `${profileDir}/${mcpConfigName}` : null;
|
||||
const mcpConfigPath = mcpConfigName
|
||||
? path.join(profileDir, mcpConfigName)
|
||||
: null;
|
||||
|
||||
// Standard file mapping with custom overrides
|
||||
// Use taskmaster subdirectory only if profile supports it
|
||||
|
||||
@@ -59,6 +59,63 @@ function onAddRulesProfile(targetDir, assetsDir) {
|
||||
`[Claude] An error occurred during directory copy: ${err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Handle CLAUDE.md import for non-destructive integration
|
||||
const sourceFile = path.join(assetsDir, 'AGENTS.md');
|
||||
const userClaudeFile = path.join(targetDir, 'CLAUDE.md');
|
||||
const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md');
|
||||
const importLine = '@./.taskmaster/CLAUDE.md';
|
||||
const importSection = `\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**\n${importLine}`;
|
||||
|
||||
if (fs.existsSync(sourceFile)) {
|
||||
try {
|
||||
// Ensure .taskmaster directory exists
|
||||
const taskMasterDir = path.join(targetDir, '.taskmaster');
|
||||
if (!fs.existsSync(taskMasterDir)) {
|
||||
fs.mkdirSync(taskMasterDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy Task Master instructions to .taskmaster/CLAUDE.md
|
||||
fs.copyFileSync(sourceFile, taskMasterClaudeFile);
|
||||
log(
|
||||
'debug',
|
||||
`[Claude] Created Task Master instructions at ${taskMasterClaudeFile}`
|
||||
);
|
||||
|
||||
// Handle user's CLAUDE.md
|
||||
if (fs.existsSync(userClaudeFile)) {
|
||||
// Check if import already exists
|
||||
const content = fs.readFileSync(userClaudeFile, 'utf8');
|
||||
if (!content.includes(importLine)) {
|
||||
// Append import section at the end
|
||||
const updatedContent = content.trim() + '\n' + importSection + '\n';
|
||||
fs.writeFileSync(userClaudeFile, updatedContent);
|
||||
log(
|
||||
'info',
|
||||
`[Claude] Added Task Master import to existing ${userClaudeFile}`
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'info',
|
||||
`[Claude] Task Master import already present in ${userClaudeFile}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create minimal CLAUDE.md with the import section
|
||||
const minimalContent = `# Claude Code Instructions\n${importSection}\n`;
|
||||
fs.writeFileSync(userClaudeFile, minimalContent);
|
||||
log(
|
||||
'info',
|
||||
`[Claude] Created ${userClaudeFile} with Task Master import`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
log(
|
||||
'error',
|
||||
`[Claude] Failed to set up Claude instructions: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
@@ -67,11 +124,146 @@ function onRemoveRulesProfile(targetDir) {
|
||||
if (removeDirectoryRecursive(claudeDir)) {
|
||||
log('debug', `[Claude] Removed .claude directory from ${claudeDir}`);
|
||||
}
|
||||
|
||||
// Clean up CLAUDE.md import
|
||||
const userClaudeFile = path.join(targetDir, 'CLAUDE.md');
|
||||
const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md');
|
||||
const importLine = '@./.taskmaster/CLAUDE.md';
|
||||
|
||||
try {
|
||||
// Remove Task Master CLAUDE.md from .taskmaster
|
||||
if (fs.existsSync(taskMasterClaudeFile)) {
|
||||
fs.rmSync(taskMasterClaudeFile, { force: true });
|
||||
log('debug', `[Claude] Removed ${taskMasterClaudeFile}`);
|
||||
}
|
||||
|
||||
// Clean up import from user's CLAUDE.md
|
||||
if (fs.existsSync(userClaudeFile)) {
|
||||
const content = fs.readFileSync(userClaudeFile, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const filteredLines = [];
|
||||
let skipNextLines = 0;
|
||||
|
||||
// Remove the Task Master section
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (skipNextLines > 0) {
|
||||
skipNextLines--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is the start of our Task Master section
|
||||
if (lines[i].includes('## Task Master AI Instructions')) {
|
||||
// Skip this line and the next two lines (bold text and import)
|
||||
skipNextLines = 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also remove standalone import lines (for backward compatibility)
|
||||
if (lines[i].trim() === importLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filteredLines.push(lines[i]);
|
||||
}
|
||||
|
||||
// Join back and clean up excessive newlines
|
||||
let updatedContent = filteredLines
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
|
||||
// Check if file only contained our minimal template
|
||||
if (
|
||||
updatedContent === '# Claude Code Instructions' ||
|
||||
updatedContent === ''
|
||||
) {
|
||||
// File only contained our import, remove it
|
||||
fs.rmSync(userClaudeFile, { force: true });
|
||||
log('debug', `[Claude] Removed empty ${userClaudeFile}`);
|
||||
} else {
|
||||
// Write back without the import
|
||||
fs.writeFileSync(userClaudeFile, updatedContent + '\n');
|
||||
log(
|
||||
'debug',
|
||||
`[Claude] Removed Task Master import from ${userClaudeFile}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log(
|
||||
'error',
|
||||
`[Claude] Failed to remove Claude instructions: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform standard MCP config format to Claude format
|
||||
* @param {Object} mcpConfig - Standard MCP configuration object
|
||||
* @returns {Object} - Transformed Claude configuration object
|
||||
*/
|
||||
function transformToClaudeFormat(mcpConfig) {
|
||||
const claudeConfig = {};
|
||||
|
||||
// Transform mcpServers to servers (keeping the same structure but adding type)
|
||||
if (mcpConfig.mcpServers) {
|
||||
claudeConfig.mcpServers = {};
|
||||
|
||||
for (const [serverName, serverConfig] of Object.entries(
|
||||
mcpConfig.mcpServers
|
||||
)) {
|
||||
// Transform server configuration with type as first key
|
||||
const reorderedServer = {};
|
||||
|
||||
// Add type: "stdio" as the first key
|
||||
reorderedServer.type = 'stdio';
|
||||
|
||||
// Then add the rest of the properties in order
|
||||
if (serverConfig.command) reorderedServer.command = serverConfig.command;
|
||||
if (serverConfig.args) reorderedServer.args = serverConfig.args;
|
||||
if (serverConfig.env) reorderedServer.env = serverConfig.env;
|
||||
|
||||
// Add any other properties that might exist
|
||||
Object.keys(serverConfig).forEach((key) => {
|
||||
if (!['command', 'args', 'env', 'type'].includes(key)) {
|
||||
reorderedServer[key] = serverConfig[key];
|
||||
}
|
||||
});
|
||||
|
||||
claudeConfig.mcpServers[serverName] = reorderedServer;
|
||||
}
|
||||
}
|
||||
|
||||
return claudeConfig;
|
||||
}
|
||||
|
||||
function onPostConvertRulesProfile(targetDir, assetsDir) {
|
||||
// For Claude, post-convert is the same as add since we don't transform rules
|
||||
onAddRulesProfile(targetDir, assetsDir);
|
||||
|
||||
// Transform MCP configuration to Claude format
|
||||
const mcpConfigPath = path.join(targetDir, '.mcp.json');
|
||||
if (fs.existsSync(mcpConfigPath)) {
|
||||
try {
|
||||
const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
|
||||
const claudeConfig = transformToClaudeFormat(mcpConfig);
|
||||
|
||||
// Write back the transformed configuration
|
||||
fs.writeFileSync(
|
||||
mcpConfigPath,
|
||||
JSON.stringify(claudeConfig, null, '\t') + '\n'
|
||||
);
|
||||
log(
|
||||
'debug',
|
||||
`[Claude] Transformed MCP configuration to Claude format at ${mcpConfigPath}`
|
||||
);
|
||||
} catch (err) {
|
||||
log(
|
||||
'error',
|
||||
`[Claude] Failed to transform MCP configuration: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export claude profile using the base factory
|
||||
@@ -82,11 +274,10 @@ export const claudeProfile = createProfile({
|
||||
docsUrl: 'docs.anthropic.com/en/docs/claude-code',
|
||||
profileDir: '.', // Root directory
|
||||
rulesDir: '.', // No specific rules directory needed
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
mcpConfigName: '.mcp.json', // Place MCP config in project root
|
||||
includeDefaultRules: false,
|
||||
fileMap: {
|
||||
'AGENTS.md': 'CLAUDE.md'
|
||||
'AGENTS.md': '.taskmaster/CLAUDE.md'
|
||||
},
|
||||
onAdd: onAddRulesProfile,
|
||||
onRemove: onRemoveRulesProfile,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// Profile exports for centralized importing
|
||||
export { ampProfile } from './amp.js';
|
||||
export { claudeProfile } from './claude.js';
|
||||
export { clineProfile } from './cline.js';
|
||||
export { codexProfile } from './codex.js';
|
||||
export { cursorProfile } from './cursor.js';
|
||||
export { geminiProfile } from './gemini.js';
|
||||
export { opencodeProfile } from './opencode.js';
|
||||
export { rooProfile } from './roo.js';
|
||||
export { traeProfile } from './trae.js';
|
||||
export { vscodeProfile } from './vscode.js';
|
||||
export { windsurfProfile } from './windsurf.js';
|
||||
export { zedProfile } from './zed.js';
|
||||
|
||||
183
src/profiles/opencode.js
Normal file
183
src/profiles/opencode.js
Normal file
@@ -0,0 +1,183 @@
|
||||
// Opencode profile for rule-transformer
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
import { createProfile } from './base-profile.js';
|
||||
|
||||
/**
|
||||
* Transform standard MCP config format to OpenCode format
|
||||
* @param {Object} mcpConfig - Standard MCP configuration object
|
||||
* @returns {Object} - Transformed OpenCode configuration object
|
||||
*/
|
||||
function transformToOpenCodeFormat(mcpConfig) {
|
||||
const openCodeConfig = {
|
||||
$schema: 'https://opencode.ai/config.json'
|
||||
};
|
||||
|
||||
// Transform mcpServers to mcp
|
||||
if (mcpConfig.mcpServers) {
|
||||
openCodeConfig.mcp = {};
|
||||
|
||||
for (const [serverName, serverConfig] of Object.entries(
|
||||
mcpConfig.mcpServers
|
||||
)) {
|
||||
// Transform server configuration
|
||||
const transformedServer = {
|
||||
type: 'local'
|
||||
};
|
||||
|
||||
// Combine command and args into single command array
|
||||
if (serverConfig.command && serverConfig.args) {
|
||||
transformedServer.command = [
|
||||
serverConfig.command,
|
||||
...serverConfig.args
|
||||
];
|
||||
} else if (serverConfig.command) {
|
||||
transformedServer.command = [serverConfig.command];
|
||||
}
|
||||
|
||||
// Add enabled flag
|
||||
transformedServer.enabled = true;
|
||||
|
||||
// Transform env to environment
|
||||
if (serverConfig.env) {
|
||||
transformedServer.environment = serverConfig.env;
|
||||
}
|
||||
|
||||
// update with transformed config
|
||||
openCodeConfig.mcp[serverName] = transformedServer;
|
||||
}
|
||||
}
|
||||
|
||||
return openCodeConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle function called after MCP config generation to transform to OpenCode format
|
||||
* @param {string} targetDir - Target project directory
|
||||
* @param {string} assetsDir - Assets directory (unused for OpenCode)
|
||||
*/
|
||||
function onPostConvertRulesProfile(targetDir, assetsDir) {
|
||||
const openCodeConfigPath = path.join(targetDir, 'opencode.json');
|
||||
|
||||
if (!fs.existsSync(openCodeConfigPath)) {
|
||||
log('debug', '[OpenCode] No opencode.json found to transform');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the generated standard MCP config
|
||||
const mcpConfigContent = fs.readFileSync(openCodeConfigPath, 'utf8');
|
||||
const mcpConfig = JSON.parse(mcpConfigContent);
|
||||
|
||||
// Check if it's already in OpenCode format (has $schema)
|
||||
if (mcpConfig.$schema) {
|
||||
log(
|
||||
'info',
|
||||
'[OpenCode] opencode.json already in OpenCode format, skipping transformation'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform to OpenCode format
|
||||
const openCodeConfig = transformToOpenCodeFormat(mcpConfig);
|
||||
|
||||
// Write back the transformed config with proper formatting
|
||||
fs.writeFileSync(
|
||||
openCodeConfigPath,
|
||||
JSON.stringify(openCodeConfig, null, 2) + '\n'
|
||||
);
|
||||
|
||||
log('info', '[OpenCode] Transformed opencode.json to OpenCode format');
|
||||
log(
|
||||
'debug',
|
||||
`[OpenCode] Added schema, renamed mcpServers->mcp, combined command+args, added type/enabled, renamed env->environment`
|
||||
);
|
||||
} catch (error) {
|
||||
log(
|
||||
'error',
|
||||
`[OpenCode] Failed to transform opencode.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle function called when removing OpenCode profile
|
||||
* @param {string} targetDir - Target project directory
|
||||
*/
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
const openCodeConfigPath = path.join(targetDir, 'opencode.json');
|
||||
|
||||
if (!fs.existsSync(openCodeConfigPath)) {
|
||||
log('debug', '[OpenCode] No opencode.json found to clean up');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the current config
|
||||
const configContent = fs.readFileSync(openCodeConfigPath, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
|
||||
// Check if it has the mcp section and taskmaster-ai server
|
||||
if (config.mcp && config.mcp['taskmaster-ai']) {
|
||||
// Remove taskmaster-ai server
|
||||
delete config.mcp['taskmaster-ai'];
|
||||
|
||||
// Check if there are other MCP servers
|
||||
const remainingServers = Object.keys(config.mcp);
|
||||
|
||||
if (remainingServers.length === 0) {
|
||||
// No other servers, remove entire mcp section
|
||||
delete config.mcp;
|
||||
}
|
||||
|
||||
// Check if config is now empty (only has $schema)
|
||||
const remainingKeys = Object.keys(config).filter(
|
||||
(key) => key !== '$schema'
|
||||
);
|
||||
|
||||
if (remainingKeys.length === 0) {
|
||||
// Config only has schema left, remove entire file
|
||||
fs.rmSync(openCodeConfigPath, { force: true });
|
||||
log('info', '[OpenCode] Removed empty opencode.json file');
|
||||
} else {
|
||||
// Write back the modified config
|
||||
fs.writeFileSync(
|
||||
openCodeConfigPath,
|
||||
JSON.stringify(config, null, 2) + '\n'
|
||||
);
|
||||
log(
|
||||
'info',
|
||||
'[OpenCode] Removed TaskMaster from opencode.json, preserved other configurations'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log('debug', '[OpenCode] TaskMaster not found in opencode.json');
|
||||
}
|
||||
} catch (error) {
|
||||
log(
|
||||
'error',
|
||||
`[OpenCode] Failed to clean up opencode.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export opencode profile using the base factory
|
||||
export const opencodeProfile = createProfile({
|
||||
name: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
url: 'opencode.ai',
|
||||
docsUrl: 'opencode.ai/docs/',
|
||||
profileDir: '.', // Root directory
|
||||
rulesDir: '.', // Root directory for AGENTS.md
|
||||
mcpConfigName: 'opencode.json', // Override default 'mcp.json'
|
||||
includeDefaultRules: false,
|
||||
fileMap: {
|
||||
'AGENTS.md': 'AGENTS.md'
|
||||
},
|
||||
onPostConvert: onPostConvertRulesProfile,
|
||||
onRemove: onRemoveRulesProfile
|
||||
});
|
||||
|
||||
// Export lifecycle functions separately to avoid naming conflicts
|
||||
export { onPostConvertRulesProfile, onRemoveRulesProfile };
|
||||
178
src/profiles/zed.js
Normal file
178
src/profiles/zed.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// Zed profile for rule-transformer
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { isSilentMode, log } from '../../scripts/modules/utils.js';
|
||||
import { createProfile } from './base-profile.js';
|
||||
|
||||
/**
|
||||
* Transform standard MCP config format to Zed format
|
||||
* @param {Object} mcpConfig - Standard MCP configuration object
|
||||
* @returns {Object} - Transformed Zed configuration object
|
||||
*/
|
||||
function transformToZedFormat(mcpConfig) {
|
||||
const zedConfig = {};
|
||||
|
||||
// Transform mcpServers to context_servers
|
||||
if (mcpConfig.mcpServers) {
|
||||
zedConfig['context_servers'] = mcpConfig.mcpServers;
|
||||
}
|
||||
|
||||
// Preserve any other existing settings
|
||||
for (const [key, value] of Object.entries(mcpConfig)) {
|
||||
if (key !== 'mcpServers') {
|
||||
zedConfig[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return zedConfig;
|
||||
}
|
||||
|
||||
// Lifecycle functions for Zed profile
|
||||
function onAddRulesProfile(targetDir, assetsDir) {
|
||||
// MCP transformation will be handled in onPostConvertRulesProfile
|
||||
// File copying is handled by the base profile via fileMap
|
||||
}
|
||||
|
||||
function onRemoveRulesProfile(targetDir) {
|
||||
// Clean up .rules (Zed uses .rules directly in root)
|
||||
const userRulesFile = path.join(targetDir, '.rules');
|
||||
|
||||
try {
|
||||
// Remove Task Master .rules
|
||||
if (fs.existsSync(userRulesFile)) {
|
||||
fs.rmSync(userRulesFile, { force: true });
|
||||
log('debug', `[Zed] Removed ${userRulesFile}`);
|
||||
}
|
||||
} catch (err) {
|
||||
log('error', `[Zed] Failed to remove Zed instructions: ${err.message}`);
|
||||
}
|
||||
|
||||
// MCP Removal: Remove context_servers section
|
||||
const mcpConfigPath = path.join(targetDir, '.zed', 'settings.json');
|
||||
|
||||
if (!fs.existsSync(mcpConfigPath)) {
|
||||
log('debug', '[Zed] No .zed/settings.json found to clean up');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the current config
|
||||
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
|
||||
// Check if it has the context_servers section and task-master-ai server
|
||||
if (
|
||||
config['context_servers'] &&
|
||||
config['context_servers']['task-master-ai']
|
||||
) {
|
||||
// Remove task-master-ai server
|
||||
delete config['context_servers']['task-master-ai'];
|
||||
|
||||
// Check if there are other MCP servers in context_servers
|
||||
const remainingServers = Object.keys(config['context_servers']);
|
||||
|
||||
if (remainingServers.length === 0) {
|
||||
// No other servers, remove entire context_servers section
|
||||
delete config['context_servers'];
|
||||
log('debug', '[Zed] Removed empty context_servers section');
|
||||
}
|
||||
|
||||
// Check if config is now empty
|
||||
const remainingKeys = Object.keys(config);
|
||||
|
||||
if (remainingKeys.length === 0) {
|
||||
// Config is empty, remove entire file
|
||||
fs.rmSync(mcpConfigPath, { force: true });
|
||||
log('info', '[Zed] Removed empty settings.json file');
|
||||
|
||||
// Check if .zed directory is empty
|
||||
const zedDirPath = path.join(targetDir, '.zed');
|
||||
if (fs.existsSync(zedDirPath)) {
|
||||
const remainingContents = fs.readdirSync(zedDirPath);
|
||||
if (remainingContents.length === 0) {
|
||||
fs.rmSync(zedDirPath, { recursive: true, force: true });
|
||||
log('debug', '[Zed] Removed empty .zed directory');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Write back the modified config
|
||||
fs.writeFileSync(
|
||||
mcpConfigPath,
|
||||
JSON.stringify(config, null, '\t') + '\n'
|
||||
);
|
||||
log(
|
||||
'info',
|
||||
'[Zed] Removed TaskMaster from settings.json, preserved other configurations'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log('debug', '[Zed] TaskMaster not found in context_servers');
|
||||
}
|
||||
} catch (error) {
|
||||
log('error', `[Zed] Failed to clean up settings.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onPostConvertRulesProfile(targetDir, assetsDir) {
|
||||
// Handle .rules setup (same as onAddRulesProfile)
|
||||
onAddRulesProfile(targetDir, assetsDir);
|
||||
|
||||
// Transform MCP config to Zed format
|
||||
const mcpConfigPath = path.join(targetDir, '.zed', 'settings.json');
|
||||
|
||||
if (!fs.existsSync(mcpConfigPath)) {
|
||||
log('debug', '[Zed] No .zed/settings.json found to transform');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the generated standard MCP config
|
||||
const mcpConfigContent = fs.readFileSync(mcpConfigPath, 'utf8');
|
||||
const mcpConfig = JSON.parse(mcpConfigContent);
|
||||
|
||||
// Check if it's already in Zed format (has context_servers)
|
||||
if (mcpConfig['context_servers']) {
|
||||
log(
|
||||
'info',
|
||||
'[Zed] settings.json already in Zed format, skipping transformation'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform to Zed format
|
||||
const zedConfig = transformToZedFormat(mcpConfig);
|
||||
|
||||
// Write back the transformed config with proper formatting
|
||||
fs.writeFileSync(
|
||||
mcpConfigPath,
|
||||
JSON.stringify(zedConfig, null, '\t') + '\n'
|
||||
);
|
||||
|
||||
log('info', '[Zed] Transformed settings.json to Zed format');
|
||||
log('debug', '[Zed] Renamed mcpServers to context_servers');
|
||||
} catch (error) {
|
||||
log('error', `[Zed] Failed to transform settings.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export zed profile using the base factory
|
||||
export const zedProfile = createProfile({
|
||||
name: 'zed',
|
||||
displayName: 'Zed',
|
||||
url: 'zed.dev',
|
||||
docsUrl: 'zed.dev/docs',
|
||||
profileDir: '.zed',
|
||||
rulesDir: '.',
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'settings.json',
|
||||
includeDefaultRules: false,
|
||||
fileMap: {
|
||||
'AGENTS.md': '.rules'
|
||||
},
|
||||
onAdd: onAddRulesProfile,
|
||||
onRemove: onRemoveRulesProfile,
|
||||
onPostConvert: onPostConvertRulesProfile
|
||||
});
|
||||
|
||||
// Export lifecycle functions separately to avoid naming conflicts
|
||||
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };
|
||||
@@ -113,12 +113,12 @@ export async function runInteractiveProfilesSetup() {
|
||||
const hasMcpConfig = profile.mcpConfig === true;
|
||||
|
||||
if (!profile.includeDefaultRules) {
|
||||
// Integration guide profiles (claude, codex, gemini) - don't include standard coding rules
|
||||
// Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules
|
||||
if (profileName === 'claude') {
|
||||
description = 'Integration guide with Task Master slash commands';
|
||||
} else if (profileName === 'codex') {
|
||||
description = 'Comprehensive Task Master integration guide';
|
||||
} else if (profileName === 'gemini') {
|
||||
} else if (hasMcpConfig) {
|
||||
description = 'Integration guide and MCP config';
|
||||
} else {
|
||||
description = 'Integration guide';
|
||||
@@ -199,7 +199,7 @@ export function generateProfileSummary(profileName, addResult) {
|
||||
const profileConfig = getRulesProfile(profileName);
|
||||
|
||||
if (!profileConfig.includeDefaultRules) {
|
||||
// Integration guide profiles (claude, codex, gemini)
|
||||
// Integration guide profiles (claude, codex, gemini, amp)
|
||||
return `Summary for ${profileName}: Integration guide installed.`;
|
||||
} else {
|
||||
// Rule profiles with coding guidelines
|
||||
@@ -225,7 +225,7 @@ export function generateProfileRemovalSummary(profileName, removeResult) {
|
||||
const profileConfig = getRulesProfile(profileName);
|
||||
|
||||
if (!profileConfig.includeDefaultRules) {
|
||||
// Integration guide profiles (claude, codex, gemini)
|
||||
// Integration guide profiles (claude, codex, gemini, amp)
|
||||
const baseMessage = `Summary for ${profileName}: Integration guide removed`;
|
||||
if (removeResult.notice) {
|
||||
return `${baseMessage} (${removeResult.notice})`;
|
||||
|
||||
346
tests/integration/profiles/amp-init-functionality.test.js
Normal file
346
tests/integration/profiles/amp-init-functionality.test.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
|
||||
import { convertAllRulesToProfileRules } from '../../../src/utils/rule-transformer.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
describe('Amp Profile Init Functionality', () => {
|
||||
let tempDir;
|
||||
let ampProfile;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(__dirname, 'temp-amp-'));
|
||||
|
||||
// Get the Amp profile
|
||||
ampProfile = getRulesProfile('amp');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directory
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Profile Configuration', () => {
|
||||
test('should have correct profile metadata', () => {
|
||||
expect(ampProfile).toBeDefined();
|
||||
expect(ampProfile.profileName).toBe('amp');
|
||||
expect(ampProfile.displayName).toBe('Amp');
|
||||
expect(ampProfile.profileDir).toBe('.vscode');
|
||||
expect(ampProfile.rulesDir).toBe('.');
|
||||
expect(ampProfile.mcpConfig).toBe(true);
|
||||
expect(ampProfile.mcpConfigName).toBe('settings.json');
|
||||
expect(ampProfile.mcpConfigPath).toBe('.vscode/settings.json');
|
||||
expect(ampProfile.includeDefaultRules).toBe(false);
|
||||
});
|
||||
|
||||
test('should have correct file mapping', () => {
|
||||
expect(ampProfile.fileMap).toBeDefined();
|
||||
expect(ampProfile.fileMap['AGENTS.md']).toBe('.taskmaster/AGENT.md');
|
||||
});
|
||||
|
||||
test('should have lifecycle functions', () => {
|
||||
expect(typeof ampProfile.onAddRulesProfile).toBe('function');
|
||||
expect(typeof ampProfile.onRemoveRulesProfile).toBe('function');
|
||||
expect(typeof ampProfile.onPostConvertRulesProfile).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AGENT.md Handling', () => {
|
||||
test('should create AGENT.md with import when none exists', () => {
|
||||
// Create mock AGENTS.md source
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onAddRulesProfile
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
|
||||
// Check that AGENT.md was created with import
|
||||
const agentFile = path.join(tempDir, 'AGENT.md');
|
||||
expect(fs.existsSync(agentFile)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(agentFile, 'utf8');
|
||||
expect(content).toContain('# Amp Instructions');
|
||||
expect(content).toContain('## Task Master AI Instructions');
|
||||
expect(content).toContain('@./.taskmaster/AGENT.md');
|
||||
|
||||
// Check that .taskmaster/AGENT.md was created
|
||||
const taskMasterAgent = path.join(tempDir, '.taskmaster', 'AGENT.md');
|
||||
expect(fs.existsSync(taskMasterAgent)).toBe(true);
|
||||
});
|
||||
|
||||
test('should append import to existing AGENT.md', () => {
|
||||
// Create existing AGENT.md
|
||||
const existingContent =
|
||||
'# My Existing Amp Instructions\n\nSome content here.';
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENT.md'), existingContent);
|
||||
|
||||
// Create mock AGENTS.md source
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onAddRulesProfile
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
|
||||
// Check that import was appended
|
||||
const agentFile = path.join(tempDir, 'AGENT.md');
|
||||
const content = fs.readFileSync(agentFile, 'utf8');
|
||||
expect(content).toContain('# My Existing Amp Instructions');
|
||||
expect(content).toContain('Some content here.');
|
||||
expect(content).toContain('## Task Master AI Instructions');
|
||||
expect(content).toContain('@./.taskmaster/AGENT.md');
|
||||
});
|
||||
|
||||
test('should not duplicate import if already exists', () => {
|
||||
// Create AGENT.md with existing import
|
||||
const existingContent =
|
||||
"# My Amp Instructions\n\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**\n@./.taskmaster/AGENT.md";
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENT.md'), existingContent);
|
||||
|
||||
// Create mock AGENTS.md source
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onAddRulesProfile
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
|
||||
// Check that import was not duplicated
|
||||
const agentFile = path.join(tempDir, 'AGENT.md');
|
||||
const content = fs.readFileSync(agentFile, 'utf8');
|
||||
const importCount = (content.match(/@\.\/.taskmaster\/AGENT\.md/g) || [])
|
||||
.length;
|
||||
expect(importCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration', () => {
|
||||
test('should rename mcpServers to amp.mcpServers', () => {
|
||||
// Create .vscode directory and settings.json with mcpServers
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
|
||||
const initialConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
JSON.stringify(initialConfig, null, '\t')
|
||||
);
|
||||
|
||||
// Call onPostConvertRulesProfile (which should transform mcpServers to amp.mcpServers)
|
||||
ampProfile.onPostConvertRulesProfile(
|
||||
tempDir,
|
||||
path.join(tempDir, 'assets')
|
||||
);
|
||||
|
||||
// Check that mcpServers was renamed to amp.mcpServers
|
||||
const settingsFile = path.join(vscodeDirPath, 'settings.json');
|
||||
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
expect(config.mcpServers).toBeUndefined();
|
||||
expect(config['amp.mcpServers']).toBeDefined();
|
||||
expect(config['amp.mcpServers']['task-master-ai']).toBeDefined();
|
||||
});
|
||||
|
||||
test('should not rename if amp.mcpServers already exists', () => {
|
||||
// Create .vscode directory and settings.json with both mcpServers and amp.mcpServers
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
|
||||
const initialConfig = {
|
||||
mcpServers: {
|
||||
'some-other-server': {
|
||||
command: 'other-command'
|
||||
}
|
||||
},
|
||||
'amp.mcpServers': {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
JSON.stringify(initialConfig, null, '\t')
|
||||
);
|
||||
|
||||
// Call onAddRulesProfile
|
||||
ampProfile.onAddRulesProfile(tempDir, path.join(tempDir, 'assets'));
|
||||
|
||||
// Check that both sections remain unchanged
|
||||
const settingsFile = path.join(vscodeDirPath, 'settings.json');
|
||||
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
expect(config.mcpServers).toBeDefined();
|
||||
expect(config.mcpServers['some-other-server']).toBeDefined();
|
||||
expect(config['amp.mcpServers']).toBeDefined();
|
||||
expect(config['amp.mcpServers']['task-master-ai']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Removal Functionality', () => {
|
||||
test('should remove AGENT.md import and clean up files', () => {
|
||||
// Setup: Create AGENT.md with import and .taskmaster/AGENT.md
|
||||
const agentContent =
|
||||
"# My Amp Instructions\n\nSome content.\n\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**\n@./.taskmaster/AGENT.md\n";
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENT.md'), agentContent);
|
||||
|
||||
fs.mkdirSync(path.join(tempDir, '.taskmaster'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.taskmaster', 'AGENT.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onRemoveRulesProfile
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
|
||||
// Check that .taskmaster/AGENT.md was removed
|
||||
expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
// Check that import was removed from AGENT.md
|
||||
const remainingContent = fs.readFileSync(
|
||||
path.join(tempDir, 'AGENT.md'),
|
||||
'utf8'
|
||||
);
|
||||
expect(remainingContent).not.toContain('## Task Master AI Instructions');
|
||||
expect(remainingContent).not.toContain('@./.taskmaster/AGENT.md');
|
||||
expect(remainingContent).toContain('# My Amp Instructions');
|
||||
expect(remainingContent).toContain('Some content.');
|
||||
});
|
||||
|
||||
test('should remove empty AGENT.md if only contained import', () => {
|
||||
// Setup: Create AGENT.md with only import
|
||||
const agentContent =
|
||||
"# Amp Instructions\n\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**\n@./.taskmaster/AGENT.md";
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENT.md'), agentContent);
|
||||
|
||||
fs.mkdirSync(path.join(tempDir, '.taskmaster'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.taskmaster', 'AGENT.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onRemoveRulesProfile
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
|
||||
// Check that AGENT.md was removed
|
||||
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should remove amp.mcpServers section from settings.json', () => {
|
||||
// Setup: Create .vscode/settings.json with amp.mcpServers and other settings
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
|
||||
const initialConfig = {
|
||||
'amp.mcpServers': {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
}
|
||||
},
|
||||
'other.setting': 'value'
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
JSON.stringify(initialConfig, null, '\t')
|
||||
);
|
||||
|
||||
// Call onRemoveRulesProfile
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
|
||||
// Check that amp.mcpServers was removed but other settings remain
|
||||
const settingsFile = path.join(vscodeDirPath, 'settings.json');
|
||||
expect(fs.existsSync(settingsFile)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
expect(config['amp.mcpServers']).toBeUndefined();
|
||||
expect(config['other.setting']).toBe('value');
|
||||
});
|
||||
|
||||
test('should remove settings.json and .vscode directory if empty after removal', () => {
|
||||
// Setup: Create .vscode/settings.json with only amp.mcpServers
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
|
||||
const initialConfig = {
|
||||
'amp.mcpServers': {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
JSON.stringify(initialConfig, null, '\t')
|
||||
);
|
||||
|
||||
// Call onRemoveRulesProfile
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
|
||||
// Check that settings.json and .vscode directory were removed
|
||||
expect(fs.existsSync(path.join(vscodeDirPath, 'settings.json'))).toBe(
|
||||
false
|
||||
);
|
||||
expect(fs.existsSync(vscodeDirPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full Integration', () => {
|
||||
test('should work with convertAllRulesToProfileRules', () => {
|
||||
// This test ensures the profile works with the full rule transformer
|
||||
const result = convertAllRulesToProfileRules(tempDir, ampProfile);
|
||||
|
||||
expect(result.success).toBeGreaterThan(0);
|
||||
expect(result.failed).toBe(0);
|
||||
|
||||
// Check that .taskmaster/AGENT.md was created
|
||||
expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Check that AGENT.md was created with import
|
||||
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
|
||||
const agentContent = fs.readFileSync(
|
||||
path.join(tempDir, 'AGENT.md'),
|
||||
'utf8'
|
||||
);
|
||||
expect(agentContent).toContain('@./.taskmaster/AGENT.md');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,19 +21,22 @@ describe('Claude Profile Initialization Functionality', () => {
|
||||
expect(claudeProfileContent).toContain("displayName: 'Claude Code'");
|
||||
expect(claudeProfileContent).toContain("profileDir: '.'"); // non-default
|
||||
expect(claudeProfileContent).toContain("rulesDir: '.'"); // non-default
|
||||
expect(claudeProfileContent).toContain('mcpConfig: false'); // non-default
|
||||
expect(claudeProfileContent).toContain("mcpConfigName: '.mcp.json'"); // non-default
|
||||
expect(claudeProfileContent).toContain('includeDefaultRules: false'); // non-default
|
||||
expect(claudeProfileContent).toContain("'AGENTS.md': 'CLAUDE.md'");
|
||||
expect(claudeProfileContent).toContain(
|
||||
"'AGENTS.md': '.taskmaster/CLAUDE.md'"
|
||||
);
|
||||
|
||||
// Check the final computed properties on the profile object
|
||||
expect(claudeProfile.profileName).toBe('claude');
|
||||
expect(claudeProfile.displayName).toBe('Claude Code');
|
||||
expect(claudeProfile.profileDir).toBe('.');
|
||||
expect(claudeProfile.rulesDir).toBe('.');
|
||||
expect(claudeProfile.mcpConfig).toBe(false);
|
||||
expect(claudeProfile.mcpConfigName).toBe(null); // computed
|
||||
expect(claudeProfile.mcpConfig).toBe(true); // default from base profile
|
||||
expect(claudeProfile.mcpConfigName).toBe('.mcp.json'); // explicitly set
|
||||
expect(claudeProfile.mcpConfigPath).toBe('.mcp.json'); // computed
|
||||
expect(claudeProfile.includeDefaultRules).toBe(false);
|
||||
expect(claudeProfile.fileMap['AGENTS.md']).toBe('CLAUDE.md');
|
||||
expect(claudeProfile.fileMap['AGENTS.md']).toBe('.taskmaster/CLAUDE.md');
|
||||
});
|
||||
|
||||
test('claude.js has lifecycle functions for file management', () => {
|
||||
@@ -44,9 +47,11 @@ describe('Claude Profile Initialization Functionality', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('claude.js handles .claude directory in lifecycle functions', () => {
|
||||
test('claude.js handles .claude directory and .taskmaster/CLAUDE.md import in lifecycle functions', () => {
|
||||
expect(claudeProfileContent).toContain('.claude');
|
||||
expect(claudeProfileContent).toContain('copyRecursiveSync');
|
||||
expect(claudeProfileContent).toContain('.taskmaster/CLAUDE.md');
|
||||
expect(claudeProfileContent).toContain('@./.taskmaster/CLAUDE.md');
|
||||
});
|
||||
|
||||
test('claude.js has proper error handling in lifecycle functions', () => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { opencodeProfile } from '../../../src/profiles/opencode.js';
|
||||
|
||||
describe('OpenCode Profile Initialization Functionality', () => {
|
||||
let opencodeProfileContent;
|
||||
|
||||
beforeAll(() => {
|
||||
const opencodeJsPath = path.join(
|
||||
process.cwd(),
|
||||
'src',
|
||||
'profiles',
|
||||
'opencode.js'
|
||||
);
|
||||
opencodeProfileContent = fs.readFileSync(opencodeJsPath, 'utf8');
|
||||
});
|
||||
|
||||
test('opencode.js has correct asset-only profile configuration', () => {
|
||||
// Check for explicit, non-default values in the source file
|
||||
expect(opencodeProfileContent).toContain("name: 'opencode'");
|
||||
expect(opencodeProfileContent).toContain("displayName: 'OpenCode'");
|
||||
expect(opencodeProfileContent).toContain("url: 'opencode.ai'");
|
||||
expect(opencodeProfileContent).toContain("docsUrl: 'opencode.ai/docs/'");
|
||||
expect(opencodeProfileContent).toContain("profileDir: '.'"); // non-default
|
||||
expect(opencodeProfileContent).toContain("rulesDir: '.'"); // non-default
|
||||
expect(opencodeProfileContent).toContain("mcpConfigName: 'opencode.json'"); // non-default
|
||||
expect(opencodeProfileContent).toContain('includeDefaultRules: false'); // non-default
|
||||
expect(opencodeProfileContent).toContain("'AGENTS.md': 'AGENTS.md'");
|
||||
|
||||
// Check the final computed properties on the profile object
|
||||
expect(opencodeProfile.profileName).toBe('opencode');
|
||||
expect(opencodeProfile.displayName).toBe('OpenCode');
|
||||
expect(opencodeProfile.profileDir).toBe('.');
|
||||
expect(opencodeProfile.rulesDir).toBe('.');
|
||||
expect(opencodeProfile.mcpConfig).toBe(true); // computed from mcpConfigName
|
||||
expect(opencodeProfile.mcpConfigName).toBe('opencode.json');
|
||||
expect(opencodeProfile.mcpConfigPath).toBe('opencode.json'); // computed
|
||||
expect(opencodeProfile.includeDefaultRules).toBe(false);
|
||||
expect(opencodeProfile.fileMap['AGENTS.md']).toBe('AGENTS.md');
|
||||
});
|
||||
|
||||
test('opencode.js has lifecycle functions for MCP config transformation', () => {
|
||||
expect(opencodeProfileContent).toContain(
|
||||
'function onPostConvertRulesProfile'
|
||||
);
|
||||
expect(opencodeProfileContent).toContain('function onRemoveRulesProfile');
|
||||
expect(opencodeProfileContent).toContain('transformToOpenCodeFormat');
|
||||
});
|
||||
|
||||
test('opencode.js handles opencode.json transformation in lifecycle functions', () => {
|
||||
expect(opencodeProfileContent).toContain('opencode.json');
|
||||
expect(opencodeProfileContent).toContain('transformToOpenCodeFormat');
|
||||
expect(opencodeProfileContent).toContain('$schema');
|
||||
expect(opencodeProfileContent).toContain('mcpServers');
|
||||
expect(opencodeProfileContent).toContain('mcp');
|
||||
});
|
||||
|
||||
test('opencode.js has proper error handling in lifecycle functions', () => {
|
||||
expect(opencodeProfileContent).toContain('try {');
|
||||
expect(opencodeProfileContent).toContain('} catch (error) {');
|
||||
expect(opencodeProfileContent).toContain('log(');
|
||||
});
|
||||
|
||||
test('opencode.js uses custom MCP config name', () => {
|
||||
// OpenCode uses opencode.json instead of mcp.json
|
||||
expect(opencodeProfileContent).toContain("mcpConfigName: 'opencode.json'");
|
||||
// Should not contain mcp.json as a config value (comments are OK)
|
||||
expect(opencodeProfileContent).not.toMatch(
|
||||
/mcpConfigName:\s*['"]mcp\.json['"]/
|
||||
);
|
||||
});
|
||||
|
||||
test('opencode.js has transformation logic for OpenCode format', () => {
|
||||
// Check for transformation function
|
||||
expect(opencodeProfileContent).toContain('transformToOpenCodeFormat');
|
||||
|
||||
// Check for specific transformation logic
|
||||
expect(opencodeProfileContent).toContain('mcpServers');
|
||||
expect(opencodeProfileContent).toContain('command');
|
||||
expect(opencodeProfileContent).toContain('args');
|
||||
expect(opencodeProfileContent).toContain('environment');
|
||||
expect(opencodeProfileContent).toContain('enabled');
|
||||
expect(opencodeProfileContent).toContain('type');
|
||||
});
|
||||
});
|
||||
@@ -177,6 +177,13 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||
getRequiredApiKeyName: jest.fn(() => 'XAI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
GroqProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'GROQ_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
OpenRouterAIProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
|
||||
299
tests/unit/profiles/amp-integration.test.js
Normal file
299
tests/unit/profiles/amp-integration.test.js
Normal file
@@ -0,0 +1,299 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
describe('Amp Profile Integration', () => {
|
||||
let tempDir;
|
||||
let ampProfile;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(__dirname, 'temp-amp-unit-'));
|
||||
|
||||
// Get the Amp profile
|
||||
ampProfile = getRulesProfile('amp');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directory
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Profile Structure', () => {
|
||||
test('should have expected profile structure', () => {
|
||||
expect(ampProfile).toBeDefined();
|
||||
expect(ampProfile.profileName).toBe('amp');
|
||||
expect(ampProfile.displayName).toBe('Amp');
|
||||
expect(ampProfile.profileDir).toBe('.vscode');
|
||||
expect(ampProfile.rulesDir).toBe('.');
|
||||
expect(ampProfile.mcpConfig).toBe(true);
|
||||
expect(ampProfile.mcpConfigName).toBe('settings.json');
|
||||
expect(ampProfile.mcpConfigPath).toBe('.vscode/settings.json');
|
||||
expect(ampProfile.includeDefaultRules).toBe(false);
|
||||
});
|
||||
|
||||
test('should have correct file mapping', () => {
|
||||
expect(ampProfile.fileMap).toEqual({
|
||||
'AGENTS.md': '.taskmaster/AGENT.md'
|
||||
});
|
||||
});
|
||||
|
||||
test('should not create unnecessary directories', () => {
|
||||
// Unlike profiles that copy entire directories, Amp should only create what's needed
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onAddRulesProfile
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
|
||||
// Should only have created .taskmaster directory and AGENT.md
|
||||
expect(fs.existsSync(path.join(tempDir, '.taskmaster'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
|
||||
|
||||
// Should not have created any other directories (like .claude)
|
||||
expect(fs.existsSync(path.join(tempDir, '.amp'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(tempDir, '.claude'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AGENT.md Import Logic', () => {
|
||||
test('should handle missing source file gracefully', () => {
|
||||
// Call onAddRulesProfile without creating source file
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
|
||||
// Should not throw error
|
||||
expect(() => {
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
}).not.toThrow();
|
||||
|
||||
// Should not create any files
|
||||
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('should preserve existing content when adding import', () => {
|
||||
// Create existing AGENT.md with specific content
|
||||
const existingContent =
|
||||
'# My Custom Amp Setup\n\nThis is my custom configuration.\n\n## Custom Section\n\nSome custom rules here.';
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENT.md'), existingContent);
|
||||
|
||||
// Create mock source
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onAddRulesProfile
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
|
||||
// Check that existing content is preserved
|
||||
const updatedContent = fs.readFileSync(
|
||||
path.join(tempDir, 'AGENT.md'),
|
||||
'utf8'
|
||||
);
|
||||
expect(updatedContent).toContain('# My Custom Amp Setup');
|
||||
expect(updatedContent).toContain('This is my custom configuration.');
|
||||
expect(updatedContent).toContain('## Custom Section');
|
||||
expect(updatedContent).toContain('Some custom rules here.');
|
||||
expect(updatedContent).toContain('@./.taskmaster/AGENT.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration Handling', () => {
|
||||
test('should handle missing .vscode directory gracefully', () => {
|
||||
// Call onAddRulesProfile without .vscode directory
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
|
||||
// Should not throw error
|
||||
expect(() => {
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should handle malformed JSON gracefully', () => {
|
||||
// Create .vscode directory with malformed JSON
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
'{ malformed json'
|
||||
);
|
||||
|
||||
// Should not throw error
|
||||
expect(() => {
|
||||
ampProfile.onAddRulesProfile(tempDir, path.join(tempDir, 'assets'));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should preserve other VS Code settings when renaming', () => {
|
||||
// Create .vscode/settings.json with various settings
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
|
||||
const initialConfig = {
|
||||
'editor.fontSize': 14,
|
||||
'editor.tabSize': 2,
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
}
|
||||
},
|
||||
'workbench.colorTheme': 'Dark+'
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
JSON.stringify(initialConfig, null, '\t')
|
||||
);
|
||||
|
||||
// Call onPostConvertRulesProfile (which handles MCP transformation)
|
||||
ampProfile.onPostConvertRulesProfile(
|
||||
tempDir,
|
||||
path.join(tempDir, 'assets')
|
||||
);
|
||||
|
||||
// Check that other settings are preserved
|
||||
const settingsFile = path.join(vscodeDirPath, 'settings.json');
|
||||
const content = fs.readFileSync(settingsFile, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
expect(config['editor.fontSize']).toBe(14);
|
||||
expect(config['editor.tabSize']).toBe(2);
|
||||
expect(config['workbench.colorTheme']).toBe('Dark+');
|
||||
expect(config['amp.mcpServers']).toBeDefined();
|
||||
expect(config.mcpServers).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Removal Logic', () => {
|
||||
test('should handle missing files gracefully during removal', () => {
|
||||
// Should not throw error when removing non-existent files
|
||||
expect(() => {
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should handle malformed JSON gracefully during removal', () => {
|
||||
// Create .vscode directory with malformed JSON
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
'{ malformed json'
|
||||
);
|
||||
|
||||
// Should not throw error
|
||||
expect(() => {
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should preserve .vscode directory if it contains other files', () => {
|
||||
// Create .vscode directory with amp.mcpServers and other files
|
||||
const vscodeDirPath = path.join(tempDir, '.vscode');
|
||||
fs.mkdirSync(vscodeDirPath, { recursive: true });
|
||||
|
||||
const initialConfig = {
|
||||
'amp.mcpServers': {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vscodeDirPath, 'settings.json'),
|
||||
JSON.stringify(initialConfig, null, '\t')
|
||||
);
|
||||
|
||||
// Create another file in .vscode
|
||||
fs.writeFileSync(path.join(vscodeDirPath, 'launch.json'), '{}');
|
||||
|
||||
// Call onRemoveRulesProfile
|
||||
ampProfile.onRemoveRulesProfile(tempDir);
|
||||
|
||||
// Check that .vscode directory is preserved
|
||||
expect(fs.existsSync(vscodeDirPath)).toBe(true);
|
||||
expect(fs.existsSync(path.join(vscodeDirPath, 'launch.json'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lifecycle Function Integration', () => {
|
||||
test('should have all required lifecycle functions', () => {
|
||||
expect(typeof ampProfile.onAddRulesProfile).toBe('function');
|
||||
expect(typeof ampProfile.onRemoveRulesProfile).toBe('function');
|
||||
expect(typeof ampProfile.onPostConvertRulesProfile).toBe('function');
|
||||
});
|
||||
|
||||
test('onPostConvertRulesProfile should behave like onAddRulesProfile', () => {
|
||||
// Create mock source
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Call onPostConvertRulesProfile
|
||||
ampProfile.onPostConvertRulesProfile(tempDir, assetsDir);
|
||||
|
||||
// Should have same result as onAddRulesProfile
|
||||
expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
|
||||
|
||||
const agentContent = fs.readFileSync(
|
||||
path.join(tempDir, 'AGENT.md'),
|
||||
'utf8'
|
||||
);
|
||||
expect(agentContent).toContain('@./.taskmaster/AGENT.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle file system errors gracefully', () => {
|
||||
// Mock fs.writeFileSync to throw an error
|
||||
const originalWriteFileSync = fs.writeFileSync;
|
||||
fs.writeFileSync = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
// Create mock source
|
||||
const assetsDir = path.join(tempDir, 'assets');
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
originalWriteFileSync.call(
|
||||
fs,
|
||||
path.join(assetsDir, 'AGENTS.md'),
|
||||
'Task Master instructions'
|
||||
);
|
||||
|
||||
// Should not throw error
|
||||
expect(() => {
|
||||
ampProfile.onAddRulesProfile(tempDir, assetsDir);
|
||||
}).not.toThrow();
|
||||
|
||||
// Restore original function
|
||||
fs.writeFileSync = originalWriteFileSync;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { claudeProfile } from '../../../src/profiles/claude.js';
|
||||
|
||||
// Mock external modules
|
||||
jest.mock('child_process', () => ({
|
||||
@@ -77,11 +78,22 @@ describe('Claude Profile Integration', () => {
|
||||
expect(mkdirCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not create MCP configuration files', () => {
|
||||
test('supports MCP configuration when using rule transformer', () => {
|
||||
// This test verifies that the Claude profile is configured to support MCP
|
||||
// The actual MCP file creation is handled by the rule transformer
|
||||
|
||||
// Assert - Claude profile should now support MCP configuration
|
||||
expect(claudeProfile.mcpConfig).toBe(true);
|
||||
expect(claudeProfile.mcpConfigName).toBe('.mcp.json');
|
||||
expect(claudeProfile.mcpConfigPath).toBe('.mcp.json');
|
||||
});
|
||||
|
||||
test('mock function does not create MCP configuration files', () => {
|
||||
// Act
|
||||
mockCreateClaudeStructure();
|
||||
|
||||
// Assert - Claude profile should not create any MCP config files
|
||||
// Assert - The mock function should not create MCP config files
|
||||
// (This is expected since the mock doesn't use the rule transformer)
|
||||
const writeFileCalls = fs.writeFileSync.mock.calls;
|
||||
const mcpConfigCalls = writeFileCalls.filter(
|
||||
(call) =>
|
||||
|
||||
@@ -5,12 +5,30 @@ import path from 'path';
|
||||
describe('MCP Configuration Validation', () => {
|
||||
describe('Profile MCP Configuration Properties', () => {
|
||||
const expectedMcpConfigurations = {
|
||||
amp: {
|
||||
shouldHaveMcp: true,
|
||||
expectedDir: '.vscode',
|
||||
expectedConfigName: 'settings.json',
|
||||
expectedPath: '.vscode/settings.json'
|
||||
},
|
||||
claude: {
|
||||
shouldHaveMcp: true,
|
||||
expectedDir: '.',
|
||||
expectedConfigName: '.mcp.json',
|
||||
expectedPath: '.mcp.json'
|
||||
},
|
||||
cline: {
|
||||
shouldHaveMcp: false,
|
||||
expectedDir: '.clinerules',
|
||||
expectedConfigName: null,
|
||||
expectedPath: null
|
||||
},
|
||||
codex: {
|
||||
shouldHaveMcp: false,
|
||||
expectedDir: '.',
|
||||
expectedConfigName: null,
|
||||
expectedPath: null
|
||||
},
|
||||
cursor: {
|
||||
shouldHaveMcp: true,
|
||||
expectedDir: '.cursor',
|
||||
@@ -23,6 +41,12 @@ describe('MCP Configuration Validation', () => {
|
||||
expectedConfigName: 'settings.json',
|
||||
expectedPath: '.gemini/settings.json'
|
||||
},
|
||||
opencode: {
|
||||
shouldHaveMcp: true,
|
||||
expectedDir: '.',
|
||||
expectedConfigName: 'opencode.json',
|
||||
expectedPath: 'opencode.json'
|
||||
},
|
||||
roo: {
|
||||
shouldHaveMcp: true,
|
||||
expectedDir: '.roo',
|
||||
@@ -46,6 +70,12 @@ describe('MCP Configuration Validation', () => {
|
||||
expectedDir: '.windsurf',
|
||||
expectedConfigName: 'mcp.json',
|
||||
expectedPath: '.windsurf/mcp.json'
|
||||
},
|
||||
zed: {
|
||||
shouldHaveMcp: true,
|
||||
expectedDir: '.zed',
|
||||
expectedConfigName: 'settings.json',
|
||||
expectedPath: '.zed/settings.json'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,10 +98,18 @@ describe('MCP Configuration Validation', () => {
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
const expectedPath = path.join(
|
||||
profile.profileDir,
|
||||
profile.mcpConfigName
|
||||
);
|
||||
// For root directory profiles, path.join('.', filename) normalizes to just 'filename'
|
||||
// except for Claude which uses '.mcp.json' explicitly
|
||||
let expectedPath;
|
||||
if (profile.profileDir === '.') {
|
||||
if (profileName === 'claude') {
|
||||
expectedPath = '.mcp.json'; // Claude explicitly uses '.mcp.json'
|
||||
} else {
|
||||
expectedPath = profile.mcpConfigName; // Other root profiles normalize to just the filename
|
||||
}
|
||||
} else {
|
||||
expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
|
||||
}
|
||||
expect(profile.mcpConfigPath).toBe(expectedPath);
|
||||
}
|
||||
});
|
||||
@@ -89,10 +127,23 @@ describe('MCP Configuration Validation', () => {
|
||||
});
|
||||
|
||||
test('should ensure all MCP-enabled profiles use proper directory structure', () => {
|
||||
const rootProfiles = ['opencode', 'claude', 'codex']; // Profiles that use root directory for config
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
|
||||
if (rootProfiles.includes(profileName)) {
|
||||
// Root profiles have different patterns
|
||||
if (profileName === 'claude') {
|
||||
expect(profile.mcpConfigPath).toBe('.mcp.json');
|
||||
} else {
|
||||
// Other root profiles normalize to just the filename (no ./ prefix)
|
||||
expect(profile.mcpConfigPath).toMatch(/^[\w_.]+$/);
|
||||
}
|
||||
} else {
|
||||
// Other profiles should have config files in their specific directories
|
||||
expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -123,17 +174,13 @@ describe('MCP Configuration Validation', () => {
|
||||
});
|
||||
|
||||
test('should have null config name for non-MCP profiles', () => {
|
||||
const clineProfile = getRulesProfile('cline');
|
||||
expect(clineProfile.mcpConfigName).toBe(null);
|
||||
// Only codex, cline, and trae profiles should have null config names
|
||||
const nonMcpProfiles = ['codex', 'cline', 'trae'];
|
||||
|
||||
const traeProfile = getRulesProfile('trae');
|
||||
expect(traeProfile.mcpConfigName).toBe(null);
|
||||
|
||||
const claudeProfile = getRulesProfile('claude');
|
||||
expect(claudeProfile.mcpConfigName).toBe(null);
|
||||
|
||||
const codexProfile = getRulesProfile('codex');
|
||||
expect(codexProfile.mcpConfigName).toBe(null);
|
||||
for (const profileName of nonMcpProfiles) {
|
||||
const profile = getRulesProfile(profileName);
|
||||
expect(profile.mcpConfigName).toBe(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,7 +188,9 @@ describe('MCP Configuration Validation', () => {
|
||||
test('should ensure each profile has a unique directory', () => {
|
||||
const profileDirs = new Set();
|
||||
// Profiles that use root directory (can share the same directory)
|
||||
const rootProfiles = ['claude', 'codex', 'gemini'];
|
||||
const rootProfiles = ['claude', 'codex', 'gemini', 'opencode'];
|
||||
// Profiles that intentionally share the same directory
|
||||
const sharedDirectoryProfiles = ['amp', 'vscode']; // Both use .vscode
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
@@ -151,17 +200,25 @@ describe('MCP Configuration Validation', () => {
|
||||
expect(profile.rulesDir).toBe('.');
|
||||
}
|
||||
|
||||
// Profile directories should be unique (except for root profiles)
|
||||
if (!rootProfiles.includes(profileName) || profile.profileDir !== '.') {
|
||||
expect(profileDirs.has(profile.profileDir)).toBe(false);
|
||||
profileDirs.add(profile.profileDir);
|
||||
// Profile directories should be unique (except for root profiles and shared directory profiles)
|
||||
if (
|
||||
!rootProfiles.includes(profileName) &&
|
||||
!sharedDirectoryProfiles.includes(profileName)
|
||||
) {
|
||||
if (profile.profileDir !== '.') {
|
||||
expect(profileDirs.has(profile.profileDir)).toBe(false);
|
||||
profileDirs.add(profile.profileDir);
|
||||
}
|
||||
} else if (sharedDirectoryProfiles.includes(profileName)) {
|
||||
// Shared directory profiles should use .vscode
|
||||
expect(profile.profileDir).toBe('.vscode');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure profile directories follow expected naming convention', () => {
|
||||
// Profiles that use root directory for rules
|
||||
const rootRulesProfiles = ['claude', 'codex', 'gemini'];
|
||||
const rootRulesProfiles = ['claude', 'codex', 'gemini', 'opencode'];
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
@@ -185,17 +242,22 @@ describe('MCP Configuration Validation', () => {
|
||||
|
||||
describe('MCP Configuration Creation Logic', () => {
|
||||
test('should indicate which profiles require MCP configuration creation', () => {
|
||||
// Get all profiles that have MCP configuration enabled
|
||||
const mcpEnabledProfiles = RULE_PROFILES.filter((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
return profile.mcpConfig !== false;
|
||||
});
|
||||
|
||||
// Verify expected MCP-enabled profiles
|
||||
expect(mcpEnabledProfiles).toContain('amp');
|
||||
expect(mcpEnabledProfiles).toContain('claude');
|
||||
expect(mcpEnabledProfiles).toContain('cursor');
|
||||
expect(mcpEnabledProfiles).toContain('gemini');
|
||||
expect(mcpEnabledProfiles).toContain('opencode');
|
||||
expect(mcpEnabledProfiles).toContain('roo');
|
||||
expect(mcpEnabledProfiles).toContain('vscode');
|
||||
expect(mcpEnabledProfiles).toContain('windsurf');
|
||||
expect(mcpEnabledProfiles).not.toContain('claude');
|
||||
expect(mcpEnabledProfiles).toContain('zed');
|
||||
expect(mcpEnabledProfiles).not.toContain('cline');
|
||||
expect(mcpEnabledProfiles).not.toContain('codex');
|
||||
expect(mcpEnabledProfiles).not.toContain('trae');
|
||||
@@ -215,17 +277,36 @@ describe('MCP Configuration Validation', () => {
|
||||
|
||||
describe('MCP Configuration Path Usage Verification', () => {
|
||||
test('should verify that rule transformer functions use mcpConfigPath correctly', () => {
|
||||
// This test verifies that the mcpConfigPath property exists and is properly formatted
|
||||
// for use with the setupMCPConfiguration function
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
// Verify the path is properly formatted for path.join usage
|
||||
expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
|
||||
expect(profile.mcpConfigPath).toContain('/');
|
||||
|
||||
// Verify it matches the expected pattern: profileDir/configName
|
||||
const expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
|
||||
// Root directory profiles have different patterns
|
||||
if (profile.profileDir === '.') {
|
||||
if (profileName === 'claude') {
|
||||
expect(profile.mcpConfigPath).toBe('.mcp.json');
|
||||
} else {
|
||||
// Other root profiles (opencode) normalize to just the filename
|
||||
expect(profile.mcpConfigPath).toBe(profile.mcpConfigName);
|
||||
}
|
||||
} else {
|
||||
// Non-root profiles should contain a directory separator
|
||||
expect(profile.mcpConfigPath).toContain('/');
|
||||
}
|
||||
|
||||
// Verify it matches the expected pattern based on how path.join works
|
||||
let expectedPath;
|
||||
if (profile.profileDir === '.') {
|
||||
if (profileName === 'claude') {
|
||||
expectedPath = '.mcp.json'; // Claude explicitly uses '.mcp.json'
|
||||
} else {
|
||||
expectedPath = profile.mcpConfigName; // path.join('.', 'filename') normalizes to 'filename'
|
||||
}
|
||||
} else {
|
||||
expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
|
||||
}
|
||||
expect(profile.mcpConfigPath).toBe(expectedPath);
|
||||
}
|
||||
});
|
||||
@@ -240,8 +321,12 @@ describe('MCP Configuration Validation', () => {
|
||||
const fullPath = path.join(testProjectRoot, profile.mcpConfigPath);
|
||||
|
||||
// Should result in a proper absolute path
|
||||
expect(fullPath).toBe(`${testProjectRoot}/${profile.mcpConfigPath}`);
|
||||
expect(fullPath).toContain(profile.profileDir);
|
||||
// Note: path.join normalizes paths, so './opencode.json' becomes 'opencode.json'
|
||||
const normalizedExpectedPath = path.join(
|
||||
testProjectRoot,
|
||||
profile.mcpConfigPath
|
||||
);
|
||||
expect(fullPath).toBe(normalizedExpectedPath);
|
||||
expect(fullPath).toContain(profile.mcpConfigName);
|
||||
}
|
||||
});
|
||||
@@ -250,28 +335,45 @@ describe('MCP Configuration Validation', () => {
|
||||
|
||||
describe('MCP Configuration Function Integration', () => {
|
||||
test('should verify that setupMCPConfiguration receives the correct mcpConfigPath parameter', () => {
|
||||
// This test verifies the integration between rule transformer and mcp-utils
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
// Verify that the mcpConfigPath can be used directly with setupMCPConfiguration
|
||||
// The function signature is: setupMCPConfiguration(projectDir, mcpConfigPath)
|
||||
expect(profile.mcpConfigPath).toBeDefined();
|
||||
expect(typeof profile.mcpConfigPath).toBe('string');
|
||||
|
||||
// Verify the path structure is correct for the new function signature
|
||||
const parts = profile.mcpConfigPath.split('/');
|
||||
expect(parts).toHaveLength(2); // Should be profileDir/configName
|
||||
expect(parts[0]).toBe(profile.profileDir);
|
||||
expect(parts[1]).toBe(profile.mcpConfigName);
|
||||
if (profile.profileDir === '.') {
|
||||
// Root directory profiles have special handling
|
||||
if (profileName === 'claude') {
|
||||
expect(profile.mcpConfigPath).toBe('.mcp.json');
|
||||
} else {
|
||||
// Other root profiles normalize to just the filename
|
||||
expect(profile.mcpConfigPath).toBe(profile.mcpConfigName);
|
||||
}
|
||||
} else {
|
||||
// Non-root profiles should have profileDir/configName structure
|
||||
const parts = profile.mcpConfigPath.split('/');
|
||||
expect(parts).toHaveLength(2); // Should be profileDir/configName
|
||||
expect(parts[0]).toBe(profile.profileDir);
|
||||
expect(parts[1]).toBe(profile.mcpConfigName);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP configuration validation', () => {
|
||||
const mcpProfiles = ['cursor', 'gemini', 'roo', 'windsurf', 'vscode'];
|
||||
const nonMcpProfiles = ['claude', 'codex', 'cline', 'trae'];
|
||||
const mcpProfiles = [
|
||||
'amp',
|
||||
'claude',
|
||||
'cursor',
|
||||
'gemini',
|
||||
'opencode',
|
||||
'roo',
|
||||
'windsurf',
|
||||
'vscode',
|
||||
'zed'
|
||||
];
|
||||
const nonMcpProfiles = ['codex', 'cline', 'trae'];
|
||||
const profilesWithLifecycle = ['claude'];
|
||||
const profilesWithoutLifecycle = ['codex'];
|
||||
|
||||
test.each(mcpProfiles)(
|
||||
'should have valid MCP config for %s profile',
|
||||
@@ -295,19 +397,25 @@ describe('MCP Configuration Validation', () => {
|
||||
});
|
||||
|
||||
describe('Profile structure validation', () => {
|
||||
const mcpProfiles = [
|
||||
const allProfiles = [
|
||||
'amp',
|
||||
'claude',
|
||||
'cline',
|
||||
'codex',
|
||||
'cursor',
|
||||
'gemini',
|
||||
'opencode',
|
||||
'roo',
|
||||
'windsurf',
|
||||
'cline',
|
||||
'trae',
|
||||
'vscode'
|
||||
'vscode',
|
||||
'windsurf',
|
||||
'zed'
|
||||
];
|
||||
const profilesWithLifecycle = ['claude'];
|
||||
const profilesWithLifecycle = ['amp', 'claude'];
|
||||
const profilesWithPostConvertLifecycle = ['opencode'];
|
||||
const profilesWithoutLifecycle = ['codex'];
|
||||
|
||||
test.each(mcpProfiles)(
|
||||
test.each(allProfiles)(
|
||||
'should have file mappings for %s profile',
|
||||
(profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
@@ -333,6 +441,21 @@ describe('MCP Configuration Validation', () => {
|
||||
}
|
||||
);
|
||||
|
||||
test.each(profilesWithPostConvertLifecycle)(
|
||||
'should have file mappings and post-convert lifecycle functions for %s profile',
|
||||
(profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
expect(profile).toBeDefined();
|
||||
// OpenCode profile has fileMap and post-convert lifecycle functions
|
||||
expect(profile.fileMap).toBeDefined();
|
||||
expect(typeof profile.fileMap).toBe('object');
|
||||
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
||||
expect(profile.onAddRulesProfile).toBeUndefined(); // OpenCode doesn't have onAdd
|
||||
expect(typeof profile.onRemoveRulesProfile).toBe('function');
|
||||
expect(typeof profile.onPostConvertRulesProfile).toBe('function');
|
||||
}
|
||||
);
|
||||
|
||||
test.each(profilesWithoutLifecycle)(
|
||||
'should have file mappings without lifecycle functions for %s profile',
|
||||
(profileName) => {
|
||||
|
||||
123
tests/unit/profiles/opencode-integration.test.js
Normal file
123
tests/unit/profiles/opencode-integration.test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
describe('OpenCode Profile Integration', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('AGENTS.md')) {
|
||||
return 'Sample AGENTS.md content for OpenCode integration';
|
||||
}
|
||||
if (filePath.toString().includes('opencode.json')) {
|
||||
return JSON.stringify({ mcpServers: {} }, null, 2);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test function that simulates the OpenCode profile file copying behavior
|
||||
function mockCreateOpenCodeStructure() {
|
||||
// OpenCode profile copies AGENTS.md to AGENTS.md in project root (same name)
|
||||
const sourceContent = 'Sample AGENTS.md content for OpenCode integration';
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), sourceContent);
|
||||
|
||||
// OpenCode profile creates opencode.json config file
|
||||
const configContent = JSON.stringify({ mcpServers: {} }, null, 2);
|
||||
fs.writeFileSync(path.join(tempDir, 'opencode.json'), configContent);
|
||||
}
|
||||
|
||||
test('creates AGENTS.md file in project root', () => {
|
||||
// Act
|
||||
mockCreateOpenCodeStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, 'AGENTS.md'),
|
||||
'Sample AGENTS.md content for OpenCode integration'
|
||||
);
|
||||
});
|
||||
|
||||
test('creates opencode.json config file in project root', () => {
|
||||
// Act
|
||||
mockCreateOpenCodeStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, 'opencode.json'),
|
||||
JSON.stringify({ mcpServers: {} }, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('does not create any profile directories', () => {
|
||||
// Act
|
||||
mockCreateOpenCodeStructure();
|
||||
|
||||
// Assert - OpenCode profile should not create any directories
|
||||
// Only the temp directory creation calls should exist
|
||||
const mkdirCalls = fs.mkdirSync.mock.calls.filter(
|
||||
(call) => !call[0].includes('task-master-test-')
|
||||
);
|
||||
expect(mkdirCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('handles transformation of MCP config format', () => {
|
||||
// This test simulates the transformation behavior that would happen in onPostConvert
|
||||
const standardMcpConfig = {
|
||||
mcpServers: {
|
||||
'taskmaster-ai': {
|
||||
command: 'node',
|
||||
args: ['path/to/server.js'],
|
||||
env: {
|
||||
API_KEY: 'test-key'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const expectedOpenCodeConfig = {
|
||||
$schema: 'https://opencode.ai/config.json',
|
||||
mcp: {
|
||||
'taskmaster-ai': {
|
||||
type: 'local',
|
||||
command: ['node', 'path/to/server.js'],
|
||||
enabled: true,
|
||||
environment: {
|
||||
API_KEY: 'test-key'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mock the transformation behavior
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'opencode.json'),
|
||||
JSON.stringify(expectedOpenCodeConfig, null, 2)
|
||||
);
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, 'opencode.json'),
|
||||
JSON.stringify(expectedOpenCodeConfig, null, 2)
|
||||
);
|
||||
});
|
||||
});
|
||||
59
tests/unit/profiles/rule-transformer-opencode.test.js
Normal file
59
tests/unit/profiles/rule-transformer-opencode.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
|
||||
import { opencodeProfile } from '../../../src/profiles/opencode.js';
|
||||
|
||||
describe('Rule Transformer - OpenCode Profile', () => {
|
||||
test('should have correct profile configuration', () => {
|
||||
const opencodeProfile = getRulesProfile('opencode');
|
||||
|
||||
expect(opencodeProfile).toBeDefined();
|
||||
expect(opencodeProfile.profileName).toBe('opencode');
|
||||
expect(opencodeProfile.displayName).toBe('OpenCode');
|
||||
expect(opencodeProfile.profileDir).toBe('.');
|
||||
expect(opencodeProfile.rulesDir).toBe('.');
|
||||
expect(opencodeProfile.mcpConfig).toBe(true);
|
||||
expect(opencodeProfile.mcpConfigName).toBe('opencode.json');
|
||||
expect(opencodeProfile.mcpConfigPath).toBe('opencode.json');
|
||||
expect(opencodeProfile.includeDefaultRules).toBe(false);
|
||||
expect(opencodeProfile.fileMap).toEqual({
|
||||
'AGENTS.md': 'AGENTS.md'
|
||||
});
|
||||
});
|
||||
|
||||
test('should have lifecycle functions for MCP config transformation', () => {
|
||||
// Verify that opencode.js has lifecycle functions
|
||||
expect(opencodeProfile.onPostConvertRulesProfile).toBeDefined();
|
||||
expect(typeof opencodeProfile.onPostConvertRulesProfile).toBe('function');
|
||||
expect(opencodeProfile.onRemoveRulesProfile).toBeDefined();
|
||||
expect(typeof opencodeProfile.onRemoveRulesProfile).toBe('function');
|
||||
});
|
||||
|
||||
test('should use opencode.json instead of mcp.json', () => {
|
||||
const opencodeProfile = getRulesProfile('opencode');
|
||||
expect(opencodeProfile.mcpConfigName).toBe('opencode.json');
|
||||
expect(opencodeProfile.mcpConfigPath).toBe('opencode.json');
|
||||
});
|
||||
|
||||
test('should not include default rules', () => {
|
||||
const opencodeProfile = getRulesProfile('opencode');
|
||||
expect(opencodeProfile.includeDefaultRules).toBe(false);
|
||||
});
|
||||
|
||||
test('should have correct file mapping', () => {
|
||||
const opencodeProfile = getRulesProfile('opencode');
|
||||
expect(opencodeProfile.fileMap).toEqual({
|
||||
'AGENTS.md': 'AGENTS.md'
|
||||
});
|
||||
});
|
||||
|
||||
test('should use root directory for both profile and rules', () => {
|
||||
const opencodeProfile = getRulesProfile('opencode');
|
||||
expect(opencodeProfile.profileDir).toBe('.');
|
||||
expect(opencodeProfile.rulesDir).toBe('.');
|
||||
});
|
||||
|
||||
test('should have MCP configuration enabled', () => {
|
||||
const opencodeProfile = getRulesProfile('opencode');
|
||||
expect(opencodeProfile.mcpConfig).toBe(true);
|
||||
});
|
||||
});
|
||||
212
tests/unit/profiles/rule-transformer-zed.test.js
Normal file
212
tests/unit/profiles/rule-transformer-zed.test.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock fs module before importing anything that uses it
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn(),
|
||||
writeFileSync: jest.fn(),
|
||||
existsSync: jest.fn(),
|
||||
mkdirSync: jest.fn()
|
||||
}));
|
||||
|
||||
// Import modules after mocking
|
||||
import fs from 'fs';
|
||||
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
|
||||
import { zedProfile } from '../../../src/profiles/zed.js';
|
||||
|
||||
describe('Zed Rule Transformer', () => {
|
||||
// Set up spies on the mocked modules
|
||||
const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
|
||||
const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
|
||||
const mockConsoleError = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Setup default mocks
|
||||
mockReadFileSync.mockReturnValue('');
|
||||
mockWriteFileSync.mockImplementation(() => {});
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockMkdirSync.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for basic terms
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
|
||||
Also has references to .mdc files.`;
|
||||
|
||||
// Mock file read to return our test content
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
|
||||
// Mock file system operations
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
// Call the function
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
// Verify the result
|
||||
expect(result).toBe(true);
|
||||
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Get the transformed content
|
||||
const transformedContent = mockWriteFileSync.mock.calls[0][1];
|
||||
|
||||
// Verify Cursor -> Zed transformations
|
||||
expect(transformedContent).toContain('zed.dev');
|
||||
expect(transformedContent).toContain('Zed');
|
||||
expect(transformedContent).not.toContain('cursor.so');
|
||||
expect(transformedContent).not.toContain('Cursor');
|
||||
expect(transformedContent).toContain('.md');
|
||||
expect(transformedContent).not.toContain('.mdc');
|
||||
});
|
||||
|
||||
it('should handle URL transformations', () => {
|
||||
const testContent = `Visit https://cursor.so/docs for more information.
|
||||
Also check out cursor.so and www.cursor.so for updates.`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const transformedContent = mockWriteFileSync.mock.calls[0][1];
|
||||
|
||||
// Verify URL transformations
|
||||
expect(transformedContent).toContain('https://zed.dev');
|
||||
expect(transformedContent).toContain('zed.dev');
|
||||
expect(transformedContent).not.toContain('cursor.so');
|
||||
});
|
||||
|
||||
it('should handle file extension transformations', () => {
|
||||
const testContent = `This rule references file.mdc and another.mdc file.
|
||||
Use the .mdc extension for all rule files.`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const transformedContent = mockWriteFileSync.mock.calls[0][1];
|
||||
|
||||
// Verify file extension transformations
|
||||
expect(transformedContent).toContain('file.md');
|
||||
expect(transformedContent).toContain('another.md');
|
||||
expect(transformedContent).toContain('.md extension');
|
||||
expect(transformedContent).not.toContain('.mdc');
|
||||
});
|
||||
|
||||
it('should handle case variations', () => {
|
||||
const testContent = `CURSOR, Cursor, cursor should all be transformed.`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const transformedContent = mockWriteFileSync.mock.calls[0][1];
|
||||
|
||||
// Verify case transformations
|
||||
// Due to regex order, the case-insensitive rule runs first:
|
||||
// CURSOR -> Zed (because it starts with 'C'), Cursor -> Zed, cursor -> zed
|
||||
expect(transformedContent).toContain('Zed');
|
||||
expect(transformedContent).toContain('zed');
|
||||
expect(transformedContent).not.toContain('CURSOR');
|
||||
expect(transformedContent).not.toContain('Cursor');
|
||||
expect(transformedContent).not.toContain('cursor');
|
||||
});
|
||||
|
||||
it('should create target directory if it does not exist', () => {
|
||||
const testContent = 'Test content';
|
||||
mockReadFileSync.mockReturnValue(testContent);
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'nested/path/test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockMkdirSync).toHaveBeenCalledWith('nested/path', {
|
||||
recursive: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle file system errors gracefully', () => {
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
'Error converting rule file: File not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle write errors gracefully', () => {
|
||||
mockReadFileSync.mockReturnValue('Test content');
|
||||
mockWriteFileSync.mockImplementation(() => {
|
||||
throw new Error('Write permission denied');
|
||||
});
|
||||
|
||||
const result = convertRuleToProfileRule(
|
||||
'test-source.mdc',
|
||||
'test-target.md',
|
||||
zedProfile
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
'Error converting rule file: Write permission denied'
|
||||
);
|
||||
});
|
||||
|
||||
it('should verify profile configuration', () => {
|
||||
expect(zedProfile.profileName).toBe('zed');
|
||||
expect(zedProfile.displayName).toBe('Zed');
|
||||
expect(zedProfile.profileDir).toBe('.zed');
|
||||
expect(zedProfile.mcpConfig).toBe(true);
|
||||
expect(zedProfile.mcpConfigName).toBe('settings.json');
|
||||
expect(zedProfile.mcpConfigPath).toBe('.zed/settings.json');
|
||||
expect(zedProfile.includeDefaultRules).toBe(false);
|
||||
expect(zedProfile.fileMap).toEqual({
|
||||
'AGENTS.md': '.rules'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getRulesProfile
|
||||
} from '../../../src/utils/rule-transformer.js';
|
||||
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
|
||||
import path from 'path';
|
||||
|
||||
describe('Rule Transformer - General', () => {
|
||||
describe('Profile Configuration Validation', () => {
|
||||
@@ -18,10 +19,12 @@ describe('Rule Transformer - General', () => {
|
||||
'codex',
|
||||
'cursor',
|
||||
'gemini',
|
||||
'opencode',
|
||||
'roo',
|
||||
'trae',
|
||||
'vscode',
|
||||
'windsurf'
|
||||
'windsurf',
|
||||
'zed'
|
||||
];
|
||||
expectedProfiles.forEach((profile) => {
|
||||
expect(RULE_PROFILES).toContain(profile);
|
||||
@@ -166,29 +169,28 @@ describe('Rule Transformer - General', () => {
|
||||
// Check types based on MCP configuration
|
||||
expect(typeof profileConfig.mcpConfig).toBe('boolean');
|
||||
|
||||
if (profileConfig.mcpConfig === false) {
|
||||
// Profiles without MCP configuration
|
||||
expect(profileConfig.mcpConfigName).toBe(null);
|
||||
expect(profileConfig.mcpConfigPath).toBe(null);
|
||||
} else {
|
||||
// Profiles with MCP configuration
|
||||
expect(typeof profileConfig.mcpConfigName).toBe('string');
|
||||
expect(typeof profileConfig.mcpConfigPath).toBe('string');
|
||||
|
||||
if (profileConfig.mcpConfig !== false) {
|
||||
// Check that mcpConfigPath is properly constructed
|
||||
expect(profileConfig.mcpConfigPath).toBe(
|
||||
`${profileConfig.profileDir}/${profileConfig.mcpConfigName}`
|
||||
const expectedPath = path.join(
|
||||
profileConfig.profileDir,
|
||||
profileConfig.mcpConfigName
|
||||
);
|
||||
expect(profileConfig.mcpConfigPath).toBe(expectedPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct MCP configuration for each profile', () => {
|
||||
const expectedConfigs = {
|
||||
amp: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'settings.json',
|
||||
expectedPath: '.vscode/settings.json'
|
||||
},
|
||||
claude: {
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
expectedPath: null
|
||||
mcpConfig: true,
|
||||
mcpConfigName: '.mcp.json',
|
||||
expectedPath: '.mcp.json'
|
||||
},
|
||||
cline: {
|
||||
mcpConfig: false,
|
||||
@@ -210,6 +212,11 @@ describe('Rule Transformer - General', () => {
|
||||
mcpConfigName: 'settings.json',
|
||||
expectedPath: '.gemini/settings.json'
|
||||
},
|
||||
opencode: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'opencode.json',
|
||||
expectedPath: 'opencode.json'
|
||||
},
|
||||
roo: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
@@ -229,6 +236,11 @@ describe('Rule Transformer - General', () => {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
expectedPath: '.windsurf/mcp.json'
|
||||
},
|
||||
zed: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'settings.json',
|
||||
expectedPath: '.zed/settings.json'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -245,25 +257,27 @@ describe('Rule Transformer - General', () => {
|
||||
it('should have consistent profileDir and mcpConfigPath relationship', () => {
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
if (profileConfig.mcpConfig === false) {
|
||||
// Profiles without MCP configuration have null mcpConfigPath
|
||||
expect(profileConfig.mcpConfigPath).toBe(null);
|
||||
} else {
|
||||
if (profileConfig.mcpConfig !== false) {
|
||||
// Profiles with MCP configuration should have valid paths
|
||||
// The mcpConfigPath should start with the profileDir
|
||||
expect(profileConfig.mcpConfigPath).toMatch(
|
||||
new RegExp(
|
||||
`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
|
||||
)
|
||||
);
|
||||
|
||||
// The mcpConfigPath should end with the mcpConfigName
|
||||
expect(profileConfig.mcpConfigPath).toMatch(
|
||||
new RegExp(
|
||||
`${profileConfig.mcpConfigName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`
|
||||
)
|
||||
);
|
||||
// Handle root directory profiles differently
|
||||
if (profileConfig.profileDir === '.') {
|
||||
if (profile === 'claude') {
|
||||
// Claude explicitly uses '.mcp.json'
|
||||
expect(profileConfig.mcpConfigPath).toBe('.mcp.json');
|
||||
} else {
|
||||
// Other root profiles normalize to just the filename
|
||||
expect(profileConfig.mcpConfigPath).toBe(
|
||||
profileConfig.mcpConfigName
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Non-root profiles should have profileDir/configName pattern
|
||||
expect(profileConfig.mcpConfigPath).toMatch(
|
||||
new RegExp(
|
||||
`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
99
tests/unit/profiles/zed-integration.test.js
Normal file
99
tests/unit/profiles/zed-integration.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Mock external modules
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock console methods
|
||||
jest.mock('console', () => ({
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
clear: jest.fn()
|
||||
}));
|
||||
|
||||
describe('Zed Integration', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('settings.json')) {
|
||||
return JSON.stringify({ context_servers: {} }, null, 2);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test function that simulates the createProjectStructure behavior for Zed files
|
||||
function mockCreateZedStructure() {
|
||||
// Create main .zed directory
|
||||
fs.mkdirSync(path.join(tempDir, '.zed'), { recursive: true });
|
||||
|
||||
// Create MCP config file (settings.json)
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.zed', 'settings.json'),
|
||||
JSON.stringify({ context_servers: {} }, null, 2)
|
||||
);
|
||||
|
||||
// Create AGENTS.md in project root
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'AGENTS.md'),
|
||||
'# Task Master Instructions\n\nThis is the Task Master agents file.'
|
||||
);
|
||||
}
|
||||
|
||||
test('creates all required .zed directories', () => {
|
||||
// Act
|
||||
mockCreateZedStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.zed'), {
|
||||
recursive: true
|
||||
});
|
||||
});
|
||||
|
||||
test('creates Zed settings.json with context_servers format', () => {
|
||||
// Act
|
||||
mockCreateZedStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.zed', 'settings.json'),
|
||||
JSON.stringify({ context_servers: {} }, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates AGENTS.md in project root', () => {
|
||||
// Act
|
||||
mockCreateZedStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, 'AGENTS.md'),
|
||||
'# Task Master Instructions\n\nThis is the Task Master agents file.'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user