Compare commits

..

2 Commits

Author SHA1 Message Date
Ralph Khreish
fbe859f886 chore: add changeset 2025-04-18 23:38:14 +02:00
Ralph Khreish
45cc0cf527 fix: remove the need for projectName, description, version in mcp and cli 2025-04-18 23:33:22 +02:00
9 changed files with 125 additions and 3003 deletions

View File

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

View File

@@ -0,0 +1,5 @@
---
'task-master-ai': patch
---
Remove the need for project name, description, and version. Since we no longer create a package.json for you

View File

@@ -10,7 +10,7 @@ import os from 'os'; // Import os module for home directory check
/** /**
* Direct function wrapper for initializing a project. * Direct function wrapper for initializing a project.
* Derives target directory from session, sets CWD, and calls core init logic. * Derives target directory from session, sets CWD, and calls core init logic.
* @param {object} args - Arguments containing project details and options (projectName, projectDescription, yes, etc.) * @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot)
* @param {object} log - The FastMCP logger instance. * @param {object} log - The FastMCP logger instance.
* @param {object} context - The context object, must contain { session }. * @param {object} context - The context object, must contain { session }.
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object. * @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
@@ -92,12 +92,8 @@ export async function initializeProjectDirect(args, log, context = {}) {
try { try {
// Always force yes: true when called via MCP to avoid interactive prompts // Always force yes: true when called via MCP to avoid interactive prompts
const options = { const options = {
name: args.projectName,
description: args.projectDescription,
version: args.projectVersion,
author: args.authorName,
skipInstall: args.skipInstall,
aliases: args.addAliases, aliases: args.addAliases,
skipInstall: args.skipInstall,
yes: true // Force yes mode yes: true // Force yes mode
}; };

View File

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

View File

@@ -10,32 +10,8 @@ export function registerInitializeProjectTool(server) {
server.addTool({ server.addTool({
name: 'initialize_project', name: 'initialize_project',
description: description:
"Initializes a new Task Master project structure by calling the core initialization logic. Derives target directory from client session. If project details (name, description, author) are not provided, prompts the user or skips if 'yes' flag is true. DO NOT run without parameters.", 'Initializes a new Task Master project structure by calling the core initialization logic. Creates necessary folders and configuration files for Task Master in the current directory.',
parameters: z.object({ parameters: z.object({
projectName: z
.string()
.optional()
.describe(
'The name for the new project. If not provided, prompt the user for it.'
),
projectDescription: z
.string()
.optional()
.describe(
'A brief description for the project. If not provided, prompt the user for it.'
),
projectVersion: z
.string()
.optional()
.describe(
"The initial version for the project (e.g., '0.1.0'). User input not needed unless user requests to override."
),
authorName: z
.string()
.optional()
.describe(
"The author's name. User input not needed unless user requests to override."
),
skipInstall: z skipInstall: z
.boolean() .boolean()
.optional() .optional()
@@ -47,15 +23,13 @@ export function registerInitializeProjectTool(server) {
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.describe( .describe('Add shell aliases (tm, taskmaster) to shell config file.'),
'Add shell aliases (tm, taskmaster) to shell config file. User input not needed.'
),
yes: z yes: z
.boolean() .boolean()
.optional() .optional()
.default(false) .default(true)
.describe( .describe(
"Skip prompts and use default values or provided arguments. Use true if you wish to skip details like the project name, etc. If the project information required for the initialization is not available or provided by the user, prompt if the user wishes to provide them (name, description, author) or skip them. If the user wishes to skip, set the 'yes' flag to true and do not set any other parameters." 'Skip prompts and use default values. Always set to true for MCP tools.'
), ),
projectRoot: z projectRoot: z
.string() .string()

View File

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

View File

@@ -335,36 +335,11 @@ async function initializeProject(options = {}) {
console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED ====='); console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
console.log('Full options object:', JSON.stringify(options)); console.log('Full options object:', JSON.stringify(options));
console.log('options.yes:', options.yes); console.log('options.yes:', options.yes);
console.log('options.name:', options.name);
console.log('=================================================='); 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 // Determine if we should skip prompts based on the passed options
const skipPrompts = options.yes || (options.name && options.description); const skipPrompts = options.yes;
if (!isSilentMode()) { if (!isSilentMode()) {
console.log('Skip prompts determined:', skipPrompts); console.log('Skip prompts determined:', skipPrompts);
} }
@@ -374,44 +349,24 @@ async function initializeProject(options = {}) {
console.log('SKIPPING PROMPTS - Using defaults or provided values'); console.log('SKIPPING PROMPTS - Using defaults or provided values');
} }
// Use provided options or defaults // We no longer need these variables
const projectName = options.name || 'task-master-project';
const projectDescription =
options.description || 'A project managed with Task Master AI';
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 dryRun = options.dryRun || false;
const addAliases = options.aliases || false; const addAliases = options.aliases || false;
if (dryRun) { if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified'); log('info', 'DRY RUN MODE: No files will be modified');
log( log('info', 'Would initialize Task Master project');
'info',
`Would initialize project: ${projectName} (${projectVersion})`
);
log('info', `Description: ${projectDescription}`);
log('info', `Author: ${authorName || 'Not specified'}`);
log('info', 'Would create/update necessary project files'); log('info', 'Would create/update necessary project files');
if (addAliases) { if (addAliases) {
log('info', 'Would add shell aliases for task-master'); log('info', 'Would add shell aliases for task-master');
} }
return { return {
projectName,
projectDescription,
projectVersion,
authorName,
dryRun: true dryRun: true
}; };
} }
// Create structure using determined values // Create structure using only necessary values
createProjectStructure( createProjectStructure(addAliases);
projectName,
projectDescription,
projectVersion,
authorName,
addAliases
);
} else { } else {
// Prompting logic (only runs if skipPrompts is false) // Prompting logic (only runs if skipPrompts is false)
log('info', 'Required options not provided, proceeding with prompts.'); log('info', 'Required options not provided, proceeding with prompts.');
@@ -421,41 +376,17 @@ async function initializeProject(options = {}) {
}); });
try { try {
// Prompt user for input... // Only prompt for shell aliases
const projectName = await promptQuestion(
rl,
chalk.cyan('Enter project name: ')
);
const projectDescription = await promptQuestion(
rl,
chalk.cyan('Enter project description: ')
);
const projectVersionInput = await promptQuestion(
rl,
chalk.cyan('Enter project version (default: 1.0.0): ')
); // Use a default for prompt
const authorName = await promptQuestion(
rl,
chalk.cyan('Enter your name: ')
);
const addAliasesInput = await promptQuestion( const addAliasesInput = await promptQuestion(
rl, rl,
chalk.cyan('Add shell aliases for task-master? (Y/n): ') chalk.cyan(
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
)
); );
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n'; const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
const projectVersion = projectVersionInput.trim()
? projectVersionInput
: '1.0.0';
// Confirm settings... // Confirm settings...
console.log('\nProject settings:'); console.log('\nTask Master Project settings:');
console.log(chalk.blue('Name:'), chalk.white(projectName));
console.log(chalk.blue('Description:'), chalk.white(projectDescription));
console.log(chalk.blue('Version:'), chalk.white(projectVersion));
console.log(
chalk.blue('Author:'),
chalk.white(authorName || 'Not specified')
);
console.log( console.log(
chalk.blue( chalk.blue(
'Add shell aliases (so you can use "tm" instead of "task-master"):' 'Add shell aliases (so you can use "tm" instead of "task-master"):'
@@ -481,33 +412,18 @@ async function initializeProject(options = {}) {
if (dryRun) { if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified'); log('info', 'DRY RUN MODE: No files will be modified');
log( log('info', 'Would initialize Task Master project');
'info',
`Would initialize project: ${projectName} (${projectVersion})`
);
log('info', `Description: ${projectDescription}`);
log('info', `Author: ${authorName || 'Not specified'}`);
log('info', 'Would create/update necessary project files'); log('info', 'Would create/update necessary project files');
if (addAliasesPrompted) { if (addAliasesPrompted) {
log('info', 'Would add shell aliases for task-master'); log('info', 'Would add shell aliases for task-master');
} }
return { return {
projectName,
projectDescription,
projectVersion,
authorName,
dryRun: true dryRun: true
}; };
} }
// Create structure using prompted values, respecting initial options where relevant // Create structure using only necessary values
createProjectStructure( createProjectStructure(addAliasesPrompted);
projectName,
projectDescription,
projectVersion,
authorName,
addAliasesPrompted // Use value from prompt
);
} catch (error) { } catch (error) {
rl.close(); rl.close();
log('error', `Error during prompting: ${error.message}`); // Use log function log('error', `Error during prompting: ${error.message}`); // Use log function
@@ -526,13 +442,7 @@ function promptQuestion(rl, question) {
} }
// Function to create the project structure // Function to create the project structure
function createProjectStructure( function createProjectStructure(addAliases) {
projectName,
projectDescription,
projectVersion,
authorName,
addAliases
) {
const targetDir = process.cwd(); const targetDir = process.cwd();
log('info', `Initializing project in ${targetDir}`); log('info', `Initializing project in ${targetDir}`);
@@ -542,14 +452,10 @@ function createProjectStructure(
ensureDirectoryExists(path.join(targetDir, 'tasks')); ensureDirectoryExists(path.join(targetDir, 'tasks'));
// Setup MCP configuration for integration with Cursor // Setup MCP configuration for integration with Cursor
setupMCPConfiguration(targetDir, projectName); setupMCPConfiguration(targetDir);
// Copy template files with replacements // Copy template files with replacements
const replacements = { const replacements = {
projectName,
projectDescription,
projectVersion,
authorName,
year: new Date().getFullYear() year: new Date().getFullYear()
}; };
@@ -695,7 +601,7 @@ function createProjectStructure(
} }
// Function to setup MCP configuration for Cursor integration // Function to setup MCP configuration for Cursor integration
function setupMCPConfiguration(targetDir, projectName) { function setupMCPConfiguration(targetDir) {
const mcpDirPath = path.join(targetDir, '.cursor'); const mcpDirPath = path.join(targetDir, '.cursor');
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json'); const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');

View File

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

File diff suppressed because one or more lines are too long