mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-03 07:04:28 +00:00

Refactors the `expandTask` and `expandAllTasks` features to complete subtask 61.38 and enhance functionality based on subtask 61.37's refactor. Key Changes: - **Additive Expansion (`expandTask`, `expandAllTasks`):** - Modified `expandTask` default behavior to append newly generated subtasks to any existing ones. - Added a `force` flag (passed down from CLI/MCP via `--force` option/parameter) to `expandTask` and `expandAllTasks`. When `force` is true, existing subtasks are cleared before generating new ones. - Updated relevant CLI command (`expand`), MCP tool (`expand_task`, `expand_all`), and direct function wrappers (`expandTaskDirect`, `expandAllTasksDirect`) to handle and pass the `force` flag. - **Complexity Report Integration (`expandTask`):** - `expandTask` now reads `scripts/task-complexity-report.json`. - If an analysis entry exists for the target task: - `recommendedSubtasks` is used to determine the number of subtasks to generate (unless `--num` is explicitly provided). - `expansionPrompt` is used as the primary prompt content for the AI. - `reasoning` is appended to any additional context provided. - If no report entry exists or the report is missing, it falls back to default subtask count (from config) and standard prompt generation. - **`expandAllTasks` Orchestration:** - Refactored `expandAllTasks` to primarily iterate through eligible tasks (pending/in-progress, considering `force` flag and existing subtasks) and call the updated `expandTask` function for each. - Removed redundant logic (like complexity reading or explicit subtask clearing) now handled within `expandTask`. - Ensures correct context (`session`, `mcpLog`) and flags (`useResearch`, `force`) are passed down. - **Configuration & Cleanup:** - Updated `.cursor/mcp.json` with new Perplexity/Anthropic API keys (old ones invalidated). - Completed refactoring of `expandTask` started in 61.37, confirming usage of `generateTextService` and appropriate prompts. - **Task Management:** - Marked subtask 61.37 as complete. - Updated `.changeset/cuddly-zebras-matter.md` to reflect user-facing changes. These changes finalize the refactoring of the task expansion features, making them more robust, configurable via complexity analysis, and aligned with the unified AI service architecture.
2465 lines
71 KiB
JavaScript
2465 lines
71 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 https from 'https';
|
|
import inquirer from 'inquirer';
|
|
import Table from 'cli-table3';
|
|
import { exec } from 'child_process';
|
|
import readline from 'readline';
|
|
|
|
import { log, readJSON } from './utils.js';
|
|
import {
|
|
parsePRD,
|
|
updateTasks,
|
|
generateTaskFiles,
|
|
setTaskStatus,
|
|
listTasks,
|
|
expandTask,
|
|
expandAllTasks,
|
|
clearSubtasks,
|
|
addTask,
|
|
addSubtask,
|
|
removeSubtask,
|
|
analyzeTaskComplexity,
|
|
updateTaskById,
|
|
updateSubtaskById,
|
|
removeTask,
|
|
findTaskById,
|
|
taskExists
|
|
} from './task-manager.js';
|
|
|
|
import {
|
|
addDependency,
|
|
removeDependency,
|
|
validateDependenciesCommand,
|
|
fixDependenciesCommand
|
|
} from './dependency-manager.js';
|
|
|
|
import {
|
|
getMainModelId,
|
|
getResearchModelId,
|
|
getFallbackModelId,
|
|
getAvailableModels,
|
|
VALID_PROVIDERS,
|
|
getMainProvider,
|
|
getResearchProvider,
|
|
getFallbackProvider,
|
|
isApiKeySet,
|
|
getMcpApiKeyStatus,
|
|
getDebugFlag,
|
|
getConfig,
|
|
writeConfig,
|
|
ConfigurationError // Import the custom error
|
|
} from './config-manager.js';
|
|
|
|
import {
|
|
displayBanner,
|
|
displayHelp,
|
|
displayNextTask,
|
|
displayTaskById,
|
|
displayComplexityReport,
|
|
getStatusWithColor,
|
|
confirmTaskOverwrite,
|
|
startLoadingIndicator,
|
|
stopLoadingIndicator
|
|
} from './ui.js';
|
|
|
|
import { initializeProject } from '../init.js';
|
|
import {
|
|
getModelConfiguration,
|
|
getAvailableModelsList,
|
|
setModel
|
|
} from './task-manager/models.js'; // Import new core functions
|
|
import { findProjectRoot } from './utils.js';
|
|
|
|
/**
|
|
* Configure and register CLI commands
|
|
* @param {Object} program - Commander program instance
|
|
*/
|
|
function registerCommands(programInstance) {
|
|
// Add global error handler for unknown options
|
|
programInstance.on('option:unknown', function (unknownOption) {
|
|
const commandName = this._name || 'unknown';
|
|
console.error(chalk.red(`Error: Unknown option '${unknownOption}'`));
|
|
console.error(
|
|
chalk.yellow(
|
|
`Run 'task-master ${commandName} --help' to see available options`
|
|
)
|
|
);
|
|
process.exit(1);
|
|
});
|
|
|
|
// 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(
|
|
'-i, --input <file>',
|
|
'Path to the PRD file (alternative to positional argument)'
|
|
)
|
|
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
|
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
|
.option('-f, --force', 'Skip confirmation when overwriting existing tasks')
|
|
.action(async (file, options) => {
|
|
// Use input option if file argument not provided
|
|
const inputFile = file || options.input;
|
|
const defaultPrdPath = 'scripts/prd.txt';
|
|
const numTasks = parseInt(options.numTasks, 10);
|
|
const outputPath = options.output;
|
|
const force = options.force || false;
|
|
|
|
// Helper function to check if tasks.json exists and confirm overwrite
|
|
async function confirmOverwriteIfNeeded() {
|
|
if (fs.existsSync(outputPath) && !force) {
|
|
const shouldContinue = await confirmTaskOverwrite(outputPath);
|
|
if (!shouldContinue) {
|
|
console.log(chalk.yellow('Operation cancelled by user.'));
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// If no input file specified, check for default PRD location
|
|
if (!inputFile) {
|
|
if (fs.existsSync(defaultPrdPath)) {
|
|
console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`));
|
|
|
|
// Check for existing tasks.json before proceeding
|
|
if (!(await confirmOverwriteIfNeeded())) return;
|
|
|
|
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
|
await parsePRD(defaultPrdPath, outputPath, numTasks);
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
chalk.yellow(
|
|
'No PRD file specified and default PRD file not found at scripts/prd.txt.'
|
|
)
|
|
);
|
|
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' +
|
|
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
|
|
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
|
|
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
|
|
' -f, --force Skip confirmation when overwriting existing tasks\n\n' +
|
|
chalk.cyan('Example:') +
|
|
'\n' +
|
|
' task-master parse-prd requirements.txt --num-tasks 15\n' +
|
|
' task-master parse-prd --input=requirements.txt\n' +
|
|
' task-master parse-prd --force\n\n' +
|
|
chalk.yellow('Note: This command will:') +
|
|
'\n' +
|
|
' 1. Look for a PRD file at scripts/prd.txt by default\n' +
|
|
' 2. Use the file specified by --input or positional argument if provided\n' +
|
|
' 3. Generate tasks from the PRD and overwrite any existing tasks.json file',
|
|
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check for existing tasks.json before proceeding with specified input file
|
|
if (!(await confirmOverwriteIfNeeded())) return;
|
|
|
|
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
|
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
|
|
|
await parsePRD(inputFile, outputPath, numTasks);
|
|
});
|
|
|
|
// update command
|
|
programInstance
|
|
.command('update')
|
|
.description(
|
|
'Update multiple tasks with ID >= "from" 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;
|
|
|
|
// Check if there's an 'id' option which is a common mistake (instead of 'from')
|
|
if (
|
|
process.argv.includes('--id') ||
|
|
process.argv.some((arg) => arg.startsWith('--id='))
|
|
) {
|
|
console.error(
|
|
chalk.red('Error: The update command uses --from=<id>, not --id=<id>')
|
|
);
|
|
console.log(chalk.yellow('\nTo update multiple tasks:'));
|
|
console.log(
|
|
` task-master update --from=${fromId} --prompt="Your prompt here"`
|
|
);
|
|
console.log(
|
|
chalk.yellow(
|
|
'\nTo update a single specific task, use the update-task command instead:'
|
|
)
|
|
);
|
|
console.log(
|
|
` task-master update-task --id=<id> --prompt="Your prompt here"`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
// update-task command
|
|
programInstance
|
|
.command('update-task')
|
|
.description(
|
|
'Update a single specific task by ID with new information (use --id parameter)'
|
|
)
|
|
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
|
.option('-i, --id <id>', 'Task ID to update (required)')
|
|
.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) => {
|
|
try {
|
|
const tasksPath = options.file;
|
|
|
|
// Validate required parameters
|
|
if (!options.id) {
|
|
console.error(chalk.red('Error: --id parameter is required'));
|
|
console.log(
|
|
chalk.yellow(
|
|
'Usage example: task-master update-task --id=23 --prompt="Update with new information"'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Parse the task ID and validate it's a number
|
|
const taskId = parseInt(options.id, 10);
|
|
if (isNaN(taskId) || taskId <= 0) {
|
|
console.error(
|
|
chalk.red(
|
|
`Error: Invalid task ID: ${options.id}. Task ID must be a positive integer.`
|
|
)
|
|
);
|
|
console.log(
|
|
chalk.yellow(
|
|
'Usage example: task-master update-task --id=23 --prompt="Update with new information"'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!options.prompt) {
|
|
console.error(
|
|
chalk.red(
|
|
'Error: --prompt parameter is required. Please provide information about the changes.'
|
|
)
|
|
);
|
|
console.log(
|
|
chalk.yellow(
|
|
'Usage example: task-master update-task --id=23 --prompt="Update with new information"'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const prompt = options.prompt;
|
|
const useResearch = options.research || false;
|
|
|
|
// Validate tasks file exists
|
|
if (!fs.existsSync(tasksPath)) {
|
|
console.error(
|
|
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
|
);
|
|
if (tasksPath === 'tasks/tasks.json') {
|
|
console.log(
|
|
chalk.yellow(
|
|
'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
|
|
)
|
|
);
|
|
} else {
|
|
console.log(
|
|
chalk.yellow(
|
|
`Hint: Check if the file path is correct: ${tasksPath}`
|
|
)
|
|
);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
chalk.blue(`Updating task ${taskId} with prompt: "${prompt}"`)
|
|
);
|
|
console.log(chalk.blue(`Tasks file: ${tasksPath}`));
|
|
|
|
if (useResearch) {
|
|
// Verify Perplexity API key exists if using research
|
|
if (!isApiKeySet('perplexity')) {
|
|
console.log(
|
|
chalk.yellow(
|
|
'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.'
|
|
)
|
|
);
|
|
console.log(
|
|
chalk.yellow('Falling back to Claude AI for task update.')
|
|
);
|
|
} else {
|
|
console.log(
|
|
chalk.blue('Using Perplexity AI for research-backed task update')
|
|
);
|
|
}
|
|
}
|
|
|
|
const result = await updateTaskById(
|
|
tasksPath,
|
|
taskId,
|
|
prompt,
|
|
useResearch
|
|
);
|
|
|
|
// If the task wasn't updated (e.g., if it was already marked as done)
|
|
if (!result) {
|
|
console.log(
|
|
chalk.yellow(
|
|
'\nTask update was not completed. Review the messages above for details.'
|
|
)
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
|
|
// Provide more helpful error messages for common issues
|
|
if (
|
|
error.message.includes('task') &&
|
|
error.message.includes('not found')
|
|
) {
|
|
console.log(chalk.yellow('\nTo fix this issue:'));
|
|
console.log(
|
|
' 1. Run task-master list to see all available task IDs'
|
|
);
|
|
console.log(' 2. Use a valid task ID with the --id parameter');
|
|
} else if (error.message.includes('API key')) {
|
|
console.log(
|
|
chalk.yellow(
|
|
'\nThis error is related to API keys. Check your environment variables.'
|
|
)
|
|
);
|
|
}
|
|
|
|
// Use getDebugFlag getter instead of CONFIG.debug
|
|
if (getDebugFlag()) {
|
|
console.error(error);
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// update-subtask command
|
|
programInstance
|
|
.command('update-subtask')
|
|
.description(
|
|
'Update a subtask by appending additional timestamped information'
|
|
)
|
|
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
|
.option(
|
|
'-i, --id <id>',
|
|
'Subtask ID to update in format "parentId.subtaskId" (required)'
|
|
)
|
|
.option(
|
|
'-p, --prompt <text>',
|
|
'Prompt explaining what information to add (required)'
|
|
)
|
|
.option('-r, --research', 'Use Perplexity AI for research-backed updates')
|
|
.action(async (options) => {
|
|
try {
|
|
const tasksPath = options.file;
|
|
|
|
// Validate required parameters
|
|
if (!options.id) {
|
|
console.error(chalk.red('Error: --id parameter is required'));
|
|
console.log(
|
|
chalk.yellow(
|
|
'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Validate subtask ID format (should contain a dot)
|
|
const subtaskId = options.id;
|
|
if (!subtaskId.includes('.')) {
|
|
console.error(
|
|
chalk.red(
|
|
`Error: Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`
|
|
)
|
|
);
|
|
console.log(
|
|
chalk.yellow(
|
|
'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!options.prompt) {
|
|
console.error(
|
|
chalk.red(
|
|
'Error: --prompt parameter is required. Please provide information to add to the subtask.'
|
|
)
|
|
);
|
|
console.log(
|
|
chalk.yellow(
|
|
'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const prompt = options.prompt;
|
|
const useResearch = options.research || false;
|
|
|
|
// Validate tasks file exists
|
|
if (!fs.existsSync(tasksPath)) {
|
|
console.error(
|
|
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
|
);
|
|
if (tasksPath === 'tasks/tasks.json') {
|
|
console.log(
|
|
chalk.yellow(
|
|
'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
|
|
)
|
|
);
|
|
} else {
|
|
console.log(
|
|
chalk.yellow(
|
|
`Hint: Check if the file path is correct: ${tasksPath}`
|
|
)
|
|
);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
chalk.blue(`Updating subtask ${subtaskId} with prompt: "${prompt}"`)
|
|
);
|
|
console.log(chalk.blue(`Tasks file: ${tasksPath}`));
|
|
|
|
if (useResearch) {
|
|
// Verify Perplexity API key exists if using research
|
|
if (!isApiKeySet('perplexity')) {
|
|
console.log(
|
|
chalk.yellow(
|
|
'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.'
|
|
)
|
|
);
|
|
console.log(
|
|
chalk.yellow('Falling back to Claude AI for subtask update.')
|
|
);
|
|
} else {
|
|
console.log(
|
|
chalk.blue(
|
|
'Using Perplexity AI for research-backed subtask update'
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const result = await updateSubtaskById(
|
|
tasksPath,
|
|
subtaskId,
|
|
prompt,
|
|
useResearch
|
|
);
|
|
|
|
if (!result) {
|
|
console.log(
|
|
chalk.yellow(
|
|
'\nSubtask update was not completed. Review the messages above for details.'
|
|
)
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
|
|
// Provide more helpful error messages for common issues
|
|
if (
|
|
error.message.includes('subtask') &&
|
|
error.message.includes('not found')
|
|
) {
|
|
console.log(chalk.yellow('\nTo fix this issue:'));
|
|
console.log(
|
|
' 1. Run task-master list --with-subtasks to see all available subtask IDs'
|
|
);
|
|
console.log(
|
|
' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"'
|
|
);
|
|
} else if (error.message.includes('API key')) {
|
|
console.log(
|
|
chalk.yellow(
|
|
'\nThis error is related to API keys. Check your environment variables.'
|
|
)
|
|
);
|
|
}
|
|
|
|
// Use getDebugFlag getter instead of CONFIG.debug
|
|
if (getDebugFlag()) {
|
|
console.error(error);
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// 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('Expand a task into subtasks using AI')
|
|
.option('-i, --id <id>', 'ID of the task to expand')
|
|
.option(
|
|
'-a, --all',
|
|
'Expand all pending tasks based on complexity analysis'
|
|
)
|
|
.option(
|
|
'-n, --num <number>',
|
|
'Number of subtasks to generate (uses complexity analysis by default if available)'
|
|
)
|
|
.option(
|
|
'-r, --research',
|
|
'Enable research-backed generation (e.g., using Perplexity)',
|
|
false
|
|
)
|
|
.option('-p, --prompt <text>', 'Additional context for subtask generation')
|
|
.option('-f, --force', 'Force expansion even if subtasks exist', false) // Ensure force option exists
|
|
.option(
|
|
'--file <file>',
|
|
'Path to the tasks file (relative to project root)',
|
|
'tasks/tasks.json'
|
|
) // Allow file override
|
|
.action(async (options) => {
|
|
const projectRoot = findProjectRoot();
|
|
if (!projectRoot) {
|
|
console.error(chalk.red('Error: Could not find project root.'));
|
|
process.exit(1);
|
|
}
|
|
const tasksPath = path.resolve(projectRoot, options.file); // Resolve tasks path
|
|
|
|
if (options.all) {
|
|
// --- Handle expand --all ---
|
|
console.log(chalk.blue('Expanding all pending tasks...'));
|
|
// Updated call to the refactored expandAllTasks
|
|
try {
|
|
const result = await expandAllTasks(
|
|
tasksPath,
|
|
options.num, // Pass num
|
|
options.research, // Pass research flag
|
|
options.prompt, // Pass additional context
|
|
options.force, // Pass force flag
|
|
{} // Pass empty context for CLI calls
|
|
// outputFormat defaults to 'text' in expandAllTasks for CLI
|
|
);
|
|
// Optional: Display summary from result
|
|
console.log(chalk.green(`Expansion Summary:`));
|
|
console.log(chalk.green(` - Attempted: ${result.tasksToExpand}`));
|
|
console.log(chalk.green(` - Expanded: ${result.expandedCount}`));
|
|
console.log(chalk.yellow(` - Skipped: ${result.skippedCount}`));
|
|
console.log(chalk.red(` - Failed: ${result.failedCount}`));
|
|
} catch (error) {
|
|
console.error(
|
|
chalk.red(`Error expanding all tasks: ${error.message}`)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
} else if (options.id) {
|
|
// --- Handle expand --id <id> (Should be correct from previous refactor) ---
|
|
if (!options.id) {
|
|
console.error(
|
|
chalk.red('Error: Task ID is required unless using --all.')
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.blue(`Expanding task ${options.id}...`));
|
|
try {
|
|
// Call the refactored expandTask function
|
|
await expandTask(
|
|
tasksPath,
|
|
options.id,
|
|
options.num,
|
|
options.research,
|
|
options.prompt,
|
|
{}, // Pass empty context for CLI calls
|
|
options.force // Pass the force flag down
|
|
);
|
|
// expandTask logs its own success/failure for single task
|
|
} catch (error) {
|
|
console.error(
|
|
chalk.red(`Error expanding task ${options.id}: ${error.message}`)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
console.error(
|
|
chalk.red('Error: You must specify either a task ID (--id) or --all.')
|
|
);
|
|
programInstance.help(); // Show help
|
|
}
|
|
});
|
|
|
|
// 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 or manual input')
|
|
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
|
.option(
|
|
'-p, --prompt <prompt>',
|
|
'Description of the task to add (required if not using manual fields)'
|
|
)
|
|
.option('-t, --title <title>', 'Task title (for manual task creation)')
|
|
.option(
|
|
'-d, --description <description>',
|
|
'Task description (for manual task creation)'
|
|
)
|
|
.option(
|
|
'--details <details>',
|
|
'Implementation details (for manual task creation)'
|
|
)
|
|
.option(
|
|
'--test-strategy <testStrategy>',
|
|
'Test strategy (for manual task creation)'
|
|
)
|
|
.option(
|
|
'--dependencies <dependencies>',
|
|
'Comma-separated list of task IDs this task depends on'
|
|
)
|
|
.option(
|
|
'--priority <priority>',
|
|
'Task priority (high, medium, low)',
|
|
'medium'
|
|
)
|
|
.option(
|
|
'-r, --research',
|
|
'Whether to use research capabilities for task creation'
|
|
)
|
|
.action(async (options) => {
|
|
const isManualCreation = options.title && options.description;
|
|
|
|
// Validate that either prompt or title+description are provided
|
|
if (!options.prompt && !isManualCreation) {
|
|
console.error(
|
|
chalk.red(
|
|
'Error: Either --prompt or both --title and --description must be provided'
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
// Prepare dependencies if provided
|
|
let dependencies = [];
|
|
if (options.dependencies) {
|
|
dependencies = options.dependencies
|
|
.split(',')
|
|
.map((id) => parseInt(id.trim(), 10));
|
|
}
|
|
|
|
// Create manual task data if title and description are provided
|
|
let manualTaskData = null;
|
|
if (isManualCreation) {
|
|
manualTaskData = {
|
|
title: options.title,
|
|
description: options.description,
|
|
details: options.details || '',
|
|
testStrategy: options.testStrategy || ''
|
|
};
|
|
|
|
console.log(
|
|
chalk.blue(`Creating task manually with title: "${options.title}"`)
|
|
);
|
|
if (dependencies.length > 0) {
|
|
console.log(
|
|
chalk.blue(`Dependencies: [${dependencies.join(', ')}]`)
|
|
);
|
|
}
|
|
if (options.priority) {
|
|
console.log(chalk.blue(`Priority: ${options.priority}`));
|
|
}
|
|
} else {
|
|
console.log(
|
|
chalk.blue(
|
|
`Creating task with AI using prompt: "${options.prompt}"`
|
|
)
|
|
);
|
|
if (dependencies.length > 0) {
|
|
console.log(
|
|
chalk.blue(`Dependencies: [${dependencies.join(', ')}]`)
|
|
);
|
|
}
|
|
if (options.priority) {
|
|
console.log(chalk.blue(`Priority: ${options.priority}`));
|
|
}
|
|
}
|
|
|
|
// Pass mcpLog and session for MCP mode
|
|
const newTaskId = await addTask(
|
|
options.file,
|
|
options.prompt,
|
|
dependencies,
|
|
options.priority,
|
|
{
|
|
session: process.env // Pass environment as session for CLI
|
|
},
|
|
'text', // outputFormat
|
|
null, // manualTaskData
|
|
options.research || false // Pass the research flag value
|
|
);
|
|
|
|
console.log(chalk.green(`✓ Added new task #${newTaskId}`));
|
|
console.log(chalk.gray('Next: Complete this task or add more tasks'));
|
|
} catch (error) {
|
|
console.error(chalk.red(`Error adding task: ${error.message}`));
|
|
if (error.stack && getDebugFlag()) {
|
|
console.error(error.stack);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Handle subtask IDs correctly by preserving the string format for IDs containing dots
|
|
// Only use parseInt for simple numeric IDs
|
|
const formattedTaskId = taskId.includes('.')
|
|
? taskId
|
|
: parseInt(taskId, 10);
|
|
const formattedDependencyId = dependencyId.includes('.')
|
|
? dependencyId
|
|
: parseInt(dependencyId, 10);
|
|
|
|
await addDependency(tasksPath, formattedTaskId, formattedDependencyId);
|
|
});
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Handle subtask IDs correctly by preserving the string format for IDs containing dots
|
|
// Only use parseInt for simple numeric IDs
|
|
const formattedTaskId = taskId.includes('.')
|
|
? taskId
|
|
: parseInt(taskId, 10);
|
|
const formattedDependencyId = dependencyId.includes('.')
|
|
? dependencyId
|
|
: parseInt(dependencyId, 10);
|
|
|
|
await removeDependency(tasksPath, formattedTaskId, formattedDependencyId);
|
|
});
|
|
|
|
// 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-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('--skip-generate', 'Skip regenerating task files')
|
|
.action(async (options) => {
|
|
const tasksPath = options.file;
|
|
const parentId = options.parent;
|
|
const existingTaskId = options.taskId;
|
|
const generateFiles = !options.skipGenerate;
|
|
|
|
if (!parentId) {
|
|
console.error(
|
|
chalk.red(
|
|
'Error: --parent parameter is required. Please provide a parent task ID.'
|
|
)
|
|
);
|
|
showAddSubtaskHelp();
|
|
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);
|
|
}
|
|
})
|
|
.on('error', function (err) {
|
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
showAddSubtaskHelp();
|
|
process.exit(1);
|
|
});
|
|
|
|
// Helper function to show add-subtask command help
|
|
function showAddSubtaskHelp() {
|
|
console.log(
|
|
boxen(
|
|
chalk.white.bold('Add Subtask Command Help') +
|
|
'\n\n' +
|
|
chalk.cyan('Usage:') +
|
|
'\n' +
|
|
` task-master add-subtask --parent=<id> [options]\n\n` +
|
|
chalk.cyan('Options:') +
|
|
'\n' +
|
|
' -p, --parent <id> Parent task ID (required)\n' +
|
|
' -i, --task-id <id> Existing task ID to convert to subtask\n' +
|
|
' -t, --title <title> Title for the new subtask\n' +
|
|
' -d, --description <text> Description for the new subtask\n' +
|
|
' --details <text> Implementation details for the new subtask\n' +
|
|
' --dependencies <ids> Comma-separated list of dependency IDs\n' +
|
|
' -s, --status <status> Status for the new subtask (default: "pending")\n' +
|
|
' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' +
|
|
' --skip-generate Skip regenerating task files\n\n' +
|
|
chalk.cyan('Examples:') +
|
|
'\n' +
|
|
' task-master add-subtask --parent=5 --task-id=8\n' +
|
|
' task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"',
|
|
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
|
)
|
|
);
|
|
}
|
|
|
|
// 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(s) to remove in format "parentId.subtaskId" (can be comma-separated for multiple subtasks)'
|
|
)
|
|
.option(
|
|
'-c, --convert',
|
|
'Convert the subtask to a standalone task instead of deleting it'
|
|
)
|
|
.option('--skip-generate', 'Skip regenerating task files')
|
|
.action(async (options) => {
|
|
const tasksPath = options.file;
|
|
const subtaskIds = options.id;
|
|
const convertToTask = options.convert || false;
|
|
const generateFiles = !options.skipGenerate;
|
|
|
|
if (!subtaskIds) {
|
|
console.error(
|
|
chalk.red(
|
|
'Error: --id parameter is required. Please provide subtask ID(s) in format "parentId.subtaskId".'
|
|
)
|
|
);
|
|
showRemoveSubtaskHelp();
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
// Split by comma to support multiple subtask IDs
|
|
const subtaskIdArray = subtaskIds.split(',').map((id) => id.trim());
|
|
|
|
for (const subtaskId of subtaskIdArray) {
|
|
// Validate subtask ID format
|
|
if (!subtaskId.includes('.')) {
|
|
console.error(
|
|
chalk.red(
|
|
`Error: Subtask ID "${subtaskId}" must be in format "parentId.subtaskId"`
|
|
)
|
|
);
|
|
showRemoveSubtaskHelp();
|
|
process.exit(1);
|
|
}
|
|
|
|
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}`));
|
|
showRemoveSubtaskHelp();
|
|
process.exit(1);
|
|
}
|
|
})
|
|
.on('error', function (err) {
|
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
showRemoveSubtaskHelp();
|
|
process.exit(1);
|
|
});
|
|
|
|
// Helper function to show remove-subtask command help
|
|
function showRemoveSubtaskHelp() {
|
|
console.log(
|
|
boxen(
|
|
chalk.white.bold('Remove Subtask Command Help') +
|
|
'\n\n' +
|
|
chalk.cyan('Usage:') +
|
|
'\n' +
|
|
` task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` +
|
|
chalk.cyan('Options:') +
|
|
'\n' +
|
|
' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' +
|
|
' -c, --convert Convert the subtask to a standalone task instead of deleting it\n' +
|
|
' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' +
|
|
' --skip-generate Skip regenerating task files\n\n' +
|
|
chalk.cyan('Examples:') +
|
|
'\n' +
|
|
' task-master remove-subtask --id=5.2\n' +
|
|
' task-master remove-subtask --id=5.2,6.3,7.1\n' +
|
|
' task-master remove-subtask --id=5.2 --convert',
|
|
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
|
)
|
|
);
|
|
}
|
|
|
|
// remove-task command
|
|
programInstance
|
|
.command('remove-task')
|
|
.description('Remove a task or subtask permanently')
|
|
.option(
|
|
'-i, --id <id>',
|
|
'ID of the task or subtask to remove (e.g., "5" or "5.2")'
|
|
)
|
|
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
|
.option('-y, --yes', 'Skip confirmation prompt', false)
|
|
.action(async (options) => {
|
|
const tasksPath = options.file;
|
|
const taskId = options.id;
|
|
|
|
if (!taskId) {
|
|
console.error(chalk.red('Error: Task ID is required'));
|
|
console.error(
|
|
chalk.yellow('Usage: task-master remove-task --id=<taskId>')
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
// Check if the task exists
|
|
const data = readJSON(tasksPath);
|
|
if (!data || !data.tasks) {
|
|
console.error(
|
|
chalk.red(`Error: No valid tasks found in ${tasksPath}`)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!taskExists(data.tasks, taskId)) {
|
|
console.error(chalk.red(`Error: Task with ID ${taskId} not found`));
|
|
process.exit(1);
|
|
}
|
|
|
|
// Load task for display
|
|
const task = findTaskById(data.tasks, taskId);
|
|
|
|
// Skip confirmation if --yes flag is provided
|
|
if (!options.yes) {
|
|
// Display task information
|
|
console.log();
|
|
console.log(
|
|
chalk.red.bold(
|
|
'⚠️ WARNING: This will permanently delete the following task:'
|
|
)
|
|
);
|
|
console.log();
|
|
|
|
if (typeof taskId === 'string' && taskId.includes('.')) {
|
|
// It's a subtask
|
|
const [parentId, subtaskId] = taskId.split('.');
|
|
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
|
|
console.log(
|
|
chalk.gray(
|
|
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
|
|
)
|
|
);
|
|
} else {
|
|
// It's a main task
|
|
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
|
|
|
|
// Show if it has subtasks
|
|
if (task.subtasks && task.subtasks.length > 0) {
|
|
console.log(
|
|
chalk.yellow(
|
|
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Show if other tasks depend on it
|
|
const dependentTasks = data.tasks.filter(
|
|
(t) =>
|
|
t.dependencies && t.dependencies.includes(parseInt(taskId, 10))
|
|
);
|
|
|
|
if (dependentTasks.length > 0) {
|
|
console.log(
|
|
chalk.yellow(
|
|
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
|
|
)
|
|
);
|
|
console.log(chalk.yellow('These dependencies will be removed:'));
|
|
dependentTasks.forEach((t) => {
|
|
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
|
|
// Prompt for confirmation
|
|
const { confirm } = await inquirer.prompt([
|
|
{
|
|
type: 'confirm',
|
|
name: 'confirm',
|
|
message: chalk.red.bold(
|
|
'Are you sure you want to permanently delete this task?'
|
|
),
|
|
default: false
|
|
}
|
|
]);
|
|
|
|
if (!confirm) {
|
|
console.log(chalk.blue('Task deletion cancelled.'));
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
const indicator = startLoadingIndicator('Removing task...');
|
|
|
|
// Remove the task
|
|
const result = await removeTask(tasksPath, taskId);
|
|
|
|
stopLoadingIndicator(indicator);
|
|
|
|
// Display success message with appropriate color based on task or subtask
|
|
if (typeof taskId === 'string' && taskId.includes('.')) {
|
|
// It was a subtask
|
|
console.log(
|
|
boxen(
|
|
chalk.green(`Subtask ${taskId} has been successfully removed`),
|
|
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
|
)
|
|
);
|
|
} else {
|
|
// It was a main task
|
|
console.log(
|
|
boxen(chalk.green(`Task ${taskId} has been successfully removed`), {
|
|
padding: 1,
|
|
borderColor: 'green',
|
|
borderStyle: 'round'
|
|
})
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
chalk.red(`Error: ${error.message || 'An unknown error occurred'}`)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// init command (Directly calls the implementation from init.js)
|
|
programInstance
|
|
.command('init')
|
|
.description('Initialize a new project with Task Master structure')
|
|
.option('-y, --yes', 'Skip prompts and use default values')
|
|
.option('-n, --name <name>', 'Project name')
|
|
.option('-d, --description <description>', 'Project description')
|
|
.option('-v, --version <version>', 'Project version', '0.1.0') // Set default here
|
|
.option('-a, --author <author>', 'Author name')
|
|
.option('--skip-install', 'Skip installing dependencies')
|
|
.option('--dry-run', 'Show what would be done without making changes')
|
|
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
|
|
.action(async (cmdOptions) => {
|
|
// cmdOptions contains parsed arguments
|
|
try {
|
|
console.log('DEBUG: Running init command action in commands.js');
|
|
console.log(
|
|
'DEBUG: Options received by action:',
|
|
JSON.stringify(cmdOptions)
|
|
);
|
|
// Directly call the initializeProject function, passing the parsed options
|
|
await initializeProject(cmdOptions);
|
|
// initializeProject handles its own flow, including potential process.exit()
|
|
} catch (error) {
|
|
console.error(
|
|
chalk.red(`Error during initialization: ${error.message}`)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// models command
|
|
programInstance
|
|
.command('models')
|
|
.description('Manage AI model configurations')
|
|
.option(
|
|
'--set-main <model_id>',
|
|
'Set the primary model for task generation/updates'
|
|
)
|
|
.option(
|
|
'--set-research <model_id>',
|
|
'Set the model for research-backed operations'
|
|
)
|
|
.option(
|
|
'--set-fallback <model_id>',
|
|
'Set the model to use if the primary fails'
|
|
)
|
|
.option('--setup', 'Run interactive setup to configure models')
|
|
.action(async (options) => {
|
|
try {
|
|
// --- Set Operations ---
|
|
if (options.setMain || options.setResearch || options.setFallback) {
|
|
let resultSet = null;
|
|
if (options.setMain) {
|
|
resultSet = await setModel('main', options.setMain);
|
|
} else if (options.setResearch) {
|
|
resultSet = await setModel('research', options.setResearch);
|
|
} else if (options.setFallback) {
|
|
resultSet = await setModel('fallback', options.setFallback);
|
|
}
|
|
|
|
if (resultSet?.success) {
|
|
console.log(chalk.green(resultSet.data.message));
|
|
} else {
|
|
console.error(
|
|
chalk.red(
|
|
`Error setting model: ${resultSet?.error?.message || 'Unknown error'}`
|
|
)
|
|
);
|
|
if (resultSet?.error?.code === 'MODEL_NOT_FOUND') {
|
|
console.log(
|
|
chalk.yellow(
|
|
'\nRun `task-master models` to see available models.'
|
|
)
|
|
);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
return; // Exit after successful set operation
|
|
}
|
|
|
|
// --- Interactive Setup ---
|
|
if (options.setup) {
|
|
// Get available models for interactive setup
|
|
const availableModelsResult = await getAvailableModelsList();
|
|
if (!availableModelsResult.success) {
|
|
console.error(
|
|
chalk.red(
|
|
`Error fetching available models: ${availableModelsResult.error?.message || 'Unknown error'}`
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const availableModelsForSetup = availableModelsResult.data.models;
|
|
|
|
const currentConfigResult = await getModelConfiguration();
|
|
if (!currentConfigResult.success) {
|
|
console.error(
|
|
chalk.red(
|
|
`Error fetching current configuration: ${currentConfigResult.error?.message || 'Unknown error'}`
|
|
)
|
|
);
|
|
// Allow setup even if current config fails (might be first time run)
|
|
}
|
|
const currentModels = currentConfigResult.data?.activeModels || {
|
|
main: {},
|
|
research: {},
|
|
fallback: {}
|
|
};
|
|
|
|
console.log(chalk.cyan.bold('\nInteractive Model Setup:'));
|
|
|
|
const getMainChoicesAndDefault = () => {
|
|
const mainChoices = allModelsForSetup.filter((modelChoice) =>
|
|
availableModelsForSetup
|
|
.find((m) => m.modelId === modelChoice.value.id)
|
|
?.allowedRoles?.includes('main')
|
|
);
|
|
const defaultIndex = mainChoices.findIndex(
|
|
(m) => m.value.id === currentModels.main?.modelId
|
|
);
|
|
return { choices: mainChoices, default: defaultIndex };
|
|
};
|
|
|
|
// Get all available models, including active ones
|
|
const allModelsForSetup = availableModelsForSetup.map((model) => ({
|
|
name: `${model.provider} / ${model.modelId}`,
|
|
value: { provider: model.provider, id: model.modelId } // Use id here for comparison
|
|
}));
|
|
|
|
if (allModelsForSetup.length === 0) {
|
|
console.error(
|
|
chalk.red('Error: No selectable models found in configuration.')
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Function to find the index of the currently selected model ID
|
|
// Ensure it correctly searches the unfiltered selectableModels list
|
|
const findDefaultIndex = (roleModelId) => {
|
|
if (!roleModelId) return -1; // Handle cases where a role isn't set
|
|
return allModelsForSetup.findIndex(
|
|
(m) => m.value.id === roleModelId // Compare using the 'id' from the value object
|
|
);
|
|
};
|
|
|
|
// Helper to get research choices and default index
|
|
const getResearchChoicesAndDefault = () => {
|
|
const researchChoices = allModelsForSetup.filter((modelChoice) =>
|
|
availableModelsForSetup
|
|
.find((m) => m.modelId === modelChoice.value.id)
|
|
?.allowedRoles?.includes('research')
|
|
);
|
|
const defaultIndex = researchChoices.findIndex(
|
|
(m) => m.value.id === currentModels.research?.modelId
|
|
);
|
|
return { choices: researchChoices, default: defaultIndex };
|
|
};
|
|
|
|
// Helper to get fallback choices and default index
|
|
const getFallbackChoicesAndDefault = () => {
|
|
const choices = [
|
|
{ name: 'None (disable fallback)', value: null },
|
|
new inquirer.Separator(),
|
|
...allModelsForSetup
|
|
];
|
|
const currentFallbackId = currentModels.fallback?.modelId;
|
|
let defaultIndex = 0; // Default to 'None'
|
|
if (currentFallbackId) {
|
|
const foundIndex = allModelsForSetup.findIndex(
|
|
(m) => m.value.id === currentFallbackId
|
|
);
|
|
if (foundIndex !== -1) {
|
|
defaultIndex = foundIndex + 2; // +2 because of 'None' and Separator
|
|
}
|
|
}
|
|
return { choices, default: defaultIndex };
|
|
};
|
|
|
|
const researchPromptData = getResearchChoicesAndDefault();
|
|
const fallbackPromptData = getFallbackChoicesAndDefault();
|
|
// Call the helper function for main model choices
|
|
const mainPromptData = getMainChoicesAndDefault();
|
|
|
|
// Add cancel option for all prompts
|
|
const cancelOption = {
|
|
name: 'Cancel setup (q)',
|
|
value: '__CANCEL__'
|
|
};
|
|
|
|
const mainModelChoices = [
|
|
cancelOption,
|
|
new inquirer.Separator(),
|
|
...mainPromptData.choices
|
|
];
|
|
|
|
const researchModelChoices = [
|
|
cancelOption,
|
|
new inquirer.Separator(),
|
|
...researchPromptData.choices
|
|
];
|
|
|
|
const fallbackModelChoices = [
|
|
cancelOption,
|
|
new inquirer.Separator(),
|
|
...fallbackPromptData.choices
|
|
];
|
|
|
|
// Add key press handler for 'q' to cancel
|
|
process.stdin.on('keypress', (str, key) => {
|
|
if (key.name === 'q') {
|
|
process.stdin.pause();
|
|
console.log(chalk.yellow('\nSetup canceled. No changes made.'));
|
|
process.exit(0);
|
|
}
|
|
});
|
|
|
|
console.log(chalk.gray('Press "q" at any time to cancel the setup.'));
|
|
|
|
const answers = await inquirer.prompt([
|
|
{
|
|
type: 'list',
|
|
name: 'mainModel',
|
|
message: 'Select the main model for generation/updates:',
|
|
choices: mainModelChoices,
|
|
default: mainPromptData.default + 2 // +2 for cancel option and separator
|
|
},
|
|
{
|
|
type: 'list',
|
|
name: 'researchModel',
|
|
message: 'Select the research model:',
|
|
choices: researchModelChoices,
|
|
default: researchPromptData.default + 2, // +2 for cancel option and separator
|
|
when: (answers) => answers.mainModel !== '__CANCEL__'
|
|
},
|
|
{
|
|
type: 'list',
|
|
name: 'fallbackModel',
|
|
message: 'Select the fallback model (optional):',
|
|
choices: fallbackModelChoices,
|
|
default: fallbackPromptData.default + 2, // +2 for cancel option and separator
|
|
when: (answers) =>
|
|
answers.mainModel !== '__CANCEL__' &&
|
|
answers.researchModel !== '__CANCEL__'
|
|
}
|
|
]);
|
|
|
|
// Clean up the keypress handler
|
|
process.stdin.removeAllListeners('keypress');
|
|
|
|
// Check if user canceled at any point
|
|
if (
|
|
answers.mainModel === '__CANCEL__' ||
|
|
answers.researchModel === '__CANCEL__' ||
|
|
answers.fallbackModel === '__CANCEL__'
|
|
) {
|
|
console.log(chalk.yellow('\nSetup canceled. No changes made.'));
|
|
return;
|
|
}
|
|
|
|
// Apply changes using setModel
|
|
let setupSuccess = true;
|
|
let setupConfigModified = false;
|
|
|
|
if (
|
|
answers.mainModel &&
|
|
answers.mainModel.id !== currentModels.main?.modelId
|
|
) {
|
|
const result = await setModel('main', answers.mainModel.id);
|
|
if (result.success) {
|
|
console.log(
|
|
chalk.blue(
|
|
`Selected main model: ${result.data.provider} / ${result.data.modelId}`
|
|
)
|
|
);
|
|
setupConfigModified = true;
|
|
} else {
|
|
console.error(
|
|
chalk.red(
|
|
`Error setting main model: ${result.error?.message || 'Unknown'}`
|
|
)
|
|
);
|
|
setupSuccess = false;
|
|
}
|
|
}
|
|
|
|
if (
|
|
answers.researchModel &&
|
|
answers.researchModel.id !== currentModels.research?.modelId
|
|
) {
|
|
const result = await setModel('research', answers.researchModel.id);
|
|
if (result.success) {
|
|
console.log(
|
|
chalk.blue(
|
|
`Selected research model: ${result.data.provider} / ${result.data.modelId}`
|
|
)
|
|
);
|
|
setupConfigModified = true;
|
|
} else {
|
|
console.error(
|
|
chalk.red(
|
|
`Error setting research model: ${result.error?.message || 'Unknown'}`
|
|
)
|
|
);
|
|
setupSuccess = false;
|
|
}
|
|
}
|
|
|
|
// Set Fallback Model - Handle 'None' selection
|
|
const currentFallbackId = currentModels.fallback?.modelId;
|
|
const selectedFallbackId = answers.fallbackModel?.id; // Will be null if 'None' selected
|
|
|
|
if (selectedFallbackId !== currentFallbackId) {
|
|
if (selectedFallbackId) {
|
|
// User selected a specific fallback model
|
|
const result = await setModel('fallback', selectedFallbackId);
|
|
if (result.success) {
|
|
console.log(
|
|
chalk.blue(
|
|
`Selected fallback model: ${result.data.provider} / ${result.data.modelId}`
|
|
)
|
|
);
|
|
setupConfigModified = true;
|
|
} else {
|
|
console.error(
|
|
chalk.red(
|
|
`Error setting fallback model: ${result.error?.message || 'Unknown'}`
|
|
)
|
|
);
|
|
setupSuccess = false;
|
|
}
|
|
} else if (currentFallbackId) {
|
|
// User selected 'None' but a fallback was previously set
|
|
// Need to explicitly clear it in the config file
|
|
const currentCfg = getConfig();
|
|
currentCfg.models.fallback = {
|
|
...currentCfg.models.fallback,
|
|
provider: undefined,
|
|
modelId: undefined
|
|
};
|
|
if (writeConfig(currentCfg)) {
|
|
console.log(chalk.blue('Fallback model disabled.'));
|
|
setupConfigModified = true;
|
|
} else {
|
|
console.error(
|
|
chalk.red('Failed to disable fallback model in config file.')
|
|
);
|
|
setupSuccess = false;
|
|
}
|
|
}
|
|
// No action needed if fallback was already null/undefined and user selected None
|
|
}
|
|
|
|
if (setupSuccess && setupConfigModified) {
|
|
console.log(chalk.green.bold('\nModel setup complete!'));
|
|
} else if (setupSuccess && !setupConfigModified) {
|
|
console.log(
|
|
chalk.yellow('\nNo changes made to model configuration.')
|
|
);
|
|
} else if (!setupSuccess) {
|
|
console.error(
|
|
chalk.red(
|
|
'\nErrors occurred during model selection. Please review and try again.'
|
|
)
|
|
);
|
|
}
|
|
return; // Exit after setup attempt
|
|
}
|
|
|
|
// --- Default: Display Current Configuration ---
|
|
// No longer need to check configModified here, as the set/setup logic returns early
|
|
// Fetch configuration using the core function
|
|
const result = await getModelConfiguration();
|
|
|
|
if (!result.success) {
|
|
// Handle specific CONFIG_MISSING error gracefully
|
|
if (result.error?.code === 'CONFIG_MISSING') {
|
|
console.error(
|
|
boxen(
|
|
chalk.red.bold('Configuration File Missing!') +
|
|
'\n\n' +
|
|
chalk.white(
|
|
'The .taskmasterconfig file was not found in your project root.\n\n' +
|
|
'Run the interactive setup to create and configure it:'
|
|
) +
|
|
'\n' +
|
|
chalk.green(' task-master models --setup'),
|
|
{
|
|
padding: 1,
|
|
margin: { top: 1 },
|
|
borderColor: 'red',
|
|
borderStyle: 'round'
|
|
}
|
|
)
|
|
);
|
|
process.exit(0); // Exit gracefully, user needs to run setup
|
|
} else {
|
|
console.error(
|
|
chalk.red(
|
|
`Error fetching model configuration: ${result.error?.message || 'Unknown error'}`
|
|
)
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const configData = result.data;
|
|
const active = configData.activeModels;
|
|
const warnings = configData.warnings || []; // Warnings now come from core function
|
|
|
|
// --- Display Warning Banner (if any) ---
|
|
if (warnings.length > 0) {
|
|
console.log(
|
|
boxen(
|
|
chalk.red.bold('API Key Warnings:') +
|
|
'\n\n' +
|
|
warnings.join('\n'),
|
|
{
|
|
padding: 1,
|
|
margin: { top: 1, bottom: 1 },
|
|
borderColor: 'red',
|
|
borderStyle: 'round'
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
// --- Active Configuration Section ---
|
|
console.log(chalk.cyan.bold('\nActive Model Configuration:'));
|
|
const activeTable = new Table({
|
|
head: [
|
|
'Role',
|
|
'Provider',
|
|
'Model ID',
|
|
'SWE Score',
|
|
'Cost ($/1M tkns)',
|
|
'API Key Status'
|
|
].map((h) => chalk.cyan.bold(h)),
|
|
colWidths: [10, 14, 30, 18, 20, 28],
|
|
style: { head: ['cyan', 'bold'] }
|
|
});
|
|
|
|
// --- Helper functions for formatting (can be moved to ui.js if complex) ---
|
|
const formatSweScoreWithTertileStars = (score, allModels) => {
|
|
if (score === null || score === undefined || score <= 0) return 'N/A';
|
|
const formattedPercentage = `${(score * 100).toFixed(1)}%`;
|
|
|
|
const validScores = allModels
|
|
.map((m) => m.sweScore)
|
|
.filter((s) => s !== null && s !== undefined && s > 0);
|
|
const sortedScores = [...validScores].sort((a, b) => b - a);
|
|
const n = sortedScores.length;
|
|
let stars = chalk.gray('☆☆☆');
|
|
|
|
if (n > 0) {
|
|
const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1);
|
|
const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1);
|
|
if (score >= sortedScores[topThirdIndex])
|
|
stars = chalk.yellow('★★★');
|
|
else if (score >= sortedScores[midThirdIndex])
|
|
stars = chalk.yellow('★★') + chalk.gray('☆');
|
|
else stars = chalk.yellow('★') + chalk.gray('☆☆');
|
|
}
|
|
return `${formattedPercentage} ${stars}`;
|
|
};
|
|
|
|
const formatCost = (costObj) => {
|
|
if (!costObj) return 'N/A';
|
|
|
|
// Check if both input and output costs are 0 and return "Free"
|
|
if (costObj.input === 0 && costObj.output === 0) {
|
|
return chalk.green('Free');
|
|
}
|
|
|
|
const formatSingleCost = (costValue) => {
|
|
if (costValue === null || costValue === undefined) return 'N/A';
|
|
const isInteger = Number.isInteger(costValue);
|
|
return `$${costValue.toFixed(isInteger ? 0 : 2)}`;
|
|
};
|
|
return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(
|
|
costObj.output
|
|
)} out`;
|
|
};
|
|
|
|
const getCombinedStatus = (keyStatus) => {
|
|
const cliOk = keyStatus?.cli;
|
|
const mcpOk = keyStatus?.mcp;
|
|
const cliSymbol = cliOk ? chalk.green('✓') : chalk.red('✗');
|
|
const mcpSymbol = mcpOk ? chalk.green('✓') : chalk.red('✗');
|
|
|
|
if (cliOk && mcpOk) return `${cliSymbol} CLI & ${mcpSymbol} MCP OK`;
|
|
if (cliOk && !mcpOk)
|
|
return `${cliSymbol} CLI OK / ${mcpSymbol} MCP Missing`;
|
|
if (!cliOk && mcpOk)
|
|
return `${cliSymbol} CLI Missing / ${mcpSymbol} MCP OK`;
|
|
return chalk.gray(`${cliSymbol} CLI & MCP Both Missing`);
|
|
};
|
|
|
|
// Get all available models data once for SWE Score calculation
|
|
const availableModelsResultForScore = await getAvailableModelsList();
|
|
const allAvailModelsForScore =
|
|
availableModelsResultForScore.data?.models || [];
|
|
|
|
// Populate Active Table
|
|
activeTable.push([
|
|
chalk.white('Main'),
|
|
active.main.provider,
|
|
active.main.modelId,
|
|
formatSweScoreWithTertileStars(
|
|
active.main.sweScore,
|
|
allAvailModelsForScore
|
|
),
|
|
formatCost(active.main.cost),
|
|
getCombinedStatus(active.main.keyStatus)
|
|
]);
|
|
activeTable.push([
|
|
chalk.white('Research'),
|
|
active.research.provider,
|
|
active.research.modelId,
|
|
formatSweScoreWithTertileStars(
|
|
active.research.sweScore,
|
|
allAvailModelsForScore
|
|
),
|
|
formatCost(active.research.cost),
|
|
getCombinedStatus(active.research.keyStatus)
|
|
]);
|
|
if (active.fallback) {
|
|
activeTable.push([
|
|
chalk.white('Fallback'),
|
|
active.fallback.provider,
|
|
active.fallback.modelId,
|
|
formatSweScoreWithTertileStars(
|
|
active.fallback.sweScore,
|
|
allAvailModelsForScore
|
|
),
|
|
formatCost(active.fallback.cost),
|
|
getCombinedStatus(active.fallback.keyStatus)
|
|
]);
|
|
}
|
|
console.log(activeTable.toString());
|
|
|
|
// --- Available Models Section ---
|
|
const availableResult = await getAvailableModelsList();
|
|
if (availableResult.success && availableResult.data.models.length > 0) {
|
|
console.log(chalk.cyan.bold('\nOther Available Models:'));
|
|
const availableTable = new Table({
|
|
head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map(
|
|
(h) => chalk.cyan.bold(h)
|
|
),
|
|
colWidths: [15, 40, 18, 25],
|
|
style: { head: ['cyan', 'bold'] }
|
|
});
|
|
availableResult.data.models.forEach((model) => {
|
|
availableTable.push([
|
|
model.provider,
|
|
model.modelId,
|
|
formatSweScoreWithTertileStars(
|
|
model.sweScore,
|
|
allAvailModelsForScore
|
|
),
|
|
formatCost(model.cost)
|
|
]);
|
|
});
|
|
console.log(availableTable.toString());
|
|
} else if (availableResult.success) {
|
|
console.log(
|
|
chalk.gray('\n(All available models are currently configured)')
|
|
);
|
|
} else {
|
|
console.warn(
|
|
chalk.yellow(
|
|
`Could not fetch available models list: ${availableResult.error?.message}`
|
|
)
|
|
);
|
|
}
|
|
|
|
// --- Suggested Actions Section ---
|
|
console.log(
|
|
boxen(
|
|
chalk.white.bold('Next Steps:') +
|
|
'\n' +
|
|
chalk.cyan(
|
|
`1. Set main model: ${chalk.yellow('task-master models --set-main <model_id>')}`
|
|
) +
|
|
'\n' +
|
|
chalk.cyan(
|
|
`2. Set research model: ${chalk.yellow('task-master models --set-research <model_id>')}`
|
|
) +
|
|
'\n' +
|
|
chalk.cyan(
|
|
`3. Set fallback model: ${chalk.yellow('task-master models --set-fallback <model_id>')}`
|
|
) +
|
|
'\n' +
|
|
chalk.cyan(
|
|
`4. Run interactive setup: ${chalk.yellow('task-master models --setup')}`
|
|
),
|
|
{
|
|
padding: 1,
|
|
borderColor: 'yellow',
|
|
borderStyle: 'round',
|
|
margin: { top: 1 }
|
|
}
|
|
)
|
|
);
|
|
} catch (error) {
|
|
// Catch errors specifically from the core model functions
|
|
console.error(
|
|
chalk.red(`Error processing models command: ${error.message}`)
|
|
);
|
|
if (error instanceof ConfigurationError) {
|
|
// Provide specific guidance if it's a config error
|
|
console.error(
|
|
chalk.yellow(
|
|
'This might be a configuration file issue. Try running `task-master models --setup`.'
|
|
)
|
|
);
|
|
}
|
|
if (getDebugFlag()) {
|
|
console.error(error.stack);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
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 ONLY
|
|
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 'unknown'
|
|
log(
|
|
'warn',
|
|
'Could not read package.json for version info in .version()'
|
|
);
|
|
}
|
|
return 'unknown'; // Default fallback if package.json fails
|
|
})
|
|
.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;
|
|
}
|
|
|
|
/**
|
|
* Check for newer version of task-master-ai
|
|
* @returns {Promise<{currentVersion: string, latestVersion: string, needsUpdate: boolean}>}
|
|
*/
|
|
async function checkForUpdate() {
|
|
// Get current version from package.json ONLY
|
|
let currentVersion = 'unknown'; // Initialize with a default
|
|
try {
|
|
// Try to get the version from the installed package (if applicable) or current dir
|
|
let packageJsonPath = path.join(
|
|
process.cwd(),
|
|
'node_modules',
|
|
'task-master-ai',
|
|
'package.json'
|
|
);
|
|
// Fallback to current directory package.json if not found in node_modules
|
|
if (!fs.existsSync(packageJsonPath)) {
|
|
packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
}
|
|
|
|
if (fs.existsSync(packageJsonPath)) {
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
currentVersion = packageJson.version;
|
|
}
|
|
} catch (error) {
|
|
// Silently fail and use default
|
|
log('debug', `Error reading current package version: ${error.message}`);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
// Get the latest version from npm registry
|
|
const options = {
|
|
hostname: 'registry.npmjs.org',
|
|
path: '/task-master-ai',
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/vnd.npm.install-v1+json' // Lightweight response
|
|
}
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
|
|
res.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
|
|
res.on('end', () => {
|
|
try {
|
|
const npmData = JSON.parse(data);
|
|
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
|
|
|
|
// Compare versions
|
|
const needsUpdate =
|
|
compareVersions(currentVersion, latestVersion) < 0;
|
|
|
|
resolve({
|
|
currentVersion,
|
|
latestVersion,
|
|
needsUpdate
|
|
});
|
|
} catch (error) {
|
|
log('debug', `Error parsing npm response: ${error.message}`);
|
|
resolve({
|
|
currentVersion,
|
|
latestVersion: currentVersion,
|
|
needsUpdate: false
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', (error) => {
|
|
log('debug', `Error checking for updates: ${error.message}`);
|
|
resolve({
|
|
currentVersion,
|
|
latestVersion: currentVersion,
|
|
needsUpdate: false
|
|
});
|
|
});
|
|
|
|
// Set a timeout to avoid hanging if npm is slow
|
|
req.setTimeout(3000, () => {
|
|
req.abort();
|
|
log('debug', 'Update check timed out');
|
|
resolve({
|
|
currentVersion,
|
|
latestVersion: currentVersion,
|
|
needsUpdate: false
|
|
});
|
|
});
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Compare semantic versions
|
|
* @param {string} v1 - First version
|
|
* @param {string} v2 - Second version
|
|
* @returns {number} -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
|
|
*/
|
|
function compareVersions(v1, v2) {
|
|
const v1Parts = v1.split('.').map((p) => parseInt(p, 10));
|
|
const v2Parts = v2.split('.').map((p) => parseInt(p, 10));
|
|
|
|
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
const v1Part = v1Parts[i] || 0;
|
|
const v2Part = v2Parts[i] || 0;
|
|
|
|
if (v1Part < v2Part) return -1;
|
|
if (v1Part > v2Part) return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Display upgrade notification message
|
|
* @param {string} currentVersion - Current version
|
|
* @param {string} latestVersion - Latest version
|
|
*/
|
|
function displayUpgradeNotification(currentVersion, latestVersion) {
|
|
const message = boxen(
|
|
`${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` +
|
|
`Run ${chalk.cyan('npm i task-master-ai@latest -g')} to update to the latest version with new features and bug fixes.`,
|
|
{
|
|
padding: 1,
|
|
margin: { top: 1, bottom: 1 },
|
|
borderColor: 'yellow',
|
|
borderStyle: 'round'
|
|
}
|
|
);
|
|
|
|
console.log(message);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
// Start the update check in the background - don't await yet
|
|
const updateCheckPromise = checkForUpdate();
|
|
|
|
// Setup and parse
|
|
// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
|
|
// This means the ConfigurationError might be thrown here if .taskmasterconfig is missing.
|
|
const programInstance = setupCLI();
|
|
await programInstance.parseAsync(argv);
|
|
|
|
// After command execution, check if an update is available
|
|
const updateInfo = await updateCheckPromise;
|
|
if (updateInfo.needsUpdate) {
|
|
displayUpgradeNotification(
|
|
updateInfo.currentVersion,
|
|
updateInfo.latestVersion
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// ** Specific catch block for missing configuration file **
|
|
if (error instanceof ConfigurationError) {
|
|
console.error(
|
|
boxen(
|
|
chalk.red.bold('Configuration Update Required!') +
|
|
'\n\n' +
|
|
chalk.white('Taskmaster now uses the ') +
|
|
chalk.yellow.bold('.taskmasterconfig') +
|
|
chalk.white(
|
|
' file in your project root for AI model choices and settings.\n\n' +
|
|
'This file appears to be '
|
|
) +
|
|
chalk.red.bold('missing') +
|
|
chalk.white('. No worries though.\n\n') +
|
|
chalk.cyan.bold('To create this file, run the interactive setup:') +
|
|
'\n' +
|
|
chalk.green(' task-master models --setup') +
|
|
'\n\n' +
|
|
chalk.white.bold('Key Points:') +
|
|
'\n' +
|
|
chalk.white('* ') +
|
|
chalk.yellow.bold('.taskmasterconfig') +
|
|
chalk.white(
|
|
': Stores your AI model settings (do not manually edit)\n'
|
|
) +
|
|
chalk.white('* ') +
|
|
chalk.yellow.bold('.env & .mcp.json') +
|
|
chalk.white(': Still used ') +
|
|
chalk.red.bold('only') +
|
|
chalk.white(' for your AI provider API keys.\n\n') +
|
|
chalk.cyan(
|
|
'`task-master models` to check your config & available models\n'
|
|
) +
|
|
chalk.cyan(
|
|
'`task-master models --setup` to adjust the AI models used by Taskmaster'
|
|
),
|
|
{
|
|
padding: 1,
|
|
margin: { top: 1 },
|
|
borderColor: 'red',
|
|
borderStyle: 'round'
|
|
}
|
|
)
|
|
);
|
|
} else {
|
|
// Generic error handling for other errors
|
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
if (getDebugFlag()) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export {
|
|
registerCommands,
|
|
setupCLI,
|
|
runCLI,
|
|
checkForUpdate,
|
|
compareVersions,
|
|
displayUpgradeNotification
|
|
};
|