Compare commits

..

1 Commits

Author SHA1 Message Date
Ralph Khreish
b0d0eecfab fix: README bug not showing precise instructions 2025-04-12 15:55:30 +02:00
18 changed files with 341 additions and 293 deletions

View File

@@ -1,6 +0,0 @@
---
'task-master-ai': patch
---
- Fixes shebang issue not allowing task-master to run on certain windows operating systems
- Resolves #241 #211 #184 #193

View File

@@ -1,5 +0,0 @@
---
'task-master-ai': patch
---
Fix remove-task command to handle multiple comma-separated task IDs

View File

@@ -1,5 +0,0 @@
---
'task-master-ai': patch
---
Updates the parameter descriptions for update, update-task and update-subtask to ensure the MCP server correctly reaches for the right update command based on what is being updated -- all tasks, one task, or a subtask.

View File

@@ -1,6 +0,0 @@
---
'task-master-ai': patch
---
- Fix `task-master init` polluting codebase with new packages inside `package.json` and modifying project `README`
- Now only initializes with cursor rules, windsurf rules, mcp.json, scripts/example_prd.txt, .gitignore modifications, and `README-task-master.md`

View File

@@ -1,5 +0,0 @@
---
'task-master-ai': minor
---
Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``

View File

@@ -146,7 +146,7 @@ To enable enhanced task management capabilities directly within Cursor using the
4. Configure with the following details:
- Name: "Task Master"
- Type: "Command"
- Command: "npx -y task-master-ai"
- Command: "npx -y task-master-mcp"
5. Save the settings
Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience.

View File

