Compare commits
2 Commits
ralph/fix/
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74997468de | ||
|
|
d7fca1844f |
5
.changeset/metal-rocks-help.md
Normal file
5
.changeset/metal-rocks-help.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Improve next command to work with remote
|
||||
@@ -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',
|
||||
|
||||
247
apps/cli/src/commands/next.command.ts
Normal file
247
apps/cli/src/commands/next.command.ts
Normal file
@@ -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<StorageType, 'auto'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <tag>', 'Filter by tag')
|
||||
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||
.action(async (options: NextCommandOptions) => {
|
||||
await this.executeCommand(options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the next command
|
||||
*/
|
||||
private async executeCommand(options: NextCommandOptions): Promise<void> {
|
||||
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<void> {
|
||||
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<NextTaskResult> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,18 @@ sidebarTitle: "CLI Commands"
|
||||
```bash
|
||||
# Show the next task to work on based on dependencies and status
|
||||
task-master next
|
||||
|
||||
# Filter by tag
|
||||
task-master next --tag <tag>
|
||||
|
||||
# Output in JSON format (useful for programmatic usage)
|
||||
task-master next --format json
|
||||
|
||||
# Suppress output (useful for scripts)
|
||||
task-master next --silent
|
||||
|
||||
# Specify project directory
|
||||
task-master next --project /path/to/project
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
|
||||
30
output.txt
Normal file
30
output.txt
Normal file
File diff suppressed because one or more lines are too long
@@ -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 <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option(
|
||||
'-r, --report <report>',
|
||||
'Path to the complexity report file',
|
||||
COMPLEXITY_REPORT_FILE
|
||||
)
|
||||
.option('--tag <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')
|
||||
|
||||
Reference in New Issue
Block a user