Files
claude-task-master/scripts/modules/commands.js
Eyal Toledano 9b87dd23de fix(gateway/auth): Implement proper auth/init flow with automatic background userId generation
- Fix getUserId() to use placeholder that triggers auth/init if the auth/init endpoint is down for whatever reason
- Add silent auth/init attempt in AI services
- Improve hosted mode error handling
- Remove fake userId/email generation from init.js
2025-05-31 19:47:18 -04:00

3200 lines
103 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 http from "http";
import inquirer from "inquirer";
import ora from "ora"; // Import ora
import { log, readJSON, findProjectRoot } from "./utils.js";
import {
parsePRD,
updateTasks,
generateTaskFiles,
setTaskStatus,
listTasks,
expandTask,
expandAllTasks,
clearSubtasks,
addTask,
addSubtask,
removeSubtask,
analyzeTaskComplexity,
updateTaskById,
updateSubtaskById,
removeTask,
findTaskById,
taskExists,
moveTask,
} from "./task-manager.js";
import {
addDependency,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand,
} from "./dependency-manager.js";
import {
isApiKeySet,
getDebugFlag,
getConfig,
writeConfig,
ConfigurationError,
isConfigFilePresent,
getAvailableModels,
getBaseUrlForRole,
} from "./config-manager.js";
import {
displayBanner,
displayHelp,
displayNextTask,
displayTaskById,
displayComplexityReport,
getStatusWithColor,
confirmTaskOverwrite,
startLoadingIndicator,
stopLoadingIndicator,
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayAiUsageSummary,
displayMultipleTasksSummary,
} from "./ui.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 { getTaskMasterVersion } from "../../src/utils/getVersion.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;
const customOpenRouterOption = {
name: "* Custom OpenRouter model", // Symbol updated
value: "__CUSTOM_OPENROUTER__",
};
const customOllamaOption = {
name: "* Custom Ollama model", // Symbol updated
value: "__CUSTOM_OLLAMA__",
};
const customBedrockOption = {
name: "* Custom Bedrock model", // Add Bedrock custom option
value: "__CUSTOM_BEDROCK__",
};
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 based on whether 'None' is allowed
const commonPrefix = [];
if (noChangeOption) {
commonPrefix.push(noChangeOption);
}
commonPrefix.push(cancelOption);
commonPrefix.push(customOpenRouterOption);
commonPrefix.push(customOllamaOption);
commonPrefix.push(customBedrockOption);
let prefixLength = commonPrefix.length; // Initial prefix length
if (allowNone) {
choices = [
...commonPrefix,
new inquirer.Separator(),
{ name: "⚪ None (disable)", value: null }, // Symbol updated
new inquirer.Separator(),
...roleChoices,
];
// Adjust default index: Prefix + Sep1 + None + Sep2 (+3)
const noneOptionIndex = prefixLength + 1;
defaultIndex =
currentChoiceIndex !== -1
? currentChoiceIndex + prefixLength + 3 // Offset by prefix and separators
: noneOptionIndex; // Default to 'None' if no current model matched
} else {
choices = [
...commonPrefix,
new inquirer.Separator(),
...roleChoices,
new inquirer.Separator(),
];
// Adjust default index: Prefix + Sep (+1)
defaultIndex =
currentChoiceIndex !== -1
? currentChoiceIndex + prefixLength + 1 // Offset by prefix 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
const answers = await inquirer.prompt([
{
type: "list",
name: "mainModel",
message: "Select the main model for generation/updates:",
choices: mainPromptData.choices,
default: mainPromptData.default,
},
{
type: "list",
name: "researchModel",
message: "Select the research model:",
choices: researchPromptData.choices,
default: researchPromptData.default,
when: (ans) => ans.mainModel !== "__CANCEL__",
},
{
type: "list",
name: "fallbackModel",
message: "Select the fallback model (optional):",
choices: fallbackPromptData.choices,
default: fallbackPromptData.default,
when: (ans) =>
ans.mainModel !== "__CANCEL__" && ans.researchModel !== "__CANCEL__",
},
]);
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 = "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 = "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 = "bedrock";
// Check if AWS environment variables exist
if (
!process.env.AWS_ACCESS_KEY_ID ||
!process.env.AWS_SECRET_ACCESS_KEY
) {
console.error(
chalk.red(
`Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.`
)
);
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 &&
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", "tasks/tasks.json")
.option("-n, --num-tasks <number>", "Number of tasks to generate", "10")
.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"
)
.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;
const append = options.append || false;
const research = options.research || false;
let useForce = force;
let useAppend = append;
// Helper function to check if tasks.json exists and confirm overwrite
async function confirmOverwriteIfNeeded() {
if (fs.existsSync(outputPath) && !useForce && !useAppend) {
const overwrite = await confirmTaskOverwrite(outputPath);
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 (!inputFile) {
if (fs.existsSync(defaultPrdPath)) {
console.log(
chalk.blue(`Using default PRD file path: ${defaultPrdPath}`)
);
if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
spinner = ora("Parsing PRD and generating tasks...\n").start();
await parsePRD(defaultPrdPath, outputPath, numTasks, {
append: useAppend, // Changed key from useAppend to append
force: useForce, // Changed key from useForce to force
research: research,
});
spinner.succeed("Tasks generated successfully!");
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" +
" --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 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 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" }
)
);
return;
}
if (!fs.existsSync(inputFile)) {
console.error(
chalk.red(`Error: Input PRD file not found: ${inputFile}`)
);
process.exit(1);
}
if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
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();
await parsePRD(inputFile, outputPath, numTasks, {
append: useAppend,
force: useForce,
research: research,
});
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", "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); // Validation happens here
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")
);
}
// Call core updateTasks, passing empty context for CLI
await updateTasks(
tasksPath,
fromId,
prompt,
useResearch,
{} // Pass empty context
);
});
// 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")
.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", "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);
}
if (!isValidTaskStatus(status)) {
console.error(
chalk.red(
`Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}`
)
);
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(
"-r, --report <report>",
"Path to the complexity report file",
"scripts/task-complexity-report.json"
)
.option("-s, --status <status>", "Filter by status")
.option("--with-subtasks", "Show subtasks for each task")
.action(async (options) => {
const tasksPath = options.file;
const reportPath = options.report;
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, reportPath, withSubtasks);
});
// expand command
programInstance
.command("expand")
.description("Expand a task into subtasks using AI")
.option("-i, --id <id>", "ID of the task to expand")
.option(
"-a, --all",
"Expand all pending tasks based on complexity analysis"
)
.option(
"-n, --num <number>",
"Number of subtasks to generate (uses complexity analysis by default if available)"
)
.option(
"-r, --research",
"Enable research-backed generation (e.g., using Perplexity)",
false
)
.option("-p, --prompt <text>", "Additional context for subtask generation")
.option("-f, --force", "Force expansion even if subtasks exist", false) // Ensure force option exists
.option(
"--file <file>",
"Path to the tasks file (relative to project root)",
"tasks/tasks.json"
) // Allow file override
.action(async (options) => {
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.error(chalk.red("Error: Could not find project root."));
process.exit(1);
}
const tasksPath = path.resolve(projectRoot, options.file); // Resolve tasks path
if (options.all) {
// --- Handle expand --all ---
console.log(chalk.blue("Expanding all pending tasks..."));
// Updated call to the refactored expandAllTasks
try {
const result = await expandAllTasks(
tasksPath,
options.num, // Pass num
options.research, // Pass research flag
options.prompt, // Pass additional context
options.force, // Pass force flag
{} // Pass empty context for CLI calls
// outputFormat defaults to 'text' in expandAllTasks for CLI
);
} catch (error) {
console.error(
chalk.red(`Error expanding all tasks: ${error.message}`)
);
process.exit(1);
}
} else if (options.id) {
// --- Handle expand --id <id> (Should be correct from previous refactor) ---
if (!options.id) {
console.error(
chalk.red("Error: Task ID is required unless using --all.")
);
process.exit(1);
}
console.log(chalk.blue(`Expanding task ${options.id}...`));
try {
// Call the refactored expandTask function
await expandTask(
tasksPath,
options.id,
options.num,
options.research,
options.prompt,
{}, // Pass empty context for CLI calls
options.force // Pass the force flag down
);
// expandTask logs its own success/failure for single task
} catch (error) {
console.error(
chalk.red(`Error expanding task ${options.id}: ${error.message}`)
);
process.exit(1);
}
} else {
console.error(
chalk.red("Error: You must specify either a task ID (--id) or --all.")
);
programInstance.help(); // Show help
}
});
// analyze-complexity command
programInstance
.command("analyze-complexity")
.description(
`Analyze tasks and generate expansion recommendations${chalk.reset("")}`
)
.option(
"-o, --output <file>",
"Output file path for the report",
"scripts/task-complexity-report.json"
)
.option(
"-m, --model <model>",
"LLM model to use for analysis (defaults to configured model)"
)
.option(
"-t, --threshold <number>",
"Minimum complexity score to recommend expansion (1-10)",
"5"
)
.option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json")
.option(
"-r, --research",
"Use Perplexity AI for research-backed complexity analysis"
)
.option(
"-i, --id <ids>",
'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")'
)
.option("--from <id>", "Starting task ID in a range to analyze")
.option("--to <id>", "Ending task ID in a range to analyze")
.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 (options.id) {
console.log(chalk.blue(`Analyzing specific task IDs: ${options.id}`));
} else if (options.from || options.to) {
const fromStr = options.from ? options.from : "first";
const toStr = options.to ? options.to : "last";
console.log(
chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`)
);
}
if (useResearch) {
console.log(
chalk.blue(
"Using Perplexity AI for research-backed complexity analysis"
)
);
}
await analyzeTaskComplexity(options);
});
// research command
programInstance
.command("research")
.description("Perform AI-powered research queries with project context")
.argument("<prompt>", "Research prompt to investigate")
.option("--file <file>", "Path to the tasks file", "tasks/tasks.json")
.option(
"-i, --id <ids>",
'Comma-separated task/subtask IDs to include as context (e.g., "15,16.2")'
)
.option(
"-f, --files <paths>",
"Comma-separated file paths to include as context"
)
.option(
"-c, --context <text>",
"Additional custom context to include in the research prompt"
)
.option(
"-t, --tree",
"Include project file tree structure in the research context"
)
.option(
"-s, --save <file>",
"Save research results to the specified task/subtask(s)"
)
.option(
"-d, --detail <level>",
"Output detail level: low, medium, high",
"medium"
)
.action(async (prompt, options) => {
// Parameter validation
if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) {
console.error(
chalk.red("Error: Research prompt is required and cannot be empty")
);
process.exit(1);
}
// Validate detail level
const validDetailLevels = ["low", "medium", "high"];
if (
options.detail &&
!validDetailLevels.includes(options.detail.toLowerCase())
) {
console.error(
chalk.red(
`Error: Detail level must be one of: ${validDetailLevels.join(", ")}`
)
);
process.exit(1);
}
// Validate and parse task IDs if provided
let taskIds = [];
if (options.id) {
try {
taskIds = options.id.split(",").map((id) => {
const trimmedId = id.trim();
// Support both task IDs (e.g., "15") and subtask IDs (e.g., "15.2")
if (!/^\d+(\.\d+)?$/.test(trimmedId)) {
throw new Error(
`Invalid task ID format: "${trimmedId}". Expected format: "15" or "15.2"`
);
}
return trimmedId;
});
} catch (error) {
console.error(chalk.red(`Error parsing task IDs: ${error.message}`));
process.exit(1);
}
}
// Validate and parse file paths if provided
let filePaths = [];
if (options.files) {
try {
filePaths = options.files.split(",").map((filePath) => {
const trimmedPath = filePath.trim();
if (trimmedPath.length === 0) {
throw new Error("Empty file path provided");
}
return trimmedPath;
});
} catch (error) {
console.error(
chalk.red(`Error parsing file paths: ${error.message}`)
);
process.exit(1);
}
}
// Validate save option if provided
if (options.save) {
const saveTarget = options.save.trim();
if (saveTarget.length === 0) {
console.error(chalk.red("Error: Save target cannot be empty"));
process.exit(1);
}
// Check if it's a valid file path (basic validation)
if (saveTarget.includes("..") || saveTarget.startsWith("/")) {
console.error(
chalk.red(
'Error: Save path must be relative and cannot contain ".."'
)
);
process.exit(1);
}
}
// Determine project root and tasks file path
const projectRoot = findProjectRoot() || ".";
const tasksPath =
options.file || path.join(projectRoot, "tasks", "tasks.json");
// Validate tasks file exists if task IDs are specified
if (taskIds.length > 0) {
try {
const tasksData = readJSON(tasksPath);
if (!tasksData || !tasksData.tasks) {
console.error(
chalk.red(`Error: No valid tasks found in ${tasksPath}`)
);
process.exit(1);
}
} catch (error) {
console.error(
chalk.red(`Error reading tasks file: ${error.message}`)
);
process.exit(1);
}
}
// Validate file paths exist if specified
if (filePaths.length > 0) {
for (const filePath of filePaths) {
const fullPath = path.isAbsolute(filePath)
? filePath
: path.join(projectRoot, filePath);
if (!fs.existsSync(fullPath)) {
console.error(chalk.red(`Error: File not found: ${filePath}`));
process.exit(1);
}
}
}
// Create validated parameters object
const validatedParams = {
prompt: prompt.trim(),
taskIds: taskIds,
filePaths: filePaths,
customContext: options.context ? options.context.trim() : null,
includeProjectTree: !!options.tree,
saveTarget: options.save ? options.save.trim() : null,
detailLevel: options.detail ? options.detail.toLowerCase() : "medium",
tasksPath: tasksPath,
projectRoot: projectRoot,
};
// Display what we're about to do
console.log(chalk.blue(`Researching: "${validatedParams.prompt}"`));
if (validatedParams.taskIds.length > 0) {
console.log(
chalk.gray(`Task context: ${validatedParams.taskIds.join(", ")}`)
);
}
if (validatedParams.filePaths.length > 0) {
console.log(
chalk.gray(`File context: ${validatedParams.filePaths.join(", ")}`)
);
}
if (validatedParams.customContext) {
console.log(
chalk.gray(
`Custom context: ${validatedParams.customContext.substring(0, 50)}${validatedParams.customContext.length > 50 ? "..." : ""}`
)
);
}
if (validatedParams.includeProjectTree) {
console.log(chalk.gray("Including project file tree"));
}
console.log(chalk.gray(`Detail level: ${validatedParams.detailLevel}`));
try {
// Import the research function
const { performResearch } = await import("./task-manager/research.js");
// Prepare research options
const researchOptions = {
taskIds: validatedParams.taskIds,
filePaths: validatedParams.filePaths,
customContext: validatedParams.customContext || "",
includeProjectTree: validatedParams.includeProjectTree,
detailLevel: validatedParams.detailLevel,
projectRoot: validatedParams.projectRoot,
};
// Execute research
const result = await performResearch(
validatedParams.prompt,
researchOptions,
{
commandName: "research",
outputType: "cli",
},
"text"
);
// Save results if requested
if (validatedParams.saveTarget) {
const saveContent = `# Research Query: ${validatedParams.prompt}
**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toISOString()}
## Results
${result.result}
`;
fs.writeFileSync(validatedParams.saveTarget, saveContent, "utf-8");
console.log(
chalk.green(`\n💾 Results saved to: ${validatedParams.saveTarget}`)
);
}
} catch (error) {
console.error(chalk.red(`\n❌ Research failed: ${error.message}`));
process.exit(1);
}
});
// 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, optionally providing manual details")
.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(
"--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);
}
const tasksPath =
options.file ||
path.join(findProjectRoot() || ".", "tasks", "tasks.json") || // Ensure tasksPath is also relative to a found root or current dir
"tasks/tasks.json";
// Correctly determine projectRoot
const projectRoot = findProjectRoot();
let manualTaskData = null;
if (isManualCreation) {
manualTaskData = {
title: options.title,
description: options.description,
details: options.details || "",
testStrategy: options.testStrategy || "",
};
// Restore specific logging for manual creation
console.log(
chalk.blue(`Creating task manually with title: "${options.title}"`)
);
} else {
// Restore specific logging for AI creation
console.log(
chalk.blue(`Creating task with AI using prompt: "${options.prompt}"`)
);
}
// Log dependencies and priority if provided (restored)
const dependenciesArray = options.dependencies
? options.dependencies.split(",").map((id) => id.trim())
: [];
if (dependenciesArray.length > 0) {
console.log(
chalk.blue(`Dependencies: [${dependenciesArray.join(", ")}]`)
);
}
if (options.priority) {
console.log(chalk.blue(`Priority: ${options.priority}`));
}
const context = {
projectRoot,
commandName: "add-task",
outputType: "cli",
};
try {
const { newTaskId, telemetryData } = await addTask(
tasksPath,
options.prompt,
dependenciesArray,
options.priority,
context,
"text",
manualTaskData,
options.research
);
// addTask handles detailed CLI success logging AND telemetry display when outputFormat is 'text'
// No need to call displayAiUsageSummary here anymore.
} catch (error) {
// addTask already handles error reporting for CLI output
// Just exit with error code
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")
.option(
"-r, --report <report>",
"Path to the complexity report file",
"scripts/task-complexity-report.json"
)
.action(async (options) => {
const tasksPath = options.file;
const reportPath = options.report;
await displayNextTask(tasksPath, reportPath);
});
// show command
programInstance
.command("show")
.description(
`Display detailed information about one or more tasks${chalk.reset("")}`
)
.argument("[id]", "Task ID(s) to show (comma-separated for multiple)")
.option(
"-i, --id <id>",
"Task ID(s) to show (comma-separated for multiple)"
)
.option("-s, --status <status>", "Filter subtasks by status")
.option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json")
.option(
"-r, --report <report>",
"Path to the complexity report file",
"scripts/task-complexity-report.json"
)
.action(async (taskId, options) => {
const idArg = taskId || options.id;
const statusFilter = options.status;
if (!idArg) {
console.error(chalk.red("Error: Please provide a task ID"));
process.exit(1);
}
const tasksPath = options.file;
const reportPath = options.report;
// Check if multiple IDs are provided (comma-separated)
const taskIds = idArg
.split(",")
.map((id) => id.trim())
.filter((id) => id.length > 0);
if (taskIds.length > 1) {
// Multiple tasks - use compact summary view with interactive drill-down
await displayMultipleTasksSummary(
tasksPath,
taskIds,
reportPath,
statusFilter
);
} else {
// Single task - use detailed view
await displayTaskById(tasksPath, taskIds[0], reportPath, statusFilter);
}
});
// 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}`));
showAddSubtaskHelp();
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 one or more tasks or subtasks permanently")
.description("Remove one or more tasks or subtasks permanently")
.option(
"-i, --id <ids>",
'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")'
)
.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 taskIdsString = options.id;
if (!taskIdsString) {
console.error(chalk.red("Error: Task ID(s) are required"));
console.error(
chalk.yellow(
"Usage: task-master remove-task --id=<taskId1,taskId2...>"
)
);
process.exit(1);
}
const taskIdsToRemove = taskIdsString
.split(",")
.map((id) => id.trim())
.filter(Boolean);
if (taskIdsToRemove.length === 0) {
console.error(chalk.red("Error: No valid task IDs provided."));
process.exit(1);
}
try {
// Read data once for checks and confirmation
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
console.error(
chalk.red(`Error: No valid tasks found in ${tasksPath}`)
);
process.exit(1);
}
const existingTasksToRemove = [];
const nonExistentIds = [];
let totalSubtasksToDelete = 0;
const dependentTaskMessages = [];
for (const taskId of taskIdsToRemove) {
if (!taskExists(data.tasks, taskId)) {
nonExistentIds.push(taskId);
} else {
// Correctly extract the task object from the result of findTaskById
const findResult = findTaskById(data.tasks, taskId);
const taskObject = findResult.task; // Get the actual task/subtask object
if (taskObject) {
existingTasksToRemove.push({ id: taskId, task: taskObject }); // Push the actual task object
// If it's a main task, count its subtasks and check dependents
if (!taskObject.isSubtask) {
// Check the actual task object
if (taskObject.subtasks && taskObject.subtasks.length > 0) {
totalSubtasksToDelete += taskObject.subtasks.length;
}
const dependentTasks = data.tasks.filter(
(t) =>
t.dependencies &&
t.dependencies.includes(parseInt(taskId, 10))
);
if (dependentTasks.length > 0) {
dependentTaskMessages.push(
` - Task ${taskId}: ${dependentTasks.length} dependent tasks (${dependentTasks.map((t) => t.id).join(", ")})`
);
}
}
} else {
// Handle case where findTaskById returned null for the task property (should be rare)
nonExistentIds.push(`${taskId} (error finding details)`);
}
}
}
if (nonExistentIds.length > 0) {
console.warn(
chalk.yellow(
`Warning: The following task IDs were not found: ${nonExistentIds.join(", ")}`
)
);
}
if (existingTasksToRemove.length === 0) {
console.log(chalk.blue("No existing tasks found to remove."));
process.exit(0);
}
// Skip confirmation if --yes flag is provided
if (!options.yes) {
console.log();
console.log(
chalk.red.bold(
`⚠️ WARNING: This will permanently delete the following ${existingTasksToRemove.length} item(s):`
)
);
console.log();
existingTasksToRemove.forEach(({ id, task }) => {
if (!task) return; // Should not happen due to taskExists check, but safeguard
if (task.isSubtask) {
// Subtask - title is directly on the task object
console.log(
chalk.white(` Subtask ${id}: ${task.title || "(no title)"}`)
);
// Optionally show parent context if available
if (task.parentTask) {
console.log(
chalk.gray(
` (Parent: ${task.parentTask.id} - ${task.parentTask.title || "(no title)"})`
)
);
}
} else {
// Main task - title is directly on the task object
console.log(
chalk.white.bold(` Task ${id}: ${task.title || "(no title)"}`)
);
}
});
if (totalSubtasksToDelete > 0) {
console.log(
chalk.yellow(
`⚠️ This will also delete ${totalSubtasksToDelete} subtasks associated with the selected main tasks!`
)
);
}
if (dependentTaskMessages.length > 0) {
console.log(
chalk.yellow(
"⚠️ Warning: Dependencies on the following tasks will be removed:"
)
);
dependentTaskMessages.forEach((msg) =>
console.log(chalk.yellow(msg))
);
}
console.log();
const { confirm } = await inquirer.prompt([
{
type: "confirm",
name: "confirm",
message: chalk.red.bold(
`Are you sure you want to permanently delete these ${existingTasksToRemove.length} item(s)?`
),
default: false,
},
]);
if (!confirm) {
console.log(chalk.blue("Task deletion cancelled."));
process.exit(0);
}
}
const indicator = startLoadingIndicator(
`Removing ${existingTasksToRemove.length} task(s)/subtask(s)...`
);
// Use the string of existing IDs for the core function
const existingIdsString = existingTasksToRemove
.map(({ id }) => id)
.join(",");
const result = await removeTask(tasksPath, existingIdsString);
stopLoadingIndicator(indicator);
if (result.success) {
console.log(
boxen(
chalk.green(
`Successfully removed ${result.removedTasks.length} task(s)/subtask(s).`
) +
(result.message ? `\n\nDetails:\n${result.message}` : "") +
(result.error
? `\n\nWarnings:\n${chalk.yellow(result.error)}`
: ""),
{ padding: 1, borderColor: "green", borderStyle: "round" }
)
);
} else {
console.error(
boxen(
chalk.red(
`Operation completed with errors. Removed ${result.removedTasks.length} task(s)/subtask(s).`
) +
(result.message ? `\n\nDetails:\n${result.message}` : "") +
(result.error ? `\n\nErrors:\n${chalk.red(result.error)}` : ""),
{
padding: 1,
borderColor: "red",
borderStyle: "round",
}
)
);
process.exit(1); // Exit with error code if any part failed
}
// Log any initially non-existent IDs again for clarity
if (nonExistentIds.length > 0) {
console.warn(
chalk.yellow(
`Note: The following IDs were not found initially and were skipped: ${nonExistentIds.join(", ")}`
)
);
// Exit with error if any removals failed
if (result.removedTasks.length === 0) {
process.exit(1);
}
}
} 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")
.option(
"--openrouter",
"Allow setting a custom OpenRouter model ID (use with --set-*) "
)
.option(
"--ollama",
"Allow setting a custom Ollama model ID (use with --set-*) "
)
.option(
"--bedrock",
"Allow setting a custom Bedrock model ID (use with --set-*) "
)
.addHelpText(
"after",
`
Examples:
$ task-master models # View current configuration
$ task-master models --set-main gpt-4o # Set main model (provider inferred)
$ task-master models --set-research sonar-pro # Set research model
$ task-master models --set-fallback claude-3-5-sonnet-20241022 # Set fallback
$ task-master models --set-main my-custom-model --ollama # Set custom Ollama model for main role
$ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role
$ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role
$ task-master models --setup # Run interactive setup`
)
.action(async (options) => {
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.error(chalk.red("Error: Could not find project root."));
process.exit(1);
}
// Validate flags: cannot use multiple provider flags simultaneously
const providerFlags = [
options.openrouter,
options.ollama,
options.bedrock,
].filter(Boolean).length;
if (providerFlags > 1) {
console.error(
chalk.red(
"Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock) simultaneously."
)
);
process.exit(1);
}
// Determine the primary action based on flags
const isSetup = options.setup;
const isSetOperation =
options.setMain || options.setResearch || options.setFallback;
// --- Execute Action ---
if (isSetup) {
// Action 1: Run Interactive Setup
console.log(chalk.blue("Starting interactive model setup...")); // Added feedback
try {
await runInteractiveSetup(projectRoot);
// runInteractiveSetup logs its own completion/error messages
} catch (setupError) {
console.error(
chalk.red("\\nInteractive setup failed unexpectedly:"),
setupError.message
);
}
// --- IMPORTANT: Exit after setup ---
return; // Stop execution here
}
if (isSetOperation) {
// Action 2: Perform Direct Set Operations
let updateOccurred = false; // Track if any update actually happened
if (options.setMain) {
const result = await setModel("main", options.setMain, {
projectRoot,
providerHint: options.openrouter
? "openrouter"
: options.ollama
? "ollama"
: options.bedrock
? "bedrock"
: undefined,
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
if (result.data.warning)
console.log(chalk.yellow(result.data.warning));
updateOccurred = true;
} else {
console.error(
chalk.red(`❌ Error setting main model: ${result.error.message}`)
);
}
}
if (options.setResearch) {
const result = await setModel("research", options.setResearch, {
projectRoot,
providerHint: options.openrouter
? "openrouter"
: options.ollama
? "ollama"
: options.bedrock
? "bedrock"
: undefined,
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
if (result.data.warning)
console.log(chalk.yellow(result.data.warning));
updateOccurred = true;
} else {
console.error(
chalk.red(
`❌ Error setting research model: ${result.error.message}`
)
);
}
}
if (options.setFallback) {
const result = await setModel("fallback", options.setFallback, {
projectRoot,
providerHint: options.openrouter
? "openrouter"
: options.ollama
? "ollama"
: options.bedrock
? "bedrock"
: undefined,
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
if (result.data.warning)
console.log(chalk.yellow(result.data.warning));
updateOccurred = true;
} else {
console.error(
chalk.red(
`❌ Error setting fallback model: ${result.error.message}`
)
);
}
}
// Optional: Add a final confirmation if any update occurred
if (updateOccurred) {
console.log(chalk.blue("\nModel configuration updated."));
} else {
console.log(
chalk.yellow(
"\nNo model configuration changes were made (or errors occurred)."
)
);
}
// --- IMPORTANT: Exit after set operations ---
return; // Stop execution here
}
// Action 3: Display Full Status (Only runs if no setup and no set flags)
console.log(chalk.blue("Fetching current model configuration...")); // Added feedback
const configResult = await getModelConfiguration({ projectRoot });
const availableResult = await getAvailableModelsList({ projectRoot });
const apiKeyStatusResult = await getApiKeyStatusReport({ projectRoot });
// 1. Display Active Models
if (!configResult.success) {
console.error(
chalk.red(
`❌ Error fetching configuration: ${configResult.error.message}`
)
);
} else {
displayModelConfiguration(
configResult.data,
availableResult.data?.models || []
);
}
// 2. Display API Key Status
if (apiKeyStatusResult.success) {
displayApiKeyStatus(apiKeyStatusResult.data.report);
} else {
console.error(
chalk.yellow(
`⚠️ Warning: Could not display API Key status: ${apiKeyStatusResult.error.message}`
)
);
}
// 3. Display Other Available Models (Filtered)
if (availableResult.success) {
const activeIds = configResult.success
? [
configResult.data.activeModels.main.modelId,
configResult.data.activeModels.research.modelId,
configResult.data.activeModels.fallback?.modelId,
].filter(Boolean)
: [];
const displayableAvailable = availableResult.data.models.filter(
(m) => !activeIds.includes(m.modelId) && !m.modelId.startsWith("[")
);
displayAvailableModels(displayableAvailable);
} else {
console.error(
chalk.yellow(
`⚠️ Warning: Could not display available models: ${availableResult.error.message}`
)
);
}
// 4. Conditional Hint if Config File is Missing
const configExists = isConfigFilePresent(projectRoot);
if (!configExists) {
console.log(
chalk.yellow(
"\\nHint: Run 'task-master models --setup' to create or update your configuration."
)
);
}
// --- IMPORTANT: Exit after displaying status ---
return; // Stop execution here
});
// move-task command
programInstance
.command("move")
.description("Move a task or subtask to a new position")
.option("-f, --file <file>", "Path to the tasks file", "tasks/tasks.json")
.option(
"--from <id>",
'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
)
.option(
"--to <id>",
'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated'
)
.action(async (options) => {
const tasksPath = options.file;
const sourceId = options.from;
const destinationId = options.to;
if (!sourceId || !destinationId) {
console.error(
chalk.red("Error: Both --from and --to parameters are required")
);
console.log(
chalk.yellow(
"Usage: task-master move --from=<sourceId> --to=<destinationId>"
)
);
process.exit(1);
}
// Check if we're moving multiple tasks (comma-separated IDs)
const sourceIds = sourceId.split(",").map((id) => id.trim());
const destinationIds = destinationId.split(",").map((id) => id.trim());
// Validate that the number of source and destination IDs match
if (sourceIds.length !== destinationIds.length) {
console.error(
chalk.red(
"Error: The number of source and destination IDs must match"
)
);
console.log(
chalk.yellow("Example: task-master move --from=5,6,7 --to=10,11,12")
);
process.exit(1);
}
// If moving multiple tasks
if (sourceIds.length > 1) {
console.log(
chalk.blue(
`Moving multiple tasks: ${sourceIds.join(", ")} to ${destinationIds.join(", ")}...`
)
);
try {
// Read tasks data once to validate destination IDs
const tasksData = readJSON(tasksPath);
if (!tasksData || !tasksData.tasks) {
console.error(
chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`)
);
process.exit(1);
}
// Move tasks one by one
for (let i = 0; i < sourceIds.length; i++) {
const fromId = sourceIds[i];
const toId = destinationIds[i];
// Skip if source and destination are the same
if (fromId === toId) {
console.log(
chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`)
);
continue;
}
console.log(
chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`)
);
try {
await moveTask(
tasksPath,
fromId,
toId,
i === sourceIds.length - 1
);
console.log(
chalk.green(
`✓ Successfully moved task/subtask ${fromId} to ${toId}`
)
);
} catch (error) {
console.error(
chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`)
);
// Continue with the next task rather than exiting
}
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
} else {
// Moving a single task (existing logic)
console.log(
chalk.blue(`Moving task/subtask ${sourceId} to ${destinationId}...`)
);
try {
const result = await moveTask(
tasksPath,
sourceId,
destinationId,
true
);
console.log(
chalk.green(
`✓ Successfully moved task/subtask ${sourceId} to ${destinationId}`
)
);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
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
// 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
const currentVersion = getTaskMasterVersion();
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,
};