Compare commits
8 Commits
chore/crea
...
fix/mcp.is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5089c4cf | ||
|
|
f2d4f9668a | ||
|
|
4639eee097 | ||
|
|
b78de8dbb4 | ||
|
|
256d7cf19c | ||
|
|
b87499b56e | ||
|
|
1c7badff2f | ||
|
|
5b0eda07f2 |
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
|
||||
5
.changeset/puny-friends-give.md
Normal file
5
.changeset/puny-friends-give.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Fix MCP server error when retrieving tools and resources
|
||||
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
|
||||
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
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -1,11 +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_api_key_here" # Optional, for Groq models. Format: gsk_...
|
||||
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_...
|
||||
@@ -32,10 +32,6 @@ class TaskMasterMCPServer {
|
||||
this.server = new FastMCP(this.options);
|
||||
this.initialized = false;
|
||||
|
||||
this.server.addResource({});
|
||||
|
||||
this.server.addResourceTemplate({});
|
||||
|
||||
// Bind methods
|
||||
this.init = this.init.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'roo' | 'trae' | 'windsurf' | 'vscode'} RulesProfile
|
||||
* @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -16,10 +16,12 @@
|
||||
* - 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
|
||||
@@ -33,10 +35,12 @@ export const RULE_PROFILES = [
|
||||
'codex',
|
||||
'cursor',
|
||||
'gemini',
|
||||
'opencode',
|
||||
'roo',
|
||||
'trae',
|
||||
'vscode',
|
||||
'windsurf'
|
||||
'windsurf',
|
||||
'zed'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,9 @@ 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,14 +113,12 @@ export async function runInteractiveProfilesSetup() {
|
||||
const hasMcpConfig = profile.mcpConfig === true;
|
||||
|
||||
if (!profile.includeDefaultRules) {
|
||||
// Integration guide profiles (claude, codex, gemini, amp) - 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') {
|
||||
description = 'Integration guide and MCP config';
|
||||
} else if (profileName === 'amp') {
|
||||
} else if (hasMcpConfig) {
|
||||
description = 'Integration guide and MCP config';
|
||||
} else {
|
||||
description = 'Integration guide';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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,13 +127,21 @@ 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) {
|
||||
// Claude profile uses root directory (.), so its path is just '.mcp.json'
|
||||
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_.]+$/);
|
||||
}
|
||||
}
|
||||
@@ -142,7 +188,7 @@ 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
|
||||
|
||||
@@ -172,7 +218,7 @@ describe('MCP Configuration Validation', () => {
|
||||
|
||||
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);
|
||||
@@ -203,12 +249,15 @@ describe('MCP Configuration Validation', () => {
|
||||
});
|
||||
|
||||
// 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).toContain('zed');
|
||||
expect(mcpEnabledProfiles).not.toContain('cline');
|
||||
expect(mcpEnabledProfiles).not.toContain('codex');
|
||||
expect(mcpEnabledProfiles).not.toContain('trae');
|
||||
@@ -234,19 +283,31 @@ describe('MCP Configuration Validation', () => {
|
||||
// Verify the path is properly formatted for path.join usage
|
||||
expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
|
||||
|
||||
// Claude profile uses root directory (.), so its path is just '.mcp.json'
|
||||
// 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: profileDir/configName
|
||||
const expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
|
||||
// For Claude, path.join('.', '.mcp.json') returns '.mcp.json'
|
||||
const normalizedExpected =
|
||||
profileName === 'claude' ? '.mcp.json' : expectedPath;
|
||||
expect(profile.mcpConfigPath).toBe(normalizedExpected);
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -260,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);
|
||||
}
|
||||
});
|
||||
@@ -274,10 +339,16 @@ describe('MCP Configuration Validation', () => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
// Verify the path structure is correct for the new function signature
|
||||
if (profile.profileDir === '.') {
|
||||
// Root directory profiles have special handling
|
||||
if (profileName === 'claude') {
|
||||
// Claude profile uses root directory, so path is just '.mcp.json'
|
||||
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);
|
||||
@@ -289,7 +360,17 @@ describe('MCP Configuration Validation', () => {
|
||||
});
|
||||
|
||||
describe('MCP configuration validation', () => {
|
||||
const mcpProfiles = ['cursor', 'gemini', 'roo', 'windsurf', 'vscode'];
|
||||
const mcpProfiles = [
|
||||
'amp',
|
||||
'claude',
|
||||
'cursor',
|
||||
'gemini',
|
||||
'opencode',
|
||||
'roo',
|
||||
'windsurf',
|
||||
'vscode',
|
||||
'zed'
|
||||
];
|
||||
const nonMcpProfiles = ['codex', 'cline', 'trae'];
|
||||
const profilesWithLifecycle = ['claude'];
|
||||
const profilesWithoutLifecycle = ['codex'];
|
||||
@@ -316,20 +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 = ['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);
|
||||
@@ -355,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'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,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);
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -247,11 +259,19 @@ describe('Rule Transformer - General', () => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
if (profileConfig.mcpConfig !== false) {
|
||||
// Profiles with MCP configuration should have valid paths
|
||||
// The mcpConfigPath should start with the profileDir
|
||||
// Handle root directory profiles differently
|
||||
if (profileConfig.profileDir === '.') {
|
||||
if (profile === 'claude') {
|
||||
// Claude uses root directory (.), so path.join('.', '.mcp.json') = '.mcp.json'
|
||||
// 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