mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-06-27 00:29:58 +00:00

Resolves persistent 404 'Not Found' errors when calling Anthropic models via the Vercel AI SDK. The primary issue was likely related to incorrect or missing API headers. - Refactors Anthropic provider (src/ai-providers/anthropic.js) to use the standard 'anthropic-version' header instead of potentially outdated/incorrect beta headers when creating the client instance. - Updates the default fallback model ID in .taskmasterconfig to 'claude-3-5-sonnet-20241022'. - Fixes the interactive model setup (task-master models --setup) in scripts/modules/commands.js to correctly filter and default the main model selection. - Improves the cost display in the 'task-master models' command output to explicitly show 'Free' for models with zero cost. - Updates description for the 'id' parameter in the 'set_task_status' MCP tool definition for clarity. - Updates list of models and costs
2463 lines
70 KiB
JavaScript
2463 lines
70 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
|
|
|
|
/**
|
|
* 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('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 (default from config)',
|
|
'5' // Set a simple string default here
|
|
)
|
|
.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 idArg = options.id;
|
|
// Get the actual default if the user didn't provide --num
|
|
const numSubtasks =
|
|
options.num === '5'
|
|
? getDefaultSubtasks(null)
|
|
: parseInt(options.num, 10);
|
|
const useResearch = options.research || false;
|
|
const additionalContext = options.prompt || '';
|
|
const forceFlag = options.force || false;
|
|
const tasksPath = options.file || 'tasks/tasks.json';
|
|
|
|
if (options.all) {
|
|
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(
|
|
tasksPath,
|
|
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(
|
|
tasksPath,
|
|
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 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}`));
|
|
}
|
|
}
|
|
|
|
const newTaskId = await addTask(
|
|
options.file,
|
|
options.prompt,
|
|
dependencies,
|
|
options.priority,
|
|
{
|
|
session: process.env
|
|
},
|
|
options.research || false,
|
|
null,
|
|
manualTaskData
|
|
);
|
|
|
|
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
|
|
};
|