488 lines
17 KiB
JavaScript

/**
* commands.js
* Command-line interface for the Task Master CLI
*/
import { program } from 'commander';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import fs from 'fs';
import { CONFIG, log, readJSON } from './utils.js';
import {
parsePRD,
updateTasks,
generateTaskFiles,
setTaskStatus,
listTasks,
expandTask,
expandAllTasks,
clearSubtasks,
addTask,
analyzeTaskComplexity
} from './task-manager.js';
import {
addDependency,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand
} from './dependency-manager.js';
import {
displayBanner,
displayHelp,
displayNextTask,
displayTaskById,
displayComplexityReport,
} from './ui.js';
/**
* Configure and register CLI commands
* @param {Object} program - Commander program instance
*/
function registerCommands(programInstance) {
// Default help
programInstance.on('--help', function() {
displayHelp();
});
// parse-prd command
programInstance
.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(async (file, options) => {
if (!file) {
console.log(chalk.yellow('No PRD file specified.'));
console.log(boxen(
chalk.white.bold('Parse PRD Help') + '\n\n' +
chalk.cyan('Usage:') + '\n' +
` task-master parse-prd <prd-file.txt> [options]\n\n` +
chalk.cyan('Options:') + '\n' +
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n\n' +
chalk.cyan('Example:') + '\n' +
' task-master parse-prd requirements.txt --num-tasks 15\n\n' +
chalk.yellow('Note: This command will generate tasks from a PRD document and will overwrite any existing tasks.json file.'),
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
));
return;
}
const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
console.log(chalk.blue(`Parsing PRD file: ${file}`));
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
await parsePRD(file, outputPath, numTasks);
});
// update command
programInstance
.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 (tasks with ID >= this value will be updated)', '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(async (options) => {
const tasksPath = options.file;
const fromId = parseInt(options.from, 10);
const prompt = options.prompt;
const useResearch = options.research || false;
if (!prompt) {
console.error(chalk.red('Error: --prompt parameter is required. Please provide information about the changes.'));
process.exit(1);
}
console.log(chalk.blue(`Updating tasks from ID >= ${fromId} with prompt: "${prompt}"`));
console.log(chalk.blue(`Tasks file: ${tasksPath}`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed task updates'));
}
await updateTasks(tasksPath, fromId, prompt, useResearch);
});
// generate command
programInstance
.command('generate')
.description('Generate task files from tasks.json')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-o, --output <dir>', 'Output directory', 'tasks')
.action(async (options) => {
const tasksPath = options.file;
const outputDir = options.output;
console.log(chalk.blue(`Generating task files from: ${tasksPath}`));
console.log(chalk.blue(`Output directory: ${outputDir}`));
await generateTaskFiles(tasksPath, outputDir);
});
// set-status command
programInstance
.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(async (options) => {
const tasksPath = options.file;
const taskId = options.id;
const status = options.status;
if (!taskId || !status) {
console.error(chalk.red('Error: Both --id and --status are required'));
process.exit(1);
}
console.log(chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`));
await setTaskStatus(tasksPath, taskId, status);
});
// list command
programInstance
.command('list')
.description('List all tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-s, --status <status>', 'Filter by status')
.option('--with-subtasks', 'Show subtasks for each task')
.action(async (options) => {
const tasksPath = options.file;
const statusFilter = options.status;
const withSubtasks = options.withSubtasks || false;
console.log(chalk.blue(`Listing tasks from: ${tasksPath}`));
if (statusFilter) {
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
}
if (withSubtasks) {
console.log(chalk.blue('Including subtasks in listing'));
}
await listTasks(tasksPath, statusFilter, withSubtasks);
});
// expand command
programInstance
.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', CONFIG.defaultSubtasks.toString())
.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(async (options) => {
const tasksPath = options.file;
const idArg = options.id ? parseInt(options.id, 10) : null;
const allFlag = options.all;
const numSubtasks = parseInt(options.num, 10);
const forceFlag = options.force;
const useResearch = options.research === true;
const additionalContext = options.prompt || '';
// Debug log to verify the value
log('debug', `Research enabled: ${useResearch}`);
if (allFlag) {
console.log(chalk.blue(`Expanding all tasks with ${numSubtasks} subtasks each...`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
} else {
console.log(chalk.yellow('Research-backed subtask generation disabled'));
}
if (additionalContext) {
console.log(chalk.blue(`Additional context: "${additionalContext}"`));
}
await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag);
} else if (idArg) {
console.log(chalk.blue(`Expanding task ${idArg} with ${numSubtasks} subtasks...`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
} else {
console.log(chalk.yellow('Research-backed subtask generation disabled'));
}
if (additionalContext) {
console.log(chalk.blue(`Additional context: "${additionalContext}"`));
}
await expandTask(idArg, numSubtasks, useResearch, additionalContext);
} else {
console.error(chalk.red('Error: Please specify a task ID with --id=<id> or use --all to expand all tasks.'));
}
});
// analyze-complexity command
programInstance
.command('analyze-complexity')
.description(`Analyze tasks and generate expansion recommendations${chalk.reset('')}`)
.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 (defaults to configured model)')
.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(async (options) => {
const tasksPath = options.file || 'tasks/tasks.json';
const outputPath = options.output;
const modelOverride = options.model;
const thresholdScore = parseFloat(options.threshold);
const useResearch = options.research || false;
console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`));
console.log(chalk.blue(`Output report will be saved to: ${outputPath}`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed complexity analysis'));
}
await analyzeTaskComplexity(options);
});
// clear-subtasks command
programInstance
.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(async (options) => {
const tasksPath = options.file;
const taskIds = options.id;
const all = options.all;
if (!taskIds && !all) {
console.error(chalk.red('Error: Please specify task IDs with --id=<ids> or use --all to clear all tasks'));
process.exit(1);
}
if (all) {
// If --all is specified, get all task IDs
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
console.error(chalk.red('Error: No valid tasks found'));
process.exit(1);
}
const allIds = data.tasks.map(t => t.id).join(',');
clearSubtasks(tasksPath, allIds);
} else {
clearSubtasks(tasksPath, taskIds);
}
});
// add-task command
programInstance
.command('add-task')
.description('Add a new task 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(async (options) => {
const tasksPath = options.file;
const prompt = options.prompt;
const dependencies = options.dependencies ? options.dependencies.split(',').map(id => parseInt(id.trim(), 10)) : [];
const priority = options.priority;
if (!prompt) {
console.error(chalk.red('Error: --prompt parameter is required. Please provide a task description.'));
process.exit(1);
}
console.log(chalk.blue(`Adding new task with description: "${prompt}"`));
console.log(chalk.blue(`Dependencies: ${dependencies.length > 0 ? dependencies.join(', ') : 'None'}`));
console.log(chalk.blue(`Priority: ${priority}`));
await addTask(tasksPath, prompt, dependencies, priority);
});
// next command
programInstance
.command('next')
.description(`Show the next task to work on based on dependencies and status${chalk.reset('')}`)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
await displayNextTask(tasksPath);
});
// show command
programInstance
.command('show')
.description(`Display detailed information about a specific task${chalk.reset('')}`)
.argument('[id]', 'Task ID to show')
.option('-i, --id <id>', 'Task ID to show')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (taskId, options) => {
const idArg = taskId || options.id;
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
process.exit(1);
}
const tasksPath = options.file;
await displayTaskById(tasksPath, idArg);
});
// add-dependency command
programInstance
.command('add-dependency')
.description('Add a dependency to a task')
.option('-i, --id <id>', 'Task ID to add dependency to')
.option('-d, --depends-on <id>', 'Task ID that will become a dependency')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
const taskId = options.id;
const dependencyId = options.dependsOn;
if (!taskId || !dependencyId) {
console.error(chalk.red('Error: Both --id and --depends-on are required'));
process.exit(1);
}
await addDependency(tasksPath, parseInt(taskId, 10), parseInt(dependencyId, 10));
});
// remove-dependency command
programInstance
.command('remove-dependency')
.description('Remove a dependency from a task')
.option('-i, --id <id>', 'Task ID to remove dependency from')
.option('-d, --depends-on <id>', 'Task ID to remove as a dependency')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
const taskId = options.id;
const dependencyId = options.dependsOn;
if (!taskId || !dependencyId) {
console.error(chalk.red('Error: Both --id and --depends-on are required'));
process.exit(1);
}
await removeDependency(tasksPath, parseInt(taskId, 10), parseInt(dependencyId, 10));
});
// validate-dependencies command
programInstance
.command('validate-dependencies')
.description(`Identify invalid dependencies without fixing them${chalk.reset('')}`)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
await validateDependenciesCommand(options.file);
});
// fix-dependencies command
programInstance
.command('fix-dependencies')
.description(`Fix invalid dependencies automatically${chalk.reset('')}`)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
await fixDependenciesCommand(options.file);
});
// complexity-report command
programInstance
.command('complexity-report')
.description(`Display the complexity analysis report${chalk.reset('')}`)
.option('-f, --file <file>', 'Path to the report file', 'scripts/task-complexity-report.json')
.action(async (options) => {
await displayComplexityReport(options.file);
});
// Add more commands as needed...
return programInstance;
}
/**
* Setup the CLI application
* @returns {Object} Configured Commander program
*/
function setupCLI() {
// Create a new program instance
const programInstance = program
.name('dev')
.description('AI-driven development task management')
.version(() => {
// Read version directly from package.json
try {
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}
} catch (error) {
// Silently fall back to default version
}
return CONFIG.projectVersion; // Default fallback
})
.helpOption('-h, --help', 'Display help')
.addHelpCommand(false) // Disable default help command
.on('--help', () => {
displayHelp(); // Use your custom help display instead
})
.on('-h', () => {
displayHelp();
process.exit(0);
});
// Modify the help option to use your custom display
programInstance.helpInformation = () => {
displayHelp();
return '';
};
// Register commands
registerCommands(programInstance);
return programInstance;
}
/**
* Parse arguments and run the CLI
* @param {Array} argv - Command-line arguments
*/
async function runCLI(argv = process.argv) {
try {
// Display banner if not in a pipe
if (process.stdout.isTTY) {
displayBanner();
}
// If no arguments provided, show help
if (argv.length <= 2) {
displayHelp();
process.exit(0);
}
// Setup and parse
const programInstance = setupCLI();
await programInstance.parseAsync(argv);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
if (CONFIG.debug) {
console.error(error);
}
process.exit(1);
}
}
export {
registerCommands,
setupCLI,
runCLI
};