@@ -20,14 +20,20 @@ A task management system for AI-driven development with Claude, designed to work
MCP (Model Control Protocol) provides the easiest way to get started with Task Master directly in your editor.
1. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
1. **Install the package**
```bash
npm i -g task-master-ai
```
2. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
```json
{
"mcpServers": {
"taskmaster-ai": {
"command": "npx",
"args": ["-y", "task-master-ai"],
"args": ["-y", "task-master-mcp"],
"env": {
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env node --trace-deprecation
/**
* Task Master

View File

@@ -10,14 +10,20 @@ There are two ways to set up Task Master: using MCP (recommended) or via npm ins
MCP (Model Control Protocol) provides the easiest way to get started with Task Master directly in your editor.
1. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
1. **Install the package**
```bash
npm i -g task-master-ai
```
2. **Add the MCP config to your editor** (Cursor recommended, but it works with other text editors):
```json
{
"mcpServers": {
"taskmaster-ai": {
"command": "npx",
"args": ["-y", "task-master-ai"],
"args": ["-y", "task-master-mcp"],
"env": {
"ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE",
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",

View File

@@ -3,23 +3,18 @@
* Direct function implementation for removing a task
*/
import {
removeTask,
taskExists
} from '../../../../scripts/modules/task-manager.js';
import { removeTask } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode,
readJSON
disableSilentMode
} from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for removeTask with error handling.
* Supports removing multiple tasks at once with comma-separated IDs.
*
* @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple).
* @param {string} args.id - The ID of the task or subtask to remove.
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
*/
@@ -41,7 +36,8 @@ export async function removeTaskDirect(args, log) {
}
// Validate task ID parameter
if (!id) {
const taskId = id;
if (!taskId) {
log.error('Task ID is required');
return {
success: false,
@@ -53,103 +49,46 @@ export async function removeTaskDirect(args, log) {
};
}
// Split task IDs if comma-separated
const taskIdArray = id.split(',').map((taskId) => taskId.trim());
log.info(
`Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}`
);
// Validate all task IDs exist before proceeding
const data = readJSON(tasksJsonPath);
if (!data || !data.tasks) {
return {
success: false,
error: {
code: 'INVALID_TASKS_FILE',
message: `No valid tasks found in ${tasksJsonPath}`
},
fromCache: false
};
}
const invalidTasks = taskIdArray.filter(
(taskId) => !taskExists(data.tasks, taskId)
);
if (invalidTasks.length > 0) {
return {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: `The following tasks were not found: ${invalidTasks.join(', ')}`
},
fromCache: false
};
}
// Remove tasks one by one
const results = [];
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Skip confirmation in the direct function since it's handled by the client
log.info(`Removing task with ID: ${taskId} from ${tasksJsonPath}`);
try {
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksJsonPath, taskId);
results.push({
taskId,
success: true,
message: result.message,
removedTask: result.removedTask
});
log.info(`Successfully removed task: ${taskId}`);
} catch (error) {
results.push({
taskId,
success: false,
error: error.message
});
log.error(`Error removing task ${taskId}: ${error.message}`);
}
}
} finally {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core removeTask function using the provided path
const result = await removeTask(tasksJsonPath, taskId);
// Restore normal logging
disableSilentMode();
}
// Check if all tasks were successfully removed
const successfulRemovals = results.filter((r) => r.success);
const failedRemovals = results.filter((r) => !r.success);
log.info(`Successfully removed task: ${taskId}`);
if (successfulRemovals.length === 0) {
// All removals failed
// Return the result
return {
success: true,
data: {
message: result.message,
taskId: taskId,
tasksPath: tasksJsonPath,
removedTask: result.removedTask
},
fromCache: false
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error removing task: ${error.message}`);
return {
success: false,
error: {
code: 'REMOVE_TASK_ERROR',
message: 'Failed to remove any tasks',
details: failedRemovals
.map((r) => `${r.taskId}: ${r.error}`)
.join('; ')
code: error.code || 'REMOVE_TASK_ERROR',
message: error.message || 'Failed to remove task'
},
fromCache: false
};
}
// At least some tasks were removed successfully
return {
success: true,
data: {
totalTasks: taskIdArray.length,
successful: successfulRemovals.length,
failed: failedRemovals.length,
results: results,
tasksPath: tasksJsonPath
},
fromCache: false
};
} catch (error) {
// Ensure silent mode is disabled even if an outer error occurs
disableSilentMode();

View File

@@ -23,9 +23,7 @@ export function registerRemoveTaskTool(server) {
parameters: z.object({
id: z
.string()
.describe(
"ID(s) of the task(s) or subtask(s) to remove (e.g., '5' or '5.2' or '5,6,7')"
),
.describe("ID of the task or subtask to remove (e.g., '5' or '5.2')"),
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
@@ -37,7 +35,7 @@ export function registerRemoveTaskTool(server) {
}),
execute: async (args, { log, session }) => {
try {
log.info(`Removing task(s) with ID(s): ${args.id}`);
log.info(`Removing task with ID: ${args.id}`);
// Get project root from args or session
const rootFolder =

View File

@@ -20,12 +20,12 @@ export function registerUpdateSubtaskTool(server) {
server.addTool({
name: 'update_subtask',
description:
'Appends timestamped information to a specific subtask without replacing existing content',
'Appends additional information to a specific subtask without replacing existing content',
parameters: z.object({
id: z
.string()
.describe(
'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2"). Parent ID is the ID of the task that contains the subtask.'
'ID of the subtask to update in format "parentId.subtaskId" (e.g., "5.2")'
),
prompt: z.string().describe('Information to add to the subtask'),
research: z

View File

@@ -24,9 +24,7 @@ export function registerUpdateTaskTool(server) {
parameters: z.object({
id: z
.string()
.describe(
"ID of the task (e.g., '15') to update. Subtasks are supported using the update-subtask tool."
),
.describe("ID of the task or subtask (e.g., '15', '15.2') to update"),
prompt: z
.string()
.describe('New information or context to incorporate into the task'),

View File

@@ -20,7 +20,7 @@ export function registerUpdateTool(server) {
server.addTool({
name: 'update',
description:
"Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt. Use 'update_task' instead for a single specific task or 'update_subtask' for subtasks.",
"Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the prompt. Use 'update_task' instead for a single specific task.",
parameters: z.object({
from: z
.string()

5
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.11.1",
"version": "0.10.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.11.1",
"version": "0.10.1",
"license": "MIT WITH Commons-Clause",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
@@ -31,7 +31,6 @@
},
"bin": {
"task-master": "bin/task-master.js",
"task-master-ai": "mcp-server/server.js",
"task-master-mcp": "mcp-server/server.js"
},
"devDependencies": {

View File

@@ -1,13 +1,12 @@
{
"name": "task-master-ai",
"version": "0.11.1",
"version": "0.11.0",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
"bin": {
"task-master": "bin/task-master.js",
"task-master-mcp": "mcp-server/server.js",
"task-master-ai": "mcp-server/server.js"
"task-master-mcp": "mcp-server/server.js"
},
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest",

View File

@@ -15,6 +15,7 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import readline from 'readline';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
@@ -178,6 +179,9 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
// Map template names to their actual source paths
switch (templateName) {
case 'dev.js':
sourcePath = path.join(__dirname, 'dev.js');
break;
case 'scripts_README.md':
sourcePath = path.join(__dirname, '..', 'assets', 'scripts_README.md');
break;
@@ -293,8 +297,61 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
return;
}
// Handle package.json - merge dependencies
if (filename === 'package.json') {
log('info', `${targetPath} already exists, merging dependencies...`);
try {
const existingPackageJson = JSON.parse(
fs.readFileSync(targetPath, 'utf8')
);
const newPackageJson = JSON.parse(content);
// Merge dependencies, preferring existing versions in case of conflicts
existingPackageJson.dependencies = {
...newPackageJson.dependencies,
...existingPackageJson.dependencies
};
// Add our scripts if they don't already exist
existingPackageJson.scripts = {
...existingPackageJson.scripts,
...Object.fromEntries(
Object.entries(newPackageJson.scripts).filter(
([key]) => !existingPackageJson.scripts[key]
)
)
};
// Preserve existing type if present
if (!existingPackageJson.type && newPackageJson.type) {
existingPackageJson.type = newPackageJson.type;
}
fs.writeFileSync(
targetPath,
JSON.stringify(existingPackageJson, null, 2)
);
log(
'success',
`Updated ${targetPath} with required dependencies and scripts`
);
} catch (error) {
log('error', `Failed to merge package.json: ${error.message}`);
// Fallback to writing a backup of the existing file and creating a new one
const backupPath = `${targetPath}.backup-${Date.now()}`;
fs.copyFileSync(targetPath, backupPath);
log('info', `Created backup of existing package.json at ${backupPath}`);
fs.writeFileSync(targetPath, content);
log(
'warn',
`Replaced ${targetPath} with new content (due to JSON parsing error)`
);
}
return;
}
// Handle README.md - offer to preserve or create a different file
if (filename === 'README-task-master.md') {
if (filename === 'README.md') {
log('info', `${targetPath} already exists`);
// Create a separate README file specifically for this project
const taskMasterReadmePath = path.join(
@@ -304,7 +361,7 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
fs.writeFileSync(taskMasterReadmePath, content);
log(
'success',
`Created ${taskMasterReadmePath} (preserved original README-task-master.md)`
`Created ${taskMasterReadmePath} (preserved original README.md)`
);
return;
}
@@ -339,30 +396,6 @@ async function initializeProject(options = {}) {
console.log('==================================================');
}
// Try to get project name from package.json if not provided
if (!options.name) {
const packageJsonPath = path.join(process.cwd(), 'package.json');
try {
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath, 'utf8')
);
if (packageJson.name) {
log(
'info',
`Found project name '${packageJson.name}' in package.json`
);
options.name = packageJson.name;
}
}
} catch (error) {
log(
'debug',
`Could not read project name from package.json: ${error.message}`
);
}
}
// Determine if we should skip prompts based on the passed options
const skipPrompts = options.yes || (options.name && options.description);
if (!isSilentMode()) {
@@ -381,6 +414,7 @@ async function initializeProject(options = {}) {
const projectVersion = options.version || '0.1.0'; // Default from commands.js or here
const authorName = options.author || 'Vibe coder'; // Default if not provided
const dryRun = options.dryRun || false;
const skipInstall = options.skipInstall || false;
const addAliases = options.aliases || false;
if (dryRun) {
@@ -395,6 +429,9 @@ async function initializeProject(options = {}) {
if (addAliases) {
log('info', 'Would add shell aliases for task-master');
}
if (!skipInstall) {
log('info', 'Would install dependencies');
}
return {
projectName,
projectDescription,
@@ -410,6 +447,7 @@ async function initializeProject(options = {}) {
projectDescription,
projectVersion,
authorName,
skipInstall,
addAliases
);
} else {
@@ -476,8 +514,9 @@ async function initializeProject(options = {}) {
return; // Added return for clarity
}
// Still respect dryRun if passed initially even when prompting
// Still respect dryRun/skipInstall if passed initially even when prompting
const dryRun = options.dryRun || false;
const skipInstall = options.skipInstall || false;
if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified');
@@ -491,6 +530,9 @@ async function initializeProject(options = {}) {
if (addAliasesPrompted) {
log('info', 'Would add shell aliases for task-master');
}
if (!skipInstall) {
log('info', 'Would install dependencies');
}
return {
projectName,
projectDescription,
@@ -506,6 +548,7 @@ async function initializeProject(options = {}) {
projectDescription,
projectVersion,
authorName,
skipInstall, // Use value from initial options
addAliasesPrompted // Use value from prompt
);
} catch (error) {
@@ -531,6 +574,7 @@ function createProjectStructure(
projectDescription,
projectVersion,
authorName,
skipInstall,
addAliases
) {
const targetDir = process.cwd();
@@ -541,8 +585,105 @@ function createProjectStructure(
ensureDirectoryExists(path.join(targetDir, 'scripts'));
ensureDirectoryExists(path.join(targetDir, 'tasks'));
// Define our package.json content
const packageJson = {
name: projectName.toLowerCase().replace(/\s+/g, '-'),
version: projectVersion,
description: projectDescription,
author: authorName,
type: 'module',
scripts: {
dev: 'node scripts/dev.js',
list: 'node scripts/dev.js list',
generate: 'node scripts/dev.js generate',
'parse-prd': 'node scripts/dev.js parse-prd'
},
dependencies: {
'@anthropic-ai/sdk': '^0.39.0',
boxen: '^8.0.1',
chalk: '^4.1.2',
commander: '^11.1.0',
'cli-table3': '^0.6.5',
cors: '^2.8.5',
dotenv: '^16.3.1',
express: '^4.21.2',
fastmcp: '^1.20.5',
figlet: '^1.8.0',
'fuse.js': '^7.0.0',
'gradient-string': '^3.0.0',
helmet: '^8.1.0',
inquirer: '^12.5.0',
jsonwebtoken: '^9.0.2',
'lru-cache': '^10.2.0',
openai: '^4.89.0',
ora: '^8.2.0'
}
};
// Check if package.json exists and merge if it does
const packageJsonPath = path.join(targetDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
log('info', 'package.json already exists, merging content...');
try {
const existingPackageJson = JSON.parse(
fs.readFileSync(packageJsonPath, 'utf8')
);
// Preserve existing fields but add our required ones
const mergedPackageJson = {
...existingPackageJson,
scripts: {
...existingPackageJson.scripts,
...Object.fromEntries(
Object.entries(packageJson.scripts).filter(
([key]) =>
!existingPackageJson.scripts ||
!existingPackageJson.scripts[key]
)
)
},
dependencies: {
...(existingPackageJson.dependencies || {}),
...Object.fromEntries(
Object.entries(packageJson.dependencies).filter(
([key]) =>
!existingPackageJson.dependencies ||
!existingPackageJson.dependencies[key]
)
)
}
};
// Ensure type is set if not already present
if (!mergedPackageJson.type && packageJson.type) {
mergedPackageJson.type = packageJson.type;
}
fs.writeFileSync(
packageJsonPath,
JSON.stringify(mergedPackageJson, null, 2)
);
log('success', 'Updated package.json with required fields');
} catch (error) {
log('error', `Failed to merge package.json: ${error.message}`);
// Create a backup before potentially modifying
const backupPath = `${packageJsonPath}.backup-${Date.now()}`;
fs.copyFileSync(packageJsonPath, backupPath);
log('info', `Created backup of existing package.json at ${backupPath}`);
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
log(
'warn',
'Created new package.json (backup of original file was created)'
);
}
} else {
// If package.json doesn't exist, create it
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
log('success', 'Created package.json');
}
// Setup MCP configuration for integration with Cursor
setupMCPConfiguration(targetDir, projectName);
setupMCPConfiguration(targetDir, packageJson.name);
// Copy template files with replacements
const replacements = {
@@ -590,6 +731,15 @@ function createProjectStructure(
// Copy .windsurfrules
copyTemplateFile('windsurfrules', path.join(targetDir, '.windsurfrules'));
// Copy scripts/dev.js
copyTemplateFile('dev.js', path.join(targetDir, 'scripts', 'dev.js'));
// Copy scripts/README.md
copyTemplateFile(
'scripts_README.md',
path.join(targetDir, 'scripts', 'README.md')
);
// Copy example_prd.txt
copyTemplateFile(
'example_prd.txt',
@@ -599,13 +749,43 @@ function createProjectStructure(
// Create main README.md
copyTemplateFile(
'README-task-master.md',
path.join(targetDir, 'README-task-master.md'),
path.join(targetDir, 'README.md'),
replacements
);
// Add shell aliases if requested
if (addAliases) {
addShellAliases();
// Initialize git repository if git is available
try {
if (!fs.existsSync(path.join(targetDir, '.git'))) {
log('info', 'Initializing git repository...');
execSync('git init', { stdio: 'ignore' });
log('success', 'Git repository initialized');
}
} catch (error) {
log('warn', 'Git not available, skipping repository initialization');
}
// Run npm install automatically
if (!isSilentMode()) {
console.log(
boxen(chalk.cyan('Installing dependencies...'), {
padding: 0.5,
margin: 0.5,
borderStyle: 'round',
borderColor: 'blue'
})
);
}
try {
if (!skipInstall) {
execSync('npm install', { stdio: 'inherit', cwd: targetDir });
log('success', 'Dependencies installed successfully!');
} else {
log('info', 'Dependencies installation skipped');
}
} catch (error) {
log('error', 'Failed to install dependencies:', error.message);
log('error', 'Please run npm install manually');
}
// Display success message
@@ -627,6 +807,11 @@ function createProjectStructure(
);
}
// Add shell aliases if requested
if (addAliases) {
addShellAliases();
}
// Display next steps in a nice box
if (!isSilentMode()) {
console.log(

View File

@@ -1374,18 +1374,18 @@ function registerCommands(programInstance) {
// remove-task command
programInstance
.command('remove-task')
.description('Remove one or more tasks or subtasks permanently')
.description('Remove a task or subtask permanently')
.option(
'-i, --id <id>',
'ID(s) of the task(s) or subtask(s) to remove (e.g., "5" or "5.2" or "5,6,7")'
'ID of the task or subtask to remove (e.g., "5" or "5.2")'
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-y, --yes', 'Skip confirmation prompt', false)
.action(async (options) => {
const tasksPath = options.file;
const taskIds = options.id;
const taskId = options.id;
if (!taskIds) {
if (!taskId) {
console.error(chalk.red('Error: Task ID is required'));
console.error(
chalk.yellow('Usage: task-master remove-task --id=<taskId>')
@@ -1394,7 +1394,7 @@ function registerCommands(programInstance) {
}
try {
// Check if the tasks file exists and is valid
// Check if the task exists
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
console.error(
@@ -1403,89 +1403,75 @@ function registerCommands(programInstance) {
process.exit(1);
}
// Split task IDs if comma-separated
const taskIdArray = taskIds.split(',').map((id) => id.trim());
// Validate all task IDs exist before proceeding
const invalidTasks = taskIdArray.filter(
(id) => !taskExists(data.tasks, id)
);
if (invalidTasks.length > 0) {
console.error(
chalk.red(
`Error: The following tasks were not found: ${invalidTasks.join(', ')}`
)
);
if (!taskExists(data.tasks, taskId)) {
console.error(chalk.red(`Error: Task with ID ${taskId} not found`));
process.exit(1);
}
// Load task for display
const task = findTaskById(data.tasks, taskId);
// Skip confirmation if --yes flag is provided
if (!options.yes) {
// Display tasks to be removed
// Display task information
console.log();
console.log(
chalk.red.bold(
'⚠️ WARNING: This will permanently delete the following tasks:'
'⚠️ WARNING: This will permanently delete the following task:'
)
);
console.log();
for (const taskId of taskIdArray) {
const task = findTaskById(data.tasks, taskId);
if (typeof taskId === 'string' && taskId.includes('.')) {
// It's a subtask
const [parentId, subtaskId] = taskId.split('.');
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
console.log(
chalk.gray(
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
)
);
} else {
// It's a main task
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
if (typeof taskId === 'string' && taskId.includes('.')) {
// It's a subtask
const [parentId, subtaskId] = taskId.split('.');
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
// Show if it has subtasks
if (task.subtasks && task.subtasks.length > 0) {
console.log(
chalk.gray(
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
chalk.yellow(
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
)
);
} else {
// It's a main task
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
// Show if it has subtasks
if (task.subtasks && task.subtasks.length > 0) {
console.log(
chalk.yellow(
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
)
);
}
// Show if other tasks depend on it
const dependentTasks = data.tasks.filter(
(t) =>
t.dependencies &&
t.dependencies.includes(parseInt(taskId, 10))
);
if (dependentTasks.length > 0) {
console.log(
chalk.yellow(
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
)
);
console.log(
chalk.yellow('These dependencies will be removed:')
);
dependentTasks.forEach((t) => {
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
});
}
}
console.log();
// Show if other tasks depend on it
const dependentTasks = data.tasks.filter(
(t) =>
t.dependencies && t.dependencies.includes(parseInt(taskId, 10))
);
if (dependentTasks.length > 0) {
console.log(
chalk.yellow(
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
)
);
console.log(chalk.yellow('These dependencies will be removed:'));
dependentTasks.forEach((t) => {
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
});
}
}
console.log();
// Prompt for confirmation
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.red.bold(
`Are you sure you want to permanently delete ${taskIdArray.length > 1 ? 'these tasks' : 'this task'}?`
'Are you sure you want to permanently delete this task?'
),
default: false
}
@@ -1497,72 +1483,31 @@ function registerCommands(programInstance) {
}
}
const indicator = startLoadingIndicator('Removing tasks...');
const indicator = startLoadingIndicator('Removing task...');
// Remove each task
const results = [];
for (const taskId of taskIdArray) {
try {
const result = await removeTask(tasksPath, taskId);
results.push({ taskId, success: true, ...result });
} catch (error) {
results.push({ taskId, success: false, error: error.message });
}
}
// Remove the task
const result = await removeTask(tasksPath, taskId);
stopLoadingIndicator(indicator);
// Display results
const successfulRemovals = results.filter((r) => r.success);
const failedRemovals = results.filter((r) => !r.success);
if (successfulRemovals.length > 0) {
// Display success message with appropriate color based on task or subtask
if (typeof taskId === 'string' && taskId.includes('.')) {
// It was a subtask
console.log(
boxen(
chalk.green(
`Successfully removed ${successfulRemovals.length} task${successfulRemovals.length > 1 ? 's' : ''}`
) +
'\n\n' +
successfulRemovals
.map((r) =>
chalk.white(
`${r.taskId.includes('.') ? 'Subtask' : 'Task'} ${r.taskId}`
)
)
.join('\n'),
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
chalk.green(`Subtask ${taskId} has been successfully removed`),
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);
}
if (failedRemovals.length > 0) {
} else {
// It was a main task
console.log(
boxen(
chalk.red(
`Failed to remove ${failedRemovals.length} task${failedRemovals.length > 1 ? 's' : ''}`
) +
'\n\n' +
failedRemovals
.map((r) => chalk.white(`${r.taskId}: ${r.error}`))
.join('\n'),
{
padding: 1,
borderColor: 'red',
borderStyle: 'round',
margin: { top: 1 }
}
)
boxen(chalk.green(`Task ${taskId} has been successfully removed`), {
padding: 1,
borderColor: 'green',
borderStyle: 'round'
})
);
// Exit with error if any removals failed
if (successfulRemovals.length === 0) {
process.exit(1);
}
}
} catch (error) {
console.error(