Eyal Toledano 7df58df199 feat: Enhance Task Master CLI with Testing Framework, Perplexity AI Integration, and Refactored Core Logic
This commit introduces significant enhancements and refactoring to the Task Master CLI, focusing on improved testing, integration with Perplexity AI for research-backed task updates, and core logic refactoring for better maintainability and functionality.

**Testing Infrastructure Setup:**
- Implemented Jest as the primary testing framework, setting up a comprehensive testing environment.
- Added new test scripts to  including , , and  for streamlined testing workflows.
- Integrated necessary devDependencies for testing, such as , , , , and , to support unit, integration, and end-to-end testing.

**Dependency Updates:**
- Updated  and  to reflect the latest dependency versions, ensuring project stability and access to the newest features and security patches.
- Upgraded  to version 0.9.16 and usage: openai [-h] [-v] [-b API_BASE] [-k API_KEY] [-p PROXY [PROXY ...]]
              [-o ORGANIZATION] [-t {openai,azure}]
              [--api-version API_VERSION] [--azure-endpoint AZURE_ENDPOINT]
              [--azure-ad-token AZURE_AD_TOKEN] [-V]
              {api,tools,migrate,grit} ...

positional arguments:
  {api,tools,migrate,grit}
    api                 Direct API calls
    tools               Client side tools for convenience

options:
  -h, --help            show this help message and exit
  -v, --verbose         Set verbosity.
  -b, --api-base API_BASE
                        What API base url to use.
  -k, --api-key API_KEY
                        What API key to use.
  -p, --proxy PROXY [PROXY ...]
                        What proxy to use.
  -o, --organization ORGANIZATION
                        Which organization to run as (will use your default
                        organization if not specified)
  -t, --api-type {openai,azure}
                        The backend API to call, must be `openai` or `azure`
  --api-version API_VERSION
                        The Azure API version, e.g.
                        'https://learn.microsoft.com/en-us/azure/ai-
                        services/openai/reference#rest-api-versioning'
  --azure-endpoint AZURE_ENDPOINT
                        The Azure endpoint, e.g.
                        'https://endpoint.openai.azure.com'
  --azure-ad-token AZURE_AD_TOKEN
                        A token from Azure Active Directory,
                        https://www.microsoft.com/en-
                        us/security/business/identity-access/microsoft-entra-
                        id
  -V, --version         show program's version number and exit to 4.89.0.
- Added  dependency (version 2.3.0) and updated  related dependencies to their latest versions.

**Perplexity AI Integration for Research-Backed Updates:**
- Introduced an option to leverage Perplexity AI for task updates, enabling research-backed enhancements to task details.
- Implemented logic to initialize a Perplexity AI client if the  environment variable is available.
- Modified the  function to accept a  parameter, allowing dynamic selection between Perplexity AI and Claude AI for task updates based on API key availability and user preference.
- Enhanced  to handle responses from Perplexity AI and update tasks accordingly, including improved error handling and logging for robust operation.

**Core Logic Refactoring and Improvements:**
- Refactored the  function to utilize task IDs instead of dependency IDs, ensuring consistency and clarity in dependency management.
- Implemented a new  function to rigorously check for both circular dependencies and self-dependencies within tasks, improving task relationship integrity.
- Enhanced UI elements in :
    - Refactored  to incorporate icons for different task statuses and utilize a  object for color mapping, improving visual representation of task status.
    - Updated  to display colored complexity scores with emojis, providing a more intuitive and visually appealing representation of task complexity.
- Refactored the task data structure creation and validation process:
    - Updated the JSON Schema for  to reflect a more streamlined and efficient task structure.
    - Implemented Task Model Classes for better data modeling and type safety.
    - Improved File System Operations for task data management.
    - Developed robust Validation Functions and an Error Handling System to ensure data integrity and application stability.

**Testing Guidelines Implementation:**
- Implemented guidelines for writing testable code when developing new features, promoting a test-driven development approach.
- Added testing requirements and best practices for unit, integration, and edge case testing to ensure comprehensive test coverage.
- Updated the development workflow to mandate writing tests before proceeding with configuration and documentation updates, reinforcing the importance of testing throughout the development lifecycle.

This commit collectively enhances the Task Master CLI's reliability, functionality, and developer experience through improved testing practices, AI-powered research capabilities, and a more robust and maintainable codebase.
2025-03-24 13:28:08 -04:00

