UNPKG

task-master-ai

Version:

A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.

1,752 lines (1,609 loc) 151 kB
/** * 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 http from 'http'; import inquirer from 'inquirer'; import search from '@inquirer/search'; import ora from 'ora'; // Import ora import { log, readJSON, writeJSON, getCurrentTag, detectCamelCaseFlags, toKebabCase } from './utils.js'; import { parsePRD, updateTasks, generateTaskFiles, setTaskStatus, listTasks, expandTask, expandAllTasks, clearSubtasks, addTask, addSubtask, removeSubtask, analyzeTaskComplexity, updateTaskById, updateSubtaskById, removeTask, findTaskById, taskExists, moveTask, migrateProject, setResponseLanguage, scopeUpTask, scopeDownTask, validateStrength } from './task-manager.js'; import { createTag, deleteTag, tags, useTag, renameTag, copyTag } from './task-manager/tag-management.js'; import { addDependency, removeDependency, validateDependenciesCommand, fixDependenciesCommand } from './dependency-manager.js'; import { isApiKeySet, getDebugFlag, getConfig, writeConfig, ConfigurationError, isConfigFilePresent, getAvailableModels, getBaseUrlForRole, getDefaultNumTasks, getDefaultSubtasks } from './config-manager.js'; import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js'; import { COMPLEXITY_REPORT_FILE, TASKMASTER_TASKS_FILE, TASKMASTER_DOCS_DIR } from '../../src/constants/paths.js'; import { initTaskMaster } from '../../src/task-master.js'; import { displayBanner, displayHelp, displayNextTask, displayTaskById, displayComplexityReport, getStatusWithColor, confirmTaskOverwrite, startLoadingIndicator, stopLoadingIndicator, displayModelConfiguration, displayAvailableModels, displayApiKeyStatus, displayAiUsageSummary, displayMultipleTasksSummary, displayTaggedTasksFYI, displayCurrentTagIndicator } from './ui.js'; import { confirmProfilesRemove, confirmRemoveAllRemainingProfiles } from '../../src/ui/confirm.js'; import { wouldRemovalLeaveNoProfiles, getInstalledProfiles } from '../../src/utils/profiles.js'; import { initializeProject } from '../init.js'; import { getModelConfiguration, getAvailableModelsList, setModel, getApiKeyStatusReport } from './task-manager/models.js'; import { isValidTaskStatus, TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js'; import { isValidRulesAction, RULES_ACTIONS, RULES_SETUP_ACTION } from '../../src/constants/rules-actions.js'; import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; import { syncTasksToReadme } from './sync-readme.js'; import { RULE_PROFILES } from '../../src/constants/profiles.js'; import { convertAllRulesToProfileRules, removeProfileRules, isValidProfile, getRulesProfile } from '../../src/utils/rule-transformer.js'; import { runInteractiveProfilesSetup, generateProfileSummary, categorizeProfileResults, generateProfileRemovalSummary, categorizeRemovalResults } from '../../src/utils/profiles.js'; /** * Runs the interactive setup process for model configuration. * @param {string|null} projectRoot - The resolved project root directory. */ async function runInteractiveSetup(projectRoot) { if (!projectRoot) { console.error( chalk.red( 'Error: Could not determine project root for interactive setup.' ) ); process.exit(1); } const currentConfigResult = await getModelConfiguration({ projectRoot }); const currentModels = currentConfigResult.success ? currentConfigResult.data.activeModels : { main: null, research: null, fallback: null }; // Handle potential config load failure gracefully for the setup flow if ( !currentConfigResult.success && currentConfigResult.error?.code !== 'CONFIG_MISSING' ) { console.warn( chalk.yellow( `Warning: Could not load current model configuration: ${currentConfigResult.error?.message || 'Unknown error'}. Proceeding with defaults.` ) ); } // Helper function to fetch OpenRouter models (duplicated for CLI context) function fetchOpenRouterModelsCLI() { return new Promise((resolve) => { const options = { hostname: 'openrouter.ai', path: '/api/v1/models', method: 'GET', headers: { Accept: 'application/json' } }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { if (res.statusCode === 200) { try { const parsedData = JSON.parse(data); resolve(parsedData.data || []); // Return the array of models } catch (e) { console.error('Error parsing OpenRouter response:', e); resolve(null); // Indicate failure } } else { console.error( `OpenRouter API request failed with status code: ${res.statusCode}` ); resolve(null); // Indicate failure } }); }); req.on('error', (e) => { console.error('Error fetching OpenRouter models:', e); resolve(null); // Indicate failure }); req.end(); }); } // Helper function to fetch Ollama models (duplicated for CLI context) function fetchOllamaModelsCLI(baseURL = 'http://localhost:11434/api') { return new Promise((resolve) => { try { // Parse the base URL to extract hostname, port, and base path const url = new URL(baseURL); const isHttps = url.protocol === 'https:'; const port = url.port || (isHttps ? 443 : 80); const basePath = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname; const options = { hostname: url.hostname, port: parseInt(port, 10), path: `${basePath}/tags`, method: 'GET', headers: { Accept: 'application/json' } }; const requestLib = isHttps ? https : http; const req = requestLib.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { if (res.statusCode === 200) { try { const parsedData = JSON.parse(data); resolve(parsedData.models || []); // Return the array of models } catch (e) { console.error('Error parsing Ollama response:', e); resolve(null); // Indicate failure } } else { console.error( `Ollama API request failed with status code: ${res.statusCode}` ); resolve(null); // Indicate failure } }); }); req.on('error', (e) => { console.error('Error fetching Ollama models:', e); resolve(null); // Indicate failure }); req.end(); } catch (e) { console.error('Error parsing Ollama base URL:', e); resolve(null); // Indicate failure } }); } // Helper to get choices and default index for a role const getPromptData = (role, allowNone = false) => { const currentModel = currentModels[role]; // Use the fetched data const allModelsRaw = getAvailableModels(); // Get all available models // Manually group models by provider const modelsByProvider = allModelsRaw.reduce((acc, model) => { if (!acc[model.provider]) { acc[model.provider] = []; } acc[model.provider].push(model); return acc; }, {}); const cancelOption = { name: '⏹ Cancel Model Setup', value: '__CANCEL__' }; // Symbol updated const noChangeOption = currentModel?.modelId ? { name: `✔ No change to current ${role} model (${currentModel.modelId})`, // Symbol updated value: '__NO_CHANGE__' } : null; // Define custom provider options const customProviderOptions = [ { name: '* Custom OpenRouter model', value: '__CUSTOM_OPENROUTER__' }, { name: '* Custom Ollama model', value: '__CUSTOM_OLLAMA__' }, { name: '* Custom Bedrock model', value: '__CUSTOM_BEDROCK__' }, { name: '* Custom Azure model', value: '__CUSTOM_AZURE__' }, { name: '* Custom Vertex model', value: '__CUSTOM_VERTEX__' } ]; let choices = []; let defaultIndex = 0; // Default to 'Cancel' // Filter and format models allowed for this role using the manually grouped data const roleChoices = Object.entries(modelsByProvider) .map(([provider, models]) => { const providerModels = models .filter((m) => m.allowed_roles.includes(role)) .map((m) => ({ name: `${provider} / ${m.id} ${ m.cost_per_1m_tokens ? chalk.gray( `($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)` ) : '' }`, value: { id: m.id, provider }, short: `${provider}/${m.id}` })); if (providerModels.length > 0) { return [...providerModels]; } return null; }) .filter(Boolean) .flat(); // Find the index of the currently selected model for setting the default let currentChoiceIndex = -1; if (currentModel?.modelId && currentModel?.provider) { currentChoiceIndex = roleChoices.findIndex( (choice) => typeof choice.value === 'object' && choice.value.id === currentModel.modelId && choice.value.provider === currentModel.provider ); } // Construct final choices list with custom options moved to bottom const systemOptions = []; if (noChangeOption) { systemOptions.push(noChangeOption); } systemOptions.push(cancelOption); const systemLength = systemOptions.length; if (allowNone) { choices = [ ...systemOptions, new inquirer.Separator('\n── Standard Models ──'), { name: '⚪ None (disable)', value: null }, ...roleChoices, new inquirer.Separator('\n── Custom Providers ──'), ...customProviderOptions ]; // Adjust default index: System + Sep1 + None (+2) const noneOptionIndex = systemLength + 1; defaultIndex = currentChoiceIndex !== -1 ? currentChoiceIndex + systemLength + 2 // Offset by system options and separators : noneOptionIndex; // Default to 'None' if no current model matched } else { choices = [ ...systemOptions, new inquirer.Separator('\n── Standard Models ──'), ...roleChoices, new inquirer.Separator('\n── Custom Providers ──'), ...customProviderOptions ]; // Adjust default index: System + Sep (+1) defaultIndex = currentChoiceIndex !== -1 ? currentChoiceIndex + systemLength + 1 // Offset by system options and separator : noChangeOption ? 1 : 0; // Default to 'No Change' if present, else 'Cancel' } // Ensure defaultIndex is valid within the final choices array length if (defaultIndex < 0 || defaultIndex >= choices.length) { // If default calculation failed or pointed outside bounds, reset intelligently defaultIndex = 0; // Default to 'Cancel' console.warn( `Warning: Could not determine default model for role '${role}'. Defaulting to 'Cancel'.` ); // Add warning } return { choices, default: defaultIndex }; }; // --- Generate choices using the helper --- const mainPromptData = getPromptData('main'); const researchPromptData = getPromptData('research'); const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback // Display helpful intro message console.log(chalk.cyan('\n🎯 Interactive Model Setup')); console.log(chalk.gray('━'.repeat(50))); console.log(chalk.yellow('💡 Navigation tips:')); console.log(chalk.gray(' • Type to search and filter options')); console.log(chalk.gray(' • Use ↑↓ arrow keys to navigate results')); console.log( chalk.gray( ' • Standard models are listed first, custom providers at bottom' ) ); console.log(chalk.gray(' • Press Enter to select\n')); // Helper function to create search source for models const createSearchSource = (choices, defaultValue) => { return (searchTerm = '') => { const filteredChoices = choices.filter((choice) => { if (choice.type === 'separator') return true; // Always show separators const searchText = choice.name || ''; return searchText.toLowerCase().includes(searchTerm.toLowerCase()); }); return Promise.resolve(filteredChoices); }; }; const answers = {}; // Main model selection answers.mainModel = await search({ message: 'Select the main model for generation/updates:', source: createSearchSource(mainPromptData.choices, mainPromptData.default), pageSize: 15 }); if (answers.mainModel !== '__CANCEL__') { // Research model selection answers.researchModel = await search({ message: 'Select the research model:', source: createSearchSource( researchPromptData.choices, researchPromptData.default ), pageSize: 15 }); if (answers.researchModel !== '__CANCEL__') { // Fallback model selection answers.fallbackModel = await search({ message: 'Select the fallback model (optional):', source: createSearchSource( fallbackPromptData.choices, fallbackPromptData.default ), pageSize: 15 }); } } let setupSuccess = true; let setupConfigModified = false; const coreOptionsSetup = { projectRoot }; // Pass root for setup actions // Helper to handle setting a model (including custom) async function handleSetModel(role, selectedValue, currentModelId) { if (selectedValue === '__CANCEL__') { console.log( chalk.yellow(`\nSetup canceled during ${role} model selection.`) ); setupSuccess = false; // Also mark success as false on cancel return false; // Indicate cancellation } // Handle the new 'No Change' option if (selectedValue === '__NO_CHANGE__') { console.log(chalk.gray(`No change selected for ${role} model.`)); return true; // Indicate success, continue setup } let modelIdToSet = null; let providerHint = null; let isCustomSelection = false; if (selectedValue === '__CUSTOM_OPENROUTER__') { isCustomSelection = true; const { customId } = await inquirer.prompt([ { type: 'input', name: 'customId', message: `Enter the custom OpenRouter Model ID for the ${role} role:` } ]); if (!customId) { console.log(chalk.yellow('No custom ID entered. Skipping role.')); return true; // Continue setup, but don't set this role } modelIdToSet = customId; providerHint = CUSTOM_PROVIDERS.OPENROUTER; // Validate against live OpenRouter list const openRouterModels = await fetchOpenRouterModelsCLI(); if ( !openRouterModels || !openRouterModels.some((m) => m.id === modelIdToSet) ) { console.error( chalk.red( `Error: Model ID "${modelIdToSet}" not found in the live OpenRouter model list. Please check the ID.` ) ); setupSuccess = false; return true; // Continue setup, but mark as failed } } else if (selectedValue === '__CUSTOM_OLLAMA__') { isCustomSelection = true; const { customId } = await inquirer.prompt([ { type: 'input', name: 'customId', message: `Enter the custom Ollama Model ID for the ${role} role:` } ]); if (!customId) { console.log(chalk.yellow('No custom ID entered. Skipping role.')); return true; // Continue setup, but don't set this role } modelIdToSet = customId; providerHint = CUSTOM_PROVIDERS.OLLAMA; // Get the Ollama base URL from config for this role const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); // Validate against live Ollama list const ollamaModels = await fetchOllamaModelsCLI(ollamaBaseURL); if (ollamaModels === null) { console.error( chalk.red( `Error: Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` ) ); setupSuccess = false; return true; // Continue setup, but mark as failed } else if (!ollamaModels.some((m) => m.model === modelIdToSet)) { console.error( chalk.red( `Error: Model ID "${modelIdToSet}" not found in the Ollama instance. Please verify the model is pulled and available.` ) ); console.log( chalk.yellow( `You can check available models with: curl ${ollamaBaseURL}/tags` ) ); setupSuccess = false; return true; // Continue setup, but mark as failed } } else if (selectedValue === '__CUSTOM_BEDROCK__') { isCustomSelection = true; const { customId } = await inquirer.prompt([ { type: 'input', name: 'customId', message: `Enter the custom Bedrock Model ID for the ${role} role (e.g., anthropic.claude-3-sonnet-20240229-v1:0):` } ]); if (!customId) { console.log(chalk.yellow('No custom ID entered. Skipping role.')); return true; // Continue setup, but don't set this role } modelIdToSet = customId; providerHint = CUSTOM_PROVIDERS.BEDROCK; // Check if AWS environment variables exist if ( !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY ) { console.warn( chalk.yellow( 'Warning: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Will fallback to system configuration. (ex: aws config files or ec2 instance profiles)' ) ); setupSuccess = false; return true; // Continue setup, but mark as failed } console.log( chalk.blue( `Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.` ) ); } else if (selectedValue === '__CUSTOM_AZURE__') { isCustomSelection = true; const { customId } = await inquirer.prompt([ { type: 'input', name: 'customId', message: `Enter the custom Azure OpenAI Model ID for the ${role} role (e.g., gpt-4o):` } ]); if (!customId) { console.log(chalk.yellow('No custom ID entered. Skipping role.')); return true; // Continue setup, but don't set this role } modelIdToSet = customId; providerHint = CUSTOM_PROVIDERS.AZURE; // Check if Azure environment variables exist if ( !process.env.AZURE_OPENAI_API_KEY || !process.env.AZURE_OPENAI_ENDPOINT ) { console.error( chalk.red( 'Error: AZURE_OPENAI_API_KEY and/or AZURE_OPENAI_ENDPOINT environment variables are missing. Please set them before using custom Azure models.' ) ); setupSuccess = false; return true; // Continue setup, but mark as failed } console.log( chalk.blue( `Custom Azure OpenAI model "${modelIdToSet}" will be used. No validation performed.` ) ); } else if (selectedValue === '__CUSTOM_VERTEX__') { isCustomSelection = true; const { customId } = await inquirer.prompt([ { type: 'input', name: 'customId', message: `Enter the custom Vertex AI Model ID for the ${role} role (e.g., gemini-1.5-pro-002):` } ]); if (!customId) { console.log(chalk.yellow('No custom ID entered. Skipping role.')); return true; // Continue setup, but don't set this role } modelIdToSet = customId; providerHint = CUSTOM_PROVIDERS.VERTEX; // Check if Google/Vertex environment variables exist if ( !process.env.GOOGLE_API_KEY && !process.env.GOOGLE_APPLICATION_CREDENTIALS ) { console.error( chalk.red( 'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using custom Vertex models.' ) ); setupSuccess = false; return true; // Continue setup, but mark as failed } console.log( chalk.blue( `Custom Vertex AI model "${modelIdToSet}" will be used. No validation performed.` ) ); } else if ( selectedValue && typeof selectedValue === 'object' && selectedValue.id ) { // Standard model selected from list modelIdToSet = selectedValue.id; providerHint = selectedValue.provider; // Provider is known } else if (selectedValue === null && role === 'fallback') { // Handle disabling fallback modelIdToSet = null; providerHint = null; } else if (selectedValue) { console.error( chalk.red( `Internal Error: Unexpected selection value for ${role}: ${JSON.stringify(selectedValue)}` ) ); setupSuccess = false; return true; } // Only proceed if there's a change to be made if (modelIdToSet !== currentModelId) { if (modelIdToSet) { // Set a specific model (standard or custom) const result = await setModel(role, modelIdToSet, { ...coreOptionsSetup, providerHint // Pass the hint }); if (result.success) { console.log( chalk.blue( `Set ${role} model: ${result.data.provider} / ${result.data.modelId}` ) ); if (result.data.warning) { // Display warning if returned by setModel console.log(chalk.yellow(result.data.warning)); } setupConfigModified = true; } else { console.error( chalk.red( `Error setting ${role} model: ${result.error?.message || 'Unknown'}` ) ); setupSuccess = false; } } else if (role === 'fallback') { // Disable fallback model const currentCfg = getConfig(projectRoot); if (currentCfg?.models?.fallback?.modelId) { // Check if it was actually set before clearing currentCfg.models.fallback = { ...currentCfg.models.fallback, provider: undefined, modelId: undefined }; if (writeConfig(currentCfg, projectRoot)) { console.log(chalk.blue('Fallback model disabled.')); setupConfigModified = true; } else { console.error( chalk.red('Failed to disable fallback model in config file.') ); setupSuccess = false; } } else { console.log(chalk.blue('Fallback model was already disabled.')); } } } return true; // Indicate setup should continue } // Process answers using the handler if ( !(await handleSetModel( 'main', answers.mainModel, currentModels.main?.modelId // <--- Now 'currentModels' is defined )) ) { return false; // Explicitly return false if cancelled } if ( !(await handleSetModel( 'research', answers.researchModel, currentModels.research?.modelId // <--- Now 'currentModels' is defined )) ) { return false; // Explicitly return false if cancelled } if ( !(await handleSetModel( 'fallback', answers.fallbackModel, currentModels.fallback?.modelId // <--- Now 'currentModels' is defined )) ) { return false; // Explicitly return false if cancelled } 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 true; // Indicate setup flow completed (not cancelled) // Let the main command flow continue to display results } /** * 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); }); // 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') .option( '-n, --num-tasks <number>', 'Number of tasks to generate', getDefaultNumTasks() ) .option('-f, --force', 'Skip confirmation when overwriting existing tasks') .option( '--append', 'Append new tasks to existing tasks.json instead of overwriting' ) .option( '-r, --research', 'Use Perplexity AI for research-backed task generation, providing more comprehensive and accurate task breakdown' ) .option('--tag <tag>', 'Specify tag context for task operations') .action(async (file, options) => { // Initialize TaskMaster let taskMaster; try { const initOptions = { prdPath: file || options.input || true, tag: options.tag }; // Only include tasksPath if output is explicitly specified if (options.output) { initOptions.tasksPath = options.output; } taskMaster = initTaskMaster(initOptions); } catch (error) { 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: .taskmaster/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 --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\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 task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${TASKMASTER_DOCS_DIR}/PRD.md by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`, { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); console.error(chalk.red(`\nError: ${error.message}`)); process.exit(1); } const numTasks = parseInt(options.numTasks, 10); const force = options.force || false; const append = options.append || false; const research = options.research || false; let useForce = force; const useAppend = append; // Resolve tag using standard pattern const tag = taskMaster.getCurrentTag(); // Show current tag context displayCurrentTagIndicator(tag); // Helper function to check if there are existing tasks in the target tag and confirm overwrite async function confirmOverwriteIfNeeded() { // Check if there are existing tasks in the target tag let hasExistingTasksInTag = false; const tasksPath = taskMaster.getTasksPath(); if (fs.existsSync(tasksPath)) { try { // Read the entire file to check if the tag exists const existingFileContent = fs.readFileSync(tasksPath, 'utf8'); const allData = JSON.parse(existingFileContent); // Check if the target tag exists and has tasks if ( allData[tag] && Array.isArray(allData[tag].tasks) && allData[tag].tasks.length > 0 ) { hasExistingTasksInTag = true; } } catch (error) { // If we can't read the file or parse it, assume no existing tasks in this tag hasExistingTasksInTag = false; } } // Only show confirmation if there are existing tasks in the target tag if (hasExistingTasksInTag && !useForce && !useAppend) { const overwrite = await confirmTaskOverwrite(tasksPath); if (!overwrite) { log('info', 'Operation cancelled.'); return false; } // If user confirms 'y', we should set useForce = true for the parsePRD call // Only overwrite if not appending useForce = true; } return true; } let spinner; try { if (!(await confirmOverwriteIfNeeded())) return; console.log(chalk.blue(`Parsing PRD file: ${taskMaster.getPrdPath()}`)); console.log(chalk.blue(`Generating ${numTasks} tasks...`)); if (append) { console.log(chalk.blue('Appending to existing tasks...')); } if (research) { console.log( chalk.blue( 'Using Perplexity AI for research-backed task generation' ) ); } spinner = ora('Parsing PRD and generating tasks...\n').start(); // Handle case where getTasksPath() returns null const outputPath = taskMaster.getTasksPath() || path.join(taskMaster.getProjectRoot(), TASKMASTER_TASKS_FILE); await parsePRD(taskMaster.getPrdPath(), outputPath, numTasks, { append: useAppend, force: useForce, research: research, projectRoot: taskMaster.getProjectRoot(), tag: tag }); spinner.succeed('Tasks generated successfully!'); } catch (error) { if (spinner) { spinner.fail(`Error parsing PRD: ${error.message}`); } else { console.error(chalk.red(`Error parsing PRD: ${error.message}`)); } process.exit(1); } }); // 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', TASKMASTER_TASKS_FILE ) .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' ) .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); const fromId = parseInt(options.from, 10); // Validation happens here const prompt = options.prompt; const useResearch = options.research || false; const tasksPath = taskMaster.getTasksPath(); // Resolve tag using standard pattern const tag = taskMaster.getCurrentTag(); // Show current tag context displayCurrentTagIndicator(tag); // 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') ); } // Call core updateTasks, passing context for CLI await updateTasks( taskMaster.getTasksPath(), fromId, prompt, useResearch, { projectRoot: taskMaster.getProjectRoot(), tag } // Pass context with projectRoot and tag ); }); // 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', TASKMASTER_TASKS_FILE ) .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' ) .option( '--append', 'Append timestamped information to task details instead of full update' ) .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { try { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); const tasksPath = taskMaster.getTasksPath(); // Resolve tag using standard pattern const tag = taskMaster.getCurrentTag(); // Show current tag context displayCurrentTagIndicator(tag); // 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 (Number.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 === TASKMASTER_TASKS_FILE) { 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( taskMaster.getTasksPath(), taskId, prompt, useResearch, { projectRoot: taskMaster.getProjectRoot(), tag }, 'text', options.append || false ); // 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', TASKMASTER_TASKS_FILE ) .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') .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { try { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); const tasksPath = taskMaster.getTasksPath(); // Resolve tag using standard pattern const tag = taskMaster.getCurrentTag(); // Show current tag context displayCurrentTagIndicator(tag); // 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 === TASKMASTER_TASKS_FILE) { 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( taskMaster.getTasksPath(), subtaskId, prompt, useResearch, { projectRoot: taskMaster.getProjectRoot(), tag } ); 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); } }); // scope-up command programInstance .command('scope-up') .description('Increase task complexity with AI assistance') .option( '-f, --file <file>', 'Path to the tasks file', TASKMASTER_TASKS_FILE ) .option( '-i, --id <ids>', 'Comma-separated task/subtask IDs to scope up (required)' ) .option( '-s, --strength <level>', 'Complexity increase strength: light, regular, heavy', 'regular' ) .option( '-p, --prompt <text>', 'Custom instructions for targeted scope adjustments' ) .option('-r, --research', 'Use research AI for more informed adjustments') .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { try { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); const tasksPath = taskMaster.getTasksPath(); const tag = taskMaster.getCurrentTag(); // Show current tag context displayCurrentTagIndicator(tag); // Validate required parameters if (!options.id) { console.error(chalk.red('Error: --id parameter is required')); console.log( chalk.yellow( 'Usage example: task-master scope-up --id=1,2,3 --strength=regular' ) ); process.exit(1); } // Parse and validate task IDs const taskIds = options.id.split(',').map((id) => { const parsed = parseInt(id.trim(), 10); if (Number.isNaN(parsed) || parsed <= 0) { console.error(chalk.red(`Error: Invalid task ID: ${id.trim()}`)); process.exit(1); } return parsed; }); // Validate strength level if (!validateStrength(options.strength)) { console.error( chalk.red( `Error: Invalid strength level: ${options.strength}. Must be one of: light, regular, heavy` ) ); process.exit(1); } // Validate tasks file exists if (!fs.existsSync(tasksPath)) { console.error( chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) ); process.exit(1); } console.log( chalk.blue( `Scoping up ${taskIds.length} task(s): ${taskIds.join(', ')}` ) ); console.log(chalk.blue(`Strength level: ${options.strength}`)); if (options.prompt) { console.log(chalk.blue(`Custom instructions: ${options.prompt}`)); } const context = { projectRoot: taskMaster.getProjectRoot(), tag, commandName: 'scope-up', outputType: 'cli' }; const result = await scopeUpTask( tasksPath, taskIds, options.strength, options.prompt || null, context, 'text' ); console.log( chalk.green( `✅ Successfully scoped up ${result.updatedTasks.length} task(s)` ) ); } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); if (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 valid task IDs with the --id parameter'); } if (getDebugFlag()) { console.error(error); } process.exit(1); } }); // scope-down command programInstance .command('scope-down') .description('Decrease task complexity with AI assistance') .option( '-f, --file <file>', 'Path to the tasks file', TASKMASTER_TASKS_FILE ) .option( '-i, --id <ids>', 'Comma-separated task/subtask IDs to scope down (required)' ) .option( '-s, --strength <level>', 'Complexity decrease strength: light, regular, heavy', 'regular' ) .option( '-p, --prompt <text>', 'Custom instructions for targeted scope adjustments' ) .option('-r, --research', 'Use research AI for more informed adjustments') .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { try { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); const tasksPath = taskMaster.getTasksPath(); const tag = taskMaster.getCurrentTag(); // Show current tag context displayCurrentTagIndicator(tag); // Validate required parameters if (!options.id) { console.error(chalk.red('Error: --id parameter is required')); console.log( chalk.yellow( 'Usage example: task-master scope-down --id=1,2,3 --strength=regular' ) ); process.exit(1); } // Parse and validate task IDs const taskIds = options.id.split(',').map((id) => { const parsed = parseInt(id.trim(), 10); if (Number.isNaN(parsed) || parsed <= 0) { console.error(chalk.red(`Error: Invalid task ID: ${id.trim()}`)); process.exit(1); } return parsed; }); // Validate strength level if (!validateStrength(options.strength)) { console.error( chalk.red( `Error: Invalid strength level: ${options.strength}. Must be one of: light, regular, heavy` ) ); process.exit(1); } // Validate tasks file exists if (!fs.existsSync(tasksPath)) { console.error( chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) ); process.exit(1); } console.log( chalk.blue( `Scoping down ${taskIds.length} task(s): ${taskIds.join(', ')}` ) ); console.log(chalk.blue(`Strength level: ${options.strength}`)); if (options.prompt) { console.log(chalk.blue(`Custom instructions: ${options.prompt}`)); } const context = { projectRoot: taskMaster.getProjectRoot(), tag, commandName: 'scope-down', outputType: 'cli' }; const result = await scopeDownTask( tasksPath, taskIds, options.strength, options.prompt || null, context, 'text' ); console.log( chalk.green( `✅ Successfully scoped down ${result.updatedTasks.length} task(s)` ) ); } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); if (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 valid task IDs with the --id parameter'); } 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', TASKMASTER_TASKS_FILE ) .option( '-o, --output <dir>', 'Output directory', path.dirname(TASKMASTER_TASKS_FILE) ) .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); const outputDir = options.output; const tag = taskMaster.getCurrentTag(); console.log( chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`) ); console.log(chalk.blue(`Output directory: ${outputDir}`)); await generateTaskFiles(taskMaster.getTasksPath(), outputDir, { projectRoot: taskMaster.getProjectRoot(), tag }); }); // set-status command programInstance .command('set-status') .alias('mark') .alias('set') .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 (one of: ${TASK_STATUS_OPTIONS.join(', ')})` ) .option( '-f, --file <file>', 'Path to the tasks file', TASKMASTER_TASKS_FILE ) .option('--tag <tag>', 'Specify tag context for task operations') .action(async (options) => { // Initialize TaskMaster const taskMaster = initTaskMaster({ tasksPath: options.file || true, tag: options.tag }); 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); } if (!isValidTaskStatus(status)) { console.error( chalk.red( `Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` ) ); process.exit(1); } const tag = taskMaster.getCurrentTag(); displayCurrentTagIndicator(tag); console.log( chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`) ); await setTaskStatus(taskMaster.getTasksPath(), taskId, status, { projectRoot: taskMaster.getProjectRoot(), tag }); }); // list command programInstance .command('list') .description('List all tasks') .option( '-f, --file <file>', 'Path to the tasks file', TASKMASTER_TASKS_FILE ) .option( '-r, --report <report>', 'Path to the complexity report file', COMPLEXITY_REPORT_FILE ) .opt