Compare commits

..

5 Commits

Author SHA1 Message Date
Ralph Khreish
b724048033 chore: remove all defaults 2025-07-16 19:52:41 +03:00
Ralph Khreish
68f759f215 chore: more coderabbit reviews 2025-07-16 19:26:25 +03:00
Ralph Khreish
2ac7f20601 chore: improve coderabbit config 2025-07-16 19:18:37 +03:00
Ralph Khreish
e91609a373 chore: fix coderabbit config 2025-07-16 19:06:37 +03:00
Ralph Khreish
9fea22cbc3 chore: add coderabbit configuration 2025-07-16 18:30:15 +03:00
23 changed files with 57 additions and 1205 deletions

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": minor
---
feat: Add Zed editor rule profile with agent rules and MCP config
- Resolves #637

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix MCP server error when retrieving tools and resources

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Correct MCP server name and use 'Add to Cursor' button with updated placeholder keys.

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": minor
---
Add OpenCode profile with AGENTS.md and MCP config
- Resolves #965

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Add missing API keys to .env.example and README.md

View File

@@ -8,7 +8,6 @@ GROQ_API_KEY=YOUR_GROQ_KEY_HERE
OPENROUTER_API_KEY=YOUR_OPENROUTER_KEY_HERE OPENROUTER_API_KEY=YOUR_OPENROUTER_KEY_HERE
XAI_API_KEY=YOUR_XAI_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
# Google Vertex AI Configuration # Google Vertex AI Configuration
VERTEX_PROJECT_ID=your-gcp-project-id VERTEX_PROJECT_ID=your-gcp-project-id

View File

