/** * 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 ', 'Output file path', 'tasks/tasks.json') .option('-n, --num-tasks ', '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 [options]\n\n` + chalk.cyan('Options:') + '\n' + ' -o, --output Output file path (default: "tasks/tasks.json")\n' + ' -n, --num-tasks 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 ', 'Path to the tasks file', 'tasks/tasks.json') .option('--from ', 'Task ID to start updating from (tasks with ID >= this value will be updated)', '1') .option('-p, --prompt ', '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 ', 'Path to the tasks file', 'tasks/tasks.json') .option('-o, --output ', '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 ', 'Task ID (can be comma-separated for multiple tasks)') .option('-s, --status ', 'New status (todo, in-progress, review, done)') .option('-f, --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 ', 'Path to the tasks file', 'tasks/tasks.json') .option('-s, --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 ', 'Path to the tasks file', 'tasks/tasks.json') .option('-i, --id ', 'Task ID to expand') .option('-a, --all', 'Expand all tasks') .option('-n, --num ', 'Number of subtasks to generate', CONFIG.defaultSubtasks.toString()) .option('--research', 'Enable Perplexity AI for research-backed subtask generation') .option('-p, --prompt ', '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= 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 ', 'Output file path for the report', 'scripts/task-complexity-report.json') .option('-m, --model ', 'LLM model to use for analysis (defaults to configured model)') .option('-t, --threshold ', 'Minimum complexity score to recommend expansion (1-10)', '5') .option('-f, --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 ', 'Path to the tasks file', 'tasks/tasks.json') .option('-i, --id ', '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= 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 ', 'Path to the tasks file', 'tasks/tasks.json') .option('-p, --prompt ', 'Description of the task to add (required)') .option('-d, --dependencies ', 'Comma-separated list of task IDs this task depends on') .option('--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 ', '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 ', 'Task ID to show') .option('-f, --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 ', 'Task ID to add dependency to') .option('-d, --depends-on ', 'Task ID that will become a dependency') .option('-f, --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 ', 'Task ID to remove dependency from') .option('-d, --depends-on ', 'Task ID to remove as a dependency') .option('-f, --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 ', '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 ', '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 ', '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 };