diff --git a/.changeset/metal-rocks-help.md b/.changeset/metal-rocks-help.md new file mode 100644 index 00000000..fb12e637 --- /dev/null +++ b/.changeset/metal-rocks-help.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": minor +--- + +Improve next command to work with remote diff --git a/apps/cli/src/command-registry.ts b/apps/cli/src/command-registry.ts index fffc8379..20721242 100644 --- a/apps/cli/src/command-registry.ts +++ b/apps/cli/src/command-registry.ts @@ -8,6 +8,7 @@ import { Command } from 'commander'; // Import all commands import { ListTasksCommand } from './commands/list.command.js'; import { ShowCommand } from './commands/show.command.js'; +import { NextCommand } from './commands/next.command.js'; import { AuthCommand } from './commands/auth.command.js'; import { ContextCommand } from './commands/context.command.js'; import { StartCommand } from './commands/start.command.js'; @@ -45,6 +46,12 @@ export class CommandRegistry { commandClass: ShowCommand as any, category: 'task' }, + { + name: 'next', + description: 'Find the next available task to work on', + commandClass: NextCommand as any, + category: 'task' + }, { name: 'start', description: 'Start working on a task with claude-code', diff --git a/apps/cli/src/commands/next.command.ts b/apps/cli/src/commands/next.command.ts new file mode 100644 index 00000000..5c037177 --- /dev/null +++ b/apps/cli/src/commands/next.command.ts @@ -0,0 +1,247 @@ +/** + * @fileoverview NextCommand using Commander's native class pattern + * Extends Commander.Command for better integration with the framework + */ + +import path from 'node:path'; +import { Command } from 'commander'; +import chalk from 'chalk'; +import boxen from 'boxen'; +import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; +import type { StorageType } from '@tm/core/types'; +import { displayTaskDetails } from '../ui/components/task-detail.component.js'; +import { displayHeader } from '../ui/index.js'; + +/** + * Options interface for the next command + */ +export interface NextCommandOptions { + tag?: string; + format?: 'text' | 'json'; + silent?: boolean; + project?: string; +} + +/** + * Result type from next command + */ +export interface NextTaskResult { + task: Task | null; + found: boolean; + tag: string; + storageType: Exclude; +} + +/** + * NextCommand extending Commander's Command class + * This is a thin presentation layer over @tm/core + */ +export class NextCommand extends Command { + private tmCore?: TaskMasterCore; + private lastResult?: NextTaskResult; + + constructor(name?: string) { + super(name || 'next'); + + // Configure the command + this.description('Find the next available task to work on') + .option('-t, --tag ', 'Filter by tag') + .option('-f, --format ', 'Output format (text, json)', 'text') + .option('--silent', 'Suppress output (useful for programmatic usage)') + .option('-p, --project ', 'Project root directory', process.cwd()) + .action(async (options: NextCommandOptions) => { + await this.executeCommand(options); + }); + } + + /** + * Execute the next command + */ + private async executeCommand(options: NextCommandOptions): Promise { + try { + // Validate options (throws on invalid options) + this.validateOptions(options); + + // Initialize tm-core + await this.initializeCore(options.project || process.cwd()); + + // Get next task from core + const result = await this.getNextTask(options); + + // Store result for programmatic access + this.setLastResult(result); + + // Display results + if (!options.silent) { + this.displayResults(result, options); + } + } catch (error: any) { + const msg = error?.getSanitizedDetails?.() ?? { + message: error?.message ?? String(error) + }; + + // Allow error to propagate for library compatibility + throw new Error(msg.message || 'Unexpected error in next command'); + } finally { + // Always clean up resources, even on error + await this.cleanup(); + } + } + + /** + * Validate command options + */ + private validateOptions(options: NextCommandOptions): void { + // Validate format + if (options.format && !['text', 'json'].includes(options.format)) { + throw new Error( + `Invalid format: ${options.format}. Valid formats are: text, json` + ); + } + } + + /** + * Initialize TaskMasterCore + */ + private async initializeCore(projectRoot: string): Promise { + if (!this.tmCore) { + const resolved = path.resolve(projectRoot); + this.tmCore = await createTaskMasterCore({ projectPath: resolved }); + } + } + + /** + * Get next task from tm-core + */ + private async getNextTask( + options: NextCommandOptions + ): Promise { + if (!this.tmCore) { + throw new Error('TaskMasterCore not initialized'); + } + + // Call tm-core to get next task + const task = await this.tmCore.getNextTask(options.tag); + + // Get storage type and active tag + const storageType = this.tmCore.getStorageType(); + if (storageType === 'auto') { + throw new Error('Storage type must be resolved before use'); + } + const activeTag = options.tag || this.tmCore.getActiveTag(); + + return { + task, + found: task !== null, + tag: activeTag, + storageType + }; + } + + /** + * Display results based on format + */ + private displayResults( + result: NextTaskResult, + options: NextCommandOptions + ): void { + const format = options.format || 'text'; + + switch (format) { + case 'json': + this.displayJson(result); + break; + + case 'text': + default: + this.displayText(result); + break; + } + } + + /** + * Display in JSON format + */ + private displayJson(result: NextTaskResult): void { + console.log(JSON.stringify(result, null, 2)); + } + + /** + * Display in text format + */ + private displayText(result: NextTaskResult): void { + // Display header with tag (no file path for next command) + displayHeader({ + tag: result.tag || 'master' + }); + + if (!result.found || !result.task) { + // No next task available + console.log( + boxen( + chalk.yellow( + 'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.' + ), + { + padding: 1, + borderStyle: 'round', + borderColor: 'yellow', + title: '⚠ NO TASKS AVAILABLE ⚠', + titleAlignment: 'center' + } + ) + ); + console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`); + console.log( + `\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}` + ); + return; + } + + const task = result.task; + + // Display the task details using the same component as 'show' command + // with a custom header indicating this is the next task + const customHeader = `Next Task: #${task.id} - ${task.title}`; + displayTaskDetails(task, { + customHeader, + headerColor: 'green', + showSuggestedActions: true + }); + + console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`); + } + + /** + * Set the last result for programmatic access + */ + private setLastResult(result: NextTaskResult): void { + this.lastResult = result; + } + + /** + * Get the last result (for programmatic usage) + */ + getLastResult(): NextTaskResult | undefined { + return this.lastResult; + } + + /** + * Clean up resources + */ + async cleanup(): Promise { + if (this.tmCore) { + await this.tmCore.close(); + this.tmCore = undefined; + } + } + + /** + * Register this command on an existing program + */ + static register(program: Command, name?: string): NextCommand { + const nextCommand = new NextCommand(name); + program.addCommand(nextCommand); + return nextCommand; + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index fbf8757a..cd010fb1 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -6,6 +6,7 @@ // Commands export { ListTasksCommand } from './commands/list.command.js'; export { ShowCommand } from './commands/show.command.js'; +export { NextCommand } from './commands/next.command.js'; export { AuthCommand } from './commands/auth.command.js'; export { ContextCommand } from './commands/context.command.js'; export { StartCommand } from './commands/start.command.js'; diff --git a/apps/cli/src/ui/components/header.component.ts b/apps/cli/src/ui/components/header.component.ts index 5416d04b..e47337c2 100644 --- a/apps/cli/src/ui/components/header.component.ts +++ b/apps/cli/src/ui/components/header.component.ts @@ -25,9 +25,9 @@ export function displayHeader(options: HeaderOptions = {}): void { let tagInfo = ''; if (tag && tag !== 'master') { - tagInfo = `🏷 tag: ${chalk.cyan(tag)}`; + tagInfo = `🏷 tag: ${chalk.cyan(tag)}`; } else { - tagInfo = `🏷 tag: ${chalk.cyan('master')}`; + tagInfo = `🏷 tag: ${chalk.cyan('master')}`; } console.log(tagInfo); @@ -39,7 +39,5 @@ export function displayHeader(options: HeaderOptions = {}): void { : `${process.cwd()}/${filePath}`; console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`); } - - console.log(); // Empty line for spacing } } diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 9d36315e..f3032bd5 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -2441,57 +2441,6 @@ ${result.result} } }); - // next command - programInstance - .command('next') - .description( - `Show the next task to work on based on dependencies and status${chalk.reset('')}` - ) - .option( - '-f, --file ', - 'Path to the tasks file', - TASKMASTER_TASKS_FILE - ) - .option( - '-r, --report ', - 'Path to the complexity report file', - COMPLEXITY_REPORT_FILE - ) - .option('--tag ', 'Specify tag context for task operations') - .action(async (options) => { - const initOptions = { - tasksPath: options.file || true, - tag: options.tag - }; - - if (options.report && options.report !== COMPLEXITY_REPORT_FILE) { - initOptions.complexityReportPath = options.report; - } - - // Initialize TaskMaster - const taskMaster = initTaskMaster({ - tasksPath: options.file || true, - tag: options.tag, - complexityReportPath: options.report || false - }); - - const tag = taskMaster.getCurrentTag(); - - const context = { - projectRoot: taskMaster.getProjectRoot(), - tag - }; - - // Show current tag context - displayCurrentTagIndicator(tag); - - await displayNextTask( - taskMaster.getTasksPath(), - taskMaster.getComplexityReportPath(), - context - ); - }); - // add-dependency command programInstance .command('add-dependency')