mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: add slash commands (#1461)
This commit is contained in:
17
.changeset/mode-filtering-slash-commands.md
Normal file
17
.changeset/mode-filtering-slash-commands.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add operating mode filtering for slash commands and rules
|
||||||
|
|
||||||
|
Solo mode and team mode now have distinct sets of commands and rules:
|
||||||
|
- **Solo mode**: Local file-based storage commands (parse-prd, add-task, expand, etc.) plus common commands
|
||||||
|
- **Team mode**: Team-specific commands (goham) plus common commands (show-task, list-tasks, help, etc.)
|
||||||
|
|
||||||
|
Both modes share common commands for viewing and navigating tasks. The difference is:
|
||||||
|
- Solo users get commands for local file management (PRD parsing, task expansion, dependencies)
|
||||||
|
- Team users get Hamster cloud integration commands instead
|
||||||
|
|
||||||
|
When switching modes (e.g., from solo to team), all existing TaskMaster commands and rules are automatically cleaned up before adding the new mode's files. This prevents orphaned commands/rules from previous modes.
|
||||||
|
|
||||||
|
The operating mode is auto-detected from config or auth status, and can be overridden with `--mode=solo|team` flag on the `rules` command.
|
||||||
13
.changeset/warm-things-shake.md
Normal file
13
.changeset/warm-things-shake.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add Taskmaster slash commands for:
|
||||||
|
|
||||||
|
- Roo
|
||||||
|
- Cursor
|
||||||
|
- Codex
|
||||||
|
- Gemini
|
||||||
|
- Opencode
|
||||||
|
|
||||||
|
Add them with `task-master rules add <provider>`
|
||||||
89
.github/workflows/ci.yml
vendored
89
.github/workflows/ci.yml
vendored
@@ -18,24 +18,54 @@ permissions:
|
|||||||
env:
|
env:
|
||||||
DO_NOT_TRACK: 1
|
DO_NOT_TRACK: 1
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
|
NODE_VERSION: 20
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# Single install job that caches node_modules for all other jobs
|
||||||
|
install:
|
||||||
|
name: Install Dependencies
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
run: npm ci
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
# Fast checks that can run in parallel
|
# Fast checks that can run in parallel
|
||||||
format-check:
|
format-check:
|
||||||
name: Format Check
|
name: Format Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: install
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: "npm"
|
|
||||||
|
- name: Restore node_modules cache
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
run: npm ci
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Format Check
|
- name: Format Check
|
||||||
@@ -62,7 +92,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
if: steps.changes.outputs.changesets == 'true'
|
if: steps.changes.outputs.changesets == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Validate changeset package references
|
- name: Validate changeset package references
|
||||||
@@ -78,18 +108,24 @@ jobs:
|
|||||||
name: Typecheck
|
name: Typecheck
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: install
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: "npm"
|
|
||||||
|
- name: Restore node_modules cache
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
run: npm ci
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
@@ -101,18 +137,24 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: install
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: "npm"
|
|
||||||
|
- name: Restore node_modules cache
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
run: npm ci
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -139,16 +181,21 @@ jobs:
|
|||||||
if: always() && !cancelled() && !contains(needs.*.result, 'failure')
|
if: always() && !cancelled() && !contains(needs.*.result, 'failure')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: "npm"
|
|
||||||
|
- name: Restore node_modules cache
|
||||||
|
id: cache-node-modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install --frozen-lockfile --prefer-offline
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
|
run: npm ci
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
|
|||||||
@@ -148,9 +148,7 @@ export class MCPClientManager {
|
|||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {}
|
||||||
tools: {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ describe('generate MCP tool', () => {
|
|||||||
|
|
||||||
const output = execSync(
|
const output = execSync(
|
||||||
`npx @modelcontextprotocol/inspector --cli node "${mcpServerPath}" --method tools/call --tool-name ${toolName} ${toolArgs}`,
|
`npx @modelcontextprotocol/inspector --cli node "${mcpServerPath}" --method tools/call --tool-name ${toolName} ${toolArgs}`,
|
||||||
{ encoding: 'utf-8', stdio: 'pipe' }
|
{
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
env: { ...process.env, TASK_MASTER_TOOLS: 'all' }
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Parse the MCP protocol response: { content: [{ type: "text", text: "<json>" }] }
|
// Parse the MCP protocol response: { content: [{ type: "text", text: "<json>" }] }
|
||||||
|
|||||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.37.2",
|
"version": "0.38.0-rc.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.37.2",
|
"version": "0.38.0-rc.0",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"task-master-ai": "*",
|
"task-master-ai": "0.38.0-rc.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -13432,6 +13432,10 @@
|
|||||||
"resolved": "apps/mcp",
|
"resolved": "apps/mcp",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@tm/profiles": {
|
||||||
|
"resolved": "packages/tm-profiles",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
||||||
@@ -36630,6 +36634,32 @@
|
|||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"packages/tm-profiles": {
|
||||||
|
"name": "@tm/profiles",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@vitest/coverage-v8": "^4.0.10",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vitest": "^4.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/tm-profiles/node_modules/@types/node": {
|
||||||
|
"version": "22.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
|
||||||
|
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/tm-profiles/node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ listTasks(tasksPath, statusFilter, reportPath, withSubtasks, outputFormat, conte
|
|||||||
const tmCore = createTaskMasterCore(projectPath, {
|
const tmCore = createTaskMasterCore(projectPath, {
|
||||||
storage: {
|
storage: {
|
||||||
type: 'api', // or 'file'
|
type: 'api', // or 'file'
|
||||||
apiEndpoint: 'https://hamster.ai/api',
|
apiEndpoint: 'https://tryhamster.com',
|
||||||
apiAccessToken: 'xxx'
|
apiAccessToken: 'xxx'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
33
packages/tm-profiles/package.json
Normal file
33
packages/tm-profiles/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@tm/profiles",
|
||||||
|
"private": true,
|
||||||
|
"description": "Editor profile management for Task Master - handles commands and rules across different editors",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"lint": "biome check --write",
|
||||||
|
"lint:check": "biome check",
|
||||||
|
"lint:fix": "biome check --fix --unsafe",
|
||||||
|
"format": "biome format --write",
|
||||||
|
"format:check": "biome format",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@vitest/coverage-v8": "^4.0.10",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vitest": "^4.0.10"
|
||||||
|
},
|
||||||
|
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||||
|
"keywords": ["task-master", "profiles", "editor", "commands", "typescript"],
|
||||||
|
"author": "Task Master AI",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
7
packages/tm-profiles/src/index.ts
Normal file
7
packages/tm-profiles/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview TaskMaster Profiles Package
|
||||||
|
* Provides slash commands and formatters for different editor profiles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-export everything from slash-commands module
|
||||||
|
export * from './slash-commands/index.js';
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Analyze Project Slash Command
|
||||||
|
* Advanced project analysis with actionable insights and recommendations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The analyze-project slash command - Analyze Project
|
||||||
|
*
|
||||||
|
* Advanced project analysis with actionable insights and recommendations.
|
||||||
|
*/
|
||||||
|
export const analyzeProject = dynamicCommand(
|
||||||
|
'analyze-project',
|
||||||
|
'Analyze Project',
|
||||||
|
'[focus-area]',
|
||||||
|
`Advanced project analysis with actionable insights and recommendations.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Comprehensive Project Analysis
|
||||||
|
|
||||||
|
Multi-dimensional analysis based on requested focus area.
|
||||||
|
|
||||||
|
### 1. **Analysis Modes**
|
||||||
|
|
||||||
|
Based on $ARGUMENTS:
|
||||||
|
- "velocity" → Sprint velocity and trends
|
||||||
|
- "quality" → Code quality metrics
|
||||||
|
- "risk" → Risk assessment and mitigation
|
||||||
|
- "dependencies" → Dependency graph analysis
|
||||||
|
- "team" → Workload and skill distribution
|
||||||
|
- "architecture" → System design coherence
|
||||||
|
- Default → Full spectrum analysis
|
||||||
|
|
||||||
|
### 2. **Velocity Analytics**
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
📊 Velocity Analysis
|
||||||
|
━━━━━━━━━━━━━━━━━━━
|
||||||
|
Current Sprint: 24 points/week ↗️ +20%
|
||||||
|
Rolling Average: 20 points/week
|
||||||
|
Efficiency: 85% (17/20 tasks on time)
|
||||||
|
|
||||||
|
Bottlenecks Detected:
|
||||||
|
- Code review delays (avg 4h wait)
|
||||||
|
- Test environment availability
|
||||||
|
- Dependency on external team
|
||||||
|
|
||||||
|
Recommendations:
|
||||||
|
1. Implement parallel review process
|
||||||
|
2. Add staging environment
|
||||||
|
3. Mock external dependencies
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 3. **Risk Assessment**
|
||||||
|
|
||||||
|
**Technical Risks**
|
||||||
|
- High complexity tasks without backup assignee
|
||||||
|
- Single points of failure in architecture
|
||||||
|
- Insufficient test coverage in critical paths
|
||||||
|
- Technical debt accumulation rate
|
||||||
|
|
||||||
|
**Project Risks**
|
||||||
|
- Critical path dependencies
|
||||||
|
- Resource availability gaps
|
||||||
|
- Deadline feasibility analysis
|
||||||
|
- Scope creep indicators
|
||||||
|
|
||||||
|
### 4. **Dependency Intelligence**
|
||||||
|
|
||||||
|
Visual dependency analysis:
|
||||||
|
\`\`\`
|
||||||
|
Critical Path:
|
||||||
|
#12 → #15 → #23 → #45 → #50 (20 days)
|
||||||
|
↘ #24 → #46 ↗
|
||||||
|
|
||||||
|
Optimization: Parallelize #15 and #24
|
||||||
|
Time Saved: 3 days
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 5. **Quality Metrics**
|
||||||
|
|
||||||
|
**Code Quality**
|
||||||
|
- Test coverage trends
|
||||||
|
- Complexity scores
|
||||||
|
- Technical debt ratio
|
||||||
|
- Review feedback patterns
|
||||||
|
|
||||||
|
**Process Quality**
|
||||||
|
- Rework frequency
|
||||||
|
- Bug introduction rate
|
||||||
|
- Time to resolution
|
||||||
|
- Knowledge distribution
|
||||||
|
|
||||||
|
### 6. **Predictive Insights**
|
||||||
|
|
||||||
|
Based on patterns:
|
||||||
|
- Completion probability by deadline
|
||||||
|
- Resource needs projection
|
||||||
|
- Risk materialization likelihood
|
||||||
|
- Suggested interventions
|
||||||
|
|
||||||
|
### 7. **Executive Dashboard**
|
||||||
|
|
||||||
|
High-level summary with:
|
||||||
|
- Health score (0-100)
|
||||||
|
- Top 3 risks
|
||||||
|
- Top 3 opportunities
|
||||||
|
- Recommended actions
|
||||||
|
- Success probability
|
||||||
|
|
||||||
|
Result: Data-driven decisions with clear action paths.`
|
||||||
|
);
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Auto Implement Tasks Slash Command
|
||||||
|
* Enhanced auto-implementation with intelligent code generation and testing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auto-implement-tasks slash command - Auto Implement Tasks
|
||||||
|
*
|
||||||
|
* Enhanced auto-implementation with intelligent code generation and testing.
|
||||||
|
*/
|
||||||
|
export const autoImplementTasks = dynamicCommand(
|
||||||
|
'auto-implement-tasks',
|
||||||
|
'Auto Implement Tasks',
|
||||||
|
'[task-id]',
|
||||||
|
`Enhanced auto-implementation with intelligent code generation and testing.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Intelligent Auto-Implementation
|
||||||
|
|
||||||
|
Advanced implementation with context awareness and quality checks.
|
||||||
|
|
||||||
|
### 1. **Pre-Implementation Analysis**
|
||||||
|
|
||||||
|
Before starting:
|
||||||
|
- Analyze task complexity and requirements
|
||||||
|
- Check codebase patterns and conventions
|
||||||
|
- Identify similar completed tasks
|
||||||
|
- Assess test coverage needs
|
||||||
|
- Detect potential risks
|
||||||
|
|
||||||
|
### 2. **Smart Implementation Strategy**
|
||||||
|
|
||||||
|
Based on task type and context:
|
||||||
|
|
||||||
|
**Feature Tasks**
|
||||||
|
1. Research existing patterns
|
||||||
|
2. Design component architecture
|
||||||
|
3. Implement with tests
|
||||||
|
4. Integrate with system
|
||||||
|
5. Update documentation
|
||||||
|
|
||||||
|
**Bug Fix Tasks**
|
||||||
|
1. Reproduce issue
|
||||||
|
2. Identify root cause
|
||||||
|
3. Implement minimal fix
|
||||||
|
4. Add regression tests
|
||||||
|
5. Verify side effects
|
||||||
|
|
||||||
|
**Refactoring Tasks**
|
||||||
|
1. Analyze current structure
|
||||||
|
2. Plan incremental changes
|
||||||
|
3. Maintain test coverage
|
||||||
|
4. Refactor step-by-step
|
||||||
|
5. Verify behavior unchanged
|
||||||
|
|
||||||
|
### 3. **Code Intelligence**
|
||||||
|
|
||||||
|
**Pattern Recognition**
|
||||||
|
- Learn from existing code
|
||||||
|
- Follow team conventions
|
||||||
|
- Use preferred libraries
|
||||||
|
- Match style guidelines
|
||||||
|
|
||||||
|
**Test-Driven Approach**
|
||||||
|
- Write tests first when possible
|
||||||
|
- Ensure comprehensive coverage
|
||||||
|
- Include edge cases
|
||||||
|
- Performance considerations
|
||||||
|
|
||||||
|
### 4. **Progressive Implementation**
|
||||||
|
|
||||||
|
Step-by-step with validation:
|
||||||
|
\`\`\`
|
||||||
|
Step 1/5: Setting up component structure ✓
|
||||||
|
Step 2/5: Implementing core logic ✓
|
||||||
|
Step 3/5: Adding error handling ⚡ (in progress)
|
||||||
|
Step 4/5: Writing tests ⏳
|
||||||
|
Step 5/5: Integration testing ⏳
|
||||||
|
|
||||||
|
Current: Adding try-catch blocks and validation...
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 5. **Quality Assurance**
|
||||||
|
|
||||||
|
Automated checks:
|
||||||
|
- Linting and formatting
|
||||||
|
- Test execution
|
||||||
|
- Type checking
|
||||||
|
- Dependency validation
|
||||||
|
- Performance analysis
|
||||||
|
|
||||||
|
### 6. **Smart Recovery**
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
- Diagnostic analysis
|
||||||
|
- Suggestion generation
|
||||||
|
- Fallback strategies
|
||||||
|
- Manual intervention points
|
||||||
|
- Learning from failures
|
||||||
|
|
||||||
|
### 7. **Post-Implementation**
|
||||||
|
|
||||||
|
After completion:
|
||||||
|
- Generate PR description
|
||||||
|
- Update documentation
|
||||||
|
- Log lessons learned
|
||||||
|
- Suggest follow-up tasks
|
||||||
|
- Update task relationships
|
||||||
|
|
||||||
|
Result: High-quality, production-ready implementations.`
|
||||||
|
);
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Command Pipeline Slash Command
|
||||||
|
* Execute a pipeline of commands based on a specification.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command-pipeline slash command - Command Pipeline
|
||||||
|
*
|
||||||
|
* Execute a pipeline of commands based on a specification.
|
||||||
|
*/
|
||||||
|
export const commandPipeline = dynamicCommand(
|
||||||
|
'command-pipeline',
|
||||||
|
'Command Pipeline',
|
||||||
|
'<pipeline-spec>',
|
||||||
|
`Execute a pipeline of commands based on a specification.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Command Pipeline Execution
|
||||||
|
|
||||||
|
Parse pipeline specification from arguments. Supported formats:
|
||||||
|
|
||||||
|
### Simple Pipeline
|
||||||
|
\`init → expand-all → sprint-plan\`
|
||||||
|
|
||||||
|
### Conditional Pipeline
|
||||||
|
\`status → if:pending>10 → sprint-plan → else → next\`
|
||||||
|
|
||||||
|
### Iterative Pipeline
|
||||||
|
\`for:pending-tasks → expand → complexity-check\`
|
||||||
|
|
||||||
|
### Smart Pipeline Patterns
|
||||||
|
|
||||||
|
**1. Project Setup Pipeline**
|
||||||
|
\`\`\`
|
||||||
|
init [prd] →
|
||||||
|
expand-all →
|
||||||
|
complexity-report →
|
||||||
|
sprint-plan →
|
||||||
|
show first-sprint
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**2. Daily Work Pipeline**
|
||||||
|
\`\`\`
|
||||||
|
standup →
|
||||||
|
if:in-progress → continue →
|
||||||
|
else → next → start
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**3. Task Completion Pipeline**
|
||||||
|
\`\`\`
|
||||||
|
complete [id] →
|
||||||
|
git-commit →
|
||||||
|
if:blocked-tasks-freed → show-freed →
|
||||||
|
next
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**4. Quality Check Pipeline**
|
||||||
|
\`\`\`
|
||||||
|
list in-progress →
|
||||||
|
for:each → check-idle-time →
|
||||||
|
if:idle>1day → prompt-update
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Pipeline Features
|
||||||
|
|
||||||
|
**Variables**
|
||||||
|
- Store results: \`status → $count=pending-count\`
|
||||||
|
- Use in conditions: \`if:$count>10\`
|
||||||
|
- Pass between commands: \`expand $high-priority-tasks\`
|
||||||
|
|
||||||
|
**Error Handling**
|
||||||
|
- On failure: \`try:complete → catch:show-blockers\`
|
||||||
|
- Skip on error: \`optional:test-run\`
|
||||||
|
- Retry logic: \`retry:3:commit\`
|
||||||
|
|
||||||
|
**Parallel Execution**
|
||||||
|
- Parallel branches: \`[analyze | test | lint]\`
|
||||||
|
- Join results: \`parallel → join:report\`
|
||||||
|
|
||||||
|
### Execution Flow
|
||||||
|
|
||||||
|
1. Parse pipeline specification
|
||||||
|
2. Validate command sequence
|
||||||
|
3. Execute with state passing
|
||||||
|
4. Handle conditions and loops
|
||||||
|
5. Aggregate results
|
||||||
|
6. Show summary
|
||||||
|
|
||||||
|
This enables complex workflows like:
|
||||||
|
\`parse-prd → expand-all → filter:complex>70 → assign:senior → sprint-plan:weighted\``
|
||||||
|
);
|
||||||
115
packages/tm-profiles/src/slash-commands/commands/common/help.ts
Normal file
115
packages/tm-profiles/src/slash-commands/commands/common/help.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Help Slash Command
|
||||||
|
* Show help for Task Master AI commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The help slash command - Help
|
||||||
|
*
|
||||||
|
* Show help for Task Master AI commands.
|
||||||
|
*/
|
||||||
|
export const help = dynamicCommand(
|
||||||
|
'help',
|
||||||
|
'Help',
|
||||||
|
'[command-name]',
|
||||||
|
`Show help for Task Master AI commands.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Display help for Task Master commands and available options.
|
||||||
|
|
||||||
|
## Task Master AI Command Help
|
||||||
|
|
||||||
|
### Quick Navigation
|
||||||
|
|
||||||
|
Type \`/taskmaster:\` and use tab completion to explore all commands.
|
||||||
|
|
||||||
|
### Command Categories
|
||||||
|
|
||||||
|
#### 🚀 Setup & Installation
|
||||||
|
- \`/taskmaster:install-taskmaster\` - Comprehensive installation guide
|
||||||
|
- \`/taskmaster:quick-install-taskmaster\` - One-line global install
|
||||||
|
|
||||||
|
#### 📋 Project Setup
|
||||||
|
- \`/taskmaster:init-project\` - Initialize new project
|
||||||
|
- \`/taskmaster:init-project-quick\` - Quick setup with auto-confirm
|
||||||
|
- \`/taskmaster:view-models\` - View AI configuration
|
||||||
|
- \`/taskmaster:setup-models\` - Configure AI providers
|
||||||
|
|
||||||
|
#### 🎯 Task Generation
|
||||||
|
- \`/taskmaster:parse-prd\` - Generate tasks from PRD
|
||||||
|
- \`/taskmaster:parse-prd-with-research\` - Enhanced parsing
|
||||||
|
- \`/taskmaster:generate-tasks\` - Create task files
|
||||||
|
|
||||||
|
#### 📝 Task Management
|
||||||
|
- \`/taskmaster:list-tasks\` - List all tasks
|
||||||
|
- \`/taskmaster:list-tasks-by-status\` - List tasks filtered by status
|
||||||
|
- \`/taskmaster:list-tasks-with-subtasks\` - List tasks with subtasks
|
||||||
|
- \`/taskmaster:show-task\` - Display task details
|
||||||
|
- \`/taskmaster:add-task\` - Create new task
|
||||||
|
- \`/taskmaster:update-task\` - Update single task
|
||||||
|
- \`/taskmaster:update-tasks-from-id\` - Update multiple tasks
|
||||||
|
- \`/taskmaster:next-task\` - Get next task recommendation
|
||||||
|
|
||||||
|
#### 🔄 Status Management
|
||||||
|
- \`/taskmaster:to-pending\` - Set task to pending
|
||||||
|
- \`/taskmaster:to-in-progress\` - Set task to in-progress
|
||||||
|
- \`/taskmaster:to-done\` - Set task to done
|
||||||
|
- \`/taskmaster:to-review\` - Set task to review
|
||||||
|
- \`/taskmaster:to-deferred\` - Set task to deferred
|
||||||
|
- \`/taskmaster:to-cancelled\` - Set task to cancelled
|
||||||
|
|
||||||
|
#### 🔍 Analysis & Breakdown
|
||||||
|
- \`/taskmaster:analyze-complexity\` - Analyze task complexity
|
||||||
|
- \`/taskmaster:complexity-report\` - View complexity report
|
||||||
|
- \`/taskmaster:expand-task\` - Break down complex task
|
||||||
|
- \`/taskmaster:expand-all-tasks\` - Expand all eligible tasks
|
||||||
|
|
||||||
|
#### 🔗 Dependencies
|
||||||
|
- \`/taskmaster:add-dependency\` - Add task dependency
|
||||||
|
- \`/taskmaster:remove-dependency\` - Remove dependency
|
||||||
|
- \`/taskmaster:validate-dependencies\` - Check for issues
|
||||||
|
- \`/taskmaster:fix-dependencies\` - Auto-fix dependency issues
|
||||||
|
|
||||||
|
#### 📦 Subtasks
|
||||||
|
- \`/taskmaster:add-subtask\` - Add subtask to task
|
||||||
|
- \`/taskmaster:convert-task-to-subtask\` - Convert task to subtask
|
||||||
|
- \`/taskmaster:remove-subtask\` - Remove subtask
|
||||||
|
- \`/taskmaster:remove-subtasks\` - Clear specific task subtasks
|
||||||
|
- \`/taskmaster:remove-all-subtasks\` - Clear all subtasks
|
||||||
|
|
||||||
|
#### 🗑️ Task Removal
|
||||||
|
- \`/taskmaster:remove-task\` - Remove task permanently
|
||||||
|
|
||||||
|
#### 🤖 Workflows
|
||||||
|
- \`/taskmaster:smart-workflow\` - Intelligent workflows
|
||||||
|
- \`/taskmaster:command-pipeline\` - Command chaining
|
||||||
|
- \`/taskmaster:auto-implement-tasks\` - Auto-implementation
|
||||||
|
|
||||||
|
#### 📊 Utilities
|
||||||
|
- \`/taskmaster:analyze-project\` - Project analysis
|
||||||
|
- \`/taskmaster:project-status\` - Project dashboard
|
||||||
|
- \`/taskmaster:sync-readme\` - Sync README with tasks
|
||||||
|
- \`/taskmaster:learn\` - Interactive learning
|
||||||
|
- \`/taskmaster:tm-main\` - Main Task Master interface
|
||||||
|
|
||||||
|
### Quick Start Examples
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:list-tasks
|
||||||
|
/taskmaster:show-task 1.2
|
||||||
|
/taskmaster:add-task
|
||||||
|
/taskmaster:next-task
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
1. Install: \`/taskmaster:quick-install-taskmaster\`
|
||||||
|
2. Initialize: \`/taskmaster:init-project-quick\`
|
||||||
|
3. Learn: \`/taskmaster:learn\`
|
||||||
|
4. Work: \`/taskmaster:smart-workflow\`
|
||||||
|
|
||||||
|
For detailed command info, run the specific command with \`--help\` or check command documentation.`
|
||||||
|
);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Common Commands
|
||||||
|
* Commands that work in both solo and team modes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Display
|
||||||
|
export { showTask } from './show-task.js';
|
||||||
|
export { listTasks } from './list-tasks.js';
|
||||||
|
export { listTasksWithSubtasks } from './list-tasks-with-subtasks.js';
|
||||||
|
export { listTasksByStatus } from './list-tasks-by-status.js';
|
||||||
|
export { projectStatus } from './project-status.js';
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
export { nextTask } from './next-task.js';
|
||||||
|
export { help } from './help.js';
|
||||||
|
|
||||||
|
// Status (common)
|
||||||
|
export { toDone } from './to-done.js';
|
||||||
|
export { toPending } from './to-pending.js';
|
||||||
|
export { toInProgress } from './to-in-progress.js';
|
||||||
|
|
||||||
|
// Updates
|
||||||
|
export { updateTask } from './update-task.js';
|
||||||
|
export { updateSingleTask } from './update-single-task.js';
|
||||||
|
export { updateTasksFromId } from './update-tasks-from-id.js';
|
||||||
|
|
||||||
|
// Workflows
|
||||||
|
export { tmMain } from './tm-main.js';
|
||||||
|
export { smartWorkflow } from './smart-workflow.js';
|
||||||
|
export { learn } from './learn.js';
|
||||||
|
export { commandPipeline } from './command-pipeline.js';
|
||||||
|
export { autoImplementTasks } from './auto-implement-tasks.js';
|
||||||
|
|
||||||
|
// Other
|
||||||
|
export { analyzeProject } from './analyze-project.js';
|
||||||
|
export { syncReadme } from './sync-readme.js';
|
||||||
120
packages/tm-profiles/src/slash-commands/commands/common/learn.ts
Normal file
120
packages/tm-profiles/src/slash-commands/commands/common/learn.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Learn Slash Command
|
||||||
|
* Learn about Task Master capabilities through interactive exploration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The learn slash command - Learn
|
||||||
|
*
|
||||||
|
* Learn about Task Master capabilities through interactive exploration.
|
||||||
|
*/
|
||||||
|
export const learn = dynamicCommand(
|
||||||
|
'learn',
|
||||||
|
'Learn',
|
||||||
|
'[topic]',
|
||||||
|
`Learn about Task Master capabilities through interactive exploration.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Interactive Task Master Learning
|
||||||
|
|
||||||
|
Based on your input, I'll help you discover capabilities:
|
||||||
|
|
||||||
|
### 1. **What are you trying to do?**
|
||||||
|
|
||||||
|
If $ARGUMENTS contains:
|
||||||
|
- "start" / "begin" → Show project initialization workflows
|
||||||
|
- "manage" / "organize" → Show task management commands
|
||||||
|
- "automate" / "auto" → Show automation workflows
|
||||||
|
- "analyze" / "report" → Show analysis tools
|
||||||
|
- "fix" / "problem" → Show troubleshooting commands
|
||||||
|
- "fast" / "quick" → Show efficiency shortcuts
|
||||||
|
|
||||||
|
### 2. **Intelligent Suggestions**
|
||||||
|
|
||||||
|
Based on your project state:
|
||||||
|
|
||||||
|
**No tasks yet?**
|
||||||
|
\`\`\`
|
||||||
|
You'll want to start with:
|
||||||
|
1. /project:task-master:init <prd-file>
|
||||||
|
→ Creates tasks from requirements
|
||||||
|
|
||||||
|
2. /project:task-master:parse-prd <file>
|
||||||
|
→ Alternative task generation
|
||||||
|
|
||||||
|
Try: /project:task-master:init demo-prd.md
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Have tasks?**
|
||||||
|
Let me analyze what you might need...
|
||||||
|
- Many pending tasks? → Learn sprint planning
|
||||||
|
- Complex tasks? → Learn task expansion
|
||||||
|
- Daily work? → Learn workflow automation
|
||||||
|
|
||||||
|
### 3. **Command Discovery**
|
||||||
|
|
||||||
|
**By Category:**
|
||||||
|
- 📋 Task Management: list, show, add, update, complete
|
||||||
|
- 🔄 Workflows: auto-implement, sprint-plan, daily-standup
|
||||||
|
- 🛠️ Utilities: check-health, complexity-report, sync-memory
|
||||||
|
- 🔍 Analysis: validate-deps, show dependencies
|
||||||
|
|
||||||
|
**By Scenario:**
|
||||||
|
- "I want to see what to work on" → \`/project:task-master:next\`
|
||||||
|
- "I need to break this down" → \`/project:task-master:expand <id>\`
|
||||||
|
- "Show me everything" → \`/project:task-master:status\`
|
||||||
|
- "Just do it for me" → \`/project:workflows:auto-implement\`
|
||||||
|
|
||||||
|
### 4. **Power User Patterns**
|
||||||
|
|
||||||
|
**Command Chaining:**
|
||||||
|
\`\`\`
|
||||||
|
/project:task-master:next
|
||||||
|
/project:task-master:start <id>
|
||||||
|
/project:workflows:auto-implement
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Smart Filters:**
|
||||||
|
\`\`\`
|
||||||
|
/project:task-master:list pending high
|
||||||
|
/project:task-master:list blocked
|
||||||
|
/project:task-master:list 1-5 tree
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Automation:**
|
||||||
|
\`\`\`
|
||||||
|
/project:workflows:pipeline init → expand-all → sprint-plan
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 5. **Learning Path**
|
||||||
|
|
||||||
|
Based on your experience level:
|
||||||
|
|
||||||
|
**Beginner Path:**
|
||||||
|
1. init → Create project
|
||||||
|
2. status → Understand state
|
||||||
|
3. next → Find work
|
||||||
|
4. complete → Finish task
|
||||||
|
|
||||||
|
**Intermediate Path:**
|
||||||
|
1. expand → Break down complex tasks
|
||||||
|
2. sprint-plan → Organize work
|
||||||
|
3. complexity-report → Understand difficulty
|
||||||
|
4. validate-deps → Ensure consistency
|
||||||
|
|
||||||
|
**Advanced Path:**
|
||||||
|
1. pipeline → Chain operations
|
||||||
|
2. smart-flow → Context-aware automation
|
||||||
|
3. Custom commands → Extend the system
|
||||||
|
|
||||||
|
### 6. **Try This Now**
|
||||||
|
|
||||||
|
Based on what you asked about, try:
|
||||||
|
[Specific command suggestion based on $ARGUMENTS]
|
||||||
|
|
||||||
|
Want to learn more about a specific command?
|
||||||
|
Type: /project:help <command-name>`
|
||||||
|
);
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview List Tasks By Status Slash Command
|
||||||
|
* List tasks filtered by a specific status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list-tasks-by-status slash command - List Tasks By Status
|
||||||
|
*
|
||||||
|
* List tasks filtered by a specific status.
|
||||||
|
*/
|
||||||
|
export const listTasksByStatus = dynamicCommand(
|
||||||
|
'list-tasks-by-status',
|
||||||
|
'List Tasks By Status',
|
||||||
|
'<status>',
|
||||||
|
`List tasks filtered by a specific status.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse the status from arguments and list only tasks matching that status.
|
||||||
|
|
||||||
|
## Status Options
|
||||||
|
- \`pending\` - Not yet started
|
||||||
|
- \`in-progress\` - Currently being worked on
|
||||||
|
- \`done\` - Completed
|
||||||
|
- \`review\` - Awaiting review
|
||||||
|
- \`deferred\` - Postponed
|
||||||
|
- \`cancelled\` - Cancelled
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
Based on $ARGUMENTS, run:
|
||||||
|
\`\`\`bash
|
||||||
|
task-master list --status=$ARGUMENTS
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Enhanced Display
|
||||||
|
|
||||||
|
For the filtered results:
|
||||||
|
- Group by priority within the status
|
||||||
|
- Show time in current status
|
||||||
|
- Highlight tasks approaching deadlines
|
||||||
|
- Display blockers and dependencies
|
||||||
|
- Suggest next actions for each status group
|
||||||
|
|
||||||
|
## Intelligent Insights
|
||||||
|
|
||||||
|
Based on the status filter:
|
||||||
|
- **Pending**: Show recommended start order
|
||||||
|
- **In-Progress**: Display idle time warnings
|
||||||
|
- **Done**: Show newly unblocked tasks
|
||||||
|
- **Review**: Indicate review duration
|
||||||
|
- **Deferred**: Show reactivation criteria
|
||||||
|
- **Cancelled**: Display impact analysis`
|
||||||
|
);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview List Tasks With Subtasks Slash Command
|
||||||
|
* List all tasks including their subtasks in a hierarchical view.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list-tasks-with-subtasks slash command - List Tasks With Subtasks
|
||||||
|
*
|
||||||
|
* List all tasks including their subtasks in a hierarchical view.
|
||||||
|
*/
|
||||||
|
export const listTasksWithSubtasks = staticCommand({
|
||||||
|
name: 'list-tasks-with-subtasks',
|
||||||
|
description: 'List Tasks With Subtasks',
|
||||||
|
content: `List all tasks including their subtasks in a hierarchical view.
|
||||||
|
|
||||||
|
This command shows all tasks with their nested subtasks, providing a complete project overview.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
Run the Task Master list command with subtasks flag:
|
||||||
|
\`\`\`bash
|
||||||
|
task-master list --with-subtasks
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Enhanced Display
|
||||||
|
|
||||||
|
I'll organize the output to show:
|
||||||
|
- Parent tasks with clear indicators
|
||||||
|
- Nested subtasks with proper indentation
|
||||||
|
- Status badges for quick scanning
|
||||||
|
- Dependencies and blockers highlighted
|
||||||
|
- Progress indicators for tasks with subtasks
|
||||||
|
|
||||||
|
## Smart Filtering
|
||||||
|
|
||||||
|
Based on the task hierarchy:
|
||||||
|
- Show completion percentage for parent tasks
|
||||||
|
- Highlight blocked subtask chains
|
||||||
|
- Group by functional areas
|
||||||
|
- Indicate critical path items
|
||||||
|
|
||||||
|
This gives you a complete tree view of your project structure.`
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview List Tasks Slash Command
|
||||||
|
* List tasks with intelligent argument parsing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list-tasks slash command - List Tasks
|
||||||
|
*
|
||||||
|
* List tasks with intelligent argument parsing.
|
||||||
|
*/
|
||||||
|
export const listTasks = dynamicCommand(
|
||||||
|
'list-tasks',
|
||||||
|
'List Tasks',
|
||||||
|
'[filters]',
|
||||||
|
`List tasks with intelligent argument parsing.
|
||||||
|
|
||||||
|
Parse arguments to determine filters and display options:
|
||||||
|
- Status: pending, in-progress, done, review, deferred, cancelled
|
||||||
|
- Priority: high, medium, low (or priority:high)
|
||||||
|
- Special: subtasks, tree, dependencies, blocked
|
||||||
|
- IDs: Direct numbers (e.g., "1,3,5" or "1-5")
|
||||||
|
- Complex: "pending high" = pending AND high priority
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Let me parse your request intelligently:
|
||||||
|
|
||||||
|
1. **Detect Filter Intent**
|
||||||
|
- If arguments contain status keywords → filter by status
|
||||||
|
- If arguments contain priority → filter by priority
|
||||||
|
- If arguments contain "subtasks" → include subtasks
|
||||||
|
- If arguments contain "tree" → hierarchical view
|
||||||
|
- If arguments contain numbers → show specific tasks
|
||||||
|
- If arguments contain "blocked" → show blocked tasks only
|
||||||
|
|
||||||
|
2. **Smart Combinations**
|
||||||
|
Examples of what I understand:
|
||||||
|
- "pending high" → pending tasks with high priority
|
||||||
|
- "done today" → tasks completed today
|
||||||
|
- "blocked" → tasks with unmet dependencies
|
||||||
|
- "1-5" → tasks 1 through 5
|
||||||
|
- "subtasks tree" → hierarchical view with subtasks
|
||||||
|
|
||||||
|
3. **Execute Appropriate Query**
|
||||||
|
Based on parsed intent, run the most specific task-master command
|
||||||
|
|
||||||
|
4. **Enhanced Display**
|
||||||
|
- Group by relevant criteria
|
||||||
|
- Show most important information first
|
||||||
|
- Use visual indicators for quick scanning
|
||||||
|
- Include relevant metrics
|
||||||
|
|
||||||
|
5. **Intelligent Suggestions**
|
||||||
|
Based on what you're viewing, suggest next actions:
|
||||||
|
- Many pending? → Suggest priority order
|
||||||
|
- Many blocked? → Show dependency resolution
|
||||||
|
- Looking at specific tasks? → Show related tasks`
|
||||||
|
);
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Next Task Slash Command
|
||||||
|
* Intelligently determine and prepare the next action based on comprehensive context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The next-task slash command - Next Task
|
||||||
|
*
|
||||||
|
* Intelligently determine and prepare the next action based on comprehensive context.
|
||||||
|
*/
|
||||||
|
export const nextTask = dynamicCommand(
|
||||||
|
'next-task',
|
||||||
|
'Next Task',
|
||||||
|
'[preference]',
|
||||||
|
`Intelligently determine and prepare the next action based on comprehensive context.
|
||||||
|
|
||||||
|
This enhanced version of 'next' considers:
|
||||||
|
- Current task states
|
||||||
|
- Recent activity
|
||||||
|
- Time constraints
|
||||||
|
- Dependencies
|
||||||
|
- Your working patterns
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Intelligent Next Action
|
||||||
|
|
||||||
|
### 1. **Context Gathering**
|
||||||
|
Let me analyze the current situation:
|
||||||
|
- Active tasks (in-progress)
|
||||||
|
- Recently completed tasks
|
||||||
|
- Blocked tasks
|
||||||
|
- Time since last activity
|
||||||
|
- Arguments provided: $ARGUMENTS
|
||||||
|
|
||||||
|
### 2. **Smart Decision Tree**
|
||||||
|
|
||||||
|
**If you have an in-progress task:**
|
||||||
|
- Has it been idle > 2 hours? → Suggest resuming or switching
|
||||||
|
- Near completion? → Show remaining steps
|
||||||
|
- Blocked? → Find alternative task
|
||||||
|
|
||||||
|
**If no in-progress tasks:**
|
||||||
|
- Unblocked high-priority tasks? → Start highest
|
||||||
|
- Complex tasks need breakdown? → Suggest expansion
|
||||||
|
- All tasks blocked? → Show dependency resolution
|
||||||
|
|
||||||
|
**Special arguments handling:**
|
||||||
|
- "quick" → Find task < 2 hours
|
||||||
|
- "easy" → Find low complexity task
|
||||||
|
- "important" → Find high priority regardless of complexity
|
||||||
|
- "continue" → Resume last worked task
|
||||||
|
|
||||||
|
### 3. **Preparation Workflow**
|
||||||
|
|
||||||
|
Based on selected task:
|
||||||
|
1. Show full context and history
|
||||||
|
2. Set up development environment
|
||||||
|
3. Run relevant tests
|
||||||
|
4. Open related files
|
||||||
|
5. Show similar completed tasks
|
||||||
|
6. Estimate completion time
|
||||||
|
|
||||||
|
### 4. **Alternative Suggestions**
|
||||||
|
|
||||||
|
Always provide options:
|
||||||
|
- Primary recommendation
|
||||||
|
- Quick alternative (< 1 hour)
|
||||||
|
- Strategic option (unblocks most tasks)
|
||||||
|
- Learning option (new technology/skill)
|
||||||
|
|
||||||
|
### 5. **Workflow Integration**
|
||||||
|
|
||||||
|
Seamlessly connect to:
|
||||||
|
- \`/project:task-master:start [selected]\`
|
||||||
|
- \`/project:workflows:auto-implement\`
|
||||||
|
- \`/project:task-master:expand\` (if complex)
|
||||||
|
- \`/project:utils:complexity-report\` (if unsure)
|
||||||
|
|
||||||
|
The goal: Zero friction from decision to implementation.`
|
||||||
|
);
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Project Status Slash Command
|
||||||
|
* Enhanced status command with comprehensive project insights.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The project-status slash command - Project Status
|
||||||
|
*
|
||||||
|
* Enhanced status command with comprehensive project insights.
|
||||||
|
*/
|
||||||
|
export const projectStatus = dynamicCommand(
|
||||||
|
'project-status',
|
||||||
|
'Project Status',
|
||||||
|
'[focus-area]',
|
||||||
|
`Enhanced status command with comprehensive project insights.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Intelligent Status Overview
|
||||||
|
|
||||||
|
### 1. **Executive Summary**
|
||||||
|
Quick dashboard view:
|
||||||
|
- 🏃 Active work (in-progress tasks)
|
||||||
|
- 📊 Progress metrics (% complete, velocity)
|
||||||
|
- 🚧 Blockers and risks
|
||||||
|
- ⏱️ Time analysis (estimated vs actual)
|
||||||
|
- 🎯 Sprint/milestone progress
|
||||||
|
|
||||||
|
### 2. **Contextual Analysis**
|
||||||
|
|
||||||
|
Based on $ARGUMENTS, focus on:
|
||||||
|
- "sprint" → Current sprint progress and burndown
|
||||||
|
- "blocked" → Dependency chains and resolution paths
|
||||||
|
- "team" → Task distribution and workload
|
||||||
|
- "timeline" → Schedule adherence and projections
|
||||||
|
- "risk" → High complexity or overdue items
|
||||||
|
|
||||||
|
### 3. **Smart Insights**
|
||||||
|
|
||||||
|
**Workflow Health:**
|
||||||
|
- Idle tasks (in-progress > 24h without updates)
|
||||||
|
- Bottlenecks (multiple tasks waiting on same dependency)
|
||||||
|
- Quick wins (low complexity, high impact)
|
||||||
|
|
||||||
|
**Predictive Analytics:**
|
||||||
|
- Completion projections based on velocity
|
||||||
|
- Risk of missing deadlines
|
||||||
|
- Recommended task order for optimal flow
|
||||||
|
|
||||||
|
### 4. **Visual Intelligence**
|
||||||
|
|
||||||
|
Dynamic visualization based on data:
|
||||||
|
\`\`\`
|
||||||
|
Sprint Progress: ████████░░ 80% (16/20 tasks)
|
||||||
|
Velocity Trend: ↗️ +15% this week
|
||||||
|
Blocked Tasks: 🔴 3 critical path items
|
||||||
|
|
||||||
|
Priority Distribution:
|
||||||
|
High: ████████ 8 tasks (2 blocked)
|
||||||
|
Medium: ████░░░░ 4 tasks
|
||||||
|
Low: ██░░░░░░ 2 tasks
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 5. **Actionable Recommendations**
|
||||||
|
|
||||||
|
Based on analysis:
|
||||||
|
1. **Immediate actions** (unblock critical path)
|
||||||
|
2. **Today's focus** (optimal task sequence)
|
||||||
|
3. **Process improvements** (recurring patterns)
|
||||||
|
4. **Resource needs** (skills, time, dependencies)
|
||||||
|
|
||||||
|
### 6. **Historical Context**
|
||||||
|
|
||||||
|
Compare to previous periods:
|
||||||
|
- Velocity changes
|
||||||
|
- Pattern recognition
|
||||||
|
- Improvement areas
|
||||||
|
- Success patterns to repeat`
|
||||||
|
);
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Show Task Slash Command
|
||||||
|
* Show detailed task information with rich context and insights.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The show-task slash command - Show Task
|
||||||
|
*
|
||||||
|
* Show detailed task information with rich context and insights.
|
||||||
|
*/
|
||||||
|
export const showTask = dynamicCommand(
|
||||||
|
'show-task',
|
||||||
|
'Show Task',
|
||||||
|
'<task-id>',
|
||||||
|
`Show detailed task information with rich context and insights.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Enhanced Task Display
|
||||||
|
|
||||||
|
Parse arguments to determine what to show and how.
|
||||||
|
|
||||||
|
### 1. **Smart Task Selection**
|
||||||
|
|
||||||
|
Based on $ARGUMENTS:
|
||||||
|
- Number → Show specific task with full context
|
||||||
|
- "current" → Show active in-progress task(s)
|
||||||
|
- "next" → Show recommended next task
|
||||||
|
- "blocked" → Show all blocked tasks with reasons
|
||||||
|
- "critical" → Show critical path tasks
|
||||||
|
- Multiple IDs → Comparative view
|
||||||
|
|
||||||
|
### 2. **Contextual Information**
|
||||||
|
|
||||||
|
For each task, intelligently include:
|
||||||
|
|
||||||
|
**Core Details**
|
||||||
|
- Full task information (id, title, description, details)
|
||||||
|
- Current status with history
|
||||||
|
- Test strategy and acceptance criteria
|
||||||
|
- Priority and complexity analysis
|
||||||
|
|
||||||
|
**Relationships**
|
||||||
|
- Dependencies (what it needs)
|
||||||
|
- Dependents (what needs it)
|
||||||
|
- Parent/subtask hierarchy
|
||||||
|
- Related tasks (similar work)
|
||||||
|
|
||||||
|
**Time Intelligence**
|
||||||
|
- Created/updated timestamps
|
||||||
|
- Time in current status
|
||||||
|
- Estimated vs actual time
|
||||||
|
- Historical completion patterns
|
||||||
|
|
||||||
|
### 3. **Visual Enhancements**
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
📋 Task #45: Implement User Authentication
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Status: 🟡 in-progress (2 hours)
|
||||||
|
Priority: 🔴 High | Complexity: 73/100
|
||||||
|
|
||||||
|
Dependencies: ✅ #41, ✅ #42, ⏳ #43 (blocked)
|
||||||
|
Blocks: #46, #47, #52
|
||||||
|
|
||||||
|
Progress: ████████░░ 80% complete
|
||||||
|
|
||||||
|
Recent Activity:
|
||||||
|
- 2h ago: Status changed to in-progress
|
||||||
|
- 4h ago: Dependency #42 completed
|
||||||
|
- Yesterday: Task expanded with 3 subtasks
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4. **Intelligent Insights**
|
||||||
|
|
||||||
|
Based on task analysis:
|
||||||
|
- **Risk Assessment**: Complexity vs time remaining
|
||||||
|
- **Bottleneck Analysis**: Is this blocking critical work?
|
||||||
|
- **Recommendation**: Suggested approach or concerns
|
||||||
|
- **Similar Tasks**: How others completed similar work
|
||||||
|
|
||||||
|
### 5. **Action Suggestions**
|
||||||
|
|
||||||
|
Context-aware next steps:
|
||||||
|
- If blocked → Show how to unblock
|
||||||
|
- If complex → Suggest expansion
|
||||||
|
- If in-progress → Show completion checklist
|
||||||
|
- If done → Show dependent tasks ready to start
|
||||||
|
|
||||||
|
### 6. **Multi-Task View**
|
||||||
|
|
||||||
|
When showing multiple tasks:
|
||||||
|
- Common dependencies
|
||||||
|
- Optimal completion order
|
||||||
|
- Parallel work opportunities
|
||||||
|
- Combined complexity analysis`
|
||||||
|
);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Smart Workflow Slash Command
|
||||||
|
* Execute an intelligent workflow based on current project state and recent commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The smart-workflow slash command - Smart Workflow
|
||||||
|
*
|
||||||
|
* Execute an intelligent workflow based on current project state and recent commands.
|
||||||
|
*/
|
||||||
|
export const smartWorkflow = dynamicCommand(
|
||||||
|
'smart-workflow',
|
||||||
|
'Smart Workflow',
|
||||||
|
'[context]',
|
||||||
|
`Execute an intelligent workflow based on current project state and recent commands.
|
||||||
|
|
||||||
|
This command analyzes:
|
||||||
|
1. Recent commands you've run
|
||||||
|
2. Current project state
|
||||||
|
3. Time of day / day of week
|
||||||
|
4. Your working patterns
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Intelligent Workflow Selection
|
||||||
|
|
||||||
|
Based on context, I'll determine the best workflow:
|
||||||
|
|
||||||
|
### Context Analysis
|
||||||
|
- Previous command executed
|
||||||
|
- Current task states
|
||||||
|
- Unfinished work from last session
|
||||||
|
- Your typical patterns
|
||||||
|
|
||||||
|
### Smart Execution
|
||||||
|
|
||||||
|
If last command was:
|
||||||
|
- \`status\` → Likely starting work → Run daily standup
|
||||||
|
- \`complete\` → Task finished → Find next task
|
||||||
|
- \`list pending\` → Planning → Suggest sprint planning
|
||||||
|
- \`expand\` → Breaking down work → Show complexity analysis
|
||||||
|
- \`init\` → New project → Show onboarding workflow
|
||||||
|
|
||||||
|
If no recent commands:
|
||||||
|
- Morning? → Daily standup workflow
|
||||||
|
- Many pending tasks? → Sprint planning
|
||||||
|
- Tasks blocked? → Dependency resolution
|
||||||
|
- Friday? → Weekly review
|
||||||
|
|
||||||
|
### Workflow Composition
|
||||||
|
|
||||||
|
I'll chain appropriate commands:
|
||||||
|
1. Analyze current state
|
||||||
|
2. Execute primary workflow
|
||||||
|
3. Suggest follow-up actions
|
||||||
|
4. Prepare environment for coding
|
||||||
|
|
||||||
|
### Learning Mode
|
||||||
|
|
||||||
|
This command learns from your patterns:
|
||||||
|
- Track command sequences
|
||||||
|
- Note time preferences
|
||||||
|
- Remember common workflows
|
||||||
|
- Adapt to your style
|
||||||
|
|
||||||
|
Example flows detected:
|
||||||
|
- Morning: standup → next → start
|
||||||
|
- After lunch: status → continue task
|
||||||
|
- End of day: complete → commit → status`
|
||||||
|
);
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Sync README Slash Command
|
||||||
|
* Export tasks to README.md with professional formatting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync-readme slash command - Sync README
|
||||||
|
*
|
||||||
|
* Export tasks to README.md with professional formatting.
|
||||||
|
*/
|
||||||
|
export const syncReadme = dynamicCommand(
|
||||||
|
'sync-readme',
|
||||||
|
'Sync README',
|
||||||
|
'[options]',
|
||||||
|
`Export tasks to README.md with professional formatting.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Generate a well-formatted README with current task information.
|
||||||
|
|
||||||
|
## README Synchronization
|
||||||
|
|
||||||
|
Creates or updates README.md with beautifully formatted task information.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
Optional filters:
|
||||||
|
- "pending" → Only pending tasks
|
||||||
|
- "with-subtasks" → Include subtask details
|
||||||
|
- "by-priority" → Group by priority
|
||||||
|
- "sprint" → Current sprint only
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master sync-readme [--with-subtasks] [--status=<status>]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## README Generation
|
||||||
|
|
||||||
|
### 1. **Project Header**
|
||||||
|
\`\`\`markdown
|
||||||
|
# Project Name
|
||||||
|
|
||||||
|
## 📋 Task Progress
|
||||||
|
|
||||||
|
Last Updated: 2024-01-15 10:30 AM
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
- Total Tasks: 45
|
||||||
|
- Completed: 15 (33%)
|
||||||
|
- In Progress: 5 (11%)
|
||||||
|
- Pending: 25 (56%)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 2. **Task Sections**
|
||||||
|
Organized by status or priority:
|
||||||
|
- Progress indicators
|
||||||
|
- Task descriptions
|
||||||
|
- Dependencies noted
|
||||||
|
- Time estimates
|
||||||
|
|
||||||
|
### 3. **Visual Elements**
|
||||||
|
- Progress bars
|
||||||
|
- Status badges
|
||||||
|
- Priority indicators
|
||||||
|
- Completion checkmarks
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
1. **Intelligent Grouping**
|
||||||
|
- By feature area
|
||||||
|
- By sprint/milestone
|
||||||
|
- By assigned developer
|
||||||
|
- By priority
|
||||||
|
|
||||||
|
2. **Progress Tracking**
|
||||||
|
- Overall completion
|
||||||
|
- Sprint velocity
|
||||||
|
- Burndown indication
|
||||||
|
- Time tracking
|
||||||
|
|
||||||
|
3. **Formatting Options**
|
||||||
|
- GitHub-flavored markdown
|
||||||
|
- Task checkboxes
|
||||||
|
- Collapsible sections
|
||||||
|
- Table format available
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
\`\`\`markdown
|
||||||
|
## 🚀 Current Sprint
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
- [ ] 🔄 #5 **Implement user authentication** (60% complete)
|
||||||
|
- Dependencies: API design (#3 ✅)
|
||||||
|
- Subtasks: 4 (2 completed)
|
||||||
|
- Est: 8h / Spent: 5h
|
||||||
|
|
||||||
|
### Pending (High Priority)
|
||||||
|
- [ ] ⚡ #8 **Create dashboard UI**
|
||||||
|
- Blocked by: #5
|
||||||
|
- Complexity: High
|
||||||
|
- Est: 12h
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
Based on arguments:
|
||||||
|
- Include/exclude sections
|
||||||
|
- Detail level control
|
||||||
|
- Custom grouping
|
||||||
|
- Filter by criteria
|
||||||
|
|
||||||
|
## Post-Sync
|
||||||
|
|
||||||
|
After generation:
|
||||||
|
1. Show diff preview
|
||||||
|
2. Backup existing README
|
||||||
|
3. Write new content
|
||||||
|
4. Commit reminder
|
||||||
|
5. Update timestamp
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
Works well with:
|
||||||
|
- Git workflows
|
||||||
|
- CI/CD pipelines
|
||||||
|
- Project documentation
|
||||||
|
- Team updates
|
||||||
|
- Client reports`
|
||||||
|
);
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview TM Main Slash Command
|
||||||
|
* Task Master Command Reference - comprehensive command structure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tm-main slash command - Task Master Main
|
||||||
|
*
|
||||||
|
* Task Master Command Reference - comprehensive command structure.
|
||||||
|
*/
|
||||||
|
export const tmMain = staticCommand({
|
||||||
|
name: 'tm-main',
|
||||||
|
description: 'Task Master Main',
|
||||||
|
content: `# Task Master Command Reference
|
||||||
|
|
||||||
|
Comprehensive command structure for Task Master integration with Claude Code.
|
||||||
|
|
||||||
|
## Command Organization
|
||||||
|
|
||||||
|
Commands are organized hierarchically to match Task Master's CLI structure while providing enhanced Claude Code integration.
|
||||||
|
|
||||||
|
## Project Setup & Configuration
|
||||||
|
|
||||||
|
### \`/taskmaster:init\`
|
||||||
|
- \`init-project\` - Initialize new project (handles PRD files intelligently)
|
||||||
|
- \`init-project-quick\` - Quick setup with auto-confirmation (-y flag)
|
||||||
|
|
||||||
|
### \`/taskmaster:models\`
|
||||||
|
- \`view-models\` - View current AI model configuration
|
||||||
|
- \`setup-models\` - Interactive model configuration
|
||||||
|
- \`set-main\` - Set primary generation model
|
||||||
|
- \`set-research\` - Set research model
|
||||||
|
- \`set-fallback\` - Set fallback model
|
||||||
|
|
||||||
|
## Task Generation
|
||||||
|
|
||||||
|
### \`/taskmaster:parse-prd\`
|
||||||
|
- \`parse-prd\` - Generate tasks from PRD document
|
||||||
|
- \`parse-prd-with-research\` - Enhanced parsing with research mode
|
||||||
|
|
||||||
|
### \`/taskmaster:generate\`
|
||||||
|
- \`generate-tasks\` - Create individual task files from tasks.json
|
||||||
|
|
||||||
|
## Task Management
|
||||||
|
|
||||||
|
### \`/taskmaster:list\`
|
||||||
|
- \`list-tasks\` - Smart listing with natural language filters
|
||||||
|
- \`list-tasks-with-subtasks\` - Include subtasks in hierarchical view
|
||||||
|
- \`list-tasks-by-status\` - Filter by specific status
|
||||||
|
|
||||||
|
### \`/taskmaster:set-status\`
|
||||||
|
- \`to-pending\` - Reset task to pending
|
||||||
|
- \`to-in-progress\` - Start working on task
|
||||||
|
- \`to-done\` - Mark task complete
|
||||||
|
- \`to-review\` - Submit for review
|
||||||
|
- \`to-deferred\` - Defer task
|
||||||
|
- \`to-cancelled\` - Cancel task
|
||||||
|
|
||||||
|
### \`/taskmaster:sync-readme\`
|
||||||
|
- \`sync-readme\` - Export tasks to README.md with formatting
|
||||||
|
|
||||||
|
### \`/taskmaster:update\`
|
||||||
|
- \`update-task\` - Update tasks with natural language
|
||||||
|
- \`update-tasks-from-id\` - Update multiple tasks from a starting point
|
||||||
|
- \`update-single-task\` - Update specific task
|
||||||
|
|
||||||
|
### \`/taskmaster:add-task\`
|
||||||
|
- \`add-task\` - Add new task with AI assistance
|
||||||
|
|
||||||
|
### \`/taskmaster:remove-task\`
|
||||||
|
- \`remove-task\` - Remove task with confirmation
|
||||||
|
|
||||||
|
## Subtask Management
|
||||||
|
|
||||||
|
### \`/taskmaster:add-subtask\`
|
||||||
|
- \`add-subtask\` - Add new subtask to parent
|
||||||
|
- \`convert-task-to-subtask\` - Convert existing task to subtask
|
||||||
|
|
||||||
|
### \`/taskmaster:remove-subtask\`
|
||||||
|
- \`remove-subtask\` - Remove subtask (with optional conversion)
|
||||||
|
|
||||||
|
### \`/taskmaster:clear-subtasks\`
|
||||||
|
- \`clear-subtasks\` - Clear subtasks from specific task
|
||||||
|
- \`clear-all-subtasks\` - Clear all subtasks globally
|
||||||
|
|
||||||
|
## Task Analysis & Breakdown
|
||||||
|
|
||||||
|
### \`/taskmaster:analyze-complexity\`
|
||||||
|
- \`analyze-complexity\` - Analyze and generate expansion recommendations
|
||||||
|
|
||||||
|
### \`/taskmaster:complexity-report\`
|
||||||
|
- \`complexity-report\` - Display complexity analysis report
|
||||||
|
|
||||||
|
### \`/taskmaster:expand\`
|
||||||
|
- \`expand-task\` - Break down specific task
|
||||||
|
- \`expand-all-tasks\` - Expand all eligible tasks
|
||||||
|
- \`with-research\` - Enhanced expansion
|
||||||
|
|
||||||
|
## Task Navigation
|
||||||
|
|
||||||
|
### \`/taskmaster:next\`
|
||||||
|
- \`next-task\` - Intelligent next task recommendation
|
||||||
|
|
||||||
|
### \`/taskmaster:show\`
|
||||||
|
- \`show-task\` - Display detailed task information
|
||||||
|
|
||||||
|
### \`/taskmaster:status\`
|
||||||
|
- \`project-status\` - Comprehensive project dashboard
|
||||||
|
|
||||||
|
## Dependency Management
|
||||||
|
|
||||||
|
### \`/taskmaster:add-dependency\`
|
||||||
|
- \`add-dependency\` - Add task dependency
|
||||||
|
|
||||||
|
### \`/taskmaster:remove-dependency\`
|
||||||
|
- \`remove-dependency\` - Remove task dependency
|
||||||
|
|
||||||
|
### \`/taskmaster:validate-dependencies\`
|
||||||
|
- \`validate-dependencies\` - Check for dependency issues
|
||||||
|
|
||||||
|
### \`/taskmaster:fix-dependencies\`
|
||||||
|
- \`fix-dependencies\` - Automatically fix dependency problems
|
||||||
|
|
||||||
|
## Workflows & Automation
|
||||||
|
|
||||||
|
### \`/taskmaster:workflows\`
|
||||||
|
- \`smart-workflow\` - Context-aware intelligent workflow execution
|
||||||
|
- \`command-pipeline\` - Chain multiple commands together
|
||||||
|
- \`auto-implement-tasks\` - Advanced auto-implementation with code generation
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
### \`/taskmaster:utils\`
|
||||||
|
- \`analyze-project\` - Deep project analysis and insights
|
||||||
|
|
||||||
|
### \`/taskmaster:setup\`
|
||||||
|
- \`install-taskmaster\` - Comprehensive installation guide
|
||||||
|
- \`quick-install-taskmaster\` - One-line global installation
|
||||||
|
|
||||||
|
## Usage Patterns
|
||||||
|
|
||||||
|
### Natural Language
|
||||||
|
Most commands accept natural language arguments:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:add-task create user authentication system
|
||||||
|
/taskmaster:update mark all API tasks as high priority
|
||||||
|
/taskmaster:list show blocked tasks
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### ID-Based Commands
|
||||||
|
Commands requiring IDs intelligently parse from $ARGUMENTS:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:show 45
|
||||||
|
/taskmaster:expand 23
|
||||||
|
/taskmaster:set-status/to-done 67
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Smart Defaults
|
||||||
|
Commands provide intelligent defaults and suggestions based on context.`
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview To Done Slash Command
|
||||||
|
* Mark a task as completed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The to-done slash command - To Done
|
||||||
|
*
|
||||||
|
* Mark a task as completed.
|
||||||
|
*/
|
||||||
|
export const toDone = dynamicCommand(
|
||||||
|
'to-done',
|
||||||
|
'To Done',
|
||||||
|
'<task-id>',
|
||||||
|
`Mark a task as completed.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Completing a Task
|
||||||
|
|
||||||
|
This command validates task completion and updates project state intelligently.
|
||||||
|
|
||||||
|
## Pre-Completion Checks
|
||||||
|
|
||||||
|
1. Verify test strategy was followed
|
||||||
|
2. Check if all subtasks are complete
|
||||||
|
3. Validate acceptance criteria met
|
||||||
|
4. Ensure code is committed
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master set-status --id=$ARGUMENTS --status=done
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Post-Completion Actions
|
||||||
|
|
||||||
|
1. **Update Dependencies**
|
||||||
|
- Identify newly unblocked tasks
|
||||||
|
- Update sprint progress
|
||||||
|
- Recalculate project timeline
|
||||||
|
|
||||||
|
2. **Documentation**
|
||||||
|
- Generate completion summary
|
||||||
|
- Update CLAUDE.md with learnings
|
||||||
|
- Log implementation approach
|
||||||
|
|
||||||
|
3. **Next Steps**
|
||||||
|
- Show newly available tasks
|
||||||
|
- Suggest logical next task
|
||||||
|
- Update velocity metrics
|
||||||
|
|
||||||
|
## Celebration & Learning
|
||||||
|
|
||||||
|
- Show impact of completion
|
||||||
|
- Display unblocked work
|
||||||
|
- Recognize achievement
|
||||||
|
- Capture lessons learned`
|
||||||
|
);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview To In Progress Slash Command
|
||||||
|
* Start working on a task by setting its status to in-progress.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The to-in-progress slash command - To In Progress
|
||||||
|
*
|
||||||
|
* Start working on a task by setting its status to in-progress.
|
||||||
|
*/
|
||||||
|
export const toInProgress = dynamicCommand(
|
||||||
|
'to-in-progress',
|
||||||
|
'To In Progress',
|
||||||
|
'<task-id>',
|
||||||
|
`Start working on a task by setting its status to in-progress.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Starting Work on Task
|
||||||
|
|
||||||
|
This command does more than just change status - it prepares your environment for productive work.
|
||||||
|
|
||||||
|
## Pre-Start Checks
|
||||||
|
|
||||||
|
1. Verify dependencies are met
|
||||||
|
2. Check if another task is already in-progress
|
||||||
|
3. Ensure task details are complete
|
||||||
|
4. Validate test strategy exists
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master set-status --id=$ARGUMENTS --status=in-progress
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
After setting to in-progress:
|
||||||
|
1. Create/checkout appropriate git branch
|
||||||
|
2. Open relevant documentation
|
||||||
|
3. Set up test watchers if applicable
|
||||||
|
4. Display task details and acceptance criteria
|
||||||
|
5. Show similar completed tasks for reference
|
||||||
|
|
||||||
|
## Smart Suggestions
|
||||||
|
|
||||||
|
- Estimated completion time based on complexity
|
||||||
|
- Related files from similar tasks
|
||||||
|
- Potential blockers to watch for
|
||||||
|
- Recommended first steps`
|
||||||
|
);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview To Pending Slash Command
|
||||||
|
* Set a task's status to pending.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The to-pending slash command - To Pending
|
||||||
|
*
|
||||||
|
* Set a task's status to pending.
|
||||||
|
*/
|
||||||
|
export const toPending = dynamicCommand(
|
||||||
|
'to-pending',
|
||||||
|
'To Pending',
|
||||||
|
'<task-id>',
|
||||||
|
`Set a task's status to pending.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Setting Task to Pending
|
||||||
|
|
||||||
|
This moves a task back to the pending state, useful for:
|
||||||
|
- Resetting erroneously started tasks
|
||||||
|
- Deferring work that was prematurely begun
|
||||||
|
- Reorganizing sprint priorities
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master set-status --id=$ARGUMENTS --status=pending
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Before setting to pending:
|
||||||
|
- Warn if task is currently in-progress
|
||||||
|
- Check if this will block other tasks
|
||||||
|
- Suggest documenting why it's being reset
|
||||||
|
- Preserve any work already done
|
||||||
|
|
||||||
|
## Smart Actions
|
||||||
|
|
||||||
|
After setting to pending:
|
||||||
|
- Update sprint planning if needed
|
||||||
|
- Notify about freed resources
|
||||||
|
- Suggest priority reassessment
|
||||||
|
- Log the status change with context`
|
||||||
|
);
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Update Single Task Slash Command
|
||||||
|
* Update a single specific task with new information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The update-single-task slash command - Update Single Task
|
||||||
|
*
|
||||||
|
* Update a single specific task with new information.
|
||||||
|
*/
|
||||||
|
export const updateSingleTask = dynamicCommand(
|
||||||
|
'update-single-task',
|
||||||
|
'Update Single Task',
|
||||||
|
'<task-id> <changes>',
|
||||||
|
`Update a single specific task with new information.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse task ID and update details.
|
||||||
|
|
||||||
|
## Single Task Update
|
||||||
|
|
||||||
|
Precisely update one task with AI assistance to maintain consistency.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
Natural language updates:
|
||||||
|
- "5: add caching requirement"
|
||||||
|
- "update 5 to include error handling"
|
||||||
|
- "task 5 needs rate limiting"
|
||||||
|
- "5 change priority to high"
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master update-task --id=<id> --prompt="<context>"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Update Types
|
||||||
|
|
||||||
|
### 1. **Content Updates**
|
||||||
|
- Enhance description
|
||||||
|
- Add requirements
|
||||||
|
- Clarify details
|
||||||
|
- Update acceptance criteria
|
||||||
|
|
||||||
|
### 2. **Metadata Updates**
|
||||||
|
- Change priority
|
||||||
|
- Adjust time estimates
|
||||||
|
- Update complexity
|
||||||
|
- Modify dependencies
|
||||||
|
|
||||||
|
### 3. **Strategic Updates**
|
||||||
|
- Revise approach
|
||||||
|
- Change test strategy
|
||||||
|
- Update implementation notes
|
||||||
|
- Adjust subtask needs
|
||||||
|
|
||||||
|
## AI-Powered Updates
|
||||||
|
|
||||||
|
The AI:
|
||||||
|
1. **Understands Context**
|
||||||
|
- Reads current task state
|
||||||
|
- Identifies update intent
|
||||||
|
- Maintains consistency
|
||||||
|
- Preserves important info
|
||||||
|
|
||||||
|
2. **Applies Changes**
|
||||||
|
- Updates relevant fields
|
||||||
|
- Keeps style consistent
|
||||||
|
- Adds without removing
|
||||||
|
- Enhances clarity
|
||||||
|
|
||||||
|
3. **Validates Results**
|
||||||
|
- Checks coherence
|
||||||
|
- Verifies completeness
|
||||||
|
- Maintains relationships
|
||||||
|
- Suggests related updates
|
||||||
|
|
||||||
|
## Example Updates
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:update/single 5: add rate limiting
|
||||||
|
→ Updating Task #5: "Implement API endpoints"
|
||||||
|
|
||||||
|
Current: Basic CRUD endpoints
|
||||||
|
Adding: Rate limiting requirements
|
||||||
|
|
||||||
|
Updated sections:
|
||||||
|
✓ Description: Added rate limiting mention
|
||||||
|
✓ Details: Added specific limits (100/min)
|
||||||
|
✓ Test Strategy: Added rate limit tests
|
||||||
|
✓ Complexity: Increased from 5 to 6
|
||||||
|
✓ Time Estimate: Increased by 2 hours
|
||||||
|
|
||||||
|
Suggestion: Also update task #6 (API Gateway) for consistency?
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
1. **Incremental Updates**
|
||||||
|
- Adds without overwriting
|
||||||
|
- Preserves work history
|
||||||
|
- Tracks what changed
|
||||||
|
- Shows diff view
|
||||||
|
|
||||||
|
2. **Consistency Checks**
|
||||||
|
- Related task alignment
|
||||||
|
- Subtask compatibility
|
||||||
|
- Dependency validity
|
||||||
|
- Timeline impact
|
||||||
|
|
||||||
|
3. **Update History**
|
||||||
|
- Timestamp changes
|
||||||
|
- Track who/what updated
|
||||||
|
- Reason for update
|
||||||
|
- Previous versions
|
||||||
|
|
||||||
|
## Field-Specific Updates
|
||||||
|
|
||||||
|
Quick syntax for specific fields:
|
||||||
|
- "5 priority:high" → Update priority only
|
||||||
|
- "5 add-time:4h" → Add to time estimate
|
||||||
|
- "5 status:review" → Change status
|
||||||
|
- "5 depends:3,4" → Add dependencies
|
||||||
|
|
||||||
|
## Post-Update
|
||||||
|
|
||||||
|
- Show updated task
|
||||||
|
- Highlight changes
|
||||||
|
- Check related tasks
|
||||||
|
- Update suggestions
|
||||||
|
- Timeline adjustments`
|
||||||
|
);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Update Task Slash Command
|
||||||
|
* Update tasks with intelligent field detection and bulk operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The update-task slash command - Update Task
|
||||||
|
*
|
||||||
|
* Update tasks with intelligent field detection and bulk operations.
|
||||||
|
*/
|
||||||
|
export const updateTask = dynamicCommand(
|
||||||
|
'update-task',
|
||||||
|
'Update Task',
|
||||||
|
'<update-spec>',
|
||||||
|
`Update tasks with intelligent field detection and bulk operations.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Intelligent Task Updates
|
||||||
|
|
||||||
|
Parse arguments to determine update intent and execute smartly.
|
||||||
|
|
||||||
|
### 1. **Natural Language Processing**
|
||||||
|
|
||||||
|
Understand update requests like:
|
||||||
|
- "mark 23 as done" → Update status to done
|
||||||
|
- "increase priority of 45" → Set priority to high
|
||||||
|
- "add dependency on 12 to task 34" → Add dependency
|
||||||
|
- "tasks 20-25 need review" → Bulk status update
|
||||||
|
- "all API tasks high priority" → Pattern-based update
|
||||||
|
|
||||||
|
### 2. **Smart Field Detection**
|
||||||
|
|
||||||
|
Automatically detect what to update:
|
||||||
|
- Status keywords: done, complete, start, pause, review
|
||||||
|
- Priority changes: urgent, high, low, deprioritize
|
||||||
|
- Dependency updates: depends on, blocks, after
|
||||||
|
- Assignment: assign to, owner, responsible
|
||||||
|
- Time: estimate, spent, deadline
|
||||||
|
|
||||||
|
### 3. **Bulk Operations**
|
||||||
|
|
||||||
|
Support for multiple task updates:
|
||||||
|
\`\`\`
|
||||||
|
Examples:
|
||||||
|
- "complete tasks 12, 15, 18"
|
||||||
|
- "all pending auth tasks to in-progress"
|
||||||
|
- "increase priority for tasks blocking 45"
|
||||||
|
- "defer all documentation tasks"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4. **Contextual Validation**
|
||||||
|
|
||||||
|
Before updating, check:
|
||||||
|
- Status transitions are valid
|
||||||
|
- Dependencies don't create cycles
|
||||||
|
- Priority changes make sense
|
||||||
|
- Bulk updates won't break project flow
|
||||||
|
|
||||||
|
Show preview:
|
||||||
|
\`\`\`
|
||||||
|
Update Preview:
|
||||||
|
─────────────────
|
||||||
|
Tasks to update: #23, #24, #25
|
||||||
|
Change: status → in-progress
|
||||||
|
Impact: Will unblock tasks #30, #31
|
||||||
|
Warning: Task #24 has unmet dependencies
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 5. **Smart Suggestions**
|
||||||
|
|
||||||
|
Based on update:
|
||||||
|
- Completing task? → Show newly unblocked tasks
|
||||||
|
- Changing priority? → Show impact on sprint
|
||||||
|
- Adding dependency? → Check for conflicts
|
||||||
|
- Bulk update? → Show summary of changes
|
||||||
|
|
||||||
|
### 6. **Workflow Integration**
|
||||||
|
|
||||||
|
After updates:
|
||||||
|
- Auto-update dependent task states
|
||||||
|
- Trigger status recalculation
|
||||||
|
- Update sprint/milestone progress
|
||||||
|
- Log changes with context
|
||||||
|
|
||||||
|
Result: Flexible, intelligent task updates with safety checks.`
|
||||||
|
);
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Update Tasks From ID Slash Command
|
||||||
|
* Update multiple tasks starting from a specific ID.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The update-tasks-from-id slash command - Update Tasks From ID
|
||||||
|
*
|
||||||
|
* Update multiple tasks starting from a specific ID.
|
||||||
|
*/
|
||||||
|
export const updateTasksFromId = dynamicCommand(
|
||||||
|
'update-tasks-from-id',
|
||||||
|
'Update Tasks From ID',
|
||||||
|
'<from-id> <changes>',
|
||||||
|
`Update multiple tasks starting from a specific ID.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse starting task ID and update context.
|
||||||
|
|
||||||
|
## Bulk Task Updates
|
||||||
|
|
||||||
|
Update multiple related tasks based on new requirements or context changes.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
- "from 5: add security requirements"
|
||||||
|
- "5 onwards: update API endpoints"
|
||||||
|
- "starting at 5: change to use new framework"
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master update --from=<id> --prompt="<context>"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Update Process
|
||||||
|
|
||||||
|
### 1. **Task Selection**
|
||||||
|
Starting from specified ID:
|
||||||
|
- Include the task itself
|
||||||
|
- Include all dependent tasks
|
||||||
|
- Include related subtasks
|
||||||
|
- Smart boundary detection
|
||||||
|
|
||||||
|
### 2. **Context Application**
|
||||||
|
AI analyzes the update context and:
|
||||||
|
- Identifies what needs changing
|
||||||
|
- Maintains consistency
|
||||||
|
- Preserves completed work
|
||||||
|
- Updates related information
|
||||||
|
|
||||||
|
### 3. **Intelligent Updates**
|
||||||
|
- Modify descriptions appropriately
|
||||||
|
- Update test strategies
|
||||||
|
- Adjust time estimates
|
||||||
|
- Revise dependencies if needed
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
1. **Scope Detection**
|
||||||
|
- Find natural task groupings
|
||||||
|
- Identify related features
|
||||||
|
- Stop at logical boundaries
|
||||||
|
- Avoid over-updating
|
||||||
|
|
||||||
|
2. **Consistency Maintenance**
|
||||||
|
- Keep naming conventions
|
||||||
|
- Preserve relationships
|
||||||
|
- Update cross-references
|
||||||
|
- Maintain task flow
|
||||||
|
|
||||||
|
3. **Change Preview**
|
||||||
|
\`\`\`
|
||||||
|
Bulk Update Preview
|
||||||
|
━━━━━━━━━━━━━━━━━━
|
||||||
|
Starting from: Task #5
|
||||||
|
Tasks to update: 8 tasks + 12 subtasks
|
||||||
|
|
||||||
|
Context: "add security requirements"
|
||||||
|
|
||||||
|
Changes will include:
|
||||||
|
- Add security sections to descriptions
|
||||||
|
- Update test strategies for security
|
||||||
|
- Add security-related subtasks where needed
|
||||||
|
- Adjust time estimates (+20% average)
|
||||||
|
|
||||||
|
Continue? (y/n)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Example Updates
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:update-tasks-from-id 5: change database to PostgreSQL
|
||||||
|
→ Analyzing impact starting from task #5
|
||||||
|
→ Found 6 related tasks to update
|
||||||
|
→ Updates will maintain consistency
|
||||||
|
→ Preview changes? (y/n)
|
||||||
|
|
||||||
|
Applied updates:
|
||||||
|
✓ Task #5: Updated connection logic references
|
||||||
|
✓ Task #6: Changed migration approach
|
||||||
|
✓ Task #7: Updated query syntax notes
|
||||||
|
✓ Task #8: Revised testing strategy
|
||||||
|
✓ Task #9: Updated deployment steps
|
||||||
|
✓ Task #12: Changed backup procedures
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
- Preview all changes
|
||||||
|
- Selective confirmation
|
||||||
|
- Rollback capability
|
||||||
|
- Change logging
|
||||||
|
- Validation checks
|
||||||
|
|
||||||
|
## Post-Update
|
||||||
|
|
||||||
|
- Summary of changes
|
||||||
|
- Consistency verification
|
||||||
|
- Suggest review tasks
|
||||||
|
- Update timeline if needed`
|
||||||
|
);
|
||||||
171
packages/tm-profiles/src/slash-commands/commands/index.ts
Normal file
171
packages/tm-profiles/src/slash-commands/commands/index.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Slash Commands Index
|
||||||
|
* Exports all TaskMaster slash commands organized by operating mode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SlashCommand } from '../types.js';
|
||||||
|
|
||||||
|
// Solo commands (local file-based storage)
|
||||||
|
import {
|
||||||
|
parsePrd,
|
||||||
|
parsePrdWithResearch,
|
||||||
|
analyzeComplexity,
|
||||||
|
complexityReport,
|
||||||
|
expandTask,
|
||||||
|
expandAllTasks,
|
||||||
|
addTask,
|
||||||
|
addSubtask,
|
||||||
|
removeTask,
|
||||||
|
removeSubtask,
|
||||||
|
removeSubtasks,
|
||||||
|
removeAllSubtasks,
|
||||||
|
convertTaskToSubtask,
|
||||||
|
addDependency,
|
||||||
|
removeDependency,
|
||||||
|
fixDependencies,
|
||||||
|
validateDependencies,
|
||||||
|
setupModels,
|
||||||
|
viewModels,
|
||||||
|
installTaskmaster,
|
||||||
|
quickInstallTaskmaster,
|
||||||
|
toReview,
|
||||||
|
toDeferred,
|
||||||
|
toCancelled,
|
||||||
|
initProject,
|
||||||
|
initProjectQuick
|
||||||
|
} from './solo/index.js';
|
||||||
|
|
||||||
|
// Team commands (API-based storage via Hamster)
|
||||||
|
import { goham } from './team/index.js';
|
||||||
|
|
||||||
|
// Common commands (work in both modes)
|
||||||
|
import {
|
||||||
|
showTask,
|
||||||
|
listTasks,
|
||||||
|
listTasksWithSubtasks,
|
||||||
|
listTasksByStatus,
|
||||||
|
projectStatus,
|
||||||
|
nextTask,
|
||||||
|
help,
|
||||||
|
toDone,
|
||||||
|
toPending,
|
||||||
|
toInProgress,
|
||||||
|
updateTask,
|
||||||
|
updateSingleTask,
|
||||||
|
updateTasksFromId,
|
||||||
|
tmMain,
|
||||||
|
smartWorkflow,
|
||||||
|
learn,
|
||||||
|
commandPipeline,
|
||||||
|
autoImplementTasks,
|
||||||
|
analyzeProject,
|
||||||
|
syncReadme
|
||||||
|
} from './common/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All TaskMaster slash commands
|
||||||
|
* Add new commands here to have them automatically distributed to all profiles.
|
||||||
|
*/
|
||||||
|
export const allCommands: SlashCommand[] = [
|
||||||
|
// Solo commands
|
||||||
|
parsePrd,
|
||||||
|
parsePrdWithResearch,
|
||||||
|
analyzeComplexity,
|
||||||
|
complexityReport,
|
||||||
|
expandTask,
|
||||||
|
expandAllTasks,
|
||||||
|
addTask,
|
||||||
|
addSubtask,
|
||||||
|
removeTask,
|
||||||
|
removeSubtask,
|
||||||
|
removeSubtasks,
|
||||||
|
removeAllSubtasks,
|
||||||
|
convertTaskToSubtask,
|
||||||
|
addDependency,
|
||||||
|
removeDependency,
|
||||||
|
fixDependencies,
|
||||||
|
validateDependencies,
|
||||||
|
setupModels,
|
||||||
|
viewModels,
|
||||||
|
installTaskmaster,
|
||||||
|
quickInstallTaskmaster,
|
||||||
|
toReview,
|
||||||
|
toDeferred,
|
||||||
|
toCancelled,
|
||||||
|
initProject,
|
||||||
|
initProjectQuick,
|
||||||
|
|
||||||
|
// Team commands
|
||||||
|
goham,
|
||||||
|
|
||||||
|
// Common commands
|
||||||
|
showTask,
|
||||||
|
listTasks,
|
||||||
|
listTasksWithSubtasks,
|
||||||
|
listTasksByStatus,
|
||||||
|
projectStatus,
|
||||||
|
nextTask,
|
||||||
|
help,
|
||||||
|
toDone,
|
||||||
|
toPending,
|
||||||
|
toInProgress,
|
||||||
|
updateTask,
|
||||||
|
updateSingleTask,
|
||||||
|
updateTasksFromId,
|
||||||
|
tmMain,
|
||||||
|
smartWorkflow,
|
||||||
|
learn,
|
||||||
|
commandPipeline,
|
||||||
|
autoImplementTasks,
|
||||||
|
analyzeProject,
|
||||||
|
syncReadme
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter commands by operating mode
|
||||||
|
*
|
||||||
|
* Both modes include common commands:
|
||||||
|
* - Solo mode: solo + common commands
|
||||||
|
* - Team mode: team + common commands
|
||||||
|
*
|
||||||
|
* @param commands - Array of slash commands to filter
|
||||||
|
* @param mode - Operating mode ('solo' or 'team')
|
||||||
|
* @returns Filtered array of commands for the specified mode
|
||||||
|
*/
|
||||||
|
export function filterCommandsByMode(
|
||||||
|
commands: SlashCommand[],
|
||||||
|
mode: 'solo' | 'team'
|
||||||
|
): SlashCommand[] {
|
||||||
|
if (mode === 'team') {
|
||||||
|
// Team mode: team + common commands
|
||||||
|
return commands.filter(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.metadata.mode === 'team' ||
|
||||||
|
cmd.metadata.mode === 'common' ||
|
||||||
|
!cmd.metadata.mode // backward compat: no mode = common
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Solo mode: solo + common commands
|
||||||
|
return commands.filter(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.metadata.mode === 'solo' ||
|
||||||
|
cmd.metadata.mode === 'common' ||
|
||||||
|
!cmd.metadata.mode // backward compat: no mode = common
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Commands for solo mode (solo + common) */
|
||||||
|
export const soloCommands = filterCommandsByMode(allCommands, 'solo');
|
||||||
|
|
||||||
|
/** Commands for team mode (team + common) */
|
||||||
|
export const teamCommands = filterCommandsByMode(allCommands, 'team');
|
||||||
|
|
||||||
|
/** Commands that work in both modes */
|
||||||
|
export const commonCommands = allCommands.filter(
|
||||||
|
(cmd) => cmd.metadata.mode === 'common' || !cmd.metadata.mode
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-export from subdirectories for direct access
|
||||||
|
export * from './solo/index.js';
|
||||||
|
export * from './team/index.js';
|
||||||
|
export * from './common/index.js';
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit tests for mode-based command filtering
|
||||||
|
*
|
||||||
|
* Tests the filterCommandsByMode function and command mode categorization:
|
||||||
|
* - Solo mode: Returns solo + common commands
|
||||||
|
* - Team mode: Returns team + common commands
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
filterCommandsByMode,
|
||||||
|
allCommands,
|
||||||
|
soloCommands,
|
||||||
|
teamCommands,
|
||||||
|
commonCommands
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
|
describe('Mode-based Command Filtering', () => {
|
||||||
|
describe('filterCommandsByMode', () => {
|
||||||
|
describe('solo mode filtering', () => {
|
||||||
|
it('returns solo and common commands for solo mode', () => {
|
||||||
|
// Act
|
||||||
|
const filtered = filterCommandsByMode(allCommands, 'solo');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (const cmd of filtered) {
|
||||||
|
const mode = cmd.metadata.mode;
|
||||||
|
expect(
|
||||||
|
mode === 'solo' || mode === 'common' || mode === undefined
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes team-only commands from solo mode', () => {
|
||||||
|
// Act
|
||||||
|
const filtered = filterCommandsByMode(allCommands, 'solo');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const teamOnlyCommands = filtered.filter(
|
||||||
|
(cmd) => cmd.metadata.mode === 'team'
|
||||||
|
);
|
||||||
|
expect(teamOnlyCommands).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes commands without explicit mode (backward compat)', () => {
|
||||||
|
// Act
|
||||||
|
const filtered = filterCommandsByMode(allCommands, 'solo');
|
||||||
|
|
||||||
|
// Assert - commands without mode should be included
|
||||||
|
// This is a backward compat check - commands with undefined mode are treated as common
|
||||||
|
expect(filtered.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('team mode filtering', () => {
|
||||||
|
it('returns team and common commands for team mode', () => {
|
||||||
|
// Act
|
||||||
|
const filtered = filterCommandsByMode(allCommands, 'team');
|
||||||
|
|
||||||
|
// Assert - team mode includes team + common commands
|
||||||
|
for (const cmd of filtered) {
|
||||||
|
const mode = cmd.metadata.mode;
|
||||||
|
expect(
|
||||||
|
mode === 'team' || mode === 'common' || mode === undefined
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes solo commands from team mode', () => {
|
||||||
|
// Act
|
||||||
|
const filtered = filterCommandsByMode(allCommands, 'team');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const soloInTeam = filtered.filter(
|
||||||
|
(cmd) => cmd.metadata.mode === 'solo'
|
||||||
|
);
|
||||||
|
expect(soloInTeam).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes common commands in team mode', () => {
|
||||||
|
// Act
|
||||||
|
const filtered = filterCommandsByMode(allCommands, 'team');
|
||||||
|
|
||||||
|
// Assert - team mode includes common commands
|
||||||
|
const commonInTeam = filtered.filter(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.metadata.mode === 'common' || cmd.metadata.mode === undefined
|
||||||
|
);
|
||||||
|
expect(commonInTeam.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Pre-filtered exports', () => {
|
||||||
|
describe('soloCommands export', () => {
|
||||||
|
it('matches filterCommandsByMode(allCommands, "solo")', () => {
|
||||||
|
// Act
|
||||||
|
const expectedSolo = filterCommandsByMode(allCommands, 'solo');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(soloCommands).toHaveLength(expectedSolo.length);
|
||||||
|
const soloNames = soloCommands.map((c) => c.metadata.name);
|
||||||
|
const expectedNames = expectedSolo.map((c) => c.metadata.name);
|
||||||
|
expect(soloNames.sort()).toEqual(expectedNames.sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains known solo commands', () => {
|
||||||
|
// Assert - verify some known solo commands are present
|
||||||
|
const names = soloCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).toContain('parse-prd');
|
||||||
|
expect(names).toContain('add-task');
|
||||||
|
expect(names).toContain('expand-task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains common commands', () => {
|
||||||
|
// Assert - verify common commands are included in solo
|
||||||
|
const names = soloCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).toContain('show-task');
|
||||||
|
expect(names).toContain('list-tasks');
|
||||||
|
expect(names).toContain('to-done');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain team commands', () => {
|
||||||
|
// Assert
|
||||||
|
const names = soloCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).not.toContain('goham');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('teamCommands export', () => {
|
||||||
|
it('matches filterCommandsByMode(allCommands, "team")', () => {
|
||||||
|
// Act
|
||||||
|
const expectedTeam = filterCommandsByMode(allCommands, 'team');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(teamCommands).toHaveLength(expectedTeam.length);
|
||||||
|
const teamNames = teamCommands.map((c) => c.metadata.name);
|
||||||
|
const expectedNames = expectedTeam.map((c) => c.metadata.name);
|
||||||
|
expect(teamNames.sort()).toEqual(expectedNames.sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains goham command', () => {
|
||||||
|
// Assert
|
||||||
|
const names = teamCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).toContain('goham');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain solo commands', () => {
|
||||||
|
// Assert
|
||||||
|
const names = teamCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).not.toContain('parse-prd');
|
||||||
|
expect(names).not.toContain('add-task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains common commands', () => {
|
||||||
|
// Assert - team mode includes common commands
|
||||||
|
const names = teamCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).toContain('show-task');
|
||||||
|
expect(names).toContain('list-tasks');
|
||||||
|
expect(names).toContain('help');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('commonCommands export', () => {
|
||||||
|
it('contains only commands with mode=common or undefined', () => {
|
||||||
|
// Assert
|
||||||
|
for (const cmd of commonCommands) {
|
||||||
|
const mode = cmd.metadata.mode;
|
||||||
|
expect(mode === 'common' || mode === undefined).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains known common commands', () => {
|
||||||
|
// Assert
|
||||||
|
const names = commonCommands.map((c) => c.metadata.name);
|
||||||
|
expect(names).toContain('show-task');
|
||||||
|
expect(names).toContain('list-tasks');
|
||||||
|
expect(names).toContain('next-task');
|
||||||
|
expect(names).toContain('help');
|
||||||
|
expect(names).toContain('to-done');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Command mode categorization', () => {
|
||||||
|
it('all commands have valid mode property', () => {
|
||||||
|
// Assert
|
||||||
|
for (const cmd of allCommands) {
|
||||||
|
const mode = cmd.metadata.mode;
|
||||||
|
// Mode should be 'solo', 'team', 'common', or undefined
|
||||||
|
expect(
|
||||||
|
mode === 'solo' ||
|
||||||
|
mode === 'team' ||
|
||||||
|
mode === 'common' ||
|
||||||
|
mode === undefined
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goham is the only team command', () => {
|
||||||
|
// Act
|
||||||
|
const teamOnly = allCommands.filter(
|
||||||
|
(cmd) => cmd.metadata.mode === 'team'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(teamOnly).toHaveLength(1);
|
||||||
|
expect(teamOnly[0].metadata.name).toBe('goham');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('solo commands are tagged correctly', () => {
|
||||||
|
// Known solo commands
|
||||||
|
const knownSolo = [
|
||||||
|
'parse-prd',
|
||||||
|
'parse-prd-with-research',
|
||||||
|
'analyze-complexity',
|
||||||
|
'complexity-report',
|
||||||
|
'expand-task',
|
||||||
|
'expand-all-tasks',
|
||||||
|
'add-task',
|
||||||
|
'add-subtask',
|
||||||
|
'remove-task',
|
||||||
|
'remove-subtask',
|
||||||
|
'remove-subtasks',
|
||||||
|
'remove-all-subtasks',
|
||||||
|
'convert-task-to-subtask',
|
||||||
|
'add-dependency',
|
||||||
|
'remove-dependency',
|
||||||
|
'fix-dependencies',
|
||||||
|
'validate-dependencies',
|
||||||
|
'setup-models',
|
||||||
|
'view-models',
|
||||||
|
'install-taskmaster',
|
||||||
|
'quick-install-taskmaster',
|
||||||
|
'to-review',
|
||||||
|
'to-deferred',
|
||||||
|
'to-cancelled',
|
||||||
|
'init-project',
|
||||||
|
'init-project-quick'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const name of knownSolo) {
|
||||||
|
const cmd = allCommands.find((c) => c.metadata.name === name);
|
||||||
|
expect(cmd).toBeDefined();
|
||||||
|
expect(cmd?.metadata.mode).toBe('solo');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('common commands are tagged correctly or have undefined mode', () => {
|
||||||
|
// Known common commands - they should be 'common' or undefined (backward compat)
|
||||||
|
const knownCommon = [
|
||||||
|
'show-task',
|
||||||
|
'list-tasks',
|
||||||
|
'list-tasks-with-subtasks',
|
||||||
|
'list-tasks-by-status',
|
||||||
|
'project-status',
|
||||||
|
'next-task',
|
||||||
|
'help',
|
||||||
|
'to-done',
|
||||||
|
'to-pending',
|
||||||
|
'to-in-progress',
|
||||||
|
'update-task',
|
||||||
|
'update-single-task',
|
||||||
|
'update-tasks-from-id',
|
||||||
|
'tm-main',
|
||||||
|
'smart-workflow',
|
||||||
|
'learn',
|
||||||
|
'command-pipeline',
|
||||||
|
'auto-implement-tasks',
|
||||||
|
'analyze-project',
|
||||||
|
'sync-readme'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const name of knownCommon) {
|
||||||
|
const cmd = allCommands.find((c) => c.metadata.name === name);
|
||||||
|
expect(cmd).toBeDefined();
|
||||||
|
// Common commands can be explicitly 'common' or undefined (backward compat)
|
||||||
|
const mode = cmd?.metadata.mode;
|
||||||
|
expect(mode === 'common' || mode === undefined).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('filtering empty array returns empty array', () => {
|
||||||
|
// Act
|
||||||
|
const soloFiltered = filterCommandsByMode([], 'solo');
|
||||||
|
const teamFiltered = filterCommandsByMode([], 'team');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(soloFiltered).toHaveLength(0);
|
||||||
|
expect(teamFiltered).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('total commands equals solo + team + common (no overlap)', () => {
|
||||||
|
// This verifies our categorization is complete and non-overlapping
|
||||||
|
const soloCount = allCommands.filter(
|
||||||
|
(cmd) => cmd.metadata.mode === 'solo'
|
||||||
|
).length;
|
||||||
|
const teamCount = allCommands.filter(
|
||||||
|
(cmd) => cmd.metadata.mode === 'team'
|
||||||
|
).length;
|
||||||
|
const commonCount = allCommands.filter(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.metadata.mode === 'common' || cmd.metadata.mode === undefined
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(soloCount + teamCount + commonCount).toBe(allCommands.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Add Dependency Slash Command
|
||||||
|
* Add a dependency between tasks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The add-dependency slash command - Add Dependency
|
||||||
|
*
|
||||||
|
* Add a dependency between tasks.
|
||||||
|
*/
|
||||||
|
export const addDependency = dynamicCommand(
|
||||||
|
'add-dependency',
|
||||||
|
'Add Dependency',
|
||||||
|
'<task-id> <depends-on-id>',
|
||||||
|
`Add a dependency between tasks.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse the task IDs to establish dependency relationship.
|
||||||
|
|
||||||
|
## Adding Dependencies
|
||||||
|
|
||||||
|
Creates a dependency where one task must be completed before another can start.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
Parse natural language or IDs:
|
||||||
|
- "make 5 depend on 3" → task 5 depends on task 3
|
||||||
|
- "5 needs 3" → task 5 depends on task 3
|
||||||
|
- "5 3" → task 5 depends on task 3
|
||||||
|
- "5 after 3" → task 5 depends on task 3
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master add-dependency --id=<task-id> --depends-on=<dependency-id>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Before adding:
|
||||||
|
1. **Verify both tasks exist**
|
||||||
|
2. **Check for circular dependencies**
|
||||||
|
3. **Ensure dependency makes logical sense**
|
||||||
|
4. **Warn if creating complex chains**
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
- Detect if dependency already exists
|
||||||
|
- Suggest related dependencies
|
||||||
|
- Show impact on task flow
|
||||||
|
- Update task priorities if needed
|
||||||
|
|
||||||
|
## Post-Addition
|
||||||
|
|
||||||
|
After adding dependency:
|
||||||
|
1. Show updated dependency graph
|
||||||
|
2. Identify any newly blocked tasks
|
||||||
|
3. Suggest task order changes
|
||||||
|
4. Update project timeline
|
||||||
|
|
||||||
|
## Example Flows
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:add-dependency 5 needs 3
|
||||||
|
→ Task #5 now depends on Task #3
|
||||||
|
→ Task #5 is now blocked until #3 completes
|
||||||
|
→ Suggested: Also consider if #5 needs #4
|
||||||
|
\`\`\``,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Add Subtask Slash Command
|
||||||
|
* Add a subtask to a parent task.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The add-subtask slash command - Add Subtask
|
||||||
|
*
|
||||||
|
* Add a subtask to a parent task.
|
||||||
|
*/
|
||||||
|
export const addSubtask = dynamicCommand(
|
||||||
|
'add-subtask',
|
||||||
|
'Add Subtask',
|
||||||
|
'<parent-id> <title>',
|
||||||
|
`Add a subtask to a parent task.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse arguments to create a new subtask or convert existing task.
|
||||||
|
|
||||||
|
## Adding Subtasks
|
||||||
|
|
||||||
|
Creates subtasks to break down complex parent tasks into manageable pieces.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
Flexible natural language:
|
||||||
|
- "add subtask to 5: implement login form"
|
||||||
|
- "break down 5 with: setup, implement, test"
|
||||||
|
- "subtask for 5: handle edge cases"
|
||||||
|
- "5: validate user input" → adds subtask to task 5
|
||||||
|
|
||||||
|
## Execution Modes
|
||||||
|
|
||||||
|
### 1. Create New Subtask
|
||||||
|
\`\`\`bash
|
||||||
|
task-master add-subtask --parent=<id> --title="<title>" --description="<desc>"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 2. Convert Existing Task
|
||||||
|
\`\`\`bash
|
||||||
|
task-master add-subtask --parent=<id> --task-id=<existing-id>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
1. **Automatic Subtask Generation**
|
||||||
|
- If title contains "and" or commas, create multiple
|
||||||
|
- Suggest common subtask patterns
|
||||||
|
- Inherit parent's context
|
||||||
|
|
||||||
|
2. **Intelligent Defaults**
|
||||||
|
- Priority based on parent
|
||||||
|
- Appropriate time estimates
|
||||||
|
- Logical dependencies between subtasks
|
||||||
|
|
||||||
|
3. **Validation**
|
||||||
|
- Check parent task complexity
|
||||||
|
- Warn if too many subtasks
|
||||||
|
- Ensure subtask makes sense
|
||||||
|
|
||||||
|
## Creation Process
|
||||||
|
|
||||||
|
1. Parse parent task context
|
||||||
|
2. Generate subtask with ID like "5.1"
|
||||||
|
3. Set appropriate defaults
|
||||||
|
4. Link to parent task
|
||||||
|
5. Update parent's time estimate
|
||||||
|
|
||||||
|
## Example Flows
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:add-subtask to 5: implement user authentication
|
||||||
|
→ Created subtask #5.1: "implement user authentication"
|
||||||
|
→ Parent task #5 now has 1 subtask
|
||||||
|
→ Suggested next subtasks: tests, documentation
|
||||||
|
|
||||||
|
/taskmaster:add-subtask 5: setup, implement, test
|
||||||
|
→ Created 3 subtasks:
|
||||||
|
#5.1: setup
|
||||||
|
#5.2: implement
|
||||||
|
#5.3: test
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Post-Creation
|
||||||
|
|
||||||
|
- Show updated task hierarchy
|
||||||
|
- Suggest logical next subtasks
|
||||||
|
- Update complexity estimates
|
||||||
|
- Recommend subtask order`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Add Task Slash Command
|
||||||
|
* Add new tasks with intelligent parsing and context awareness.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The add-task slash command - Add Task
|
||||||
|
*
|
||||||
|
* Add new tasks with intelligent parsing and context awareness.
|
||||||
|
*/
|
||||||
|
export const addTask = dynamicCommand(
|
||||||
|
'add-task',
|
||||||
|
'Add Task',
|
||||||
|
'<description>',
|
||||||
|
`Add new tasks with intelligent parsing and context awareness.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
## Smart Task Addition
|
||||||
|
|
||||||
|
Parse natural language to create well-structured tasks.
|
||||||
|
|
||||||
|
### 1. **Input Understanding**
|
||||||
|
|
||||||
|
I'll intelligently parse your request:
|
||||||
|
- Natural language → Structured task
|
||||||
|
- Detect priority from keywords (urgent, ASAP, important)
|
||||||
|
- Infer dependencies from context
|
||||||
|
- Suggest complexity based on description
|
||||||
|
- Determine task type (feature, bug, refactor, test, docs)
|
||||||
|
|
||||||
|
### 2. **Smart Parsing Examples**
|
||||||
|
|
||||||
|
**"Add urgent task to fix login bug"**
|
||||||
|
→ Title: Fix login bug
|
||||||
|
→ Priority: high
|
||||||
|
→ Type: bug
|
||||||
|
→ Suggested complexity: medium
|
||||||
|
|
||||||
|
**"Create task for API documentation after task 23 is done"**
|
||||||
|
→ Title: API documentation
|
||||||
|
→ Dependencies: [23]
|
||||||
|
→ Type: documentation
|
||||||
|
→ Priority: medium
|
||||||
|
|
||||||
|
**"Need to refactor auth module - depends on 12 and 15, high complexity"**
|
||||||
|
→ Title: Refactor auth module
|
||||||
|
→ Dependencies: [12, 15]
|
||||||
|
→ Complexity: high
|
||||||
|
→ Type: refactor
|
||||||
|
|
||||||
|
### 3. **Context Enhancement**
|
||||||
|
|
||||||
|
Based on current project state:
|
||||||
|
- Suggest related existing tasks
|
||||||
|
- Warn about potential conflicts
|
||||||
|
- Recommend dependencies
|
||||||
|
- Propose subtasks if complex
|
||||||
|
|
||||||
|
### 4. **Interactive Refinement**
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
Task Preview:
|
||||||
|
─────────────
|
||||||
|
Title: [Extracted title]
|
||||||
|
Priority: [Inferred priority]
|
||||||
|
Dependencies: [Detected dependencies]
|
||||||
|
Complexity: [Estimated complexity]
|
||||||
|
|
||||||
|
Suggestions:
|
||||||
|
- Similar task #34 exists, consider as dependency?
|
||||||
|
- This seems complex, break into subtasks?
|
||||||
|
- Tasks #45-47 work on same module
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 5. **Validation & Creation**
|
||||||
|
|
||||||
|
Before creating:
|
||||||
|
- Validate dependencies exist
|
||||||
|
- Check for duplicates
|
||||||
|
- Ensure logical ordering
|
||||||
|
- Verify task completeness
|
||||||
|
|
||||||
|
### 6. **Smart Defaults**
|
||||||
|
|
||||||
|
Intelligent defaults based on:
|
||||||
|
- Task type patterns
|
||||||
|
- Team conventions
|
||||||
|
- Historical data
|
||||||
|
- Current sprint/phase
|
||||||
|
|
||||||
|
Result: High-quality tasks from minimal input.`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Analyze Complexity Slash Command
|
||||||
|
* Analyze task complexity and generate expansion recommendations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The analyze-complexity slash command - Analyze Complexity
|
||||||
|
*
|
||||||
|
* Analyze task complexity and generate expansion recommendations.
|
||||||
|
*/
|
||||||
|
export const analyzeComplexity = dynamicCommand(
|
||||||
|
'analyze-complexity',
|
||||||
|
'Analyze Complexity',
|
||||||
|
'[options]',
|
||||||
|
`Analyze task complexity and generate expansion recommendations.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Perform deep analysis of task complexity across the project.
|
||||||
|
|
||||||
|
## Complexity Analysis
|
||||||
|
|
||||||
|
Uses AI to analyze tasks and recommend which ones need breakdown.
|
||||||
|
|
||||||
|
## Execution Options
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master analyze-complexity [--research] [--threshold=5]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Analysis Parameters
|
||||||
|
|
||||||
|
- \`--research\` → Use research AI for deeper analysis
|
||||||
|
- \`--threshold=5\` → Only flag tasks above complexity 5
|
||||||
|
- Default: Analyze all pending tasks
|
||||||
|
|
||||||
|
## Analysis Process
|
||||||
|
|
||||||
|
### 1. **Task Evaluation**
|
||||||
|
For each task, AI evaluates:
|
||||||
|
- Technical complexity
|
||||||
|
- Time requirements
|
||||||
|
- Dependency complexity
|
||||||
|
- Risk factors
|
||||||
|
- Knowledge requirements
|
||||||
|
|
||||||
|
### 2. **Complexity Scoring**
|
||||||
|
Assigns score 1-10 based on:
|
||||||
|
- Implementation difficulty
|
||||||
|
- Integration challenges
|
||||||
|
- Testing requirements
|
||||||
|
- Unknown factors
|
||||||
|
- Technical debt risk
|
||||||
|
|
||||||
|
### 3. **Recommendations**
|
||||||
|
For complex tasks:
|
||||||
|
- Suggest expansion approach
|
||||||
|
- Recommend subtask breakdown
|
||||||
|
- Identify risk areas
|
||||||
|
- Propose mitigation strategies
|
||||||
|
|
||||||
|
## Smart Analysis Features
|
||||||
|
|
||||||
|
1. **Pattern Recognition**
|
||||||
|
- Similar task comparisons
|
||||||
|
- Historical complexity accuracy
|
||||||
|
- Team velocity consideration
|
||||||
|
- Technology stack factors
|
||||||
|
|
||||||
|
2. **Contextual Factors**
|
||||||
|
- Team expertise
|
||||||
|
- Available resources
|
||||||
|
- Timeline constraints
|
||||||
|
- Business criticality
|
||||||
|
|
||||||
|
3. **Risk Assessment**
|
||||||
|
- Technical risks
|
||||||
|
- Timeline risks
|
||||||
|
- Dependency risks
|
||||||
|
- Knowledge gaps
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Task Complexity Analysis Report
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
High Complexity Tasks (>7):
|
||||||
|
📍 #5 "Implement real-time sync" - Score: 9/10
|
||||||
|
Factors: WebSocket complexity, state management, conflict resolution
|
||||||
|
Recommendation: Expand into 5-7 subtasks
|
||||||
|
Risks: Performance, data consistency
|
||||||
|
|
||||||
|
📍 #12 "Migrate database schema" - Score: 8/10
|
||||||
|
Factors: Data migration, zero downtime, rollback strategy
|
||||||
|
Recommendation: Expand into 4-5 subtasks
|
||||||
|
Risks: Data loss, downtime
|
||||||
|
|
||||||
|
Medium Complexity Tasks (5-7):
|
||||||
|
📍 #23 "Add export functionality" - Score: 6/10
|
||||||
|
Consider expansion if timeline tight
|
||||||
|
|
||||||
|
Low Complexity Tasks (<5):
|
||||||
|
✅ 15 tasks - No expansion needed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
- Expand immediately: 2 tasks
|
||||||
|
- Consider expanding: 5 tasks
|
||||||
|
- Keep as-is: 15 tasks
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Actionable Output
|
||||||
|
|
||||||
|
For each high-complexity task:
|
||||||
|
1. Complexity score with reasoning
|
||||||
|
2. Specific expansion suggestions
|
||||||
|
3. Risk mitigation approaches
|
||||||
|
4. Recommended subtask structure
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
Results are:
|
||||||
|
- Saved to \`.taskmaster/reports/complexity-analysis.md\`
|
||||||
|
- Used by expand command
|
||||||
|
- Inform sprint planning
|
||||||
|
- Guide resource allocation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After analysis:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:expand 5 # Expand specific task
|
||||||
|
/taskmaster:expand-all # Expand all recommended
|
||||||
|
/taskmaster:complexity-report # View detailed report
|
||||||
|
\`\`\``,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Complexity Report Slash Command
|
||||||
|
* Display the task complexity analysis report.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The complexity-report slash command - Complexity Report
|
||||||
|
*
|
||||||
|
* Display the task complexity analysis report.
|
||||||
|
*/
|
||||||
|
export const complexityReport = dynamicCommand(
|
||||||
|
'complexity-report',
|
||||||
|
'Complexity Report',
|
||||||
|
'[--file=<path>]',
|
||||||
|
`Display the task complexity analysis report.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
View the detailed complexity analysis generated by analyze-complexity command.
|
||||||
|
|
||||||
|
## Viewing Complexity Report
|
||||||
|
|
||||||
|
Shows comprehensive task complexity analysis with actionable insights.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master complexity-report [--file=<path>]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Report Location
|
||||||
|
|
||||||
|
Default: \`.taskmaster/reports/complexity-analysis.md\`
|
||||||
|
Custom: Specify with --file parameter
|
||||||
|
|
||||||
|
## Report Contents
|
||||||
|
|
||||||
|
### 1. **Executive Summary**
|
||||||
|
\`\`\`
|
||||||
|
Complexity Analysis Summary
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Analysis Date: 2024-01-15
|
||||||
|
Tasks Analyzed: 32
|
||||||
|
High Complexity: 5 (16%)
|
||||||
|
Medium Complexity: 12 (37%)
|
||||||
|
Low Complexity: 15 (47%)
|
||||||
|
|
||||||
|
Critical Findings:
|
||||||
|
- 5 tasks need immediate expansion
|
||||||
|
- 3 tasks have high technical risk
|
||||||
|
- 2 tasks block critical path
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 2. **Detailed Task Analysis**
|
||||||
|
For each complex task:
|
||||||
|
- Complexity score breakdown
|
||||||
|
- Contributing factors
|
||||||
|
- Specific risks identified
|
||||||
|
- Expansion recommendations
|
||||||
|
- Similar completed tasks
|
||||||
|
|
||||||
|
### 3. **Risk Matrix**
|
||||||
|
Visual representation:
|
||||||
|
\`\`\`
|
||||||
|
Risk vs Complexity Matrix
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
High Risk | #5(9) #12(8) | #23(6)
|
||||||
|
Med Risk | #34(7) | #45(5) #67(5)
|
||||||
|
Low Risk | #78(8) | [15 tasks]
|
||||||
|
| High Complex | Med Complex
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4. **Recommendations**
|
||||||
|
|
||||||
|
**Immediate Actions:**
|
||||||
|
1. Expand task #5 - Critical path + high complexity
|
||||||
|
2. Expand task #12 - High risk + dependencies
|
||||||
|
3. Review task #34 - Consider splitting
|
||||||
|
|
||||||
|
**Sprint Planning:**
|
||||||
|
- Don't schedule multiple high-complexity tasks together
|
||||||
|
- Ensure expertise available for complex tasks
|
||||||
|
- Build in buffer time for unknowns
|
||||||
|
|
||||||
|
## Interactive Features
|
||||||
|
|
||||||
|
When viewing report:
|
||||||
|
1. **Quick Actions**
|
||||||
|
- Press 'e' to expand a task
|
||||||
|
- Press 'd' for task details
|
||||||
|
- Press 'r' to refresh analysis
|
||||||
|
|
||||||
|
2. **Filtering**
|
||||||
|
- View by complexity level
|
||||||
|
- Filter by risk factors
|
||||||
|
- Show only actionable items
|
||||||
|
|
||||||
|
3. **Export Options**
|
||||||
|
- Markdown format
|
||||||
|
- CSV for spreadsheets
|
||||||
|
- JSON for tools
|
||||||
|
|
||||||
|
## Report Intelligence
|
||||||
|
|
||||||
|
- Compares with historical data
|
||||||
|
- Shows complexity trends
|
||||||
|
- Identifies patterns
|
||||||
|
- Suggests process improvements
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
Use report for:
|
||||||
|
- Sprint planning sessions
|
||||||
|
- Resource allocation
|
||||||
|
- Risk assessment
|
||||||
|
- Team discussions
|
||||||
|
- Client updates
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:complexity-report
|
||||||
|
→ Opens latest analysis
|
||||||
|
|
||||||
|
/taskmaster:complexity-report --file=archived/2024-01-01.md
|
||||||
|
→ View historical analysis
|
||||||
|
|
||||||
|
After viewing:
|
||||||
|
/taskmaster:expand 5
|
||||||
|
→ Expand high-complexity task
|
||||||
|
\`\`\``,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Convert Task To Subtask Slash Command
|
||||||
|
* Convert an existing task into a subtask.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The convert-task-to-subtask slash command - Convert Task To Subtask
|
||||||
|
*
|
||||||
|
* Convert an existing task into a subtask.
|
||||||
|
*/
|
||||||
|
export const convertTaskToSubtask = dynamicCommand(
|
||||||
|
'convert-task-to-subtask',
|
||||||
|
'Convert Task To Subtask',
|
||||||
|
'<parent-id> <task-id>',
|
||||||
|
`Convert an existing task into a subtask.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse parent ID and task ID to convert.
|
||||||
|
|
||||||
|
## Task Conversion
|
||||||
|
|
||||||
|
Converts an existing standalone task into a subtask of another task.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
- "move task 8 under 5"
|
||||||
|
- "make 8 a subtask of 5"
|
||||||
|
- "nest 8 in 5"
|
||||||
|
- "5 8" → make task 8 a subtask of task 5
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master add-subtask --parent=<parent-id> --task-id=<task-to-convert>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Pre-Conversion Checks
|
||||||
|
|
||||||
|
1. **Validation**
|
||||||
|
- Both tasks exist and are valid
|
||||||
|
- No circular parent relationships
|
||||||
|
- Task isn't already a subtask
|
||||||
|
- Logical hierarchy makes sense
|
||||||
|
|
||||||
|
2. **Impact Analysis**
|
||||||
|
- Dependencies that will be affected
|
||||||
|
- Tasks that depend on converting task
|
||||||
|
- Priority alignment needed
|
||||||
|
- Status compatibility
|
||||||
|
|
||||||
|
## Conversion Process
|
||||||
|
|
||||||
|
1. Change task ID from "8" to "5.1" (next available)
|
||||||
|
2. Update all dependency references
|
||||||
|
3. Inherit parent's context where appropriate
|
||||||
|
4. Adjust priorities if needed
|
||||||
|
5. Update time estimates
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
- Preserve task history
|
||||||
|
- Maintain dependencies
|
||||||
|
- Update all references
|
||||||
|
- Create conversion log
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:add-subtask/from-task 5 8
|
||||||
|
→ Converting: Task #8 becomes subtask #5.1
|
||||||
|
→ Updated: 3 dependency references
|
||||||
|
→ Parent task #5 now has 1 subtask
|
||||||
|
→ Note: Subtask inherits parent's priority
|
||||||
|
|
||||||
|
Before: #8 "Implement validation" (standalone)
|
||||||
|
After: #5.1 "Implement validation" (subtask of #5)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Post-Conversion
|
||||||
|
|
||||||
|
- Show new task hierarchy
|
||||||
|
- List updated dependencies
|
||||||
|
- Verify project integrity
|
||||||
|
- Suggest related conversions`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Expand All Tasks Slash Command
|
||||||
|
* Bulk expansion of all pending tasks that need subtasks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expand-all-tasks slash command - Expand All Tasks
|
||||||
|
*
|
||||||
|
* Bulk expansion of all pending tasks that need subtasks.
|
||||||
|
*/
|
||||||
|
export const expandAllTasks = staticCommand({
|
||||||
|
name: 'expand-all-tasks',
|
||||||
|
description: 'Expand All Tasks',
|
||||||
|
content: `Expand all pending tasks that need subtasks.
|
||||||
|
|
||||||
|
## Bulk Task Expansion
|
||||||
|
|
||||||
|
Intelligently expands all tasks that would benefit from breakdown.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master expand --all
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Selection
|
||||||
|
|
||||||
|
Only expands tasks that:
|
||||||
|
- Are marked as pending
|
||||||
|
- Have high complexity (>5)
|
||||||
|
- Lack existing subtasks
|
||||||
|
- Would benefit from breakdown
|
||||||
|
|
||||||
|
## Expansion Process
|
||||||
|
|
||||||
|
1. **Analysis Phase**
|
||||||
|
- Identify expansion candidates
|
||||||
|
- Group related tasks
|
||||||
|
- Plan expansion strategy
|
||||||
|
|
||||||
|
2. **Batch Processing**
|
||||||
|
- Expand tasks in logical order
|
||||||
|
- Maintain consistency
|
||||||
|
- Preserve relationships
|
||||||
|
- Optimize for parallelism
|
||||||
|
|
||||||
|
3. **Quality Control**
|
||||||
|
- Ensure subtask quality
|
||||||
|
- Avoid over-decomposition
|
||||||
|
- Maintain task coherence
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
- Add \`force\` to expand all regardless of complexity
|
||||||
|
- Add \`research\` for enhanced AI analysis
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
After bulk expansion:
|
||||||
|
- Summary of tasks expanded
|
||||||
|
- New subtask count
|
||||||
|
- Updated complexity metrics
|
||||||
|
- Suggested task order`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Expand Task Slash Command
|
||||||
|
* Break down a complex task into subtasks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expand-task slash command - Expand Task
|
||||||
|
*
|
||||||
|
* Break down a complex task into subtasks.
|
||||||
|
*/
|
||||||
|
export const expandTask = dynamicCommand(
|
||||||
|
'expand-task',
|
||||||
|
'Expand Task',
|
||||||
|
'<task-id>',
|
||||||
|
`Break down a complex task into subtasks.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Intelligent Task Expansion
|
||||||
|
|
||||||
|
Analyzes a task and creates detailed subtasks for better manageability.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master expand --id=$ARGUMENTS
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Expansion Process
|
||||||
|
|
||||||
|
1. **Task Analysis**
|
||||||
|
- Review task complexity
|
||||||
|
- Identify components
|
||||||
|
- Detect technical challenges
|
||||||
|
- Estimate time requirements
|
||||||
|
|
||||||
|
2. **Subtask Generation**
|
||||||
|
- Create 3-7 subtasks typically
|
||||||
|
- Each subtask 1-4 hours
|
||||||
|
- Logical implementation order
|
||||||
|
- Clear acceptance criteria
|
||||||
|
|
||||||
|
3. **Smart Breakdown**
|
||||||
|
- Setup/configuration tasks
|
||||||
|
- Core implementation
|
||||||
|
- Testing components
|
||||||
|
- Integration steps
|
||||||
|
- Documentation updates
|
||||||
|
|
||||||
|
## Enhanced Features
|
||||||
|
|
||||||
|
Based on task type:
|
||||||
|
- **Feature**: Setup → Implement → Test → Integrate
|
||||||
|
- **Bug Fix**: Reproduce → Diagnose → Fix → Verify
|
||||||
|
- **Refactor**: Analyze → Plan → Refactor → Validate
|
||||||
|
|
||||||
|
## Post-Expansion
|
||||||
|
|
||||||
|
After expansion:
|
||||||
|
1. Show subtask hierarchy
|
||||||
|
2. Update time estimates
|
||||||
|
3. Suggest implementation order
|
||||||
|
4. Highlight critical path`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Fix Dependencies Slash Command
|
||||||
|
* Automatically fix dependency issues found during validation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fix-dependencies slash command - Fix Dependencies
|
||||||
|
*
|
||||||
|
* Automatically fix dependency issues found during validation.
|
||||||
|
*/
|
||||||
|
export const fixDependencies = staticCommand({
|
||||||
|
name: 'fix-dependencies',
|
||||||
|
description: 'Fix Dependencies',
|
||||||
|
content: `Automatically fix dependency issues found during validation.
|
||||||
|
|
||||||
|
## Automatic Dependency Repair
|
||||||
|
|
||||||
|
Intelligently fixes common dependency problems while preserving project logic.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master fix-dependencies
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## What Gets Fixed
|
||||||
|
|
||||||
|
### 1. **Auto-Fixable Issues**
|
||||||
|
- Remove references to deleted tasks
|
||||||
|
- Break simple circular dependencies
|
||||||
|
- Remove self-dependencies
|
||||||
|
- Clean up duplicate dependencies
|
||||||
|
|
||||||
|
### 2. **Smart Resolutions**
|
||||||
|
- Reorder dependencies to maintain logic
|
||||||
|
- Suggest task merging for over-dependent tasks
|
||||||
|
- Flatten unnecessary dependency chains
|
||||||
|
- Remove redundant transitive dependencies
|
||||||
|
|
||||||
|
### 3. **Manual Review Required**
|
||||||
|
- Complex circular dependencies
|
||||||
|
- Critical path modifications
|
||||||
|
- Business logic dependencies
|
||||||
|
- High-impact changes
|
||||||
|
|
||||||
|
## Fix Process
|
||||||
|
|
||||||
|
1. **Analysis Phase**
|
||||||
|
- Run validation check
|
||||||
|
- Categorize issues by type
|
||||||
|
- Determine fix strategy
|
||||||
|
|
||||||
|
2. **Execution Phase**
|
||||||
|
- Apply automatic fixes
|
||||||
|
- Log all changes made
|
||||||
|
- Preserve task relationships
|
||||||
|
|
||||||
|
3. **Verification Phase**
|
||||||
|
- Re-validate after fixes
|
||||||
|
- Show before/after comparison
|
||||||
|
- Highlight manual fixes needed
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
- Preserves intended task flow
|
||||||
|
- Minimal disruption approach
|
||||||
|
- Creates fix history/log
|
||||||
|
- Suggests manual interventions
|
||||||
|
|
||||||
|
## Output Example
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Dependency Auto-Fix Report
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Fixed Automatically:
|
||||||
|
✅ Removed 2 references to deleted tasks
|
||||||
|
✅ Resolved 1 self-dependency
|
||||||
|
✅ Cleaned 3 redundant dependencies
|
||||||
|
|
||||||
|
Manual Review Needed:
|
||||||
|
⚠️ Complex circular dependency: #12 → #15 → #18 → #12
|
||||||
|
Suggestion: Make #15 not depend on #12
|
||||||
|
⚠️ Task #45 has 8 dependencies
|
||||||
|
Suggestion: Break into subtasks
|
||||||
|
|
||||||
|
Run '/taskmaster:validate-dependencies' to verify fixes
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- Preview mode available
|
||||||
|
- Rollback capability
|
||||||
|
- Change logging
|
||||||
|
- No data loss`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Generate Tasks Slash Command
|
||||||
|
* Generate individual task files from tasks.json.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generate-tasks slash command - Generate Task Files
|
||||||
|
*
|
||||||
|
* Creates separate markdown files for each task.
|
||||||
|
*/
|
||||||
|
export const generateTasks = staticCommand({
|
||||||
|
name: 'generate-tasks',
|
||||||
|
description: 'Generate Task Files',
|
||||||
|
content: `Generate individual task files from tasks.json.
|
||||||
|
|
||||||
|
## Task File Generation
|
||||||
|
|
||||||
|
Creates separate markdown files for each task, perfect for AI agents or documentation.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master generate
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## What It Creates
|
||||||
|
|
||||||
|
For each task, generates a file like \`task_001.md\`:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Task ID: 1
|
||||||
|
Title: Implement user authentication
|
||||||
|
Status: pending
|
||||||
|
Priority: high
|
||||||
|
Dependencies: []
|
||||||
|
Created: 2024-01-15
|
||||||
|
Complexity: 7
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Create a secure user authentication system with login, logout, and session management.
|
||||||
|
|
||||||
|
## Details
|
||||||
|
- Use JWT tokens for session management
|
||||||
|
- Implement secure password hashing
|
||||||
|
- Add remember me functionality
|
||||||
|
- Include password reset flow
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
- Unit tests for auth functions
|
||||||
|
- Integration tests for login flow
|
||||||
|
- Security testing for vulnerabilities
|
||||||
|
- Performance tests for concurrent logins
|
||||||
|
|
||||||
|
## Subtasks
|
||||||
|
1.1 Setup authentication framework (pending)
|
||||||
|
1.2 Create login endpoints (pending)
|
||||||
|
1.3 Implement session management (pending)
|
||||||
|
1.4 Add password reset (pending)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
Creates structure:
|
||||||
|
\`\`\`
|
||||||
|
.taskmaster/
|
||||||
|
└── tasks/
|
||||||
|
├── task_001.md
|
||||||
|
├── task_002.md
|
||||||
|
├── task_003.md
|
||||||
|
└── ...
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
1. **Consistent Formatting**
|
||||||
|
- Standardized structure
|
||||||
|
- Clear sections
|
||||||
|
- AI-readable format
|
||||||
|
- Markdown compatible
|
||||||
|
|
||||||
|
2. **Contextual Information**
|
||||||
|
- Full task details
|
||||||
|
- Related task references
|
||||||
|
- Progress indicators
|
||||||
|
- Implementation notes
|
||||||
|
|
||||||
|
3. **Incremental Updates**
|
||||||
|
- Only regenerate changed tasks
|
||||||
|
- Preserve custom additions
|
||||||
|
- Track generation timestamp
|
||||||
|
- Version control friendly
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **AI Context**: Provide task context to AI assistants
|
||||||
|
- **Documentation**: Standalone task documentation
|
||||||
|
- **Archival**: Task history preservation
|
||||||
|
- **Sharing**: Send specific tasks to team members
|
||||||
|
- **Review**: Easier task review process
|
||||||
|
|
||||||
|
## Post-Generation
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Task File Generation Complete
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Generated: 45 task files
|
||||||
|
Location: .taskmaster/tasks/
|
||||||
|
Total size: 156 KB
|
||||||
|
|
||||||
|
New files: 5
|
||||||
|
Updated files: 12
|
||||||
|
Unchanged: 28
|
||||||
|
|
||||||
|
Ready for:
|
||||||
|
- AI agent consumption
|
||||||
|
- Version control
|
||||||
|
- Team distribution
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Integration Benefits
|
||||||
|
|
||||||
|
- Git-trackable task history
|
||||||
|
- Easy task sharing
|
||||||
|
- AI tool compatibility
|
||||||
|
- Offline task access
|
||||||
|
- Backup redundancy`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Solo Mode Commands
|
||||||
|
* Commands that only work with local file-based storage (Taskmaster standalone).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// PRD parsing
|
||||||
|
export { parsePrd } from './parse-prd.js';
|
||||||
|
export { parsePrdWithResearch } from './parse-prd-with-research.js';
|
||||||
|
|
||||||
|
// Analysis
|
||||||
|
export { analyzeComplexity } from './analyze-complexity.js';
|
||||||
|
export { complexityReport } from './complexity-report.js';
|
||||||
|
|
||||||
|
// Task expansion
|
||||||
|
export { expandTask } from './expand-task.js';
|
||||||
|
export { expandAllTasks } from './expand-all-tasks.js';
|
||||||
|
|
||||||
|
// Task mutation
|
||||||
|
export { addTask } from './add-task.js';
|
||||||
|
export { addSubtask } from './add-subtask.js';
|
||||||
|
export { removeTask } from './remove-task.js';
|
||||||
|
export { removeSubtask } from './remove-subtask.js';
|
||||||
|
export { removeSubtasks } from './remove-subtasks.js';
|
||||||
|
export { removeAllSubtasks } from './remove-all-subtasks.js';
|
||||||
|
export { convertTaskToSubtask } from './convert-task-to-subtask.js';
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
export { addDependency } from './add-dependency.js';
|
||||||
|
export { removeDependency } from './remove-dependency.js';
|
||||||
|
export { fixDependencies } from './fix-dependencies.js';
|
||||||
|
export { validateDependencies } from './validate-dependencies.js';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
export { setupModels } from './setup-models.js';
|
||||||
|
export { viewModels } from './view-models.js';
|
||||||
|
export { installTaskmaster } from './install-taskmaster.js';
|
||||||
|
export { quickInstallTaskmaster } from './quick-install-taskmaster.js';
|
||||||
|
|
||||||
|
// Status (solo-only)
|
||||||
|
export { toReview } from './to-review.js';
|
||||||
|
export { toDeferred } from './to-deferred.js';
|
||||||
|
export { toCancelled } from './to-cancelled.js';
|
||||||
|
|
||||||
|
// Init
|
||||||
|
export { initProject } from './init-project.js';
|
||||||
|
export { initProjectQuick } from './init-project-quick.js';
|
||||||
|
|
||||||
|
// Generation
|
||||||
|
export { generateTasks } from './generate-tasks.js';
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Init Project Quick Slash Command
|
||||||
|
* Quick initialization with auto-confirmation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The init-project-quick slash command - Init Project Quick
|
||||||
|
*
|
||||||
|
* Quick initialization with auto-confirmation.
|
||||||
|
*/
|
||||||
|
export const initProjectQuick = dynamicCommand(
|
||||||
|
'init-project-quick',
|
||||||
|
'Init Project Quick',
|
||||||
|
'[prd-file]',
|
||||||
|
`Quick initialization with auto-confirmation.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Initialize a Task Master project without prompts, accepting all defaults.
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master init -y
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
1. Creates \`.taskmaster/\` directory structure
|
||||||
|
2. Initializes empty \`tasks.json\`
|
||||||
|
3. Sets up default configuration
|
||||||
|
4. Uses directory name as project name
|
||||||
|
5. Skips all confirmation prompts
|
||||||
|
|
||||||
|
## Smart Defaults
|
||||||
|
|
||||||
|
- Project name: Current directory name
|
||||||
|
- Description: "Task Master Project"
|
||||||
|
- Model config: Existing environment vars
|
||||||
|
- Task structure: Standard format
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After quick init:
|
||||||
|
1. Configure AI models if needed:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:models/setup
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Parse PRD if available:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:parse-prd <file>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. Or create first task:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:add-task create initial setup
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Perfect for rapid project setup!`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Init Project Slash Command
|
||||||
|
* Initialize a new Task Master project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The init-project slash command - Init Project
|
||||||
|
*
|
||||||
|
* Initialize a new Task Master project.
|
||||||
|
*/
|
||||||
|
export const initProject = dynamicCommand(
|
||||||
|
'init-project',
|
||||||
|
'Init Project',
|
||||||
|
'[prd-file]',
|
||||||
|
`Initialize a new Task Master project.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse arguments to determine initialization preferences.
|
||||||
|
|
||||||
|
## Initialization Process
|
||||||
|
|
||||||
|
1. **Parse Arguments**
|
||||||
|
- PRD file path (if provided)
|
||||||
|
- Project name
|
||||||
|
- Auto-confirm flag (-y)
|
||||||
|
|
||||||
|
2. **Project Setup**
|
||||||
|
\`\`\`bash
|
||||||
|
task-master init
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. **Smart Initialization**
|
||||||
|
- Detect existing project files
|
||||||
|
- Suggest project name from directory
|
||||||
|
- Check for git repository
|
||||||
|
- Verify AI provider configuration
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
Based on arguments:
|
||||||
|
- \`quick\` / \`-y\` → Skip confirmations
|
||||||
|
- \`<file.md>\` → Use as PRD after init
|
||||||
|
- \`--name=<name>\` → Set project name
|
||||||
|
- \`--description=<desc>\` → Set description
|
||||||
|
|
||||||
|
## Post-Initialization
|
||||||
|
|
||||||
|
After successful init:
|
||||||
|
1. Show project structure created
|
||||||
|
2. Verify AI models configured
|
||||||
|
3. Suggest next steps:
|
||||||
|
- Parse PRD if available
|
||||||
|
- Configure AI providers
|
||||||
|
- Set up git hooks
|
||||||
|
- Create first tasks
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
If PRD file provided:
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:init my-prd.md
|
||||||
|
→ Automatically runs parse-prd after init
|
||||||
|
\`\`\``,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Install TaskMaster Slash Command
|
||||||
|
* Check if Task Master is installed and install it if needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The install-taskmaster slash command - Install TaskMaster
|
||||||
|
*
|
||||||
|
* Check if Task Master is installed and install it if needed.
|
||||||
|
*/
|
||||||
|
export const installTaskmaster = staticCommand({
|
||||||
|
name: 'install-taskmaster',
|
||||||
|
description: 'Install TaskMaster',
|
||||||
|
content: `Check if Task Master is installed and install it if needed.
|
||||||
|
|
||||||
|
This command helps you get Task Master set up globally on your system.
|
||||||
|
|
||||||
|
## Detection and Installation Process
|
||||||
|
|
||||||
|
1. **Check Current Installation**
|
||||||
|
\`\`\`bash
|
||||||
|
# Check if task-master command exists
|
||||||
|
which task-master || echo "Task Master not found"
|
||||||
|
|
||||||
|
# Check npm global packages
|
||||||
|
npm list -g task-master-ai
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. **System Requirements Check**
|
||||||
|
\`\`\`bash
|
||||||
|
# Verify Node.js is installed
|
||||||
|
node --version
|
||||||
|
|
||||||
|
# Verify npm is installed
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
# Check Node version (need 16+)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. **Install Task Master Globally**
|
||||||
|
If not installed, run:
|
||||||
|
\`\`\`bash
|
||||||
|
npm install -g task-master-ai
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
4. **Verify Installation**
|
||||||
|
\`\`\`bash
|
||||||
|
# Check version
|
||||||
|
task-master --version
|
||||||
|
|
||||||
|
# Verify command is available
|
||||||
|
which task-master
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
5. **Initial Setup**
|
||||||
|
\`\`\`bash
|
||||||
|
# Initialize in current directory
|
||||||
|
task-master init
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
6. **Configure AI Provider**
|
||||||
|
Ensure you have at least one AI provider API key set:
|
||||||
|
\`\`\`bash
|
||||||
|
# Check current configuration
|
||||||
|
task-master models --status
|
||||||
|
|
||||||
|
# If no API keys found, guide setup
|
||||||
|
echo "You'll need at least one API key:"
|
||||||
|
echo "- ANTHROPIC_API_KEY for Claude"
|
||||||
|
echo "- OPENAI_API_KEY for GPT models"
|
||||||
|
echo "- PERPLEXITY_API_KEY for research"
|
||||||
|
echo ""
|
||||||
|
echo "Set them in your shell profile or .env file"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
7. **Quick Test**
|
||||||
|
\`\`\`bash
|
||||||
|
# Create a test PRD
|
||||||
|
echo "Build a simple hello world API" > test-prd.txt
|
||||||
|
|
||||||
|
# Try parsing it
|
||||||
|
task-master parse-prd test-prd.txt -n 3
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If installation fails:
|
||||||
|
|
||||||
|
**Permission Errors:**
|
||||||
|
\`\`\`bash
|
||||||
|
# Try with sudo (macOS/Linux)
|
||||||
|
sudo npm install -g task-master-ai
|
||||||
|
|
||||||
|
# Or fix npm permissions
|
||||||
|
npm config set prefix ~/.npm-global
|
||||||
|
export PATH=~/.npm-global/bin:$PATH
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Network Issues:**
|
||||||
|
\`\`\`bash
|
||||||
|
# Use different registry
|
||||||
|
npm install -g task-master-ai --registry https://registry.npmjs.org/
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Node Version Issues:**
|
||||||
|
\`\`\`bash
|
||||||
|
# Install Node 20+ via nvm
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||||
|
nvm install 20
|
||||||
|
nvm use 20
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Success Confirmation
|
||||||
|
|
||||||
|
Once installed, you should see:
|
||||||
|
\`\`\`
|
||||||
|
✅ Task Master installed
|
||||||
|
✅ Command 'task-master' available globally
|
||||||
|
✅ AI provider configured
|
||||||
|
✅ Ready to use slash commands!
|
||||||
|
|
||||||
|
Try: /taskmaster:init your-prd.md
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After installation:
|
||||||
|
1. Run \`/taskmaster:status\` to verify setup
|
||||||
|
2. Configure AI providers with \`/taskmaster:setup-models\`
|
||||||
|
3. Start using Task Master commands!`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Parse PRD With Research Slash Command
|
||||||
|
* Parse PRD with enhanced research mode for better task generation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parse-prd-with-research slash command - Parse PRD With Research
|
||||||
|
*
|
||||||
|
* Parse PRD with enhanced research mode for better task generation.
|
||||||
|
*/
|
||||||
|
export const parsePrdWithResearch = dynamicCommand(
|
||||||
|
'parse-prd-with-research',
|
||||||
|
'Parse PRD With Research',
|
||||||
|
'<prd-file>',
|
||||||
|
`Parse PRD with enhanced research mode for better task generation.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (PRD file path)
|
||||||
|
|
||||||
|
## Research-Enhanced Parsing
|
||||||
|
|
||||||
|
Uses the research AI provider (typically Perplexity) for more comprehensive task generation with current best practices.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master parse-prd --input=$ARGUMENTS --research
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Research Benefits
|
||||||
|
|
||||||
|
1. **Current Best Practices**
|
||||||
|
- Latest framework patterns
|
||||||
|
- Security considerations
|
||||||
|
- Performance optimizations
|
||||||
|
- Accessibility requirements
|
||||||
|
|
||||||
|
2. **Technical Deep Dive**
|
||||||
|
- Implementation approaches
|
||||||
|
- Library recommendations
|
||||||
|
- Architecture patterns
|
||||||
|
- Testing strategies
|
||||||
|
|
||||||
|
3. **Comprehensive Coverage**
|
||||||
|
- Edge cases consideration
|
||||||
|
- Error handling tasks
|
||||||
|
- Monitoring setup
|
||||||
|
- Deployment tasks
|
||||||
|
|
||||||
|
## Enhanced Output
|
||||||
|
|
||||||
|
Research mode typically:
|
||||||
|
- Generates more detailed tasks
|
||||||
|
- Includes industry standards
|
||||||
|
- Adds compliance considerations
|
||||||
|
- Suggests modern tooling
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- New technology domains
|
||||||
|
- Complex requirements
|
||||||
|
- Regulatory compliance needed
|
||||||
|
- Best practices crucial`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Parse PRD Slash Command
|
||||||
|
* Parse a PRD document to generate tasks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parse-prd slash command - Parse PRD
|
||||||
|
*
|
||||||
|
* Parse a PRD document to generate tasks.
|
||||||
|
*/
|
||||||
|
export const parsePrd = dynamicCommand(
|
||||||
|
'parse-prd',
|
||||||
|
'Parse PRD',
|
||||||
|
'<prd-file>',
|
||||||
|
`Parse a PRD document to generate tasks.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (PRD file path)
|
||||||
|
|
||||||
|
## Intelligent PRD Parsing
|
||||||
|
|
||||||
|
Analyzes your requirements document and generates a complete task breakdown.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master parse-prd --input=$ARGUMENTS
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Parsing Process
|
||||||
|
|
||||||
|
1. **Document Analysis**
|
||||||
|
- Extract key requirements
|
||||||
|
- Identify technical components
|
||||||
|
- Detect dependencies
|
||||||
|
- Estimate complexity
|
||||||
|
|
||||||
|
2. **Task Generation**
|
||||||
|
- Create 10-15 tasks by default
|
||||||
|
- Include implementation tasks
|
||||||
|
- Add testing tasks
|
||||||
|
- Include documentation tasks
|
||||||
|
- Set logical dependencies
|
||||||
|
|
||||||
|
3. **Smart Enhancements**
|
||||||
|
- Group related functionality
|
||||||
|
- Set appropriate priorities
|
||||||
|
- Add acceptance criteria
|
||||||
|
- Include test strategies
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
Parse arguments for modifiers:
|
||||||
|
- Number after filename → \`--num-tasks\`
|
||||||
|
- \`research\` → Use research mode
|
||||||
|
- \`comprehensive\` → Generate more tasks
|
||||||
|
|
||||||
|
## Post-Generation
|
||||||
|
|
||||||
|
After parsing:
|
||||||
|
1. Display task summary
|
||||||
|
2. Show dependency graph
|
||||||
|
3. Suggest task expansion for complex items
|
||||||
|
4. Recommend sprint planning`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Quick Install TaskMaster Slash Command
|
||||||
|
* Quick install Task Master globally if not already installed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quick-install-taskmaster slash command - Quick Install TaskMaster
|
||||||
|
*
|
||||||
|
* Quick install Task Master globally if not already installed.
|
||||||
|
*/
|
||||||
|
export const quickInstallTaskmaster = staticCommand({
|
||||||
|
name: 'quick-install-taskmaster',
|
||||||
|
description: 'Quick Install TaskMaster',
|
||||||
|
content: `Quick install Task Master globally if not already installed.
|
||||||
|
|
||||||
|
Execute this streamlined installation:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Check and install in one command
|
||||||
|
task-master --version 2>/dev/null || npm install -g task-master-ai
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
task-master --version
|
||||||
|
|
||||||
|
# Quick setup check
|
||||||
|
task-master models --status || echo "Note: You'll need to set up an AI provider API key"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
If you see "command not found" after installation, you may need to:
|
||||||
|
1. Restart your terminal
|
||||||
|
2. Or add npm global bin to PATH: \`export PATH=$(npm bin -g):$PATH\`
|
||||||
|
|
||||||
|
Once installed, you can use all the Task Master commands!
|
||||||
|
|
||||||
|
Quick test: Run \`/taskmaster:help\` to see all available commands.`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Remove All Subtasks Slash Command
|
||||||
|
* Clear all subtasks from all tasks globally.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remove-all-subtasks slash command - Remove All Subtasks
|
||||||
|
*
|
||||||
|
* Clear all subtasks from all tasks globally.
|
||||||
|
*/
|
||||||
|
export const removeAllSubtasks = staticCommand({
|
||||||
|
name: 'remove-all-subtasks',
|
||||||
|
description: 'Remove All Subtasks',
|
||||||
|
content: `Clear all subtasks from all tasks globally.
|
||||||
|
|
||||||
|
## Global Subtask Clearing
|
||||||
|
|
||||||
|
Remove all subtasks across the entire project. Use with extreme caution.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master clear-subtasks --all
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Pre-Clear Analysis
|
||||||
|
|
||||||
|
1. **Project-Wide Summary**
|
||||||
|
\`\`\`
|
||||||
|
Global Subtask Summary
|
||||||
|
━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Total parent tasks: 12
|
||||||
|
Total subtasks: 47
|
||||||
|
- Completed: 15
|
||||||
|
- In-progress: 8
|
||||||
|
- Pending: 24
|
||||||
|
|
||||||
|
Work at risk: ~120 hours
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. **Critical Warnings**
|
||||||
|
- In-progress subtasks that will lose work
|
||||||
|
- Completed subtasks with valuable history
|
||||||
|
- Complex dependency chains
|
||||||
|
- Integration test results
|
||||||
|
|
||||||
|
## Double Confirmation
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
⚠️ DESTRUCTIVE OPERATION WARNING ⚠️
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
This will remove ALL 47 subtasks from your project
|
||||||
|
Including 8 in-progress and 15 completed subtasks
|
||||||
|
|
||||||
|
This action CANNOT be undone
|
||||||
|
|
||||||
|
Type 'CLEAR ALL SUBTASKS' to confirm:
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Safeguards
|
||||||
|
|
||||||
|
- Require explicit confirmation phrase
|
||||||
|
- Create automatic backup
|
||||||
|
- Log all removed data
|
||||||
|
- Option to export first
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
Valid reasons for global clear:
|
||||||
|
- Project restructuring
|
||||||
|
- Major pivot in approach
|
||||||
|
- Starting fresh breakdown
|
||||||
|
- Switching to different task organization
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Full project analysis
|
||||||
|
2. Create backup file
|
||||||
|
3. Show detailed impact
|
||||||
|
4. Require confirmation
|
||||||
|
5. Execute removal
|
||||||
|
6. Generate summary report
|
||||||
|
|
||||||
|
## Alternative Suggestions
|
||||||
|
|
||||||
|
Before clearing all:
|
||||||
|
- Export subtasks to file
|
||||||
|
- Clear only pending subtasks
|
||||||
|
- Clear by task category
|
||||||
|
- Archive instead of delete
|
||||||
|
|
||||||
|
## Post-Clear Report
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Global Subtask Clear Complete
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Removed: 47 subtasks from 12 tasks
|
||||||
|
Backup saved: .taskmaster/backup/subtasks-20240115.json
|
||||||
|
Parent tasks updated: 12
|
||||||
|
Time estimates adjusted: Yes
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
- Review updated task list
|
||||||
|
- Re-expand complex tasks as needed
|
||||||
|
- Check project timeline
|
||||||
|
\`\`\``,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Remove Dependency Slash Command
|
||||||
|
* Remove a dependency between tasks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remove-dependency slash command - Remove Dependency
|
||||||
|
*
|
||||||
|
* Remove a dependency between tasks.
|
||||||
|
*/
|
||||||
|
export const removeDependency = dynamicCommand(
|
||||||
|
'remove-dependency',
|
||||||
|
'Remove Dependency',
|
||||||
|
'<task-id> <depends-on-id>',
|
||||||
|
`Remove a dependency between tasks.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse the task IDs to remove dependency relationship.
|
||||||
|
|
||||||
|
## Removing Dependencies
|
||||||
|
|
||||||
|
Removes a dependency relationship, potentially unblocking tasks.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
Parse natural language or IDs:
|
||||||
|
- "remove dependency between 5 and 3"
|
||||||
|
- "5 no longer needs 3"
|
||||||
|
- "unblock 5 from 3"
|
||||||
|
- "5 3" → remove dependency of 5 on 3
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master remove-dependency --id=<task-id> --depends-on=<dependency-id>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Pre-Removal Checks
|
||||||
|
|
||||||
|
1. **Verify dependency exists**
|
||||||
|
2. **Check impact on task flow**
|
||||||
|
3. **Warn if it breaks logical sequence**
|
||||||
|
4. **Show what will be unblocked**
|
||||||
|
|
||||||
|
## Smart Analysis
|
||||||
|
|
||||||
|
Before removing:
|
||||||
|
- Show why dependency might have existed
|
||||||
|
- Check if removal makes tasks executable
|
||||||
|
- Verify no critical path disruption
|
||||||
|
- Suggest alternative dependencies
|
||||||
|
|
||||||
|
## Post-Removal
|
||||||
|
|
||||||
|
After removing:
|
||||||
|
1. Show updated task status
|
||||||
|
2. List newly unblocked tasks
|
||||||
|
3. Update project timeline
|
||||||
|
4. Suggest next actions
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
- Confirm if removing critical dependency
|
||||||
|
- Show tasks that become immediately actionable
|
||||||
|
- Warn about potential issues
|
||||||
|
- Keep removal history
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:remove-dependency 5 from 3
|
||||||
|
→ Removed: Task #5 no longer depends on #3
|
||||||
|
→ Task #5 is now UNBLOCKED and ready to start
|
||||||
|
→ Warning: Consider if #5 still needs #2 completed first
|
||||||
|
\`\`\``,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Remove Subtask Slash Command
|
||||||
|
* Remove a subtask from its parent task.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remove-subtask slash command - Remove Subtask
|
||||||
|
*
|
||||||
|
* Remove a subtask from its parent task.
|
||||||
|
*/
|
||||||
|
export const removeSubtask = dynamicCommand(
|
||||||
|
'remove-subtask',
|
||||||
|
'Remove Subtask',
|
||||||
|
'<subtask-id>',
|
||||||
|
`Remove a subtask from its parent task.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS
|
||||||
|
|
||||||
|
Parse subtask ID to remove, with option to convert to standalone task.
|
||||||
|
|
||||||
|
## Removing Subtasks
|
||||||
|
|
||||||
|
Remove a subtask and optionally convert it back to a standalone task.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
- "remove subtask 5.1"
|
||||||
|
- "delete 5.1"
|
||||||
|
- "convert 5.1 to task" → remove and convert
|
||||||
|
- "5.1 standalone" → convert to standalone
|
||||||
|
|
||||||
|
## Execution Options
|
||||||
|
|
||||||
|
### 1. Delete Subtask
|
||||||
|
\`\`\`bash
|
||||||
|
task-master remove-subtask --id=<parentId.subtaskId>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 2. Convert to Standalone
|
||||||
|
\`\`\`bash
|
||||||
|
task-master remove-subtask --id=<parentId.subtaskId> --convert
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Pre-Removal Checks
|
||||||
|
|
||||||
|
1. **Validate Subtask**
|
||||||
|
- Verify subtask exists
|
||||||
|
- Check completion status
|
||||||
|
- Review dependencies
|
||||||
|
|
||||||
|
2. **Impact Analysis**
|
||||||
|
- Other subtasks that depend on it
|
||||||
|
- Parent task implications
|
||||||
|
- Data that will be lost
|
||||||
|
|
||||||
|
## Removal Process
|
||||||
|
|
||||||
|
### For Deletion:
|
||||||
|
1. Confirm if subtask has work done
|
||||||
|
2. Update parent task estimates
|
||||||
|
3. Remove subtask and its data
|
||||||
|
4. Clean up dependencies
|
||||||
|
|
||||||
|
### For Conversion:
|
||||||
|
1. Assign new standalone task ID
|
||||||
|
2. Preserve all task data
|
||||||
|
3. Update dependency references
|
||||||
|
4. Maintain task history
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
- Warn if subtask is in-progress
|
||||||
|
- Show impact on parent task
|
||||||
|
- Preserve important data
|
||||||
|
- Update related estimates
|
||||||
|
|
||||||
|
## Example Flows
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:remove-subtask 5.1
|
||||||
|
→ Warning: Subtask #5.1 is in-progress
|
||||||
|
→ This will delete all subtask data
|
||||||
|
→ Parent task #5 will be updated
|
||||||
|
Confirm deletion? (y/n)
|
||||||
|
|
||||||
|
/taskmaster:remove-subtask 5.1 convert
|
||||||
|
→ Converting subtask #5.1 to standalone task #89
|
||||||
|
→ Preserved: All task data and history
|
||||||
|
→ Updated: 2 dependency references
|
||||||
|
→ New task #89 is now independent
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Post-Removal
|
||||||
|
|
||||||
|
- Update parent task status
|
||||||
|
- Recalculate estimates
|
||||||
|
- Show updated hierarchy
|
||||||
|
- Suggest next actions`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Remove Subtasks Slash Command
|
||||||
|
* Clear all subtasks from a specific task.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remove-subtasks slash command - Remove Subtasks
|
||||||
|
*
|
||||||
|
* Clear all subtasks from a specific task.
|
||||||
|
*/
|
||||||
|
export const removeSubtasks = dynamicCommand(
|
||||||
|
'remove-subtasks',
|
||||||
|
'Remove Subtasks',
|
||||||
|
'<task-id>',
|
||||||
|
`Clear all subtasks from a specific task.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
Remove all subtasks from a parent task at once.
|
||||||
|
|
||||||
|
## Clearing Subtasks
|
||||||
|
|
||||||
|
Bulk removal of all subtasks from a parent task.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master remove-subtasks --id=$ARGUMENTS
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Pre-Clear Analysis
|
||||||
|
|
||||||
|
1. **Subtask Summary**
|
||||||
|
- Number of subtasks
|
||||||
|
- Completion status of each
|
||||||
|
- Work already done
|
||||||
|
- Dependencies affected
|
||||||
|
|
||||||
|
2. **Impact Assessment**
|
||||||
|
- Data that will be lost
|
||||||
|
- Dependencies to be removed
|
||||||
|
- Effect on project timeline
|
||||||
|
- Parent task implications
|
||||||
|
|
||||||
|
## Confirmation Required
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Remove Subtasks Confirmation
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Parent Task: #5 "Implement user authentication"
|
||||||
|
Subtasks to remove: 4
|
||||||
|
- #5.1 "Setup auth framework" (done)
|
||||||
|
- #5.2 "Create login form" (in-progress)
|
||||||
|
- #5.3 "Add validation" (pending)
|
||||||
|
- #5.4 "Write tests" (pending)
|
||||||
|
|
||||||
|
⚠️ This will permanently delete all subtask data
|
||||||
|
Continue? (y/n)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Features
|
||||||
|
|
||||||
|
- Option to convert to standalone tasks
|
||||||
|
- Backup task data before clearing
|
||||||
|
- Preserve completed work history
|
||||||
|
- Update parent task appropriately
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. List all subtasks for confirmation
|
||||||
|
2. Check for in-progress work
|
||||||
|
3. Remove all subtasks
|
||||||
|
4. Update parent task
|
||||||
|
5. Clean up dependencies
|
||||||
|
|
||||||
|
## Alternative Options
|
||||||
|
|
||||||
|
Suggest alternatives:
|
||||||
|
- Convert important subtasks to tasks
|
||||||
|
- Keep completed subtasks
|
||||||
|
- Archive instead of delete
|
||||||
|
- Export subtask data first
|
||||||
|
|
||||||
|
## Post-Clear
|
||||||
|
|
||||||
|
- Show updated parent task
|
||||||
|
- Recalculate time estimates
|
||||||
|
- Update task complexity
|
||||||
|
- Suggest next steps
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:remove-subtasks 5
|
||||||
|
→ Found 4 subtasks to remove
|
||||||
|
→ Warning: Subtask #5.2 is in-progress
|
||||||
|
→ Cleared all subtasks from task #5
|
||||||
|
→ Updated parent task estimates
|
||||||
|
→ Suggestion: Consider re-expanding with better breakdown
|
||||||
|
\`\`\``,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Remove Task Slash Command
|
||||||
|
* Remove a task permanently from the project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remove-task slash command - Remove Task
|
||||||
|
*
|
||||||
|
* Remove a task permanently from the project.
|
||||||
|
*/
|
||||||
|
export const removeTask = dynamicCommand(
|
||||||
|
'remove-task',
|
||||||
|
'Remove Task',
|
||||||
|
'<task-id>',
|
||||||
|
`Remove a task permanently from the project.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
Delete a task and handle all its relationships properly.
|
||||||
|
|
||||||
|
## Task Removal
|
||||||
|
|
||||||
|
Permanently removes a task while maintaining project integrity.
|
||||||
|
|
||||||
|
## Argument Parsing
|
||||||
|
|
||||||
|
- "remove task 5"
|
||||||
|
- "delete 5"
|
||||||
|
- "5" → remove task 5
|
||||||
|
- Can include "-y" for auto-confirm
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master remove-task --id=<id> [-y]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Pre-Removal Analysis
|
||||||
|
|
||||||
|
1. **Task Details**
|
||||||
|
- Current status
|
||||||
|
- Work completed
|
||||||
|
- Time invested
|
||||||
|
- Associated data
|
||||||
|
|
||||||
|
2. **Relationship Check**
|
||||||
|
- Tasks that depend on this
|
||||||
|
- Dependencies this task has
|
||||||
|
- Subtasks that will be removed
|
||||||
|
- Blocking implications
|
||||||
|
|
||||||
|
3. **Impact Assessment**
|
||||||
|
\`\`\`
|
||||||
|
Task Removal Impact
|
||||||
|
━━━━━━━━━━━━━━━━━━
|
||||||
|
Task: #5 "Implement authentication" (in-progress)
|
||||||
|
Status: 60% complete (~8 hours work)
|
||||||
|
|
||||||
|
Will affect:
|
||||||
|
- 3 tasks depend on this (will be blocked)
|
||||||
|
- Has 4 subtasks (will be deleted)
|
||||||
|
- Part of critical path
|
||||||
|
|
||||||
|
⚠️ This action cannot be undone
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Smart Warnings
|
||||||
|
|
||||||
|
- Warn if task is in-progress
|
||||||
|
- Show dependent tasks that will be blocked
|
||||||
|
- Highlight if part of critical path
|
||||||
|
- Note any completed work being lost
|
||||||
|
|
||||||
|
## Removal Process
|
||||||
|
|
||||||
|
1. Show comprehensive impact
|
||||||
|
2. Require confirmation (unless -y)
|
||||||
|
3. Update dependent task references
|
||||||
|
4. Remove task and subtasks
|
||||||
|
5. Clean up orphaned dependencies
|
||||||
|
6. Log removal with timestamp
|
||||||
|
|
||||||
|
## Alternative Actions
|
||||||
|
|
||||||
|
Suggest before deletion:
|
||||||
|
- Mark as cancelled instead
|
||||||
|
- Convert to documentation
|
||||||
|
- Archive task data
|
||||||
|
- Transfer work to another task
|
||||||
|
|
||||||
|
## Post-Removal
|
||||||
|
|
||||||
|
- List affected tasks
|
||||||
|
- Show broken dependencies
|
||||||
|
- Update project statistics
|
||||||
|
- Suggest dependency fixes
|
||||||
|
- Recalculate timeline
|
||||||
|
|
||||||
|
## Example Flows
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
/taskmaster:remove-task 5
|
||||||
|
→ Task #5 is in-progress with 8 hours logged
|
||||||
|
→ 3 other tasks depend on this
|
||||||
|
→ Suggestion: Mark as cancelled instead?
|
||||||
|
Remove anyway? (y/n)
|
||||||
|
|
||||||
|
/taskmaster:remove-task 5 -y
|
||||||
|
→ Removed: Task #5 and 4 subtasks
|
||||||
|
→ Updated: 3 task dependencies
|
||||||
|
→ Warning: Tasks #7, #8, #9 now have missing dependency
|
||||||
|
→ Run /taskmaster:fix-dependencies to resolve
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
- Confirmation required
|
||||||
|
- Impact preview
|
||||||
|
- Removal logging
|
||||||
|
- Suggest alternatives
|
||||||
|
- No cascade delete of dependents`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Setup Models Slash Command
|
||||||
|
* Run interactive setup to configure AI models.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The setup-models slash command - Setup Models
|
||||||
|
*
|
||||||
|
* Run interactive setup to configure AI models.
|
||||||
|
*/
|
||||||
|
export const setupModels = staticCommand({
|
||||||
|
name: 'setup-models',
|
||||||
|
description: 'Setup Models',
|
||||||
|
content: `Run interactive setup to configure AI models.
|
||||||
|
|
||||||
|
## Interactive Model Configuration
|
||||||
|
|
||||||
|
Guides you through setting up AI providers for Task Master.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master models --setup
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Setup Process
|
||||||
|
|
||||||
|
1. **Environment Check**
|
||||||
|
- Detect existing API keys
|
||||||
|
- Show current configuration
|
||||||
|
- Identify missing providers
|
||||||
|
|
||||||
|
2. **Provider Selection**
|
||||||
|
- Choose main provider (required)
|
||||||
|
- Select research provider (recommended)
|
||||||
|
- Configure fallback (optional)
|
||||||
|
|
||||||
|
3. **API Key Configuration**
|
||||||
|
- Prompt for missing keys
|
||||||
|
- Validate key format
|
||||||
|
- Test connectivity
|
||||||
|
- Save configuration
|
||||||
|
|
||||||
|
## Smart Recommendations
|
||||||
|
|
||||||
|
Based on your needs:
|
||||||
|
- **For best results**: Claude + Perplexity
|
||||||
|
- **Budget conscious**: GPT-3.5 + Perplexity
|
||||||
|
- **Maximum capability**: GPT-4 + Perplexity + Claude fallback
|
||||||
|
|
||||||
|
## Configuration Storage
|
||||||
|
|
||||||
|
Keys can be stored in:
|
||||||
|
1. Environment variables (recommended)
|
||||||
|
2. \`.env\` file in project
|
||||||
|
3. Global \`.taskmaster/config\`
|
||||||
|
|
||||||
|
## Post-Setup
|
||||||
|
|
||||||
|
After configuration:
|
||||||
|
- Test each provider
|
||||||
|
- Show usage examples
|
||||||
|
- Suggest next steps
|
||||||
|
- Verify parse-prd works`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview To Cancelled Slash Command
|
||||||
|
* Cancel a task permanently.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The to-cancelled slash command - To Cancelled
|
||||||
|
*
|
||||||
|
* Cancel a task permanently.
|
||||||
|
*/
|
||||||
|
export const toCancelled = dynamicCommand(
|
||||||
|
'to-cancelled',
|
||||||
|
'To Cancelled',
|
||||||
|
'<task-id>',
|
||||||
|
`Cancel a task permanently.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Cancelling a Task
|
||||||
|
|
||||||
|
This status indicates a task is no longer needed and won't be completed.
|
||||||
|
|
||||||
|
## Valid Reasons for Cancellation
|
||||||
|
|
||||||
|
- Requirements changed
|
||||||
|
- Feature deprecated
|
||||||
|
- Duplicate of another task
|
||||||
|
- Strategic pivot
|
||||||
|
- Technical approach invalidated
|
||||||
|
|
||||||
|
## Pre-Cancellation Checks
|
||||||
|
|
||||||
|
1. Confirm no critical dependencies
|
||||||
|
2. Check for partial implementation
|
||||||
|
3. Verify cancellation rationale
|
||||||
|
4. Document lessons learned
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master set-status --id=$ARGUMENTS --status=cancelled
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Cancellation Impact
|
||||||
|
|
||||||
|
When cancelling:
|
||||||
|
1. **Dependency Updates**
|
||||||
|
- Notify dependent tasks
|
||||||
|
- Update project scope
|
||||||
|
- Recalculate timelines
|
||||||
|
|
||||||
|
2. **Clean-up Actions**
|
||||||
|
- Remove related branches
|
||||||
|
- Archive any work done
|
||||||
|
- Update documentation
|
||||||
|
- Close related issues
|
||||||
|
|
||||||
|
3. **Learning Capture**
|
||||||
|
- Document why cancelled
|
||||||
|
- Note what was learned
|
||||||
|
- Update estimation models
|
||||||
|
- Prevent future duplicates
|
||||||
|
|
||||||
|
## Historical Preservation
|
||||||
|
|
||||||
|
- Keep for reference
|
||||||
|
- Tag with cancellation reason
|
||||||
|
- Link to replacement if any
|
||||||
|
- Maintain audit trail`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview To Deferred Slash Command
|
||||||
|
* Defer a task for later consideration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The to-deferred slash command - To Deferred
|
||||||
|
*
|
||||||
|
* Defer a task for later consideration.
|
||||||
|
*/
|
||||||
|
export const toDeferred = dynamicCommand(
|
||||||
|
'to-deferred',
|
||||||
|
'To Deferred',
|
||||||
|
'<task-id>',
|
||||||
|
`Defer a task for later consideration.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Deferring a Task
|
||||||
|
|
||||||
|
This status indicates a task is valid but not currently actionable or prioritized.
|
||||||
|
|
||||||
|
## Valid Reasons for Deferral
|
||||||
|
|
||||||
|
- Waiting for external dependencies
|
||||||
|
- Reprioritized for future sprint
|
||||||
|
- Blocked by technical limitations
|
||||||
|
- Resource constraints
|
||||||
|
- Strategic timing considerations
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master set-status --id=$ARGUMENTS --status=deferred
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Deferral Management
|
||||||
|
|
||||||
|
When deferring:
|
||||||
|
1. **Document Reason**
|
||||||
|
- Capture why it's being deferred
|
||||||
|
- Set reactivation criteria
|
||||||
|
- Note any partial work completed
|
||||||
|
|
||||||
|
2. **Impact Analysis**
|
||||||
|
- Check dependent tasks
|
||||||
|
- Update project timeline
|
||||||
|
- Notify affected stakeholders
|
||||||
|
|
||||||
|
3. **Future Planning**
|
||||||
|
- Set review reminders
|
||||||
|
- Tag for specific milestone
|
||||||
|
- Preserve context for reactivation
|
||||||
|
- Link to blocking issues
|
||||||
|
|
||||||
|
## Smart Tracking
|
||||||
|
|
||||||
|
- Monitor deferral duration
|
||||||
|
- Alert when criteria met
|
||||||
|
- Prevent scope creep
|
||||||
|
- Regular review cycles`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview To Review Slash Command
|
||||||
|
* Set a task's status to review.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dynamicCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The to-review slash command - To Review
|
||||||
|
*
|
||||||
|
* Set a task's status to review.
|
||||||
|
*/
|
||||||
|
export const toReview = dynamicCommand(
|
||||||
|
'to-review',
|
||||||
|
'To Review',
|
||||||
|
'<task-id>',
|
||||||
|
`Set a task's status to review.
|
||||||
|
|
||||||
|
Arguments: $ARGUMENTS (task ID)
|
||||||
|
|
||||||
|
## Marking Task for Review
|
||||||
|
|
||||||
|
This status indicates work is complete but needs verification before final approval.
|
||||||
|
|
||||||
|
## When to Use Review Status
|
||||||
|
|
||||||
|
- Code complete but needs peer review
|
||||||
|
- Implementation done but needs testing
|
||||||
|
- Documentation written but needs proofreading
|
||||||
|
- Design complete but needs stakeholder approval
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master set-status --id=$ARGUMENTS --status=review
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Review Preparation
|
||||||
|
|
||||||
|
When setting to review:
|
||||||
|
1. **Generate Review Checklist**
|
||||||
|
- Link to PR/MR if applicable
|
||||||
|
- Highlight key changes
|
||||||
|
- Note areas needing attention
|
||||||
|
- Include test results
|
||||||
|
|
||||||
|
2. **Documentation**
|
||||||
|
- Update task with review notes
|
||||||
|
- Link relevant artifacts
|
||||||
|
- Specify reviewers if known
|
||||||
|
|
||||||
|
3. **Smart Actions**
|
||||||
|
- Create review reminders
|
||||||
|
- Track review duration
|
||||||
|
- Suggest reviewers based on expertise
|
||||||
|
- Prepare rollback plan if needed`,
|
||||||
|
'solo'
|
||||||
|
);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Validate Dependencies Slash Command
|
||||||
|
* Validate all task dependencies for issues.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validate-dependencies slash command - Validate Dependencies
|
||||||
|
*
|
||||||
|
* Validate all task dependencies for issues.
|
||||||
|
*/
|
||||||
|
export const validateDependencies = staticCommand({
|
||||||
|
name: 'validate-dependencies',
|
||||||
|
description: 'Validate Dependencies',
|
||||||
|
content: `Validate all task dependencies for issues.
|
||||||
|
|
||||||
|
## Dependency Validation
|
||||||
|
|
||||||
|
Comprehensive check for dependency problems across the entire project.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master validate-dependencies
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Validation Checks
|
||||||
|
|
||||||
|
1. **Circular Dependencies**
|
||||||
|
- A depends on B, B depends on A
|
||||||
|
- Complex circular chains
|
||||||
|
- Self-dependencies
|
||||||
|
|
||||||
|
2. **Missing Dependencies**
|
||||||
|
- References to non-existent tasks
|
||||||
|
- Deleted task references
|
||||||
|
- Invalid task IDs
|
||||||
|
|
||||||
|
3. **Logical Issues**
|
||||||
|
- Completed tasks depending on pending
|
||||||
|
- Cancelled tasks in dependency chains
|
||||||
|
- Impossible sequences
|
||||||
|
|
||||||
|
4. **Complexity Warnings**
|
||||||
|
- Over-complex dependency chains
|
||||||
|
- Too many dependencies per task
|
||||||
|
- Bottleneck tasks
|
||||||
|
|
||||||
|
## Smart Analysis
|
||||||
|
|
||||||
|
The validation provides:
|
||||||
|
- Visual dependency graph
|
||||||
|
- Critical path analysis
|
||||||
|
- Bottleneck identification
|
||||||
|
- Suggested optimizations
|
||||||
|
|
||||||
|
## Report Format
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Dependency Validation Report
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
✅ No circular dependencies found
|
||||||
|
⚠️ 2 warnings found:
|
||||||
|
- Task #23 has 7 dependencies (consider breaking down)
|
||||||
|
- Task #45 blocks 5 other tasks (potential bottleneck)
|
||||||
|
❌ 1 error found:
|
||||||
|
- Task #67 depends on deleted task #66
|
||||||
|
|
||||||
|
Critical Path: #1 → #5 → #23 → #45 → #50 (15 days)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Actionable Output
|
||||||
|
|
||||||
|
For each issue found:
|
||||||
|
- Clear description
|
||||||
|
- Impact assessment
|
||||||
|
- Suggested fix
|
||||||
|
- Command to resolve
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After validation:
|
||||||
|
- Run \`/taskmaster:fix-dependencies\` to auto-fix
|
||||||
|
- Manually adjust problematic dependencies
|
||||||
|
- Rerun to verify fixes`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview View Models Slash Command
|
||||||
|
* View current AI model configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view-models slash command - View Models
|
||||||
|
*
|
||||||
|
* View current AI model configuration.
|
||||||
|
*/
|
||||||
|
export const viewModels = staticCommand({
|
||||||
|
name: 'view-models',
|
||||||
|
description: 'View Models',
|
||||||
|
content: `View current AI model configuration.
|
||||||
|
|
||||||
|
## Model Configuration Display
|
||||||
|
|
||||||
|
Shows the currently configured AI providers and models for Task Master.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
task-master models
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Information Displayed
|
||||||
|
|
||||||
|
1. **Main Provider**
|
||||||
|
- Model ID and name
|
||||||
|
- API key status (configured/missing)
|
||||||
|
- Usage: Primary task generation
|
||||||
|
|
||||||
|
2. **Research Provider**
|
||||||
|
- Model ID and name
|
||||||
|
- API key status
|
||||||
|
- Usage: Enhanced research mode
|
||||||
|
|
||||||
|
3. **Fallback Provider**
|
||||||
|
- Model ID and name
|
||||||
|
- API key status
|
||||||
|
- Usage: Backup when main fails
|
||||||
|
|
||||||
|
## Visual Status
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Task Master AI Model Configuration
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Main: ✅ claude-3-5-sonnet (configured)
|
||||||
|
Research: ✅ perplexity-sonar (configured)
|
||||||
|
Fallback: ⚠️ Not configured (optional)
|
||||||
|
|
||||||
|
Available Models:
|
||||||
|
- claude-3-5-sonnet
|
||||||
|
- gpt-4-turbo
|
||||||
|
- gpt-3.5-turbo
|
||||||
|
- perplexity-sonar
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Next Actions
|
||||||
|
|
||||||
|
Based on configuration:
|
||||||
|
- If missing API keys → Suggest setup
|
||||||
|
- If no research model → Explain benefits
|
||||||
|
- If all configured → Show usage tips`,
|
||||||
|
mode: 'solo'
|
||||||
|
});
|
||||||
344
packages/tm-profiles/src/slash-commands/commands/team/goham.ts
Normal file
344
packages/tm-profiles/src/slash-commands/commands/team/goham.ts
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Goham Slash Command
|
||||||
|
* End-to-end workflow for working on tasks from a connected Hamster brief.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { staticCommand } from '../../factories.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The goham slash command - Start Working with Hamster Brief
|
||||||
|
*
|
||||||
|
* End-to-end workflow for working on tasks from a connected Hamster brief.
|
||||||
|
* All tasks from the brief are worked on in a single branch, with one PR created at the end.
|
||||||
|
*/
|
||||||
|
export const goham = staticCommand({
|
||||||
|
name: 'goham',
|
||||||
|
description: 'Start Working with Hamster Brief',
|
||||||
|
argumentHint: '[brief-url]',
|
||||||
|
mode: 'team',
|
||||||
|
content: `# Start Working with Hamster Brief
|
||||||
|
|
||||||
|
End-to-end workflow for working on tasks from a connected Hamster brief. All tasks from the brief are worked on in a single branch, with one PR created at the end.
|
||||||
|
|
||||||
|
## Step 1: Verify Connection & Authentication
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Check current context and authentication status
|
||||||
|
tm context
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
If not connected or authentication fails:
|
||||||
|
- Get brief URL from user if not available
|
||||||
|
- Connect: \`tm context <brief url>\`
|
||||||
|
- Refresh token if needed: \`tm auth refresh\`
|
||||||
|
|
||||||
|
## Step 2: List Available Tasks
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# View all tasks from the brief
|
||||||
|
tm list
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Review the task list to understand what needs to be done. Note the total number of tasks.
|
||||||
|
|
||||||
|
## Step 3: Initialize Git Branch for Brief
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Ensure you're on dev branch and pull latest
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
# Create a single branch for the entire brief (e.g., hamster-brief-YYYY-MM-DD or brief-specific name)
|
||||||
|
git checkout -b hamster-brief
|
||||||
|
|
||||||
|
# Verify branch creation
|
||||||
|
git branch
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Note**: This branch will be used for ALL tasks in the brief. Do not create separate branches per task.
|
||||||
|
|
||||||
|
## Step 4: Task Loop (Repeat for Each Task)
|
||||||
|
|
||||||
|
Work through all tasks sequentially in the same branch:
|
||||||
|
|
||||||
|
### 4.1: Read Task Details
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Get detailed information about the task
|
||||||
|
tm show 1
|
||||||
|
|
||||||
|
# If task has subtasks, examine them all
|
||||||
|
tm show 1,1.1,1.2,1.3 # Adjust IDs as needed
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4.2: Log Initial Context
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Document task understanding and initial findings
|
||||||
|
tm update-task -i 1 --append --prompt="Starting task implementation.
|
||||||
|
|
||||||
|
Initial context:
|
||||||
|
- Task requirements: [summarize key requirements]
|
||||||
|
- Dependencies identified: [list any dependencies]
|
||||||
|
- Files that may need modification: [list relevant files]
|
||||||
|
- Approach planned: [brief implementation approach]"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4.3: Mark Task as In-Progress
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Mark task and first subtask (if exists) as in-progress
|
||||||
|
tm set-status -i 1,1.1 -s in-progress
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 4.4: Subtask Implementation Loop
|
||||||
|
|
||||||
|
For each subtask (1.1, 1.2, 1.3, etc.):
|
||||||
|
|
||||||
|
#### 4.4.1: Read Subtask Details
|
||||||
|
\`\`\`bash
|
||||||
|
tm show 1.1 # Replace with current subtask ID
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.2: Log Research & Context Gathering
|
||||||
|
\`\`\`bash
|
||||||
|
# Document findings during implementation
|
||||||
|
tm update-task -i 1 --append --prompt="Subtask 1.1 - Context gathered:
|
||||||
|
|
||||||
|
- Code exploration findings: [what you discovered]
|
||||||
|
- Implementation approach: [how you plan to implement]
|
||||||
|
- Key decisions made: [important choices]
|
||||||
|
- Challenges encountered: [any blockers or issues]"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.3: Implement Subtask
|
||||||
|
- Write code following the subtask requirements
|
||||||
|
- Make necessary changes to files
|
||||||
|
|
||||||
|
#### 4.4.4: Quality Verification
|
||||||
|
\`\`\`bash
|
||||||
|
# Run linting
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Run type checking
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
# If either fails, fix issues and re-run until both pass
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.5: CodeRabbit Review
|
||||||
|
\`\`\`bash
|
||||||
|
# Generate code review (wait for plain text results)
|
||||||
|
coderabbit --prompt-only
|
||||||
|
|
||||||
|
# Review the output and address any critical issues if needed
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.6: Log Implementation Completion
|
||||||
|
\`\`\`bash
|
||||||
|
# Document what was completed
|
||||||
|
tm update-task -i 1 --append --prompt="Subtask 1.1 - Implementation complete:
|
||||||
|
|
||||||
|
- Files modified: [list files changed]
|
||||||
|
- Key changes: [summary of implementation]
|
||||||
|
- CodeRabbit feedback addressed: [if any issues were fixed]
|
||||||
|
- Ready for commit"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.7: Commit Subtask Work
|
||||||
|
\`\`\`bash
|
||||||
|
# Stage changes
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commit with detailed message following git_workflow.mdc format
|
||||||
|
git commit -m "feat(task-1): Complete subtask 1.1 - [Subtask Title]
|
||||||
|
|
||||||
|
- Implementation details
|
||||||
|
- Key changes made
|
||||||
|
- Files modified: [list files]
|
||||||
|
- CodeRabbit review completed
|
||||||
|
|
||||||
|
Subtask 1.1: [Brief description of what was accomplished]
|
||||||
|
Relates to Task 1: [Main task title]"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.8: Mark Subtask as Done
|
||||||
|
\`\`\`bash
|
||||||
|
tm set-status -i 1.1 -s done
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.4.9: Move to Next Subtask
|
||||||
|
Repeat steps 4.4.1 through 4.4.8 for the next subtask (1.2, 1.3, etc.)
|
||||||
|
|
||||||
|
### 4.5: Complete Parent Task
|
||||||
|
|
||||||
|
After all subtasks are complete:
|
||||||
|
|
||||||
|
#### 4.5.1: Final Quality Checks
|
||||||
|
\`\`\`bash
|
||||||
|
# Final linting
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Final type checking
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
# Final CodeRabbit review
|
||||||
|
coderabbit --prompt-only
|
||||||
|
|
||||||
|
# Address any remaining issues if critical
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.5.2: Log Task Completion
|
||||||
|
\`\`\`bash
|
||||||
|
# Document final task completion
|
||||||
|
tm update-task -i 1 --append --prompt="Task 1 - Complete:
|
||||||
|
|
||||||
|
- All subtasks completed: [list all subtasks]
|
||||||
|
- Final verification passed: lint, typecheck, CodeRabbit review
|
||||||
|
- Files changed: [comprehensive list]
|
||||||
|
- Committed to brief branch"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### 4.5.3: Mark Parent Task as Done
|
||||||
|
\`\`\`bash
|
||||||
|
tm set-status -i 1 -s done
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Note**: Do NOT push or create PR yet. Continue to next task in the same branch.
|
||||||
|
|
||||||
|
### 4.6: Move to Next Task
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Verify remaining tasks
|
||||||
|
tm list
|
||||||
|
|
||||||
|
# Continue with next task (e.g., Task 2)
|
||||||
|
# Repeat steps 4.1 through 4.5 for Task 2, then Task 3, etc.
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Step 5: Complete All Tasks
|
||||||
|
|
||||||
|
Continue working through all tasks (Steps 4.1-4.6) until all tasks in the brief are complete. All work is committed to the same \`hamster-brief\` branch.
|
||||||
|
|
||||||
|
## Step 6: Final Verification & PR Creation
|
||||||
|
|
||||||
|
After ALL tasks are complete:
|
||||||
|
|
||||||
|
### 6.1: Verify All Tasks Complete
|
||||||
|
\`\`\`bash
|
||||||
|
# Verify all tasks are done
|
||||||
|
tm list
|
||||||
|
|
||||||
|
# Should show all tasks with status 'done'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 6.2: Final Quality Checks
|
||||||
|
\`\`\`bash
|
||||||
|
# Final comprehensive checks
|
||||||
|
pnpm lint
|
||||||
|
pnpm typecheck
|
||||||
|
coderabbit --prompt-only
|
||||||
|
|
||||||
|
# Address any remaining issues if critical
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 6.3: Push Branch
|
||||||
|
\`\`\`bash
|
||||||
|
# Push the brief branch to remote
|
||||||
|
git push origin hamster-brief
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 6.4: Create Pull Request to Dev
|
||||||
|
\`\`\`bash
|
||||||
|
# Get all task titles (adjust task IDs as needed)
|
||||||
|
# Create comprehensive PR description
|
||||||
|
|
||||||
|
gh pr create \\
|
||||||
|
--base dev \\
|
||||||
|
--title "Hamster Brief: Complete Implementation" \\
|
||||||
|
--body "## Brief Overview
|
||||||
|
Completed all tasks from Hamster brief.
|
||||||
|
|
||||||
|
## Tasks Completed
|
||||||
|
- [x] Task 1: [Task 1 title]
|
||||||
|
- Subtasks: 1.1, 1.2, 1.3
|
||||||
|
- [x] Task 2: [Task 2 title]
|
||||||
|
- Subtasks: 2.1, 2.2
|
||||||
|
- [x] Task 3: [Task 3 title]
|
||||||
|
- [Continue listing all tasks]
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
- Total tasks: [number]
|
||||||
|
- Total subtasks: [number]
|
||||||
|
- Files modified: [comprehensive list]
|
||||||
|
- All quality checks passed
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
- Linting passed (pnpm lint)
|
||||||
|
- Type checking passed (pnpm typecheck)
|
||||||
|
- CodeRabbit review completed for all changes
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- [ ] Manual testing completed
|
||||||
|
- [ ] All checks passing
|
||||||
|
|
||||||
|
Complete implementation of Hamster brief tasks"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Step 7: Cleanup
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# After PR is merged, switch back to dev
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
# Delete local branch (optional)
|
||||||
|
git branch -d hamster-brief
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Use ONLY**: \`tm list\`, \`tm show <id>\`, \`tm set-status\`, \`tm update-task\`, \`tm auth refresh\`, \`tm context <brief url>\`
|
||||||
|
- **DON'T use MCP tools** - not compatible with Hamster integration
|
||||||
|
- **Single branch per brief**: All tasks work in the same branch (\`hamster-brief\`)
|
||||||
|
- **Single PR per brief**: One PR created after all tasks are complete
|
||||||
|
- **Always target dev branch** - never main branch
|
||||||
|
- **Regular logging**: Use \`tm update-task -i <id> --append\` frequently to document:
|
||||||
|
- Context gathered during exploration
|
||||||
|
- Implementation decisions made
|
||||||
|
- Challenges encountered
|
||||||
|
- Completion status
|
||||||
|
- **Quality gates**: Never skip lint, typecheck, or CodeRabbit review
|
||||||
|
- **Commit format**: Follow git_workflow.mdc commit message standards
|
||||||
|
- **PR format**: Always use \`--base dev\` when creating PRs
|
||||||
|
|
||||||
|
## Workflow Summary
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
1. Verify connection -> tm context
|
||||||
|
2. List tasks -> tm list
|
||||||
|
3. Create single branch -> git checkout -b hamster-brief
|
||||||
|
4. For each task (in same branch):
|
||||||
|
a. Read task -> tm show X
|
||||||
|
b. Log context -> tm update-task -i X --append
|
||||||
|
c. Mark in-progress -> tm set-status -i X,X.Y -s in-progress
|
||||||
|
d. For each subtask:
|
||||||
|
- Read -> tm show X.Y
|
||||||
|
- Log context -> tm update-task -i X --append
|
||||||
|
- Implement code
|
||||||
|
- Verify -> pnpm lint && pnpm typecheck
|
||||||
|
- Review -> coderabbit --prompt-only
|
||||||
|
- Log completion -> tm update-task -i X --append
|
||||||
|
- Commit -> git commit (following git_workflow.mdc format)
|
||||||
|
- Mark done -> tm set-status -i X.Y -s done
|
||||||
|
e. Final checks -> pnpm lint && pnpm typecheck && coderabbit --prompt-only
|
||||||
|
f. Log completion -> tm update-task -i X --append
|
||||||
|
g. Mark task done -> tm set-status -i X -s done
|
||||||
|
h. Continue to next task (same branch)
|
||||||
|
5. After ALL tasks complete:
|
||||||
|
a. Final verification -> pnpm lint && pnpm typecheck && coderabbit --prompt-only
|
||||||
|
b. Push branch -> git push origin hamster-brief
|
||||||
|
c. Create PR -> gh pr create --base dev
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Team Mode Commands
|
||||||
|
* Commands that only work with API-based storage (Hamster cloud integration).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { goham } from './goham.js';
|
||||||
102
packages/tm-profiles/src/slash-commands/factories.ts
Normal file
102
packages/tm-profiles/src/slash-commands/factories.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Factory Functions for Slash Commands
|
||||||
|
* Simple functions to create type-safe slash command objects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
StaticSlashCommand,
|
||||||
|
DynamicSlashCommand,
|
||||||
|
OperatingMode
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for creating a static slash command
|
||||||
|
*/
|
||||||
|
export interface StaticCommandOptions {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
/** Optional argument hint for documentation (command doesn't use $ARGUMENTS) */
|
||||||
|
argumentHint?: string;
|
||||||
|
/** Operating mode - defaults to 'common' */
|
||||||
|
mode?: OperatingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a static slash command (no $ARGUMENTS placeholder)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Simple static command
|
||||||
|
* const help = staticCommand({
|
||||||
|
* name: 'help',
|
||||||
|
* description: 'Show available commands',
|
||||||
|
* content: '# Help\n\nList of commands...'
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Static command with optional argument hint
|
||||||
|
* const goham = staticCommand({
|
||||||
|
* name: 'goham',
|
||||||
|
* description: 'Start Working with Hamster Brief',
|
||||||
|
* argumentHint: '[brief-url]',
|
||||||
|
* content: '# Start Working...'
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function staticCommand(
|
||||||
|
options: StaticCommandOptions
|
||||||
|
): StaticSlashCommand {
|
||||||
|
const { name, description, content, argumentHint, mode } = options;
|
||||||
|
return {
|
||||||
|
type: 'static',
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
...(argumentHint && { argumentHint }),
|
||||||
|
...(mode && { mode })
|
||||||
|
},
|
||||||
|
content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dynamic slash command that accepts arguments
|
||||||
|
*
|
||||||
|
* The content must contain at least one `$ARGUMENTS` placeholder.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const goham = dynamicCommand(
|
||||||
|
* 'goham',
|
||||||
|
* 'Start Working with Hamster Brief',
|
||||||
|
* '[brief-url]',
|
||||||
|
* '# Start Working\n\nBrief URL: $ARGUMENTS'
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @throws Error if content doesn't contain $ARGUMENTS placeholder
|
||||||
|
*/
|
||||||
|
export function dynamicCommand(
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
argumentHint: string,
|
||||||
|
content: string,
|
||||||
|
mode?: OperatingMode
|
||||||
|
): DynamicSlashCommand {
|
||||||
|
if (!content.includes('$ARGUMENTS')) {
|
||||||
|
throw new Error(
|
||||||
|
`Dynamic slash command "${name}" must contain $ARGUMENTS placeholder`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'dynamic',
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
argumentHint,
|
||||||
|
...(mode && { mode })
|
||||||
|
},
|
||||||
|
content
|
||||||
|
};
|
||||||
|
}
|
||||||
43
packages/tm-profiles/src/slash-commands/index.ts
Normal file
43
packages/tm-profiles/src/slash-commands/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Slash Commands Module
|
||||||
|
* Central exports for the slash command system.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type {
|
||||||
|
SlashCommand,
|
||||||
|
StaticSlashCommand,
|
||||||
|
DynamicSlashCommand,
|
||||||
|
SlashCommandMetadata,
|
||||||
|
FormattedSlashCommand
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
// Factory functions
|
||||||
|
export { staticCommand, dynamicCommand } from './factories.js';
|
||||||
|
export type { StaticCommandOptions } from './factories.js';
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
export { allCommands, goham } from './commands/index.js';
|
||||||
|
|
||||||
|
// Profiles - self-contained profile classes for each editor
|
||||||
|
export {
|
||||||
|
// Base class
|
||||||
|
BaseSlashCommandProfile,
|
||||||
|
// Profile classes (editors that support slash commands)
|
||||||
|
ClaudeProfile,
|
||||||
|
CodexProfile,
|
||||||
|
CursorProfile,
|
||||||
|
OpenCodeProfile,
|
||||||
|
RooProfile,
|
||||||
|
GeminiProfile,
|
||||||
|
// Utility functions
|
||||||
|
getProfile,
|
||||||
|
getAllProfiles,
|
||||||
|
getProfileNames
|
||||||
|
} from './profiles/index.js';
|
||||||
|
|
||||||
|
// Profile types
|
||||||
|
export type { SlashCommandResult } from './profiles/index.js';
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
export { resolveProjectRoot } from './utils.js';
|
||||||
376
packages/tm-profiles/src/slash-commands/profiles/base-profile.ts
Normal file
376
packages/tm-profiles/src/slash-commands/profiles/base-profile.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Base Slash Command Profile
|
||||||
|
* Abstract base class for all slash command profiles.
|
||||||
|
* Follows the same pattern as ai-providers/base-provider.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import type { SlashCommand, FormattedSlashCommand } from '../types.js';
|
||||||
|
import { filterCommandsByMode } from '../commands/index.js';
|
||||||
|
|
||||||
|
/** Default namespace for TaskMaster commands */
|
||||||
|
export const TM_NAMESPACE = 'tm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of adding or removing slash commands
|
||||||
|
*/
|
||||||
|
export interface SlashCommandResult {
|
||||||
|
/** Whether the operation was successful */
|
||||||
|
success: boolean;
|
||||||
|
/** Number of commands affected */
|
||||||
|
count: number;
|
||||||
|
/** Directory where commands were written/removed */
|
||||||
|
directory: string;
|
||||||
|
/** List of filenames affected */
|
||||||
|
files: string[];
|
||||||
|
/** Error message if operation failed */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for adding slash commands
|
||||||
|
*/
|
||||||
|
export interface AddSlashCommandsOptions {
|
||||||
|
/**
|
||||||
|
* Operating mode to filter commands.
|
||||||
|
* - 'solo': Solo + common commands (for local file storage)
|
||||||
|
* - 'team': Team-only commands (exclusive, for Hamster cloud)
|
||||||
|
* - undefined: All commands (no filtering)
|
||||||
|
*/
|
||||||
|
mode?: 'solo' | 'team';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for slash command profiles.
|
||||||
|
*
|
||||||
|
* Each profile encapsulates its own formatting logic, directory structure,
|
||||||
|
* and any profile-specific transformations. This follows SOLID principles:
|
||||||
|
* - Single Responsibility: Each profile handles only its own formatting
|
||||||
|
* - Open/Closed: Add new profiles without modifying existing code
|
||||||
|
* - Liskov Substitution: All profiles are interchangeable via base class
|
||||||
|
* - Interface Segregation: Base class defines minimal interface
|
||||||
|
* - Dependency Inversion: Consumers depend on abstraction, not concrete profiles
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { CursorProfile } from '@tm/profiles';
|
||||||
|
* import { allCommands } from '@tm/profiles';
|
||||||
|
*
|
||||||
|
* const cursor = new CursorProfile();
|
||||||
|
* cursor.addSlashCommands('/path/to/project', allCommands);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export abstract class BaseSlashCommandProfile {
|
||||||
|
/** Profile identifier (lowercase, e.g., 'claude', 'cursor') */
|
||||||
|
abstract readonly name: string;
|
||||||
|
|
||||||
|
/** Display name for UI/logging (e.g., 'Claude Code', 'Cursor') */
|
||||||
|
abstract readonly displayName: string;
|
||||||
|
|
||||||
|
/** Commands directory relative to project root (e.g., '.claude/commands') */
|
||||||
|
abstract readonly commandsDir: string;
|
||||||
|
|
||||||
|
/** File extension for command files (e.g., '.md') */
|
||||||
|
abstract readonly extension: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this profile supports nested command directories.
|
||||||
|
* - true: Commands go in a subdirectory (e.g., `.claude/commands/tm/help.md`)
|
||||||
|
* - false: Commands use a prefix (e.g., `.opencode/command/tm-help.md`)
|
||||||
|
*
|
||||||
|
* Override in profiles that don't support nested directories.
|
||||||
|
*/
|
||||||
|
readonly supportsNestedCommands: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this profile supports slash commands.
|
||||||
|
* Profiles with empty commandsDir do not support commands.
|
||||||
|
*/
|
||||||
|
get supportsCommands(): boolean {
|
||||||
|
return this.commandsDir !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a single command for this profile.
|
||||||
|
* Each profile implements its own formatting logic.
|
||||||
|
*
|
||||||
|
* @param command - The slash command to format
|
||||||
|
* @returns Formatted command ready to write to file
|
||||||
|
*/
|
||||||
|
abstract format(command: SlashCommand): FormattedSlashCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format all commands for this profile.
|
||||||
|
*
|
||||||
|
* @param commands - Array of slash commands to format
|
||||||
|
* @returns Array of formatted commands
|
||||||
|
*/
|
||||||
|
formatAll(commands: SlashCommand[]): FormattedSlashCommand[] {
|
||||||
|
return commands.map((cmd) => this.format(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full filename for a command.
|
||||||
|
* - Nested profiles: `commandName.md` (goes in tm/ subdirectory)
|
||||||
|
* - Flat profiles: `tm-commandName.md` (uses prefix)
|
||||||
|
*
|
||||||
|
* @param commandName - The command name (without extension)
|
||||||
|
* @returns Full filename with extension
|
||||||
|
*/
|
||||||
|
getFilename(commandName: string): string {
|
||||||
|
if (this.supportsNestedCommands) {
|
||||||
|
return `${commandName}${this.extension}`;
|
||||||
|
}
|
||||||
|
return `${TM_NAMESPACE}-${commandName}${this.extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the argument placeholder if needed.
|
||||||
|
* Override in profiles that use different placeholder syntax.
|
||||||
|
*
|
||||||
|
* @param content - The command content
|
||||||
|
* @returns Content with transformed placeholders
|
||||||
|
*/
|
||||||
|
transformArgumentPlaceholder(content: string): string {
|
||||||
|
return content; // Default: no transformation ($ARGUMENTS stays as-is)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for additional post-processing after formatting.
|
||||||
|
* Override for profile-specific transformations.
|
||||||
|
*
|
||||||
|
* @param content - The formatted content
|
||||||
|
* @returns Post-processed content
|
||||||
|
*/
|
||||||
|
postProcess(content: string): string {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the absolute path to the commands directory for a project.
|
||||||
|
* - Nested profiles: Returns `projectRoot/commandsDir/tm/`
|
||||||
|
* - Flat profiles: Returns `projectRoot/commandsDir/`
|
||||||
|
*
|
||||||
|
* @param projectRoot - Absolute path to the project root
|
||||||
|
* @returns Absolute path to the commands directory
|
||||||
|
*/
|
||||||
|
getCommandsPath(projectRoot: string): string {
|
||||||
|
if (this.supportsNestedCommands) {
|
||||||
|
return path.join(projectRoot, this.commandsDir, TM_NAMESPACE);
|
||||||
|
}
|
||||||
|
return path.join(projectRoot, this.commandsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add slash commands to a project.
|
||||||
|
*
|
||||||
|
* Formats and writes all provided commands to the profile's commands directory.
|
||||||
|
* Creates the directory if it doesn't exist.
|
||||||
|
*
|
||||||
|
* @param projectRoot - Absolute path to the project root
|
||||||
|
* @param commands - Array of slash commands to add
|
||||||
|
* @param options - Options including mode filtering
|
||||||
|
* @returns Result of the operation
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const cursor = new CursorProfile();
|
||||||
|
* // Add all commands
|
||||||
|
* const result = cursor.addSlashCommands('/path/to/project', allCommands);
|
||||||
|
*
|
||||||
|
* // Add only solo mode commands
|
||||||
|
* const soloResult = cursor.addSlashCommands('/path/to/project', allCommands, { mode: 'solo' });
|
||||||
|
*
|
||||||
|
* // Add only team mode commands (exclusive)
|
||||||
|
* const teamResult = cursor.addSlashCommands('/path/to/project', allCommands, { mode: 'team' });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
addSlashCommands(
|
||||||
|
projectRoot: string,
|
||||||
|
commands: SlashCommand[],
|
||||||
|
options?: AddSlashCommandsOptions
|
||||||
|
): SlashCommandResult {
|
||||||
|
const commandsPath = this.getCommandsPath(projectRoot);
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
if (!this.supportsCommands) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
count: 0,
|
||||||
|
directory: commandsPath,
|
||||||
|
files: [],
|
||||||
|
error: `Profile "${this.name}" does not support slash commands`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When mode is specified, first remove ALL existing TaskMaster commands
|
||||||
|
// to ensure clean slate (prevents orphaned commands when switching modes)
|
||||||
|
if (options?.mode) {
|
||||||
|
this.removeSlashCommands(projectRoot, commands, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter commands by mode if specified
|
||||||
|
const filteredCommands = options?.mode
|
||||||
|
? filterCommandsByMode(commands, options.mode)
|
||||||
|
: commands;
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(commandsPath)) {
|
||||||
|
fs.mkdirSync(commandsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format and write each command
|
||||||
|
const formatted = this.formatAll(filteredCommands);
|
||||||
|
for (const output of formatted) {
|
||||||
|
const filePath = path.join(commandsPath, output.filename);
|
||||||
|
fs.writeFileSync(filePath, output.content);
|
||||||
|
files.push(output.filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: files.length,
|
||||||
|
directory: commandsPath,
|
||||||
|
files
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
count: 0,
|
||||||
|
directory: commandsPath,
|
||||||
|
files: [],
|
||||||
|
error: err instanceof Error ? err.message : String(err)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove slash commands from a project.
|
||||||
|
*
|
||||||
|
* Removes only the commands that match the provided command names.
|
||||||
|
* Preserves user's custom commands that are not in the list.
|
||||||
|
* Optionally removes the directory if empty after removal.
|
||||||
|
*
|
||||||
|
* @param projectRoot - Absolute path to the project root
|
||||||
|
* @param commands - Array of slash commands to remove (matches by name)
|
||||||
|
* @param removeEmptyDir - Whether to remove the directory if empty (default: true)
|
||||||
|
* @returns Result of the operation
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const cursor = new CursorProfile();
|
||||||
|
* const result = cursor.removeSlashCommands('/path/to/project', allCommands);
|
||||||
|
* console.log(`Removed ${result.count} commands`);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
removeSlashCommands(
|
||||||
|
projectRoot: string,
|
||||||
|
commands: SlashCommand[],
|
||||||
|
removeEmptyDir: boolean = true
|
||||||
|
): SlashCommandResult {
|
||||||
|
const commandsPath = this.getCommandsPath(projectRoot);
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
if (!this.supportsCommands) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
count: 0,
|
||||||
|
directory: commandsPath,
|
||||||
|
files: [],
|
||||||
|
error: `Profile "${this.name}" does not support slash commands`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(commandsPath)) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: 0,
|
||||||
|
directory: commandsPath,
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get command names to remove (with appropriate prefix for flat profiles)
|
||||||
|
const commandNames = new Set(
|
||||||
|
commands.map((cmd) => {
|
||||||
|
const name = cmd.metadata.name.toLowerCase();
|
||||||
|
// For flat profiles, filenames have tm- prefix
|
||||||
|
return this.supportsNestedCommands ? name : `${TM_NAMESPACE}-${name}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all files in directory
|
||||||
|
const existingFiles = fs.readdirSync(commandsPath);
|
||||||
|
|
||||||
|
for (const file of existingFiles) {
|
||||||
|
const baseName = path.basename(file, path.extname(file)).toLowerCase();
|
||||||
|
|
||||||
|
// Only remove files that match our command names
|
||||||
|
if (commandNames.has(baseName)) {
|
||||||
|
const filePath = path.join(commandsPath, file);
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove directory if empty and requested
|
||||||
|
if (removeEmptyDir) {
|
||||||
|
const remainingFiles = fs.readdirSync(commandsPath);
|
||||||
|
if (remainingFiles.length === 0) {
|
||||||
|
fs.rmSync(commandsPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: files.length,
|
||||||
|
directory: commandsPath,
|
||||||
|
files
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
count: files.length,
|
||||||
|
directory: commandsPath,
|
||||||
|
files,
|
||||||
|
error: err instanceof Error ? err.message : String(err)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace slash commands for a new operating mode.
|
||||||
|
*
|
||||||
|
* Removes all existing TaskMaster commands and adds commands for the new mode.
|
||||||
|
* This is useful when switching between solo and team modes.
|
||||||
|
*
|
||||||
|
* @param projectRoot - Absolute path to the project root
|
||||||
|
* @param commands - Array of all slash commands (will be filtered by mode)
|
||||||
|
* @param newMode - The new operating mode to switch to
|
||||||
|
* @returns Result of the operation
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const cursor = new CursorProfile();
|
||||||
|
* // Switch from solo to team mode
|
||||||
|
* const result = cursor.replaceSlashCommands('/path/to/project', allCommands, 'team');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
replaceSlashCommands(
|
||||||
|
projectRoot: string,
|
||||||
|
commands: SlashCommand[],
|
||||||
|
newMode: 'solo' | 'team'
|
||||||
|
): SlashCommandResult {
|
||||||
|
// Remove all existing TaskMaster commands
|
||||||
|
const removeResult = this.removeSlashCommands(projectRoot, commands);
|
||||||
|
if (!removeResult.success) {
|
||||||
|
return removeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add commands for the new mode
|
||||||
|
return this.addSlashCommands(projectRoot, commands, { mode: newMode });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { ClaudeProfile } from './claude-profile.js';
|
||||||
|
import { staticCommand, dynamicCommand } from '../factories.js';
|
||||||
|
|
||||||
|
describe('ClaudeProfile', () => {
|
||||||
|
describe('Profile metadata', () => {
|
||||||
|
it('should have correct name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.name).toBe('claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct displayName', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.displayName).toBe('Claude Code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct commandsDir', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.commandsDir).toBe('.claude/commands');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsCommands', () => {
|
||||||
|
it('should return true when commandsDir is not empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.supportsCommands;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilename', () => {
|
||||||
|
it('should append .md extension to command name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const commandName = 'goham';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.getFilename(commandName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('goham.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with hyphens', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const commandName = 'my-command';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.getFilename(commandName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('my-command.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character command names', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const commandName = 'x';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.getFilename(commandName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('x.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() for static commands', () => {
|
||||||
|
it('should format static command with description on first line', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'goham',
|
||||||
|
description: 'Start Working with Hamster Brief',
|
||||||
|
content: '# Start Working...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'Start Working with Hamster Brief\n# Start Working...'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct filename for static command', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help\n\nList of commands...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('help.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle static command with empty content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'empty',
|
||||||
|
description: 'Empty command',
|
||||||
|
content: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe('Empty command\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle static command with multiline content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'multi',
|
||||||
|
description: 'Multiline command',
|
||||||
|
content: '# Title\n\nParagraph 1\n\nParagraph 2'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'Multiline command\n# Title\n\nParagraph 1\n\nParagraph 2'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include Arguments line for static command with argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'goham',
|
||||||
|
description: 'Start Working with Hamster Brief',
|
||||||
|
argumentHint: '[brief-url]',
|
||||||
|
content: '# Start Working...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Static commands with argumentHint should include Arguments line
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'Start Working with Hamster Brief\n\nArguments: $ARGUMENTS\n# Start Working...'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() for dynamic commands', () => {
|
||||||
|
it('should format dynamic command with Arguments line', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'help',
|
||||||
|
'Help',
|
||||||
|
'[command]',
|
||||||
|
'Show help for Task Master AI commands...\n\nCommand: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'Help\n\nArguments: $ARGUMENTS\nShow help for Task Master AI commands...\n\nCommand: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct filename for dynamic command', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search codebase',
|
||||||
|
'<query>',
|
||||||
|
'Search for: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('search.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve $ARGUMENTS placeholder in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'run',
|
||||||
|
'Run command',
|
||||||
|
'<cmd>',
|
||||||
|
'Execute: $ARGUMENTS\n\nDone!'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('Execute: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle dynamic command with multiple $ARGUMENTS placeholders', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'repeat',
|
||||||
|
'Repeat input',
|
||||||
|
'<text>',
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS\nThird: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const placeholderCount = (result.content.match(/\$ARGUMENTS/g) || [])
|
||||||
|
.length;
|
||||||
|
// Header has 1 + content has 3 = 4 total
|
||||||
|
expect(placeholderCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include empty line between description and Arguments line', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'test',
|
||||||
|
'Test description',
|
||||||
|
'<arg>',
|
||||||
|
'Content with $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const lines = result.content.split('\n');
|
||||||
|
expect(lines[0]).toBe('Test description');
|
||||||
|
expect(lines[1]).toBe('');
|
||||||
|
expect(lines[2]).toBe('Arguments: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include empty line between Arguments line and content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'test',
|
||||||
|
'Test',
|
||||||
|
'<arg>',
|
||||||
|
'Content with $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const lines = result.content.split('\n');
|
||||||
|
expect(lines[2]).toBe('Arguments: $ARGUMENTS');
|
||||||
|
expect(lines[3]).toBe('Content with $ARGUMENTS');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() output structure', () => {
|
||||||
|
it('should return object with filename and content properties', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toHaveProperty('filename');
|
||||||
|
expect(result).toHaveProperty('content');
|
||||||
|
expect(typeof result.filename).toBe('string');
|
||||||
|
expect(typeof result.content).toBe('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAll()', () => {
|
||||||
|
it('should format multiple commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'Command 1',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'cmd2',
|
||||||
|
'Command 2',
|
||||||
|
'<arg>',
|
||||||
|
'Content 2 with $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].filename).toBe('cmd1.md');
|
||||||
|
expect(results[1].filename).toBe('cmd2.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll([]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Claude Code Profile
|
||||||
|
* Slash command profile for Claude Code.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ```
|
||||||
|
* ${description}
|
||||||
|
*
|
||||||
|
* Arguments: $ARGUMENTS
|
||||||
|
*
|
||||||
|
* [content]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Location: .claude/commands/*.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
import type { SlashCommand, FormattedSlashCommand } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude Code profile for slash commands.
|
||||||
|
*
|
||||||
|
* Claude Code uses a simple format with the description as the first line,
|
||||||
|
* followed by an optional "Arguments: $ARGUMENTS" line for dynamic commands,
|
||||||
|
* then the main content.
|
||||||
|
*/
|
||||||
|
export class ClaudeProfile extends BaseSlashCommandProfile {
|
||||||
|
readonly name = 'claude';
|
||||||
|
readonly displayName = 'Claude Code';
|
||||||
|
readonly commandsDir = '.claude/commands';
|
||||||
|
readonly extension = '.md';
|
||||||
|
|
||||||
|
format(command: SlashCommand): FormattedSlashCommand {
|
||||||
|
const header = this.buildHeader(command);
|
||||||
|
const content = this.transformArgumentPlaceholder(command.content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: this.getFilename(command.metadata.name),
|
||||||
|
content: `${header}${content}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the header section for Claude Code format.
|
||||||
|
* Includes description and optional Arguments line.
|
||||||
|
*/
|
||||||
|
private buildHeader(command: SlashCommand): string {
|
||||||
|
const lines = [command.metadata.description, ''];
|
||||||
|
|
||||||
|
// Claude uses "Arguments: $ARGUMENTS" on second line for dynamic commands
|
||||||
|
if (command.metadata.argumentHint) {
|
||||||
|
lines.push('Arguments: $ARGUMENTS');
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit tests for CodexProfile
|
||||||
|
* Tests the Codex CLI slash command profile formatting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { dynamicCommand, staticCommand } from '../factories.js';
|
||||||
|
import { CodexProfile } from './codex-profile.js';
|
||||||
|
|
||||||
|
describe('CodexProfile', () => {
|
||||||
|
describe('Profile metadata', () => {
|
||||||
|
it('should have correct profile name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.name).toBe('codex');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct display name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.displayName).toBe('Codex');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.commandsDir).toBe('.codex/prompts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have .md file extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsCommands getter', () => {
|
||||||
|
it('should return true when commandsDir is set', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.supportsCommands;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsNestedCommands property', () => {
|
||||||
|
it('should be false for Codex profile (uses tm- prefix)', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act & Assert - Codex uses flat namespace with tm- prefix
|
||||||
|
expect(profile.supportsNestedCommands).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilename()', () => {
|
||||||
|
it('should prepend tm- prefix and append .md extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('help');
|
||||||
|
|
||||||
|
// Assert - Codex uses flat namespace with tm- prefix
|
||||||
|
expect(filename).toBe('tm-help.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with hyphens', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('task-status');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-task-status.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with underscores', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('get_tasks');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-get_tasks.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() with static commands', () => {
|
||||||
|
it('should format static command without argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help\n\nThis is the help content.'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-help.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\n' +
|
||||||
|
'description: "Show available commands"\n' +
|
||||||
|
'---\n' +
|
||||||
|
'# Help\n\n' +
|
||||||
|
'This is the help content.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format static command with argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'goham',
|
||||||
|
description: 'Start Working with Hamster Brief',
|
||||||
|
argumentHint: '[brief-url]',
|
||||||
|
content: '# Start Working\n\nBegin your task.'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-goham.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\n' +
|
||||||
|
'description: "Start Working with Hamster Brief"\n' +
|
||||||
|
'argument-hint: "[brief-url]"\n' +
|
||||||
|
'---\n' +
|
||||||
|
'# Start Working\n\n' +
|
||||||
|
'Begin your task.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include YAML frontmatter delimiter correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content here'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toMatch(/^---\n/);
|
||||||
|
expect(result.content).toMatch(/\n---\n/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const multilineContent =
|
||||||
|
'# Title\n\n## Section 1\n\nParagraph one.\n\n## Section 2\n\nParagraph two.';
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'docs',
|
||||||
|
description: 'Documentation command',
|
||||||
|
content: multilineContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(multilineContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() with dynamic commands', () => {
|
||||||
|
it('should format dynamic command with $ARGUMENTS placeholder', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search for items',
|
||||||
|
'<query>',
|
||||||
|
'Search for: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-search.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\n' +
|
||||||
|
'description: "Search for items"\n' +
|
||||||
|
'argument-hint: "<query>"\n' +
|
||||||
|
'---\n' +
|
||||||
|
'Search for: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always include argument-hint for dynamic commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'task',
|
||||||
|
'Manage tasks',
|
||||||
|
'[task-id]',
|
||||||
|
'Task ID: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('argument-hint: "[task-id]"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiple $ARGUMENTS placeholders in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'compare',
|
||||||
|
'Compare items',
|
||||||
|
'<id1> <id2>',
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('First: $ARGUMENTS');
|
||||||
|
expect(result.content).toContain('Second: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() edge cases', () => {
|
||||||
|
it('should handle description with double quotes', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'quoted',
|
||||||
|
description: 'Command with "quoted" text',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(
|
||||||
|
'description: "Command with \\"quoted\\" text"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'empty',
|
||||||
|
description: 'Empty content command',
|
||||||
|
content: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-empty.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\n' + 'description: "Empty content command"\n' + '---\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle content that starts with frontmatter-like syntax', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'nested',
|
||||||
|
description: 'Nested frontmatter test',
|
||||||
|
content: '---\nsome: yaml\n---\nActual content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// The profile should add its own frontmatter, preserving the content as-is
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\n' +
|
||||||
|
'description: "Nested frontmatter test"\n' +
|
||||||
|
'---\n' +
|
||||||
|
'---\n' +
|
||||||
|
'some: yaml\n' +
|
||||||
|
'---\n' +
|
||||||
|
'Actual content'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle special characters in argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Special args',
|
||||||
|
argumentHint: '<file-path|url> [--flag]',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(
|
||||||
|
'argument-hint: "<file-path|url> [--flag]"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAll()', () => {
|
||||||
|
it('should format multiple commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: 'Help content'
|
||||||
|
}),
|
||||||
|
dynamicCommand('search', 'Search items', '<query>', 'Query: $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].filename).toBe('tm-help.md');
|
||||||
|
expect(results[1].filename).toBe('tm-search.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll([]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isHomeRelative property', () => {
|
||||||
|
it('should be true indicating home directory usage', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.isHomeRelative).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor options', () => {
|
||||||
|
it('should use os.homedir() by default', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.getCommandsPath('/any/path');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(path.join(os.homedir(), '.codex/prompts'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use provided homeDir option when specified', () => {
|
||||||
|
// Arrange
|
||||||
|
const customHomeDir = '/custom/home';
|
||||||
|
const profile = new CodexProfile({ homeDir: customHomeDir });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.getCommandsPath('/any/path');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('/custom/home/.codex/prompts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommandsPath()', () => {
|
||||||
|
it('should return path in user home directory, ignoring projectRoot', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
const projectRoot = '/Users/test/my-project';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert - Codex uses ~/.codex/prompts, not project-relative
|
||||||
|
expect(result).toBe(path.join(os.homedir(), '.codex/prompts'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return same path regardless of projectRoot value', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result1 = profile.getCommandsPath('/project/a');
|
||||||
|
const result2 = profile.getCommandsPath('/project/b');
|
||||||
|
|
||||||
|
// Assert - Both should return the same home directory path
|
||||||
|
expect(result1).toBe(result2);
|
||||||
|
expect(result1).toBe(path.join(os.homedir(), '.codex/prompts'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Codex Profile
|
||||||
|
* Slash command profile for OpenAI Codex CLI.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ```
|
||||||
|
* ---
|
||||||
|
* description: "..."
|
||||||
|
* argument-hint: "..."
|
||||||
|
* ---
|
||||||
|
* [content]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Location: ~/.codex/prompts/*.md (user's home directory)
|
||||||
|
*
|
||||||
|
* Note: Unlike other profiles, Codex stores prompts in the user's home directory,
|
||||||
|
* not project-relative. This is how Codex CLI discovers custom prompts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
import type { SlashCommand, FormattedSlashCommand } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for CodexProfile constructor.
|
||||||
|
*/
|
||||||
|
export interface CodexProfileOptions {
|
||||||
|
/**
|
||||||
|
* Override the home directory path.
|
||||||
|
* Used primarily for testing to avoid modifying the real home directory.
|
||||||
|
* If not provided, uses os.homedir().
|
||||||
|
*/
|
||||||
|
homeDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codex CLI profile for slash commands.
|
||||||
|
*
|
||||||
|
* Codex uses YAML frontmatter format with description and optional argument-hint.
|
||||||
|
*/
|
||||||
|
export class CodexProfile extends BaseSlashCommandProfile {
|
||||||
|
readonly name = 'codex';
|
||||||
|
readonly displayName = 'Codex';
|
||||||
|
readonly commandsDir = '.codex/prompts';
|
||||||
|
readonly extension = '.md';
|
||||||
|
readonly supportsNestedCommands = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this profile uses the user's home directory instead of project root.
|
||||||
|
* Codex CLI reads prompts from ~/.codex/prompts, not project-relative paths.
|
||||||
|
*/
|
||||||
|
readonly isHomeRelative = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The home directory to use for command paths.
|
||||||
|
* Defaults to os.homedir() but can be overridden for testing.
|
||||||
|
*/
|
||||||
|
private readonly homeDir: string;
|
||||||
|
|
||||||
|
constructor(options?: CodexProfileOptions) {
|
||||||
|
super();
|
||||||
|
this.homeDir = options?.homeDir ?? os.homedir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to return home directory path instead of project-relative path.
|
||||||
|
* Codex CLI reads prompts from ~/.codex/prompts.
|
||||||
|
*
|
||||||
|
* @param _projectRoot - Ignored for Codex (uses home directory)
|
||||||
|
* @returns Absolute path to ~/.codex/prompts
|
||||||
|
*/
|
||||||
|
override getCommandsPath(_projectRoot: string): string {
|
||||||
|
return path.join(this.homeDir, this.commandsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
format(command: SlashCommand): FormattedSlashCommand {
|
||||||
|
const frontmatter = this.buildFrontmatter(command);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: this.getFilename(command.metadata.name),
|
||||||
|
content: `${frontmatter}${command.content}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildFrontmatter(command: SlashCommand): string {
|
||||||
|
const escapeQuotes = (str: string): string => str.replace(/"/g, '\\"');
|
||||||
|
const lines = [
|
||||||
|
'---',
|
||||||
|
`description: "${escapeQuotes(command.metadata.description)}"`
|
||||||
|
];
|
||||||
|
|
||||||
|
// Include argument-hint if present
|
||||||
|
if (command.metadata.argumentHint) {
|
||||||
|
lines.push(
|
||||||
|
`argument-hint: "${escapeQuotes(command.metadata.argumentHint)}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('---', '');
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit Tests for CursorProfile
|
||||||
|
* Tests the Cursor slash command profile formatting and metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { CursorProfile } from './cursor-profile.js';
|
||||||
|
import { staticCommand, dynamicCommand } from '../factories.js';
|
||||||
|
|
||||||
|
describe('CursorProfile', () => {
|
||||||
|
describe('Profile Metadata', () => {
|
||||||
|
it('should have correct profile name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.name).toBe('cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct display name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.displayName).toBe('Cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.commandsDir).toBe('.cursor/commands');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct file extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsCommands getter', () => {
|
||||||
|
it('should return true when commandsDir is non-empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.supportsCommands;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilename() method', () => {
|
||||||
|
it('should append .md extension to command name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('help');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('help.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with hyphens', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my-command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('my-command.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with underscores', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my_command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('my_command.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for static commands', () => {
|
||||||
|
it('should return content unchanged for simple static command', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help\n\nList of available commands...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('help.md');
|
||||||
|
expect(result.content).toBe('# Help\n\nList of available commands...');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content exactly', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const multilineContent = `# Task Runner
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Run automated tasks for the project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Check dependencies
|
||||||
|
2. Run build
|
||||||
|
3. Execute tests
|
||||||
|
4. Generate report`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'task-runner',
|
||||||
|
description: 'Run automated tasks',
|
||||||
|
content: multilineContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('task-runner.md');
|
||||||
|
expect(result.content).toBe(multilineContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve static command with argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'analyze',
|
||||||
|
description: 'Analyze codebase',
|
||||||
|
argumentHint: '[path]',
|
||||||
|
content: '# Analyze\n\nAnalyze the specified path.'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('analyze.md');
|
||||||
|
expect(result.content).toBe('# Analyze\n\nAnalyze the specified path.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve code blocks in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const contentWithCode = `# Deploy
|
||||||
|
|
||||||
|
Run the deployment:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npm run deploy
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Done!`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy the application',
|
||||||
|
content: contentWithCode
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(contentWithCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve special characters in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const contentWithSpecialChars =
|
||||||
|
'# Special\n\nUse `$HOME` and `$PATH` variables. Also: <tag> & "quotes"';
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Command with special chars',
|
||||||
|
content: contentWithSpecialChars
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(contentWithSpecialChars);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for dynamic commands', () => {
|
||||||
|
it('should preserve $ARGUMENTS placeholder unchanged', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'review',
|
||||||
|
'Review a pull request',
|
||||||
|
'<pr-number>',
|
||||||
|
'# Review PR\n\nReviewing PR: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('review.md');
|
||||||
|
expect(result.content).toBe('# Review PR\n\nReviewing PR: $ARGUMENTS');
|
||||||
|
expect(result.content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiple $ARGUMENTS placeholders', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'compare',
|
||||||
|
'Compare two items',
|
||||||
|
'<item1> <item2>',
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
expect(result.content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve $ARGUMENTS in complex markdown content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const complexContent = `# Search Command
|
||||||
|
|
||||||
|
## Input
|
||||||
|
User provided: $ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Parse the input: \`$ARGUMENTS\`
|
||||||
|
2. Search for matches
|
||||||
|
3. Display results
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Query: $ARGUMENTS
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search the codebase',
|
||||||
|
'<query>',
|
||||||
|
complexContent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(complexContent);
|
||||||
|
expect(result.content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAll() method', () => {
|
||||||
|
it('should format multiple commands correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help Content'
|
||||||
|
}),
|
||||||
|
dynamicCommand('run', 'Run a command', '<cmd>', 'Running: $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].filename).toBe('help.md');
|
||||||
|
expect(results[0].content).toBe('# Help Content');
|
||||||
|
expect(results[1].filename).toBe('run.md');
|
||||||
|
expect(results[1].content).toBe('Running: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll([]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommandsPath() method', () => {
|
||||||
|
it('should return correct absolute path for commands directory with tm subdirectory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const projectRoot = '/home/user/my-project';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert - Cursor supports nested commands, so path includes tm/ subdirectory
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.cursor/commands/tm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle project root with trailing slash', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
const projectRoot = '/home/user/my-project/';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert - path.join normalizes the path, includes tm/ subdirectory
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.cursor/commands/tm');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsNestedCommands property', () => {
|
||||||
|
it('should be true for Cursor profile', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Assert - Cursor supports nested command directories
|
||||||
|
expect(profile.supportsNestedCommands).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Cursor Profile
|
||||||
|
* Slash command profile for Cursor.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ```
|
||||||
|
* [content as-is]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Cursor uses plain markdown format with no header or transformation.
|
||||||
|
*
|
||||||
|
* Location: .cursor/commands/*.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
import type { SlashCommand, FormattedSlashCommand } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor profile for slash commands.
|
||||||
|
*
|
||||||
|
* Cursor uses plain markdown format - commands are written as-is
|
||||||
|
* without any header or transformation. The content is simply
|
||||||
|
* passed through directly.
|
||||||
|
*/
|
||||||
|
export class CursorProfile extends BaseSlashCommandProfile {
|
||||||
|
readonly name = 'cursor';
|
||||||
|
readonly displayName = 'Cursor';
|
||||||
|
readonly commandsDir = '.cursor/commands';
|
||||||
|
readonly extension = '.md';
|
||||||
|
|
||||||
|
format(command: SlashCommand): FormattedSlashCommand {
|
||||||
|
return {
|
||||||
|
filename: this.getFilename(command.metadata.name),
|
||||||
|
content: command.content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,675 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit Tests for GeminiProfile
|
||||||
|
* Tests the Gemini CLI slash command profile formatting and metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { dynamicCommand, staticCommand } from '../factories.js';
|
||||||
|
import { GeminiProfile } from './gemini-profile.js';
|
||||||
|
|
||||||
|
describe('GeminiProfile', () => {
|
||||||
|
describe('Profile Metadata', () => {
|
||||||
|
it('should have correct profile name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.name).toBe('gemini');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct display name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.displayName).toBe('Gemini');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.commandsDir).toBe('.gemini/commands');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct file extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.extension).toBe('.toml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsCommands getter', () => {
|
||||||
|
it('should return true when commandsDir is non-empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.supportsCommands;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilename() method', () => {
|
||||||
|
it('should append .toml extension to command name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('help');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('help.toml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with hyphens', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my-command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('my-command.toml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with underscores', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my_command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('my_command.toml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character command names', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('x');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('x.toml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for static commands', () => {
|
||||||
|
it('should format simple static command with description and prompt', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help\n\nList of available commands...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('help.toml');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Show available commands"\nprompt = """\n# Help\n\nList of available commands...\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim content inside prompt block', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test command',
|
||||||
|
content: ' \n# Test Content\n\n '
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Test command"\nprompt = """\n# Test Content\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content in prompt block', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const multilineContent = `# Task Runner
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Run automated tasks for the project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Check dependencies
|
||||||
|
2. Run build
|
||||||
|
3. Execute tests
|
||||||
|
4. Generate report`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'task-runner',
|
||||||
|
description: 'Run automated tasks',
|
||||||
|
content: multilineContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('task-runner.toml');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
`description="Run automated tasks"
|
||||||
|
prompt = """
|
||||||
|
${multilineContent}
|
||||||
|
"""
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape double quotes in description', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test "quoted" description',
|
||||||
|
content: '# Test Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Test \\"quoted\\" description"\nprompt = """\n# Test Content\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape multiple double quotes in description', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Use "this" and "that" and "other"',
|
||||||
|
content: '# Test'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(
|
||||||
|
'description="Use \\"this\\" and \\"that\\" and \\"other\\"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve static command with argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'analyze',
|
||||||
|
description: 'Analyze codebase',
|
||||||
|
argumentHint: '[path]',
|
||||||
|
content: '# Analyze\n\nAnalyze the specified path.'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('analyze.toml');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Analyze codebase"\nprompt = """\n# Analyze\n\nAnalyze the specified path.\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve code blocks in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const contentWithCode = `# Deploy
|
||||||
|
|
||||||
|
Run the deployment:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npm run deploy
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Done!`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy the application',
|
||||||
|
content: contentWithCode
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('```bash');
|
||||||
|
expect(result.content).toContain('npm run deploy');
|
||||||
|
expect(result.content).toContain('```');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve special characters in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const contentWithSpecialChars =
|
||||||
|
'# Special\n\nUse `$HOME` and `$PATH` variables. Also: <tag> & "quotes"';
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Command with special chars',
|
||||||
|
content: contentWithSpecialChars
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('$HOME');
|
||||||
|
expect(result.content).toContain('$PATH');
|
||||||
|
expect(result.content).toContain('<tag>');
|
||||||
|
expect(result.content).toContain('&');
|
||||||
|
expect(result.content).toContain('"quotes"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'empty',
|
||||||
|
description: 'Empty command',
|
||||||
|
content: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Empty command"\nprompt = """\n\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for dynamic commands', () => {
|
||||||
|
it('should format dynamic command with $ARGUMENTS placeholder', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'review',
|
||||||
|
'Review a pull request',
|
||||||
|
'<pr-number>',
|
||||||
|
'# Review PR\n\nReviewing PR: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('review.toml');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Review a pull request"\nprompt = """\n# Review PR\n\nReviewing PR: $ARGUMENTS\n"""\n'
|
||||||
|
);
|
||||||
|
expect(result.content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiple $ARGUMENTS placeholders', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'compare',
|
||||||
|
'Compare two items',
|
||||||
|
'<item1> <item2>',
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const placeholderCount = (result.content.match(/\$ARGUMENTS/g) || [])
|
||||||
|
.length;
|
||||||
|
expect(placeholderCount).toBe(3);
|
||||||
|
expect(result.content).toContain('First: $ARGUMENTS');
|
||||||
|
expect(result.content).toContain('Second: $ARGUMENTS');
|
||||||
|
expect(result.content).toContain('Both: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve $ARGUMENTS in complex markdown content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const complexContent = `# Search Command
|
||||||
|
|
||||||
|
## Input
|
||||||
|
User provided: $ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Parse the input: \`$ARGUMENTS\`
|
||||||
|
2. Search for matches
|
||||||
|
3. Display results
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Query: $ARGUMENTS
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search the codebase',
|
||||||
|
'<query>',
|
||||||
|
complexContent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const placeholderCount = (result.content.match(/\$ARGUMENTS/g) || [])
|
||||||
|
.length;
|
||||||
|
expect(placeholderCount).toBe(3);
|
||||||
|
expect(result.content).toContain('User provided: $ARGUMENTS');
|
||||||
|
expect(result.content).toContain('Parse the input: `$ARGUMENTS`');
|
||||||
|
expect(result.content).toContain('Query: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape quotes in dynamic command description', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'run',
|
||||||
|
'Run "command" with args',
|
||||||
|
'<cmd>',
|
||||||
|
'Running: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(
|
||||||
|
'description="Run \\"command\\" with args"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim content in dynamic commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'test',
|
||||||
|
'Test command',
|
||||||
|
'<arg>',
|
||||||
|
' \nContent: $ARGUMENTS\n '
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'description="Test command"\nprompt = """\nContent: $ARGUMENTS\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() output structure', () => {
|
||||||
|
it('should return object with filename and content properties', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toHaveProperty('filename');
|
||||||
|
expect(result).toHaveProperty('content');
|
||||||
|
expect(typeof result.filename).toBe('string');
|
||||||
|
expect(typeof result.content).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have consistent format structure across different commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'Command 1',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
dynamicCommand('cmd2', 'Command 2', '<arg>', 'Content 2 $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = commands.map((cmd) => profile.format(cmd));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
results.forEach((result) => {
|
||||||
|
expect(result.content).toMatch(
|
||||||
|
/^description=".*"\nprompt = """\n[\s\S]*\n"""\n$/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAll() method', () => {
|
||||||
|
it('should format multiple commands correctly', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help Content'
|
||||||
|
}),
|
||||||
|
dynamicCommand('run', 'Run a command', '<cmd>', 'Running: $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].filename).toBe('help.toml');
|
||||||
|
expect(results[0].content).toContain('description="Show help"');
|
||||||
|
expect(results[1].filename).toBe('run.toml');
|
||||||
|
expect(results[1].content).toContain('description="Run a command"');
|
||||||
|
expect(results[1].content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll([]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed static and dynamic commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static1',
|
||||||
|
description: 'Static command 1',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
dynamicCommand('dynamic1', 'Dynamic command 1', '<arg>', '$ARGUMENTS'),
|
||||||
|
staticCommand({
|
||||||
|
name: 'static2',
|
||||||
|
description: 'Static command 2',
|
||||||
|
content: 'Content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(3);
|
||||||
|
expect(results[0].filename).toBe('static1.toml');
|
||||||
|
expect(results[1].filename).toBe('dynamic1.toml');
|
||||||
|
expect(results[2].filename).toBe('static2.toml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommandsPath() method', () => {
|
||||||
|
it('should return correct absolute path for commands directory with tm subdirectory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const projectRoot = '/home/user/my-project';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.gemini/commands/tm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle project root with trailing slash', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const projectRoot = '/home/user/my-project/';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// path.join normalizes the path
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.gemini/commands/tm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Windows-style paths', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const projectRoot = 'C:\\Users\\user\\my-project';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(commandsPath).toContain('.gemini');
|
||||||
|
expect(commandsPath).toContain('commands');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('escapeForTripleQuotedString() edge cases', () => {
|
||||||
|
it('should escape triple quotes in content to prevent TOML delimiter break', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content with """ triple quotes'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// The triple quotes should be escaped so they don't break the TOML delimiter
|
||||||
|
expect(result.content).not.toContain('Content with """ triple quotes');
|
||||||
|
expect(result.content).toContain('Content with ""\\" triple quotes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape multiple triple quote sequences in content', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'First """ and second """ here'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('First ""\\" and second ""\\" here');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle content that is just triple quotes', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test command',
|
||||||
|
content: '"""'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('prompt = """\n""\\"\n"""');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('escapeForPython() edge cases', () => {
|
||||||
|
it('should handle description with only quotes', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: '"""',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain('description="\\"\\"\\""');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle description with mixed quotes and text', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Start "working" on "task" now',
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(
|
||||||
|
'description="Start \\"working\\" on \\"task\\" now"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not escape single quotes in description', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: "It's a test with 'single quotes'",
|
||||||
|
content: 'Content'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toContain(
|
||||||
|
"description=\"It's a test with 'single quotes'\""
|
||||||
|
);
|
||||||
|
expect(result.content).not.toContain("\\'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Gemini CLI Profile
|
||||||
|
* Slash command profile for Google Gemini CLI.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ```
|
||||||
|
* description="..."
|
||||||
|
* prompt = """
|
||||||
|
* [content]
|
||||||
|
* """
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Location: .gemini/commands/*.toml
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
import type { SlashCommand, FormattedSlashCommand } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gemini CLI profile for slash commands.
|
||||||
|
*
|
||||||
|
* Gemini uses a Python-style format with description and prompt fields.
|
||||||
|
* The prompt content is wrapped in triple quotes.
|
||||||
|
*/
|
||||||
|
export class GeminiProfile extends BaseSlashCommandProfile {
|
||||||
|
readonly name = 'gemini';
|
||||||
|
readonly displayName = 'Gemini';
|
||||||
|
readonly commandsDir = '.gemini/commands';
|
||||||
|
readonly extension = '.toml';
|
||||||
|
|
||||||
|
format(command: SlashCommand): FormattedSlashCommand {
|
||||||
|
const description = this.escapeForPython(command.metadata.description);
|
||||||
|
const content = this.escapeForTripleQuotedString(command.content.trim());
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: this.getFilename(command.metadata.name),
|
||||||
|
content: `description="${description}"
|
||||||
|
prompt = """
|
||||||
|
${content}
|
||||||
|
"""
|
||||||
|
`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape double quotes for Python string literals.
|
||||||
|
*/
|
||||||
|
private escapeForPython(str: string): string {
|
||||||
|
return str.replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape content for use inside triple-quoted strings.
|
||||||
|
* Prevents `"""` sequences from breaking the TOML delimiter.
|
||||||
|
*/
|
||||||
|
private escapeForTripleQuotedString(str: string): string {
|
||||||
|
return str.replace(/"""/g, '""\\"');
|
||||||
|
}
|
||||||
|
}
|
||||||
458
packages/tm-profiles/src/slash-commands/profiles/index.spec.ts
Normal file
458
packages/tm-profiles/src/slash-commands/profiles/index.spec.ts
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit tests for profile utility functions
|
||||||
|
*
|
||||||
|
* Tests the profile lookup and management functions exported from index.ts:
|
||||||
|
* - getProfile(name) - returns profile by name (case-insensitive)
|
||||||
|
* - getAllProfiles() - returns array of all profile instances
|
||||||
|
* - getProfileNames() - returns array of profile names
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
BaseSlashCommandProfile,
|
||||||
|
ClaudeProfile,
|
||||||
|
CodexProfile,
|
||||||
|
CursorProfile,
|
||||||
|
GeminiProfile,
|
||||||
|
OpenCodeProfile,
|
||||||
|
RooProfile,
|
||||||
|
getAllProfiles,
|
||||||
|
getProfile,
|
||||||
|
getProfileNames
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
|
describe('Profile Utility Functions', () => {
|
||||||
|
describe('getProfile', () => {
|
||||||
|
describe('returns correct profile for valid names', () => {
|
||||||
|
it('returns ClaudeProfile for "claude"', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'claude';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(ClaudeProfile);
|
||||||
|
expect(profile?.name).toBe('claude');
|
||||||
|
expect(profile?.displayName).toBe('Claude Code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns CursorProfile for "cursor"', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'cursor';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(CursorProfile);
|
||||||
|
expect(profile?.name).toBe('cursor');
|
||||||
|
expect(profile?.displayName).toBe('Cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns RooProfile for "roo"', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'roo';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(RooProfile);
|
||||||
|
expect(profile?.name).toBe('roo');
|
||||||
|
expect(profile?.displayName).toBe('Roo Code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns GeminiProfile for "gemini"', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'gemini';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(GeminiProfile);
|
||||||
|
expect(profile?.name).toBe('gemini');
|
||||||
|
expect(profile?.displayName).toBe('Gemini');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns CodexProfile for "codex"', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'codex';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(CodexProfile);
|
||||||
|
expect(profile?.name).toBe('codex');
|
||||||
|
expect(profile?.displayName).toBe('Codex');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('case insensitive lookup', () => {
|
||||||
|
it('returns ClaudeProfile for "CLAUDE" (uppercase)', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'CLAUDE';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(ClaudeProfile);
|
||||||
|
expect(profile?.name).toBe('claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns ClaudeProfile for "Claude" (title case)', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'Claude';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(ClaudeProfile);
|
||||||
|
expect(profile?.name).toBe('claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns ClaudeProfile for "cLaUdE" (mixed case)', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'cLaUdE';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(name);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(ClaudeProfile);
|
||||||
|
expect(profile?.name).toBe('claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles case insensitivity for other profiles', () => {
|
||||||
|
// Act & Assert
|
||||||
|
expect(getProfile('CURSOR')).toBeInstanceOf(CursorProfile);
|
||||||
|
expect(getProfile('Roo')).toBeInstanceOf(RooProfile);
|
||||||
|
expect(getProfile('GEMINI')).toBeInstanceOf(GeminiProfile);
|
||||||
|
expect(getProfile('CODEX')).toBeInstanceOf(CodexProfile);
|
||||||
|
expect(getProfile('OPENCODE')).toBeInstanceOf(OpenCodeProfile);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unknown profile handling', () => {
|
||||||
|
it('returns undefined for unknown profile name', () => {
|
||||||
|
// Arrange
|
||||||
|
const unknownName = 'unknown-profile';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(unknownName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined for empty string', () => {
|
||||||
|
// Arrange
|
||||||
|
const emptyName = '';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(emptyName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined for profile with typo', () => {
|
||||||
|
// Arrange
|
||||||
|
const typoName = 'cusor'; // missing 'r'
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = getProfile(typoName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllProfiles', () => {
|
||||||
|
it('returns an array', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(Array.isArray(profiles)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains 6 profiles', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profiles).toHaveLength(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each profile is a BaseSlashCommandProfile instance', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (const profile of profiles) {
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains all expected profile types', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const profileTypes = profiles.map((p) => p.constructor.name);
|
||||||
|
expect(profileTypes).toContain('ClaudeProfile');
|
||||||
|
expect(profileTypes).toContain('CursorProfile');
|
||||||
|
expect(profileTypes).toContain('RooProfile');
|
||||||
|
expect(profileTypes).toContain('GeminiProfile');
|
||||||
|
expect(profileTypes).toContain('CodexProfile');
|
||||||
|
expect(profileTypes).toContain('OpenCodeProfile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns new array reference on each call (defensive copy)', () => {
|
||||||
|
// Act
|
||||||
|
const profiles1 = getAllProfiles();
|
||||||
|
const profiles2 = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert - arrays should be different references
|
||||||
|
expect(profiles1).not.toBe(profiles2);
|
||||||
|
// But contain the same profile instances (singleton pattern)
|
||||||
|
expect(profiles1).toEqual(profiles2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each profile has required properties', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (const profile of profiles) {
|
||||||
|
expect(profile.name).toBeDefined();
|
||||||
|
expect(typeof profile.name).toBe('string');
|
||||||
|
expect(profile.displayName).toBeDefined();
|
||||||
|
expect(typeof profile.displayName).toBe('string');
|
||||||
|
expect(profile.commandsDir).toBeDefined();
|
||||||
|
expect(typeof profile.commandsDir).toBe('string');
|
||||||
|
expect(profile.extension).toBeDefined();
|
||||||
|
expect(typeof profile.extension).toBe('string');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each profile has supportsCommands === true', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = getAllProfiles();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (const profile of profiles) {
|
||||||
|
expect(profile.supportsCommands).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProfileNames', () => {
|
||||||
|
it('returns an array of strings', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(Array.isArray(names)).toBe(true);
|
||||||
|
for (const name of names) {
|
||||||
|
expect(typeof name).toBe('string');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains "claude"', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(names).toContain('claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains "cursor"', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(names).toContain('cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains "roo"', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(names).toContain('roo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains "gemini"', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(names).toContain('gemini');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains "codex"', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(names).toContain('codex');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all 6 profile names', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(names).toHaveLength(6);
|
||||||
|
expect(names).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
'claude',
|
||||||
|
'cursor',
|
||||||
|
'roo',
|
||||||
|
'gemini',
|
||||||
|
'codex',
|
||||||
|
'opencode'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all names are lowercase', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (const name of names) {
|
||||||
|
expect(name).toBe(name.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('names match getProfile lookup keys', () => {
|
||||||
|
// Act
|
||||||
|
const names = getProfileNames();
|
||||||
|
|
||||||
|
// Assert - each name should return a valid profile
|
||||||
|
for (const name of names) {
|
||||||
|
const profile = getProfile(name);
|
||||||
|
expect(profile).toBeDefined();
|
||||||
|
expect(profile?.name).toBe(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('profile singleton consistency', () => {
|
||||||
|
it('getProfile returns same instance for repeated calls', () => {
|
||||||
|
// Act
|
||||||
|
const profile1 = getProfile('claude');
|
||||||
|
const profile2 = getProfile('claude');
|
||||||
|
|
||||||
|
// Assert - should be same singleton instance
|
||||||
|
expect(profile1).toBe(profile2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAllProfiles contains same instances as getProfile', () => {
|
||||||
|
// Act
|
||||||
|
const allProfiles = getAllProfiles();
|
||||||
|
const claudeFromGet = getProfile('claude');
|
||||||
|
const claudeFromAll = allProfiles.find((p) => p.name === 'claude');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(claudeFromGet).toBe(claudeFromAll);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Profile class instantiation', () => {
|
||||||
|
it('can instantiate ClaudeProfile', () => {
|
||||||
|
// Act
|
||||||
|
const profile = new ClaudeProfile();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(ClaudeProfile);
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
expect(profile.name).toBe('claude');
|
||||||
|
expect(profile.displayName).toBe('Claude Code');
|
||||||
|
expect(profile.commandsDir).toBe('.claude/commands');
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
expect(profile.supportsCommands).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can instantiate CodexProfile', () => {
|
||||||
|
// Act
|
||||||
|
const profile = new CodexProfile();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(CodexProfile);
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
expect(profile.name).toBe('codex');
|
||||||
|
expect(profile.displayName).toBe('Codex');
|
||||||
|
expect(profile.commandsDir).toBe('.codex/prompts');
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
expect(profile.supportsCommands).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can instantiate CursorProfile', () => {
|
||||||
|
// Act
|
||||||
|
const profile = new CursorProfile();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(CursorProfile);
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
expect(profile.name).toBe('cursor');
|
||||||
|
expect(profile.displayName).toBe('Cursor');
|
||||||
|
expect(profile.commandsDir).toBe('.cursor/commands');
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
expect(profile.supportsCommands).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can instantiate RooProfile', () => {
|
||||||
|
// Act
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(RooProfile);
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
expect(profile.name).toBe('roo');
|
||||||
|
expect(profile.displayName).toBe('Roo Code');
|
||||||
|
expect(profile.commandsDir).toBe('.roo/commands');
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
expect(profile.supportsCommands).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can instantiate GeminiProfile', () => {
|
||||||
|
// Act
|
||||||
|
const profile = new GeminiProfile();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile).toBeInstanceOf(GeminiProfile);
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
expect(profile.name).toBe('gemini');
|
||||||
|
expect(profile.displayName).toBe('Gemini');
|
||||||
|
expect(profile.commandsDir).toBe('.gemini/commands');
|
||||||
|
expect(profile.extension).toBe('.toml');
|
||||||
|
expect(profile.supportsCommands).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all instantiated profiles extend BaseSlashCommandProfile', () => {
|
||||||
|
// Act
|
||||||
|
const profiles = [
|
||||||
|
new ClaudeProfile(),
|
||||||
|
new CodexProfile(),
|
||||||
|
new CursorProfile(),
|
||||||
|
new RooProfile(),
|
||||||
|
new GeminiProfile(),
|
||||||
|
new OpenCodeProfile()
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (const profile of profiles) {
|
||||||
|
expect(profile).toBeInstanceOf(BaseSlashCommandProfile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
97
packages/tm-profiles/src/slash-commands/profiles/index.ts
Normal file
97
packages/tm-profiles/src/slash-commands/profiles/index.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Slash Command Profiles Index
|
||||||
|
*
|
||||||
|
* This module exports all slash command profile classes and provides
|
||||||
|
* utility functions for profile lookup and management.
|
||||||
|
*
|
||||||
|
* Supported profiles (with slash commands):
|
||||||
|
* - Claude Code: .claude/commands
|
||||||
|
* - Cursor: .cursor/commands
|
||||||
|
* - Roo Code: .roo/commands
|
||||||
|
* - Gemini: .gemini/commands
|
||||||
|
* - Codex: .codex/prompts
|
||||||
|
* - OpenCode: .opencode/command
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Base profile class and types
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
export type { SlashCommandResult } from './base-profile.js';
|
||||||
|
|
||||||
|
// Individual profile classes
|
||||||
|
import { ClaudeProfile } from './claude-profile.js';
|
||||||
|
import { CodexProfile } from './codex-profile.js';
|
||||||
|
import { CursorProfile } from './cursor-profile.js';
|
||||||
|
import { GeminiProfile } from './gemini-profile.js';
|
||||||
|
import { OpenCodeProfile } from './opencode-profile.js';
|
||||||
|
import { RooProfile } from './roo-profile.js';
|
||||||
|
|
||||||
|
// Re-export base class and all profile classes for direct use
|
||||||
|
export { BaseSlashCommandProfile };
|
||||||
|
export { ClaudeProfile };
|
||||||
|
export { CodexProfile };
|
||||||
|
export type { CodexProfileOptions } from './codex-profile.js';
|
||||||
|
export { CursorProfile };
|
||||||
|
export { GeminiProfile };
|
||||||
|
export { OpenCodeProfile };
|
||||||
|
export { RooProfile };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instances of all available slash command profiles.
|
||||||
|
* Keys are lowercase profile names for case-insensitive lookup.
|
||||||
|
*/
|
||||||
|
const profiles: Record<string, BaseSlashCommandProfile> = {
|
||||||
|
claude: new ClaudeProfile(),
|
||||||
|
codex: new CodexProfile(),
|
||||||
|
cursor: new CursorProfile(),
|
||||||
|
gemini: new GeminiProfile(),
|
||||||
|
opencode: new OpenCodeProfile(),
|
||||||
|
roo: new RooProfile()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a slash command profile by name.
|
||||||
|
*
|
||||||
|
* @param name - The profile name (case-insensitive)
|
||||||
|
* @returns The profile instance if found, undefined otherwise
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const claudeProfile = getProfile('claude');
|
||||||
|
* const cursorProfile = getProfile('CURSOR'); // Case-insensitive
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function getProfile(name: string): BaseSlashCommandProfile | undefined {
|
||||||
|
return profiles[name.toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available slash command profiles.
|
||||||
|
*
|
||||||
|
* @returns Array of all profile instances
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const allProfiles = getAllProfiles();
|
||||||
|
* allProfiles.forEach(profile => {
|
||||||
|
* console.log(profile.name, profile.commandsDir);
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function getAllProfiles(): BaseSlashCommandProfile[] {
|
||||||
|
return Object.values(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available profile names.
|
||||||
|
*
|
||||||
|
* @returns Array of profile names (lowercase)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const names = getProfileNames();
|
||||||
|
* // ['claude', 'cursor', 'roo', 'gemini']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function getProfileNames(): string[] {
|
||||||
|
return Object.keys(profiles);
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit Tests for OpenCodeProfile
|
||||||
|
* Tests the OpenCode slash command profile formatting and metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { OpenCodeProfile } from './opencode-profile.js';
|
||||||
|
import { staticCommand, dynamicCommand } from '../factories.js';
|
||||||
|
|
||||||
|
describe('OpenCodeProfile', () => {
|
||||||
|
describe('Profile Metadata', () => {
|
||||||
|
it('should have correct profile name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.name).toBe('opencode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct display name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.displayName).toBe('OpenCode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.commandsDir).toBe('.opencode/command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct file extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsCommands getter', () => {
|
||||||
|
it('should return true when commandsDir is non-empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.supportsCommands;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsNestedCommands property', () => {
|
||||||
|
it('should be false (uses tm- prefix instead of subdirectory)', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.supportsNestedCommands).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilename() method', () => {
|
||||||
|
it('should add tm- prefix and .md extension to command name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('help');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-help.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with hyphens', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my-command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-my-command.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with underscores', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my_command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-my_command.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for static commands', () => {
|
||||||
|
it('should add frontmatter with description for simple static command', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help\n\nList of available commands...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-help.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Show available commands\n---\n# Help\n\nList of available commands...'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const multilineContent = `# Task Runner
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Run automated tasks for the project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Check dependencies
|
||||||
|
2. Run build
|
||||||
|
3. Execute tests
|
||||||
|
4. Generate report`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'task-runner',
|
||||||
|
description: 'Run automated tasks',
|
||||||
|
content: multilineContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-task-runner.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Run automated tasks\n---\n' + multilineContent
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve code blocks in content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const contentWithCode = `# Deploy
|
||||||
|
|
||||||
|
Run the deployment:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npm run deploy
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Done!`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy the application',
|
||||||
|
content: contentWithCode
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Deploy the application\n---\n' + contentWithCode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve special characters in content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const contentWithSpecialChars =
|
||||||
|
'# Special\n\nUse `$HOME` and `$PATH` variables. Also: <tag> & "quotes"';
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Command with special chars',
|
||||||
|
content: contentWithSpecialChars
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Command with special chars\n---\n' +
|
||||||
|
contentWithSpecialChars
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for dynamic commands', () => {
|
||||||
|
it('should include description in frontmatter and preserve $ARGUMENTS placeholder', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'review',
|
||||||
|
'Review a pull request',
|
||||||
|
'<pr-number>',
|
||||||
|
'# Review PR\n\nReviewing PR: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-review.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Review a pull request\n---\n# Review PR\n\nReviewing PR: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
expect(result.content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiple $ARGUMENTS placeholders with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'compare',
|
||||||
|
'Compare two items',
|
||||||
|
'<item1> <item2>',
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Compare two items\n---\nFirst: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
expect(result.content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve $ARGUMENTS in complex markdown content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const complexContent = `# Search Command
|
||||||
|
|
||||||
|
## Input
|
||||||
|
User provided: $ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Parse the input: \`$ARGUMENTS\`
|
||||||
|
2. Search for matches
|
||||||
|
3. Display results
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Query: $ARGUMENTS
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search the codebase',
|
||||||
|
'<query>',
|
||||||
|
complexContent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Search the codebase\n---\n' + complexContent
|
||||||
|
);
|
||||||
|
expect(result.content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAll() method', () => {
|
||||||
|
it('should format multiple commands correctly with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help Content'
|
||||||
|
}),
|
||||||
|
dynamicCommand('run', 'Run a command', '<cmd>', 'Running: $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].filename).toBe('tm-help.md');
|
||||||
|
expect(results[0].content).toBe(
|
||||||
|
'---\ndescription: Show help\n---\n# Help Content'
|
||||||
|
);
|
||||||
|
expect(results[1].filename).toBe('tm-run.md');
|
||||||
|
expect(results[1].content).toBe(
|
||||||
|
'---\ndescription: Run a command\n---\nRunning: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll([]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommandsPath() method', () => {
|
||||||
|
it('should return correct absolute path for commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const projectRoot = '/home/user/my-project';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.opencode/command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle project root with trailing slash', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new OpenCodeProfile();
|
||||||
|
const projectRoot = '/home/user/my-project/';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// path.join normalizes the path
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.opencode/command');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview OpenCode Profile
|
||||||
|
* Slash command profile for OpenCode.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ```
|
||||||
|
* ---
|
||||||
|
* description: "..."
|
||||||
|
* ---
|
||||||
|
* [content]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* OpenCode uses YAML frontmatter format with description field.
|
||||||
|
* Additional fields (agent, model, subtask) are optional.
|
||||||
|
*
|
||||||
|
* Location: .opencode/command/*.md (note: singular "command", not "commands")
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FormattedSlashCommand, SlashCommand } from '../types.js';
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenCode profile for slash commands.
|
||||||
|
*
|
||||||
|
* OpenCode uses YAML frontmatter for command metadata:
|
||||||
|
* - description: Short description for the command picker
|
||||||
|
* - agent (optional): Which agent should handle this command
|
||||||
|
* - model (optional): Override model for this command
|
||||||
|
* - subtask (optional): Whether to run as a subtask
|
||||||
|
*
|
||||||
|
* Supports $ARGUMENTS and positional args ($1, $2, etc.) placeholders.
|
||||||
|
*/
|
||||||
|
export class OpenCodeProfile extends BaseSlashCommandProfile {
|
||||||
|
readonly name = 'opencode';
|
||||||
|
readonly displayName = 'OpenCode';
|
||||||
|
readonly commandsDir = '.opencode/command';
|
||||||
|
readonly extension = '.md';
|
||||||
|
readonly supportsNestedCommands = false;
|
||||||
|
|
||||||
|
format(command: SlashCommand): FormattedSlashCommand {
|
||||||
|
const frontmatter = this.buildFrontmatter(command);
|
||||||
|
const content = this.transformArgumentPlaceholder(command.content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: this.getFilename(command.metadata.name),
|
||||||
|
content: `${frontmatter}${content}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build YAML frontmatter for OpenCode format.
|
||||||
|
* Includes description (required).
|
||||||
|
*/
|
||||||
|
private buildFrontmatter(command: SlashCommand): string {
|
||||||
|
const lines = [
|
||||||
|
'---',
|
||||||
|
`description: ${command.metadata.description}`,
|
||||||
|
'---',
|
||||||
|
''
|
||||||
|
];
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Unit Tests for RooProfile
|
||||||
|
* Tests the Roo Code slash command profile formatting and metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { RooProfile } from './roo-profile.js';
|
||||||
|
import { staticCommand, dynamicCommand } from '../factories.js';
|
||||||
|
|
||||||
|
describe('RooProfile', () => {
|
||||||
|
describe('Profile Metadata', () => {
|
||||||
|
it('should have correct profile name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.name).toBe('roo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct display name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.displayName).toBe('Roo Code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.commandsDir).toBe('.roo/commands');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct file extension', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.extension).toBe('.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsCommands getter', () => {
|
||||||
|
it('should return true when commandsDir is non-empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.supportsCommands;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('supportsNestedCommands property', () => {
|
||||||
|
it('should be false (uses tm- prefix instead of subdirectory)', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(profile.supportsNestedCommands).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilename() method', () => {
|
||||||
|
it('should add tm- prefix and .md extension to command name', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('help');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-help.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with hyphens', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my-command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-my-command.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command names with underscores', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filename = profile.getFilename('my_command');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filename).toBe('tm-my_command.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for static commands', () => {
|
||||||
|
it('should add frontmatter with description for simple static command', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help\n\nList of available commands...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-help.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Show available commands\n---\n\n# Help\n\nList of available commands...'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const multilineContent = `# Task Runner
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Run automated tasks for the project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Check dependencies
|
||||||
|
2. Run build
|
||||||
|
3. Execute tests
|
||||||
|
4. Generate report`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'task-runner',
|
||||||
|
description: 'Run automated tasks',
|
||||||
|
content: multilineContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-task-runner.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Run automated tasks\n---\n\n' + multilineContent
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include argument-hint in frontmatter for static command with argumentHint', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'analyze',
|
||||||
|
description: 'Analyze codebase',
|
||||||
|
argumentHint: '[path]',
|
||||||
|
content: '# Analyze\n\nAnalyze the specified path.'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-analyze.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Analyze codebase\nargument-hint: [path]\n---\n\n# Analyze\n\nAnalyze the specified path.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve code blocks in content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const contentWithCode = `# Deploy
|
||||||
|
|
||||||
|
Run the deployment:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npm run deploy
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Done!`;
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy the application',
|
||||||
|
content: contentWithCode
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Deploy the application\n---\n\n' + contentWithCode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve special characters in content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const contentWithSpecialChars =
|
||||||
|
'# Special\n\nUse `$HOME` and `$PATH` variables. Also: <tag> & "quotes"';
|
||||||
|
|
||||||
|
const command = staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Command with special chars',
|
||||||
|
content: contentWithSpecialChars
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Command with special chars\n---\n\n' +
|
||||||
|
contentWithSpecialChars
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('format() method for dynamic commands', () => {
|
||||||
|
it('should include argument-hint in frontmatter and preserve $ARGUMENTS placeholder', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'review',
|
||||||
|
'Review a pull request',
|
||||||
|
'<pr-number>',
|
||||||
|
'# Review PR\n\nReviewing PR: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.filename).toBe('tm-review.md');
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Review a pull request\nargument-hint: <pr-number>\n---\n\n# Review PR\n\nReviewing PR: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
expect(result.content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiple $ARGUMENTS placeholders with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'compare',
|
||||||
|
'Compare two items',
|
||||||
|
'<item1> <item2>',
|
||||||
|
'First: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Compare two items\nargument-hint: <item1> <item2>\n---\n\nFirst: $ARGUMENTS\nSecond: $ARGUMENTS\nBoth: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
expect(result.content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve $ARGUMENTS in complex markdown content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const complexContent = `# Search Command
|
||||||
|
|
||||||
|
## Input
|
||||||
|
User provided: $ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Parse the input: \`$ARGUMENTS\`
|
||||||
|
2. Search for matches
|
||||||
|
3. Display results
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Query: $ARGUMENTS
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const command = dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search the codebase',
|
||||||
|
'<query>',
|
||||||
|
complexContent
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.format(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'---\ndescription: Search the codebase\nargument-hint: <query>\n---\n\n' +
|
||||||
|
complexContent
|
||||||
|
);
|
||||||
|
expect(result.content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAll() method', () => {
|
||||||
|
it('should format multiple commands correctly with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help Content'
|
||||||
|
}),
|
||||||
|
dynamicCommand('run', 'Run a command', '<cmd>', 'Running: $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll(commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results[0].filename).toBe('tm-help.md');
|
||||||
|
expect(results[0].content).toBe(
|
||||||
|
'---\ndescription: Show help\n---\n\n# Help Content'
|
||||||
|
);
|
||||||
|
expect(results[1].filename).toBe('tm-run.md');
|
||||||
|
expect(results[1].content).toBe(
|
||||||
|
'---\ndescription: Run a command\nargument-hint: <cmd>\n---\n\nRunning: $ARGUMENTS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty input', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const results = profile.formatAll([]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCommandsPath() method', () => {
|
||||||
|
it('should return correct absolute path for commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const projectRoot = '/home/user/my-project';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.roo/commands');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle project root with trailing slash', () => {
|
||||||
|
// Arrange
|
||||||
|
const profile = new RooProfile();
|
||||||
|
const projectRoot = '/home/user/my-project/';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const commandsPath = profile.getCommandsPath(projectRoot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// path.join normalizes the path
|
||||||
|
expect(commandsPath).toBe('/home/user/my-project/.roo/commands');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Roo Code Profile
|
||||||
|
* Slash command profile for Roo Code.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ```
|
||||||
|
* ---
|
||||||
|
* description: Short description for command picker
|
||||||
|
* argument-hint: <optional-hint>
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [content]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Roo Code uses YAML frontmatter for metadata, similar to other markdown-based tools.
|
||||||
|
* The frontmatter contains a description (required) and optional argument-hint.
|
||||||
|
*
|
||||||
|
* Location: .roo/commands/*.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseSlashCommandProfile } from './base-profile.js';
|
||||||
|
import type { SlashCommand, FormattedSlashCommand } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roo Code profile for slash commands.
|
||||||
|
*
|
||||||
|
* Roo Code uses YAML frontmatter for command metadata:
|
||||||
|
* - description: Appears in the command menu to help users understand the command's purpose
|
||||||
|
* - argument-hint: Optional hint about expected arguments when using the command
|
||||||
|
*
|
||||||
|
* The content follows the frontmatter and supports $ARGUMENTS placeholders.
|
||||||
|
*/
|
||||||
|
export class RooProfile extends BaseSlashCommandProfile {
|
||||||
|
readonly name = 'roo';
|
||||||
|
readonly displayName = 'Roo Code';
|
||||||
|
readonly commandsDir = '.roo/commands';
|
||||||
|
readonly extension = '.md';
|
||||||
|
readonly supportsNestedCommands = false;
|
||||||
|
|
||||||
|
format(command: SlashCommand): FormattedSlashCommand {
|
||||||
|
const frontmatter = this.buildFrontmatter(command);
|
||||||
|
const content = this.transformArgumentPlaceholder(command.content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: this.getFilename(command.metadata.name),
|
||||||
|
content: `${frontmatter}${content}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build YAML frontmatter for Roo Code format.
|
||||||
|
* Includes description (required) and optional argument-hint.
|
||||||
|
* Adds a blank line after the closing --- for proper markdown separation.
|
||||||
|
*/
|
||||||
|
private buildFrontmatter(command: SlashCommand): string {
|
||||||
|
const lines = ['---', `description: ${command.metadata.description}`];
|
||||||
|
|
||||||
|
if (command.metadata.argumentHint) {
|
||||||
|
lines.push(`argument-hint: ${command.metadata.argumentHint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add closing --- and two empty strings to produce "---\n\n" (blank line before content)
|
||||||
|
lines.push('---', '', '');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/tm-profiles/src/slash-commands/types.ts
Normal file
66
packages/tm-profiles/src/slash-commands/types.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Slash Command Type Definitions
|
||||||
|
* Uses discriminated unions for type-safe static vs dynamic commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operating mode for Task Master
|
||||||
|
* - 'solo': Local file-based storage (Taskmaster standalone)
|
||||||
|
* - 'team': API-based storage via Hamster (collaborative features)
|
||||||
|
* - 'common': Works in both modes
|
||||||
|
*/
|
||||||
|
export type OperatingMode = 'solo' | 'team' | 'common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base metadata shared by all slash commands
|
||||||
|
*/
|
||||||
|
export interface SlashCommandMetadata {
|
||||||
|
/** Command name (filename without extension) */
|
||||||
|
readonly name: string;
|
||||||
|
/** Short description shown in command picker */
|
||||||
|
readonly description: string;
|
||||||
|
/** Optional hint for arguments (e.g., "[brief-url]") */
|
||||||
|
readonly argumentHint?: string;
|
||||||
|
/** Operating mode - defaults to 'common' if not specified */
|
||||||
|
readonly mode?: OperatingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A static slash command with fixed content (no $ARGUMENTS placeholder)
|
||||||
|
* May still have an argumentHint for documentation purposes
|
||||||
|
*/
|
||||||
|
export interface StaticSlashCommand {
|
||||||
|
readonly type: 'static';
|
||||||
|
readonly metadata: SlashCommandMetadata;
|
||||||
|
/** The markdown content */
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dynamic slash command that accepts arguments via $ARGUMENTS placeholder
|
||||||
|
*/
|
||||||
|
export interface DynamicSlashCommand {
|
||||||
|
readonly type: 'dynamic';
|
||||||
|
readonly metadata: SlashCommandMetadata & {
|
||||||
|
/** Hint for arguments - required for dynamic commands */
|
||||||
|
readonly argumentHint: string;
|
||||||
|
};
|
||||||
|
/** The markdown content containing $ARGUMENTS placeholder(s) */
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union type for all slash commands
|
||||||
|
* Use `command.type` to narrow the type
|
||||||
|
*/
|
||||||
|
export type SlashCommand = StaticSlashCommand | DynamicSlashCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatted command output ready to be written to file
|
||||||
|
*/
|
||||||
|
export interface FormattedSlashCommand {
|
||||||
|
/** Filename (e.g., "goham.md") */
|
||||||
|
readonly filename: string;
|
||||||
|
/** Formatted content for the target editor */
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
46
packages/tm-profiles/src/slash-commands/utils.ts
Normal file
46
packages/tm-profiles/src/slash-commands/utils.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Utility functions for slash commands module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve project root from a target directory by navigating up
|
||||||
|
* based on a known relative path structure.
|
||||||
|
*
|
||||||
|
* This is useful when lifecycle hooks receive a nested directory
|
||||||
|
* (like `.roo/rules`) and need to get back to the project root
|
||||||
|
* to place commands in the correct location.
|
||||||
|
*
|
||||||
|
* @param targetDir - The target directory (usually rulesDir)
|
||||||
|
* @param relativePath - The relative path from project root (e.g., ".roo/rules")
|
||||||
|
* @returns The project root directory
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // If targetDir is "/project/.roo/rules" and relativePath is ".roo/rules"
|
||||||
|
* const projectRoot = resolveProjectRoot("/project/.roo/rules", ".roo/rules");
|
||||||
|
* // Returns: "/project"
|
||||||
|
*
|
||||||
|
* // If relativePath is "." then targetDir is already project root
|
||||||
|
* const projectRoot = resolveProjectRoot("/project", ".");
|
||||||
|
* // Returns: "/project"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function resolveProjectRoot(
|
||||||
|
targetDir: string,
|
||||||
|
relativePath: string
|
||||||
|
): string {
|
||||||
|
// If relativePath is just "." then targetDir is already the project root
|
||||||
|
if (relativePath === '.') {
|
||||||
|
return targetDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count how many directory levels we need to go up
|
||||||
|
const levels = relativePath.split(path.sep).filter(Boolean).length;
|
||||||
|
let projectRoot = targetDir;
|
||||||
|
for (let i = 0; i < levels; i++) {
|
||||||
|
projectRoot = path.dirname(projectRoot);
|
||||||
|
}
|
||||||
|
return projectRoot;
|
||||||
|
}
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { ClaudeProfile } from '../../src/slash-commands/profiles/claude-profile.js';
|
||||||
|
import {
|
||||||
|
staticCommand,
|
||||||
|
dynamicCommand
|
||||||
|
} from '../../src/slash-commands/factories.js';
|
||||||
|
|
||||||
|
describe('ClaudeProfile Integration Tests', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let claudeProfile: ClaudeProfile;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-profile-test-'));
|
||||||
|
claudeProfile = new ClaudeProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSlashCommands', () => {
|
||||||
|
it('should create the .claude/commands/tm directory (nested structure)', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Claude supports nested commands, so files go to .claude/commands/tm/
|
||||||
|
const commandsDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.statSync(commandsDir).isDirectory()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write correctly formatted static command files', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-test',
|
||||||
|
description: 'Static test command',
|
||||||
|
content: '# Static Content\n\nThis is a test.'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.claude',
|
||||||
|
'commands',
|
||||||
|
'tm',
|
||||||
|
'static-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const expectedContent =
|
||||||
|
'Static test command\n# Static Content\n\nThis is a test.';
|
||||||
|
expect(content).toBe(expectedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write correctly formatted dynamic command files with argumentHint', () => {
|
||||||
|
const testCommands = [
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-test',
|
||||||
|
'Dynamic test command',
|
||||||
|
'[task-id]',
|
||||||
|
'Process task: $ARGUMENTS\n\nThis processes the specified task.'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.claude',
|
||||||
|
'commands',
|
||||||
|
'tm',
|
||||||
|
'dynamic-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const expectedContent =
|
||||||
|
'Dynamic test command\n\n' +
|
||||||
|
'Arguments: $ARGUMENTS\n' +
|
||||||
|
'Process task: $ARGUMENTS\n\n' +
|
||||||
|
'This processes the specified task.';
|
||||||
|
expect(content).toBe(expectedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success result with correct count', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'First command',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'Second command',
|
||||||
|
content: 'Content 2'
|
||||||
|
}),
|
||||||
|
dynamicCommand('cmd3', 'Third command', '[arg]', 'Content $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
expect(result.files).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing files on re-run', () => {
|
||||||
|
const initialCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Initial description',
|
||||||
|
content: 'Initial content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
claudeProfile.addSlashCommands(tempDir, initialCommands);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.claude',
|
||||||
|
'commands',
|
||||||
|
'tm',
|
||||||
|
'test-cmd.md'
|
||||||
|
);
|
||||||
|
const initialContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(initialContent).toContain('Initial description');
|
||||||
|
expect(initialContent).toContain('Initial content');
|
||||||
|
|
||||||
|
// Re-run with updated command
|
||||||
|
const updatedCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Updated description',
|
||||||
|
content: 'Updated content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
claudeProfile.addSlashCommands(tempDir, updatedCommands);
|
||||||
|
|
||||||
|
const updatedContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(updatedContent).toContain('Updated description');
|
||||||
|
expect(updatedContent).toContain('Updated content');
|
||||||
|
expect(updatedContent).not.toContain('Initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple commands with mixed types', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static1',
|
||||||
|
description: 'Static command 1',
|
||||||
|
content: 'Static content 1'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic1',
|
||||||
|
'Dynamic command 1',
|
||||||
|
'[id]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
),
|
||||||
|
staticCommand({
|
||||||
|
name: 'static2',
|
||||||
|
description: 'Static command 2',
|
||||||
|
content: 'Static content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const commandsDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
const static1Path = path.join(commandsDir, 'static1.md');
|
||||||
|
const dynamic1Path = path.join(commandsDir, 'dynamic1.md');
|
||||||
|
const static2Path = path.join(commandsDir, 'static2.md');
|
||||||
|
|
||||||
|
expect(fs.existsSync(static1Path)).toBe(true);
|
||||||
|
expect(fs.existsSync(dynamic1Path)).toBe(true);
|
||||||
|
expect(fs.existsSync(static2Path)).toBe(true);
|
||||||
|
|
||||||
|
// Verify dynamic command format
|
||||||
|
const dynamic1Content = fs.readFileSync(dynamic1Path, 'utf-8');
|
||||||
|
expect(dynamic1Content).toContain('Arguments: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSlashCommands', () => {
|
||||||
|
it('should remove only TaskMaster commands and preserve user files', async () => {
|
||||||
|
// Add TaskMaster commands
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'tm-cmd1',
|
||||||
|
description: 'TaskMaster command 1',
|
||||||
|
content: 'TM Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'tm-cmd2',
|
||||||
|
description: 'TaskMaster command 2',
|
||||||
|
content: 'TM Content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
claudeProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// TaskMaster commands go to .claude/commands/tm/ subdirectory
|
||||||
|
const tmDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
const userFilePath = path.join(tmDir, 'user-custom.md');
|
||||||
|
fs.writeFileSync(userFilePath, 'User custom command\n\nUser content');
|
||||||
|
|
||||||
|
// Remove TaskMaster commands
|
||||||
|
const result = claudeProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
// Verify TaskMaster files are removed
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'tm-cmd1.md'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'tm-cmd2.md'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify user file is preserved
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
const userContent = fs.readFileSync(userFilePath, 'utf-8');
|
||||||
|
expect(userContent).toContain('User custom command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty tm directory after cleanup', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'only-cmd',
|
||||||
|
description: 'Only command',
|
||||||
|
content: 'Only content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Commands go to .claude/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(true);
|
||||||
|
|
||||||
|
// Remove all TaskMaster commands
|
||||||
|
claudeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// tm directory should be removed when empty
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep tm directory when user files remain', async () => {
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'tm-cmd',
|
||||||
|
description: 'TaskMaster command',
|
||||||
|
content: 'TM Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
claudeProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Add user file in the tm directory
|
||||||
|
const tmDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
const userFilePath = path.join(tmDir, 'my-command.md');
|
||||||
|
fs.writeFileSync(userFilePath, 'My custom command');
|
||||||
|
|
||||||
|
// Remove TaskMaster commands
|
||||||
|
const result = claudeProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Directory should still exist because user file remains
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(true);
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when no files exist', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'nonexistent',
|
||||||
|
description: 'Non-existent command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Don't add commands, just try to remove
|
||||||
|
const result = claudeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when directory does not exist', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ensure .claude/commands/tm doesn't exist
|
||||||
|
const tmDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(false);
|
||||||
|
|
||||||
|
const result = claudeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove mixed command types', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-cmd',
|
||||||
|
description: 'Static command',
|
||||||
|
content: 'Static content'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-cmd',
|
||||||
|
'Dynamic command',
|
||||||
|
'[arg]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const tmDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'static-cmd.md'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'dynamic-cmd.md'))).toBe(true);
|
||||||
|
|
||||||
|
const result = claudeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'static-cmd.md'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'dynamic-cmd.md'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty command list', () => {
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, []);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle commands with special characters in names', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd-123',
|
||||||
|
description: 'Test with numbers',
|
||||||
|
content: 'Content'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'test_underscore',
|
||||||
|
description: 'Test with underscore',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const tmDir = path.join(tempDir, '.claude', 'commands', 'tm');
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'test-cmd-123.md'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'test_underscore.md'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle commands with multiline content', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'multiline',
|
||||||
|
description: 'Multiline command',
|
||||||
|
content: 'Line 1\nLine 2\nLine 3\n\nParagraph 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.claude',
|
||||||
|
'commands',
|
||||||
|
'tm',
|
||||||
|
'multiline.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
expect(content).toContain('Line 1\nLine 2\nLine 3');
|
||||||
|
expect(content).toContain('Paragraph 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve exact formatting in content', async () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'formatted',
|
||||||
|
description: 'Formatted command',
|
||||||
|
content: '# Heading\n\n- Item 1\n- Item 2\n\n```code\nblock\n```'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = claudeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Files go to .claude/commands/tm/ subdirectory
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.claude',
|
||||||
|
'commands',
|
||||||
|
'tm',
|
||||||
|
'formatted.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
expect(content).toContain('# Heading');
|
||||||
|
expect(content).toContain('- Item 1\n- Item 2');
|
||||||
|
expect(content).toContain('```code\nblock\n```');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Integration tests for CodexProfile
|
||||||
|
* Tests actual filesystem operations for slash command management.
|
||||||
|
*
|
||||||
|
* Note: Codex stores prompts in ~/.codex/prompts (home directory), not project-relative.
|
||||||
|
* Tests use the homeDir option to redirect writes to a temp directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { CodexProfile } from '../../src/slash-commands/profiles/codex-profile.js';
|
||||||
|
import {
|
||||||
|
staticCommand,
|
||||||
|
dynamicCommand
|
||||||
|
} from '../../src/slash-commands/factories.js';
|
||||||
|
|
||||||
|
describe('CodexProfile Integration Tests', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let codexProfile: CodexProfile;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary directory to act as the "home" directory for testing
|
||||||
|
// Codex prompts go in ~/.codex/prompts, so we override homeDir to tempDir
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-profile-test-'));
|
||||||
|
codexProfile = new CodexProfile({ homeDir: tempDir });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSlashCommands', () => {
|
||||||
|
it('should create the .codex/prompts directory', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.statSync(commandsDir).isDirectory()).toBe(true);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write files with YAML frontmatter and tm- prefix', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-test',
|
||||||
|
description: 'Test description',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Codex uses tm- prefix since supportsNestedCommands = false
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.codex',
|
||||||
|
'prompts',
|
||||||
|
'tm-static-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// Verify YAML frontmatter structure
|
||||||
|
expect(content).toContain('---');
|
||||||
|
expect(content).toContain('description: "Test description"');
|
||||||
|
expect(content).toContain('# Test Content');
|
||||||
|
|
||||||
|
// Verify it does NOT include argument-hint (static command without argumentHint)
|
||||||
|
expect(content).not.toContain('argument-hint:');
|
||||||
|
|
||||||
|
// Verify exact format
|
||||||
|
const expectedContent =
|
||||||
|
'---\ndescription: "Test description"\n---\n# Test Content';
|
||||||
|
expect(content).toBe(expectedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include argument-hint only when argumentHint is present', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'with-hint',
|
||||||
|
description: 'Command with hint',
|
||||||
|
argumentHint: '[args]',
|
||||||
|
content: 'Content here'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.codex',
|
||||||
|
'prompts',
|
||||||
|
'tm-with-hint.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// Verify argument-hint is included
|
||||||
|
expect(content).toContain('argument-hint: "[args]"');
|
||||||
|
|
||||||
|
// Verify exact format
|
||||||
|
const expectedContent =
|
||||||
|
'---\ndescription: "Command with hint"\nargument-hint: "[args]"\n---\nContent here';
|
||||||
|
expect(content).toBe(expectedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format dynamic commands with argument-hint', () => {
|
||||||
|
const testCommands = [
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-test',
|
||||||
|
'Dynamic command',
|
||||||
|
'<task-id>',
|
||||||
|
'Process: $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.codex',
|
||||||
|
'prompts',
|
||||||
|
'tm-dynamic-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// Dynamic commands should include argument-hint
|
||||||
|
expect(content).toContain('argument-hint: "<task-id>"');
|
||||||
|
expect(content).toContain('Process: $ARGUMENTS');
|
||||||
|
|
||||||
|
// Verify exact format
|
||||||
|
const expectedContent =
|
||||||
|
'---\ndescription: "Dynamic command"\nargument-hint: "<task-id>"\n---\nProcess: $ARGUMENTS';
|
||||||
|
expect(content).toBe(expectedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success result with correct count', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'First command',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'Second command',
|
||||||
|
content: 'Content 2'
|
||||||
|
}),
|
||||||
|
dynamicCommand('cmd3', 'Third command', '[arg]', 'Content $ARGUMENTS')
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
expect(result.files).toHaveLength(3);
|
||||||
|
expect(result.directory).toBe(path.join(tempDir, '.codex', 'prompts'));
|
||||||
|
expect(result.files).toContain('tm-cmd1.md');
|
||||||
|
expect(result.files).toContain('tm-cmd2.md');
|
||||||
|
expect(result.files).toContain('tm-cmd3.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiline content in YAML frontmatter format', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'multiline',
|
||||||
|
description: 'Multiline test',
|
||||||
|
content: '# Title\n\nParagraph 1\n\nParagraph 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.codex',
|
||||||
|
'prompts',
|
||||||
|
'tm-multiline.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(content).toContain('# Title');
|
||||||
|
expect(content).toContain('Paragraph 1');
|
||||||
|
expect(content).toContain('Paragraph 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle commands with special characters in descriptions', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Command with "quotes" and special chars',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(tempDir, '.codex', 'prompts', 'tm-special.md');
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(content).toContain(
|
||||||
|
'description: "Command with \\"quotes\\" and special chars"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSlashCommands', () => {
|
||||||
|
it('should remove only TaskMaster commands and preserve user files', () => {
|
||||||
|
// Add TaskMaster commands
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'TaskMaster command 1',
|
||||||
|
content: 'TM Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'TaskMaster command 2',
|
||||||
|
content: 'TM Content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Create a user file manually
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
const userFilePath = path.join(commandsDir, 'user-custom.md');
|
||||||
|
fs.writeFileSync(
|
||||||
|
userFilePath,
|
||||||
|
'---\ndescription: "User command"\n---\nUser content'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove TaskMaster commands
|
||||||
|
const result = codexProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
// Verify TaskMaster files are removed (they have tm- prefix)
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-cmd1.md'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-cmd2.md'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify user file is preserved
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
const userContent = fs.readFileSync(userFilePath, 'utf-8');
|
||||||
|
expect(userContent).toContain('User command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty directory after cleanup', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'only-cmd',
|
||||||
|
description: 'Only command',
|
||||||
|
content: 'Only content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Remove all TaskMaster commands
|
||||||
|
const result = codexProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
|
||||||
|
// Directory should be removed when empty
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep directory when user files remain', () => {
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd',
|
||||||
|
description: 'TaskMaster command',
|
||||||
|
content: 'TM Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Add user file
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
const userFilePath = path.join(commandsDir, 'my-command.md');
|
||||||
|
fs.writeFileSync(
|
||||||
|
userFilePath,
|
||||||
|
'---\ndescription: "My custom command"\n---\nMy content'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove TaskMaster commands
|
||||||
|
const result = codexProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
|
||||||
|
// Directory should still exist
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when no files exist', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'nonexistent',
|
||||||
|
description: 'Non-existent command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Don't add commands, just try to remove
|
||||||
|
const result = codexProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when directory does not exist', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ensure .codex/prompts doesn't exist
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
|
||||||
|
const result = codexProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove mixed command types', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-cmd',
|
||||||
|
description: 'Static command',
|
||||||
|
content: 'Static content'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-cmd',
|
||||||
|
'Dynamic command',
|
||||||
|
'[arg]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-static-cmd.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-dynamic-cmd.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = codexProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-static-cmd.md'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-dynamic-cmd.md'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty command list', () => {
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, []);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle commands with hyphens and underscores in names', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd-123',
|
||||||
|
description: 'Test with numbers',
|
||||||
|
content: 'Content'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'test_underscore',
|
||||||
|
description: 'Test with underscore',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.codex', 'prompts');
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-test-cmd-123.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(commandsDir, 'tm-test_underscore.md'))
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve exact formatting in content', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'formatted',
|
||||||
|
description: 'Formatted command',
|
||||||
|
content: '# Heading\n\n- Item 1\n- Item 2\n\n```code\nblock\n```'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.codex',
|
||||||
|
'prompts',
|
||||||
|
'tm-formatted.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
expect(content).toContain('# Heading');
|
||||||
|
expect(content).toContain('- Item 1\n- Item 2');
|
||||||
|
expect(content).toContain('```code\nblock\n```');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty content', () => {
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'empty',
|
||||||
|
description: 'Empty content',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = codexProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(tempDir, '.codex', 'prompts', 'tm-empty.md');
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
// Should only have frontmatter
|
||||||
|
expect(content).toBe('---\ndescription: "Empty content"\n---\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing files on re-run', () => {
|
||||||
|
const initialCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Initial description',
|
||||||
|
content: 'Initial content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, initialCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.codex',
|
||||||
|
'prompts',
|
||||||
|
'tm-test-cmd.md'
|
||||||
|
);
|
||||||
|
const initialContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(initialContent).toContain('Initial description');
|
||||||
|
expect(initialContent).toContain('Initial content');
|
||||||
|
|
||||||
|
// Re-run with updated command
|
||||||
|
const updatedCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Updated description',
|
||||||
|
content: 'Updated content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
codexProfile.addSlashCommands(tempDir, updatedCommands);
|
||||||
|
|
||||||
|
const updatedContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(updatedContent).toContain('Updated description');
|
||||||
|
expect(updatedContent).toContain('Updated content');
|
||||||
|
expect(updatedContent).not.toContain('Initial');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Integration tests for CursorProfile
|
||||||
|
*
|
||||||
|
* These tests verify actual filesystem operations using addSlashCommands
|
||||||
|
* and removeSlashCommands methods. Tests ensure that:
|
||||||
|
* - Directory creation works correctly (files go to .cursor/commands/tm/)
|
||||||
|
* - Files are written with correct content (no transformation)
|
||||||
|
* - Commands can be added and removed
|
||||||
|
* - User files are preserved during cleanup
|
||||||
|
* - Empty directories are cleaned up
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { CursorProfile } from '../../src/slash-commands/profiles/cursor-profile.js';
|
||||||
|
import {
|
||||||
|
staticCommand,
|
||||||
|
dynamicCommand
|
||||||
|
} from '../../src/slash-commands/factories.js';
|
||||||
|
|
||||||
|
describe('CursorProfile - Integration Tests', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let cursorProfile: CursorProfile;
|
||||||
|
|
||||||
|
// Test commands created inline
|
||||||
|
const testStaticCommand = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help\n\nList of available Task Master commands.'
|
||||||
|
});
|
||||||
|
|
||||||
|
const testDynamicCommand = dynamicCommand(
|
||||||
|
'goham',
|
||||||
|
'Start Working with Hamster Brief',
|
||||||
|
'[brief-url]',
|
||||||
|
'# Start Working\n\nBrief URL: $ARGUMENTS\n\nThis command helps you start working on a Hamster brief.'
|
||||||
|
);
|
||||||
|
|
||||||
|
const testCommands = [testStaticCommand, testDynamicCommand];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temporary directory for each test
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-profile-test-'));
|
||||||
|
cursorProfile = new CursorProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSlashCommands', () => {
|
||||||
|
it('should create the .cursor/commands/tm directory (nested structure)', () => {
|
||||||
|
// Verify directory doesn't exist before
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(false);
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Verify tm directory exists after (nested structure)
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(true);
|
||||||
|
expect(fs.statSync(tmDir).isDirectory()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write files with content unchanged (no transformation)', () => {
|
||||||
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Cursor supports nested commands, files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// Verify static command (help.md)
|
||||||
|
const helpPath = path.join(tmDir, 'help.md');
|
||||||
|
expect(fs.existsSync(helpPath)).toBe(true);
|
||||||
|
const helpContent = fs.readFileSync(helpPath, 'utf-8');
|
||||||
|
expect(helpContent).toBe(
|
||||||
|
'# Help\n\nList of available Task Master commands.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify dynamic command (goham.md)
|
||||||
|
const gohamPath = path.join(tmDir, 'goham.md');
|
||||||
|
expect(fs.existsSync(gohamPath)).toBe(true);
|
||||||
|
const gohamContent = fs.readFileSync(gohamPath, 'utf-8');
|
||||||
|
expect(gohamContent).toBe(
|
||||||
|
'# Start Working\n\nBrief URL: $ARGUMENTS\n\nThis command helps you start working on a Hamster brief.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify $ARGUMENTS placeholder is NOT transformed
|
||||||
|
expect(gohamContent).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success result with correct count', () => {
|
||||||
|
const result = cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
// Path includes tm/ subdirectory for nested structure
|
||||||
|
expect(result.directory).toBe(
|
||||||
|
path.join(tempDir, '.cursor', 'commands', 'tm')
|
||||||
|
);
|
||||||
|
expect(result.files).toEqual(['help.md', 'goham.md']);
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing files on re-run', () => {
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// First run
|
||||||
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
const originalContent = fs.readFileSync(
|
||||||
|
path.join(tmDir, 'help.md'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(originalContent).toBe(
|
||||||
|
'# Help\n\nList of available Task Master commands.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modify the content of test command
|
||||||
|
const modifiedCommand = staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
content: '# Help - Updated\n\nThis is updated content.'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second run with modified command
|
||||||
|
const result = cursorProfile.addSlashCommands(tempDir, [modifiedCommand]);
|
||||||
|
|
||||||
|
// Verify file was overwritten
|
||||||
|
const updatedContent = fs.readFileSync(
|
||||||
|
path.join(tmDir, 'help.md'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(updatedContent).toBe(
|
||||||
|
'# Help - Updated\n\nThis is updated content.'
|
||||||
|
);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle commands with special characters in content', () => {
|
||||||
|
const specialCommand = staticCommand({
|
||||||
|
name: 'special',
|
||||||
|
description: 'Command with special characters',
|
||||||
|
content:
|
||||||
|
'# Special\n\n```bash\necho "Hello $USER"\n```\n\n- Item 1\n- Item 2\n\n**Bold** and *italic*'
|
||||||
|
});
|
||||||
|
|
||||||
|
cursorProfile.addSlashCommands(tempDir, [specialCommand]);
|
||||||
|
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
const specialPath = path.join(tmDir, 'special.md');
|
||||||
|
const content = fs.readFileSync(specialPath, 'utf-8');
|
||||||
|
|
||||||
|
// Verify content is preserved exactly
|
||||||
|
expect(content).toBe(
|
||||||
|
'# Special\n\n```bash\necho "Hello $USER"\n```\n\n- Item 1\n- Item 2\n\n**Bold** and *italic*'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSlashCommands', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Add commands before testing removal
|
||||||
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove only TaskMaster commands (preserve user files)', () => {
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// Create a user's custom command file in the tm directory
|
||||||
|
const userCommandPath = path.join(tmDir, 'custom-user-command.md');
|
||||||
|
fs.writeFileSync(
|
||||||
|
userCommandPath,
|
||||||
|
'# Custom User Command\n\nThis is a user-created command.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify all files exist before removal
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'goham.md'))).toBe(true);
|
||||||
|
expect(fs.existsSync(userCommandPath)).toBe(true);
|
||||||
|
|
||||||
|
// Remove TaskMaster commands
|
||||||
|
const result = cursorProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Verify TaskMaster commands removed
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'goham.md'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify user's custom file preserved
|
||||||
|
expect(fs.existsSync(userCommandPath)).toBe(true);
|
||||||
|
expect(fs.readFileSync(userCommandPath, 'utf-8')).toBe(
|
||||||
|
'# Custom User Command\n\nThis is a user-created command.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify result
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
// File order is not guaranteed, so check both files are present
|
||||||
|
expect(result.files).toHaveLength(2);
|
||||||
|
expect(result.files).toContain('help.md');
|
||||||
|
expect(result.files).toContain('goham.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty tm directory after cleanup', () => {
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// Verify directory exists with files
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(true);
|
||||||
|
expect(fs.readdirSync(tmDir).length).toBe(2);
|
||||||
|
|
||||||
|
// Remove all commands (should cleanup empty directory)
|
||||||
|
const result = cursorProfile.removeSlashCommands(
|
||||||
|
tempDir,
|
||||||
|
testCommands,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify tm directory removed
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(false);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove directory if removeEmptyDir is false', () => {
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// Remove commands but keep directory
|
||||||
|
const result = cursorProfile.removeSlashCommands(
|
||||||
|
tempDir,
|
||||||
|
testCommands,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify directory still exists (but empty)
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(true);
|
||||||
|
expect(fs.statSync(tmDir).isDirectory()).toBe(true);
|
||||||
|
expect(fs.readdirSync(tmDir).length).toBe(0);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when directory does not exist', () => {
|
||||||
|
const nonExistentDir = path.join(tempDir, 'nonexistent');
|
||||||
|
|
||||||
|
// Remove commands from non-existent directory
|
||||||
|
const result = cursorProfile.removeSlashCommands(
|
||||||
|
nonExistentDir,
|
||||||
|
testCommands
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should succeed with 0 count
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.files).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive when matching command names', () => {
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// Check if filesystem is case-sensitive (Linux) or case-insensitive (macOS/Windows)
|
||||||
|
const testFile = path.join(tmDir, 'TEST-CASE.md');
|
||||||
|
fs.writeFileSync(testFile, 'test');
|
||||||
|
const isCaseSensitive = !fs.existsSync(path.join(tmDir, 'test-case.md'));
|
||||||
|
fs.rmSync(testFile);
|
||||||
|
|
||||||
|
// Create command with different casing from test commands
|
||||||
|
const upperCaseFile = path.join(tmDir, 'HELP.md');
|
||||||
|
fs.writeFileSync(upperCaseFile, '# Upper case help');
|
||||||
|
|
||||||
|
// Remove using lowercase name
|
||||||
|
const result = cursorProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// help.md should always be removed
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(false);
|
||||||
|
|
||||||
|
if (isCaseSensitive) {
|
||||||
|
// On case-sensitive filesystems, HELP.md is treated as different file
|
||||||
|
expect(fs.existsSync(upperCaseFile)).toBe(true);
|
||||||
|
expect(result.count).toBe(2); // help.md, goham.md
|
||||||
|
// Clean up
|
||||||
|
fs.rmSync(upperCaseFile);
|
||||||
|
} else {
|
||||||
|
// On case-insensitive filesystems (macOS/Windows), both should be removed
|
||||||
|
// because the filesystem treats help.md and HELP.md as the same file
|
||||||
|
expect(fs.existsSync(upperCaseFile)).toBe(false);
|
||||||
|
expect(result.count).toBe(2); // help.md (which is the same as HELP.md), goham.md
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Profile configuration', () => {
|
||||||
|
it('should have correct profile properties', () => {
|
||||||
|
expect(cursorProfile.name).toBe('cursor');
|
||||||
|
expect(cursorProfile.displayName).toBe('Cursor');
|
||||||
|
expect(cursorProfile.commandsDir).toBe('.cursor/commands');
|
||||||
|
expect(cursorProfile.extension).toBe('.md');
|
||||||
|
expect(cursorProfile.supportsCommands).toBe(true);
|
||||||
|
expect(cursorProfile.supportsNestedCommands).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate correct filenames (no prefix for nested structure)', () => {
|
||||||
|
// Cursor supports nested commands, so no tm- prefix
|
||||||
|
expect(cursorProfile.getFilename('help')).toBe('help.md');
|
||||||
|
expect(cursorProfile.getFilename('goham')).toBe('goham.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate correct commands path with tm subdirectory', () => {
|
||||||
|
// Path includes tm/ subdirectory for nested structure
|
||||||
|
const expectedPath = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
expect(cursorProfile.getCommandsPath(tempDir)).toBe(expectedPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Round-trip operations', () => {
|
||||||
|
it('should successfully add, remove, and re-add commands', () => {
|
||||||
|
// Files go to .cursor/commands/tm/
|
||||||
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
const addResult1 = cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
expect(addResult1.success).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(true);
|
||||||
|
|
||||||
|
// Remove commands
|
||||||
|
const removeResult = cursorProfile.removeSlashCommands(
|
||||||
|
tempDir,
|
||||||
|
testCommands
|
||||||
|
);
|
||||||
|
expect(removeResult.success).toBe(true);
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(false);
|
||||||
|
|
||||||
|
// Re-add commands
|
||||||
|
const addResult2 = cursorProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
expect(addResult2.success).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(true);
|
||||||
|
|
||||||
|
// Verify content is still correct
|
||||||
|
const content = fs.readFileSync(path.join(tmDir, 'help.md'), 'utf-8');
|
||||||
|
expect(content).toBe('# Help\n\nList of available Task Master commands.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Integration Tests for GeminiProfile
|
||||||
|
* Tests actual filesystem operations for adding and removing slash commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { GeminiProfile } from '../../src/slash-commands/profiles/gemini-profile.js';
|
||||||
|
import {
|
||||||
|
staticCommand,
|
||||||
|
dynamicCommand
|
||||||
|
} from '../../src/slash-commands/factories.js';
|
||||||
|
|
||||||
|
describe('GeminiProfile Integration Tests', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let profile: GeminiProfile;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create temporary directory for each test
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-profile-test-'));
|
||||||
|
profile = new GeminiProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory after each test
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSlashCommands()', () => {
|
||||||
|
it('should create the .gemini/commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.statSync(commandsDir).isDirectory()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write files with Python-style format (description="...", prompt = """...""")', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test description',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
const filePath = path.join(tempDir, '.gemini/commands/tm/test.toml');
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(fileContent).toBe(
|
||||||
|
'description="Test description"\nprompt = """\n# Test Content\n"""\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success result with correct count', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy app',
|
||||||
|
content: '# Deploy'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'review',
|
||||||
|
'Review PR',
|
||||||
|
'<pr-number>',
|
||||||
|
'# Review\n\nPR: $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
expect(result.files).toHaveLength(3);
|
||||||
|
expect(result.files).toContain('help.toml');
|
||||||
|
expect(result.files).toContain('deploy.toml');
|
||||||
|
expect(result.files).toContain('review.toml');
|
||||||
|
expect(result.directory).toBe(path.join(tempDir, '.gemini/commands/tm'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly escape double quotes in description', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test "quoted" description',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
const filePath = path.join(tempDir, '.gemini/commands/tm/test.toml');
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(fileContent).toContain(
|
||||||
|
'description="Test \\"quoted\\" description"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple commands with different types', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-cmd',
|
||||||
|
description: 'Static command',
|
||||||
|
content: '# Static Content\n\nThis is static.'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-cmd',
|
||||||
|
'Dynamic command',
|
||||||
|
'<arg>',
|
||||||
|
'# Dynamic Content\n\nArgument: $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
// Verify static command
|
||||||
|
const staticFilePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.gemini/commands/tm/static-cmd.toml'
|
||||||
|
);
|
||||||
|
const staticContent = fs.readFileSync(staticFilePath, 'utf-8');
|
||||||
|
expect(staticContent).toContain('description="Static command"');
|
||||||
|
expect(staticContent).toContain('# Static Content');
|
||||||
|
expect(staticContent).not.toContain('$ARGUMENTS');
|
||||||
|
|
||||||
|
// Verify dynamic command
|
||||||
|
const dynamicFilePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.gemini/commands/tm/dynamic-cmd.toml'
|
||||||
|
);
|
||||||
|
const dynamicContent = fs.readFileSync(dynamicFilePath, 'utf-8');
|
||||||
|
expect(dynamicContent).toContain('description="Dynamic command"');
|
||||||
|
expect(dynamicContent).toContain('Argument: $ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create directory recursively if parent directories do not exist', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: '# Test'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work when directory already exists', () => {
|
||||||
|
// Arrange
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
fs.mkdirSync(commandsDir, { recursive: true });
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: '# Test'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSlashCommands()', () => {
|
||||||
|
it('should remove only TaskMaster commands (preserves user files)', () => {
|
||||||
|
// Arrange
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
fs.mkdirSync(commandsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Add TaskMaster commands
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy',
|
||||||
|
content: '# Deploy'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
profile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Add user's custom command
|
||||||
|
const userFilePath = path.join(commandsDir, 'my-custom-command.toml');
|
||||||
|
fs.writeFileSync(
|
||||||
|
userFilePath,
|
||||||
|
'description="My custom command"\nprompt = """\n# Custom\n"""\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(result.files).toContain('help.toml');
|
||||||
|
expect(result.files).toContain('deploy.toml');
|
||||||
|
|
||||||
|
// Verify TaskMaster commands are removed
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'help.toml'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'deploy.toml'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify user's custom command is preserved
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty directory after cleanup', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: '# Test'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add commands first
|
||||||
|
profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
|
||||||
|
// Directory should be removed since it's empty
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove directory if user files remain (removeEmptyDir=true)', () => {
|
||||||
|
// Arrange
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
fs.mkdirSync(commandsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Add TaskMaster command
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Help',
|
||||||
|
content: '# Help'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
profile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Add user's custom command
|
||||||
|
const userFilePath = path.join(commandsDir, 'my-command.toml');
|
||||||
|
fs.writeFileSync(
|
||||||
|
userFilePath,
|
||||||
|
'description="User"\nprompt = """\n# User\n"""\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
|
||||||
|
// Directory should still exist because user file remains
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove directory if removeEmptyDir=false', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: '# Test'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add command first
|
||||||
|
profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(tempDir, commands, false);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
|
||||||
|
// Directory should still exist because removeEmptyDir=false
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Verify directory is empty
|
||||||
|
const remainingFiles = fs.readdirSync(commandsDir);
|
||||||
|
expect(remainingFiles).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success with count 0 if directory does not exist', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: '# Test'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act (directory doesn't exist)
|
||||||
|
const result = profile.removeSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.files).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removing subset of commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
fs.mkdirSync(commandsDir, { recursive: true });
|
||||||
|
|
||||||
|
const allCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Help',
|
||||||
|
content: '# Help'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy',
|
||||||
|
content: '# Deploy'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test',
|
||||||
|
content: '# Test'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add all commands
|
||||||
|
profile.addSlashCommands(tempDir, allCommands);
|
||||||
|
|
||||||
|
// Remove only 'help' and 'test'
|
||||||
|
const commandsToRemove = [allCommands[0], allCommands[2]];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(tempDir, commandsToRemove);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(result.files).toContain('help.toml');
|
||||||
|
expect(result.files).toContain('test.toml');
|
||||||
|
|
||||||
|
// Verify removed commands
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'help.toml'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'test.toml'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify 'deploy' remains
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'deploy.toml'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match commands case-insensitively', () => {
|
||||||
|
// Arrange
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands/tm');
|
||||||
|
fs.mkdirSync(commandsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create file with uppercase in name
|
||||||
|
const upperFilePath = path.join(commandsDir, 'HELP.toml');
|
||||||
|
fs.writeFileSync(
|
||||||
|
upperFilePath,
|
||||||
|
'description="Help"\nprompt = """\n# Help\n"""\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Help',
|
||||||
|
content: '# Help'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
expect(fs.existsSync(upperFilePath)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Full workflow: add then remove', () => {
|
||||||
|
it('should successfully add and then remove commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const commands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show help',
|
||||||
|
content: '# Help Content'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'review',
|
||||||
|
'Review PR',
|
||||||
|
'<pr-number>',
|
||||||
|
'# Review\n\nPR: $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.gemini/commands');
|
||||||
|
const tmDir = path.join(commandsDir, 'tm');
|
||||||
|
|
||||||
|
// Act - Add commands
|
||||||
|
const addResult = profile.addSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert - Add worked
|
||||||
|
expect(addResult.success).toBe(true);
|
||||||
|
expect(addResult.count).toBe(2);
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'help.toml'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(tmDir, 'review.toml'))).toBe(true);
|
||||||
|
|
||||||
|
// Act - Remove commands
|
||||||
|
const removeResult = profile.removeSlashCommands(tempDir, commands);
|
||||||
|
|
||||||
|
// Assert - Remove worked
|
||||||
|
expect(removeResult.success).toBe(true);
|
||||||
|
expect(removeResult.count).toBe(2);
|
||||||
|
// The tm subdirectory should be removed
|
||||||
|
expect(fs.existsSync(tmDir)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,630 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Integration Tests for OpenCodeProfile
|
||||||
|
* Tests actual filesystem operations using addSlashCommands and removeSlashCommands methods.
|
||||||
|
*
|
||||||
|
* OpenCodeProfile details:
|
||||||
|
* - commandsDir: '.opencode/command' (note: singular "command", not "commands")
|
||||||
|
* - extension: '.md'
|
||||||
|
* - Format: YAML frontmatter with description field
|
||||||
|
* - supportsNestedCommands: false (uses tm- prefix for filenames)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { OpenCodeProfile } from '../../src/slash-commands/profiles/opencode-profile.js';
|
||||||
|
import {
|
||||||
|
staticCommand,
|
||||||
|
dynamicCommand
|
||||||
|
} from '../../src/slash-commands/factories.js';
|
||||||
|
|
||||||
|
describe('OpenCodeProfile Integration Tests', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let openCodeProfile: OpenCodeProfile;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-profile-test-'));
|
||||||
|
openCodeProfile = new OpenCodeProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSlashCommands', () => {
|
||||||
|
it('should create the .opencode/command directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.statSync(commandsDir).isDirectory()).toBe(true);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write files with frontmatter and tm- prefix', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'plain-test',
|
||||||
|
description: 'Plain test command',
|
||||||
|
content: '# Original Content\n\nThis should remain unchanged.'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-plain-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
// OpenCode uses YAML frontmatter for metadata (no blank line after frontmatter)
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Plain test command\n---\n# Original Content\n\nThis should remain unchanged.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include description in frontmatter for dynamic commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-test',
|
||||||
|
'Dynamic test command',
|
||||||
|
'[task-id]',
|
||||||
|
'Process task: $ARGUMENTS\n\nThis processes the specified task.'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-dynamic-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
// OpenCode uses YAML frontmatter with description only (no argument-hint, no blank line after frontmatter)
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Dynamic test command\n---\nProcess task: $ARGUMENTS\n\nThis processes the specified task.'
|
||||||
|
);
|
||||||
|
expect(content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success result with correct count', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'First command',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'Second command',
|
||||||
|
content: 'Content 2'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'cmd3',
|
||||||
|
'Third command',
|
||||||
|
'[arg]',
|
||||||
|
'Content 3: $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
expect(result.directory).toBe(path.join(tempDir, '.opencode', 'command'));
|
||||||
|
expect(result.files).toHaveLength(3);
|
||||||
|
expect(result.files).toContain('tm-cmd1.md');
|
||||||
|
expect(result.files).toContain('tm-cmd2.md');
|
||||||
|
expect(result.files).toContain('tm-cmd3.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing files on re-run', () => {
|
||||||
|
// Arrange
|
||||||
|
const initialCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Initial description',
|
||||||
|
content: 'Initial content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act - First run
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, initialCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-test-cmd.md'
|
||||||
|
);
|
||||||
|
const initialContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(initialContent).toBe(
|
||||||
|
'---\ndescription: Initial description\n---\nInitial content'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act - Re-run with updated command
|
||||||
|
const updatedCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Updated description',
|
||||||
|
content: 'Updated content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, updatedCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const updatedContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(updatedContent).toBe(
|
||||||
|
'---\ndescription: Updated description\n---\nUpdated content'
|
||||||
|
);
|
||||||
|
expect(updatedContent).not.toContain('Initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple commands with mixed types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static1',
|
||||||
|
description: 'Static command 1',
|
||||||
|
content: 'Static content 1'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic1',
|
||||||
|
'Dynamic command 1',
|
||||||
|
'[id]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
),
|
||||||
|
staticCommand({
|
||||||
|
name: 'static2',
|
||||||
|
description: 'Static command 2',
|
||||||
|
content: 'Static content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
|
||||||
|
// Verify all files exist (with tm- prefix)
|
||||||
|
const static1Path = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-static1.md'
|
||||||
|
);
|
||||||
|
const dynamic1Path = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-dynamic1.md'
|
||||||
|
);
|
||||||
|
const static2Path = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-static2.md'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(static1Path)).toBe(true);
|
||||||
|
expect(fs.existsSync(dynamic1Path)).toBe(true);
|
||||||
|
expect(fs.existsSync(static2Path)).toBe(true);
|
||||||
|
|
||||||
|
// Verify content includes frontmatter
|
||||||
|
const static1Content = fs.readFileSync(static1Path, 'utf-8');
|
||||||
|
expect(static1Content).toBe(
|
||||||
|
'---\ndescription: Static command 1\n---\nStatic content 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
const dynamic1Content = fs.readFileSync(dynamic1Path, 'utf-8');
|
||||||
|
expect(dynamic1Content).toBe(
|
||||||
|
'---\ndescription: Dynamic command 1\n---\nDynamic content $ARGUMENTS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty command list', () => {
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.addSlashCommands(tempDir, []);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.files).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const multilineContent = `# Task Runner
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Run automated tasks for the project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Check dependencies
|
||||||
|
2. Run build
|
||||||
|
3. Execute tests
|
||||||
|
4. Generate report`;
|
||||||
|
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'task-runner',
|
||||||
|
description: 'Run automated tasks',
|
||||||
|
content: multilineContent
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-task-runner.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Run automated tasks\n---\n' + multilineContent
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve code blocks and special characters in content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const contentWithCode = `# Deploy
|
||||||
|
|
||||||
|
Run the deployment:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npm run deploy
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Use \`$HOME\` and \`$PATH\` variables. Also: <tag> & "quotes"`;
|
||||||
|
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy the application',
|
||||||
|
content: contentWithCode
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-deploy.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Deploy the application\n---\n' + contentWithCode
|
||||||
|
);
|
||||||
|
expect(content).toContain('```bash');
|
||||||
|
expect(content).toContain('$HOME');
|
||||||
|
expect(content).toContain('<tag> & "quotes"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSlashCommands', () => {
|
||||||
|
it('should remove only TaskMaster commands and preserve user files', () => {
|
||||||
|
// Arrange - Add TaskMaster commands
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'TaskMaster command 1',
|
||||||
|
content: 'TM Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'TaskMaster command 2',
|
||||||
|
content: 'TM Content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Create a user file manually
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
const userFilePath = path.join(commandsDir, 'user-custom.md');
|
||||||
|
fs.writeFileSync(userFilePath, 'User custom command\n\nUser content');
|
||||||
|
|
||||||
|
// Act - Remove TaskMaster commands
|
||||||
|
const result = openCodeProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(result.files).toHaveLength(2);
|
||||||
|
|
||||||
|
// Verify TaskMaster files are removed (tm- prefix is added automatically)
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-cmd1.md'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-cmd2.md'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify user file is preserved
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
const userContent = fs.readFileSync(userFilePath, 'utf-8');
|
||||||
|
expect(userContent).toContain('User custom command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty directory after cleanup', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'only-cmd',
|
||||||
|
description: 'Only command',
|
||||||
|
content: 'Only content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Act - Remove all TaskMaster commands
|
||||||
|
const result = openCodeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
// Directory should be removed when empty (default behavior)
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep directory when user files remain', () => {
|
||||||
|
// Arrange
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd',
|
||||||
|
description: 'TaskMaster command',
|
||||||
|
content: 'TM Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Add user file
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
const userFilePath = path.join(commandsDir, 'my-command.md');
|
||||||
|
fs.writeFileSync(userFilePath, 'My custom command');
|
||||||
|
|
||||||
|
// Act - Remove TaskMaster commands
|
||||||
|
const result = openCodeProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
// Directory should still exist because user file remains
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when no files exist', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'nonexistent',
|
||||||
|
description: 'Non-existent command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act - Don't add commands, just try to remove
|
||||||
|
const result = openCodeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.files).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when directory does not exist', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ensure .opencode/command doesn't exist
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove mixed command types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-cmd',
|
||||||
|
description: 'Static command',
|
||||||
|
content: 'Static content'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-cmd',
|
||||||
|
'Dynamic command',
|
||||||
|
'[arg]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-static-cmd.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-dynamic-cmd.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-static-cmd.md'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-dynamic-cmd.md'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
// Directory should be removed since it's empty
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove directory when removeEmptyDir is false', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Act - Remove with removeEmptyDir=false
|
||||||
|
const result = openCodeProfile.removeSlashCommands(
|
||||||
|
tempDir,
|
||||||
|
testCommands,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
// Directory should still exist even though it's empty
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle commands with special characters in names', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd-123',
|
||||||
|
description: 'Test with numbers',
|
||||||
|
content: 'Content'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'test_underscore',
|
||||||
|
description: 'Test with underscore',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.opencode', 'command');
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-test-cmd-123.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(commandsDir, 'tm-test_underscore.md'))
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve exact formatting in complex content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexContent = `# Search Command
|
||||||
|
|
||||||
|
## Input
|
||||||
|
User provided: $ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Parse the input: \`$ARGUMENTS\`
|
||||||
|
2. Search for matches
|
||||||
|
3. Display results
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Query: $ARGUMENTS
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const testCommands = [
|
||||||
|
dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search the codebase',
|
||||||
|
'<query>',
|
||||||
|
complexContent
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
openCodeProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.opencode',
|
||||||
|
'command',
|
||||||
|
'tm-search.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Search the codebase\n---\n' + complexContent
|
||||||
|
);
|
||||||
|
// Verify all $ARGUMENTS placeholders are preserved
|
||||||
|
expect(content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,616 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Integration Tests for RooProfile
|
||||||
|
* Tests actual filesystem operations using addSlashCommands and removeSlashCommands methods.
|
||||||
|
*
|
||||||
|
* RooProfile details:
|
||||||
|
* - commandsDir: '.roo/commands'
|
||||||
|
* - extension: '.md'
|
||||||
|
* - Format: YAML frontmatter with description and optional argument-hint
|
||||||
|
* - supportsNestedCommands: false (uses tm- prefix for filenames)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { RooProfile } from '../../src/slash-commands/profiles/roo-profile.js';
|
||||||
|
import {
|
||||||
|
staticCommand,
|
||||||
|
dynamicCommand
|
||||||
|
} from '../../src/slash-commands/factories.js';
|
||||||
|
|
||||||
|
describe('RooProfile Integration Tests', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let rooProfile: RooProfile;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roo-profile-test-'));
|
||||||
|
rooProfile = new RooProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addSlashCommands', () => {
|
||||||
|
it('should create the .roo/commands directory', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: '# Test Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.statSync(commandsDir).isDirectory()).toBe(true);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write files with frontmatter and tm- prefix', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'plain-test',
|
||||||
|
description: 'Plain test command',
|
||||||
|
content: '# Original Content\n\nThis should remain unchanged.'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.roo',
|
||||||
|
'commands',
|
||||||
|
'tm-plain-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
// Roo uses YAML frontmatter for metadata
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Plain test command\n---\n\n# Original Content\n\nThis should remain unchanged.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include argument-hint in frontmatter for dynamic commands', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-test',
|
||||||
|
'Dynamic test command',
|
||||||
|
'[task-id]',
|
||||||
|
'Process task: $ARGUMENTS\n\nThis processes the specified task.'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.roo',
|
||||||
|
'commands',
|
||||||
|
'tm-dynamic-test.md'
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
// Roo uses YAML frontmatter with argument-hint
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Dynamic test command\nargument-hint: [task-id]\n---\n\nProcess task: $ARGUMENTS\n\nThis processes the specified task.'
|
||||||
|
);
|
||||||
|
expect(content).toContain('$ARGUMENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success result with correct count', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'First command',
|
||||||
|
content: 'Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'Second command',
|
||||||
|
content: 'Content 2'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'cmd3',
|
||||||
|
'Third command',
|
||||||
|
'[arg]',
|
||||||
|
'Content 3: $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
expect(result.directory).toBe(path.join(tempDir, '.roo', 'commands'));
|
||||||
|
expect(result.files).toHaveLength(3);
|
||||||
|
expect(result.files).toContain('tm-cmd1.md');
|
||||||
|
expect(result.files).toContain('tm-cmd2.md');
|
||||||
|
expect(result.files).toContain('tm-cmd3.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing files on re-run', () => {
|
||||||
|
// Arrange
|
||||||
|
const initialCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Initial description',
|
||||||
|
content: 'Initial content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act - First run
|
||||||
|
rooProfile.addSlashCommands(tempDir, initialCommands);
|
||||||
|
|
||||||
|
const filePath = path.join(tempDir, '.roo', 'commands', 'tm-test-cmd.md');
|
||||||
|
const initialContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(initialContent).toBe(
|
||||||
|
'---\ndescription: Initial description\n---\n\nInitial content'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act - Re-run with updated command
|
||||||
|
const updatedCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Updated description',
|
||||||
|
content: 'Updated content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
rooProfile.addSlashCommands(tempDir, updatedCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const updatedContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(updatedContent).toBe(
|
||||||
|
'---\ndescription: Updated description\n---\n\nUpdated content'
|
||||||
|
);
|
||||||
|
expect(updatedContent).not.toContain('Initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple commands with mixed types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static1',
|
||||||
|
description: 'Static command 1',
|
||||||
|
content: 'Static content 1'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic1',
|
||||||
|
'Dynamic command 1',
|
||||||
|
'[id]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
),
|
||||||
|
staticCommand({
|
||||||
|
name: 'static2',
|
||||||
|
description: 'Static command 2',
|
||||||
|
content: 'Static content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(3);
|
||||||
|
|
||||||
|
// Verify all files exist (with tm- prefix)
|
||||||
|
const static1Path = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.roo',
|
||||||
|
'commands',
|
||||||
|
'tm-static1.md'
|
||||||
|
);
|
||||||
|
const dynamic1Path = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.roo',
|
||||||
|
'commands',
|
||||||
|
'tm-dynamic1.md'
|
||||||
|
);
|
||||||
|
const static2Path = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.roo',
|
||||||
|
'commands',
|
||||||
|
'tm-static2.md'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(static1Path)).toBe(true);
|
||||||
|
expect(fs.existsSync(dynamic1Path)).toBe(true);
|
||||||
|
expect(fs.existsSync(static2Path)).toBe(true);
|
||||||
|
|
||||||
|
// Verify content includes frontmatter
|
||||||
|
const static1Content = fs.readFileSync(static1Path, 'utf-8');
|
||||||
|
expect(static1Content).toBe(
|
||||||
|
'---\ndescription: Static command 1\n---\n\nStatic content 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
const dynamic1Content = fs.readFileSync(dynamic1Path, 'utf-8');
|
||||||
|
expect(dynamic1Content).toBe(
|
||||||
|
'---\ndescription: Dynamic command 1\nargument-hint: [id]\n---\n\nDynamic content $ARGUMENTS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty command list', () => {
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.addSlashCommands(tempDir, []);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.files).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve multiline content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const multilineContent = `# Task Runner
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Run automated tasks for the project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Check dependencies
|
||||||
|
2. Run build
|
||||||
|
3. Execute tests
|
||||||
|
4. Generate report`;
|
||||||
|
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'task-runner',
|
||||||
|
description: 'Run automated tasks',
|
||||||
|
content: multilineContent
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
'.roo',
|
||||||
|
'commands',
|
||||||
|
'tm-task-runner.md'
|
||||||
|
);
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Run automated tasks\n---\n\n' + multilineContent
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve code blocks and special characters in content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const contentWithCode = `# Deploy
|
||||||
|
|
||||||
|
Run the deployment:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npm run deploy
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Use \`$HOME\` and \`$PATH\` variables. Also: <tag> & "quotes"`;
|
||||||
|
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deploy the application',
|
||||||
|
content: contentWithCode
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(tempDir, '.roo', 'commands', 'tm-deploy.md');
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Deploy the application\n---\n\n' + contentWithCode
|
||||||
|
);
|
||||||
|
expect(content).toContain('```bash');
|
||||||
|
expect(content).toContain('$HOME');
|
||||||
|
expect(content).toContain('<tag> & "quotes"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSlashCommands', () => {
|
||||||
|
it('should remove only TaskMaster commands and preserve user files', () => {
|
||||||
|
// Arrange - Add TaskMaster commands
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd1',
|
||||||
|
description: 'TaskMaster command 1',
|
||||||
|
content: 'TM Content 1'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd2',
|
||||||
|
description: 'TaskMaster command 2',
|
||||||
|
content: 'TM Content 2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
rooProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Create a user file manually
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
const userFilePath = path.join(commandsDir, 'user-custom.md');
|
||||||
|
fs.writeFileSync(userFilePath, 'User custom command\n\nUser content');
|
||||||
|
|
||||||
|
// Act - Remove TaskMaster commands
|
||||||
|
const result = rooProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(result.files).toHaveLength(2);
|
||||||
|
|
||||||
|
// Verify TaskMaster files are removed (tm- prefix is added automatically)
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-cmd1.md'))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-cmd2.md'))).toBe(false);
|
||||||
|
|
||||||
|
// Verify user file is preserved
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
const userContent = fs.readFileSync(userFilePath, 'utf-8');
|
||||||
|
expect(userContent).toContain('User custom command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty directory after cleanup', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'only-cmd',
|
||||||
|
description: 'Only command',
|
||||||
|
content: 'Only content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Act - Remove all TaskMaster commands
|
||||||
|
const result = rooProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
// Directory should be removed when empty (default behavior)
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep directory when user files remain', () => {
|
||||||
|
// Arrange
|
||||||
|
const tmCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'cmd',
|
||||||
|
description: 'TaskMaster command',
|
||||||
|
content: 'TM Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
rooProfile.addSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Add user file
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
const userFilePath = path.join(commandsDir, 'my-command.md');
|
||||||
|
fs.writeFileSync(userFilePath, 'My custom command');
|
||||||
|
|
||||||
|
// Act - Remove TaskMaster commands
|
||||||
|
const result = rooProfile.removeSlashCommands(tempDir, tmCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
// Directory should still exist because user file remains
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
expect(fs.existsSync(userFilePath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when no files exist', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'nonexistent',
|
||||||
|
description: 'Non-existent command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act - Don't add commands, just try to remove
|
||||||
|
const result = rooProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
expect(result.files).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal when directory does not exist', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ensure .roo/commands doesn't exist
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove mixed command types', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'static-cmd',
|
||||||
|
description: 'Static command',
|
||||||
|
content: 'Static content'
|
||||||
|
}),
|
||||||
|
dynamicCommand(
|
||||||
|
'dynamic-cmd',
|
||||||
|
'Dynamic command',
|
||||||
|
'[arg]',
|
||||||
|
'Dynamic content $ARGUMENTS'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-static-cmd.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-dynamic-cmd.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.removeSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-static-cmd.md'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-dynamic-cmd.md'))).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
// Directory should be removed since it's empty
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove directory when removeEmptyDir is false', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd',
|
||||||
|
description: 'Test command',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
|
||||||
|
// Act - Remove with removeEmptyDir=false
|
||||||
|
const result = rooProfile.removeSlashCommands(
|
||||||
|
tempDir,
|
||||||
|
testCommands,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
// Directory should still exist even though it's empty
|
||||||
|
expect(fs.existsSync(commandsDir)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle commands with special characters in names', () => {
|
||||||
|
// Arrange
|
||||||
|
const testCommands = [
|
||||||
|
staticCommand({
|
||||||
|
name: 'test-cmd-123',
|
||||||
|
description: 'Test with numbers',
|
||||||
|
content: 'Content'
|
||||||
|
}),
|
||||||
|
staticCommand({
|
||||||
|
name: 'test_underscore',
|
||||||
|
description: 'Test with underscore',
|
||||||
|
content: 'Content'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
const commandsDir = path.join(tempDir, '.roo', 'commands');
|
||||||
|
expect(fs.existsSync(path.join(commandsDir, 'tm-test-cmd-123.md'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(commandsDir, 'tm-test_underscore.md'))
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve exact formatting in complex content with frontmatter', () => {
|
||||||
|
// Arrange
|
||||||
|
const complexContent = `# Search Command
|
||||||
|
|
||||||
|
## Input
|
||||||
|
User provided: $ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Parse the input: \`$ARGUMENTS\`
|
||||||
|
2. Search for matches
|
||||||
|
3. Display results
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Query: $ARGUMENTS
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const testCommands = [
|
||||||
|
dynamicCommand(
|
||||||
|
'search',
|
||||||
|
'Search the codebase',
|
||||||
|
'<query>',
|
||||||
|
complexContent
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
rooProfile.addSlashCommands(tempDir, testCommands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const filePath = path.join(tempDir, '.roo', 'commands', 'tm-search.md');
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
expect(content).toBe(
|
||||||
|
'---\ndescription: Search the codebase\nargument-hint: <query>\n---\n\n' +
|
||||||
|
complexContent
|
||||||
|
);
|
||||||
|
// Verify all $ARGUMENTS placeholders are preserved
|
||||||
|
expect(content.match(/\$ARGUMENTS/g)?.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
36
packages/tm-profiles/tsconfig.json
Normal file
36
packages/tm-profiles/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"rootDir": ".",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"types": ["node", "vitest/globals"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
27
packages/tm-profiles/vitest.config.ts
Normal file
27
packages/tm-profiles/vitest.config.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package-specific Vitest configuration for @tm/profiles
|
||||||
|
* Only tests the profile classes, not the command definitions
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.spec.ts', 'src/**/*.test.ts', 'tests/**/*.test.ts'],
|
||||||
|
exclude: ['node_modules', 'dist'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
enabled: true,
|
||||||
|
reporter: ['text'],
|
||||||
|
// Only measure coverage for profile classes
|
||||||
|
exclude: ['node_modules', 'dist', 'src/slash-commands/commands/**'],
|
||||||
|
thresholds: {
|
||||||
|
branches: 70,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -716,12 +716,21 @@ function updateStorageConfig(configPath, selectedStorage, authCredentials) {
|
|||||||
process.env.TM_PUBLIC_BASE_DOMAIN ||
|
process.env.TM_PUBLIC_BASE_DOMAIN ||
|
||||||
'https://tryhamster.com/api';
|
'https://tryhamster.com/api';
|
||||||
|
|
||||||
|
// Set operating mode to 'team' for cloud storage (Hamster)
|
||||||
|
// This determines which slash commands and rules are installed
|
||||||
|
config.storage.operatingMode = 'team';
|
||||||
|
|
||||||
// Note: Access token is stored in ~/.taskmaster/auth.json by AuthManager
|
// Note: Access token is stored in ~/.taskmaster/auth.json by AuthManager
|
||||||
// We don't store it in config.json for security reasons
|
// We don't store it in config.json for security reasons
|
||||||
log('debug', 'Connected to Hamster Studio');
|
log('debug', 'Connected to Hamster Studio');
|
||||||
} else {
|
} else {
|
||||||
// Configure for local file storage
|
// Configure for local file storage
|
||||||
config.storage.type = 'file';
|
config.storage.type = 'file';
|
||||||
|
|
||||||
|
// Set operating mode to 'solo' for local storage (Taskmaster standalone)
|
||||||
|
// This determines which slash commands and rules are installed
|
||||||
|
config.storage.operatingMode = 'solo';
|
||||||
|
|
||||||
log('debug', 'Configured storage for local file storage');
|
log('debug', 'Configured storage for local file storage');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,10 +852,18 @@ async function createProjectStructure(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to create rule profiles
|
// Helper function to create rule profiles
|
||||||
|
// Derives operating mode from storage selection:
|
||||||
|
// - 'cloud' (Hamster) -> 'team' mode
|
||||||
|
// - 'local' (Taskmaster standalone) -> 'solo' mode
|
||||||
|
const operatingMode = selectedStorage === 'cloud' ? 'team' : 'solo';
|
||||||
|
|
||||||
function _processSingleProfile(profileName) {
|
function _processSingleProfile(profileName) {
|
||||||
const profile = getRulesProfile(profileName);
|
const profile = getRulesProfile(profileName);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
convertAllRulesToProfileRules(targetDir, profile);
|
// Pass operating mode to filter rules and slash commands
|
||||||
|
convertAllRulesToProfileRules(targetDir, profile, {
|
||||||
|
mode: operatingMode
|
||||||
|
});
|
||||||
// Also triggers MCP config setup (if applicable)
|
// Also triggers MCP config setup (if applicable)
|
||||||
} else {
|
} else {
|
||||||
log('warn', `Unknown rule profile: ${profileName}`);
|
log('warn', `Unknown rule profile: ${profileName}`);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import {
|
|||||||
getConfig,
|
getConfig,
|
||||||
getDebugFlag,
|
getDebugFlag,
|
||||||
getDefaultNumTasks,
|
getDefaultNumTasks,
|
||||||
|
getOperatingMode,
|
||||||
isApiKeySet,
|
isApiKeySet,
|
||||||
isConfigFilePresent,
|
isConfigFilePresent,
|
||||||
setSuppressConfigWarnings
|
setSuppressConfigWarnings
|
||||||
@@ -4388,11 +4389,16 @@ Examples:
|
|||||||
`--${RULES_SETUP_ACTION}`,
|
`--${RULES_SETUP_ACTION}`,
|
||||||
'Run interactive setup to select rule profiles to add'
|
'Run interactive setup to select rule profiles to add'
|
||||||
)
|
)
|
||||||
|
.option(
|
||||||
|
'-m, --mode <mode>',
|
||||||
|
'Operating mode for filtering rules/commands (solo or team). Auto-detected from config if not specified.'
|
||||||
|
)
|
||||||
.addHelpText(
|
.addHelpText(
|
||||||
'after',
|
'after',
|
||||||
`
|
`
|
||||||
Examples:
|
Examples:
|
||||||
$ task-master rules ${RULES_ACTIONS.ADD} windsurf roo # Add Windsurf and Roo rule sets
|
$ task-master rules ${RULES_ACTIONS.ADD} windsurf roo # Add Windsurf and Roo rule sets
|
||||||
|
$ task-master rules ${RULES_ACTIONS.ADD} cursor --mode=team # Add Cursor rules for team mode only
|
||||||
$ task-master rules ${RULES_ACTIONS.REMOVE} windsurf # Remove Windsurf rule set
|
$ task-master rules ${RULES_ACTIONS.REMOVE} windsurf # Remove Windsurf rule set
|
||||||
$ task-master rules --${RULES_SETUP_ACTION} # Interactive setup to select rule profiles`
|
$ task-master rules --${RULES_SETUP_ACTION} # Interactive setup to select rule profiles`
|
||||||
)
|
)
|
||||||
@@ -4447,10 +4453,11 @@ Examples:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const profileConfig = getRulesProfile(profile);
|
const profileConfig = getRulesProfile(profile);
|
||||||
|
const mode = await getOperatingMode(options.mode);
|
||||||
const addResult = convertAllRulesToProfileRules(
|
const addResult = convertAllRulesToProfileRules(
|
||||||
projectRoot,
|
projectRoot,
|
||||||
profileConfig
|
profileConfig,
|
||||||
|
{ mode }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(chalk.green(generateProfileSummary(profile, addResult)));
|
console.log(chalk.green(generateProfileSummary(profile, addResult)));
|
||||||
@@ -4525,9 +4532,11 @@ Examples:
|
|||||||
|
|
||||||
if (action === RULES_ACTIONS.ADD) {
|
if (action === RULES_ACTIONS.ADD) {
|
||||||
console.log(chalk.blue(`Adding rules for profile: ${profile}...`));
|
console.log(chalk.blue(`Adding rules for profile: ${profile}...`));
|
||||||
|
const mode = await getOperatingMode(options.mode);
|
||||||
const addResult = convertAllRulesToProfileRules(
|
const addResult = convertAllRulesToProfileRules(
|
||||||
projectRoot,
|
projectRoot,
|
||||||
profileConfig
|
profileConfig,
|
||||||
|
{ mode }
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
chalk.blue(`Completed adding rules for profile: ${profile}`)
|
chalk.blue(`Completed adding rules for profile: ${profile}`)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import path from 'path';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import {
|
import {
|
||||||
ALL_PROVIDERS,
|
ALL_PROVIDERS,
|
||||||
|
AuthManager,
|
||||||
CUSTOM_PROVIDERS,
|
CUSTOM_PROVIDERS,
|
||||||
CUSTOM_PROVIDERS_ARRAY,
|
CUSTOM_PROVIDERS_ARRAY,
|
||||||
VALIDATED_PROVIDERS
|
VALIDATED_PROVIDERS
|
||||||
@@ -1189,6 +1190,51 @@ function getBaseUrlForRole(role, explicitRoot = null) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the operating mode for rules/commands filtering.
|
||||||
|
* Priority order:
|
||||||
|
* 1. Explicit CLI flag (--mode=solo|team)
|
||||||
|
* 2. Config file (storage.operatingMode)
|
||||||
|
* 3. Auth status fallback (authenticated = team, else solo)
|
||||||
|
*
|
||||||
|
* @param {string|undefined} explicitMode - Mode passed via CLI flag
|
||||||
|
* @returns {Promise<'solo'|'team'>} The operating mode
|
||||||
|
*/
|
||||||
|
async function getOperatingMode(explicitMode) {
|
||||||
|
// 1. CLI flag takes precedence
|
||||||
|
if (explicitMode === 'solo' || explicitMode === 'team') {
|
||||||
|
return explicitMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check config file for operatingMode
|
||||||
|
try {
|
||||||
|
setSuppressConfigWarnings(true);
|
||||||
|
const config = getConfig(null, false, { storageType: 'api' });
|
||||||
|
if (config?.storage?.operatingMode) {
|
||||||
|
return config.storage.operatingMode;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Config check failed, continue to fallback
|
||||||
|
} finally {
|
||||||
|
setSuppressConfigWarnings(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback: Check auth status
|
||||||
|
// If authenticated with Hamster, assume team mode
|
||||||
|
try {
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
const credentials = await authManager.getAuthCredentials();
|
||||||
|
if (credentials) {
|
||||||
|
return 'team';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Auth check failed, default to solo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to solo mode
|
||||||
|
return 'solo';
|
||||||
|
}
|
||||||
|
|
||||||
// Export the providers without API keys array for use in other modules
|
// Export the providers without API keys array for use in other modules
|
||||||
export const providersWithoutApiKeys = [
|
export const providersWithoutApiKeys = [
|
||||||
CUSTOM_PROVIDERS.OLLAMA,
|
CUSTOM_PROVIDERS.OLLAMA,
|
||||||
@@ -1257,6 +1303,8 @@ export {
|
|||||||
getAnonymousTelemetryEnabled,
|
getAnonymousTelemetryEnabled,
|
||||||
getParametersForRole,
|
getParametersForRole,
|
||||||
getUserId,
|
getUserId,
|
||||||
|
// Operating mode
|
||||||
|
getOperatingMode,
|
||||||
// API Key Checkers (still relevant)
|
// API Key Checkers (still relevant)
|
||||||
isApiKeySet,
|
isApiKeySet,
|
||||||
getMcpApiKeyStatus,
|
getMcpApiKeyStatus,
|
||||||
|
|||||||
@@ -1,5 +1,52 @@
|
|||||||
// Base profile factory for rule-transformer
|
// Base profile factory for rule-transformer
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getProfile, allCommands } from '@tm/profiles';
|
||||||
|
import { log } from '../../scripts/modules/utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule files categorized by operating mode.
|
||||||
|
* - Solo: Rules for local file-based storage (Taskmaster standalone)
|
||||||
|
* - Team: Rules for API/cloud storage (Hamster integration)
|
||||||
|
*
|
||||||
|
* Team mode is EXCLUSIVE - team users get ONLY team-specific rules.
|
||||||
|
*/
|
||||||
|
export const RULE_MODES = {
|
||||||
|
/** Solo-only rules (local file storage) */
|
||||||
|
solo: [
|
||||||
|
'rules/taskmaster.mdc',
|
||||||
|
'rules/dev_workflow.mdc',
|
||||||
|
'rules/self_improve.mdc',
|
||||||
|
'rules/cursor_rules.mdc',
|
||||||
|
'rules/taskmaster_hooks_workflow.mdc'
|
||||||
|
],
|
||||||
|
/** Team-only rules (API/cloud storage - exclusive) */
|
||||||
|
team: ['rules/hamster.mdc']
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter rules by operating mode.
|
||||||
|
* Team mode is EXCLUSIVE - returns ONLY team-specific rules.
|
||||||
|
*
|
||||||
|
* @param {Object} fileMap - The rule fileMap object
|
||||||
|
* @param {'solo' | 'team'} mode - Operating mode
|
||||||
|
* @returns {Object} - Filtered fileMap
|
||||||
|
*/
|
||||||
|
export function filterRulesByMode(fileMap, mode) {
|
||||||
|
if (mode === 'team') {
|
||||||
|
// Team mode: ONLY team-specific rules (exclusive)
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(fileMap).filter(([sourceFile]) =>
|
||||||
|
RULE_MODES.team.includes(sourceFile)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Solo mode: solo-specific rules only (no team rules)
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(fileMap).filter(([sourceFile]) =>
|
||||||
|
RULE_MODES.solo.includes(sourceFile)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a standardized profile configuration for different editors
|
* Creates a standardized profile configuration for different editors
|
||||||
@@ -221,6 +268,23 @@ export function createProfile(editorConfig) {
|
|||||||
: sourceFilename;
|
: sourceFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-detect slash command support from @tm/profiles
|
||||||
|
let slashCommands = null;
|
||||||
|
try {
|
||||||
|
const slashCommandProfile = getProfile(name);
|
||||||
|
if (slashCommandProfile?.supportsCommands) {
|
||||||
|
slashCommands = {
|
||||||
|
profile: slashCommandProfile,
|
||||||
|
commands: allCommands
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`[${displayName}] Slash command profile lookup failed: ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profileName: name, // Use name for programmatic access (tests expect this)
|
profileName: name, // Use name for programmatic access (tests expect this)
|
||||||
displayName: displayName, // Keep displayName for UI purposes
|
displayName: displayName, // Keep displayName for UI purposes
|
||||||
@@ -236,6 +300,8 @@ export function createProfile(editorConfig) {
|
|||||||
conversionConfig,
|
conversionConfig,
|
||||||
getTargetRuleFilename,
|
getTargetRuleFilename,
|
||||||
targetExtension,
|
targetExtension,
|
||||||
|
// Declarative slash command config - rule-transformer handles execution
|
||||||
|
slashCommands,
|
||||||
// Optional lifecycle hooks
|
// Optional lifecycle hooks
|
||||||
...(onAdd && { onAddRulesProfile: onAdd }),
|
...(onAdd && { onAddRulesProfile: onAdd }),
|
||||||
...(onRemove && { onRemoveRulesProfile: onRemove }),
|
...(onRemove && { onRemoveRulesProfile: onRemove }),
|
||||||
|
|||||||
@@ -1,135 +1,6 @@
|
|||||||
import fs from 'fs';
|
|
||||||
// Cursor conversion profile for rule-transformer
|
// Cursor conversion profile for rule-transformer
|
||||||
import path from 'path';
|
|
||||||
import { log } from '../../scripts/modules/utils.js';
|
|
||||||
import { createProfile } from './base-profile.js';
|
import { createProfile } from './base-profile.js';
|
||||||
|
|
||||||
// Helper copy; use cpSync when available, fallback to manual recursion
|
|
||||||
function copyRecursiveSync(src, dest) {
|
|
||||||
if (fs.cpSync) {
|
|
||||||
try {
|
|
||||||
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to copy ${src} to ${dest}: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const exists = fs.existsSync(src);
|
|
||||||
let stats = null;
|
|
||||||
let isDirectory = false;
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
try {
|
|
||||||
stats = fs.statSync(src);
|
|
||||||
isDirectory = stats.isDirectory();
|
|
||||||
} catch (err) {
|
|
||||||
// Handle TOCTOU race condition - treat as non-existent/not-a-directory
|
|
||||||
isDirectory = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDirectory) {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
||||||
for (const child of fs.readdirSync(src)) {
|
|
||||||
copyRecursiveSync(path.join(src, child), path.join(dest, child));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to copy directory ${src} to ${dest}: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// ensure parent exists for file copies
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to copy file ${src} to ${dest}: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to recursively remove directory
|
|
||||||
function removeDirectoryRecursive(dirPath) {
|
|
||||||
if (fs.existsSync(dirPath)) {
|
|
||||||
try {
|
|
||||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the Cursor profile directory from either project root, profile root, or rules dir
|
|
||||||
function resolveCursorProfileDir(baseDir) {
|
|
||||||
const base = path.basename(baseDir);
|
|
||||||
// If called with .../.cursor/rules -> return .../.cursor
|
|
||||||
if (base === 'rules' && path.basename(path.dirname(baseDir)) === '.cursor') {
|
|
||||||
return path.dirname(baseDir);
|
|
||||||
}
|
|
||||||
// If called with .../.cursor -> return as-is
|
|
||||||
if (base === '.cursor') return baseDir;
|
|
||||||
// Otherwise assume project root and append .cursor
|
|
||||||
return path.join(baseDir, '.cursor');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle functions for Cursor profile
|
|
||||||
function onAddRulesProfile(targetDir, assetsDir) {
|
|
||||||
// Copy commands directory recursively
|
|
||||||
const commandsSourceDir = path.join(assetsDir, 'claude', 'commands');
|
|
||||||
const profileDir = resolveCursorProfileDir(targetDir);
|
|
||||||
const commandsDestDir = path.join(profileDir, 'commands');
|
|
||||||
|
|
||||||
if (!fs.existsSync(commandsSourceDir)) {
|
|
||||||
log(
|
|
||||||
'warn',
|
|
||||||
`[Cursor] Source commands directory does not exist: ${commandsSourceDir}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure fresh state to avoid stale command files
|
|
||||||
try {
|
|
||||||
fs.rmSync(commandsDestDir, { recursive: true, force: true });
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
`[Cursor] Removed existing commands directory: ${commandsDestDir}`
|
|
||||||
);
|
|
||||||
} catch (deleteErr) {
|
|
||||||
// Directory might not exist, which is fine
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
`[Cursor] Commands directory did not exist or could not be removed: ${deleteErr.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRecursiveSync(commandsSourceDir, commandsDestDir);
|
|
||||||
log('debug', `[Cursor] Copied commands directory to ${commandsDestDir}`);
|
|
||||||
} catch (err) {
|
|
||||||
log(
|
|
||||||
'error',
|
|
||||||
`[Cursor] An error occurred during commands copy: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemoveRulesProfile(targetDir) {
|
|
||||||
// Remove .cursor/commands directory recursively
|
|
||||||
const profileDir = resolveCursorProfileDir(targetDir);
|
|
||||||
const commandsDir = path.join(profileDir, 'commands');
|
|
||||||
if (removeDirectoryRecursive(commandsDir)) {
|
|
||||||
log(
|
|
||||||
'debug',
|
|
||||||
`[Cursor] Ensured commands directory removed at ${commandsDir}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and export cursor profile using the base factory
|
// Create and export cursor profile using the base factory
|
||||||
export const cursorProfile = createProfile({
|
export const cursorProfile = createProfile({
|
||||||
name: 'cursor',
|
name: 'cursor',
|
||||||
@@ -137,10 +8,5 @@ export const cursorProfile = createProfile({
|
|||||||
url: 'cursor.so',
|
url: 'cursor.so',
|
||||||
docsUrl: 'docs.cursor.com',
|
docsUrl: 'docs.cursor.com',
|
||||||
targetExtension: '.mdc', // Cursor keeps .mdc extension
|
targetExtension: '.mdc', // Cursor keeps .mdc extension
|
||||||
supportsRulesSubdirectories: true,
|
supportsRulesSubdirectories: true
|
||||||
onAdd: onAddRulesProfile,
|
|
||||||
onRemove: onRemoveRulesProfile
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export lifecycle functions separately to avoid naming conflicts
|
|
||||||
export { onAddRulesProfile, onRemoveRulesProfile };
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import { RULE_PROFILES } from '../constants/profiles.js';
|
|||||||
// --- Profile Imports ---
|
// --- Profile Imports ---
|
||||||
import * as profilesModule from '../profiles/index.js';
|
import * as profilesModule from '../profiles/index.js';
|
||||||
|
|
||||||
|
// Import rule filtering
|
||||||
|
import { filterRulesByMode } from '../profiles/base-profile.js';
|
||||||
|
|
||||||
export function isValidProfile(profile) {
|
export function isValidProfile(profile) {
|
||||||
return RULE_PROFILES.includes(profile);
|
return RULE_PROFILES.includes(profile);
|
||||||
}
|
}
|
||||||
@@ -198,14 +201,66 @@ export function convertRuleToProfileRule(sourcePath, targetPath, profile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert all Cursor rules to profile rules for a specific profile
|
* Options for converting rules to profile rules
|
||||||
|
* @typedef {Object} ConvertRulesOptions
|
||||||
|
* @property {'solo' | 'team'} [mode] - Operating mode to filter rules
|
||||||
*/
|
*/
|
||||||
export function convertAllRulesToProfileRules(projectRoot, profile) {
|
|
||||||
|
/**
|
||||||
|
* Remove all TaskMaster rule files from a profile (used when switching modes)
|
||||||
|
* This removes files from ALL modes to ensure a clean slate.
|
||||||
|
* @param {string} projectRoot - The project root directory
|
||||||
|
* @param {Object} profile - The profile configuration
|
||||||
|
*/
|
||||||
|
function removeTaskMasterRuleFiles(projectRoot, profile) {
|
||||||
const targetDir = path.join(projectRoot, profile.rulesDir);
|
const targetDir = path.join(projectRoot, profile.rulesDir);
|
||||||
|
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
return; // Nothing to remove
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all TaskMaster rule files (from all modes)
|
||||||
|
const allRuleFiles = Object.values(profile.fileMap);
|
||||||
|
|
||||||
|
for (const ruleFile of allRuleFiles) {
|
||||||
|
const filePath = path.join(targetDir, ruleFile);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
log('debug', `[Rule Transformer] Removed rule file: ${ruleFile}`);
|
||||||
|
} catch (error) {
|
||||||
|
log(
|
||||||
|
'warn',
|
||||||
|
`[Rule Transformer] Failed to remove rule file ${ruleFile}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert all Cursor rules to profile rules for a specific profile
|
||||||
|
* @param {string} projectRoot - The project root directory
|
||||||
|
* @param {Object} profile - The profile configuration
|
||||||
|
* @param {ConvertRulesOptions} [options] - Options including mode filtering
|
||||||
|
*/
|
||||||
|
export function convertAllRulesToProfileRules(projectRoot, profile, options) {
|
||||||
|
const targetDir = path.join(projectRoot, profile.rulesDir);
|
||||||
|
const mode = options?.mode;
|
||||||
|
|
||||||
let success = 0;
|
let success = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
||||||
|
// 0. When mode is specified, first remove ALL existing TaskMaster rules
|
||||||
|
// to ensure clean slate (prevents orphaned rules when switching modes)
|
||||||
|
if (mode) {
|
||||||
|
removeTaskMasterRuleFiles(projectRoot, profile);
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`[Rule Transformer] Cleaned up existing rules before adding ${mode} mode rules`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Call onAddRulesProfile first (for pre-processing like copying assets)
|
// 1. Call onAddRulesProfile first (for pre-processing like copying assets)
|
||||||
if (typeof profile.onAddRulesProfile === 'function') {
|
if (typeof profile.onAddRulesProfile === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -225,7 +280,11 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Handle fileMap-based rule conversion (if any)
|
// 2. Handle fileMap-based rule conversion (if any)
|
||||||
const sourceFiles = Object.keys(profile.fileMap);
|
// Filter by mode if specified
|
||||||
|
const filteredFileMap = mode
|
||||||
|
? filterRulesByMode(profile.fileMap, mode)
|
||||||
|
: profile.fileMap;
|
||||||
|
const sourceFiles = Object.keys(filteredFileMap);
|
||||||
if (sourceFiles.length > 0) {
|
if (sourceFiles.length > 0) {
|
||||||
// Only create rules directory if we have files to copy
|
// Only create rules directory if we have files to copy
|
||||||
if (!fs.existsSync(targetDir)) {
|
if (!fs.existsSync(targetDir)) {
|
||||||
@@ -246,7 +305,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetFilename = profile.fileMap[sourceFile];
|
const targetFilename = filteredFileMap[sourceFile];
|
||||||
const targetPath = path.join(targetDir, targetFilename);
|
const targetPath = path.join(targetDir, targetFilename);
|
||||||
|
|
||||||
// Ensure target subdirectory exists (for rules like taskmaster/dev_workflow.md)
|
// Ensure target subdirectory exists (for rules like taskmaster/dev_workflow.md)
|
||||||
@@ -318,6 +377,35 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Add slash commands (if profile supports them)
|
||||||
|
if (profile.slashCommands) {
|
||||||
|
try {
|
||||||
|
// Pass mode option for filtering commands by operating mode
|
||||||
|
const slashCommandOptions = mode ? { mode } : undefined;
|
||||||
|
const result = profile.slashCommands.profile.addSlashCommands(
|
||||||
|
projectRoot,
|
||||||
|
profile.slashCommands.commands,
|
||||||
|
slashCommandOptions
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`[Rule Transformer] Created ${result.count} slash commands in ${result.directory}${mode ? ` (mode: ${mode})` : ''}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
'error',
|
||||||
|
`[Rule Transformer] Failed to add slash commands for ${profile.profileName}: ${result.error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(
|
||||||
|
'error',
|
||||||
|
`[Rule Transformer] Slash commands failed for ${profile.profileName}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we return at least 1 success for profiles that only use lifecycle functions
|
// Ensure we return at least 1 success for profiles that only use lifecycle functions
|
||||||
return { success: Math.max(success, 1), failed };
|
return { success: Math.max(success, 1), failed };
|
||||||
}
|
}
|
||||||
@@ -493,7 +581,33 @@ export function removeProfileRules(projectRoot, profile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check if we should remove the entire profile directory
|
// 4. Remove slash commands (if profile supports them)
|
||||||
|
if (profile.slashCommands) {
|
||||||
|
try {
|
||||||
|
const slashResult = profile.slashCommands.profile.removeSlashCommands(
|
||||||
|
projectRoot,
|
||||||
|
profile.slashCommands.commands
|
||||||
|
);
|
||||||
|
if (slashResult.success && slashResult.count > 0) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`[Rule Transformer] Removed ${slashResult.count} slash commands for ${profile.profileName}`
|
||||||
|
);
|
||||||
|
} else if (!slashResult.success) {
|
||||||
|
log(
|
||||||
|
'error',
|
||||||
|
`[Rule Transformer] Failed to remove slash commands for ${profile.profileName}: ${slashResult.error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(
|
||||||
|
'error',
|
||||||
|
`[Rule Transformer] Slash command cleanup failed for ${profile.profileName}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check if we should remove the entire profile directory
|
||||||
if (fs.existsSync(profileDir)) {
|
if (fs.existsSync(profileDir)) {
|
||||||
const remainingContents = fs.readdirSync(profileDir);
|
const remainingContents = fs.readdirSync(profileDir);
|
||||||
if (remainingContents.length === 0 && profile.profileDir !== '.') {
|
if (remainingContents.length === 0 && profile.profileDir !== '.') {
|
||||||
|
|||||||
@@ -55,32 +55,28 @@ describe('Cursor Profile Initialization Functionality', () => {
|
|||||||
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
|
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cursor.js has lifecycle functions for command copying', () => {
|
test('cursor.js uses factory pattern from base-profile', () => {
|
||||||
// Check that the source file contains our new lifecycle functions
|
// The new architecture uses createProfile from base-profile.js
|
||||||
expect(cursorProfileContent).toContain('function onAddRulesProfile');
|
// which auto-generates lifecycle hooks via @tm/profiles package
|
||||||
expect(cursorProfileContent).toContain('function onRemoveRulesProfile');
|
expect(cursorProfileContent).toContain('import { createProfile }');
|
||||||
expect(cursorProfileContent).toContain('copyRecursiveSync');
|
expect(cursorProfileContent).toContain('createProfile(');
|
||||||
expect(cursorProfileContent).toContain('removeDirectoryRecursive');
|
|
||||||
|
// Verify supportsRulesSubdirectories is enabled for cursor (it uses taskmaster/ subdirectory)
|
||||||
|
expect(cursorProfileContent).toContain('supportsRulesSubdirectories: true');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cursor.js copies commands from claude/commands to .cursor/commands', () => {
|
test('cursor profile has declarative slashCommands property via @tm/profiles', () => {
|
||||||
// Check that the onAddRulesProfile function copies from the correct source
|
// The new architecture uses a declarative slashCommands property
|
||||||
expect(cursorProfileContent).toContain(
|
// instead of lifecycle hooks - rule-transformer handles execution
|
||||||
"path.join(assetsDir, 'claude', 'commands')"
|
// slashCommands will be defined if @tm/profiles returns a profile that supports commands
|
||||||
);
|
// In test environment, this may be undefined if @tm/profiles isn't fully loaded
|
||||||
// Destination path is built via a resolver to handle both project root and rules dir
|
if (cursorProfile.slashCommands) {
|
||||||
expect(cursorProfileContent).toContain('resolveCursorProfileDir(');
|
expect(cursorProfile.slashCommands.profile).toBeDefined();
|
||||||
expect(cursorProfileContent).toMatch(
|
expect(cursorProfile.slashCommands.commands).toBeDefined();
|
||||||
/path\.join\(\s*profileDir\s*,\s*['"]commands['"]\s*\)/
|
}
|
||||||
);
|
// The cursor profile should NOT have explicit lifecycle hooks
|
||||||
expect(cursorProfileContent).toContain(
|
// (it uses the declarative slashCommands approach)
|
||||||
'copyRecursiveSync(commandsSourceDir, commandsDestDir)'
|
expect(cursorProfileContent).not.toContain('onAdd:');
|
||||||
);
|
expect(cursorProfileContent).not.toContain('onRemove:');
|
||||||
|
|
||||||
// Check that lifecycle functions are properly registered with the profile
|
|
||||||
expect(cursorProfile.onAddRulesProfile).toBeDefined();
|
|
||||||
expect(cursorProfile.onRemoveRulesProfile).toBeDefined();
|
|
||||||
expect(typeof cursorProfile.onAddRulesProfile).toBe('function');
|
|
||||||
expect(typeof cursorProfile.onRemoveRulesProfile).toBe('function');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import * as profilesModule from '../../../src/profiles/index.js';
|
|||||||
* Integration tests for hamster rules distribution across all profiles.
|
* Integration tests for hamster rules distribution across all profiles.
|
||||||
*
|
*
|
||||||
* These tests verify that hamster.mdc is correctly distributed
|
* These tests verify that hamster.mdc is correctly distributed
|
||||||
* to all profiles that include default rules when running `rules add`.
|
* to all profiles that include default rules when running `rules add --mode=team`.
|
||||||
|
* Note: hamster.mdc is team-mode only (for Hamster API integration).
|
||||||
*/
|
*/
|
||||||
describe('Hamster Rules Distribution', () => {
|
describe('Hamster Rules Distribution', () => {
|
||||||
const CLI_PATH = path.join(process.cwd(), 'dist', 'task-master.js');
|
const CLI_PATH = path.join(process.cwd(), 'dist', 'task-master.js');
|
||||||
@@ -69,18 +70,18 @@ describe('Hamster Rules Distribution', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Rules add command distributes hamster file', () => {
|
describe('Rules add command distributes hamster file in team mode', () => {
|
||||||
// Test each profile that should receive hamster rules
|
// Test each profile that should receive hamster rules when --mode=team
|
||||||
PROFILES_WITH_DEFAULT_RULES.forEach((profile) => {
|
PROFILES_WITH_DEFAULT_RULES.forEach((profile) => {
|
||||||
test(`${profile} profile receives hamster rules via 'rules add'`, () => {
|
test(`${profile} profile receives hamster rules via 'rules add --mode=team'`, () => {
|
||||||
// Create a unique temp directory for this test
|
// Create a unique temp directory for this test
|
||||||
const tempDir = fs.mkdtempSync(
|
const tempDir = fs.mkdtempSync(
|
||||||
path.join(os.tmpdir(), `tm-hamster-test-${profile}-`)
|
path.join(os.tmpdir(), `tm-hamster-test-${profile}-`)
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Run the rules add command
|
// Run the rules add command with team mode (hamster.mdc is team-only)
|
||||||
execSync(`node ${CLI_PATH} rules add ${profile}`, {
|
execSync(`node ${CLI_PATH} rules add ${profile} --mode=team`, {
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
env: { ...process.env, TASKMASTER_LOG_LEVEL: 'error' }
|
env: { ...process.env, TASKMASTER_LOG_LEVEL: 'error' }
|
||||||
@@ -108,6 +109,67 @@ describe('Hamster Rules Distribution', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Solo mode excludes hamster file', () => {
|
||||||
|
// Test that hamster.mdc is NOT distributed in solo mode
|
||||||
|
PROFILES_WITH_DEFAULT_RULES.forEach((profile) => {
|
||||||
|
test(`${profile} profile does NOT receive hamster rules via 'rules add --mode=solo'`, () => {
|
||||||
|
const tempDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), `tm-hamster-solo-test-${profile}-`)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Run in solo mode - hamster should NOT be distributed
|
||||||
|
execSync(`node ${CLI_PATH} rules add ${profile} --mode=solo`, {
|
||||||
|
cwd: tempDir,
|
||||||
|
stdio: 'pipe',
|
||||||
|
env: { ...process.env, TASKMASTER_LOG_LEVEL: 'error' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedPath = getExpectedHamsterPath(profile, tempDir);
|
||||||
|
|
||||||
|
// Strictly enforce that all profiles with default rules must have hamster mapping
|
||||||
|
expect(expectedPath).not.toBeNull();
|
||||||
|
|
||||||
|
// Verify hamster file does NOT exist in solo mode
|
||||||
|
expect(fs.existsSync(expectedPath)).toBe(false);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default mode behavior (no --mode flag)', () => {
|
||||||
|
// When no mode is specified, default is 'solo' (no auth/config present)
|
||||||
|
// This means hamster.mdc should NOT be distributed by default
|
||||||
|
PROFILES_WITH_DEFAULT_RULES.forEach((profile) => {
|
||||||
|
test(`${profile} profile defaults to solo mode (no hamster rules without --mode flag)`, () => {
|
||||||
|
const tempDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), `tm-hamster-default-test-${profile}-`)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Run without mode flag - should default to solo
|
||||||
|
execSync(`node ${CLI_PATH} rules add ${profile}`, {
|
||||||
|
cwd: tempDir,
|
||||||
|
stdio: 'pipe',
|
||||||
|
env: { ...process.env, TASKMASTER_LOG_LEVEL: 'error' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedPath = getExpectedHamsterPath(profile, tempDir);
|
||||||
|
|
||||||
|
// Strictly enforce that all profiles with default rules must have hamster mapping
|
||||||
|
expect(expectedPath).not.toBeNull();
|
||||||
|
|
||||||
|
// Default is solo mode, so hamster file should NOT exist
|
||||||
|
expect(fs.existsSync(expectedPath)).toBe(false);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Profiles without default rules', () => {
|
describe('Profiles without default rules', () => {
|
||||||
// These profiles use different mechanisms (CLAUDE.md, AGENTS.md, etc.)
|
// These profiles use different mechanisms (CLAUDE.md, AGENTS.md, etc.)
|
||||||
// and don't include the defaultFileMap rules
|
// and don't include the defaultFileMap rules
|
||||||
|
|||||||
394
tests/unit/profiles/base-profile-slash-commands.test.js
Normal file
394
tests/unit/profiles/base-profile-slash-commands.test.js
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
// Mock console methods to avoid chalk issues
|
||||||
|
const mockConsole = {
|
||||||
|
log: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
clear: jest.fn()
|
||||||
|
};
|
||||||
|
const originalConsole = global.console;
|
||||||
|
global.console = mockConsole;
|
||||||
|
|
||||||
|
// Mock the utils logger
|
||||||
|
const mockLog = jest.fn();
|
||||||
|
await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
||||||
|
default: undefined,
|
||||||
|
log: mockLog,
|
||||||
|
isSilentMode: () => false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock @tm/profiles module
|
||||||
|
const mockGetProfile = jest.fn();
|
||||||
|
const mockAllCommands = [
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
metadata: { name: 'help', description: 'Show help' },
|
||||||
|
content: 'Help content'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'dynamic',
|
||||||
|
metadata: {
|
||||||
|
name: 'show-task',
|
||||||
|
description: 'Show task',
|
||||||
|
argumentHint: '[task-id]'
|
||||||
|
},
|
||||||
|
content: 'Show task $ARGUMENTS'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await jest.unstable_mockModule('@tm/profiles', () => ({
|
||||||
|
getProfile: mockGetProfile,
|
||||||
|
allCommands: mockAllCommands
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import createProfile after mocking
|
||||||
|
const { createProfile } = await import('../../../src/profiles/base-profile.js');
|
||||||
|
|
||||||
|
describe('Base Profile - Declarative Slash Commands', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
global.console = originalConsole;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('slashCommands config when profile supports commands', () => {
|
||||||
|
it('should include slashCommands config when profile supports slash commands', () => {
|
||||||
|
// Arrange - Mock a profile that supports commands
|
||||||
|
const mockSlashProfile = {
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
};
|
||||||
|
mockGetProfile.mockReturnValue(mockSlashProfile);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - Profile should have slashCommands config
|
||||||
|
expect(profile.slashCommands).toBeDefined();
|
||||||
|
expect(profile.slashCommands.profile).toBe(mockSlashProfile);
|
||||||
|
expect(profile.slashCommands.commands).toBe(mockAllCommands);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include profile methods in slashCommands config', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockAddSlashCommands = jest.fn();
|
||||||
|
const mockRemoveSlashCommands = jest.fn();
|
||||||
|
mockGetProfile.mockReturnValue({
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: mockAddSlashCommands,
|
||||||
|
removeSlashCommands: mockRemoveSlashCommands
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - Methods should be accessible
|
||||||
|
expect(profile.slashCommands.profile.addSlashCommands).toBe(
|
||||||
|
mockAddSlashCommands
|
||||||
|
);
|
||||||
|
expect(profile.slashCommands.profile.removeSlashCommands).toBe(
|
||||||
|
mockRemoveSlashCommands
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('No slashCommands config when profile does not support commands', () => {
|
||||||
|
it('should not include slashCommands when profile does not support slash commands', () => {
|
||||||
|
// Arrange - Mock a profile that does NOT support commands
|
||||||
|
mockGetProfile.mockReturnValue({
|
||||||
|
supportsCommands: false,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'amp',
|
||||||
|
displayName: 'Amp',
|
||||||
|
url: 'amp.rs',
|
||||||
|
docsUrl: 'docs.amp.rs'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - Profile should NOT have slashCommands config
|
||||||
|
expect(profile.slashCommands).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include slashCommands when getProfile returns null', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockReturnValue(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'unknown',
|
||||||
|
displayName: 'Unknown Editor',
|
||||||
|
url: 'example.com',
|
||||||
|
docsUrl: 'docs.example.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include slashCommands when getProfile returns undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'another-unknown',
|
||||||
|
displayName: 'Another Unknown',
|
||||||
|
url: 'example.org',
|
||||||
|
docsUrl: 'docs.example.org'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User hooks remain independent of slashCommands', () => {
|
||||||
|
it('should keep user onAdd hook separate from slashCommands', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockReturnValue({
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
const userOnAdd = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com',
|
||||||
|
onAdd: userOnAdd
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - Both should exist independently
|
||||||
|
expect(profile.slashCommands).toBeDefined();
|
||||||
|
expect(profile.onAddRulesProfile).toBe(userOnAdd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep user onRemove hook separate from slashCommands', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockReturnValue({
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
const userOnRemove = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com',
|
||||||
|
onRemove: userOnRemove
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - Both should exist independently
|
||||||
|
expect(profile.slashCommands).toBeDefined();
|
||||||
|
expect(profile.onRemoveRulesProfile).toBe(userOnRemove);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve user hooks when profile does not support commands', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockReturnValue({
|
||||||
|
supportsCommands: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const userOnAdd = jest.fn();
|
||||||
|
const userOnRemove = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'amp',
|
||||||
|
displayName: 'Amp',
|
||||||
|
url: 'amp.rs',
|
||||||
|
docsUrl: 'docs.amp.rs',
|
||||||
|
onAdd: userOnAdd,
|
||||||
|
onRemove: userOnRemove
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeNull();
|
||||||
|
expect(profile.onAddRulesProfile).toBe(userOnAdd);
|
||||||
|
expect(profile.onRemoveRulesProfile).toBe(userOnRemove);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handling for getProfile', () => {
|
||||||
|
it('should handle getProfile throwing an error gracefully', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockImplementation(() => {
|
||||||
|
throw new Error('Module not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act - Should not throw
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeNull();
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'debug',
|
||||||
|
'[Cursor] Slash command profile lookup failed: Module not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve user hooks when getProfile throws', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockImplementation(() => {
|
||||||
|
throw new Error('@tm/profiles not installed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const userOnAdd = jest.fn();
|
||||||
|
const userOnRemove = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com',
|
||||||
|
onAdd: userOnAdd,
|
||||||
|
onRemove: userOnRemove
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeNull();
|
||||||
|
expect(profile.onAddRulesProfile).toBe(userOnAdd);
|
||||||
|
expect(profile.onRemoveRulesProfile).toBe(userOnRemove);
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'debug',
|
||||||
|
'[Cursor] Slash command profile lookup failed: @tm/profiles not installed'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Profile metadata preserved', () => {
|
||||||
|
it('should preserve all profile metadata alongside slashCommands', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockReturnValue({
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor IDE',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com',
|
||||||
|
profileDir: '.cursor',
|
||||||
|
rulesDir: '.cursor/rules',
|
||||||
|
mcpConfig: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert - All metadata should be present
|
||||||
|
expect(profile.profileName).toBe('cursor');
|
||||||
|
expect(profile.displayName).toBe('Cursor IDE');
|
||||||
|
expect(profile.profileDir).toBe('.cursor');
|
||||||
|
expect(profile.rulesDir).toBe('.cursor/rules');
|
||||||
|
expect(profile.mcpConfig).toBe(true);
|
||||||
|
expect(profile.slashCommands).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use displayName in error logs', () => {
|
||||||
|
// Arrange
|
||||||
|
mockGetProfile.mockImplementation(() => {
|
||||||
|
throw new Error('Test error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
createProfile({
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor IDE Pro',
|
||||||
|
url: 'cursor.com',
|
||||||
|
docsUrl: 'docs.cursor.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'debug',
|
||||||
|
'[Cursor IDE Pro] Slash command profile lookup failed: Test error'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration with different profile types', () => {
|
||||||
|
it('should work with Roo profile configuration', () => {
|
||||||
|
// Arrange
|
||||||
|
const rooSlashProfile = {
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
};
|
||||||
|
mockGetProfile.mockReturnValue(rooSlashProfile);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'roo',
|
||||||
|
displayName: 'Roo Code',
|
||||||
|
url: 'roo.codes',
|
||||||
|
docsUrl: 'docs.roo.codes',
|
||||||
|
profileDir: '.roo',
|
||||||
|
rulesDir: '.roo/rules'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeDefined();
|
||||||
|
expect(profile.slashCommands.profile).toBe(rooSlashProfile);
|
||||||
|
expect(profile.rulesDir).toBe('.roo/rules');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with OpenCode profile configuration', () => {
|
||||||
|
// Arrange
|
||||||
|
const opencodeSlashProfile = {
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: jest.fn(),
|
||||||
|
removeSlashCommands: jest.fn()
|
||||||
|
};
|
||||||
|
mockGetProfile.mockReturnValue(opencodeSlashProfile);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const profile = createProfile({
|
||||||
|
name: 'opencode',
|
||||||
|
displayName: 'OpenCode',
|
||||||
|
url: 'opencode.app',
|
||||||
|
docsUrl: 'docs.opencode.app',
|
||||||
|
profileDir: '.opencode',
|
||||||
|
rulesDir: '.opencode/prompts'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(profile.slashCommands).toBeDefined();
|
||||||
|
expect(profile.slashCommands.profile).toBe(opencodeSlashProfile);
|
||||||
|
expect(profile.rulesDir).toBe('.opencode/prompts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,10 +27,26 @@ await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
|||||||
isSilentMode: () => false
|
isSilentMode: () => false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock @tm/profiles to control slash command behavior in tests
|
||||||
|
const mockAddSlashCommands = jest.fn();
|
||||||
|
const mockRemoveSlashCommands = jest.fn();
|
||||||
|
const mockProfile = {
|
||||||
|
name: 'cursor',
|
||||||
|
displayName: 'Cursor',
|
||||||
|
commandsDir: '.cursor/commands',
|
||||||
|
supportsCommands: true,
|
||||||
|
addSlashCommands: mockAddSlashCommands,
|
||||||
|
removeSlashCommands: mockRemoveSlashCommands
|
||||||
|
};
|
||||||
|
|
||||||
|
await jest.unstable_mockModule('@tm/profiles', () => ({
|
||||||
|
getProfile: jest.fn(() => mockProfile),
|
||||||
|
allCommands: [{ metadata: { name: 'help' }, content: 'Help content' }],
|
||||||
|
resolveProjectRoot: jest.fn((targetDir) => targetDir)
|
||||||
|
}));
|
||||||
|
|
||||||
// Import the cursor profile after mocking
|
// Import the cursor profile after mocking
|
||||||
const { cursorProfile, onAddRulesProfile, onRemoveRulesProfile } = await import(
|
const { cursorProfile } = await import('../../../src/profiles/cursor.js');
|
||||||
'../../../src/profiles/cursor.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Cursor Integration', () => {
|
describe('Cursor Integration', () => {
|
||||||
let tempDir;
|
let tempDir;
|
||||||
@@ -95,126 +111,137 @@ describe('Cursor Integration', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cursor profile has lifecycle functions for command copying', () => {
|
test('cursor profile has declarative slash commands configuration', () => {
|
||||||
// Assert that the profile exports the lifecycle functions
|
// The new architecture uses declarative slashCommands property
|
||||||
expect(typeof onAddRulesProfile).toBe('function');
|
// instead of lifecycle hooks - rule-transformer handles execution
|
||||||
expect(typeof onRemoveRulesProfile).toBe('function');
|
expect(cursorProfile.slashCommands).toBeDefined();
|
||||||
expect(cursorProfile.onAddRulesProfile).toBe(onAddRulesProfile);
|
expect(cursorProfile.slashCommands.profile).toBeDefined();
|
||||||
expect(cursorProfile.onRemoveRulesProfile).toBe(onRemoveRulesProfile);
|
expect(cursorProfile.slashCommands.commands).toBeDefined();
|
||||||
|
expect(cursorProfile.slashCommands.profile.supportsCommands).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('command copying lifecycle', () => {
|
test('cursor profile has correct basic configuration', () => {
|
||||||
let mockAssetsDir;
|
expect(cursorProfile.profileName).toBe('cursor');
|
||||||
|
expect(cursorProfile.displayName).toBe('Cursor');
|
||||||
|
expect(cursorProfile.profileDir).toBe('.cursor');
|
||||||
|
expect(cursorProfile.rulesDir).toBe('.cursor/rules');
|
||||||
|
expect(cursorProfile.supportsRulesSubdirectories).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('slash commands via slashCommands property', () => {
|
||||||
let mockTargetDir;
|
let mockTargetDir;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockAssetsDir = path.join(tempDir, 'assets');
|
|
||||||
mockTargetDir = path.join(tempDir, 'target');
|
mockTargetDir = path.join(tempDir, 'target');
|
||||||
|
|
||||||
// Reset all mocks
|
// Reset all mocks
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
mockAddSlashCommands.mockReset();
|
||||||
|
mockRemoveSlashCommands.mockReset();
|
||||||
|
|
||||||
// Mock fs methods for the lifecycle functions
|
// Default mock return values
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
mockAddSlashCommands.mockReturnValue({
|
||||||
const pathStr = filePath.toString();
|
success: true,
|
||||||
if (pathStr.includes('claude/commands')) {
|
count: 10,
|
||||||
return true; // Mock that source commands exist
|
directory: path.join(mockTargetDir, '.cursor', 'commands', 'tm'),
|
||||||
}
|
files: ['help.md', 'next-task.md']
|
||||||
return false;
|
});
|
||||||
|
mockRemoveSlashCommands.mockReturnValue({
|
||||||
|
success: true,
|
||||||
|
count: 10,
|
||||||
|
directory: path.join(mockTargetDir, '.cursor', 'commands', 'tm'),
|
||||||
|
files: ['help.md', 'next-task.md']
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
|
||||||
jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['tm']);
|
|
||||||
jest
|
|
||||||
.spyOn(fs, 'statSync')
|
|
||||||
.mockImplementation(() => ({ isDirectory: () => true }));
|
|
||||||
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
|
|
||||||
jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onAddRulesProfile copies commands from assets to .cursor/commands', () => {
|
test('slashCommands.profile can add slash commands', () => {
|
||||||
// Detect if cpSync exists and set up appropriate spy
|
// The rule-transformer uses slashCommands.profile.addSlashCommands
|
||||||
if (fs.cpSync) {
|
const { profile, commands } = cursorProfile.slashCommands;
|
||||||
const cpSpy = jest.spyOn(fs, 'cpSync').mockImplementation(() => {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
onAddRulesProfile(mockTargetDir, mockAssetsDir);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockAssetsDir, 'claude', 'commands')
|
|
||||||
);
|
|
||||||
expect(cpSpy).toHaveBeenCalledWith(
|
|
||||||
path.join(mockAssetsDir, 'claude', 'commands'),
|
|
||||||
path.join(mockTargetDir, '.cursor', 'commands'),
|
|
||||||
expect.objectContaining({ recursive: true, force: true })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Act
|
|
||||||
onAddRulesProfile(mockTargetDir, mockAssetsDir);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockAssetsDir, 'claude', 'commands')
|
|
||||||
);
|
|
||||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockTargetDir, '.cursor', 'commands'),
|
|
||||||
{ recursive: true }
|
|
||||||
);
|
|
||||||
expect(fs.copyFileSync).toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onAddRulesProfile handles missing source directory gracefully', () => {
|
|
||||||
// Arrange - mock source directory not existing
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
onAddRulesProfile(mockTargetDir, mockAssetsDir);
|
profile.addSlashCommands(mockTargetDir, commands);
|
||||||
|
|
||||||
// Assert - should not attempt to copy anything
|
// Assert - mock was called
|
||||||
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
expect(mockAddSlashCommands).toHaveBeenCalledWith(
|
||||||
expect(fs.copyFileSync).not.toHaveBeenCalled();
|
mockTargetDir,
|
||||||
});
|
commands
|
||||||
|
|
||||||
test('onRemoveRulesProfile removes .cursor/commands directory', () => {
|
|
||||||
// Arrange - mock directory exists
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
onRemoveRulesProfile(mockTargetDir);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(fs.rmSync).toHaveBeenCalledWith(
|
|
||||||
path.join(mockTargetDir, '.cursor', 'commands'),
|
|
||||||
{ recursive: true, force: true }
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onRemoveRulesProfile handles missing directory gracefully', () => {
|
test('slashCommands.profile handles add errors gracefully', () => {
|
||||||
// Arrange - mock directory doesn't exist
|
// Arrange - mock addSlashCommands failure
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
|
mockAddSlashCommands.mockReturnValue({
|
||||||
|
success: false,
|
||||||
// Act
|
count: 0,
|
||||||
onRemoveRulesProfile(mockTargetDir);
|
directory: '',
|
||||||
|
files: [],
|
||||||
// Assert - should still return true but not attempt removal
|
error: 'Permission denied'
|
||||||
expect(fs.rmSync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('onRemoveRulesProfile handles removal errors gracefully', () => {
|
|
||||||
// Arrange - mock directory exists but removal fails
|
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
|
|
||||||
jest.spyOn(fs, 'rmSync').mockImplementation(() => {
|
|
||||||
throw new Error('Permission denied');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act & Assert - should not throw
|
const { profile, commands } = cursorProfile.slashCommands;
|
||||||
expect(() => onRemoveRulesProfile(mockTargetDir)).not.toThrow();
|
|
||||||
|
// Act - should not throw
|
||||||
|
const result = profile.addSlashCommands(mockTargetDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Permission denied');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('slashCommands.profile can remove slash commands', () => {
|
||||||
|
const { profile, commands } = cursorProfile.slashCommands;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
profile.removeSlashCommands(mockTargetDir, commands);
|
||||||
|
|
||||||
|
// Assert - mock was called
|
||||||
|
expect(mockRemoveSlashCommands).toHaveBeenCalledWith(
|
||||||
|
mockTargetDir,
|
||||||
|
commands
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('slashCommands.profile handles remove with missing directory gracefully', () => {
|
||||||
|
// Arrange - mock removeSlashCommands returns success with 0 count
|
||||||
|
mockRemoveSlashCommands.mockReturnValue({
|
||||||
|
success: true,
|
||||||
|
count: 0,
|
||||||
|
directory: path.join(mockTargetDir, '.cursor', 'commands', 'tm'),
|
||||||
|
files: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { profile, commands } = cursorProfile.slashCommands;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(mockTargetDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('slashCommands.profile handles remove errors gracefully', () => {
|
||||||
|
// Arrange - mock removeSlashCommands failure
|
||||||
|
mockRemoveSlashCommands.mockReturnValue({
|
||||||
|
success: false,
|
||||||
|
count: 0,
|
||||||
|
directory: '',
|
||||||
|
files: [],
|
||||||
|
error: 'Permission denied'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { profile, commands } = cursorProfile.slashCommands;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = profile.removeSlashCommands(mockTargetDir, commands);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Permission denied');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -433,7 +433,13 @@ describe('MCP Configuration Validation', () => {
|
|||||||
];
|
];
|
||||||
const profilesWithLifecycle = ['amp', 'claude'];
|
const profilesWithLifecycle = ['amp', 'claude'];
|
||||||
const profilesWithPostConvertLifecycle = ['opencode'];
|
const profilesWithPostConvertLifecycle = ['opencode'];
|
||||||
const profilesWithoutLifecycle = ['codex'];
|
// Profiles that use declarative slashCommands (no auto-generated lifecycle hooks)
|
||||||
|
const profilesWithDeclarativeSlashCommands = [
|
||||||
|
'codex',
|
||||||
|
'cursor',
|
||||||
|
'gemini',
|
||||||
|
'roo'
|
||||||
|
];
|
||||||
|
|
||||||
test.each(allProfiles)(
|
test.each(allProfiles)(
|
||||||
'should have file mappings for %s profile',
|
'should have file mappings for %s profile',
|
||||||
@@ -466,28 +472,30 @@ describe('MCP Configuration Validation', () => {
|
|||||||
(profileName) => {
|
(profileName) => {
|
||||||
const profile = getRulesProfile(profileName);
|
const profile = getRulesProfile(profileName);
|
||||||
expect(profile).toBeDefined();
|
expect(profile).toBeDefined();
|
||||||
// OpenCode profile has fileMap and post-convert lifecycle functions
|
// OpenCode profile has fileMap and explicit lifecycle functions
|
||||||
|
// Note: OpenCode has onRemove and onPostConvert, but NOT onAdd
|
||||||
expect(profile.fileMap).toBeDefined();
|
expect(profile.fileMap).toBeDefined();
|
||||||
expect(typeof profile.fileMap).toBe('object');
|
expect(typeof profile.fileMap).toBe('object');
|
||||||
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
||||||
expect(profile.onAddRulesProfile).toBeUndefined(); // OpenCode doesn't have onAdd
|
// OpenCode has onRemove and onPostConvert but NOT onAdd
|
||||||
|
expect(profile.onAddRulesProfile).toBeUndefined();
|
||||||
expect(typeof profile.onRemoveRulesProfile).toBe('function');
|
expect(typeof profile.onRemoveRulesProfile).toBe('function');
|
||||||
expect(typeof profile.onPostConvertRulesProfile).toBe('function');
|
expect(typeof profile.onPostConvertRulesProfile).toBe('function');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
test.each(profilesWithoutLifecycle)(
|
test.each(profilesWithDeclarativeSlashCommands)(
|
||||||
'should have file mappings without lifecycle functions for %s profile',
|
'should have file mappings with declarative slashCommands for %s profile',
|
||||||
(profileName) => {
|
(profileName) => {
|
||||||
const profile = getRulesProfile(profileName);
|
const profile = getRulesProfile(profileName);
|
||||||
expect(profile).toBeDefined();
|
expect(profile).toBeDefined();
|
||||||
// Codex profile has fileMap but no lifecycle functions (simplified)
|
// These profiles use the declarative slashCommands property
|
||||||
|
// instead of auto-generated lifecycle hooks
|
||||||
expect(profile.fileMap).toBeDefined();
|
expect(profile.fileMap).toBeDefined();
|
||||||
expect(typeof profile.fileMap).toBe('object');
|
expect(typeof profile.fileMap).toBe('object');
|
||||||
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
||||||
expect(profile.onAddRulesProfile).toBeUndefined();
|
// No explicit lifecycle hooks - uses declarative slashCommands
|
||||||
expect(profile.onRemoveRulesProfile).toBeUndefined();
|
// (slashCommands may be null if @tm/profiles lookup fails in test env)
|
||||||
expect(profile.onPostConvertRulesProfile).toBeUndefined();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user