@@ -25,7 +25,11 @@ For more detailed information, check out the documentation in the `docs` directo
#### Quick Install for Cursor 1.0+ (One-Click) #### Quick Install for Cursor 1.0+ (One-Click)
[![Add task-master-ai MCP server to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D) 📋 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=
```
> **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. > **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.
@@ -69,7 +73,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
```json ```json
{ {
"mcpServers": { "mcpServers": {
"task-master-ai": { "taskmaster-ai": {
"command": "npx", "command": "npx",
"args": ["-y", "--package=task-master-ai", "task-master-ai"], "args": ["-y", "--package=task-master-ai", "task-master-ai"],
"env": { "env": {
@@ -78,7 +82,6 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
"OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
"GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE", "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE",
"MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE", "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE",
"GROQ_API_KEY": "YOUR_GROQ_KEY_HERE",
"OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE", "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE",
"XAI_API_KEY": "YOUR_XAI_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",
@@ -98,7 +101,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
```json ```json
{ {
"servers": { "servers": {
"task-master-ai": { "taskmaster-ai": {
"command": "npx", "command": "npx",
"args": ["-y", "--package=task-master-ai", "task-master-ai"], "args": ["-y", "--package=task-master-ai", "task-master-ai"],
"env": { "env": {
@@ -107,11 +110,9 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
"OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE",
"GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE", "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE",
"MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE", "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE",
"GROQ_API_KEY": "YOUR_GROQ_KEY_HERE",
"OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE", "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE",
"XAI_API_KEY": "YOUR_XAI_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" "type": "stdio"
} }

View File

@@ -1,12 +1,11 @@
# API Keys (Required to enable respective provider) # API Keys (Required to enable respective provider)
ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-... ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-...
PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-... PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-...
OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-... OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI/OpenRouter models. Format: sk-proj-...
GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models. 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. MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models.
XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI 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. GROQ_API_KEY="your_groq_api_key_here" # Optional, for Groq models. Format: gsk_...
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). 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. 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_... GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_...

View File

@@ -32,6 +32,10 @@ class TaskMasterMCPServer {
this.server = new FastMCP(this.options); this.server = new FastMCP(this.options);
this.initialized = false; this.initialized = false;
this.server.addResource({});
this.server.addResourceTemplate({});
// Bind methods // Bind methods
this.init = this.init.bind(this); this.init = this.init.bind(this);
this.start = this.start.bind(this); this.start = this.start.bind(this);

View File

@@ -190,45 +190,8 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
throw new Error('Parsed AI response is not a valid JSON object.'); 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 // Validate the parsed task object using Zod
const validationResult = updatedTaskSchema.safeParse(preprocessedTask); const validationResult = updatedTaskSchema.safeParse(parsedTask);
if (!validationResult.success) { if (!validationResult.success) {
report('error', 'Parsed task object failed Zod validation.'); report('error', 'Parsed task object failed Zod validation.');
validationResult.error.errors.forEach((err) => { validationResult.error.errors.forEach((err) => {

View File

@@ -196,18 +196,7 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
); );
} }
// Preprocess tasks to ensure required fields have proper defaults const validationResult = updatedTaskArraySchema.safeParse(parsedTasks);
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) { if (!validationResult.success) {
report('error', 'Parsed task array failed Zod validation.'); report('error', 'Parsed task array failed Zod validation.');
validationResult.error.errors.forEach((err) => { validationResult.error.errors.forEach((err) => {
@@ -453,17 +442,7 @@ async function updateTasks(
data.tasks.forEach((task, index) => { data.tasks.forEach((task, index) => {
if (updatedTasksMap.has(task.id)) { if (updatedTasksMap.has(task.id)) {
// Only update if the task was part of the set sent to AI // Only update if the task was part of the set sent to AI
const updatedTask = updatedTasksMap.get(task.id); data.tasks[index] = 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++; actualUpdateCount++;
} }
}); });

View File

@@ -1,5 +1,5 @@
/** /**
* @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile * @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'roo' | 'trae' | 'windsurf' | 'vscode'} RulesProfile
*/ */
/** /**
@@ -16,12 +16,10 @@
* - codex: Codex integration * - codex: Codex integration
* - cursor: Cursor IDE rules * - cursor: Cursor IDE rules
* - gemini: Gemini integration * - gemini: Gemini integration
* - opencode: OpenCode integration
* - roo: Roo Code IDE rules * - roo: Roo Code IDE rules
* - trae: Trae IDE rules * - trae: Trae IDE rules
* - vscode: VS Code with GitHub Copilot integration * - vscode: VS Code with GitHub Copilot integration
* - windsurf: Windsurf IDE rules * - windsurf: Windsurf IDE rules
* - zed: Zed IDE rules
* *
* To add a new rule profile: * To add a new rule profile:
* 1. Add the profile name to this array * 1. Add the profile name to this array
@@ -35,12 +33,10 @@ export const RULE_PROFILES = [
'codex', 'codex',
'cursor', 'cursor',
'gemini', 'gemini',
'opencode',
'roo', 'roo',
'trae', 'trae',
'vscode', 'vscode',
'windsurf', 'windsurf'
'zed'
]; ];
/** /**

View File

@@ -5,9 +5,7 @@ export { clineProfile } from './cline.js';
export { codexProfile } from './codex.js'; export { codexProfile } from './codex.js';
export { cursorProfile } from './cursor.js'; export { cursorProfile } from './cursor.js';
export { geminiProfile } from './gemini.js'; export { geminiProfile } from './gemini.js';
export { opencodeProfile } from './opencode.js';
export { rooProfile } from './roo.js'; export { rooProfile } from './roo.js';
export { traeProfile } from './trae.js'; export { traeProfile } from './trae.js';
export { vscodeProfile } from './vscode.js'; export { vscodeProfile } from './vscode.js';
export { windsurfProfile } from './windsurf.js'; export { windsurfProfile } from './windsurf.js';
export { zedProfile } from './zed.js';

View File

@@ -1,183 +0,0 @@
// 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 };

View File

@@ -1,178 +0,0 @@
// 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 };

View File

@@ -113,12 +113,14 @@ export async function runInteractiveProfilesSetup() {
const hasMcpConfig = profile.mcpConfig === true; const hasMcpConfig = profile.mcpConfig === true;
if (!profile.includeDefaultRules) { if (!profile.includeDefaultRules) {
// Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules // Integration guide profiles (claude, codex, gemini, amp) - don't include standard coding rules
if (profileName === 'claude') { if (profileName === 'claude') {
description = 'Integration guide with Task Master slash commands'; description = 'Integration guide with Task Master slash commands';
} else if (profileName === 'codex') { } else if (profileName === 'codex') {
description = 'Comprehensive Task Master integration guide'; description = 'Comprehensive Task Master integration guide';
} else if (hasMcpConfig) { } else if (profileName === 'gemini') {
description = 'Integration guide and MCP config';
} else if (profileName === 'amp') {
description = 'Integration guide and MCP config'; description = 'Integration guide and MCP config';
} else { } else {
description = 'Integration guide'; description = 'Integration guide';

View File

@@ -1,85 +0,0 @@
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');
});
});

View File

@@ -5,30 +5,12 @@ import path from 'path';
describe('MCP Configuration Validation', () => { describe('MCP Configuration Validation', () => {
describe('Profile MCP Configuration Properties', () => { describe('Profile MCP Configuration Properties', () => {
const expectedMcpConfigurations = { 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: { cline: {
shouldHaveMcp: false, shouldHaveMcp: false,
expectedDir: '.clinerules', expectedDir: '.clinerules',
expectedConfigName: null, expectedConfigName: null,
expectedPath: null expectedPath: null
}, },
codex: {
shouldHaveMcp: false,
expectedDir: '.',
expectedConfigName: null,
expectedPath: null
},
cursor: { cursor: {
shouldHaveMcp: true, shouldHaveMcp: true,
expectedDir: '.cursor', expectedDir: '.cursor',
@@ -41,12 +23,6 @@ describe('MCP Configuration Validation', () => {
expectedConfigName: 'settings.json', expectedConfigName: 'settings.json',
expectedPath: '.gemini/settings.json' expectedPath: '.gemini/settings.json'
}, },
opencode: {
shouldHaveMcp: true,
expectedDir: '.',
expectedConfigName: 'opencode.json',
expectedPath: 'opencode.json'
},
roo: { roo: {
shouldHaveMcp: true, shouldHaveMcp: true,
expectedDir: '.roo', expectedDir: '.roo',
@@ -70,12 +46,6 @@ describe('MCP Configuration Validation', () => {
expectedDir: '.windsurf', expectedDir: '.windsurf',
expectedConfigName: 'mcp.json', expectedConfigName: 'mcp.json',
expectedPath: '.windsurf/mcp.json' expectedPath: '.windsurf/mcp.json'
},
zed: {
shouldHaveMcp: true,
expectedDir: '.zed',
expectedConfigName: 'settings.json',
expectedPath: '.zed/settings.json'
} }
}; };
@@ -98,18 +68,10 @@ describe('MCP Configuration Validation', () => {
RULE_PROFILES.forEach((profileName) => { RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName); const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) { if (profile.mcpConfig !== false) {
// For root directory profiles, path.join('.', filename) normalizes to just 'filename' const expectedPath = path.join(
// except for Claude which uses '.mcp.json' explicitly profile.profileDir,
let expectedPath; profile.mcpConfigName
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); expect(profile.mcpConfigPath).toBe(expectedPath);
} }
}); });
@@ -127,21 +89,13 @@ describe('MCP Configuration Validation', () => {
}); });
test('should ensure all MCP-enabled profiles use proper directory structure', () => { 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) => { RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName); const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) { if (profile.mcpConfig !== false) {
if (rootProfiles.includes(profileName)) { // Claude profile uses root directory (.), so its path is just '.mcp.json'
// Root profiles have different patterns
if (profileName === 'claude') { if (profileName === 'claude') {
expect(profile.mcpConfigPath).toBe('.mcp.json'); expect(profile.mcpConfigPath).toBe('.mcp.json');
} else { } 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_.]+$/); expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
} }
} }
@@ -188,7 +142,7 @@ describe('MCP Configuration Validation', () => {
test('should ensure each profile has a unique directory', () => { test('should ensure each profile has a unique directory', () => {
const profileDirs = new Set(); const profileDirs = new Set();
// Profiles that use root directory (can share the same directory) // Profiles that use root directory (can share the same directory)
const rootProfiles = ['claude', 'codex', 'gemini', 'opencode']; const rootProfiles = ['claude', 'codex', 'gemini'];
// Profiles that intentionally share the same directory // Profiles that intentionally share the same directory
const sharedDirectoryProfiles = ['amp', 'vscode']; // Both use .vscode const sharedDirectoryProfiles = ['amp', 'vscode']; // Both use .vscode
@@ -218,7 +172,7 @@ describe('MCP Configuration Validation', () => {
test('should ensure profile directories follow expected naming convention', () => { test('should ensure profile directories follow expected naming convention', () => {
// Profiles that use root directory for rules // Profiles that use root directory for rules
const rootRulesProfiles = ['claude', 'codex', 'gemini', 'opencode']; const rootRulesProfiles = ['claude', 'codex', 'gemini'];
RULE_PROFILES.forEach((profileName) => { RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName); const profile = getRulesProfile(profileName);
@@ -249,15 +203,12 @@ describe('MCP Configuration Validation', () => {
}); });
// Verify expected MCP-enabled profiles // Verify expected MCP-enabled profiles
expect(mcpEnabledProfiles).toContain('amp');
expect(mcpEnabledProfiles).toContain('claude'); expect(mcpEnabledProfiles).toContain('claude');
expect(mcpEnabledProfiles).toContain('cursor'); expect(mcpEnabledProfiles).toContain('cursor');
expect(mcpEnabledProfiles).toContain('gemini'); expect(mcpEnabledProfiles).toContain('gemini');
expect(mcpEnabledProfiles).toContain('opencode');
expect(mcpEnabledProfiles).toContain('roo'); expect(mcpEnabledProfiles).toContain('roo');
expect(mcpEnabledProfiles).toContain('vscode'); expect(mcpEnabledProfiles).toContain('vscode');
expect(mcpEnabledProfiles).toContain('windsurf'); expect(mcpEnabledProfiles).toContain('windsurf');
expect(mcpEnabledProfiles).toContain('zed');
expect(mcpEnabledProfiles).not.toContain('cline'); expect(mcpEnabledProfiles).not.toContain('cline');
expect(mcpEnabledProfiles).not.toContain('codex'); expect(mcpEnabledProfiles).not.toContain('codex');
expect(mcpEnabledProfiles).not.toContain('trae'); expect(mcpEnabledProfiles).not.toContain('trae');
@@ -283,31 +234,19 @@ describe('MCP Configuration Validation', () => {
// Verify the path is properly formatted for path.join usage // Verify the path is properly formatted for path.join usage
expect(profile.mcpConfigPath.startsWith('/')).toBe(false); expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
// Root directory profiles have different patterns // Claude profile uses root directory (.), so its path is just '.mcp.json'
if (profile.profileDir === '.') {
if (profileName === 'claude') { if (profileName === 'claude') {
expect(profile.mcpConfigPath).toBe('.mcp.json'); expect(profile.mcpConfigPath).toBe('.mcp.json');
} else { } 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('/'); expect(profile.mcpConfigPath).toContain('/');
} }
// Verify it matches the expected pattern based on how path.join works // Verify it matches the expected pattern: profileDir/configName
let expectedPath; const expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
if (profile.profileDir === '.') { // For Claude, path.join('.', '.mcp.json') returns '.mcp.json'
if (profileName === 'claude') { const normalizedExpected =
expectedPath = '.mcp.json'; // Claude explicitly uses '.mcp.json' profileName === 'claude' ? '.mcp.json' : expectedPath;
} else { expect(profile.mcpConfigPath).toBe(normalizedExpected);
expectedPath = profile.mcpConfigName; // path.join('.', 'filename') normalizes to 'filename'
}
} else {
expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
}
expect(profile.mcpConfigPath).toBe(expectedPath);
} }
}); });
}); });
@@ -321,12 +260,8 @@ describe('MCP Configuration Validation', () => {
const fullPath = path.join(testProjectRoot, profile.mcpConfigPath); const fullPath = path.join(testProjectRoot, profile.mcpConfigPath);
// Should result in a proper absolute path // Should result in a proper absolute path
// Note: path.join normalizes paths, so './opencode.json' becomes 'opencode.json' expect(fullPath).toBe(`${testProjectRoot}/${profile.mcpConfigPath}`);
const normalizedExpectedPath = path.join( expect(fullPath).toContain(profile.profileDir);
testProjectRoot,
profile.mcpConfigPath
);
expect(fullPath).toBe(normalizedExpectedPath);
expect(fullPath).toContain(profile.mcpConfigName); expect(fullPath).toContain(profile.mcpConfigName);
} }
}); });
@@ -339,16 +274,10 @@ describe('MCP Configuration Validation', () => {
const profile = getRulesProfile(profileName); const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) { if (profile.mcpConfig !== false) {
// Verify the path structure is correct for the new function signature // Verify the path structure is correct for the new function signature
if (profile.profileDir === '.') {
// Root directory profiles have special handling
if (profileName === 'claude') { if (profileName === 'claude') {
// Claude profile uses root directory, so path is just '.mcp.json'
expect(profile.mcpConfigPath).toBe('.mcp.json'); expect(profile.mcpConfigPath).toBe('.mcp.json');
} else { } 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('/'); const parts = profile.mcpConfigPath.split('/');
expect(parts).toHaveLength(2); // Should be profileDir/configName expect(parts).toHaveLength(2); // Should be profileDir/configName
expect(parts[0]).toBe(profile.profileDir); expect(parts[0]).toBe(profile.profileDir);
@@ -360,17 +289,7 @@ describe('MCP Configuration Validation', () => {
}); });
describe('MCP configuration validation', () => { describe('MCP configuration validation', () => {
const mcpProfiles = [ const mcpProfiles = ['cursor', 'gemini', 'roo', 'windsurf', 'vscode'];
'amp',
'claude',
'cursor',
'gemini',
'opencode',
'roo',
'windsurf',
'vscode',
'zed'
];
const nonMcpProfiles = ['codex', 'cline', 'trae']; const nonMcpProfiles = ['codex', 'cline', 'trae'];
const profilesWithLifecycle = ['claude']; const profilesWithLifecycle = ['claude'];
const profilesWithoutLifecycle = ['codex']; const profilesWithoutLifecycle = ['codex'];
@@ -397,25 +316,20 @@ describe('MCP Configuration Validation', () => {
}); });
describe('Profile structure validation', () => { describe('Profile structure validation', () => {
const allProfiles = [ const mcpProfiles = [
'amp', 'amp',
'claude',
'cline',
'codex',
'cursor', 'cursor',
'gemini', 'gemini',
'opencode',
'roo', 'roo',
'trae',
'vscode',
'windsurf', 'windsurf',
'zed' 'cline',
'trae',
'vscode'
]; ];
const profilesWithLifecycle = ['amp', 'claude']; const profilesWithLifecycle = ['amp', 'claude'];
const profilesWithPostConvertLifecycle = ['opencode'];
const profilesWithoutLifecycle = ['codex']; const profilesWithoutLifecycle = ['codex'];
test.each(allProfiles)( test.each(mcpProfiles)(
'should have file mappings for %s profile', 'should have file mappings for %s profile',
(profileName) => { (profileName) => {
const profile = getRulesProfile(profileName); const profile = getRulesProfile(profileName);
@@ -441,21 +355,6 @@ 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)( test.each(profilesWithoutLifecycle)(
'should have file mappings without lifecycle functions for %s profile', 'should have file mappings without lifecycle functions for %s profile',
(profileName) => { (profileName) => {

View File

@@ -1,123 +0,0 @@
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)
);
});
});

View File

@@ -1,59 +0,0 @@
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);
});
});

View File

@@ -1,212 +0,0 @@
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'
});
});
});

View File

@@ -19,12 +19,10 @@ describe('Rule Transformer - General', () => {
'codex', 'codex',
'cursor', 'cursor',
'gemini', 'gemini',
'opencode',
'roo', 'roo',
'trae', 'trae',
'vscode', 'vscode',
'windsurf', 'windsurf'
'zed'
]; ];
expectedProfiles.forEach((profile) => { expectedProfiles.forEach((profile) => {
expect(RULE_PROFILES).toContain(profile); expect(RULE_PROFILES).toContain(profile);
@@ -212,11 +210,6 @@ describe('Rule Transformer - General', () => {
mcpConfigName: 'settings.json', mcpConfigName: 'settings.json',
expectedPath: '.gemini/settings.json' expectedPath: '.gemini/settings.json'
}, },
opencode: {
mcpConfig: true,
mcpConfigName: 'opencode.json',
expectedPath: 'opencode.json'
},
roo: { roo: {
mcpConfig: true, mcpConfig: true,
mcpConfigName: 'mcp.json', mcpConfigName: 'mcp.json',
@@ -236,11 +229,6 @@ describe('Rule Transformer - General', () => {
mcpConfig: true, mcpConfig: true,
mcpConfigName: 'mcp.json', mcpConfigName: 'mcp.json',
expectedPath: '.windsurf/mcp.json' expectedPath: '.windsurf/mcp.json'
},
zed: {
mcpConfig: true,
mcpConfigName: 'settings.json',
expectedPath: '.zed/settings.json'
} }
}; };
@@ -259,19 +247,11 @@ describe('Rule Transformer - General', () => {
const profileConfig = getRulesProfile(profile); const profileConfig = getRulesProfile(profile);
if (profileConfig.mcpConfig !== false) { if (profileConfig.mcpConfig !== false) {
// Profiles with MCP configuration should have valid paths // Profiles with MCP configuration should have valid paths
// Handle root directory profiles differently // The mcpConfigPath should start with the profileDir
if (profileConfig.profileDir === '.') {
if (profile === 'claude') { if (profile === 'claude') {
// Claude explicitly uses '.mcp.json' // Claude uses root directory (.), so path.join('.', '.mcp.json') = '.mcp.json'
expect(profileConfig.mcpConfigPath).toBe('.mcp.json'); expect(profileConfig.mcpConfigPath).toBe('.mcp.json');
} else { } 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( expect(profileConfig.mcpConfigPath).toMatch(
new RegExp( new RegExp(
`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/` `^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`

View File

@@ -1,99 +0,0 @@
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.'
);
});
});