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:
@@ -67,7 +67,7 @@ listTasks(tasksPath, statusFilter, reportPath, withSubtasks, outputFormat, conte
|
||||
const tmCore = createTaskMasterCore(projectPath, {
|
||||
storage: {
|
||||
type: 'api', // or 'file'
|
||||
apiEndpoint: 'https://hamster.ai/api',
|
||||
apiEndpoint: 'https://tryhamster.com',
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user