471 lines
16 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 { CONFIG, log, readJSON } from './utils.js';
import {
parsePRD,
updateTasks,
generateTaskFiles,
setTaskStatus,
listTasks,
expandTask,
expandAllTasks,
clearSubtasks,
addTask,
analyzeTaskComplexity
} from './task-manager.js';
import {
addDependency,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand
} from './dependency-manager.js';
import {
displayBanner,
displayHelp,
displayNextTask,
displayTaskById,
displayComplexityReport,
} from './ui.js';
/**
* Configure and register CLI commands
* @param {Object} program - Commander program instance
*/
function registerCommands(programInstance) {
// Default help
programInstance.on('--help', function() {
displayHelp();
});
// parse-prd command
programInstance
.command('parse-prd')
.description('Parse a PRD file and generate tasks')
.argument('<file>', 'Path to the PRD file')
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
.action(async (file, options) => {
const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
console.log(chalk.blue(`Parsing PRD file: ${file}`));
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
await parsePRD(file, outputPath, numTasks);
});
// update command
programInstance
.command('update')
.description('Update tasks based on new information or implementation changes')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('--from <id>', 'Task ID to start updating from (tasks with ID >= this value will be updated)', '1')
.option('-p, --prompt <text>', 'Prompt explaining the changes or new context (required)')
.option('-r, --research', 'Use Perplexity AI for research-backed task updates')
.action(async (options) => {
const tasksPath = options.file;
const fromId = parseInt(options.from, 10);
const prompt = options.prompt;
const useResearch = options.research || false;
if (!prompt) {
console.error(chalk.red('Error: --prompt parameter is required. Please provide information about the changes.'));
process.exit(1);
}
console.log(chalk.blue(`Updating tasks from ID >= ${fromId} with prompt: "${prompt}"`));
console.log(chalk.blue(`Tasks file: ${tasksPath}`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed task updates'));
}
await updateTasks(tasksPath, fromId, prompt, useResearch);
});
// generate command
programInstance
.command('generate')
.description('Generate task files from tasks.json')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-o, --output <dir>', 'Output directory', 'tasks')
.action(async (options) => {
const tasksPath = options.file;
const outputDir = options.output;
console.log(chalk.blue(`Generating task files from: ${tasksPath}`));
console.log(chalk.blue(`Output directory: ${outputDir}`));
await generateTaskFiles(tasksPath, outputDir);
});
// set-status command
programInstance
.command('set-status')
.description('Set the status of a task')
.option('-i, --id <id>', 'Task ID (can be comma-separated for multiple tasks)')
.option('-s, --status <status>', 'New status (todo, in-progress, review, done)')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
const taskId = options.id;
const status = options.status;
if (!taskId || !status) {
console.error(chalk.red('Error: Both --id and --status are required'));
process.exit(1);
}
console.log(chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`));
await setTaskStatus(tasksPath, taskId, status);
});
// list command
programInstance
.command('list')
.description('List all tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-s, --status <status>', 'Filter by status')
.option('--with-subtasks', 'Show subtasks for each task')
.action(async (options) => {
const tasksPath = options.file;
const statusFilter = options.status;
const withSubtasks = options.withSubtasks || false;
console.log(chalk.blue(`Listing tasks from: ${tasksPath}`));
if (statusFilter) {
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
}
if (withSubtasks) {
console.log(chalk.blue('Including subtasks in listing'));
}
await listTasks(tasksPath, statusFilter, withSubtasks);
});
// expand command
programInstance
.command('expand')
.description('Break down tasks into detailed subtasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <id>', 'Task ID to expand')
.option('-a, --all', 'Expand all tasks')
.option('-n, --num <number>', 'Number of subtasks to generate', CONFIG.defaultSubtasks.toString())
.option('--research', 'Enable Perplexity AI for research-backed subtask generation')
.option('-p, --prompt <text>', 'Additional context to guide subtask generation')
.option('--force', 'Force regeneration of subtasks for tasks that already have them')
.action(async (options) => {
const tasksPath = options.file;
const idArg = options.id ? parseInt(options.id, 10) : null;
const allFlag = options.all;
const numSubtasks = parseInt(options.num, 10);
const forceFlag = options.force;
const useResearch = options.research === true;
const additionalContext = options.prompt || '';
// Debug log to verify the value
log('debug', `Research enabled: ${useResearch}`);
if (allFlag) {
console.log(chalk.blue(`Expanding all tasks with ${numSubtasks} subtasks each...`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
} else {
console.log(chalk.yellow('Research-backed subtask generation disabled'));
}
if (additionalContext) {
console.log(chalk.blue(`Additional context: "${additionalContext}"`));
}
await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag);
} else if (idArg) {
console.log(chalk.blue(`Expanding task ${idArg} with ${numSubtasks} subtasks...`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
} else {
console.log(chalk.yellow('Research-backed subtask generation disabled'));
}
if (additionalContext) {
console.log(chalk.blue(`Additional context: "${additionalContext}"`));
}
await expandTask(idArg, numSubtasks, useResearch, additionalContext);
} else {
console.error(chalk.red('Error: Please specify a task ID with --id=<id> or use --all to expand all tasks.'));
}
});
// analyze-complexity command
programInstance
.command('analyze-complexity')
.description(`Analyze tasks and generate expansion recommendations${chalk.reset('')}`)
.option('-o, --output <file>', 'Output file path for the report', 'scripts/task-complexity-report.json')
.option('-m, --model <model>', 'LLM model to use for analysis (defaults to configured model)')
.option('-t, --threshold <number>', 'Minimum complexity score to recommend expansion (1-10)', '5')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-r, --research', 'Use Perplexity AI for research-backed complexity analysis')
.action(async (options) => {
const tasksPath = options.file || 'tasks/tasks.json';
const outputPath = options.output;
const modelOverride = options.model;
const thresholdScore = parseFloat(options.threshold);
const useResearch = options.research || false;
console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`));
console.log(chalk.blue(`Output report will be saved to: ${outputPath}`));
if (useResearch) {
console.log(chalk.blue('Using Perplexity AI for research-backed complexity analysis'));
}
await analyzeTaskComplexity(options);
});
// clear-subtasks command
programInstance
.command('clear-subtasks')
.description('Clear subtasks from specified tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-i, --id <ids>', 'Task IDs (comma-separated) to clear subtasks from')
.option('--all', 'Clear subtasks from all tasks')
.action(async (options) => {
const tasksPath = options.file;
const taskIds = options.id;
const all = options.all;
if (!taskIds && !all) {
console.error(chalk.red('Error: Please specify task IDs with --id=<ids> or use --all to clear all tasks'));
process.exit(1);
}
if (all) {
// If --all is specified, get all task IDs
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
console.error(chalk.red('Error: No valid tasks found'));
process.exit(1);
}
const allIds = data.tasks.map(t => t.id).join(',');
clearSubtasks(tasksPath, allIds);
} else {
clearSubtasks(tasksPath, taskIds);
}
});
// add-task command
programInstance
.command('add-task')
.description('Add a new task using AI')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option('-p, --prompt <text>', 'Description of the task to add (required)')
.option('-d, --dependencies <ids>', 'Comma-separated list of task IDs this task depends on')
.option('--priority <priority>', 'Task priority (high, medium, low)', 'medium')
.action(async (options) => {
const tasksPath = options.file;
const prompt = options.prompt;
const dependencies = options.dependencies ? options.dependencies.split(',').map(id => parseInt(id.trim(), 10)) : [];
const priority = options.priority;
if (!prompt) {
console.error(chalk.red('Error: --prompt parameter is required. Please provide a task description.'));
process.exit(1);
}
console.log(chalk.blue(`Adding new task with description: "${prompt}"`));
console.log(chalk.blue(`Dependencies: ${dependencies.length > 0 ? dependencies.join(', ') : 'None'}`));
console.log(chalk.blue(`Priority: ${priority}`));
await addTask(tasksPath, prompt, dependencies, priority);
});
// next command
programInstance
.command('next')
.description(`Show the next task to work on based on dependencies and status${chalk.reset('')}`)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
await displayNextTask(tasksPath);
});
// show command
programInstance
.command('show')
.description(`Display detailed information about a specific task${chalk.reset('')}`)
.argument('[id]', 'Task ID to show')
.option('-i, --id <id>', 'Task ID to show')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (taskId, options) => {
const idArg = taskId || options.id;
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
process.exit(1);
}
const tasksPath = options.file;
await displayTaskById(tasksPath, idArg);
});
// add-dependency command
programInstance
.command('add-dependency')
.description('Add a dependency to a task')
.option('-i, --id <id>', 'Task ID to add dependency to')
.option('-d, --depends-on <id>', 'Task ID that will become a dependency')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (options) => {
const tasksPath = options.file;
const taskId = options.id;
const dependencyId = options.dependsOn;
if (!taskId || !dependencyId) {
console.error(chalk.red('Error: Both --id and --depends-on are required'));
process.exit(1);
}
await addDependency(tasksPath, parseInt(taskId, 10), parseInt(dependencyId, 10));
});
// 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);
}
await removeDependency(tasksPath, parseInt(taskId, 10), parseInt(dependencyId, 10));
});
// 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 more commands as needed...
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
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 default version
}
return CONFIG.projectVersion; // Default fallback
})
.helpOption('-h, --help', 'Display help')
.addHelpCommand(false) // Disable default help command
.on('--help', () => {
displayHelp(); // Use your custom help display instead
})
.on('-h', () => {
displayHelp();
process.exit(0);
});
// Modify the help option to use your custom display
programInstance.helpInformation = () => {
displayHelp();
return '';
};
// Register commands
registerCommands(programInstance);
return programInstance;
}
/**
* 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);
}
// Setup and parse
const programInstance = setupCLI();
await programInstance.parseAsync(argv);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
if (CONFIG.debug) {
console.error(error);
}
process.exit(1);
}
}
export {
registerCommands,
setupCLI,
runCLI
};