feat: adds ability to add or remove subtasks. Can also turn subtasks into standalone features. Also refactors the task-master.js by deleting 200+ lines of duplicate code. Instead properly imports the commands from commands.js which is the single source of truth for command definitions.
This commit is contained in:
@@ -160,4 +160,71 @@ alwaysApply: false
|
||||
import { addDependency } from './dependency-manager.js';
|
||||
```
|
||||
|
||||
## Subtask Management Commands
|
||||
|
||||
- **Add Subtask Command Structure**:
|
||||
```javascript
|
||||
// ✅ DO: Follow this structure for adding subtasks
|
||||
programInstance
|
||||
.command('add-subtask')
|
||||
.description('Add a new subtask to a parent task or convert an existing task to a subtask')
|
||||
.option('-f, --file <path>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-p, --parent <id>', 'ID of the parent task (required)')
|
||||
.option('-e, --existing <id>', 'ID of an existing task to convert to a subtask')
|
||||
.option('-t, --title <title>', 'Title for the new subtask (when not converting)')
|
||||
.option('-d, --description <description>', 'Description for the new subtask (when not converting)')
|
||||
.option('--details <details>', 'Implementation details for the new subtask (when not converting)')
|
||||
.option('--dependencies <ids>', 'Comma-separated list of subtask IDs this subtask depends on')
|
||||
.option('--status <status>', 'Initial status for the subtask', 'pending')
|
||||
.action(async (options) => {
|
||||
// Validate required parameters
|
||||
if (!options.parent) {
|
||||
console.error(chalk.red('Error: --parent parameter is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate that either existing task ID or title is provided
|
||||
if (!options.existing && !options.title) {
|
||||
console.error(chalk.red('Error: Either --existing or --title must be provided'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Implementation
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- **Remove Subtask Command Structure**:
|
||||
```javascript
|
||||
// ✅ DO: Follow this structure for removing subtasks
|
||||
programInstance
|
||||
.command('remove-subtask')
|
||||
.description('Remove a subtask from its parent task, optionally converting it to a standalone task')
|
||||
.option('-f, --file <path>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <id>', 'ID of the subtask to remove in format "parentId.subtaskId" (required)')
|
||||
.option('-c, --convert', 'Convert the subtask to a standalone task')
|
||||
.action(async (options) => {
|
||||
// Validate required parameters
|
||||
if (!options.id) {
|
||||
console.error(chalk.red('Error: --id parameter is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate subtask ID format
|
||||
if (!options.id.includes('.')) {
|
||||
console.error(chalk.red('Error: Subtask ID must be in format "parentId.subtaskId"'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Implementation
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Refer to [`commands.js`](mdc:scripts/modules/commands.js) for implementation examples and [`new_features.mdc`](mdc:.cursor/rules/new_features.mdc) for integration guidelines.
|
||||
430
README.md
430
README.md
@@ -427,6 +427,436 @@ task-master add-task --prompt="Description of the new task"
|
||||
# Add a task with dependencies
|
||||
task-master add-task --prompt="Description" --dependencies=1,2,3
|
||||
|
||||
# Add a task with priority
|
||||
# Task Master
|
||||
### by [@eyaltoledano](https://x.com/eyaltoledano)
|
||||
|
||||
A task management system for AI-driven development with Claude, designed to work seamlessly with Cursor AI.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 14.0.0 or higher
|
||||
- Anthropic API key (Claude API)
|
||||
- Anthropic SDK version 0.39.0 or higher
|
||||
- OpenAI SDK (for Perplexity API integration, optional)
|
||||
|
||||
## Configuration
|
||||
|
||||
The script can be configured through environment variables in a `.env` file at the root of the project:
|
||||
|
||||
### Required Configuration
|
||||
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude
|
||||
|
||||
### Optional Configuration
|
||||
- `MODEL`: Specify which Claude model to use (default: "claude-3-7-sonnet-20250219")
|
||||
- `MAX_TOKENS`: Maximum tokens for model responses (default: 4000)
|
||||
- `TEMPERATURE`: Temperature for model responses (default: 0.7)
|
||||
- `PERPLEXITY_API_KEY`: Your Perplexity API key for research-backed subtask generation
|
||||
- `PERPLEXITY_MODEL`: Specify which Perplexity model to use (default: "sonar-medium-online")
|
||||
- `DEBUG`: Enable debug logging (default: false)
|
||||
- `LOG_LEVEL`: Log level - debug, info, warn, error (default: info)
|
||||
- `DEFAULT_SUBTASKS`: Default number of subtasks when expanding (default: 3)
|
||||
- `DEFAULT_PRIORITY`: Default priority for generated tasks (default: medium)
|
||||
- `PROJECT_NAME`: Override default project name in tasks.json
|
||||
- `PROJECT_VERSION`: Override default version in tasks.json
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g task-master-ai
|
||||
|
||||
# OR install locally within your project
|
||||
npm install task-master-ai
|
||||
```
|
||||
|
||||
### Initialize a new project
|
||||
|
||||
```bash
|
||||
# If installed globally
|
||||
task-master init
|
||||
|
||||
# If installed locally
|
||||
npx task-master-init
|
||||
```
|
||||
|
||||
This will prompt you for project details and set up a new project with the necessary files and structure.
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. This package uses ES modules. Your package.json should include `"type": "module"`.
|
||||
2. The Anthropic SDK version should be 0.39.0 or higher.
|
||||
|
||||
## Quick Start with Global Commands
|
||||
|
||||
After installing the package globally, you can use these CLI commands from any directory:
|
||||
|
||||
```bash
|
||||
# Initialize a new project
|
||||
task-master init
|
||||
|
||||
# Parse a PRD and generate tasks
|
||||
task-master parse-prd your-prd.txt
|
||||
|
||||
# List all tasks
|
||||
task-master list
|
||||
|
||||
# Show the next task to work on
|
||||
task-master next
|
||||
|
||||
# Generate task files
|
||||
task-master generate
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### If `task-master init` doesn't respond:
|
||||
|
||||
Try running it with Node directly:
|
||||
|
||||
```bash
|
||||
node node_modules/claude-task-master/scripts/init.js
|
||||
```
|
||||
|
||||
Or clone the repository and run:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/eyaltoledano/claude-task-master.git
|
||||
cd claude-task-master
|
||||
node scripts/init.js
|
||||
```
|
||||
|
||||
## Task Structure
|
||||
|
||||
Tasks in tasks.json have the following structure:
|
||||
|
||||
- `id`: Unique identifier for the task (Example: `1`)
|
||||
- `title`: Brief, descriptive title of the task (Example: `"Initialize Repo"`)
|
||||
- `description`: Concise description of what the task involves (Example: `"Create a new repository, set up initial structure."`)
|
||||
- `status`: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
|
||||
- `dependencies`: IDs of tasks that must be completed before this task (Example: `[1, 2]`)
|
||||
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
|
||||
- This helps quickly identify which prerequisite tasks are blocking work
|
||||
- `priority`: Importance level of the task (Example: `"high"`, `"medium"`, `"low"`)
|
||||
- `details`: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
|
||||
- `testStrategy`: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
|
||||
- `subtasks`: List of smaller, more specific tasks that make up the main task (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
|
||||
|
||||
## Integrating with Cursor AI
|
||||
|
||||
Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.cursor.so/), providing a structured workflow for AI-driven development.
|
||||
|
||||
### Setup with Cursor
|
||||
|
||||
1. After initializing your project, open it in Cursor
|
||||
2. The `.cursor/rules/dev_workflow.mdc` file is automatically loaded by Cursor, providing the AI with knowledge about the task management system
|
||||
3. Place your PRD document in the `scripts/` directory (e.g., `scripts/prd.txt`)
|
||||
4. Open Cursor's AI chat and switch to Agent mode
|
||||
|
||||
### Initial Task Generation
|
||||
|
||||
In Cursor's AI chat, instruct the agent to generate tasks from your PRD:
|
||||
|
||||
```
|
||||
Please use the task-master parse-prd command to generate tasks from my PRD. The PRD is located at scripts/prd.txt.
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master parse-prd scripts/prd.txt
|
||||
```
|
||||
|
||||
This will:
|
||||
- Parse your PRD document
|
||||
- Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies
|
||||
- The agent will understand this process due to the Cursor rules
|
||||
|
||||
### Generate Individual Task Files
|
||||
|
||||
Next, ask the agent to generate individual task files:
|
||||
|
||||
```
|
||||
Please generate individual task files from tasks.json
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master generate
|
||||
```
|
||||
|
||||
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
|
||||
|
||||
## AI-Driven Development Workflow
|
||||
|
||||
The Cursor agent is pre-configured (via the rules file) to follow this workflow:
|
||||
|
||||
### 1. Task Discovery and Selection
|
||||
|
||||
Ask the agent to list available tasks:
|
||||
|
||||
```
|
||||
What tasks are available to work on next?
|
||||
```
|
||||
|
||||
The agent will:
|
||||
- Run `task-master list` to see all tasks
|
||||
- Run `task-master next` to determine the next task to work on
|
||||
- Analyze dependencies to determine which tasks are ready to be worked on
|
||||
- Prioritize tasks based on priority level and ID order
|
||||
- Suggest the next task(s) to implement
|
||||
|
||||
### 2. Task Implementation
|
||||
|
||||
When implementing a task, the agent will:
|
||||
- Reference the task's details section for implementation specifics
|
||||
- Consider dependencies on previous tasks
|
||||
- Follow the project's coding standards
|
||||
- Create appropriate tests based on the task's testStrategy
|
||||
|
||||
You can ask:
|
||||
```
|
||||
Let's implement task 3. What does it involve?
|
||||
```
|
||||
|
||||
### 3. Task Verification
|
||||
|
||||
Before marking a task as complete, verify it according to:
|
||||
- The task's specified testStrategy
|
||||
- Any automated tests in the codebase
|
||||
- Manual verification if required
|
||||
|
||||
### 4. Task Completion
|
||||
|
||||
When a task is completed, tell the agent:
|
||||
|
||||
```
|
||||
Task 3 is now complete. Please update its status.
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master set-status --id=3 --status=done
|
||||
```
|
||||
|
||||
### 5. Handling Implementation Drift
|
||||
|
||||
If during implementation, you discover that:
|
||||
- The current approach differs significantly from what was planned
|
||||
- Future tasks need to be modified due to current implementation choices
|
||||
- New dependencies or requirements have emerged
|
||||
|
||||
Tell the agent:
|
||||
```
|
||||
We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change.
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master update --from=4 --prompt="Now we are using Express instead of Fastify."
|
||||
```
|
||||
|
||||
This will rewrite or re-scope subsequent tasks in tasks.json while preserving completed work.
|
||||
|
||||
### 6. Breaking Down Complex Tasks
|
||||
|
||||
For complex tasks that need more granularity:
|
||||
|
||||
```
|
||||
Task 5 seems complex. Can you break it down into subtasks?
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master expand --id=5 --num=3
|
||||
```
|
||||
|
||||
You can provide additional context:
|
||||
```
|
||||
Please break down task 5 with a focus on security considerations.
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master expand --id=5 --prompt="Focus on security aspects"
|
||||
```
|
||||
|
||||
You can also expand all pending tasks:
|
||||
```
|
||||
Please break down all pending tasks into subtasks.
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master expand --all
|
||||
```
|
||||
|
||||
For research-backed subtask generation using Perplexity AI:
|
||||
```
|
||||
Please break down task 5 using research-backed generation.
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
```bash
|
||||
task-master expand --id=5 --research
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
Here's a comprehensive reference of all available commands:
|
||||
|
||||
### Parse PRD
|
||||
```bash
|
||||
# Parse a PRD file and generate tasks
|
||||
task-master parse-prd <prd-file.txt>
|
||||
|
||||
# Limit the number of tasks generated
|
||||
task-master parse-prd <prd-file.txt> --num-tasks=10
|
||||
```
|
||||
|
||||
### List Tasks
|
||||
```bash
|
||||
# List all tasks
|
||||
task-master list
|
||||
|
||||
# List tasks with a specific status
|
||||
task-master list --status=<status>
|
||||
|
||||
# List tasks with subtasks
|
||||
task-master list --with-subtasks
|
||||
|
||||
# List tasks with a specific status and include subtasks
|
||||
task-master list --status=<status> --with-subtasks
|
||||
```
|
||||
|
||||
### Show Next Task
|
||||
```bash
|
||||
# Show the next task to work on based on dependencies and status
|
||||
task-master next
|
||||
```
|
||||
|
||||
### Show Specific Task
|
||||
```bash
|
||||
# Show details of a specific task
|
||||
task-master show <id>
|
||||
# or
|
||||
task-master show --id=<id>
|
||||
|
||||
# View a specific subtask (e.g., subtask 2 of task 1)
|
||||
task-master show 1.2
|
||||
```
|
||||
|
||||
### Update Tasks
|
||||
```bash
|
||||
# Update tasks from a specific ID and provide context
|
||||
task-master update --from=<id> --prompt="<prompt>"
|
||||
```
|
||||
|
||||
### Generate Task Files
|
||||
```bash
|
||||
# Generate individual task files from tasks.json
|
||||
task-master generate
|
||||
```
|
||||
|
||||
### Set Task Status
|
||||
```bash
|
||||
# Set status of a single task
|
||||
task-master set-status --id=<id> --status=<status>
|
||||
|
||||
# Set status for multiple tasks
|
||||
task-master set-status --id=1,2,3 --status=<status>
|
||||
|
||||
# Set status for subtasks
|
||||
task-master set-status --id=1.1,1.2 --status=<status>
|
||||
```
|
||||
|
||||
When marking a task as "done", all of its subtasks will automatically be marked as "done" as well.
|
||||
|
||||
### Expand Tasks
|
||||
```bash
|
||||
# Expand a specific task with subtasks
|
||||
task-master expand --id=<id> --num=<number>
|
||||
|
||||
# Expand with additional context
|
||||
task-master expand --id=<id> --prompt="<context>"
|
||||
|
||||
# Expand all pending tasks
|
||||
task-master expand --all
|
||||
|
||||
# Force regeneration of subtasks for tasks that already have them
|
||||
task-master expand --all --force
|
||||
|
||||
# Research-backed subtask generation for a specific task
|
||||
task-master expand --id=<id> --research
|
||||
|
||||
# Research-backed generation for all tasks
|
||||
task-master expand --all --research
|
||||
```
|
||||
|
||||
### Clear Subtasks
|
||||
```bash
|
||||
# Clear subtasks from a specific task
|
||||
task-master clear-subtasks --id=<id>
|
||||
|
||||
# Clear subtasks from multiple tasks
|
||||
task-master clear-subtasks --id=1,2,3
|
||||
|
||||
# Clear subtasks from all tasks
|
||||
task-master clear-subtasks --all
|
||||
```
|
||||
|
||||
### Analyze Task Complexity
|
||||
```bash
|
||||
# Analyze complexity of all tasks
|
||||
task-master analyze-complexity
|
||||
|
||||
# Save report to a custom location
|
||||
task-master analyze-complexity --output=my-report.json
|
||||
|
||||
# Use a specific LLM model
|
||||
task-master analyze-complexity --model=claude-3-opus-20240229
|
||||
|
||||
# Set a custom complexity threshold (1-10)
|
||||
task-master analyze-complexity --threshold=6
|
||||
|
||||
# Use an alternative tasks file
|
||||
task-master analyze-complexity --file=custom-tasks.json
|
||||
|
||||
# Use Perplexity AI for research-backed complexity analysis
|
||||
task-master analyze-complexity --research
|
||||
```
|
||||
|
||||
### View Complexity Report
|
||||
```bash
|
||||
# Display the task complexity analysis report
|
||||
task-master complexity-report
|
||||
|
||||
# View a report at a custom location
|
||||
task-master complexity-report --file=my-report.json
|
||||
```
|
||||
|
||||
### Managing Task Dependencies
|
||||
```bash
|
||||
# Add a dependency to a task
|
||||
task-master add-dependency --id=<id> --depends-on=<id>
|
||||
|
||||
# Remove a dependency from a task
|
||||
task-master remove-dependency --id=<id> --depends-on=<id>
|
||||
|
||||
# Validate dependencies without fixing them
|
||||
task-master validate-dependencies
|
||||
|
||||
# Find and fix invalid dependencies automatically
|
||||
task-master fix-dependencies
|
||||
```
|
||||
|
||||
### Add a New Task
|
||||
```bash
|
||||
# Add a new task using AI
|
||||
task-master add-task --prompt="Description of the new task"
|
||||
|
||||
# Add a task with dependencies
|
||||
task-master add-task --prompt="Description" --dependencies=1,2,3
|
||||
|
||||
# Add a task with priority
|
||||
task-master add-task --prompt="Description" --priority=high
|
||||
```
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createRequire } from 'module';
|
||||
import { spawn } from 'child_process';
|
||||
import { Command } from 'commander';
|
||||
import { displayHelp, displayBanner } from '../scripts/modules/ui.js';
|
||||
import { registerCommands } from '../scripts/modules/commands.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -36,25 +37,48 @@ function runDevScript(args) {
|
||||
});
|
||||
}
|
||||
|
||||
// Set up the command-line interface
|
||||
const program = new Command();
|
||||
/**
|
||||
* Create a wrapper action that passes the command to dev.js
|
||||
* @param {string} commandName - The name of the command
|
||||
* @returns {Function} Wrapper action function
|
||||
*/
|
||||
function createDevScriptAction(commandName) {
|
||||
return (options, cmd) => {
|
||||
// Start with the command name
|
||||
const args = [commandName];
|
||||
|
||||
program
|
||||
.name('task-master')
|
||||
.description('Claude Task Master CLI')
|
||||
.version(version)
|
||||
.addHelpText('afterAll', () => {
|
||||
// Use the same help display function as dev.js for consistency
|
||||
displayHelp();
|
||||
return ''; // Return empty string to prevent commander's default help
|
||||
// Handle direct arguments (non-option arguments)
|
||||
if (cmd && cmd.args && cmd.args.length > 0) {
|
||||
args.push(...cmd.args);
|
||||
}
|
||||
|
||||
// Add all options
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
// Skip the Command's built-in properties
|
||||
if (['parent', 'commands', 'options', 'rawArgs'].includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle boolean flags
|
||||
if (typeof value === 'boolean') {
|
||||
if (value === true) {
|
||||
args.push(`--${key}`);
|
||||
} else if (key.startsWith('no-')) {
|
||||
// Handle --no-X options
|
||||
const baseOption = key.substring(3);
|
||||
args.push(`--${baseOption}`);
|
||||
}
|
||||
} else if (value !== undefined) {
|
||||
args.push(`--${key}`, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// Add custom help option to directly call our help display
|
||||
program.helpOption('-h, --help', 'Display help information');
|
||||
program.on('--help', () => {
|
||||
displayHelp();
|
||||
});
|
||||
runDevScript(args);
|
||||
};
|
||||
}
|
||||
|
||||
// Special case for the 'init' command which uses a different script
|
||||
function registerInitCommand(program) {
|
||||
program
|
||||
.command('init')
|
||||
.description('Initialize a new project')
|
||||
@@ -85,6 +109,29 @@ program
|
||||
process.exit(code);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set up the command-line interface
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('task-master')
|
||||
.description('Claude Task Master CLI')
|
||||
.version(version)
|
||||
.addHelpText('afterAll', () => {
|
||||
// Use the same help display function as dev.js for consistency
|
||||
displayHelp();
|
||||
return ''; // Return empty string to prevent commander's default help
|
||||
});
|
||||
|
||||
// Add custom help option to directly call our help display
|
||||
program.helpOption('-h, --help', 'Display help information');
|
||||
program.on('--help', () => {
|
||||
displayHelp();
|
||||
});
|
||||
|
||||
// Add special case commands
|
||||
registerInitCommand(program);
|
||||
|
||||
program
|
||||
.command('dev')
|
||||
@@ -95,228 +142,33 @@ program
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
// Add shortcuts for common dev.js commands
|
||||
program
|
||||
.command('list')
|
||||
.description('List all tasks')
|
||||
.option('-s, --status <status>', 'Filter by status')
|
||||
.option('--with-subtasks', 'Show subtasks for each task')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action((options) => {
|
||||
const args = ['list'];
|
||||
if (options.status) args.push('--status', options.status);
|
||||
if (options.withSubtasks) args.push('--with-subtasks');
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
// Use a temporary Command instance to get all command definitions
|
||||
const tempProgram = new Command();
|
||||
registerCommands(tempProgram);
|
||||
|
||||
// For each command in the temp instance, add a modified version to our actual program
|
||||
tempProgram.commands.forEach(cmd => {
|
||||
if (['init', 'dev'].includes(cmd.name())) {
|
||||
// Skip commands we've already defined specially
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new command with the same name and description
|
||||
const newCmd = program
|
||||
.command(cmd.name())
|
||||
.description(cmd.description());
|
||||
|
||||
// Copy all options
|
||||
cmd.options.forEach(opt => {
|
||||
newCmd.option(
|
||||
opt.flags,
|
||||
opt.description,
|
||||
opt.defaultValue
|
||||
);
|
||||
});
|
||||
|
||||
program
|
||||
.command('next')
|
||||
.description('Show the next task to work on')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action((options) => {
|
||||
const args = ['next'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('generate')
|
||||
.description('Generate task files')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-o, --output <dir>', 'Output directory', 'tasks')
|
||||
.action((options) => {
|
||||
const args = ['generate'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.output) args.push('--output', options.output);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
// Add all other commands from dev.js
|
||||
program
|
||||
.command('parse-prd')
|
||||
.description('Parse a PRD file and generate tasks')
|
||||
.argument('[file]', 'Path to the PRD file')
|
||||
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
||||
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
||||
.action((file, options) => {
|
||||
const args = ['parse-prd'];
|
||||
if (file) args.push(file);
|
||||
if (options.output) args.push('--output', options.output);
|
||||
if (options.numTasks) args.push('--num-tasks', options.numTasks);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Update tasks based on new information or implementation changes')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('--from <id>', 'Task ID to start updating from', '1')
|
||||
.option('-p, --prompt <text>', 'Prompt explaining the changes or new context (required)')
|
||||
.option('-r, --research', 'Use Perplexity AI for research-backed task updates')
|
||||
.action((options) => {
|
||||
const args = ['update'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.from) args.push('--from', options.from);
|
||||
if (options.prompt) args.push('--prompt', options.prompt);
|
||||
if (options.research) args.push('--research');
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('set-status')
|
||||
.description('Set the status of a task')
|
||||
.option('-i, --id <id>', 'Task ID (can be comma-separated for multiple tasks)')
|
||||
.option('-s, --status <status>', 'New status (todo, in-progress, review, done)')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action((options) => {
|
||||
const args = ['set-status'];
|
||||
if (options.id) args.push('--id', options.id);
|
||||
if (options.status) args.push('--status', options.status);
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('expand')
|
||||
.description('Break down tasks into detailed subtasks')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <id>', 'Task ID to expand')
|
||||
.option('-a, --all', 'Expand all tasks')
|
||||
.option('-n, --num <number>', 'Number of subtasks to generate')
|
||||
.option('--research', 'Enable Perplexity AI for research-backed subtask generation')
|
||||
.option('-p, --prompt <text>', 'Additional context to guide subtask generation')
|
||||
.option('--force', 'Force regeneration of subtasks for tasks that already have them')
|
||||
.action((options) => {
|
||||
const args = ['expand'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.id) args.push('--id', options.id);
|
||||
if (options.all) args.push('--all');
|
||||
if (options.num) args.push('--num', options.num);
|
||||
if (options.research) args.push('--research');
|
||||
if (options.prompt) args.push('--prompt', options.prompt);
|
||||
if (options.force) args.push('--force');
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('analyze-complexity')
|
||||
.description('Analyze tasks and generate complexity-based expansion recommendations')
|
||||
.option('-o, --output <file>', 'Output file path for the report', 'scripts/task-complexity-report.json')
|
||||
.option('-m, --model <model>', 'LLM model to use for analysis')
|
||||
.option('-t, --threshold <number>', 'Minimum complexity score to recommend expansion (1-10)', '5')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-r, --research', 'Use Perplexity AI for research-backed complexity analysis')
|
||||
.action((options) => {
|
||||
const args = ['analyze-complexity'];
|
||||
if (options.output) args.push('--output', options.output);
|
||||
if (options.model) args.push('--model', options.model);
|
||||
if (options.threshold) args.push('--threshold', options.threshold);
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.research) args.push('--research');
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('clear-subtasks')
|
||||
.description('Clear subtasks from specified tasks')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <ids>', 'Task IDs (comma-separated) to clear subtasks from')
|
||||
.option('--all', 'Clear subtasks from all tasks')
|
||||
.action((options) => {
|
||||
const args = ['clear-subtasks'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.id) args.push('--id', options.id);
|
||||
if (options.all) args.push('--all');
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('add-task')
|
||||
.description('Add a new task to tasks.json using AI')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-p, --prompt <text>', 'Description of the task to add (required)')
|
||||
.option('-d, --dependencies <ids>', 'Comma-separated list of task IDs this task depends on')
|
||||
.option('--priority <priority>', 'Task priority (high, medium, low)', 'medium')
|
||||
.action((options) => {
|
||||
const args = ['add-task'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.prompt) args.push('--prompt', options.prompt);
|
||||
if (options.dependencies) args.push('--dependencies', options.dependencies);
|
||||
if (options.priority) args.push('--priority', options.priority);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('show')
|
||||
.description('Display detailed information about a specific task')
|
||||
.argument('[id]', 'Task ID to show')
|
||||
.option('-i, --id <id>', 'Task ID to show (alternative to argument)')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action((id, options) => {
|
||||
const args = ['show'];
|
||||
if (id) args.push(id);
|
||||
else if (options.id) args.push('--id', options.id);
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('add-dependency')
|
||||
.description('Add a dependency to a task')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <id>', 'ID of the task to add dependency to')
|
||||
.option('-d, --depends-on <id>', 'ID of the task to add as dependency')
|
||||
.action((options) => {
|
||||
const args = ['add-dependency'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.id) args.push('--id', options.id);
|
||||
if (options.dependsOn) args.push('--depends-on', options.dependsOn);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('remove-dependency')
|
||||
.description('Remove a dependency from a task')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <id>', 'ID of the task to remove dependency from')
|
||||
.option('-d, --depends-on <id>', 'ID of the task to remove as dependency')
|
||||
.action((options) => {
|
||||
const args = ['remove-dependency'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
if (options.id) args.push('--id', options.id);
|
||||
if (options.dependsOn) args.push('--depends-on', options.dependsOn);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('validate-dependencies')
|
||||
.description('Check for and identify invalid dependencies in tasks')
|
||||
.option('-f, --file <path>', 'Path to the tasks.json file', 'tasks/tasks.json')
|
||||
.action((options) => {
|
||||
const args = ['validate-dependencies'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('fix-dependencies')
|
||||
.description('Find and fix all invalid dependencies in tasks.json and task files')
|
||||
.option('-f, --file <path>', 'Path to the tasks.json file', 'tasks/tasks.json')
|
||||
.action((options) => {
|
||||
const args = ['fix-dependencies'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
});
|
||||
|
||||
program
|
||||
.command('complexity-report')
|
||||
.description('Display the complexity analysis report')
|
||||
.option('-f, --file <path>', 'Path to the complexity report file', 'scripts/task-complexity-report.json')
|
||||
.action((options) => {
|
||||
const args = ['complexity-report'];
|
||||
if (options.file) args.push('--file', options.file);
|
||||
runDevScript(args);
|
||||
// Set the action to proxy to dev.js
|
||||
newCmd.action(createDevScriptAction(cmd.name()));
|
||||
});
|
||||
|
||||
// Parse the command line arguments
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
expandAllTasks,
|
||||
clearSubtasks,
|
||||
addTask,
|
||||
addSubtask,
|
||||
removeSubtask,
|
||||
analyzeTaskComplexity
|
||||
} from './task-manager.js';
|
||||
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
displayNextTask,
|
||||
displayTaskById,
|
||||
displayComplexityReport,
|
||||
getStatusWithColor
|
||||
} from './ui.js';
|
||||
|
||||
/**
|
||||
@@ -401,6 +404,143 @@ function registerCommands(programInstance) {
|
||||
await displayComplexityReport(options.file);
|
||||
});
|
||||
|
||||
// add-subtask command
|
||||
programInstance
|
||||
.command('add-subtask')
|
||||
.description('Add a subtask to an existing task')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-p, --parent <id>', 'Parent task ID (required)')
|
||||
.option('-i, --task-id <id>', 'Existing task ID to convert to subtask')
|
||||
.option('-t, --title <title>', 'Title for the new subtask (when creating a new subtask)')
|
||||
.option('-d, --description <text>', 'Description for the new subtask')
|
||||
.option('--details <text>', 'Implementation details for the new subtask')
|
||||
.option('--dependencies <ids>', 'Comma-separated list of dependency IDs for the new subtask')
|
||||
.option('-s, --status <status>', 'Status for the new subtask', 'pending')
|
||||
.option('--no-generate', 'Skip regenerating task files')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const parentId = options.parent;
|
||||
const existingTaskId = options.taskId;
|
||||
const generateFiles = options.generate;
|
||||
|
||||
if (!parentId) {
|
||||
console.error(chalk.red('Error: --parent parameter is required. Please provide a parent task ID.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse dependencies if provided
|
||||
let dependencies = [];
|
||||
if (options.dependencies) {
|
||||
dependencies = options.dependencies.split(',').map(id => {
|
||||
// Handle both regular IDs and dot notation
|
||||
return id.includes('.') ? id.trim() : parseInt(id.trim(), 10);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (existingTaskId) {
|
||||
// Convert existing task to subtask
|
||||
console.log(chalk.blue(`Converting task ${existingTaskId} to a subtask of ${parentId}...`));
|
||||
await addSubtask(tasksPath, parentId, existingTaskId, null, generateFiles);
|
||||
console.log(chalk.green(`✓ Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`));
|
||||
} else if (options.title) {
|
||||
// Create new subtask with provided data
|
||||
console.log(chalk.blue(`Creating new subtask for parent task ${parentId}...`));
|
||||
|
||||
const newSubtaskData = {
|
||||
title: options.title,
|
||||
description: options.description || '',
|
||||
details: options.details || '',
|
||||
status: options.status || 'pending',
|
||||
dependencies: dependencies
|
||||
};
|
||||
|
||||
const subtask = await addSubtask(tasksPath, parentId, null, newSubtaskData, generateFiles);
|
||||
console.log(chalk.green(`✓ New subtask ${parentId}.${subtask.id} successfully created`));
|
||||
|
||||
// Display success message and suggested next steps
|
||||
console.log(boxen(
|
||||
chalk.white.bold(`Subtask ${parentId}.${subtask.id} Added Successfully`) + '\n\n' +
|
||||
chalk.white(`Title: ${subtask.title}`) + '\n' +
|
||||
chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + '\n' +
|
||||
(dependencies.length > 0 ? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + '\n' : '') +
|
||||
'\n' +
|
||||
chalk.white.bold('Next Steps:') + '\n' +
|
||||
chalk.cyan(`1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks`) + '\n' +
|
||||
chalk.cyan(`2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it`),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
} else {
|
||||
console.error(chalk.red('Error: Either --task-id or --title must be provided.'));
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Usage Examples:') + '\n\n' +
|
||||
chalk.white('Convert existing task to subtask:') + '\n' +
|
||||
chalk.yellow(` task-master add-subtask --parent=5 --task-id=8`) + '\n\n' +
|
||||
chalk.white('Create new subtask:') + '\n' +
|
||||
chalk.yellow(` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"`) + '\n\n',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// remove-subtask command
|
||||
programInstance
|
||||
.command('remove-subtask')
|
||||
.description('Remove a subtask from its parent task')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <id>', 'Subtask ID to remove in format "parentId.subtaskId" (required)')
|
||||
.option('-c, --convert', 'Convert the subtask to a standalone task instead of deleting it')
|
||||
.option('--no-generate', 'Skip regenerating task files')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const subtaskId = options.id;
|
||||
const convertToTask = options.convert || false;
|
||||
const generateFiles = options.generate;
|
||||
|
||||
if (!subtaskId) {
|
||||
console.error(chalk.red('Error: --id parameter is required. Please provide a subtask ID in format "parentId.subtaskId".'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(chalk.blue(`Removing subtask ${subtaskId}...`));
|
||||
if (convertToTask) {
|
||||
console.log(chalk.blue('The subtask will be converted to a standalone task'));
|
||||
}
|
||||
|
||||
const result = await removeSubtask(tasksPath, subtaskId, convertToTask, generateFiles);
|
||||
|
||||
if (convertToTask && result) {
|
||||
// Display success message and next steps for converted task
|
||||
console.log(boxen(
|
||||
chalk.white.bold(`Subtask ${subtaskId} Converted to Task #${result.id}`) + '\n\n' +
|
||||
chalk.white(`Title: ${result.title}`) + '\n' +
|
||||
chalk.white(`Status: ${getStatusWithColor(result.status)}`) + '\n' +
|
||||
chalk.white(`Dependencies: ${result.dependencies.join(', ')}`) + '\n\n' +
|
||||
chalk.white.bold('Next Steps:') + '\n' +
|
||||
chalk.cyan(`1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task`) + '\n' +
|
||||
chalk.cyan(`2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it`),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
} else {
|
||||
// Display success message for deleted subtask
|
||||
console.log(boxen(
|
||||
chalk.white.bold(`Subtask ${subtaskId} Removed`) + '\n\n' +
|
||||
chalk.white('The subtask has been successfully deleted.'),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Add more commands as needed...
|
||||
|
||||
return programInstance;
|
||||
|
||||
@@ -2288,6 +2288,274 @@ function findNextTask(tasks) {
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subtask to a parent task
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {number|string} parentId - ID of the parent task
|
||||
* @param {number|string|null} existingTaskId - ID of an existing task to convert to subtask (optional)
|
||||
* @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null)
|
||||
* @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask
|
||||
* @returns {Object} The newly created or converted subtask
|
||||
*/
|
||||
async function addSubtask(tasksPath, parentId, existingTaskId = null, newSubtaskData = null, generateFiles = true) {
|
||||
try {
|
||||
log('info', `Adding subtask to parent task ${parentId}...`);
|
||||
|
||||
// Read the existing tasks
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Convert parent ID to number
|
||||
const parentIdNum = parseInt(parentId, 10);
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find(t => t.id === parentIdNum);
|
||||
if (!parentTask) {
|
||||
throw new Error(`Parent task with ID ${parentIdNum} not found`);
|
||||
}
|
||||
|
||||
// Initialize subtasks array if it doesn't exist
|
||||
if (!parentTask.subtasks) {
|
||||
parentTask.subtasks = [];
|
||||
}
|
||||
|
||||
let newSubtask;
|
||||
|
||||
// Case 1: Convert an existing task to a subtask
|
||||
if (existingTaskId !== null) {
|
||||
const existingTaskIdNum = parseInt(existingTaskId, 10);
|
||||
|
||||
// Find the existing task
|
||||
const existingTaskIndex = data.tasks.findIndex(t => t.id === existingTaskIdNum);
|
||||
if (existingTaskIndex === -1) {
|
||||
throw new Error(`Task with ID ${existingTaskIdNum} not found`);
|
||||
}
|
||||
|
||||
const existingTask = data.tasks[existingTaskIndex];
|
||||
|
||||
// Check if task is already a subtask
|
||||
if (existingTask.parentTaskId) {
|
||||
throw new Error(`Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}`);
|
||||
}
|
||||
|
||||
// Check for circular dependency
|
||||
if (existingTaskIdNum === parentIdNum) {
|
||||
throw new Error(`Cannot make a task a subtask of itself`);
|
||||
}
|
||||
|
||||
// Check if parent task is a subtask of the task we're converting
|
||||
// This would create a circular dependency
|
||||
if (isTaskDependentOn(data.tasks, parentTask, existingTaskIdNum)) {
|
||||
throw new Error(`Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}`);
|
||||
}
|
||||
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId = parentTask.subtasks.length > 0
|
||||
? Math.max(...parentTask.subtasks.map(st => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
// Clone the existing task to be converted to a subtask
|
||||
newSubtask = { ...existingTask, id: newSubtaskId, parentTaskId: parentIdNum };
|
||||
|
||||
// Add to parent's subtasks
|
||||
parentTask.subtasks.push(newSubtask);
|
||||
|
||||
// Remove the task from the main tasks array
|
||||
data.tasks.splice(existingTaskIndex, 1);
|
||||
|
||||
log('info', `Converted task ${existingTaskIdNum} to subtask ${parentIdNum}.${newSubtaskId}`);
|
||||
}
|
||||
// Case 2: Create a new subtask
|
||||
else if (newSubtaskData) {
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId = parentTask.subtasks.length > 0
|
||||
? Math.max(...parentTask.subtasks.map(st => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
// Create the new subtask object
|
||||
newSubtask = {
|
||||
id: newSubtaskId,
|
||||
title: newSubtaskData.title,
|
||||
description: newSubtaskData.description || '',
|
||||
details: newSubtaskData.details || '',
|
||||
status: newSubtaskData.status || 'pending',
|
||||
dependencies: newSubtaskData.dependencies || [],
|
||||
parentTaskId: parentIdNum
|
||||
};
|
||||
|
||||
// Add to parent's subtasks
|
||||
parentTask.subtasks.push(newSubtask);
|
||||
|
||||
log('info', `Created new subtask ${parentIdNum}.${newSubtaskId}`);
|
||||
} else {
|
||||
throw new Error('Either existingTaskId or newSubtaskData must be provided');
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
}
|
||||
|
||||
return newSubtask;
|
||||
} catch (error) {
|
||||
log('error', `Error adding subtask: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task is dependent on another task (directly or indirectly)
|
||||
* Used to prevent circular dependencies
|
||||
* @param {Array} allTasks - Array of all tasks
|
||||
* @param {Object} task - The task to check
|
||||
* @param {number} targetTaskId - The task ID to check dependency against
|
||||
* @returns {boolean} Whether the task depends on the target task
|
||||
*/
|
||||
function isTaskDependentOn(allTasks, task, targetTaskId) {
|
||||
// If the task is a subtask, check if its parent is the target
|
||||
if (task.parentTaskId === targetTaskId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check direct dependencies
|
||||
if (task.dependencies && task.dependencies.includes(targetTaskId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check dependencies of dependencies (recursive)
|
||||
if (task.dependencies) {
|
||||
for (const depId of task.dependencies) {
|
||||
const depTask = allTasks.find(t => t.id === depId);
|
||||
if (depTask && isTaskDependentOn(allTasks, depTask, targetTaskId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check subtasks for dependencies
|
||||
if (task.subtasks) {
|
||||
for (const subtask of task.subtasks) {
|
||||
if (isTaskDependentOn(allTasks, subtask, targetTaskId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a subtask from its parent task
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} subtaskId - ID of the subtask to remove in format "parentId.subtaskId"
|
||||
* @param {boolean} convertToTask - Whether to convert the subtask to a standalone task
|
||||
* @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask
|
||||
* @returns {Object|null} The removed subtask if convertToTask is true, otherwise null
|
||||
*/
|
||||
async function removeSubtask(tasksPath, subtaskId, convertToTask = false, generateFiles = true) {
|
||||
try {
|
||||
log('info', `Removing subtask ${subtaskId}...`);
|
||||
|
||||
// Read the existing tasks
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Parse the subtask ID (format: "parentId.subtaskId")
|
||||
if (!subtaskId.includes('.')) {
|
||||
throw new Error(`Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"`);
|
||||
}
|
||||
|
||||
const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
|
||||
const parentId = parseInt(parentIdStr, 10);
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find(t => t.id === parentId);
|
||||
if (!parentTask) {
|
||||
throw new Error(`Parent task with ID ${parentId} not found`);
|
||||
}
|
||||
|
||||
// Check if parent has subtasks
|
||||
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
|
||||
throw new Error(`Parent task ${parentId} has no subtasks`);
|
||||
}
|
||||
|
||||
// Find the subtask to remove
|
||||
const subtaskIndex = parentTask.subtasks.findIndex(st => st.id === subtaskIdNum);
|
||||
if (subtaskIndex === -1) {
|
||||
throw new Error(`Subtask ${subtaskId} not found`);
|
||||
}
|
||||
|
||||
// Get a copy of the subtask before removing it
|
||||
const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };
|
||||
|
||||
// Remove the subtask from the parent
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
// If parent has no more subtasks, remove the subtasks array
|
||||
if (parentTask.subtasks.length === 0) {
|
||||
delete parentTask.subtasks;
|
||||
}
|
||||
|
||||
let convertedTask = null;
|
||||
|
||||
// Convert the subtask to a standalone task if requested
|
||||
if (convertToTask) {
|
||||
log('info', `Converting subtask ${subtaskId} to a standalone task...`);
|
||||
|
||||
// Find the highest task ID to determine the next ID
|
||||
const highestId = Math.max(...data.tasks.map(t => t.id));
|
||||
const newTaskId = highestId + 1;
|
||||
|
||||
// Create the new task from the subtask
|
||||
convertedTask = {
|
||||
id: newTaskId,
|
||||
title: removedSubtask.title,
|
||||
description: removedSubtask.description || '',
|
||||
details: removedSubtask.details || '',
|
||||
status: removedSubtask.status || 'pending',
|
||||
dependencies: removedSubtask.dependencies || [],
|
||||
priority: parentTask.priority || 'medium' // Inherit priority from parent
|
||||
};
|
||||
|
||||
// Add the parent task as a dependency if not already present
|
||||
if (!convertedTask.dependencies.includes(parentId)) {
|
||||
convertedTask.dependencies.push(parentId);
|
||||
}
|
||||
|
||||
// Add the converted task to the tasks array
|
||||
data.tasks.push(convertedTask);
|
||||
|
||||
log('info', `Created new task ${newTaskId} from subtask ${subtaskId}`);
|
||||
} else {
|
||||
log('info', `Subtask ${subtaskId} deleted`);
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
log('info', 'Regenerating task files...');
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
}
|
||||
|
||||
return convertedTask;
|
||||
} catch (error) {
|
||||
log('error', `Error removing subtask: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Export task manager functions
|
||||
export {
|
||||
@@ -2301,6 +2569,8 @@ export {
|
||||
expandAllTasks,
|
||||
clearSubtasks,
|
||||
addTask,
|
||||
addSubtask,
|
||||
removeSubtask,
|
||||
findNextTask,
|
||||
analyzeTaskComplexity,
|
||||
};
|
||||
@@ -279,5 +279,5 @@ export {
|
||||
formatTaskId,
|
||||
findTaskById,
|
||||
truncate,
|
||||
findCycles,
|
||||
findCycles
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 3
|
||||
# Title: Implement Basic Task Operations
|
||||
# Status: done
|
||||
# Dependencies: 1, 2
|
||||
# Dependencies: 1
|
||||
# Priority: high
|
||||
# Description: Create core functionality for managing tasks including listing, creating, updating, and deleting tasks.
|
||||
# Details:
|
||||
@@ -16,41 +16,3 @@ Implement the following task operations:
|
||||
|
||||
# Test Strategy:
|
||||
Test each operation with valid and invalid inputs. Verify that dependencies are properly tracked and that status changes are reflected correctly in the tasks.json file.
|
||||
|
||||
# Subtasks:
|
||||
## 1. Implement Task Listing with Filtering [done]
|
||||
### Dependencies: None
|
||||
### Description: Create a function that retrieves tasks from the tasks.json file and implements filtering options. Use the Commander.js CLI to add a 'list' command with various filter flags (e.g., --status, --priority, --dependency). Implement sorting options for the list output.
|
||||
### Details:
|
||||
|
||||
|
||||
## 2. Develop Task Creation Functionality [done]
|
||||
### Dependencies: 3.1
|
||||
### Description: Implement a 'create' command in the CLI that allows users to add new tasks to the tasks.json file. Prompt for required fields (title, description, priority) and optional fields (dependencies, details, test strategy). Validate input and assign a unique ID to the new task.
|
||||
### Details:
|
||||
|
||||
|
||||
## 3. Implement Task Update Operations [done]
|
||||
### Dependencies: 3.1, 3.2
|
||||
### Description: Create an 'update' command that allows modification of existing task properties. Implement options to update individual fields or enter an interactive mode for multiple updates. Ensure that updates maintain data integrity, especially for dependencies.
|
||||
### Details:
|
||||
|
||||
|
||||
## 4. Develop Task Deletion Functionality [done]
|
||||
### Dependencies: 3.1, 3.2, 3.3
|
||||
### Description: Implement a 'delete' command to remove tasks from tasks.json. Include safeguards against deleting tasks with dependencies and provide a force option to override. Update any tasks that had the deleted task as a dependency.
|
||||
### Details:
|
||||
|
||||
|
||||
## 5. Implement Task Status Management [done]
|
||||
### Dependencies: 3.1, 3.2, 3.3
|
||||
### Description: Create a 'status' command to change the status of tasks (pending/done/deferred). Implement logic to handle status changes, including updating dependent tasks if necessary. Add a batch mode for updating multiple task statuses at once.
|
||||
### Details:
|
||||
|
||||
|
||||
## 6. Develop Task Dependency and Priority Management [done]
|
||||
### Dependencies: 3.1, 3.2, 3.3
|
||||
### Description: Implement 'dependency' and 'priority' commands to manage task relationships and importance. Create functions to add/remove dependencies and change priorities. Ensure the system prevents circular dependencies and maintains consistent priority levels.
|
||||
### Details:
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 12
|
||||
# Title: Develop Project Initialization System
|
||||
# Status: done
|
||||
# Dependencies: 1, 2, 3, 4, 6
|
||||
# Dependencies: 1, 3, 4, 6
|
||||
# Priority: medium
|
||||
# Description: Create functionality for initializing new projects with task structure and configuration.
|
||||
# Details:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 13
|
||||
# Title: Create Cursor Rules Implementation
|
||||
# Status: done
|
||||
# Dependencies: 1, 2, 3
|
||||
# Dependencies: 1, 3
|
||||
# Priority: medium
|
||||
# Description: Develop the Cursor AI integration rules and documentation.
|
||||
# Details:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 15
|
||||
# Title: Optimize Agent Integration with Cursor and dev.js Commands
|
||||
# Status: done
|
||||
# Dependencies: 2, 14
|
||||
# Dependencies: 14
|
||||
# Priority: medium
|
||||
# Description: Document and enhance existing agent interaction patterns through Cursor rules and dev.js commands.
|
||||
# Details:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 16
|
||||
# Title: Create Configuration Management System
|
||||
# Status: done
|
||||
# Dependencies: 1, 2
|
||||
# Dependencies: 1
|
||||
# Priority: high
|
||||
# Description: Implement robust configuration handling with environment variables and .env files.
|
||||
# Details:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 17
|
||||
# Title: Implement Comprehensive Logging System
|
||||
# Status: done
|
||||
# Dependencies: 2, 16
|
||||
# Dependencies: 16
|
||||
# Priority: medium
|
||||
# Description: Create a flexible logging system with configurable levels and output formats.
|
||||
# Details:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 18
|
||||
# Title: Create Comprehensive User Documentation
|
||||
# Status: done
|
||||
# Dependencies: 1, 2, 3, 4, 5, 6, 7, 11, 12, 16
|
||||
# Dependencies: 1, 3, 4, 5, 6, 7, 11, 12, 16
|
||||
# Priority: medium
|
||||
# Description: Develop complete user documentation including README, examples, and troubleshooting guides.
|
||||
# Details:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task ID: 19
|
||||
# Title: Implement Error Handling and Recovery
|
||||
# Status: done
|
||||
# Dependencies: 1, 2, 3, 5, 9, 16, 17
|
||||
# Dependencies: 1, 3, 5, 9, 16, 17
|
||||
# Priority: high
|
||||
# Description: Create robust error handling throughout the system with helpful error messages and recovery options.
|
||||
# Details:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task ID: 25
|
||||
# Title: Implement 'add-subtask' Command for Task Hierarchy Management
|
||||
# Status: pending
|
||||
# Status: done
|
||||
# Dependencies: 3
|
||||
# Priority: medium
|
||||
# Description: Create a command-line interface command that allows users to manually add subtasks to existing tasks, establishing a parent-child relationship between tasks.
|
||||
@@ -46,7 +46,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
|
||||
- Test the command with various parameter combinations
|
||||
|
||||
# Subtasks:
|
||||
## 1. Update Data Model to Support Parent-Child Task Relationships [pending]
|
||||
## 1. Update Data Model to Support Parent-Child Task Relationships [done]
|
||||
### Dependencies: None
|
||||
### Description: Modify the task data structure to support hierarchical relationships between tasks
|
||||
### Details:
|
||||
@@ -59,7 +59,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
|
||||
7. Test by manually creating tasks with parent-child relationships and verifying they're saved correctly
|
||||
8. Write unit tests to verify the updated data model works as expected
|
||||
|
||||
## 2. Implement Core addSubtask Function in task-manager.js [pending]
|
||||
## 2. Implement Core addSubtask Function in task-manager.js [done]
|
||||
### Dependencies: 25.1
|
||||
### Description: Create the core function that handles adding subtasks to parent tasks
|
||||
### Details:
|
||||
@@ -74,7 +74,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
|
||||
9. Export the function for use by the command handler
|
||||
10. Write unit tests to verify all scenarios (new subtask, converting task, error cases)
|
||||
|
||||
## 3. Implement add-subtask Command in commands.js [pending]
|
||||
## 3. Implement add-subtask Command in commands.js [done]
|
||||
### Dependencies: 25.2
|
||||
### Description: Create the command-line interface for the add-subtask functionality
|
||||
### Details:
|
||||
@@ -88,7 +88,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
|
||||
8. Test the command with various input combinations
|
||||
9. Ensure the command follows the same patterns as other commands like add-dependency
|
||||
|
||||
## 4. Create Unit Test for add-subtask [pending]
|
||||
## 4. Create Unit Test for add-subtask [done]
|
||||
### Dependencies: 25.2, 25.3
|
||||
### Description: Develop comprehensive unit tests for the add-subtask functionality
|
||||
### Details:
|
||||
@@ -101,7 +101,7 @@ Testing should verify both the functionality and edge cases of the subtask imple
|
||||
7. Ensure test coverage for all branches and edge cases
|
||||
8. Document the testing approach for future reference
|
||||
|
||||
## 5. Implement remove-subtask Command [pending]
|
||||
## 5. Implement remove-subtask Command [done]
|
||||
### Dependencies: 25.2, 25.3
|
||||
### Description: Create functionality to remove a subtask from its parent, following the same approach as add-subtask
|
||||
### Details:
|
||||
|
||||
105
tasks/tasks.json
105
tasks/tasks.json
@@ -19,98 +19,18 @@
|
||||
"testStrategy": "Verify that the tasks.json structure can be created, read, and validated. Test with sample data to ensure all fields are properly handled and that validation correctly identifies invalid structures.",
|
||||
"subtasks": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Develop Command Line Interface Foundation",
|
||||
"description": "Create the basic CLI structure using Commander.js with command parsing and help documentation.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"1"
|
||||
],
|
||||
"priority": "high",
|
||||
"details": "Implement the CLI foundation including:\n- Set up Commander.js for command parsing\n- Create help documentation for all commands\n- Implement colorized console output for better readability\n- Add logging system with configurable levels\n- Handle global options (--help, --version, --file, --quiet, --debug, --json)",
|
||||
"testStrategy": "Test each command with various parameters to ensure proper parsing. Verify help documentation is comprehensive and accurate. Test logging at different verbosity levels.",
|
||||
"subtasks": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Implement Basic Task Operations",
|
||||
"description": "Create core functionality for managing tasks including listing, creating, updating, and deleting tasks.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2
|
||||
1
|
||||
],
|
||||
"priority": "high",
|
||||
"details": "Implement the following task operations:\n- List tasks with filtering options\n- Create new tasks with required fields\n- Update existing task properties\n- Delete tasks\n- Change task status (pending/done/deferred)\n- Handle dependencies between tasks\n- Manage task priorities",
|
||||
"testStrategy": "Test each operation with valid and invalid inputs. Verify that dependencies are properly tracked and that status changes are reflected correctly in the tasks.json file.",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Implement Task Listing with Filtering",
|
||||
"description": "Create a function that retrieves tasks from the tasks.json file and implements filtering options. Use the Commander.js CLI to add a 'list' command with various filter flags (e.g., --status, --priority, --dependency). Implement sorting options for the list output.",
|
||||
"status": "done",
|
||||
"dependencies": [],
|
||||
"acceptanceCriteria": "- 'list' command is available in the CLI with help documentation"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Develop Task Creation Functionality",
|
||||
"description": "Implement a 'create' command in the CLI that allows users to add new tasks to the tasks.json file. Prompt for required fields (title, description, priority) and optional fields (dependencies, details, test strategy). Validate input and assign a unique ID to the new task.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1
|
||||
],
|
||||
"acceptanceCriteria": "- 'create' command is available with interactive prompts for task details"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Implement Task Update Operations",
|
||||
"description": "Create an 'update' command that allows modification of existing task properties. Implement options to update individual fields or enter an interactive mode for multiple updates. Ensure that updates maintain data integrity, especially for dependencies.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"acceptanceCriteria": "- 'update' command accepts a task ID and field-specific flags for quick updates"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Develop Task Deletion Functionality",
|
||||
"description": "Implement a 'delete' command to remove tasks from tasks.json. Include safeguards against deleting tasks with dependencies and provide a force option to override. Update any tasks that had the deleted task as a dependency.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"acceptanceCriteria": "- 'delete' command removes the specified task from tasks.json"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Implement Task Status Management",
|
||||
"description": "Create a 'status' command to change the status of tasks (pending/done/deferred). Implement logic to handle status changes, including updating dependent tasks if necessary. Add a batch mode for updating multiple task statuses at once.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"acceptanceCriteria": "- 'status' command changes task status correctly in tasks.json"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Develop Task Dependency and Priority Management",
|
||||
"description": "Implement 'dependency' and 'priority' commands to manage task relationships and importance. Create functions to add/remove dependencies and change priorities. Ensure the system prevents circular dependencies and maintains consistent priority levels.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"acceptanceCriteria": "- 'dependency' command can add or remove task dependencies"
|
||||
}
|
||||
]
|
||||
"subtasks": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
@@ -653,7 +573,6 @@
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
6
|
||||
@@ -729,7 +648,6 @@
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"priority": "medium",
|
||||
@@ -862,7 +780,6 @@
|
||||
"description": "Document and enhance existing agent interaction patterns through Cursor rules and dev.js commands.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
2,
|
||||
14
|
||||
],
|
||||
"priority": "medium",
|
||||
@@ -935,8 +852,7 @@
|
||||
"description": "Implement robust configuration handling with environment variables and .env files.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2
|
||||
1
|
||||
],
|
||||
"priority": "high",
|
||||
"details": "Build configuration management including:\n- Environment variable handling\n- .env file support\n- Configuration validation\n- Sensible defaults with overrides\n- Create .env.example template\n- Add configuration documentation\n- Implement secure handling of API keys",
|
||||
@@ -1017,7 +933,6 @@
|
||||
"description": "Create a flexible logging system with configurable levels and output formats.",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
2,
|
||||
16
|
||||
],
|
||||
"priority": "medium",
|
||||
@@ -1082,7 +997,6 @@
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
@@ -1166,7 +1080,6 @@
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
9,
|
||||
@@ -1469,7 +1382,7 @@
|
||||
"id": 25,
|
||||
"title": "Implement 'add-subtask' Command for Task Hierarchy Management",
|
||||
"description": "Create a command-line interface command that allows users to manually add subtasks to existing tasks, establishing a parent-child relationship between tasks.",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
3
|
||||
],
|
||||
@@ -1483,7 +1396,7 @@
|
||||
"description": "Modify the task data structure to support hierarchical relationships between tasks",
|
||||
"dependencies": [],
|
||||
"details": "1. Examine the current task data structure in scripts/modules/task-manager.js\n2. Add a 'parentId' field to the task object schema to reference parent tasks\n3. Add a 'subtasks' array field to store references to child tasks\n4. Update any relevant validation functions to account for these new fields\n5. Ensure serialization and deserialization of tasks properly handles these new fields\n6. Update the storage mechanism to persist these relationships\n7. Test by manually creating tasks with parent-child relationships and verifying they're saved correctly\n8. Write unit tests to verify the updated data model works as expected",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"parentTaskId": 25
|
||||
},
|
||||
{
|
||||
@@ -1494,7 +1407,7 @@
|
||||
1
|
||||
],
|
||||
"details": "1. Create a new addSubtask function in scripts/modules/task-manager.js\n2. Implement logic to validate that the parent task exists\n3. Add functionality to handle both creating new subtasks and converting existing tasks\n4. For new subtasks: collect task information and create a new task with parentId set\n5. For existing tasks: validate it's not already a subtask and update its parentId\n6. Add validation to prevent circular dependencies (a task cannot be a subtask of its own subtask)\n7. Update the parent task's subtasks array\n8. Ensure proper error handling with descriptive error messages\n9. Export the function for use by the command handler\n10. Write unit tests to verify all scenarios (new subtask, converting task, error cases)",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"parentTaskId": 25
|
||||
},
|
||||
{
|
||||
@@ -1505,7 +1418,7 @@
|
||||
2
|
||||
],
|
||||
"details": "1. Add a new command registration in scripts/modules/commands.js following existing patterns\n2. Define command syntax: 'add-subtask <parentId> [--task-id=<taskId> | --title=<title>]'\n3. Implement command handler that calls the addSubtask function from task-manager.js\n4. Add interactive prompts to collect required information when not provided as arguments\n5. Implement validation for command arguments\n6. Add appropriate success and error messages\n7. Document the command syntax and options in the help system\n8. Test the command with various input combinations\n9. Ensure the command follows the same patterns as other commands like add-dependency",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"parentTaskId": 25
|
||||
},
|
||||
{
|
||||
@@ -1517,7 +1430,7 @@
|
||||
3
|
||||
],
|
||||
"details": "1. Create a test file in tests/unit/ directory for the add-subtask functionality\n2. Write tests for the addSubtask function in task-manager.js\n3. Test all key scenarios: adding new subtasks, converting existing tasks to subtasks\n4. Test error cases: non-existent parent task, circular dependencies, invalid input\n5. Use Jest mocks to isolate the function from file system operations\n6. Test the command handler in isolation using mock functions\n7. Ensure test coverage for all branches and edge cases\n8. Document the testing approach for future reference",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"parentTaskId": 25
|
||||
},
|
||||
{
|
||||
@@ -1529,7 +1442,7 @@
|
||||
3
|
||||
],
|
||||
"details": "1. Create a removeSubtask function in scripts/modules/task-manager.js\n2. Implement logic to validate the subtask exists and is actually a subtask\n3. Add options to either delete the subtask completely or convert it to a standalone task\n4. Update the parent task's subtasks array to remove the reference\n5. If converting to standalone task, clear the parentId reference\n6. Implement the remove-subtask command in scripts/modules/commands.js following patterns from add-subtask\n7. Add appropriate validation and error messages\n8. Document the command in the help system\n9. Export the function in task-manager.js\n10. Ensure proper error handling for all scenarios",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"parentTaskId": 25
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,6 +19,7 @@ const mockFormatDependenciesWithStatus = jest.fn();
|
||||
const mockValidateAndFixDependencies = jest.fn();
|
||||
const mockReadJSON = jest.fn();
|
||||
const mockLog = jest.fn();
|
||||
const mockIsTaskDependentOn = jest.fn().mockReturnValue(false);
|
||||
|
||||
// Mock fs module
|
||||
jest.mock('fs', () => ({
|
||||
@@ -66,79 +67,8 @@ jest.mock('../../scripts/modules/task-manager.js', () => {
|
||||
// Return a modified module with our custom implementation of generateTaskFiles
|
||||
return {
|
||||
...originalModule,
|
||||
generateTaskFiles: (tasksPath, outputDir) => {
|
||||
try {
|
||||
const data = mockReadJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Create the output directory if it doesn't exist
|
||||
if (!mockExistsSync(outputDir)) {
|
||||
mockMkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Validate and fix dependencies before generating files
|
||||
mockValidateAndFixDependencies(data, tasksPath);
|
||||
|
||||
// Generate task files
|
||||
data.tasks.forEach(task => {
|
||||
const taskPath = `${outputDir}/task_${task.id.toString().padStart(3, '0')}.txt`;
|
||||
|
||||
// Format the content
|
||||
let content = `# Task ID: ${task.id}\n`;
|
||||
content += `# Title: ${task.title}\n`;
|
||||
content += `# Status: ${task.status || 'pending'}\n`;
|
||||
|
||||
// Format dependencies with their status
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
content += `# Dependencies: ${mockFormatDependenciesWithStatus(task.dependencies, data.tasks, false)}\n`;
|
||||
} else {
|
||||
content += '# Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `# Priority: ${task.priority || 'medium'}\n`;
|
||||
content += `# Description: ${task.description || ''}\n`;
|
||||
|
||||
// Add more detailed sections
|
||||
content += '# Details:\n';
|
||||
content += (task.details || '').split('\n').map(line => line).join('\n');
|
||||
content += '\n\n';
|
||||
|
||||
content += '# Test Strategy:\n';
|
||||
content += (task.testStrategy || '').split('\n').map(line => line).join('\n');
|
||||
content += '\n';
|
||||
|
||||
// Add subtasks if they exist
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
content += '\n# Subtasks:\n';
|
||||
|
||||
task.subtasks.forEach(subtask => {
|
||||
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
|
||||
|
||||
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
||||
const subtaskDeps = subtask.dependencies.join(', ');
|
||||
content += `### Dependencies: ${subtaskDeps}\n`;
|
||||
} else {
|
||||
content += '### Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `### Description: ${subtask.description || ''}\n`;
|
||||
content += '### Details:\n';
|
||||
content += (subtask.details || '').split('\n').map(line => line).join('\n');
|
||||
content += '\n\n';
|
||||
});
|
||||
}
|
||||
|
||||
// Write the file
|
||||
mockWriteFileSync(taskPath, content);
|
||||
});
|
||||
} catch (error) {
|
||||
mockLog('error', `Error generating task files: ${error.message}`);
|
||||
console.error(`Error generating task files: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
generateTaskFiles: mockGenerateTaskFiles,
|
||||
isTaskDependentOn: mockIsTaskDependentOn
|
||||
};
|
||||
});
|
||||
|
||||
@@ -166,9 +96,10 @@ const testParsePRD = async (prdPath, outputPath, numTasks) => {
|
||||
// Import after mocks
|
||||
import * as taskManager from '../../scripts/modules/task-manager.js';
|
||||
import { sampleClaudeResponse } from '../fixtures/sample-claude-response.js';
|
||||
import { sampleTasks, emptySampleTasks } from '../fixtures/sample-tasks.js';
|
||||
|
||||
// Destructure the required functions for convenience
|
||||
const { findNextTask, generateTaskFiles } = taskManager;
|
||||
const { findNextTask, generateTaskFiles, clearSubtasks } = taskManager;
|
||||
|
||||
describe('Task Manager Module', () => {
|
||||
beforeEach(() => {
|
||||
@@ -833,42 +764,123 @@ describe('Task Manager Module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('clearSubtasks function', () => {
|
||||
describe('clearSubtasks function', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// Test implementation of clearSubtasks that just returns the updated data
|
||||
const testClearSubtasks = (tasksData, taskIds) => {
|
||||
// Create a deep copy of the data to avoid modifying the original
|
||||
const data = JSON.parse(JSON.stringify(tasksData));
|
||||
let clearedCount = 0;
|
||||
|
||||
// Handle multiple task IDs (comma-separated)
|
||||
const taskIdArray = taskIds.split(',').map(id => id.trim());
|
||||
|
||||
taskIdArray.forEach(taskId => {
|
||||
const id = parseInt(taskId, 10);
|
||||
if (isNaN(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const task = data.tasks.find(t => t.id === id);
|
||||
if (!task) {
|
||||
// Log error for non-existent task
|
||||
mockLog('error', `Task ${id} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task.subtasks || task.subtasks.length === 0) {
|
||||
// No subtasks to clear
|
||||
return;
|
||||
}
|
||||
|
||||
const subtaskCount = task.subtasks.length;
|
||||
delete task.subtasks;
|
||||
clearedCount++;
|
||||
});
|
||||
|
||||
return { data, clearedCount };
|
||||
};
|
||||
|
||||
test('should clear subtasks from a specific task', () => {
|
||||
// This test would verify that:
|
||||
// 1. The function reads the tasks file correctly
|
||||
// 2. It finds the target task by ID
|
||||
// 3. It clears the subtasks array
|
||||
// 4. It writes the updated tasks back to the file
|
||||
expect(true).toBe(true);
|
||||
// Create a deep copy of the sample data
|
||||
const testData = JSON.parse(JSON.stringify(sampleTasks));
|
||||
|
||||
// Execute the test function
|
||||
const { data, clearedCount } = testClearSubtasks(testData, '3');
|
||||
|
||||
// Verify results
|
||||
expect(clearedCount).toBe(1);
|
||||
|
||||
// Verify the task's subtasks were removed
|
||||
const task = data.tasks.find(t => t.id === 3);
|
||||
expect(task).toBeDefined();
|
||||
expect(task.subtasks).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should clear subtasks from multiple tasks when given comma-separated IDs', () => {
|
||||
// This test would verify that:
|
||||
// 1. The function handles comma-separated task IDs
|
||||
// 2. It clears subtasks from all specified tasks
|
||||
expect(true).toBe(true);
|
||||
// Setup data with subtasks on multiple tasks
|
||||
const testData = JSON.parse(JSON.stringify(sampleTasks));
|
||||
// Add subtasks to task 2
|
||||
testData.tasks[1].subtasks = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Test Subtask",
|
||||
description: "A test subtask",
|
||||
status: "pending",
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
|
||||
// Execute the test function
|
||||
const { data, clearedCount } = testClearSubtasks(testData, '2,3');
|
||||
|
||||
// Verify results
|
||||
expect(clearedCount).toBe(2);
|
||||
|
||||
// Verify both tasks had their subtasks cleared
|
||||
const task2 = data.tasks.find(t => t.id === 2);
|
||||
const task3 = data.tasks.find(t => t.id === 3);
|
||||
expect(task2.subtasks).toBeUndefined();
|
||||
expect(task3.subtasks).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle tasks with no subtasks', () => {
|
||||
// This test would verify that:
|
||||
// 1. The function handles tasks without subtasks gracefully
|
||||
// 2. It provides appropriate feedback
|
||||
expect(true).toBe(true);
|
||||
// Task 1 has no subtasks in the sample data
|
||||
const testData = JSON.parse(JSON.stringify(sampleTasks));
|
||||
|
||||
// Execute the test function
|
||||
const { clearedCount } = testClearSubtasks(testData, '1');
|
||||
|
||||
// Verify no tasks were cleared
|
||||
expect(clearedCount).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle non-existent task IDs', () => {
|
||||
// This test would verify that:
|
||||
// 1. The function handles non-existent task IDs gracefully
|
||||
// 2. It logs appropriate error messages
|
||||
expect(true).toBe(true);
|
||||
const testData = JSON.parse(JSON.stringify(sampleTasks));
|
||||
|
||||
// Execute the test function
|
||||
testClearSubtasks(testData, '99');
|
||||
|
||||
// Verify an error was logged
|
||||
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Task 99 not found'));
|
||||
});
|
||||
|
||||
test('should regenerate task files after clearing subtasks', () => {
|
||||
// This test would verify that:
|
||||
// 1. The function regenerates task files after clearing subtasks
|
||||
// 2. The new files reflect the changes
|
||||
expect(true).toBe(true);
|
||||
test('should handle multiple task IDs including both valid and non-existent IDs', () => {
|
||||
const testData = JSON.parse(JSON.stringify(sampleTasks));
|
||||
|
||||
// Execute the test function
|
||||
const { data, clearedCount } = testClearSubtasks(testData, '3,99');
|
||||
|
||||
// Verify results
|
||||
expect(clearedCount).toBe(1);
|
||||
expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Task 99 not found'));
|
||||
|
||||
// Verify the valid task's subtasks were removed
|
||||
const task3 = data.tasks.find(t => t.id === 3);
|
||||
expect(task3.subtasks).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -916,4 +928,525 @@ describe('Task Manager Module', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Add test suite for addSubtask function
|
||||
describe('addSubtask function', () => {
|
||||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Default mock implementations
|
||||
mockReadJSON.mockImplementation(() => ({
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
description: 'This is a parent task',
|
||||
status: 'pending',
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Existing Task',
|
||||
description: 'This is an existing task',
|
||||
status: 'pending',
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Another Task',
|
||||
description: 'This is another task',
|
||||
status: 'pending',
|
||||
dependencies: [1]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
// Setup success write response
|
||||
mockWriteJSON.mockImplementation((path, data) => {
|
||||
return data;
|
||||
});
|
||||
|
||||
// Set up default behavior for dependency check
|
||||
mockIsTaskDependentOn.mockReturnValue(false);
|
||||
});
|
||||
|
||||
test('should add a new subtask to a parent task', async () => {
|
||||
// Create new subtask data
|
||||
const newSubtaskData = {
|
||||
title: 'New Subtask',
|
||||
description: 'This is a new subtask',
|
||||
details: 'Implementation details for the subtask',
|
||||
status: 'pending',
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
// Execute the test version of addSubtask
|
||||
const newSubtask = testAddSubtask('tasks/tasks.json', 1, null, newSubtaskData, true);
|
||||
|
||||
// Verify readJSON was called with the correct path
|
||||
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
|
||||
|
||||
// Verify writeJSON was called with the correct path
|
||||
expect(mockWriteJSON).toHaveBeenCalledWith('tasks/tasks.json', expect.any(Object));
|
||||
|
||||
// Verify the subtask was created with correct data
|
||||
expect(newSubtask).toBeDefined();
|
||||
expect(newSubtask.id).toBe(1);
|
||||
expect(newSubtask.title).toBe('New Subtask');
|
||||
expect(newSubtask.parentTaskId).toBe(1);
|
||||
|
||||
// Verify generateTaskFiles was called
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should convert an existing task to a subtask', async () => {
|
||||
// Execute the test version of addSubtask to convert task 2 to a subtask of task 1
|
||||
const convertedSubtask = testAddSubtask('tasks/tasks.json', 1, 2, null, true);
|
||||
|
||||
// Verify readJSON was called with the correct path
|
||||
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
|
||||
|
||||
// Verify writeJSON was called
|
||||
expect(mockWriteJSON).toHaveBeenCalled();
|
||||
|
||||
// Verify the subtask was created with correct data
|
||||
expect(convertedSubtask).toBeDefined();
|
||||
expect(convertedSubtask.id).toBe(1);
|
||||
expect(convertedSubtask.title).toBe('Existing Task');
|
||||
expect(convertedSubtask.parentTaskId).toBe(1);
|
||||
|
||||
// Verify generateTaskFiles was called
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw an error if parent task does not exist', async () => {
|
||||
// Create new subtask data
|
||||
const newSubtaskData = {
|
||||
title: 'New Subtask',
|
||||
description: 'This is a new subtask'
|
||||
};
|
||||
|
||||
// Override mockReadJSON for this specific test case
|
||||
mockReadJSON.mockImplementationOnce(() => ({
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task 1',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
// Expect an error when trying to add a subtask to a non-existent parent
|
||||
expect(() =>
|
||||
testAddSubtask('tasks/tasks.json', 999, null, newSubtaskData)
|
||||
).toThrow(/Parent task with ID 999 not found/);
|
||||
|
||||
// Verify writeJSON was not called
|
||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw an error if existing task does not exist', async () => {
|
||||
// Expect an error when trying to convert a non-existent task
|
||||
expect(() =>
|
||||
testAddSubtask('tasks/tasks.json', 1, 999, null)
|
||||
).toThrow(/Task with ID 999 not found/);
|
||||
|
||||
// Verify writeJSON was not called
|
||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw an error if trying to create a circular dependency', async () => {
|
||||
// Force the isTaskDependentOn mock to return true for this test only
|
||||
mockIsTaskDependentOn.mockReturnValueOnce(true);
|
||||
|
||||
// Expect an error when trying to create a circular dependency
|
||||
expect(() =>
|
||||
testAddSubtask('tasks/tasks.json', 3, 1, null)
|
||||
).toThrow(/circular dependency/);
|
||||
|
||||
// Verify writeJSON was not called
|
||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not regenerate task files if generateFiles is false', async () => {
|
||||
// Create new subtask data
|
||||
const newSubtaskData = {
|
||||
title: 'New Subtask',
|
||||
description: 'This is a new subtask'
|
||||
};
|
||||
|
||||
// Execute the test version of addSubtask with generateFiles = false
|
||||
testAddSubtask('tasks/tasks.json', 1, null, newSubtaskData, false);
|
||||
|
||||
// Verify writeJSON was called
|
||||
expect(mockWriteJSON).toHaveBeenCalled();
|
||||
|
||||
// Verify task files were not regenerated
|
||||
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// Test suite for removeSubtask function
|
||||
describe('removeSubtask function', () => {
|
||||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Default mock implementations
|
||||
mockReadJSON.mockImplementation(() => ({
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
description: 'This is a parent task',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Subtask 1',
|
||||
description: 'This is subtask 1',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
parentTaskId: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Subtask 2',
|
||||
description: 'This is subtask 2',
|
||||
status: 'in-progress',
|
||||
dependencies: [1], // Depends on subtask 1
|
||||
parentTaskId: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Another Task',
|
||||
description: 'This is another task',
|
||||
status: 'pending',
|
||||
dependencies: [1]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
// Setup success write response
|
||||
mockWriteJSON.mockImplementation((path, data) => {
|
||||
return data;
|
||||
});
|
||||
});
|
||||
|
||||
test('should remove a subtask from its parent task', async () => {
|
||||
// Execute the test version of removeSubtask to remove subtask 1.1
|
||||
testRemoveSubtask('tasks/tasks.json', '1.1', false, true);
|
||||
|
||||
// Verify readJSON was called with the correct path
|
||||
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
|
||||
|
||||
// Verify writeJSON was called with updated data
|
||||
expect(mockWriteJSON).toHaveBeenCalled();
|
||||
|
||||
// Verify generateTaskFiles was called
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should convert a subtask to a standalone task', async () => {
|
||||
// Execute the test version of removeSubtask to convert subtask 1.1 to a standalone task
|
||||
const result = testRemoveSubtask('tasks/tasks.json', '1.1', true, true);
|
||||
|
||||
// Verify the result is the new task
|
||||
expect(result).toBeDefined();
|
||||
expect(result.id).toBe(3);
|
||||
expect(result.title).toBe('Subtask 1');
|
||||
expect(result.dependencies).toContain(1);
|
||||
|
||||
// Verify writeJSON was called
|
||||
expect(mockWriteJSON).toHaveBeenCalled();
|
||||
|
||||
// Verify generateTaskFiles was called
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw an error if subtask ID format is invalid', async () => {
|
||||
// Expect an error for invalid subtask ID format
|
||||
expect(() =>
|
||||
testRemoveSubtask('tasks/tasks.json', '1', false)
|
||||
).toThrow(/Invalid subtask ID format/);
|
||||
|
||||
// Verify writeJSON was not called
|
||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw an error if parent task does not exist', async () => {
|
||||
// Expect an error for non-existent parent task
|
||||
expect(() =>
|
||||
testRemoveSubtask('tasks/tasks.json', '999.1', false)
|
||||
).toThrow(/Parent task with ID 999 not found/);
|
||||
|
||||
// Verify writeJSON was not called
|
||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should throw an error if subtask does not exist', async () => {
|
||||
// Expect an error for non-existent subtask
|
||||
expect(() =>
|
||||
testRemoveSubtask('tasks/tasks.json', '1.999', false)
|
||||
).toThrow(/Subtask 1.999 not found/);
|
||||
|
||||
// Verify writeJSON was not called
|
||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should remove subtasks array if last subtask is removed', async () => {
|
||||
// Create a data object with just one subtask
|
||||
mockReadJSON.mockImplementationOnce(() => ({
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
description: 'This is a parent task',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Last Subtask',
|
||||
description: 'This is the last subtask',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
parentTaskId: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Another Task',
|
||||
description: 'This is another task',
|
||||
status: 'pending',
|
||||
dependencies: [1]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
// Mock the behavior of writeJSON to capture the updated tasks data
|
||||
const updatedTasksData = { tasks: [] };
|
||||
mockWriteJSON.mockImplementation((path, data) => {
|
||||
// Store the data for assertions
|
||||
updatedTasksData.tasks = [...data.tasks];
|
||||
return data;
|
||||
});
|
||||
|
||||
// Remove the last subtask
|
||||
testRemoveSubtask('tasks/tasks.json', '1.1', false, true);
|
||||
|
||||
// Verify writeJSON was called
|
||||
expect(mockWriteJSON).toHaveBeenCalled();
|
||||
|
||||
// Verify the subtasks array was removed completely
|
||||
const parentTask = updatedTasksData.tasks.find(t => t.id === 1);
|
||||
expect(parentTask).toBeDefined();
|
||||
expect(parentTask.subtasks).toBeUndefined();
|
||||
|
||||
// Verify generateTaskFiles was called
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not regenerate task files if generateFiles is false', async () => {
|
||||
// Execute the test version of removeSubtask with generateFiles = false
|
||||
testRemoveSubtask('tasks/tasks.json', '1.1', false, false);
|
||||
|
||||
// Verify writeJSON was called
|
||||
expect(mockWriteJSON).toHaveBeenCalled();
|
||||
|
||||
// Verify task files were not regenerated
|
||||
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Define test versions of the addSubtask and removeSubtask functions
|
||||
const testAddSubtask = (tasksPath, parentId, existingTaskId, newSubtaskData, generateFiles = true) => {
|
||||
// Read the existing tasks
|
||||
const data = mockReadJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Convert parent ID to number
|
||||
const parentIdNum = parseInt(parentId, 10);
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find(t => t.id === parentIdNum);
|
||||
if (!parentTask) {
|
||||
throw new Error(`Parent task with ID ${parentIdNum} not found`);
|
||||
}
|
||||
|
||||
// Initialize subtasks array if it doesn't exist
|
||||
if (!parentTask.subtasks) {
|
||||
parentTask.subtasks = [];
|
||||
}
|
||||
|
||||
let newSubtask;
|
||||
|
||||
// Case 1: Convert an existing task to a subtask
|
||||
if (existingTaskId !== null) {
|
||||
const existingTaskIdNum = parseInt(existingTaskId, 10);
|
||||
|
||||
// Find the existing task
|
||||
const existingTaskIndex = data.tasks.findIndex(t => t.id === existingTaskIdNum);
|
||||
if (existingTaskIndex === -1) {
|
||||
throw new Error(`Task with ID ${existingTaskIdNum} not found`);
|
||||
}
|
||||
|
||||
const existingTask = data.tasks[existingTaskIndex];
|
||||
|
||||
// Check if task is already a subtask
|
||||
if (existingTask.parentTaskId) {
|
||||
throw new Error(`Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}`);
|
||||
}
|
||||
|
||||
// Check for circular dependency
|
||||
if (existingTaskIdNum === parentIdNum) {
|
||||
throw new Error(`Cannot make a task a subtask of itself`);
|
||||
}
|
||||
|
||||
// Check for circular dependency using mockIsTaskDependentOn
|
||||
if (mockIsTaskDependentOn()) {
|
||||
throw new Error(`Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}`);
|
||||
}
|
||||
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId = parentTask.subtasks.length > 0
|
||||
? Math.max(...parentTask.subtasks.map(st => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
// Clone the existing task to be converted to a subtask
|
||||
newSubtask = { ...existingTask, id: newSubtaskId, parentTaskId: parentIdNum };
|
||||
|
||||
// Add to parent's subtasks
|
||||
parentTask.subtasks.push(newSubtask);
|
||||
|
||||
// Remove the task from the main tasks array
|
||||
data.tasks.splice(existingTaskIndex, 1);
|
||||
}
|
||||
// Case 2: Create a new subtask
|
||||
else if (newSubtaskData) {
|
||||
// Find the highest subtask ID to determine the next ID
|
||||
const highestSubtaskId = parentTask.subtasks.length > 0
|
||||
? Math.max(...parentTask.subtasks.map(st => st.id))
|
||||
: 0;
|
||||
const newSubtaskId = highestSubtaskId + 1;
|
||||
|
||||
// Create the new subtask object
|
||||
newSubtask = {
|
||||
id: newSubtaskId,
|
||||
title: newSubtaskData.title,
|
||||
description: newSubtaskData.description || '',
|
||||
details: newSubtaskData.details || '',
|
||||
status: newSubtaskData.status || 'pending',
|
||||
dependencies: newSubtaskData.dependencies || [],
|
||||
parentTaskId: parentIdNum
|
||||
};
|
||||
|
||||
// Add to parent's subtasks
|
||||
parentTask.subtasks.push(newSubtask);
|
||||
} else {
|
||||
throw new Error('Either existingTaskId or newSubtaskData must be provided');
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
mockWriteJSON(tasksPath, data);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
mockGenerateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
}
|
||||
|
||||
return newSubtask;
|
||||
};
|
||||
|
||||
const testRemoveSubtask = (tasksPath, subtaskId, convertToTask = false, generateFiles = true) => {
|
||||
// Read the existing tasks
|
||||
const data = mockReadJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Parse the subtask ID (format: "parentId.subtaskId")
|
||||
if (!subtaskId.includes('.')) {
|
||||
throw new Error(`Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"`);
|
||||
}
|
||||
|
||||
const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
|
||||
const parentId = parseInt(parentIdStr, 10);
|
||||
const subtaskIdNum = parseInt(subtaskIdStr, 10);
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find(t => t.id === parentId);
|
||||
if (!parentTask) {
|
||||
throw new Error(`Parent task with ID ${parentId} not found`);
|
||||
}
|
||||
|
||||
// Check if parent has subtasks
|
||||
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
|
||||
throw new Error(`Parent task ${parentId} has no subtasks`);
|
||||
}
|
||||
|
||||
// Find the subtask to remove
|
||||
const subtaskIndex = parentTask.subtasks.findIndex(st => st.id === subtaskIdNum);
|
||||
if (subtaskIndex === -1) {
|
||||
throw new Error(`Subtask ${subtaskId} not found`);
|
||||
}
|
||||
|
||||
// Get a copy of the subtask before removing it
|
||||
const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };
|
||||
|
||||
// Remove the subtask from the parent
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
// If parent has no more subtasks, remove the subtasks array
|
||||
if (parentTask.subtasks.length === 0) {
|
||||
delete parentTask.subtasks;
|
||||
}
|
||||
|
||||
let convertedTask = null;
|
||||
|
||||
// Convert the subtask to a standalone task if requested
|
||||
if (convertToTask) {
|
||||
// Find the highest task ID to determine the next ID
|
||||
const highestId = Math.max(...data.tasks.map(t => t.id));
|
||||
const newTaskId = highestId + 1;
|
||||
|
||||
// Create the new task from the subtask
|
||||
convertedTask = {
|
||||
id: newTaskId,
|
||||
title: removedSubtask.title,
|
||||
description: removedSubtask.description || '',
|
||||
details: removedSubtask.details || '',
|
||||
status: removedSubtask.status || 'pending',
|
||||
dependencies: removedSubtask.dependencies || [],
|
||||
priority: parentTask.priority || 'medium' // Inherit priority from parent
|
||||
};
|
||||
|
||||
// Add the parent task as a dependency if not already present
|
||||
if (!convertedTask.dependencies.includes(parentId)) {
|
||||
convertedTask.dependencies.push(parentId);
|
||||
}
|
||||
|
||||
// Add the converted task to the tasks array
|
||||
data.tasks.push(convertedTask);
|
||||
}
|
||||
|
||||
// Write the updated tasks back to the file
|
||||
mockWriteJSON(tasksPath, data);
|
||||
|
||||
// Generate task files if requested
|
||||
if (generateFiles) {
|
||||
mockGenerateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
}
|
||||
|
||||
return convertedTask;
|
||||
};
|
||||
Reference in New Issue
Block a user