Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
7ee8f67d9c docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: fix: UI list and show (#1210)\n\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-09-17 13:13:38 +00:00
Ralph Khreish
0e8c42c7cb fix: UI list and show (#1210) 2025-09-17 15:05:33 +02:00
Ralph Khreish
799d1d2cce chore: fix env variables (#1204)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-13 01:34:50 +02:00
losolosol
83af314879 feat: added vscode start task button (#1201)
Co-authored-by: Carlos Montoya <carlos@Carloss-MacBook-Pro.local>
Co-authored-by: Carlos Montoya <los@losmontoya.com>
2025-09-12 05:35:57 +02:00
Ralph Khreish
dd03374496 chore: fix CI p2 2025-09-11 18:44:40 -07:00
Ralph Khreish
4ab0affba7 chore: fix CI 2025-09-11 18:35:07 -07:00
Ralph Khreish
77e1ddc237 feat: add tm show (#1199) 2025-09-12 03:34:32 +02:00
76 changed files with 4023 additions and 6048 deletions

View File

@@ -6,7 +6,7 @@
"repo": "eyaltoledano/claude-task-master"
}
],
"commit": false,
"commit": true,
"fixed": [],
"linked": [],
"access": "public",

View File

@@ -0,0 +1,5 @@
---
"extension": minor
---
Added a Start Build button to the VSCODE Task Properties Right Panel

View File

@@ -61,7 +61,7 @@ jobs:
timeout-minutes: 5
- name: Typecheck
run: npm run typecheck
run: npm run turbo:typecheck
env:
FORCE_COLOR: 1
@@ -84,7 +84,7 @@ jobs:
timeout-minutes: 5
- name: Build
run: npm run build
run: npm run turbo:build
env:
NODE_ENV: production
FORCE_COLOR: 1

5
.gitignore vendored
View File

@@ -93,4 +93,7 @@ dev-debug.log
apps/extension/.vscode-test/
# apps/extension
apps/extension/vsix-build/
apps/extension/vsix-build/
# turbo
.turbo

2
.nvmrc
View File

@@ -1 +1 @@
22
22

View File

@@ -6,15 +6,10 @@
"main": "./dist/index.js",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.js"
}
".": "./src/index.ts"
},
"files": ["dist", "README.md"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"lint": "biome check src",
"format": "biome format --write src",
@@ -28,7 +23,6 @@
},
"dependencies": {
"@tm/core": "*",
"@tm/workflow-engine": "*",
"boxen": "^7.1.1",
"chalk": "5.6.2",
"cli-table3": "^0.6.5",
@@ -38,10 +32,8 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@tm/build-config": "*",
"@types/inquirer": "^9.0.3",
"@types/node": "^22.10.5",
"tsup": "^8.3.0",
"tsx": "^4.20.4",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
@@ -51,5 +43,10 @@
},
"keywords": ["task-master", "cli", "task-management", "productivity"],
"author": "",
"license": "MIT"
"license": "MIT",
"typesVersions": {
"*": {
"*": ["src/*"]
}
}
}

View File

@@ -494,6 +494,17 @@ export class AuthCommand extends Command {
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const authCommand = new AuthCommand();
program.addCommand(authCommand);
return authCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): AuthCommand {
const authCommand = new AuthCommand(name);

View File

@@ -1,38 +0,0 @@
/**
* Command registry - exports all CLI commands for central registration
*/
import type { Command } from 'commander';
import { ListTasksCommand } from './list.command.js';
import { AuthCommand } from './auth.command.js';
import WorkflowCommand from './workflow.command.js';
// Define interface for command classes that can register themselves
export interface CommandRegistrar {
register(program: Command, name?: string): any;
}
// Future commands can be added here as they're created
// The pattern is: each command exports a class with a static register(program: Command, name?: string) method
/**
* Auto-register all exported commands that implement the CommandRegistrar interface
*/
export function registerAllCommands(program: Command): void {
// Get all exports from this module
const commands = [
ListTasksCommand,
AuthCommand,
WorkflowCommand
// Add new commands here as they're imported above
];
commands.forEach((CommandClass) => {
if (
'register' in CommandClass &&
typeof CommandClass.register === 'function'
) {
CommandClass.register(program);
}
});
}

View File

@@ -17,6 +17,18 @@ import {
} from '@tm/core';
import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js';
import {
displayHeader,
displayDashboards,
calculateTaskStatistics,
calculateSubtaskStatistics,
calculateDependencyStatistics,
getPriorityBreakdown,
displayRecommendedNextTask,
getTaskDescription,
displaySuggestedNextSteps,
type NextTaskInfo
} from '../ui/index.js';
/**
* Options interface for the list command
@@ -173,13 +185,6 @@ export class ListTasksCommand extends Command {
includeSubtasks: options.withSubtasks
});
// Runtime guard to prevent 'auto' from reaching CLI consumers
if (result.storageType === 'auto') {
throw new Error(
'Internal error: unresolved storage type reached CLI. Please check TaskService.getStorageType() implementation.'
);
}
return result as ListTasksResult;
}
@@ -252,19 +257,16 @@ export class ListTasksCommand extends Command {
* Display in text format with tables
*/
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
const { tasks, total, filtered, tag, storageType } = data;
const { tasks, tag } = data;
// Header
ui.displayBanner(`Task List${tag ? ` (${tag})` : ''}`);
// Get file path for display
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
// Statistics
console.log(chalk.blue.bold('\n📊 Statistics:\n'));
console.log(` Total tasks: ${chalk.cyan(total)}`);
console.log(` Filtered: ${chalk.cyan(filtered)}`);
if (tag) {
console.log(` Tag: ${chalk.cyan(tag)}`);
}
console.log(` Storage: ${chalk.cyan(storageType)}`);
// Display header without banner (banner already shown by main CLI)
displayHeader({
tag: tag || 'master',
filePath: filePath
});
// No tasks message
if (tasks.length === 0) {
@@ -272,21 +274,50 @@ export class ListTasksCommand extends Command {
return;
}
// Task table
console.log(chalk.blue.bold(`\n📋 Tasks (${tasks.length}):\n`));
// Calculate statistics
const taskStats = calculateTaskStatistics(tasks);
const subtaskStats = calculateSubtaskStatistics(tasks);
const depStats = calculateDependencyStatistics(tasks);
const priorityBreakdown = getPriorityBreakdown(tasks);
// Find next task following the same logic as findNextTask
const nextTask = this.findNextTask(tasks);
// Display dashboard boxes
displayDashboards(
taskStats,
subtaskStats,
priorityBreakdown,
depStats,
nextTask
);
// Task table - no title, just show the table directly
console.log(
ui.createTaskTable(tasks, {
showSubtasks: withSubtasks,
showDependencies: true
showDependencies: true,
showComplexity: true // Enable complexity column
})
);
// Progress bar
const completedCount = tasks.filter(
(t: Task) => t.status === 'done'
).length;
console.log(chalk.blue.bold('\n📊 Overall Progress:\n'));
console.log(` ${ui.createProgressBar(completedCount, tasks.length)}`);
// Display recommended next task section immediately after table
if (nextTask) {
// Find the full task object to get description
const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id));
const description = fullTask ? getTaskDescription(fullTask) : undefined;
displayRecommendedNextTask({
...nextTask,
status: 'pending', // Next task is typically pending
description
});
} else {
displayRecommendedNextTask(undefined);
}
// Display suggested next steps at the end
displaySuggestedNextSteps();
}
/**
@@ -296,6 +327,128 @@ export class ListTasksCommand extends Command {
this.lastResult = result;
}
/**
* Find the next task to work on
* Implements the same logic as scripts/modules/task-manager/find-next-task.js
*/
private findNextTask(tasks: Task[]): NextTaskInfo | undefined {
const priorityValues: Record<string, number> = {
critical: 4,
high: 3,
medium: 2,
low: 1
};
// Build set of completed task IDs (including subtasks)
const completedIds = new Set<string>();
tasks.forEach((t) => {
if (t.status === 'done' || t.status === 'completed') {
completedIds.add(String(t.id));
}
if (t.subtasks) {
t.subtasks.forEach((st) => {
if (st.status === 'done' || st.status === 'completed') {
completedIds.add(`${t.id}.${st.id}`);
}
});
}
});
// First, look for eligible subtasks in in-progress parent tasks
const candidateSubtasks: NextTaskInfo[] = [];
tasks
.filter(
(t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0
)
.forEach((parent) => {
parent.subtasks!.forEach((st) => {
const stStatus = (st.status || 'pending').toLowerCase();
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
// Check if dependencies are satisfied
const fullDeps =
st.dependencies?.map((d) => {
// Handle both numeric and string IDs
if (typeof d === 'string' && d.includes('.')) {
return d;
}
return `${parent.id}.${d}`;
}) ?? [];
const depsSatisfied =
fullDeps.length === 0 ||
fullDeps.every((depId) => completedIds.has(String(depId)));
if (depsSatisfied) {
candidateSubtasks.push({
id: `${parent.id}.${st.id}`,
title: st.title || `Subtask ${st.id}`,
priority: st.priority || parent.priority || 'medium',
dependencies: fullDeps.map((d) => String(d))
});
}
});
});
if (candidateSubtasks.length > 0) {
// Sort by priority, then by dependencies count, then by ID
candidateSubtasks.sort((a, b) => {
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
const depCountA = a.dependencies?.length || 0;
const depCountB = b.dependencies?.length || 0;
if (depCountA !== depCountB) return depCountA - depCountB;
return String(a.id).localeCompare(String(b.id));
});
return candidateSubtasks[0];
}
// Fall back to finding eligible top-level tasks
const eligibleTasks = tasks.filter((task) => {
// Skip non-eligible statuses
const status = (task.status || 'pending').toLowerCase();
if (status !== 'pending' && status !== 'in-progress') return false;
// Check dependencies
const deps = task.dependencies || [];
const depsSatisfied =
deps.length === 0 ||
deps.every((depId) => completedIds.has(String(depId)));
return depsSatisfied;
});
if (eligibleTasks.length === 0) return undefined;
// Sort eligible tasks
eligibleTasks.sort((a, b) => {
// Priority (higher first)
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
// Dependencies count (fewer first)
const depCountA = a.dependencies?.length || 0;
const depCountB = b.dependencies?.length || 0;
if (depCountA !== depCountB) return depCountA - depCountB;
// ID (lower first)
return Number(a.id) - Number(b.id);
});
const nextTask = eligibleTasks[0];
return {
id: nextTask.id,
title: nextTask.title,
priority: nextTask.priority,
dependencies: nextTask.dependencies?.map((d) => String(d))
};
}
/**
* Get the last result (for programmatic usage)
*/
@@ -315,6 +468,17 @@ export class ListTasksCommand extends Command {
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const listCommand = new ListTasksCommand();
program.addCommand(listCommand);
return listCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): ListTasksCommand {
const listCommand = new ListTasksCommand(name);

View File

@@ -0,0 +1,383 @@
/**
* @fileoverview ShowCommand using Commander's native class pattern
* Extends Commander.Command for better integration with the framework
*/
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 * as ui from '../utils/ui.js';
import {
displayTaskHeader,
displayTaskProperties,
displayImplementationDetails,
displayTestStrategy,
displaySubtasks,
displaySuggestedActions
} from '../ui/components/task-detail.component.js';
/**
* Options interface for the show command
*/
export interface ShowCommandOptions {
id?: string;
status?: string;
format?: 'text' | 'json';
silent?: boolean;
project?: string;
}
/**
* Result type from show command
*/
export interface ShowTaskResult {
task: Task | null;
found: boolean;
storageType: Exclude<StorageType, 'auto'>;
}
/**
* Result type for multiple tasks
*/
export interface ShowMultipleTasksResult {
tasks: Task[];
notFound: string[];
storageType: Exclude<StorageType, 'auto'>;
}
/**
* ShowCommand extending Commander's Command class
* This is a thin presentation layer over @tm/core
*/
export class ShowCommand extends Command {
private tmCore?: TaskMasterCore;
private lastResult?: ShowTaskResult | ShowMultipleTasksResult;
constructor(name?: string) {
super(name || 'show');
// Configure the command
this.description('Display detailed information about one or more tasks')
.argument('[id]', 'Task ID(s) to show (comma-separated for multiple)')
.option(
'-i, --id <id>',
'Task ID(s) to show (comma-separated for multiple)'
)
.option('-s, --status <status>', 'Filter subtasks by status')
.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 (taskId: string | undefined, options: ShowCommandOptions) => {
await this.executeCommand(taskId, options);
}
);
}
/**
* Execute the show command
*/
private async executeCommand(
taskId: string | undefined,
options: ShowCommandOptions
): Promise<void> {
try {
// Validate options
if (!this.validateOptions(options)) {
process.exit(1);
}
// Initialize tm-core
await this.initializeCore(options.project || process.cwd());
// Get the task ID from argument or option
const idArg = taskId || options.id;
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
process.exit(1);
}
// Check if multiple IDs are provided (comma-separated)
const taskIds = idArg
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0);
// Get tasks from core
const result =
taskIds.length > 1
? await this.getMultipleTasks(taskIds, options)
: await this.getSingleTask(taskIds[0], 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)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
/**
* Validate command options
*/
private validateOptions(options: ShowCommandOptions): boolean {
// Validate format
if (options.format && !['text', 'json'].includes(options.format)) {
console.error(chalk.red(`Invalid format: ${options.format}`));
console.error(chalk.gray(`Valid formats: text, json`));
return false;
}
return true;
}
/**
* Initialize TaskMasterCore
*/
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
}
}
/**
* Get a single task from tm-core
*/
private async getSingleTask(
taskId: string,
_options: ShowCommandOptions
): Promise<ShowTaskResult> {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
// Get the task
const task = await this.tmCore.getTask(taskId);
// Get storage type
const storageType = this.tmCore.getStorageType();
return {
task,
found: task !== null,
storageType: storageType as Exclude<StorageType, 'auto'>
};
}
/**
* Get multiple tasks from tm-core
*/
private async getMultipleTasks(
taskIds: string[],
_options: ShowCommandOptions
): Promise<ShowMultipleTasksResult> {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
const tasks: Task[] = [];
const notFound: string[] = [];
// Get each task individually
for (const taskId of taskIds) {
const task = await this.tmCore.getTask(taskId);
if (task) {
tasks.push(task);
} else {
notFound.push(taskId);
}
}
// Get storage type
const storageType = this.tmCore.getStorageType();
return {
tasks,
notFound,
storageType: storageType as Exclude<StorageType, 'auto'>
};
}
/**
* Display results based on format
*/
private displayResults(
result: ShowTaskResult | ShowMultipleTasksResult,
options: ShowCommandOptions
): void {
const format = options.format || 'text';
switch (format) {
case 'json':
this.displayJson(result);
break;
case 'text':
default:
if ('task' in result) {
// Single task result
this.displaySingleTask(result, options);
} else {
// Multiple tasks result
this.displayMultipleTasks(result, options);
}
break;
}
}
/**
* Display in JSON format
*/
private displayJson(result: ShowTaskResult | ShowMultipleTasksResult): void {
console.log(JSON.stringify(result, null, 2));
}
/**
* Display a single task in text format
*/
private displaySingleTask(
result: ShowTaskResult,
options: ShowCommandOptions
): void {
if (!result.found || !result.task) {
console.log(
boxen(chalk.yellow(`Task not found!`), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1 }
})
);
return;
}
const task = result.task;
// Display header with tag
displayTaskHeader(task.id, task.title);
// Display task properties in table format
displayTaskProperties(task);
// Display implementation details if available
if (task.details) {
console.log(); // Empty line for spacing
displayImplementationDetails(task.details);
}
// Display test strategy if available
if ('testStrategy' in task && task.testStrategy) {
console.log(); // Empty line for spacing
displayTestStrategy(task.testStrategy as string);
}
// Display subtasks if available
if (task.subtasks && task.subtasks.length > 0) {
// Filter subtasks by status if provided
const filteredSubtasks = options.status
? task.subtasks.filter((sub) => sub.status === options.status)
: task.subtasks;
if (filteredSubtasks.length === 0 && options.status) {
console.log(
chalk.gray(` No subtasks with status '${options.status}'`)
);
} else {
displaySubtasks(filteredSubtasks, task.id);
}
}
// Display suggested actions
displaySuggestedActions(task.id);
}
/**
* Display multiple tasks in text format
*/
private displayMultipleTasks(
result: ShowMultipleTasksResult,
_options: ShowCommandOptions
): void {
// Header
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
if (result.notFound.length > 0) {
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
}
if (result.tasks.length === 0) {
ui.displayWarning('No tasks found matching the criteria.');
return;
}
// Task table
console.log(chalk.blue.bold(`\n📋 Tasks:\n`));
console.log(
ui.createTaskTable(result.tasks, {
showSubtasks: true,
showDependencies: true
})
);
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**
* Set the last result for programmatic access
*/
private setLastResult(
result: ShowTaskResult | ShowMultipleTasksResult
): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): ShowTaskResult | ShowMultipleTasksResult | undefined {
return this.lastResult;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined;
}
}
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const showCommand = new ShowCommand();
program.addCommand(showCommand);
return showCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
*/
static register(program: Command, name?: string): ShowCommand {
const showCommand = new ShowCommand(name);
program.addCommand(showCommand);
return showCommand;
}
}

View File

@@ -1,58 +0,0 @@
/**
* @fileoverview Workflow Command
* Main workflow command with subcommands
*/
import { Command } from 'commander';
import {
WorkflowStartCommand,
WorkflowListCommand,
WorkflowStopCommand,
WorkflowStatusCommand
} from './workflow/index.js';
/**
* WorkflowCommand - Main workflow command with subcommands
*/
export class WorkflowCommand extends Command {
constructor(name?: string) {
super(name || 'workflow');
this.description('Manage task execution workflows with git worktrees and Claude Code')
.alias('wf');
// Register subcommands
this.addSubcommands();
}
private addSubcommands(): void {
// Start workflow
WorkflowStartCommand.register(this);
// List workflows
WorkflowListCommand.register(this);
// Stop workflow
WorkflowStopCommand.register(this);
// Show workflow status
WorkflowStatusCommand.register(this);
// Alias commands for convenience
this.addCommand(new WorkflowStartCommand('run')); // tm workflow run <task-id>
this.addCommand(new WorkflowStopCommand('kill')); // tm workflow kill <workflow-id>
this.addCommand(new WorkflowStatusCommand('info')); // tm workflow info <workflow-id>
}
/**
* Static method to register this command on an existing program
*/
static register(program: Command, name?: string): WorkflowCommand {
const workflowCommand = new WorkflowCommand(name);
program.addCommand(workflowCommand);
return workflowCommand;
}
}
export default WorkflowCommand;

View File

@@ -1,9 +0,0 @@
/**
* @fileoverview Workflow Commands
* Exports for all workflow-related CLI commands
*/
export * from './workflow-start.command.js';
export * from './workflow-list.command.js';
export * from './workflow-stop.command.js';
export * from './workflow-status.command.js';

View File

@@ -1,253 +0,0 @@
/**
* @fileoverview Workflow List Command
* List active and recent workflow executions
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig,
type WorkflowExecutionContext
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowListOptions {
project?: string;
status?: string;
format?: 'text' | 'json' | 'compact';
worktreeBase?: string;
claude?: string;
all?: boolean;
}
/**
* WorkflowListCommand - List workflow executions
*/
export class WorkflowListCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'list');
this.description('List active and recent workflow executions')
.alias('ls')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('-s, --status <status>', 'Filter by status (running, completed, failed, etc.)')
.option('-f, --format <format>', 'Output format (text, json, compact)', 'text')
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('--all', 'Show all workflows including completed ones')
.action(async (options: WorkflowListOptions) => {
await this.executeCommand(options);
});
}
private async executeCommand(options: WorkflowListOptions): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
// Get workflows
let workflows = this.workflowManager!.listWorkflows();
// Apply status filter
if (options.status) {
workflows = workflows.filter(w => w.status === options.status);
}
// Apply active filter (default behavior)
if (!options.all) {
workflows = workflows.filter(w =>
['pending', 'initializing', 'running', 'paused'].includes(w.status)
);
}
// Display results
this.displayResults(workflows, options);
} catch (error: any) {
ui.displayError(error.message || 'Failed to list workflows');
process.exit(1);
}
}
private async initializeWorkflowManager(options: WorkflowListOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private displayResults(workflows: WorkflowExecutionContext[], options: WorkflowListOptions): void {
switch (options.format) {
case 'json':
this.displayJson(workflows);
break;
case 'compact':
this.displayCompact(workflows);
break;
case 'text':
default:
this.displayText(workflows);
break;
}
}
private displayJson(workflows: WorkflowExecutionContext[]): void {
console.log(JSON.stringify({
workflows: workflows.map(w => ({
workflowId: `workflow-${w.taskId}`,
taskId: w.taskId,
taskTitle: w.taskTitle,
status: w.status,
worktreePath: w.worktreePath,
branchName: w.branchName,
processId: w.processId,
startedAt: w.startedAt,
lastActivity: w.lastActivity,
metadata: w.metadata
})),
total: workflows.length,
timestamp: new Date().toISOString()
}, null, 2));
}
private displayCompact(workflows: WorkflowExecutionContext[]): void {
if (workflows.length === 0) {
console.log(chalk.gray('No workflows found'));
return;
}
workflows.forEach(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
const statusDisplay = this.getStatusDisplay(workflow.status);
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
console.log(
`${chalk.cyan(workflowId)} ${statusDisplay} ${workflow.taskTitle} ${chalk.gray(`(${duration})`)}`
);
});
}
private displayText(workflows: WorkflowExecutionContext[]): void {
ui.displayBanner('Active Workflows');
if (workflows.length === 0) {
ui.displayWarning('No workflows found');
console.log();
console.log(chalk.blue('💡 Start a new workflow with:'));
console.log(` ${chalk.cyan('tm workflow start <task-id>')}`);
return;
}
// Statistics
console.log(chalk.blue.bold('\n📊 Statistics:\n'));
const statusCounts = this.getStatusCounts(workflows);
Object.entries(statusCounts).forEach(([status, count]) => {
console.log(` ${this.getStatusDisplay(status)}: ${chalk.cyan(count)}`);
});
// Workflows table
console.log(chalk.blue.bold(`\n🔄 Workflows (${workflows.length}):\n`));
const tableData = workflows.map(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
return [
chalk.cyan(workflowId),
chalk.yellow(workflow.taskId),
workflow.taskTitle.substring(0, 30) + (workflow.taskTitle.length > 30 ? '...' : ''),
this.getStatusDisplay(workflow.status),
workflow.processId ? chalk.green(workflow.processId.toString()) : chalk.gray('N/A'),
chalk.gray(duration),
chalk.gray(path.basename(workflow.worktreePath))
];
});
console.log(ui.createTable(
['Workflow ID', 'Task ID', 'Task Title', 'Status', 'PID', 'Duration', 'Worktree'],
tableData
));
// Running workflows actions
const runningWorkflows = workflows.filter(w => w.status === 'running');
if (runningWorkflows.length > 0) {
console.log(chalk.blue.bold('\n🚀 Quick Actions:\n'));
runningWorkflows.slice(0, 3).forEach(workflow => {
const workflowId = `workflow-${workflow.taskId}`;
console.log(` • Attach to ${chalk.cyan(workflowId)}: ${chalk.gray(`tm workflow attach ${workflowId}`)}`);
});
if (runningWorkflows.length > 3) {
console.log(` ${chalk.gray(`... and ${runningWorkflows.length - 3} more`)}`);
}
}
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.orange },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || { icon: '❓', color: chalk.white };
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
private getStatusCounts(workflows: WorkflowExecutionContext[]): Record<string, number> {
const counts: Record<string, number> = {};
workflows.forEach(workflow => {
counts[workflow.status] = (counts[workflow.status] || 0) + 1;
});
return counts;
}
private formatDuration(start: Date, end: Date): string {
const diff = end.getTime() - start.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else {
return '<1m';
}
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowListCommand {
const command = new WorkflowListCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -1,239 +0,0 @@
/**
* @fileoverview Workflow Start Command
* Start task execution in isolated worktree with Claude Code process
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
createTaskMasterCore,
type TaskMasterCore
} from '@tm/core';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStartOptions {
project?: string;
branch?: string;
timeout?: number;
worktreeBase?: string;
claude?: string;
debug?: boolean;
env?: string;
}
/**
* WorkflowStartCommand - Start task execution workflow
*/
export class WorkflowStartCommand extends Command {
private tmCore?: TaskMasterCore;
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'start');
this.description('Start task execution in isolated worktree')
.argument('<task-id>', 'Task ID to execute')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('-b, --branch <name>', 'Custom branch name for worktree')
.option('-t, --timeout <minutes>', 'Execution timeout in minutes', '60')
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('--debug', 'Enable debug logging')
.option('--env <vars>', 'Environment variables (KEY=VALUE,KEY2=VALUE2)')
.action(async (taskId: string, options: WorkflowStartOptions) => {
await this.executeCommand(taskId, options);
});
}
private async executeCommand(taskId: string, options: WorkflowStartOptions): Promise<void> {
try {
// Initialize components
await this.initializeCore(options.project || process.cwd());
await this.initializeWorkflowManager(options);
// Get task details
const task = await this.getTask(taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
// Check if task already has active workflow
const existingWorkflow = this.workflowManager!.getWorkflowByTaskId(taskId);
if (existingWorkflow) {
ui.displayWarning(`Task ${taskId} already has an active workflow`);
console.log(`Workflow ID: ${chalk.cyan('workflow-' + taskId)}`);
console.log(`Status: ${this.getStatusDisplay(existingWorkflow.status)}`);
console.log(`Worktree: ${chalk.gray(existingWorkflow.worktreePath)}`);
return;
}
// Parse environment variables
const env = this.parseEnvironmentVariables(options.env);
// Display task info
ui.displayBanner(`Starting Workflow for Task ${taskId}`);
console.log(`${chalk.blue('Task:')} ${task.title}`);
console.log(`${chalk.blue('Description:')} ${task.description}`);
if (task.dependencies?.length) {
console.log(`${chalk.blue('Dependencies:')} ${task.dependencies.join(', ')}`);
}
console.log(`${chalk.blue('Priority:')} ${task.priority || 'normal'}`);
console.log();
// Start workflow
ui.displaySpinner('Creating worktree and starting Claude Code process...');
const workflowId = await this.workflowManager!.startTaskExecution(task, {
branchName: options.branch,
timeout: parseInt(options.timeout || '60'),
env
});
const workflow = this.workflowManager!.getWorkflowStatus(workflowId);
ui.displaySuccess('Workflow started successfully!');
console.log();
console.log(`${chalk.green('✓')} Workflow ID: ${chalk.cyan(workflowId)}`);
console.log(`${chalk.green('✓')} Worktree: ${chalk.gray(workflow?.worktreePath)}`);
console.log(`${chalk.green('✓')} Branch: ${chalk.gray(workflow?.branchName)}`);
console.log(`${chalk.green('✓')} Process ID: ${chalk.gray(workflow?.processId)}`);
console.log();
// Display next steps
console.log(chalk.blue.bold('📋 Next Steps:'));
console.log(` • Monitor: ${chalk.cyan(`tm workflow status ${workflowId}`)}`);
console.log(` • Attach: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
console.log();
// Setup event listeners for real-time updates
this.setupEventListeners();
} catch (error: any) {
ui.displayError(error.message || 'Failed to start workflow');
if (options.debug && error.stack) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
}
}
private async initializeWorkflowManager(options: WorkflowStartOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: parseInt(options.timeout || '60'),
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: options.debug || false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async getTask(taskId: string) {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
const result = await this.tmCore.getTaskList({});
return result.tasks.find(task => task.id === taskId);
}
private parseEnvironmentVariables(envString?: string): Record<string, string> | undefined {
if (!envString) return undefined;
const env: Record<string, string> = {};
for (const pair of envString.split(',')) {
const [key, ...valueParts] = pair.trim().split('=');
if (key && valueParts.length > 0) {
env[key] = valueParts.join('=');
}
}
return Object.keys(env).length > 0 ? env : undefined;
}
private getStatusDisplay(status: string): string {
const colors = {
pending: chalk.yellow,
initializing: chalk.blue,
running: chalk.green,
paused: chalk.orange,
completed: chalk.green,
failed: chalk.red,
cancelled: chalk.gray,
timeout: chalk.red
};
const color = colors[status as keyof typeof colors] || chalk.white;
return color(status);
}
private setupEventListeners(): void {
if (!this.workflowManager) return;
this.workflowManager.on('workflow.started', (event) => {
console.log(`${chalk.green('🚀')} Workflow started: ${event.workflowId}`);
});
this.workflowManager.on('process.output', (event) => {
if (event.data?.stream === 'stdout') {
console.log(`${chalk.blue('[OUT]')} ${event.data.data.trim()}`);
} else if (event.data?.stream === 'stderr') {
console.log(`${chalk.red('[ERR]')} ${event.data.data.trim()}`);
}
});
this.workflowManager.on('workflow.completed', (event) => {
console.log(`${chalk.green('✅')} Workflow completed: ${event.workflowId}`);
});
this.workflowManager.on('workflow.failed', (event) => {
console.log(`${chalk.red('❌')} Workflow failed: ${event.workflowId}`);
if (event.error) {
console.log(`${chalk.red('Error:')} ${event.error.message}`);
}
});
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
// Don't cleanup workflows, just disconnect
this.workflowManager.removeAllListeners();
}
if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined;
}
}
static register(program: Command, name?: string): WorkflowStartCommand {
const command = new WorkflowStartCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -1,339 +0,0 @@
/**
* @fileoverview Workflow Status Command
* Show detailed status of a specific workflow
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStatusOptions {
project?: string;
worktreeBase?: string;
claude?: string;
watch?: boolean;
format?: 'text' | 'json';
}
/**
* WorkflowStatusCommand - Show workflow execution status
*/
export class WorkflowStatusCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'status');
this.description('Show detailed status of a workflow execution')
.argument('<workflow-id>', 'Workflow ID or task ID to check')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option('--worktree-base <path>', 'Base directory for worktrees', '../task-worktrees')
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('-w, --watch', 'Watch for status changes (refresh every 2 seconds)')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.action(async (workflowId: string, options: WorkflowStatusOptions) => {
await this.executeCommand(workflowId, options);
});
}
private async executeCommand(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
if (options.watch) {
await this.watchWorkflowStatus(workflowId, options);
} else {
await this.showWorkflowStatus(workflowId, options);
}
} catch (error: any) {
ui.displayError(error.message || 'Failed to get workflow status');
process.exit(1);
}
}
private async initializeWorkflowManager(options: WorkflowStatusOptions): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(projectRoot, options.worktreeBase || '../task-worktrees');
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async showWorkflowStatus(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
// Try to find workflow by ID or task ID
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
// Try as task ID
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}
if (options.format === 'json') {
this.displayJsonStatus(workflow);
} else {
this.displayTextStatus(workflow);
}
}
private async watchWorkflowStatus(workflowId: string, options: WorkflowStatusOptions): Promise<void> {
console.log(chalk.blue.bold('👀 Watching workflow status (Press Ctrl+C to exit)\n'));
let lastStatus = '';
let updateCount = 0;
const updateStatus = async () => {
try {
// Clear screen and move cursor to top
if (updateCount > 0) {
process.stdout.write('\x1b[2J\x1b[0f');
}
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
console.log(chalk.red(`Workflow not found: ${workflowId}`));
return;
}
// Display header with timestamp
console.log(chalk.blue.bold('👀 Watching Workflow Status'));
console.log(chalk.gray(`Last updated: ${new Date().toLocaleTimeString()}\n`));
this.displayTextStatus(workflow);
// Check if workflow has ended
if (['completed', 'failed', 'cancelled', 'timeout'].includes(workflow.status)) {
console.log(chalk.yellow('\n⚠ Workflow has ended. Stopping watch mode.'));
return;
}
updateCount++;
} catch (error) {
console.error(chalk.red('Error updating status:'), error);
}
};
// Initial display
await updateStatus();
// Setup interval for updates
const interval = setInterval(updateStatus, 2000);
// Handle Ctrl+C
process.on('SIGINT', () => {
clearInterval(interval);
console.log(chalk.yellow('\n👋 Stopped watching workflow status'));
process.exit(0);
});
// Keep the process alive
await new Promise(() => {});
}
private displayJsonStatus(workflow: any): void {
const status = {
workflowId: `workflow-${workflow.taskId}`,
taskId: workflow.taskId,
taskTitle: workflow.taskTitle,
taskDescription: workflow.taskDescription,
status: workflow.status,
worktreePath: workflow.worktreePath,
branchName: workflow.branchName,
processId: workflow.processId,
startedAt: workflow.startedAt,
lastActivity: workflow.lastActivity,
duration: this.calculateDuration(workflow.startedAt, workflow.lastActivity),
metadata: workflow.metadata
};
console.log(JSON.stringify(status, null, 2));
}
private displayTextStatus(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
const duration = this.formatDuration(workflow.startedAt, workflow.lastActivity);
ui.displayBanner(`Workflow Status: ${workflowId}`);
// Basic information
console.log(chalk.blue.bold('\n📋 Basic Information:\n'));
console.log(` Workflow ID: ${chalk.cyan(workflowId)}`);
console.log(` Task ID: ${chalk.cyan(workflow.taskId)}`);
console.log(` Task Title: ${workflow.taskTitle}`);
console.log(` Status: ${this.getStatusDisplay(workflow.status)}`);
console.log(` Duration: ${chalk.gray(duration)}`);
// Task details
if (workflow.taskDescription) {
console.log(chalk.blue.bold('\n📝 Task Details:\n'));
console.log(` ${workflow.taskDescription}`);
}
// Process information
console.log(chalk.blue.bold('\n⚙ Process Information:\n'));
console.log(` Process ID: ${workflow.processId ? chalk.green(workflow.processId) : chalk.gray('N/A')}`);
console.log(` Worktree: ${chalk.gray(workflow.worktreePath)}`);
console.log(` Branch: ${chalk.gray(workflow.branchName)}`);
// Timing information
console.log(chalk.blue.bold('\n⏰ Timing:\n'));
console.log(` Started: ${chalk.gray(workflow.startedAt.toLocaleString())}`);
console.log(` Last Activity: ${chalk.gray(workflow.lastActivity.toLocaleString())}`);
// Metadata
if (workflow.metadata && Object.keys(workflow.metadata).length > 0) {
console.log(chalk.blue.bold('\n🔖 Metadata:\n'));
Object.entries(workflow.metadata).forEach(([key, value]) => {
console.log(` ${key}: ${chalk.gray(String(value))}`);
});
}
// Status-specific information
this.displayStatusSpecificInfo(workflow);
// Actions
this.displayAvailableActions(workflow);
}
private displayStatusSpecificInfo(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
switch (workflow.status) {
case 'running':
console.log(chalk.blue.bold('\n🚀 Running Status:\n'));
console.log(` ${chalk.green('●')} Process is actively executing`);
console.log(` ${chalk.blue('')} Monitor output with: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
break;
case 'paused':
console.log(chalk.blue.bold('\n⏸ Paused Status:\n'));
console.log(` ${chalk.yellow('●')} Workflow is paused`);
console.log(` ${chalk.blue('')} Resume with: ${chalk.cyan(`tm workflow resume ${workflowId}`)}`);
break;
case 'completed':
console.log(chalk.blue.bold('\n✅ Completed Status:\n'));
console.log(` ${chalk.green('●')} Workflow completed successfully`);
console.log(` ${chalk.blue('')} Resources have been cleaned up`);
break;
case 'failed':
console.log(chalk.blue.bold('\n❌ Failed Status:\n'));
console.log(` ${chalk.red('●')} Workflow execution failed`);
console.log(` ${chalk.blue('')} Check logs for error details`);
break;
case 'initializing':
console.log(chalk.blue.bold('\n🔄 Initializing Status:\n'));
console.log(` ${chalk.blue('●')} Setting up worktree and process`);
console.log(` ${chalk.blue('')} This should complete shortly`);
break;
}
}
private displayAvailableActions(workflow: any): void {
const workflowId = `workflow-${workflow.taskId}`;
console.log(chalk.blue.bold('\n🎯 Available Actions:\n'));
switch (workflow.status) {
case 'running':
console.log(` • Attach: ${chalk.cyan(`tm workflow attach ${workflowId}`)}`);
console.log(` • Pause: ${chalk.cyan(`tm workflow pause ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'paused':
console.log(` • Resume: ${chalk.cyan(`tm workflow resume ${workflowId}`)}`);
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'pending':
case 'initializing':
console.log(` • Stop: ${chalk.cyan(`tm workflow stop ${workflowId}`)}`);
break;
case 'completed':
case 'failed':
case 'cancelled':
console.log(` • View logs: ${chalk.cyan(`tm workflow logs ${workflowId}`)}`);
console.log(` • Start new: ${chalk.cyan(`tm workflow start ${workflow.taskId}`)}`);
break;
}
console.log(` • List all: ${chalk.cyan('tm workflow list')}`);
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.orange },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || { icon: '❓', color: chalk.white };
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
private formatDuration(start: Date, end: Date): string {
const diff = end.getTime() - start.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(minutes / 60);
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`;
} else {
return `${seconds}s`;
}
}
private calculateDuration(start: Date, end: Date): number {
return Math.floor((end.getTime() - start.getTime()) / 1000);
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowStatusCommand {
const command = new WorkflowStatusCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -1,260 +0,0 @@
/**
* @fileoverview Workflow Stop Command
* Stop and clean up workflow execution
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'node:path';
import {
TaskExecutionManager,
type TaskExecutionManagerConfig
} from '@tm/workflow-engine';
import * as ui from '../../utils/ui.js';
export interface WorkflowStopOptions {
project?: string;
worktreeBase?: string;
claude?: string;
force?: boolean;
all?: boolean;
}
/**
* WorkflowStopCommand - Stop workflow execution
*/
export class WorkflowStopCommand extends Command {
private workflowManager?: TaskExecutionManager;
constructor(name?: string) {
super(name || 'stop');
this.description('Stop workflow execution and clean up resources')
.argument('[workflow-id]', 'Workflow ID to stop (or task ID)')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option(
'--worktree-base <path>',
'Base directory for worktrees',
'../task-worktrees'
)
.option('--claude <path>', 'Claude Code executable path', 'claude')
.option('-f, --force', 'Force stop (kill process immediately)')
.option('--all', 'Stop all running workflows')
.action(
async (
workflowId: string | undefined,
options: WorkflowStopOptions
) => {
await this.executeCommand(workflowId, options);
}
);
}
private async executeCommand(
workflowId: string | undefined,
options: WorkflowStopOptions
): Promise<void> {
try {
// Initialize workflow manager
await this.initializeWorkflowManager(options);
if (options.all) {
await this.stopAllWorkflows(options);
} else if (workflowId) {
await this.stopSingleWorkflow(workflowId, options);
} else {
ui.displayError('Please specify a workflow ID or use --all flag');
process.exit(1);
}
} catch (error: any) {
ui.displayError(error.message || 'Failed to stop workflow');
process.exit(1);
}
}
private async initializeWorkflowManager(
options: WorkflowStopOptions
): Promise<void> {
if (!this.workflowManager) {
const projectRoot = options.project || process.cwd();
const worktreeBase = path.resolve(
projectRoot,
options.worktreeBase || '../task-worktrees'
);
const config: TaskExecutionManagerConfig = {
projectRoot,
maxConcurrent: 5,
defaultTimeout: 60,
worktreeBase,
claudeExecutable: options.claude || 'claude',
debug: false
};
this.workflowManager = new TaskExecutionManager(config);
await this.workflowManager.initialize();
}
}
private async stopSingleWorkflow(
workflowId: string,
options: WorkflowStopOptions
): Promise<void> {
// Try to find workflow by ID or task ID
let workflow = this.workflowManager!.getWorkflowStatus(workflowId);
if (!workflow) {
// Try as task ID
workflow = this.workflowManager!.getWorkflowByTaskId(workflowId);
}
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const actualWorkflowId = `workflow-${workflow.taskId}`;
// Display workflow info
console.log(chalk.blue.bold(`🛑 Stopping Workflow: ${actualWorkflowId}`));
console.log(`${chalk.blue('Task:')} ${workflow.taskTitle}`);
console.log(
`${chalk.blue('Status:')} ${this.getStatusDisplay(workflow.status)}`
);
console.log(
`${chalk.blue('Worktree:')} ${chalk.gray(workflow.worktreePath)}`
);
if (workflow.processId) {
console.log(
`${chalk.blue('Process ID:')} ${chalk.gray(workflow.processId)}`
);
}
console.log();
// Confirm if not forced
if (!options.force && ['running', 'paused'].includes(workflow.status)) {
const shouldProceed = await ui.confirm(
`Are you sure you want to stop this ${workflow.status} workflow?`
);
if (!shouldProceed) {
console.log(chalk.gray('Operation cancelled'));
return;
}
}
// Stop the workflow
ui.displaySpinner('Stopping workflow and cleaning up resources...');
await this.workflowManager!.stopTaskExecution(
actualWorkflowId,
options.force
);
ui.displaySuccess('Workflow stopped successfully!');
console.log();
console.log(`${chalk.green('✓')} Process terminated`);
console.log(`${chalk.green('✓')} Worktree cleaned up`);
console.log(`${chalk.green('✓')} State updated`);
}
private async stopAllWorkflows(options: WorkflowStopOptions): Promise<void> {
const workflows = this.workflowManager!.listWorkflows();
const activeWorkflows = workflows.filter((w) =>
['pending', 'initializing', 'running', 'paused'].includes(w.status)
);
if (activeWorkflows.length === 0) {
ui.displayWarning('No active workflows to stop');
return;
}
console.log(
chalk.blue.bold(`🛑 Stopping ${activeWorkflows.length} Active Workflows`)
);
console.log();
// List workflows to be stopped
activeWorkflows.forEach((workflow) => {
console.log(
`${chalk.cyan(`workflow-${workflow.taskId}`)} - ${workflow.taskTitle} ${this.getStatusDisplay(workflow.status)}`
);
});
console.log();
// Confirm if not forced
if (!options.force) {
const shouldProceed = await ui.confirm(
`Are you sure you want to stop all ${activeWorkflows.length} active workflows?`
);
if (!shouldProceed) {
console.log(chalk.gray('Operation cancelled'));
return;
}
}
// Stop all workflows
ui.displaySpinner('Stopping all workflows...');
let stopped = 0;
let failed = 0;
for (const workflow of activeWorkflows) {
try {
const workflowId = `workflow-${workflow.taskId}`;
await this.workflowManager!.stopTaskExecution(
workflowId,
options.force
);
stopped++;
} catch (error) {
console.error(
`${chalk.red('✗')} Failed to stop workflow ${workflow.taskId}: ${error}`
);
failed++;
}
}
console.log();
if (stopped > 0) {
ui.displaySuccess(`Successfully stopped ${stopped} workflows`);
}
if (failed > 0) {
ui.displayWarning(`Failed to stop ${failed} workflows`);
}
}
private getStatusDisplay(status: string): string {
const statusMap = {
pending: { icon: '⏳', color: chalk.yellow },
initializing: { icon: '🔄', color: chalk.blue },
running: { icon: '🚀', color: chalk.green },
paused: { icon: '⏸️', color: chalk.hex('#FFA500') },
completed: { icon: '✅', color: chalk.green },
failed: { icon: '❌', color: chalk.red },
cancelled: { icon: '🛑', color: chalk.gray },
timeout: { icon: '⏰', color: chalk.red }
};
const statusInfo = statusMap[status as keyof typeof statusMap] || {
icon: '❓',
color: chalk.white
};
return `${statusInfo.icon} ${statusInfo.color(status)}`;
}
async cleanup(): Promise<void> {
if (this.workflowManager) {
this.workflowManager.removeAllListeners();
}
}
static register(program: Command, name?: string): WorkflowStopCommand {
const command = new WorkflowStopCommand(name);
program.addCommand(command);
return command;
}
}

View File

@@ -5,13 +5,10 @@
// Commands
export { ListTasksCommand } from './commands/list.command.js';
export { ShowCommand } from './commands/show.command.js';
export { AuthCommand } from './commands/auth.command.js';
export { WorkflowCommand } from './commands/workflow.command.js';
export { ContextCommand } from './commands/context.command.js';
// Command registry
export { registerAllCommands } from './commands/index.js';
// UI utilities (for other commands to use)
export * as ui from './utils/ui.js';

View File

@@ -0,0 +1,567 @@
/**
* @fileoverview Dashboard components for Task Master CLI
* Displays project statistics and dependency information
*/
import chalk from 'chalk';
import boxen from 'boxen';
import type { Task, TaskPriority } from '@tm/core/types';
/**
* Statistics for task collection
*/
export interface TaskStatistics {
total: number;
done: number;
inProgress: number;
pending: number;
blocked: number;
deferred: number;
cancelled: number;
review?: number;
completionPercentage: number;
}
/**
* Statistics for dependencies
*/
export interface DependencyStatistics {
tasksWithNoDeps: number;
tasksReadyToWork: number;
tasksBlockedByDeps: number;
mostDependedOnTaskId?: number;
mostDependedOnCount?: number;
avgDependenciesPerTask: number;
}
/**
* Next task information
*/
export interface NextTaskInfo {
id: string | number;
title: string;
priority?: TaskPriority;
dependencies?: (string | number)[];
complexity?: number | string;
}
/**
* Status breakdown for progress bars
*/
export interface StatusBreakdown {
'in-progress'?: number;
pending?: number;
blocked?: number;
deferred?: number;
cancelled?: number;
review?: number;
}
/**
* Create a progress bar with color-coded status segments
*/
function createProgressBar(
completionPercentage: number,
width: number = 30,
statusBreakdown?: StatusBreakdown
): string {
// If no breakdown provided, use simple green bar
if (!statusBreakdown) {
const filled = Math.round((completionPercentage / 100) * width);
const empty = width - filled;
return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
}
// Build the bar with different colored sections
// Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
let bar = '';
let charsUsed = 0;
// 1. Green filled blocks for completed tasks (done)
const completedChars = Math.round((completionPercentage / 100) * width);
if (completedChars > 0) {
bar += chalk.green('█').repeat(completedChars);
charsUsed += completedChars;
}
// 2. Gray filled blocks for cancelled (won't be done)
if (statusBreakdown.cancelled && charsUsed < width) {
const cancelledChars = Math.round(
(statusBreakdown.cancelled / 100) * width
);
const actualChars = Math.min(cancelledChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.gray('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 3. Gray filled blocks for deferred (won't be done now)
if (statusBreakdown.deferred && charsUsed < width) {
const deferredChars = Math.round((statusBreakdown.deferred / 100) * width);
const actualChars = Math.min(deferredChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.gray('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 4. Blue filled blocks for in-progress (actively working)
if (statusBreakdown['in-progress'] && charsUsed < width) {
const inProgressChars = Math.round(
(statusBreakdown['in-progress'] / 100) * width
);
const actualChars = Math.min(inProgressChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.blue('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 5. Magenta empty blocks for review (almost done)
if (statusBreakdown.review && charsUsed < width) {
const reviewChars = Math.round((statusBreakdown.review / 100) * width);
const actualChars = Math.min(reviewChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.magenta('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// 6. Yellow empty blocks for pending (ready to start)
if (statusBreakdown.pending && charsUsed < width) {
const pendingChars = Math.round((statusBreakdown.pending / 100) * width);
const actualChars = Math.min(pendingChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.yellow('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// 7. Red empty blocks for blocked (can't start yet)
if (statusBreakdown.blocked && charsUsed < width) {
const blockedChars = Math.round((statusBreakdown.blocked / 100) * width);
const actualChars = Math.min(blockedChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.red('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// Fill any remaining space with gray empty yellow blocks
if (charsUsed < width) {
bar += chalk.yellow('░').repeat(width - charsUsed);
}
return bar;
}
/**
* Calculate task statistics from a list of tasks
*/
export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
const stats: TaskStatistics = {
total: tasks.length,
done: 0,
inProgress: 0,
pending: 0,
blocked: 0,
deferred: 0,
cancelled: 0,
review: 0,
completionPercentage: 0
};
tasks.forEach((task) => {
switch (task.status) {
case 'done':
stats.done++;
break;
case 'in-progress':
stats.inProgress++;
break;
case 'pending':
stats.pending++;
break;
case 'blocked':
stats.blocked++;
break;
case 'deferred':
stats.deferred++;
break;
case 'cancelled':
stats.cancelled++;
break;
case 'review':
stats.review = (stats.review || 0) + 1;
break;
}
});
stats.completionPercentage =
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
return stats;
}
/**
* Calculate subtask statistics from tasks
*/
export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
const stats: TaskStatistics = {
total: 0,
done: 0,
inProgress: 0,
pending: 0,
blocked: 0,
deferred: 0,
cancelled: 0,
review: 0,
completionPercentage: 0
};
tasks.forEach((task) => {
if (task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
stats.total++;
switch (subtask.status) {
case 'done':
stats.done++;
break;
case 'in-progress':
stats.inProgress++;
break;
case 'pending':
stats.pending++;
break;
case 'blocked':
stats.blocked++;
break;
case 'deferred':
stats.deferred++;
break;
case 'cancelled':
stats.cancelled++;
break;
case 'review':
stats.review = (stats.review || 0) + 1;
break;
}
});
}
});
stats.completionPercentage =
stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
return stats;
}
/**
* Calculate dependency statistics
*/
export function calculateDependencyStatistics(
tasks: Task[]
): DependencyStatistics {
const completedTaskIds = new Set(
tasks.filter((t) => t.status === 'done').map((t) => t.id)
);
const tasksWithNoDeps = tasks.filter(
(t) =>
t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0)
).length;
const tasksWithAllDepsSatisfied = tasks.filter(
(t) =>
t.status !== 'done' &&
t.dependencies &&
t.dependencies.length > 0 &&
t.dependencies.every((depId) => completedTaskIds.has(depId))
).length;
const tasksBlockedByDeps = tasks.filter(
(t) =>
t.status !== 'done' &&
t.dependencies &&
t.dependencies.length > 0 &&
!t.dependencies.every((depId) => completedTaskIds.has(depId))
).length;
// Calculate most depended-on task
const dependencyCount: Record<string, number> = {};
tasks.forEach((task) => {
if (task.dependencies && task.dependencies.length > 0) {
task.dependencies.forEach((depId) => {
const key = String(depId);
dependencyCount[key] = (dependencyCount[key] || 0) + 1;
});
}
});
let mostDependedOnTaskId: number | undefined;
let mostDependedOnCount = 0;
for (const [taskId, count] of Object.entries(dependencyCount)) {
if (count > mostDependedOnCount) {
mostDependedOnCount = count;
mostDependedOnTaskId = parseInt(taskId);
}
}
// Calculate average dependencies
const totalDependencies = tasks.reduce(
(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
0
);
const avgDependenciesPerTask =
tasks.length > 0 ? totalDependencies / tasks.length : 0;
return {
tasksWithNoDeps,
tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied,
tasksBlockedByDeps,
mostDependedOnTaskId,
mostDependedOnCount,
avgDependenciesPerTask
};
}
/**
* Get priority counts
*/
export function getPriorityBreakdown(
tasks: Task[]
): Record<TaskPriority, number> {
const breakdown: Record<TaskPriority, number> = {
critical: 0,
high: 0,
medium: 0,
low: 0
};
tasks.forEach((task) => {
const priority = task.priority || 'medium';
breakdown[priority]++;
});
return breakdown;
}
/**
* Calculate status breakdown as percentages
*/
function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
if (stats.total === 0) return {};
return {
'in-progress': (stats.inProgress / stats.total) * 100,
pending: (stats.pending / stats.total) * 100,
blocked: (stats.blocked / stats.total) * 100,
deferred: (stats.deferred / stats.total) * 100,
cancelled: (stats.cancelled / stats.total) * 100,
review: ((stats.review || 0) / stats.total) * 100
};
}
/**
* Format status counts in the correct order with colors
* @param stats - The statistics object containing counts
* @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label)
*/
function formatStatusLine(
stats: TaskStatistics,
isSubtask: boolean = false
): string {
const parts: string[] = [];
// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
if (isSubtask) {
parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
} else {
parts.push(`Done: ${chalk.green(stats.done)}`);
}
parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`);
parts.push(`Deferred: ${chalk.gray(stats.deferred)}`);
// Add line break for second row
const firstLine = parts.join(' ');
parts.length = 0;
parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`);
parts.push(`Review: ${chalk.magenta(stats.review || 0)}`);
parts.push(`Pending: ${chalk.yellow(stats.pending)}`);
parts.push(`Blocked: ${chalk.red(stats.blocked)}`);
const secondLine = parts.join(' ');
return firstLine + '\n' + secondLine;
}
/**
* Display the project dashboard box
*/
export function displayProjectDashboard(
taskStats: TaskStatistics,
subtaskStats: TaskStatistics,
priorityBreakdown: Record<TaskPriority, number>
): string {
// Calculate status breakdowns using the helper function
const taskStatusBreakdown = calculateStatusBreakdown(taskStats);
const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats);
// Create progress bars with the breakdowns
const taskProgressBar = createProgressBar(
taskStats.completionPercentage,
30,
taskStatusBreakdown
);
const subtaskProgressBar = createProgressBar(
subtaskStats.completionPercentage,
30,
subtaskStatusBreakdown
);
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
const content =
chalk.white.bold('Project Dashboard') +
'\n' +
`Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` +
formatStatusLine(taskStats, false) +
'\n\n' +
`Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` +
formatStatusLine(subtaskStats, true) +
'\n\n' +
chalk.cyan.bold('Priority Breakdown:') +
'\n' +
`${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` +
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` +
`${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`;
return content;
}
/**
* Display the dependency dashboard box
*/
export function displayDependencyDashboard(
depStats: DependencyStatistics,
nextTask?: NextTaskInfo
): string {
const content =
chalk.white.bold('Dependency Status & Next Task') +
'\n' +
chalk.cyan.bold('Dependency Metrics:') +
'\n' +
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` +
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` +
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` +
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${
depStats.mostDependedOnTaskId
? chalk.cyan(
`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`
)
: chalk.gray('None')
}\n` +
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` +
chalk.cyan.bold('Next Task to Work On:') +
'\n' +
`ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${
nextTask
? chalk.white.bold(nextTask.title)
: chalk.yellow('No task available')
}\n` +
`Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${
nextTask?.dependencies?.length
? chalk.cyan(nextTask.dependencies.join(', '))
: chalk.gray('None')
}\n` +
`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
return content;
}
/**
* Display dashboard boxes side by side or stacked
*/
export function displayDashboards(
taskStats: TaskStatistics,
subtaskStats: TaskStatistics,
priorityBreakdown: Record<TaskPriority, number>,
depStats: DependencyStatistics,
nextTask?: NextTaskInfo
): void {
const projectDashboardContent = displayProjectDashboard(
taskStats,
subtaskStats,
priorityBreakdown
);
const dependencyDashboardContent = displayDependencyDashboard(
depStats,
nextTask
);
// Get terminal width
const terminalWidth = process.stdout.columns || 80;
const minDashboardWidth = 50;
const minDependencyWidth = 50;
const totalMinWidth = minDashboardWidth + minDependencyWidth + 4;
// If terminal is wide enough, show side by side
if (terminalWidth >= totalMinWidth) {
const halfWidth = Math.floor(terminalWidth / 2);
const boxContentWidth = halfWidth - 4;
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
width: boxContentWidth,
dimBorder: false
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: 'magenta',
borderStyle: 'round',
width: boxContentWidth,
dimBorder: false
});
// Create side-by-side layout
const dashboardLines = dashboardBox.split('\n');
const dependencyLines = dependencyBox.split('\n');
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
const combinedLines = [];
for (let i = 0; i < maxHeight; i++) {
const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
const paddedDashLine = dashLine.padEnd(halfWidth, ' ');
combinedLines.push(paddedDashLine + depLine);
}
console.log(combinedLines.join('\n'));
} else {
// Show stacked vertically
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
});
console.log(dashboardBox);
console.log(dependencyBox);
}
}

View File

@@ -0,0 +1,47 @@
/**
* @fileoverview Task Master header component
* Displays the banner, version, project info, and file path
*/
import chalk from 'chalk';
import figlet from 'figlet';
import gradient from 'gradient-string';
/**
* Header configuration options
*/
export interface HeaderOptions {
title?: string;
tag?: string;
filePath?: string;
}
/**
* Display the Task Master header with project info
*/
export function displayHeader(options: HeaderOptions = {}): void {
const { filePath, tag } = options;
// Display tag and file path info
if (tag) {
let tagInfo = '';
if (tag && tag !== 'master') {
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
} else {
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
}
console.log(tagInfo);
if (filePath) {
// Convert to absolute path if it's relative
const absolutePath = filePath.startsWith('/')
? filePath
: `${process.cwd()}/${filePath}`;
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
}
console.log(); // Empty line for spacing
}
}

View File

@@ -0,0 +1,9 @@
/**
* @fileoverview UI components exports
*/
export * from './header.component.js';
export * from './dashboard.component.js';
export * from './next-task.component.js';
export * from './suggested-steps.component.js';
export * from './task-detail.component.js';

View File

@@ -0,0 +1,134 @@
/**
* @fileoverview Next task recommendation component
* Displays detailed information about the recommended next task
*/
import chalk from 'chalk';
import boxen from 'boxen';
import type { Task } from '@tm/core/types';
/**
* Next task display options
*/
export interface NextTaskDisplayOptions {
id: string | number;
title: string;
priority?: string;
status?: string;
dependencies?: (string | number)[];
description?: string;
}
/**
* Display the recommended next task section
*/
export function displayRecommendedNextTask(
task: NextTaskDisplayOptions | undefined
): void {
if (!task) {
// If no task available, show a message
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'
}
)
);
return;
}
// Build the content for the next task box
const content = [];
// Task header with ID and title
content.push(
`🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}`
);
content.push('');
// Priority and Status line
const statusLine = [];
if (task.priority) {
const priorityColor =
task.priority === 'high'
? chalk.red
: task.priority === 'medium'
? chalk.yellow
: chalk.gray;
statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`);
}
if (task.status) {
const statusDisplay =
task.status === 'pending'
? chalk.yellow('○ pending')
: task.status === 'in-progress'
? chalk.blue('▶ in-progress')
: chalk.gray(task.status);
statusLine.push(`Status: ${statusDisplay}`);
}
content.push(statusLine.join(' '));
// Dependencies
const depsDisplay =
!task.dependencies || task.dependencies.length === 0
? chalk.gray('None')
: chalk.cyan(task.dependencies.join(', '));
content.push(`Dependencies: ${depsDisplay}`);
// Description if available
if (task.description) {
content.push('');
content.push(`Description: ${chalk.white(task.description)}`);
}
// Action commands
content.push('');
content.push(
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}`
);
content.push(
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}`
);
// Display in a styled box with orange border
console.log(
boxen(content.join('\n'), {
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: 'round',
borderColor: '#FFA500', // Orange color
title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'),
titleAlignment: 'center',
width: process.stdout.columns * 0.97,
fullscreen: false
})
);
}
/**
* Get task description from the full task object
*/
export function getTaskDescription(task: Task): string | undefined {
// Try to get description from the task
// This could be from task.description or the first line of task.details
if ('description' in task && task.description) {
return task.description as string;
}
if ('details' in task && task.details) {
// Take first sentence or line from details
const details = task.details as string;
const firstLine = details.split('\n')[0];
const firstSentence = firstLine.split('.')[0];
return firstSentence;
}
return undefined;
}

View File

@@ -0,0 +1,31 @@
/**
* @fileoverview Suggested next steps component
* Displays helpful command suggestions at the end of the list
*/
import chalk from 'chalk';
import boxen from 'boxen';
/**
* Display suggested next steps section
*/
export function displaySuggestedNextSteps(): void {
const steps = [
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next`,
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`
];
console.log(
boxen(
chalk.white.bold('Suggested Next Steps:') + '\n\n' + steps.join('\n'),
{
padding: 1,
margin: { top: 0, bottom: 1 },
borderStyle: 'round',
borderColor: 'gray',
width: process.stdout.columns * 0.97
}
)
);
}

View File

@@ -0,0 +1,264 @@
/**
* @fileoverview Task detail component for show command
* Displays detailed task information in a structured format
*/
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { marked, MarkedExtension } from 'marked';
import { markedTerminal } from 'marked-terminal';
import type { Task } from '@tm/core/types';
import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js';
// Configure marked to use terminal renderer with subtle colors
marked.use(
markedTerminal({
// More subtle colors that match the overall design
code: (code: string) => {
// Custom code block handler to preserve formatting
return code
.split('\n')
.map((line) => ' ' + chalk.cyan(line))
.join('\n');
},
blockquote: chalk.gray.italic,
html: chalk.gray,
heading: chalk.white.bold, // White bold for headings
hr: chalk.gray,
listitem: chalk.white, // White for list items
paragraph: chalk.white, // White for paragraphs (default text color)
strong: chalk.white.bold, // White bold for strong text
em: chalk.white.italic, // White italic for emphasis
codespan: chalk.cyan, // Cyan for inline code (no background)
del: chalk.dim.strikethrough,
link: chalk.blue,
href: chalk.blue.underline,
// Add more explicit code block handling
showSectionPrefix: false,
unescape: true,
emoji: false,
// Try to preserve whitespace in code blocks
tab: 4,
width: 120
}) as MarkedExtension
);
// Also set marked options to preserve whitespace
marked.setOptions({
breaks: true,
gfm: true
});
/**
* Display the task header with tag
*/
export function displayTaskHeader(
taskId: string | number,
title: string
): void {
// Display task header box
console.log(
boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'blue',
borderStyle: 'round'
})
);
}
/**
* Display task properties in a table format
*/
export function displayTaskProperties(task: Task): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Create table for task properties - simple 2-column layout
const table = new Table({
head: [],
style: {
head: [],
border: ['grey']
},
colWidths: [
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.8)
],
wordWrap: true
});
const deps =
task.dependencies && task.dependencies.length > 0
? task.dependencies.map((d) => String(d)).join(', ')
: 'None';
// Build the left column (labels) and right column (values)
const labels = [
chalk.cyan('ID:'),
chalk.cyan('Title:'),
chalk.cyan('Status:'),
chalk.cyan('Priority:'),
chalk.cyan('Dependencies:'),
chalk.cyan('Complexity:'),
chalk.cyan('Description:')
].join('\n');
const values = [
String(task.id),
task.title,
getStatusWithColor(task.status),
getPriorityWithColor(task.priority),
deps,
'N/A',
task.description || ''
].join('\n');
table.push([labels, values]);
console.log(table.toString());
}
/**
* Display implementation details in a box
*/
export function displayImplementationDetails(details: string): void {
// Handle all escaped characters properly
const cleanDetails = details
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\t/g, '\t') // Convert \t to actual tabs
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Parse markdown to terminal-friendly format
const markdownResult = marked(cleanDetails);
const formattedDetails =
typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
console.log(
boxen(
chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
{
padding: 1,
borderStyle: 'round',
borderColor: 'cyan', // Changed to cyan to match the original
width: terminalWidth // Fixed width to match the original
}
)
);
}
/**
* Display test strategy in a box
*/
export function displayTestStrategy(testStrategy: string): void {
// Handle all escaped characters properly (same as implementation details)
const cleanStrategy = testStrategy
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\t/g, '\t') // Convert \t to actual tabs
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Parse markdown to terminal-friendly format (same as implementation details)
const markdownResult = marked(cleanStrategy);
const formattedStrategy =
typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
console.log(
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
padding: 1,
borderStyle: 'round',
borderColor: 'cyan', // Changed to cyan to match implementation details
width: terminalWidth
})
);
}
/**
* Display subtasks in a table format
*/
export function displaySubtasks(
subtasks: Array<{
id: string | number;
title: string;
status: any;
description?: string;
dependencies?: string[];
}>,
parentId: string | number
): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Display subtasks header
console.log(
boxen(chalk.magenta.bold('Subtasks'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 1, bottom: 0 }
})
);
// Create subtasks table
const table = new Table({
head: [
chalk.magenta.bold('ID'),
chalk.magenta.bold('Status'),
chalk.magenta.bold('Title'),
chalk.magenta.bold('Deps')
],
style: {
head: [],
border: ['grey']
},
colWidths: [
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.6),
Math.floor(terminalWidth * 0.15)
],
wordWrap: true
});
subtasks.forEach((subtask) => {
const subtaskId = `${parentId}.${subtask.id}`;
// Format dependencies
const deps =
subtask.dependencies && subtask.dependencies.length > 0
? subtask.dependencies.join(', ')
: 'None';
table.push([
subtaskId,
getStatusWithColor(subtask.status),
subtask.title,
deps
]);
});
console.log(table.toString());
}
/**
* Display suggested actions
*/
export function displaySuggestedActions(taskId: string | number): void {
console.log(
boxen(
chalk.white.bold('Suggested Actions:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
{
padding: 1,
margin: { top: 1 },
borderStyle: 'round',
borderColor: 'green',
width: process.stdout.columns * 0.95 || 100
}
)
);
}

9
apps/cli/src/ui/index.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* @fileoverview Main UI exports
*/
// Export all components
export * from './components/index.js';
// Re-export existing UI utilities
export * from '../utils/ui.js';

View File

@@ -18,19 +18,39 @@ export function getStatusWithColor(
const statusConfig = {
done: {
color: chalk.green,
icon: String.fromCharCode(8730),
tableIcon: String.fromCharCode(8730)
}, // √
pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' },
icon: '✓',
tableIcon: '✓'
},
pending: {
color: chalk.yellow,
icon: '○',
tableIcon: '○'
},
'in-progress': {
color: chalk.hex('#FFA500'),
icon: String.fromCharCode(9654),
tableIcon: '>'
}, // ▶
deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' },
blocked: { color: chalk.red, icon: '!', tableIcon: '!' },
review: { color: chalk.magenta, icon: '?', tableIcon: '?' },
cancelled: { color: chalk.gray, icon: 'X', tableIcon: 'X' }
icon: '▶',
tableIcon: ''
},
deferred: {
color: chalk.gray,
icon: 'x',
tableIcon: 'x'
},
review: {
color: chalk.magenta,
icon: '?',
tableIcon: '?'
},
cancelled: {
color: chalk.gray,
icon: 'x',
tableIcon: 'x'
},
blocked: {
color: chalk.red,
icon: '!',
tableIcon: '!'
}
};
const config = statusConfig[status] || {
@@ -39,18 +59,7 @@ export function getStatusWithColor(
tableIcon: 'X'
};
// Use simple ASCII characters for stable display
const simpleIcons = {
done: String.fromCharCode(8730), // √
pending: 'o',
'in-progress': '>',
deferred: 'x',
blocked: '!',
review: '?',
cancelled: 'X'
};
const icon = forTable ? simpleIcons[status] || 'X' : config.icon;
const icon = forTable ? config.tableIcon : config.icon;
return config.color(`${icon} ${status}`);
}
@@ -245,10 +254,24 @@ export function createTaskTable(
} = options || {};
// Calculate dynamic column widths based on terminal width
const terminalWidth = process.stdout.columns || 100;
const terminalWidth = process.stdout.columns * 0.9 || 100;
// Adjust column widths to better match the original layout
const baseColWidths = showComplexity
? [8, Math.floor(terminalWidth * 0.35), 18, 12, 15, 12] // ID, Title, Status, Priority, Dependencies, Complexity
: [8, Math.floor(terminalWidth * 0.4), 18, 12, 20]; // ID, Title, Status, Priority, Dependencies
? [
Math.floor(terminalWidth * 0.06),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.12)
] // ID, Title, Status, Priority, Dependencies, Complexity
: [
Math.floor(terminalWidth * 0.08),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.18),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2)
]; // ID, Title, Status, Priority, Dependencies
const headers = [
chalk.blue.bold('ID'),
@@ -284,11 +307,19 @@ export function createTaskTable(
];
if (showDependencies) {
row.push(formatDependenciesWithStatus(task.dependencies, tasks));
// For table display, show simple format without status icons
if (!task.dependencies || task.dependencies.length === 0) {
row.push(chalk.gray('None'));
} else {
row.push(
chalk.cyan(task.dependencies.map((d) => String(d)).join(', '))
);
}
}
if (showComplexity && 'complexity' in task) {
row.push(getComplexityWithColor(task.complexity as number | string));
if (showComplexity) {
// Show N/A if no complexity score
row.push(chalk.gray('N/A'));
}
table.push(row);
@@ -324,61 +355,3 @@ export function createTaskTable(
return table.toString();
}
/**
* Display a spinner with message (mock implementation)
*/
export function displaySpinner(message: string): void {
console.log(chalk.blue('◐'), chalk.gray(message));
}
/**
* Simple confirmation prompt
*/
export async function confirm(message: string): Promise<boolean> {
// For now, return true. In a real implementation, use inquirer
console.log(chalk.yellow('?'), chalk.white(message), chalk.gray('(y/n)'));
// Mock implementation - in production this would use inquirer
return new Promise((resolve) => {
process.stdin.once('data', (data) => {
const answer = data.toString().trim().toLowerCase();
resolve(answer === 'y' || answer === 'yes');
});
process.stdin.resume();
});
}
/**
* Create a generic table
*/
export function createTable(headers: string[], rows: string[][]): string {
const table = new Table({
head: headers.map(h => chalk.blue.bold(h)),
style: {
head: [],
border: ['gray']
},
chars: {
'top': '─',
'top-mid': '┬',
'top-left': '┌',
'top-right': '┐',
'bottom': '─',
'bottom-mid': '┴',
'bottom-left': '└',
'bottom-right': '┘',
'left': '│',
'left-mid': '├',
'mid': '─',
'mid-mid': '┼',
'right': '│',
'right-mid': '┤',
'middle': '│'
}
});
rows.forEach(row => table.push(row));
return table.toString();
}

View File

@@ -1,8 +0,0 @@
import { defineConfig } from 'tsup';
import { cliConfig, mergeConfig } from '@tm/build-config';
export default defineConfig(
mergeConfig(cliConfig, {
entry: ['src/index.ts']
})
);

View File

@@ -17,18 +17,37 @@ sidebarTitle: "CLI Commands"
<Accordion title="List Tasks">
```bash
# List all tasks
# List all tasks with enhanced dashboard view
task-master list
# Alias available
task-master ls
# List tasks with a specific status
# List tasks with a specific status (comma-separated multiple statuses supported)
task-master list --status=<status>
task-master list --status=pending,in-progress
# List tasks with subtasks
task-master list --with-subtasks
# List tasks with a specific status and include subtasks
task-master list --status=<status> --with-subtasks
# Filter by tag
task-master list --tag=<tag>
# Output formats: text (default), json, compact
task-master list --format=json
task-master list --format=compact
# Suppress output for programmatic use
task-master list --silent
# Specify project directory
task-master list --project=/path/to/project
```
The `list` command now displays:
- **Project dashboard** with task statistics and progress bars
- **Next recommended task** based on dependencies and priority
- **Suggested next steps** for project workflow
- **Enhanced table view** with complexity scores and color-coded status
</Accordion>
<Accordion title="Show Next Task">
@@ -45,9 +64,32 @@ sidebarTitle: "CLI Commands"
# or
task-master show --id=<id>
# View multiple tasks at once (comma-separated)
task-master show 1,2,3
task-master show --id=1,2,3
# View a specific subtask (e.g., subtask 2 of task 1)
task-master show 1.2
# Filter subtasks by status
task-master show <id> --status=<status>
# Output formats: text (default), json
task-master show <id> --format=json
# Suppress output for programmatic use
task-master show <id> --silent
# Specify project directory
task-master show <id> --project=/path/to/project
```
The `show` command now features:
- **Enhanced task details** with markdown rendering for descriptions
- **Structured information display** with tables and sections
- **Multiple task support** for viewing several tasks simultaneously
- **Subtask filtering** by status
- **Suggested actions** for next steps on the task
</Accordion>
<Accordion title="Update Tasks">

View File

@@ -31,6 +31,12 @@ To see all tasks in the CLI you can use:
task-master list
```
The `list` command now includes a comprehensive dashboard showing:
- Project statistics with completion progress bars
- Next recommended task based on dependencies and priority
- Suggested workflow steps
- Enhanced task table with complexity indicators
To see all implementation details of an individual task, including subtasks and testing strategy, you can use Show Task:
```

View File

@@ -5,10 +5,9 @@
"description": "Task Master documentation powered by Mintlify",
"scripts": {
"dev": "mintlify dev",
"build": "mintlify build",
"preview": "mintlify preview"
},
"devDependencies": {
"mintlify": "^4.0.0"
"mintlify": "^4.2.111"
}
}

View File

@@ -103,8 +103,8 @@ async function main() {
// This prevents the multiple React instances issue
// Ensure React is resolved from the workspace root to avoid duplicates
alias: {
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
react: path.resolve(__dirname, '../../node_modules/react'),
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',
@@ -135,8 +135,8 @@ async function main() {
jsxImportSource: 'react',
external: ['*.css'],
alias: {
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
react: path.resolve(__dirname, '../../node_modules/react'),
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',

View File

@@ -229,6 +229,7 @@
"build": "npm run build:js && npm run build:css",
"build:js": "node ./esbuild.js --production",
"build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
"dev": "npm run watch",
"package": "npm exec node ./package.mjs",
"package:direct": "node ./package.mjs",
"debug:env": "node ./debug-env.mjs",

View File

@@ -1,9 +1,10 @@
import type React from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { Loader2, Play } from 'lucide-react';
import { PriorityBadge } from './PriorityBadge';
import type { TaskMasterTask } from '../../webview/types';
import { useVSCodeContext } from '../../webview/contexts/VSCodeContext';
interface TaskMetadataSidebarProps {
currentTask: TaskMasterTask;
@@ -28,10 +29,12 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
isRegenerating = false,
isAppending = false
}) => {
const { vscode } = useVSCodeContext();
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
const [mcpComplexityScore, setMcpComplexityScore] = useState<
number | undefined
>(undefined);
const [isStartingTask, setIsStartingTask] = useState(false);
// Get complexity score from task
const currentComplexityScore = complexity?.score;
@@ -97,6 +100,29 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
}
};
// Handle starting a task
const handleStartTask = () => {
if (!currentTask || isStartingTask) {
return;
}
setIsStartingTask(true);
// Send message to extension to open terminal
if (vscode) {
vscode.postMessage({
type: 'openTerminal',
taskId: currentTask.id,
taskTitle: currentTask.title
});
}
// Reset loading state after a short delay
setTimeout(() => {
setIsStartingTask(false);
}, 500);
};
// Effect to handle complexity on task change
useEffect(() => {
if (currentTask?.id) {
@@ -284,6 +310,30 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div className="border-b border-textSeparator-foreground" />
)}
{/* Start Task Button */}
<div className="mt-4">
<Button
onClick={handleStartTask}
variant="default"
size="sm"
className="w-full text-xs"
disabled={
isRegenerating ||
isAppending ||
isStartingTask ||
currentTask?.status === 'done' ||
currentTask?.status === 'in-progress'
}
>
{isStartingTask ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Play className="w-4 h-4 mr-2" />
)}
{isStartingTask ? 'Starting...' : 'Start Task'}
</Button>
</div>
</div>
</div>
</div>

View File

@@ -361,6 +361,30 @@ export class WebviewManager {
}
return;
case 'openTerminal':
// Open VS Code terminal for task execution
this.logger.log(
`Opening terminal for task ${data.taskId}: ${data.taskTitle}`
);
try {
const terminal = vscode.window.createTerminal({
name: `Task ${data.taskId}: ${data.taskTitle}`,
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
});
terminal.show();
this.logger.log('Terminal created and shown successfully');
response = { success: true };
} catch (error) {
this.logger.error('Failed to create terminal:', error);
response = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
break;
default:
throw new Error(`Unknown message type: ${type}`);
}

View File

@@ -20,357 +20,8 @@
* Main entry point for globally installed package
*/
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { createRequire } from 'module';
import { spawn } from 'child_process';
import { Command } from 'commander';
import { displayHelp, displayBanner } from '../scripts/modules/ui.js';
import { registerCommands } from '../scripts/modules/commands.js';
import { detectCamelCaseFlags } from '../scripts/modules/utils.js';
import chalk from 'chalk';
// Direct imports instead of spawning child processes
import { runCLI } from '../scripts/modules/commands.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
// Get package information
const packageJson = require('../package.json');
const version = packageJson.version;
// Get paths to script files
const devScriptPath = resolve(__dirname, '../scripts/dev.js');
const initScriptPath = resolve(__dirname, '../scripts/init.js');
// Helper function to run dev.js with arguments
function runDevScript(args) {
// Debug: Show the transformed arguments when DEBUG=1 is set
if (process.env.DEBUG === '1') {
console.error('\nDEBUG - CLI Wrapper Analysis:');
console.error('- Original command: ' + process.argv.join(' '));
console.error('- Transformed args: ' + args.join(' '));
console.error(
'- dev.js will receive: node ' +
devScriptPath +
' ' +
args.join(' ') +
'\n'
);
}
// For testing: If TEST_MODE is set, just print args and exit
if (process.env.TEST_MODE === '1') {
console.log('Would execute:');
console.log(`node ${devScriptPath} ${args.join(' ')}`);
process.exit(0);
return;
}
const child = spawn('node', [devScriptPath, ...args], {
stdio: 'inherit',
cwd: process.cwd()
});
child.on('close', (code) => {
process.exit(code);
});
}
// Helper function to detect camelCase and convert to kebab-case
const toKebabCase = (str) => str.replace(/([A-Z])/g, '-$1').toLowerCase();
/**
* Create a wrapper action that passes the command to dev.js
* @param {string} commandName - The name of the command
* @returns {Function} Wrapper action function
*/
function createDevScriptAction(commandName) {
return (options, cmd) => {
// Check for camelCase flags and error out with helpful message
const camelCaseFlags = detectCamelCaseFlags(process.argv);
// If camelCase flags were found, show error and exit
if (camelCaseFlags.length > 0) {
console.error('\nError: Please use kebab-case for CLI flags:');
camelCaseFlags.forEach((flag) => {
console.error(` Instead of: --${flag.original}`);
console.error(` Use: --${flag.kebabCase}`);
});
console.error(
'\nExample: task-master parse-prd --num-tasks=5 instead of --numTasks=5\n'
);
process.exit(1);
}
// Since we've ensured no camelCase flags, we can now just:
// 1. Start with the command name
const args = [commandName];
// 3. Get positional arguments and explicit flags from the command line
const commandArgs = [];
const positionals = new Set(); // Track positional args we've seen
// Find the command in raw process.argv to extract args
const commandIndex = process.argv.indexOf(commandName);
if (commandIndex !== -1) {
// Process all args after the command name
for (let i = commandIndex + 1; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg.startsWith('--')) {
// It's a flag - pass through as is
commandArgs.push(arg);
// Skip the next arg if this is a flag with a value (not --flag=value format)
if (
!arg.includes('=') &&
i + 1 < process.argv.length &&
!process.argv[i + 1].startsWith('--')
) {
commandArgs.push(process.argv[++i]);
}
} else if (!positionals.has(arg)) {
// It's a positional argument we haven't seen
commandArgs.push(arg);
positionals.add(arg);
}
}
}
// Add all command line args we collected
args.push(...commandArgs);
// 4. Add default options from Commander if not specified on command line
// Track which options we've seen on the command line
const userOptions = new Set();
for (const arg of commandArgs) {
if (arg.startsWith('--')) {
// Extract option name (without -- and value)
const name = arg.split('=')[0].slice(2);
userOptions.add(name);
// Add the kebab-case version too, to prevent duplicates
const kebabName = name.replace(/([A-Z])/g, '-$1').toLowerCase();
userOptions.add(kebabName);
// Add the camelCase version as well
const camelName = kebabName.replace(/-([a-z])/g, (_, letter) =>
letter.toUpperCase()
);
userOptions.add(camelName);
}
}
// Add Commander-provided defaults for options not specified by user
Object.entries(options).forEach(([key, value]) => {
// Debug output to see what keys we're getting
if (process.env.DEBUG === '1') {
console.error(`DEBUG - Processing option: ${key} = ${value}`);
}
// Special case for numTasks > num-tasks (a known problem case)
if (key === 'numTasks') {
if (process.env.DEBUG === '1') {
console.error('DEBUG - Converting numTasks to num-tasks');
}
if (!userOptions.has('num-tasks') && !userOptions.has('numTasks')) {
args.push(`--num-tasks=${value}`);
}
return;
}
// Skip built-in Commander properties and options the user provided
if (
['parent', 'commands', 'options', 'rawArgs'].includes(key) ||
userOptions.has(key)
) {
return;
}
// Also check the kebab-case version of this key
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
if (userOptions.has(kebabKey)) {
return;
}
// Add default values, using kebab-case for the parameter name
if (value !== undefined) {
if (typeof value === 'boolean') {
if (value === true) {
args.push(`--${kebabKey}`);
} else if (value === false && key === 'generate') {
args.push('--skip-generate');
}
} else {
// Always use kebab-case for option names
args.push(`--${kebabKey}=${value}`);
}
}
});
// Special handling for parent parameter (uses -p)
if (options.parent && !args.includes('-p') && !userOptions.has('parent')) {
args.push('-p', options.parent);
}
// Debug output for troubleshooting
if (process.env.DEBUG === '1') {
console.error('DEBUG - Command args:', commandArgs);
console.error('DEBUG - User options:', Array.from(userOptions));
console.error('DEBUG - Commander options:', options);
console.error('DEBUG - Final args:', args);
}
// Run the script with our processed args
runDevScript(args);
};
}
// // Special case for the 'init' command which uses a different script
// function registerInitCommand(program) {
// program
// .command('init')
// .description('Initialize a new project')
// .option('-y, --yes', 'Skip prompts and use default values')
// .option('-n, --name <name>', 'Project name')
// .option('-d, --description <description>', 'Project description')
// .option('-v, --version <version>', 'Project version')
// .option('-a, --author <author>', 'Author name')
// .option('--skip-install', 'Skip installing dependencies')
// .option('--dry-run', 'Show what would be done without making changes')
// .action((options) => {
// // Pass through any options to the init script
// const args = [
// '--yes',
// 'name',
// 'description',
// 'version',
// 'author',
// 'skip-install',
// 'dry-run'
// ]
// .filter((opt) => options[opt])
// .map((opt) => {
// if (opt === 'yes' || opt === 'skip-install' || opt === 'dry-run') {
// return `--${opt}`;
// }
// return `--${opt}=${options[opt]}`;
// });
// const child = spawn('node', [initScriptPath, ...args], {
// stdio: 'inherit',
// cwd: process.cwd()
// });
// child.on('close', (code) => {
// process.exit(code);
// });
// });
// }
// Set up the command-line interface
const program = new Command();
program
.name('task-master')
.description('Claude Task Master CLI')
.version(version)
.addHelpText('afterAll', () => {
// Use the same help display function as dev.js for consistency
displayHelp();
return ''; // Return empty string to prevent commander's default help
});
// Add custom help option to directly call our help display
program.helpOption('-h, --help', 'Display help information');
program.on('--help', () => {
displayHelp();
});
// // Add special case commands
// registerInitCommand(program);
program
.command('dev')
.description('Run the dev.js script')
.action(() => {
const args = process.argv.slice(process.argv.indexOf('dev') + 1);
runDevScript(args);
});
// Use a temporary Command instance to get all command definitions
const tempProgram = new Command();
registerCommands(tempProgram);
// For each command in the temp instance, add a modified version to our actual program
tempProgram.commands.forEach((cmd) => {
if (['dev'].includes(cmd.name())) {
// Skip commands we've already defined specially
return;
}
// Create a new command with the same name and description
const newCmd = program.command(cmd.name()).description(cmd.description());
// Copy all options
cmd.options.forEach((opt) => {
newCmd.option(opt.flags, opt.description, opt.defaultValue);
});
// Set the action to proxy to dev.js
newCmd.action(createDevScriptAction(cmd.name()));
});
// Parse the command line arguments
program.parse(process.argv);
// Add global error handling for unknown commands and options
process.on('uncaughtException', (err) => {
// Check if this is a commander.js unknown option error
if (err.code === 'commander.unknownOption') {
const option = err.message.match(/'([^']+)'/)?.[1];
const commandArg = process.argv.find(
(arg) =>
!arg.startsWith('-') &&
arg !== 'task-master' &&
!arg.includes('/') &&
arg !== 'node'
);
const command = commandArg || 'unknown';
console.error(chalk.red(`Error: Unknown option '${option}'`));
console.error(
chalk.yellow(
`Run 'task-master ${command} --help' to see available options for this command`
)
);
process.exit(1);
}
// Check if this is a commander.js unknown command error
if (err.code === 'commander.unknownCommand') {
const command = err.message.match(/'([^']+)'/)?.[1];
console.error(chalk.red(`Error: Unknown command '${command}'`));
console.error(
chalk.yellow(`Run 'task-master --help' to see available commands`)
);
process.exit(1);
}
// Handle other uncaught exceptions
console.error(chalk.red(`Error: ${err.message}`));
if (process.env.DEBUG === '1') {
console.error(err);
}
process.exit(1);
});
// Show help if no command was provided (just 'task-master' with no args)
if (process.argv.length <= 2) {
displayBanner();
displayHelp();
process.exit(0);
}
// Add exports at the end of the file
export { detectCamelCaseFlags };
// Simply run the CLI directly
runCLI();

69
output.txt Normal file

File diff suppressed because one or more lines are too long

3336
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,19 +11,20 @@
},
"workspaces": ["apps/*", "packages/*", "."],
"scripts": {
"build": "npm run build:packages && tsup",
"dev": "npm run build:packages && npm link && (npm run dev:packages & tsup --watch --onSuccess 'echo Build complete && npm link')",
"dev:packages": "(cd packages/tm-core && npm run dev) & (cd packages/workflow-engine && npm run dev) & (cd apps/cli && npm run dev) & wait",
"dev:core": "cd packages/tm-core && npm run dev",
"dev:workflow": "cd packages/workflow-engine && npm run dev",
"dev:cli": "cd apps/cli && npm run dev",
"build:packages": "npm run build:core && npm run build:workflow && npm run build:cli",
"build:core": "cd packages/tm-core && npm run build",
"build:workflow": "cd packages/workflow-engine && npm run build",
"build:cli": "cd apps/cli && npm run build",
"typecheck": "npm run typecheck:core && npm run typecheck:cli",
"typecheck:core": "cd packages/tm-core && npm run typecheck",
"typecheck:cli": "cd apps/cli && npm run typecheck",
"build": "npm run build:build-config && tsup",
"dev": "tsup --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'",
"turbo:dev": "turbo dev",
"turbo:build": "turbo build",
"turbo:typecheck": "turbo typecheck",
"dev:main": "tsup --watch --onSuccess 'echo \"📦 Main package built\" && npm link'",
"dev:legacy": "npm run build:build-config && concurrently -n \"core,cli,main\" -c \"blue,green,yellow\" \"npm run dev:core\" \"npm run dev:cli\" \"npm run dev:main\"",
"dev:core": "npm run dev -w @tm/core",
"dev:cli": "npm run dev -w @tm/cli",
"build:packages": "turbo build --filter='./packages/*' --filter='./apps/*'",
"build:packages:parallel": "turbo build --filter='./packages/*' --filter='./apps/*'",
"build:build-config": "npm run build -w @tm/build-config",
"build:core": "npm run build -w @tm/core",
"build:cli": "npm run build -w @tm/cli",
"test": "node --experimental-vm-modules node_modules/.bin/jest",
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
"test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration",
@@ -36,6 +37,7 @@
"postpack": "chmod +x dist/task-master.js dist/mcp-server.js",
"changeset": "changeset",
"release": "changeset publish",
"publish-packages": "turbo run build lint test && changeset version && changeset publish",
"inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js",
"mcp-server": "node dist/mcp-server.js",
"format-check": "biome format .",
@@ -71,6 +73,7 @@
"@inquirer/search": "^3.0.15",
"@openrouter/ai-sdk-provider": "^0.4.5",
"@streamparser/json": "^0.0.22",
"@tm/cli": "*",
"ai": "^4.3.10",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
@@ -94,6 +97,8 @@
"jsonrepair": "^3.13.0",
"jsonwebtoken": "^9.0.2",
"lru-cache": "^10.2.0",
"marked": "^15.0.12",
"marked-terminal": "^7.3.0",
"ollama-ai-provider": "^1.2.0",
"openai": "^4.89.0",
"ora": "^8.2.0",
@@ -109,6 +114,7 @@
"engines": {
"node": ">=18.0.0"
},
"packageManager": "npm@10.9.2",
"repository": {
"type": "git",
"url": "git+https://github.com/eyaltoledano/claude-task-master.git"
@@ -124,14 +130,14 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.28.1",
"dotenv-mono": "^1.5.1",
"@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1",
"concurrently": "^9.2.1",
"cross-env": "^10.0.0",
"dotenv-mono": "^1.5.1",
"execa": "^8.0.1",
"ink": "^5.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"mock-fs": "^5.5.0",
@@ -139,6 +145,7 @@
"supertest": "^7.1.0",
"tsup": "^8.5.0",
"tsx": "^4.16.2",
"turbo": "^2.5.6",
"typescript": "^5.9.2"
}
}

View File

@@ -7,9 +7,8 @@
"types": "./dist/tsup.base.d.ts",
"exports": {
".": {
"types": "./src/tsup.base.ts",
"import": "./dist/tsup.base.js",
"require": "./dist/tsup.base.cjs"
"types": "./dist/tsup.base.d.ts",
"import": "./dist/tsup.base.js"
}
},
"files": ["dist", "src"],
@@ -17,15 +16,14 @@
"author": "",
"license": "MIT",
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"tsup": "^8.5.0",
"dotenv-mono": "^1.5.1",
"typescript": "^5.7.3"
},
"peerDependencies": {
"tsup": "^8.0.0"
"dependencies": {
"tsup": "^8.5.0"
}
}

View File

@@ -8,73 +8,29 @@ const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = !isProduction;
/**
* Base configuration for library packages (tm-core, etc.)
* Environment helpers
*/
export const libraryConfig: Partial<Options> = {
format: ['cjs', 'esm'],
target: 'es2022',
// Sourcemaps only in development to reduce production bundle size
sourcemap: isDevelopment,
clean: true,
dts: true,
// Enable optimizations in production
splitting: isProduction,
treeshake: isProduction,
minify: isProduction,
bundle: true,
esbuildOptions(options) {
options.conditions = ['module'];
// Better source mapping in development only
options.sourcesContent = isDevelopment;
// Keep original names for better debugging in development
options.keepNames = isDevelopment;
},
// Watch mode configuration for development
watch: isDevelopment ? ['src'] : false
export const env = {
isProduction,
isDevelopment,
NODE_ENV: process.env.NODE_ENV || 'development'
};
/**
* Base configuration for CLI packages
* Base tsup configuration for all packages
* Since everything gets bundled into root dist/ anyway, use consistent settings
*/
export const cliConfig: Partial<Options> = {
export const baseConfig: Partial<Options> = {
format: ['esm'],
target: 'node18',
splitting: false,
// Sourcemaps only in development to reduce production bundle size
sourcemap: isDevelopment,
clean: true,
dts: true,
shims: true,
// Enable minification in production for smaller bundles
dts: false,
minify: isProduction,
treeshake: isProduction,
esbuildOptions(options) {
options.platform = 'node';
// Better source mapping in development only
options.sourcesContent = isDevelopment;
// Keep original names for better debugging in development
options.keepNames = isDevelopment;
}
};
/**
* Base configuration for executable bundles (root level)
*/
export const executableConfig: Partial<Options> = {
format: ['esm'],
target: 'node18',
splitting: false,
// Sourcemaps only in development to reduce production bundle size
sourcemap: isDevelopment,
clean: true,
bundle: true, // Bundle everything into one file
// Minify in production for smaller executables
minify: isProduction,
// Handle TypeScript imports transparently
loader: {
'.js': 'jsx',
'.ts': 'ts'
},
// Don't bundle any other dependencies (auto-external all node_modules)
external: [/^[^./]/],
esbuildOptions(options) {
options.platform = 'node';
// Allow importing TypeScript from JavaScript
@@ -83,14 +39,18 @@ export const executableConfig: Partial<Options> = {
options.sourcesContent = isDevelopment;
// Keep original names for better debugging in development
options.keepNames = isDevelopment;
}
},
// Watch mode configuration for development
watch: false
};
/**
* Common external modules that should not be bundled
* Legacy external modules list - kept for backwards compatibility
* Note: When using tsup-node, this is not needed as it automatically
* excludes dependencies and peerDependencies from package.json
*/
export const commonExternals = [
// Native Node.js modules
// Native Node.js modules (for cases where tsup is used instead of tsup-node)
'fs',
'path',
'child_process',
@@ -119,6 +79,7 @@ export const commonExternals = [
/**
* Utility function to merge configurations
* Simplified for tsup-node usage
*/
export function mergeConfig(
baseConfig: Partial<Options>,
@@ -127,8 +88,6 @@ export function mergeConfig(
return {
...baseConfig,
...overrides,
// Merge arrays instead of overwriting
external: [...(baseConfig.external || []), ...(overrides.external || [])],
// Merge esbuildOptions
esbuildOptions(options, context) {
if (baseConfig.esbuildOptions) {
@@ -140,12 +99,3 @@ export function mergeConfig(
}
} as Options;
}
/**
* Environment helpers
*/
export const env = {
isProduction,
isDevelopment,
NODE_ENV: process.env.NODE_ENV || 'development'
};

View File

@@ -6,9 +6,10 @@
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"baseUrl": ".",
"outDir": "dist",
"allowJs": true,
"strict": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,

View File

@@ -1,23 +0,0 @@
import { defineConfig } from 'tsup';
const isProduction = process.env.NODE_ENV === 'production';
export default defineConfig({
entry: ['src/tsup.base.ts'],
format: ['esm', 'cjs'],
target: 'node18',
// Sourcemaps only in development
sourcemap: !isProduction,
clean: true,
dts: true,
// Enable minification in production
minify: isProduction,
treeshake: isProduction,
external: ['tsup'],
esbuildOptions(options) {
// Better source mapping in development only
options.sourcesContent = !isProduction;
// Keep original names for better debugging in development
options.keepNames = !isProduction;
}
});

View File

@@ -7,61 +7,19 @@
"types": "./src/index.ts",
"main": "./dist/index.js",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.js"
},
"./auth": {
"types": "./src/auth/index.ts",
"import": "./dist/auth/index.js"
},
"./storage": {
"types": "./src/storage/index.ts",
"import": "./dist/storage/index.js"
},
"./config": {
"types": "./src/config/index.ts",
"import": "./dist/config/index.js"
},
"./providers": {
"types": "./src/providers/index.ts",
"import": "./dist/providers/index.js"
},
"./services": {
"types": "./src/services/index.ts",
"import": "./dist/services/index.js"
},
"./errors": {
"types": "./src/errors/index.ts",
"import": "./dist/errors/index.js"
},
"./logger": {
"types": "./src/logger/index.ts",
"import": "./dist/logger/index.js"
},
"./types": {
"types": "./src/types/index.ts",
"import": "./dist/types/index.js"
},
"./interfaces": {
"types": "./src/interfaces/index.ts",
"import": "./dist/interfaces/index.js"
},
"./utils": {
"types": "./src/utils/index.ts",
"import": "./dist/utils/index.js",
"require": "./dist/utils/index.js"
},
"./workflow": {
"types": "./src/workflow/index.ts",
"import": "./dist/workflow/index.js",
"require": "./dist/workflow/index.js"
},
"./package.json": "./package.json"
".": "./src/index.ts",
"./auth": "./src/auth/index.ts",
"./storage": "./src/storage/index.ts",
"./config": "./src/config/index.ts",
"./providers": "./src/providers/index.ts",
"./services": "./src/services/index.ts",
"./errors": "./src/errors/index.ts",
"./logger": "./src/logger/index.ts",
"./types": "./src/types/index.ts",
"./interfaces": "./src/interfaces/index.ts",
"./utils": "./src/utils/index.ts"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
@@ -73,9 +31,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@supabase/supabase-js": "^2.57.0",
"@tm/workflow-engine": "*",
"chalk": "^5.3.0",
"@supabase/supabase-js": "^2.57.4",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -83,15 +39,16 @@
"@tm/build-config": "*",
"@types/node": "^20.11.30",
"@vitest/coverage-v8": "^2.0.5",
"dotenv-mono": "^1.3.14",
"ts-node": "^10.9.2",
"tsup": "^8.0.2",
"tsup": "^8.5.0",
"typescript": "^5.4.3",
"vitest": "^2.0.5"
},
"engines": {
"node": ">=18.0.0"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"files": ["src", "README.md", "CHANGELOG.md"],
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
"author": "Task Master AI",
"license": "MIT"

View File

@@ -31,7 +31,7 @@ export class AuthManager {
private organizationService?: OrganizationService;
private constructor(config?: Partial<AuthConfig>) {
this.credentialStore = new CredentialStore(config);
this.credentialStore = CredentialStore.getInstance(config);
this.supabaseClient = new SupabaseAuthClient();
this.oauthService = new OAuthService(this.credentialStore, config);
@@ -73,6 +73,7 @@ export class AuthManager {
*/
static resetInstance(): void {
AuthManager.instance = null;
CredentialStore.resetInstance();
}
/**

View File

@@ -19,15 +19,39 @@ import { getLogger } from '../logger/index.js';
* human-readable persisted format in the auth.json file.
*/
export class CredentialStore {
private static instance: CredentialStore | null = null;
private logger = getLogger('CredentialStore');
private config: AuthConfig;
// Clock skew tolerance for expiry checks (30 seconds)
private readonly CLOCK_SKEW_MS = 30_000;
constructor(config?: Partial<AuthConfig>) {
private constructor(config?: Partial<AuthConfig>) {
this.config = getAuthConfig(config);
}
/**
* Get the singleton instance of CredentialStore
*/
static getInstance(config?: Partial<AuthConfig>): CredentialStore {
if (!CredentialStore.instance) {
CredentialStore.instance = new CredentialStore(config);
} else if (config) {
// Warn if config is provided after initialization
const logger = getLogger('CredentialStore');
logger.warn(
'getInstance called with config after initialization; config is ignored.'
);
}
return CredentialStore.instance;
}
/**
* Reset the singleton instance (useful for testing)
*/
static resetInstance(): void {
CredentialStore.instance = null;
}
/**
* Get stored authentication credentials
* @returns AuthCredentials with expiresAt as number (milliseconds) for runtime use

View File

@@ -5,7 +5,7 @@
export { AuthManager } from './auth-manager.js';
export { CredentialStore } from './credential-store.js';
export { OAuthService } from './oauth-service.js';
export { SupabaseSessionStorage } from './supabase-session-storage';
export { SupabaseSessionStorage } from './supabase-session-storage.js';
export type {
Organization,
Brief,

View File

@@ -7,9 +7,9 @@
*/
import { SupportedStorage } from '@supabase/supabase-js';
import { CredentialStore } from './credential-store';
import { AuthCredentials } from './types';
import { getLogger } from '../logger';
import { CredentialStore } from './credential-store.js';
import { AuthCredentials } from './types.js';
import { getLogger } from '../logger/index.js';
const STORAGE_KEY = 'sb-taskmaster-auth-token';

View File

@@ -10,8 +10,8 @@ import {
} from '@supabase/supabase-js';
import { AuthenticationError } from '../auth/types.js';
import { getLogger } from '../logger/index.js';
import { SupabaseSessionStorage } from '../auth/supabase-session-storage';
import { CredentialStore } from '../auth/credential-store';
import { SupabaseSessionStorage } from '../auth/supabase-session-storage.js';
import { CredentialStore } from '../auth/credential-store.js';
export class SupabaseAuthClient {
private client: SupabaseJSClient | null = null;
@@ -19,7 +19,7 @@ export class SupabaseAuthClient {
private logger = getLogger('SupabaseAuthClient');
constructor() {
const credentialStore = new CredentialStore();
const credentialStore = CredentialStore.getInstance();
this.sessionStorage = new SupabaseSessionStorage(credentialStore);
}

View File

@@ -55,7 +55,3 @@ export {
// Re-export logger
export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
// Re-export workflow
export { WorkflowService, type WorkflowServiceConfig } from './workflow/index.js';
export type * from './workflow/index.js';

View File

@@ -16,10 +16,6 @@ import type {
TaskFilter,
StorageType
} from './types/index.js';
import {
WorkflowService,
type WorkflowServiceConfig
} from './workflow/index.js';
/**
* Options for creating TaskMasterCore instance
@@ -27,7 +23,6 @@ import {
export interface TaskMasterCoreOptions {
projectPath: string;
configuration?: Partial<IConfiguration>;
workflow?: Partial<WorkflowServiceConfig>;
}
/**
@@ -43,7 +38,6 @@ export type { GetTaskListOptions } from './services/task-service.js';
export class TaskMasterCore {
private configManager: ConfigManager;
private taskService: TaskService;
private workflowService: WorkflowService;
/**
* Create and initialize a new TaskMasterCore instance
@@ -66,7 +60,6 @@ export class TaskMasterCore {
// Services will be initialized in the initialize() method
this.configManager = null as any;
this.taskService = null as any;
this.workflowService = null as any;
}
/**
@@ -93,28 +86,6 @@ export class TaskMasterCore {
// Create task service
this.taskService = new TaskService(this.configManager);
await this.taskService.initialize();
// Create workflow service
const workflowConfig: WorkflowServiceConfig = {
projectRoot: options.projectPath,
...options.workflow
};
// Pass task retrieval function to workflow service
this.workflowService = new WorkflowService(
workflowConfig,
async (taskId: string) => {
const task = await this.getTask(taskId);
if (!task) {
throw new TaskMasterError(
`Task ${taskId} not found`,
ERROR_CODES.TASK_NOT_FOUND
);
}
return task;
}
);
await this.workflowService.initialize();
} catch (error) {
throw new TaskMasterError(
'Failed to initialize TaskMasterCore',
@@ -204,21 +175,11 @@ export class TaskMasterCore {
await this.configManager.setActiveTag(tag);
}
/**
* Get workflow service for workflow operations
*/
get workflow(): WorkflowService {
return this.workflowService;
}
/**
* Close and cleanup resources
*/
async close(): Promise<void> {
// TaskService handles storage cleanup internally
if (this.workflowService) {
await this.workflowService.dispose();
}
}
}

View File

@@ -1,17 +0,0 @@
/**
* @fileoverview Workflow Module
* Public exports for workflow functionality
*/
export { WorkflowService, type WorkflowServiceConfig } from './workflow-service.js';
// Re-export workflow engine types for convenience
export type {
WorkflowExecutionContext,
WorkflowStatus,
WorkflowEvent,
WorkflowEventType,
WorkflowProcess,
ProcessStatus,
WorktreeInfo
} from '@tm/workflow-engine';

View File

@@ -1,218 +0,0 @@
/**
* @fileoverview Workflow Service
* Integrates workflow engine into Task Master Core
*/
import {
TaskExecutionManager,
type TaskExecutionManagerConfig,
type WorkflowExecutionContext
} from '@tm/workflow-engine';
import type { Task } from '../types/index.js';
import { TaskMasterError } from '../errors/index.js';
export interface WorkflowServiceConfig {
/** Project root directory */
projectRoot: string;
/** Maximum number of concurrent workflows */
maxConcurrent?: number;
/** Default timeout for workflow execution (minutes) */
defaultTimeout?: number;
/** Base directory for worktrees */
worktreeBase?: string;
/** Claude Code executable path */
claudeExecutable?: string;
/** Enable debug logging */
debug?: boolean;
}
/**
* WorkflowService provides Task Master workflow capabilities through core
*/
export class WorkflowService {
private workflowEngine: TaskExecutionManager;
constructor(
config: WorkflowServiceConfig,
private getTask: (taskId: string) => Promise<Task>
) {
const engineConfig: TaskExecutionManagerConfig = {
projectRoot: config.projectRoot,
maxConcurrent: config.maxConcurrent || 5,
defaultTimeout: config.defaultTimeout || 60,
worktreeBase:
config.worktreeBase ||
require('path').join(config.projectRoot, '..', 'task-worktrees'),
claudeExecutable: config.claudeExecutable || 'claude',
debug: config.debug || false
};
this.workflowEngine = new TaskExecutionManager(engineConfig);
}
/**
* Initialize the workflow service
*/
async initialize(): Promise<void> {
await this.workflowEngine.initialize();
}
/**
* Start a workflow for a task
*/
async start(
taskId: string,
options?: {
branchName?: string;
timeout?: number;
env?: Record<string, string>;
}
): Promise<string> {
try {
// Get task from core
const task = await this.getTask(taskId);
// Start workflow using engine
return await this.workflowEngine.startTaskExecution(task, options);
} catch (error) {
throw new TaskMasterError(
`Failed to start workflow for task ${taskId}`,
'WORKFLOW_START_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Stop a workflow
*/
async stop(workflowId: string, force = false): Promise<void> {
try {
await this.workflowEngine.stopTaskExecution(workflowId, force);
} catch (error) {
throw new TaskMasterError(
`Failed to stop workflow ${workflowId}`,
'WORKFLOW_STOP_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Pause a workflow
*/
async pause(workflowId: string): Promise<void> {
try {
await this.workflowEngine.pauseTaskExecution(workflowId);
} catch (error) {
throw new TaskMasterError(
`Failed to pause workflow ${workflowId}`,
'WORKFLOW_PAUSE_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Resume a paused workflow
*/
async resume(workflowId: string): Promise<void> {
try {
await this.workflowEngine.resumeTaskExecution(workflowId);
} catch (error) {
throw new TaskMasterError(
`Failed to resume workflow ${workflowId}`,
'WORKFLOW_RESUME_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Get workflow status
*/
getStatus(workflowId: string): WorkflowExecutionContext | undefined {
return this.workflowEngine.getWorkflowStatus(workflowId);
}
/**
* Get workflow by task ID
*/
getByTaskId(taskId: string): WorkflowExecutionContext | undefined {
return this.workflowEngine.getWorkflowByTaskId(taskId);
}
/**
* List all workflows
*/
list(): WorkflowExecutionContext[] {
return this.workflowEngine.listWorkflows();
}
/**
* List active workflows
*/
listActive(): WorkflowExecutionContext[] {
return this.workflowEngine.listActiveWorkflows();
}
/**
* Send input to a running workflow
*/
async sendInput(workflowId: string, input: string): Promise<void> {
try {
await this.workflowEngine.sendInputToWorkflow(workflowId, input);
} catch (error) {
throw new TaskMasterError(
`Failed to send input to workflow ${workflowId}`,
'WORKFLOW_INPUT_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Clean up all workflows
*/
async cleanup(force = false): Promise<void> {
try {
await this.workflowEngine.cleanup(force);
} catch (error) {
throw new TaskMasterError(
'Failed to cleanup workflows',
'WORKFLOW_CLEANUP_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Subscribe to workflow events
*/
on(event: string, listener: (...args: any[]) => void): void {
this.workflowEngine.on(event, listener);
}
/**
* Unsubscribe from workflow events
*/
off(event: string, listener: (...args: any[]) => void): void {
this.workflowEngine.off(event, listener);
}
/**
* Get workflow engine instance (for advanced usage)
*/
getEngine(): TaskExecutionManager {
return this.workflowEngine;
}
/**
* Dispose of the workflow service
*/
async dispose(): Promise<void> {
await this.cleanup(true);
this.workflowEngine.removeAllListeners();
}
}

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"module": "NodeNext",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
@@ -24,11 +24,12 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"types": ["node"],
"resolveJsonModule": true,
"isolatedModules": true
"isolatedModules": true,
"allowImportingTsExtensions": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]

View File

@@ -1,53 +0,0 @@
import { defineConfig } from 'tsup';
import { dotenvLoad } from 'dotenv-mono';
dotenvLoad();
// Get all TM_PUBLIC_* env variables for build-time injection
const getBuildTimeEnvs = () => {
const envs: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('TM_PUBLIC_')) {
// Return the actual value, not JSON.stringify'd
envs[key] = value || '';
}
}
return envs;
};
export default defineConfig({
entry: {
index: 'src/index.ts',
'auth/index': 'src/auth/index.ts',
'config/index': 'src/config/index.ts',
'errors/index': 'src/errors/index.ts',
'interfaces/index': 'src/interfaces/index.ts',
'logger/index': 'src/logger/index.ts',
'parser/index': 'src/parser/index.ts',
'providers/index': 'src/providers/index.ts',
'services/index': 'src/services/index.ts',
'storage/index': 'src/storage/index.ts',
'types/index': 'src/types/index.ts',
'utils/index': 'src/utils/index.ts',
'workflow/index': 'src/workflow/index.ts'
},
format: ['cjs', 'esm'],
dts: true,
sourcemap: true,
clean: true,
splitting: false,
treeshake: true,
minify: false,
target: 'es2022',
tsconfig: './tsconfig.json',
outDir: 'dist',
// Replace process.env.TM_PUBLIC_* with actual values at build time
env: getBuildTimeEnvs(),
// Auto-external all dependencies from package.json
external: [
// External all node_modules - everything not starting with . or /
/^[^./]/
],
esbuildOptions(options) {
options.conditions = ['module'];
}
});

View File

@@ -1,371 +0,0 @@
# @tm/workflow-engine
Enhanced Task Master workflow execution engine with git worktree isolation and Claude Code process management.
## Overview
The Workflow Engine extends Task Master with advanced execution capabilities:
- **Git Worktree Isolation**: Each task runs in its own isolated worktree
- **Process Sandboxing**: Spawns dedicated Claude Code processes for task execution
- **Real-time Monitoring**: Track workflow progress and process output
- **State Management**: Persistent workflow state across sessions
- **Parallel Execution**: Run multiple tasks concurrently with resource limits
## Architecture
```
TaskExecutionManager
├── WorktreeManager # Git worktree lifecycle
├── ProcessSandbox # Claude Code process management
└── WorkflowStateManager # Persistent state tracking
```
## Quick Start
```typescript
import { TaskExecutionManager } from '@tm/workflow-engine';
const manager = new TaskExecutionManager({
projectRoot: '/path/to/project',
worktreeBase: '/path/to/worktrees',
claudeExecutable: 'claude',
maxConcurrent: 3,
defaultTimeout: 60,
debug: true
});
await manager.initialize();
// Start task execution
const workflowId = await manager.startTaskExecution({
id: '1.2',
title: 'Implement authentication',
description: 'Add JWT-based auth system',
status: 'pending',
priority: 'high'
});
// Monitor workflow
const workflow = manager.getWorkflowStatus(workflowId);
console.log(`Status: ${workflow.status}`);
// Stop when complete
await manager.stopTaskExecution(workflowId);
```
## CLI Integration
```bash
# Start workflow
tm workflow start 1.2
# List active workflows
tm workflow list
# Check status
tm workflow status workflow-1.2-1234567890-abc123
# Stop workflow
tm workflow stop workflow-1.2-1234567890-abc123
```
## VS Code Extension
The workflow engine integrates with the Task Master VS Code extension to provide:
- **Workflow Tree View**: Visual workflow management
- **Process Monitoring**: Real-time output streaming
- **Worktree Navigation**: Quick access to isolated workspaces
- **Status Indicators**: Visual workflow state tracking
## Core Components
### TaskExecutionManager
Orchestrates complete workflow lifecycle:
```typescript
// Event-driven workflow management
manager.on('workflow.started', (event) => {
console.log(`Started: ${event.workflowId}`);
});
manager.on('process.output', (event) => {
console.log(`[${event.data.stream}]: ${event.data.data}`);
});
```
### WorktreeManager
Manages git worktree operations:
```typescript
import { WorktreeManager } from '@tm/workflow-engine';
const manager = new WorktreeManager({
worktreeBase: './worktrees',
projectRoot: process.cwd(),
autoCleanup: true
});
// Create isolated workspace
const worktree = await manager.createWorktree('task-1.2');
console.log(`Created: ${worktree.path}`);
// List all worktrees
const worktrees = await manager.listWorktrees();
// Cleanup
await manager.removeWorktree('task-1.2');
```
### ProcessSandbox
Spawns and manages Claude Code processes:
```typescript
import { ProcessSandbox } from '@tm/workflow-engine';
const sandbox = new ProcessSandbox({
claudeExecutable: 'claude',
defaultTimeout: 30,
debug: true
});
// Start isolated process
const process = await sandbox.startProcess(
'workflow-123',
'task-1.2',
'Implement user authentication with JWT tokens',
{ cwd: '/path/to/worktree' }
);
// Send input
await sandbox.sendInput('workflow-123', 'npm test');
// Monitor output
sandbox.on('process.output', (event) => {
console.log(event.data.data);
});
```
### WorkflowStateManager
Persistent workflow state management:
```typescript
import { WorkflowStateManager } from '@tm/workflow-engine';
const stateManager = new WorkflowStateManager({
projectRoot: process.cwd()
});
await stateManager.loadState();
// Register workflow
const workflowId = await stateManager.registerWorkflow({
taskId: '1.2',
taskTitle: 'Authentication',
// ... other context
});
// Update status
await stateManager.updateWorkflowStatus(workflowId, 'running');
// Query workflows
const running = stateManager.listWorkflowsByStatus('running');
```
## Configuration
### Environment Variables
- `TASKMASTER_WORKFLOW_DEBUG`: Enable debug logging
- `TASKMASTER_CLAUDE_PATH`: Custom Claude Code executable path
- `TASKMASTER_WORKTREE_BASE`: Base directory for worktrees
- `TASKMASTER_MAX_CONCURRENT`: Maximum concurrent workflows
### Config Object
```typescript
interface TaskExecutionManagerConfig {
projectRoot: string; // Project root directory
worktreeBase: string; // Worktree base path
claudeExecutable: string; // Claude executable
maxConcurrent: number; // Concurrent limit
defaultTimeout: number; // Timeout (minutes)
debug: boolean; // Debug logging
}
```
## Workflow States
| State | Description |
|-------|-------------|
| `pending` | Created but not started |
| `initializing` | Setting up worktree/process |
| `running` | Active execution |
| `paused` | Temporarily stopped |
| `completed` | Successfully finished |
| `failed` | Error occurred |
| `cancelled` | User cancelled |
| `timeout` | Exceeded time limit |
## Events
The workflow engine emits events for real-time monitoring:
```typescript
// Workflow lifecycle
manager.on('workflow.started', (event) => {});
manager.on('workflow.completed', (event) => {});
manager.on('workflow.failed', (event) => {});
// Process events
manager.on('process.started', (event) => {});
manager.on('process.output', (event) => {});
manager.on('process.stopped', (event) => {});
// Worktree events
manager.on('worktree.created', (event) => {});
manager.on('worktree.deleted', (event) => {});
```
## Error Handling
The workflow engine provides specialized error types:
```typescript
import {
WorkflowError,
WorktreeError,
ProcessError,
MaxConcurrentWorkflowsError
} from '@tm/workflow-engine';
try {
await manager.startTaskExecution(task);
} catch (error) {
if (error instanceof MaxConcurrentWorkflowsError) {
console.log('Too many concurrent workflows');
} else if (error instanceof WorktreeError) {
console.log('Worktree operation failed');
}
}
```
## Development
```bash
# Install dependencies
npm install
# Build package
npm run build
# Run tests
npm test
# Development mode
npm run dev
```
## Integration Examples
### With Task Master Core
```typescript
import { createTaskMasterCore } from '@tm/core';
import { TaskExecutionManager } from '@tm/workflow-engine';
const core = await createTaskMasterCore({ projectPath: '.' });
const workflows = new TaskExecutionManager({ /*...*/ });
// Get task from core
const tasks = await core.getTaskList({});
const task = tasks.tasks.find(t => t.id === '1.2');
// Execute with workflow engine
if (task) {
const workflowId = await workflows.startTaskExecution(task);
}
```
### With VS Code Extension
```typescript
import { WorkflowProvider } from './workflow-provider';
// Register tree view
const provider = new WorkflowProvider(context);
vscode.window.createTreeView('taskmaster.workflows', {
treeDataProvider: provider
});
// Register commands
vscode.commands.registerCommand('taskmaster.workflow.start',
async (taskId) => {
await provider.startWorkflow(taskId);
}
);
```
## Troubleshooting
### Common Issues
1. **Worktree Creation Fails**
```bash
# Check git version (requires 2.5+)
git --version
# Verify project is git repository
git status
```
2. **Claude Code Not Found**
```bash
# Check Claude installation
which claude
# Set custom path
export TASKMASTER_CLAUDE_PATH=/path/to/claude
```
3. **Permission Errors**
```bash
# Check worktree directory permissions
chmod -R 755 ./worktrees
```
### Debug Mode
Enable debug logging for troubleshooting:
```typescript
const manager = new TaskExecutionManager({
// ... other config
debug: true
});
```
Or via environment:
```bash
export TASKMASTER_WORKFLOW_DEBUG=true
tm workflow start 1.2
```
## Roadmap
- [ ] Process resource monitoring (CPU, memory)
- [ ] Workflow templates and presets
- [ ] Integration with CI/CD pipelines
- [ ] Workflow scheduling and queueing
- [ ] Multi-machine workflow distribution
- [ ] Advanced debugging and profiling tools
## License
MIT WITH Commons-Clause

View File

@@ -1,56 +0,0 @@
{
"name": "@tm/workflow-engine",
"version": "0.1.0",
"description": "Task Master workflow execution engine with git worktree and process management",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./task-execution": {
"import": "./dist/task-execution/index.js",
"types": "./dist/task-execution/index.d.ts"
},
"./worktree": {
"import": "./dist/worktree/index.js",
"types": "./dist/worktree/index.d.ts"
},
"./process": {
"import": "./dist/process/index.js",
"types": "./dist/process/index.d.ts"
},
"./state": {
"import": "./dist/state/index.js",
"types": "./dist/state/index.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"test:watch": "vitest --watch",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@tm/core": "*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.0.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
},
"files": ["dist"],
"keywords": [
"task-master",
"workflow",
"git-worktree",
"process-management",
"claude-code"
],
"author": "Task Master AI Team",
"license": "MIT"
}

View File

@@ -1,6 +0,0 @@
/**
* @fileoverview Workflow Engine Errors
* Public error exports
*/
export * from './workflow.errors.js';

View File

@@ -1,59 +0,0 @@
/**
* @fileoverview Workflow Engine Errors
* Custom error classes for workflow operations
*/
export class WorkflowError extends Error {
constructor(
message: string,
public code: string,
public workflowId?: string,
public taskId?: string,
public cause?: Error
) {
super(message);
this.name = 'WorkflowError';
}
}
export class WorktreeError extends WorkflowError {
constructor(message: string, public path?: string, cause?: Error) {
super(message, 'WORKTREE_ERROR', undefined, undefined, cause);
this.name = 'WorktreeError';
}
}
export class ProcessError extends WorkflowError {
constructor(message: string, public pid?: number, cause?: Error) {
super(message, 'PROCESS_ERROR', undefined, undefined, cause);
this.name = 'ProcessError';
}
}
export class WorkflowTimeoutError extends WorkflowError {
constructor(workflowId: string, timeoutMinutes: number) {
super(
`Workflow ${workflowId} timed out after ${timeoutMinutes} minutes`,
'WORKFLOW_TIMEOUT',
workflowId
);
this.name = 'WorkflowTimeoutError';
}
}
export class WorkflowNotFoundError extends WorkflowError {
constructor(workflowId: string) {
super(`Workflow ${workflowId} not found`, 'WORKFLOW_NOT_FOUND', workflowId);
this.name = 'WorkflowNotFoundError';
}
}
export class MaxConcurrentWorkflowsError extends WorkflowError {
constructor(maxConcurrent: number) {
super(
`Maximum concurrent workflows (${maxConcurrent}) reached`,
'MAX_CONCURRENT_WORKFLOWS'
);
this.name = 'MaxConcurrentWorkflowsError';
}
}

View File

@@ -1,19 +0,0 @@
/**
* @fileoverview Workflow Engine
* Main entry point for the Task Master workflow execution engine
*/
// Core task execution
export * from './task-execution/index.js';
// Component managers
export * from './worktree/index.js';
export * from './process/index.js';
export * from './state/index.js';
// Types and errors
export * from './types/index.js';
export * from './errors/index.js';
// Convenience exports
export { TaskExecutionManager as WorkflowEngine } from './task-execution/index.js';

View File

@@ -1,6 +0,0 @@
/**
* @fileoverview Process Management
* Public exports for process operations
*/
export * from './process-sandbox.js';

View File

@@ -1,378 +0,0 @@
/**
* @fileoverview Process Sandbox
* Manages Claude Code process execution in isolated environments
*/
import { spawn, ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import type {
WorkflowProcess,
WorkflowEvent,
WorkflowEventType
} from '../types/workflow.types.js';
import { ProcessError } from '../errors/workflow.errors.js';
export interface ProcessSandboxConfig {
/** Claude Code executable path */
claudeExecutable: string;
/** Default timeout for processes (minutes) */
defaultTimeout: number;
/** Environment variables to pass to processes */
environment?: Record<string, string>;
/** Enable debug output */
debug: boolean;
}
export interface ProcessOptions {
/** Working directory for the process */
cwd: string;
/** Environment variables (merged with config) */
env?: Record<string, string>;
/** Timeout in minutes (overrides default) */
timeout?: number;
/** Additional Claude Code arguments */
args?: string[];
}
/**
* ProcessSandbox manages Claude Code process lifecycle
* Single responsibility: Process spawning, monitoring, and cleanup
*/
export class ProcessSandbox extends EventEmitter {
private config: ProcessSandboxConfig;
private activeProcesses = new Map<string, WorkflowProcess>();
private childProcesses = new Map<string, ChildProcess>();
private timeouts = new Map<string, NodeJS.Timeout>();
constructor(config: ProcessSandboxConfig) {
super();
this.config = config;
this.setupCleanupHandlers();
}
/**
* Start a Claude Code process for task execution
*/
async startProcess(
workflowId: string,
taskId: string,
taskPrompt: string,
options: ProcessOptions
): Promise<WorkflowProcess> {
if (this.activeProcesses.has(workflowId)) {
throw new ProcessError(
`Process already running for workflow ${workflowId}`
);
}
// Prepare command and arguments
const args = [
'-p', // Print mode for non-interactive execution
taskPrompt,
...(options.args || [])
];
// Prepare environment
const env = {
...process.env,
...this.config.environment,
...options.env,
// Ensure task context is available
TASKMASTER_WORKFLOW_ID: workflowId,
TASKMASTER_TASK_ID: taskId
};
try {
// Spawn Claude Code process
const childProcess = spawn(this.config.claudeExecutable, args, {
cwd: options.cwd,
env,
stdio: ['pipe', 'pipe', 'pipe']
});
const workflowProcess: WorkflowProcess = {
pid: childProcess.pid!,
command: this.config.claudeExecutable,
args,
cwd: options.cwd,
env,
startedAt: new Date(),
status: 'starting'
};
// Store process references
this.activeProcesses.set(workflowId, workflowProcess);
this.childProcesses.set(workflowId, childProcess);
// Setup process event handlers
this.setupProcessHandlers(workflowId, taskId, childProcess);
// Setup timeout if specified
const timeoutMinutes = options.timeout || this.config.defaultTimeout;
if (timeoutMinutes > 0) {
this.setupProcessTimeout(workflowId, timeoutMinutes);
}
// Emit process started event
this.emitEvent('process.started', workflowId, taskId, {
pid: workflowProcess.pid,
command: workflowProcess.command
});
workflowProcess.status = 'running';
return workflowProcess;
} catch (error) {
throw new ProcessError(
`Failed to start process for workflow ${workflowId}`,
undefined,
error as Error
);
}
}
/**
* Stop a running process
*/
async stopProcess(workflowId: string, force = false): Promise<void> {
const process = this.activeProcesses.get(workflowId);
const childProcess = this.childProcesses.get(workflowId);
if (!process || !childProcess) {
throw new ProcessError(
`No running process found for workflow ${workflowId}`
);
}
try {
// Clear timeout
const timeout = this.timeouts.get(workflowId);
if (timeout) {
clearTimeout(timeout);
this.timeouts.delete(workflowId);
}
// Kill the process
if (force) {
childProcess.kill('SIGKILL');
} else {
childProcess.kill('SIGTERM');
// Give it 5 seconds to gracefully exit, then force kill
setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill('SIGKILL');
}
}, 5000);
}
process.status = 'stopped';
// Emit process stopped event
this.emitEvent('process.stopped', workflowId, process.pid.toString(), {
pid: process.pid,
forced: force
});
} catch (error) {
throw new ProcessError(
`Failed to stop process for workflow ${workflowId}`,
process.pid,
error as Error
);
}
}
/**
* Send input to a running process
*/
async sendInput(workflowId: string, input: string): Promise<void> {
const childProcess = this.childProcesses.get(workflowId);
if (!childProcess) {
throw new ProcessError(
`No running process found for workflow ${workflowId}`
);
}
try {
childProcess.stdin?.write(input);
childProcess.stdin?.write('\n');
} catch (error) {
throw new ProcessError(
`Failed to send input to process for workflow ${workflowId}`,
childProcess.pid,
error as Error
);
}
}
/**
* Get process information
*/
getProcess(workflowId: string): WorkflowProcess | undefined {
return this.activeProcesses.get(workflowId);
}
/**
* List all active processes
*/
listProcesses(): WorkflowProcess[] {
return Array.from(this.activeProcesses.values());
}
/**
* Check if a process is running
*/
isProcessRunning(workflowId: string): boolean {
const process = this.activeProcesses.get(workflowId);
return process?.status === 'running' || process?.status === 'starting';
}
/**
* Clean up all processes
*/
async cleanupAll(force = false): Promise<void> {
const workflowIds = Array.from(this.activeProcesses.keys());
await Promise.all(
workflowIds.map(async (workflowId) => {
try {
await this.stopProcess(workflowId, force);
} catch (error) {
console.error(
`Failed to cleanup process for workflow ${workflowId}:`,
error
);
}
})
);
}
/**
* Setup process event handlers
*/
private setupProcessHandlers(
workflowId: string,
taskId: string,
childProcess: ChildProcess
): void {
const process = this.activeProcesses.get(workflowId);
if (!process) return;
// Handle stdout
childProcess.stdout?.on('data', (data) => {
const output = data.toString();
if (this.config.debug) {
console.log(`[${workflowId}] STDOUT:`, output);
}
this.emitEvent('process.output', workflowId, taskId, {
stream: 'stdout',
data: output
});
});
// Handle stderr
childProcess.stderr?.on('data', (data) => {
const output = data.toString();
if (this.config.debug) {
console.error(`[${workflowId}] STDERR:`, output);
}
this.emitEvent('process.output', workflowId, taskId, {
stream: 'stderr',
data: output
});
});
// Handle process exit
childProcess.on('exit', (code, signal) => {
process.status = code === 0 ? 'stopped' : 'crashed';
this.emitEvent('process.stopped', workflowId, taskId, {
pid: process.pid,
exitCode: code,
signal
});
// Cleanup
this.activeProcesses.delete(workflowId);
this.childProcesses.delete(workflowId);
const timeout = this.timeouts.get(workflowId);
if (timeout) {
clearTimeout(timeout);
this.timeouts.delete(workflowId);
}
});
// Handle process errors
childProcess.on('error', (error) => {
process.status = 'crashed';
this.emitEvent('process.error', workflowId, taskId, undefined, error);
// Cleanup
this.activeProcesses.delete(workflowId);
this.childProcesses.delete(workflowId);
});
}
/**
* Setup process timeout
*/
private setupProcessTimeout(
workflowId: string,
timeoutMinutes: number
): void {
const timeout = setTimeout(
async () => {
console.warn(`Process timeout reached for workflow ${workflowId}`);
try {
await this.stopProcess(workflowId, true);
} catch (error) {
console.error('Failed to stop timed out process:', error);
}
},
timeoutMinutes * 60 * 1000
);
this.timeouts.set(workflowId, timeout);
}
/**
* Emit workflow event
*/
private emitEvent(
type: WorkflowEventType,
workflowId: string,
taskId: string,
data?: any,
error?: Error
): void {
const event: WorkflowEvent = {
type,
workflowId,
taskId,
timestamp: new Date(),
data,
error
};
this.emit('event', event);
this.emit(type, event);
}
/**
* Setup cleanup handlers for graceful shutdown
*/
private setupCleanupHandlers(): void {
const cleanup = () => {
console.log('Cleaning up processes...');
this.cleanupAll(true).catch(console.error);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
}
}

View File

@@ -1,6 +0,0 @@
/**
* @fileoverview State Management
* Public exports for workflow state operations
*/
export * from './workflow-state-manager.js';

View File

@@ -1,320 +0,0 @@
/**
* @fileoverview Workflow State Manager
* Extends tm-core RuntimeStateManager with workflow tracking capabilities
*/
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type {
WorkflowExecutionContext,
WorkflowStatus,
WorkflowEvent
} from '../types/workflow.types.js';
import { WorkflowError } from '../errors/workflow.errors.js';
export interface WorkflowStateConfig {
/** Project root directory */
projectRoot: string;
/** Custom state directory (defaults to .taskmaster) */
stateDir?: string;
}
export interface WorkflowRegistryEntry {
/** Workflow ID */
workflowId: string;
/** Task ID being executed */
taskId: string;
/** Workflow status */
status: WorkflowStatus;
/** Worktree path */
worktreePath: string;
/** Process ID if running */
processId?: number;
/** Start timestamp */
startedAt: string;
/** Last activity timestamp */
lastActivity: string;
/** Branch name */
branchName: string;
/** Additional metadata */
metadata?: Record<string, any>;
}
/**
* WorkflowStateManager manages workflow execution state
* Extends the concept of RuntimeStateManager to track active workflows globally
*/
export class WorkflowStateManager {
private config: WorkflowStateConfig;
private stateFilePath: string;
private activeWorkflows = new Map<string, WorkflowExecutionContext>();
constructor(config: WorkflowStateConfig) {
this.config = config;
const stateDir = config.stateDir || '.taskmaster';
this.stateFilePath = path.join(config.projectRoot, stateDir, 'workflows.json');
}
/**
* Load workflow state from disk
*/
async loadState(): Promise<void> {
try {
const stateData = await fs.readFile(this.stateFilePath, 'utf-8');
const registry = JSON.parse(stateData) as Record<string, WorkflowRegistryEntry>;
// Convert registry entries to WorkflowExecutionContext
for (const [workflowId, entry] of Object.entries(registry)) {
const context: WorkflowExecutionContext = {
taskId: entry.taskId,
taskTitle: `Task ${entry.taskId}`, // Will be updated when task details are loaded
taskDescription: '',
projectRoot: this.config.projectRoot,
worktreePath: entry.worktreePath,
branchName: entry.branchName,
processId: entry.processId,
startedAt: new Date(entry.startedAt),
status: entry.status,
lastActivity: new Date(entry.lastActivity),
metadata: entry.metadata
};
this.activeWorkflows.set(workflowId, context);
}
} catch (error: any) {
if (error.code === 'ENOENT') {
// Workflows file doesn't exist, start with empty state
console.debug('No workflows.json found, starting with empty state');
return;
}
console.warn('Failed to load workflow state:', error.message);
}
}
/**
* Save workflow state to disk
*/
async saveState(): Promise<void> {
const stateDir = path.dirname(this.stateFilePath);
try {
await fs.mkdir(stateDir, { recursive: true });
// Convert contexts to registry entries
const registry: Record<string, WorkflowRegistryEntry> = {};
for (const [workflowId, context] of this.activeWorkflows.entries()) {
registry[workflowId] = {
workflowId,
taskId: context.taskId,
status: context.status,
worktreePath: context.worktreePath,
processId: context.processId,
startedAt: context.startedAt.toISOString(),
lastActivity: context.lastActivity.toISOString(),
branchName: context.branchName,
metadata: context.metadata
};
}
await fs.writeFile(
this.stateFilePath,
JSON.stringify(registry, null, 2),
'utf-8'
);
} catch (error) {
throw new WorkflowError(
'Failed to save workflow state',
'WORKFLOW_STATE_SAVE_ERROR',
undefined,
undefined,
error as Error
);
}
}
/**
* Register a new workflow
*/
async registerWorkflow(context: WorkflowExecutionContext): Promise<string> {
const workflowId = this.generateWorkflowId(context.taskId);
this.activeWorkflows.set(workflowId, {
...context,
lastActivity: new Date()
});
await this.saveState();
return workflowId;
}
/**
* Update workflow context
*/
async updateWorkflow(
workflowId: string,
updates: Partial<WorkflowExecutionContext>
): Promise<void> {
const existing = this.activeWorkflows.get(workflowId);
if (!existing) {
throw new WorkflowError(
`Workflow ${workflowId} not found`,
'WORKFLOW_NOT_FOUND',
workflowId
);
}
const updated = {
...existing,
...updates,
lastActivity: new Date()
};
this.activeWorkflows.set(workflowId, updated);
await this.saveState();
}
/**
* Update workflow status
*/
async updateWorkflowStatus(workflowId: string, status: WorkflowStatus): Promise<void> {
await this.updateWorkflow(workflowId, { status });
}
/**
* Unregister a workflow (remove from state)
*/
async unregisterWorkflow(workflowId: string): Promise<void> {
if (!this.activeWorkflows.has(workflowId)) {
throw new WorkflowError(
`Workflow ${workflowId} not found`,
'WORKFLOW_NOT_FOUND',
workflowId
);
}
this.activeWorkflows.delete(workflowId);
await this.saveState();
}
/**
* Get workflow context by ID
*/
getWorkflow(workflowId: string): WorkflowExecutionContext | undefined {
return this.activeWorkflows.get(workflowId);
}
/**
* Get workflow by task ID
*/
getWorkflowByTaskId(taskId: string): WorkflowExecutionContext | undefined {
for (const context of this.activeWorkflows.values()) {
if (context.taskId === taskId) {
return context;
}
}
return undefined;
}
/**
* List all active workflows
*/
listWorkflows(): WorkflowExecutionContext[] {
return Array.from(this.activeWorkflows.values());
}
/**
* List workflows by status
*/
listWorkflowsByStatus(status: WorkflowStatus): WorkflowExecutionContext[] {
return this.listWorkflows().filter(w => w.status === status);
}
/**
* Get running workflows count
*/
getRunningCount(): number {
return this.listWorkflowsByStatus('running').length;
}
/**
* Check if a task has an active workflow
*/
hasActiveWorkflow(taskId: string): boolean {
return this.getWorkflowByTaskId(taskId) !== undefined;
}
/**
* Clean up completed/failed workflows older than specified time
*/
async cleanupOldWorkflows(olderThanHours = 24): Promise<number> {
const cutoffTime = new Date(Date.now() - (olderThanHours * 60 * 60 * 1000));
let cleaned = 0;
for (const [workflowId, context] of this.activeWorkflows.entries()) {
const isOld = context.lastActivity < cutoffTime;
const isFinished = ['completed', 'failed', 'cancelled', 'timeout'].includes(context.status);
if (isOld && isFinished) {
this.activeWorkflows.delete(workflowId);
cleaned++;
}
}
if (cleaned > 0) {
await this.saveState();
}
return cleaned;
}
/**
* Clear all workflow state
*/
async clearState(): Promise<void> {
try {
await fs.unlink(this.stateFilePath);
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
this.activeWorkflows.clear();
}
/**
* Record workflow event (for audit trail)
*/
async recordEvent(event: WorkflowEvent): Promise<void> {
// Update workflow last activity
const workflow = this.activeWorkflows.get(event.workflowId);
if (workflow) {
workflow.lastActivity = event.timestamp;
await this.saveState();
}
// Optional: Could extend to maintain event log file
if (process.env.TASKMASTER_DEBUG) {
console.log('Workflow Event:', {
type: event.type,
workflowId: event.workflowId,
taskId: event.taskId,
timestamp: event.timestamp.toISOString(),
data: event.data
});
}
}
/**
* Generate unique workflow ID
*/
private generateWorkflowId(taskId: string): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `workflow-${taskId}-${timestamp}-${random}`;
}
}

View File

@@ -1,6 +0,0 @@
/**
* @fileoverview Task Execution Management
* Public exports for task execution operations
*/
export * from './task-execution-manager.js';

View File

@@ -1,433 +0,0 @@
/**
* @fileoverview Task Execution Manager
* Orchestrates the complete task execution workflow using worktrees and processes
*/
import { EventEmitter } from 'node:events';
import path from 'node:path';
import type { Task } from '@tm/core';
import {
WorktreeManager,
type WorktreeManagerConfig
} from '../worktree/worktree-manager.js';
import {
ProcessSandbox,
type ProcessSandboxConfig
} from '../process/process-sandbox.js';
import {
WorkflowStateManager,
type WorkflowStateConfig
} from '../state/workflow-state-manager.js';
import type {
WorkflowConfig,
WorkflowExecutionContext,
WorkflowStatus,
WorkflowEvent
} from '../types/workflow.types.js';
import {
WorkflowError,
WorkflowNotFoundError,
MaxConcurrentWorkflowsError,
WorkflowTimeoutError
} from '../errors/workflow.errors.js';
export interface TaskExecutionManagerConfig extends WorkflowConfig {
/** Project root directory */
projectRoot: string;
}
/**
* TaskExecutionManager orchestrates the complete task execution workflow
* Coordinates worktree creation, process spawning, and state management
*/
export class TaskExecutionManager extends EventEmitter {
private config: TaskExecutionManagerConfig;
private worktreeManager: WorktreeManager;
private processSandbox: ProcessSandbox;
private stateManager: WorkflowStateManager;
private initialized = false;
constructor(config: TaskExecutionManagerConfig) {
super();
this.config = config;
// Initialize component managers
const worktreeConfig: WorktreeManagerConfig = {
worktreeBase: config.worktreeBase,
projectRoot: config.projectRoot,
autoCleanup: true
};
const processConfig: ProcessSandboxConfig = {
claudeExecutable: config.claudeExecutable,
defaultTimeout: config.defaultTimeout,
debug: config.debug
};
const stateConfig: WorkflowStateConfig = {
projectRoot: config.projectRoot
};
this.worktreeManager = new WorktreeManager(worktreeConfig);
this.processSandbox = new ProcessSandbox(processConfig);
this.stateManager = new WorkflowStateManager(stateConfig);
// Forward events from components
this.processSandbox.on('event', (event: WorkflowEvent) => {
this.stateManager.recordEvent(event);
this.emit('event', event);
});
}
/**
* Initialize the task execution manager
*/
async initialize(): Promise<void> {
if (this.initialized) return;
await this.stateManager.loadState();
// Clean up any stale workflows
await this.cleanupStaleWorkflows();
this.initialized = true;
}
/**
* Start task execution workflow
*/
async startTaskExecution(
task: Task,
options?: {
branchName?: string;
timeout?: number;
env?: Record<string, string>;
}
): Promise<string> {
if (!this.initialized) {
await this.initialize();
}
// Check concurrent workflow limit
const runningCount = this.stateManager.getRunningCount();
if (runningCount >= this.config.maxConcurrent) {
throw new MaxConcurrentWorkflowsError(this.config.maxConcurrent);
}
// Check if task already has an active workflow
if (this.stateManager.hasActiveWorkflow(task.id)) {
throw new WorkflowError(
`Task ${task.id} already has an active workflow`,
'TASK_ALREADY_EXECUTING',
undefined,
task.id
);
}
try {
// Create worktree
const worktreeInfo = await this.worktreeManager.createWorktree(
task.id,
options?.branchName
);
// Prepare task context
const context: WorkflowExecutionContext = {
taskId: task.id,
taskTitle: task.title,
taskDescription: task.description,
taskDetails: task.details,
projectRoot: this.config.projectRoot,
worktreePath: worktreeInfo.path,
branchName: worktreeInfo.branch,
startedAt: new Date(),
status: 'initializing',
lastActivity: new Date(),
metadata: {
priority: task.priority,
dependencies: task.dependencies
}
};
// Register workflow
const workflowId = await this.stateManager.registerWorkflow(context);
try {
// Prepare task prompt for Claude Code
const taskPrompt = this.generateTaskPrompt(task);
// Start Claude Code process
const process = await this.processSandbox.startProcess(
workflowId,
task.id,
taskPrompt,
{
cwd: worktreeInfo.path,
timeout: options?.timeout,
env: options?.env
}
);
// Update workflow with process information
await this.stateManager.updateWorkflow(workflowId, {
processId: process.pid,
status: 'running'
});
// Emit workflow started event
this.emitEvent('workflow.started', workflowId, task.id, {
worktreePath: worktreeInfo.path,
processId: process.pid
});
return workflowId;
} catch (error) {
// Clean up worktree if process failed to start
await this.worktreeManager.removeWorktree(task.id, true);
await this.stateManager.unregisterWorkflow(workflowId);
throw error;
}
} catch (error) {
throw new WorkflowError(
`Failed to start task execution for ${task.id}`,
'TASK_EXECUTION_START_ERROR',
undefined,
task.id,
error as Error
);
}
}
/**
* Stop task execution workflow
*/
async stopTaskExecution(workflowId: string, force = false): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
try {
// Stop the process if running
if (this.processSandbox.isProcessRunning(workflowId)) {
await this.processSandbox.stopProcess(workflowId, force);
}
// Update workflow status
const status: WorkflowStatus = force ? 'cancelled' : 'completed';
await this.stateManager.updateWorkflowStatus(workflowId, status);
// Clean up worktree
await this.worktreeManager.removeWorktree(workflow.taskId, force);
// Emit workflow stopped event
this.emitEvent('workflow.completed', workflowId, workflow.taskId, {
status,
forced: force
});
// Unregister workflow
await this.stateManager.unregisterWorkflow(workflowId);
} catch (error) {
throw new WorkflowError(
`Failed to stop workflow ${workflowId}`,
'WORKFLOW_STOP_ERROR',
workflowId,
workflow.taskId,
error as Error
);
}
}
/**
* Pause task execution
*/
async pauseTaskExecution(workflowId: string): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
if (workflow.status !== 'running') {
throw new WorkflowError(
`Cannot pause workflow ${workflowId} - not currently running`,
'WORKFLOW_NOT_RUNNING',
workflowId,
workflow.taskId
);
}
// For now, we'll just mark as paused - in the future could implement
// process suspension or other pause mechanisms
await this.stateManager.updateWorkflowStatus(workflowId, 'paused');
this.emitEvent('workflow.paused', workflowId, workflow.taskId);
}
/**
* Resume paused task execution
*/
async resumeTaskExecution(workflowId: string): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
if (workflow.status !== 'paused') {
throw new WorkflowError(
`Cannot resume workflow ${workflowId} - not currently paused`,
'WORKFLOW_NOT_PAUSED',
workflowId,
workflow.taskId
);
}
await this.stateManager.updateWorkflowStatus(workflowId, 'running');
this.emitEvent('workflow.resumed', workflowId, workflow.taskId);
}
/**
* Get workflow status
*/
getWorkflowStatus(workflowId: string): WorkflowExecutionContext | undefined {
return this.stateManager.getWorkflow(workflowId);
}
/**
* Get workflow by task ID
*/
getWorkflowByTaskId(taskId: string): WorkflowExecutionContext | undefined {
return this.stateManager.getWorkflowByTaskId(taskId);
}
/**
* List all workflows
*/
listWorkflows(): WorkflowExecutionContext[] {
return this.stateManager.listWorkflows();
}
/**
* List active workflows
*/
listActiveWorkflows(): WorkflowExecutionContext[] {
return this.stateManager.listWorkflowsByStatus('running');
}
/**
* Send input to a running workflow
*/
async sendInputToWorkflow(workflowId: string, input: string): Promise<void> {
const workflow = this.stateManager.getWorkflow(workflowId);
if (!workflow) {
throw new WorkflowNotFoundError(workflowId);
}
if (!this.processSandbox.isProcessRunning(workflowId)) {
throw new WorkflowError(
`Cannot send input to workflow ${workflowId} - process not running`,
'PROCESS_NOT_RUNNING',
workflowId,
workflow.taskId
);
}
await this.processSandbox.sendInput(workflowId, input);
}
/**
* Clean up all workflows
*/
async cleanup(force = false): Promise<void> {
// Stop all processes
await this.processSandbox.cleanupAll(force);
// Clean up all worktrees
await this.worktreeManager.cleanupAll(force);
// Clear workflow state
await this.stateManager.clearState();
}
/**
* Generate task prompt for Claude Code
*/
private generateTaskPrompt(task: Task): string {
const prompt = [
`Work on Task ${task.id}: ${task.title}`,
'',
`Description: ${task.description}`
];
if (task.details) {
prompt.push('', `Details: ${task.details}`);
}
if (task.testStrategy) {
prompt.push('', `Test Strategy: ${task.testStrategy}`);
}
if (task.dependencies?.length) {
prompt.push('', `Dependencies: ${task.dependencies.join(', ')}`);
}
prompt.push(
'',
'Please implement this task following the project conventions and best practices.',
'When complete, update the task status appropriately using the available Task Master commands.'
);
return prompt.join('\n');
}
/**
* Clean up stale workflows from previous sessions
*/
private async cleanupStaleWorkflows(): Promise<void> {
const workflows = this.stateManager.listWorkflows();
for (const workflow of workflows) {
const isStale =
workflow.status === 'running' &&
!this.processSandbox.isProcessRunning(`workflow-${workflow.taskId}`);
if (isStale) {
console.log(`Cleaning up stale workflow for task ${workflow.taskId}`);
try {
await this.stateManager.updateWorkflowStatus(
`workflow-${workflow.taskId}`,
'failed'
);
// Try to clean up worktree
await this.worktreeManager.removeWorktree(workflow.taskId, true);
} catch (error) {
console.error(`Failed to cleanup stale workflow:`, error);
}
}
}
}
/**
* Emit workflow event
*/
private emitEvent(
type: string,
workflowId: string,
taskId: string,
data?: any
): void {
const event: WorkflowEvent = {
type: type as any,
workflowId,
taskId,
timestamp: new Date(),
data
};
this.emit('event', event);
this.emit(type, event);
}
}

View File

@@ -1,6 +0,0 @@
/**
* @fileoverview Workflow Engine Types
* Public type exports
*/
export * from './workflow.types.js';

View File

@@ -1,119 +0,0 @@
/**
* @fileoverview Workflow Engine Types
* Core types for workflow execution system
*/
export interface WorkflowConfig {
/** Maximum number of concurrent workflows */
maxConcurrent: number;
/** Default timeout for workflow execution (minutes) */
defaultTimeout: number;
/** Base directory for worktrees */
worktreeBase: string;
/** Claude Code executable path */
claudeExecutable: string;
/** Enable debug logging */
debug: boolean;
}
export interface WorkflowExecutionContext {
/** Task ID being executed */
taskId: string;
/** Task title for display */
taskTitle: string;
/** Full task description */
taskDescription: string;
/** Task implementation details */
taskDetails?: string;
/** Project root path */
projectRoot: string;
/** Worktree path */
worktreePath: string;
/** Branch name for this workflow */
branchName: string;
/** Process ID of running Claude Code */
processId?: number;
/** Workflow start time */
startedAt: Date;
/** Workflow status */
status: WorkflowStatus;
/** Last activity timestamp */
lastActivity: Date;
/** Execution metadata */
metadata?: Record<string, any>;
}
export type WorkflowStatus =
| 'pending' // Created but not started
| 'initializing' // Setting up worktree/process
| 'running' // Active execution
| 'paused' // Temporarily stopped
| 'completed' // Successfully finished
| 'failed' // Error occurred
| 'cancelled' // User cancelled
| 'timeout'; // Exceeded time limit
export interface WorkflowEvent {
type: WorkflowEventType;
workflowId: string;
taskId: string;
timestamp: Date;
data?: any;
error?: Error;
}
export type WorkflowEventType =
| 'workflow.created'
| 'workflow.started'
| 'workflow.paused'
| 'workflow.resumed'
| 'workflow.completed'
| 'workflow.failed'
| 'workflow.cancelled'
| 'worktree.created'
| 'worktree.deleted'
| 'process.started'
| 'process.stopped'
| 'process.output'
| 'process.error';
export interface WorkflowProcess {
/** Process ID */
pid: number;
/** Command that was executed */
command: string;
/** Command arguments */
args: string[];
/** Working directory */
cwd: string;
/** Environment variables */
env?: Record<string, string>;
/** Process start time */
startedAt: Date;
/** Process status */
status: ProcessStatus;
}
export type ProcessStatus =
| 'starting'
| 'running'
| 'stopped'
| 'crashed'
| 'killed';
export interface WorktreeInfo {
/** Worktree path */
path: string;
/** Branch name */
branch: string;
/** Creation timestamp */
createdAt: Date;
/** Associated task ID */
taskId: string;
/** Git commit hash */
commit?: string;
/** Worktree lock status */
locked: boolean;
/** Lock reason if applicable */
lockReason?: string;
}

View File

@@ -1,6 +0,0 @@
/**
* @fileoverview Worktree Management
* Public exports for worktree operations
*/
export * from './worktree-manager.js';

View File

@@ -1,351 +0,0 @@
/**
* @fileoverview Worktree Manager
* Manages git worktree lifecycle for task execution
*/
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { WorktreeInfo } from '../types/workflow.types.js';
import { WorktreeError } from '../errors/workflow.errors.js';
export interface WorktreeManagerConfig {
/** Base directory for all worktrees */
worktreeBase: string;
/** Project root directory */
projectRoot: string;
/** Auto-cleanup on process exit */
autoCleanup: boolean;
}
/**
* WorktreeManager handles git worktree operations
* Single responsibility: Git worktree lifecycle management
*/
export class WorktreeManager {
private config: WorktreeManagerConfig;
private activeWorktrees = new Map<string, WorktreeInfo>();
constructor(config: WorktreeManagerConfig) {
this.config = config;
if (config.autoCleanup) {
this.setupCleanupHandlers();
}
}
/**
* Create a new worktree for task execution
*/
async createWorktree(taskId: string, branchName?: string): Promise<WorktreeInfo> {
const sanitizedTaskId = this.sanitizeTaskId(taskId);
const worktreePath = path.join(this.config.worktreeBase, `task-${sanitizedTaskId}`);
// Ensure base directory exists
await fs.mkdir(this.config.worktreeBase, { recursive: true });
// Generate unique branch name if not provided
const branch = branchName || `task/${sanitizedTaskId}-${Date.now()}`;
try {
// Check if worktree path already exists
if (await this.pathExists(worktreePath)) {
throw new WorktreeError(`Worktree path already exists: ${worktreePath}`);
}
// Create the worktree
await this.executeGitCommand(['worktree', 'add', '-b', branch, worktreePath], {
cwd: this.config.projectRoot
});
const worktreeInfo: WorktreeInfo = {
path: worktreePath,
branch,
createdAt: new Date(),
taskId,
locked: false
};
// Get commit hash
try {
const commit = await this.executeGitCommand(['rev-parse', 'HEAD'], {
cwd: worktreePath
});
worktreeInfo.commit = commit.trim();
} catch (error) {
console.warn('Failed to get commit hash for worktree:', error);
}
this.activeWorktrees.set(taskId, worktreeInfo);
return worktreeInfo;
} catch (error) {
throw new WorktreeError(
`Failed to create worktree for task ${taskId}`,
worktreePath,
error as Error
);
}
}
/**
* Remove a worktree and clean up
*/
async removeWorktree(taskId: string, force = false): Promise<void> {
const worktreeInfo = this.activeWorktrees.get(taskId);
if (!worktreeInfo) {
throw new WorktreeError(`No active worktree found for task ${taskId}`);
}
try {
// Remove the worktree
const args = ['worktree', 'remove', worktreeInfo.path];
if (force) {
args.push('--force');
}
await this.executeGitCommand(args, {
cwd: this.config.projectRoot
});
// Remove branch if it's a task-specific branch
if (worktreeInfo.branch.startsWith('task/')) {
try {
await this.executeGitCommand(['branch', '-D', worktreeInfo.branch], {
cwd: this.config.projectRoot
});
} catch (error) {
console.warn(`Failed to delete branch ${worktreeInfo.branch}:`, error);
}
}
this.activeWorktrees.delete(taskId);
} catch (error) {
throw new WorktreeError(
`Failed to remove worktree for task ${taskId}`,
worktreeInfo.path,
error as Error
);
}
}
/**
* List all active worktrees for this project
*/
async listWorktrees(): Promise<WorktreeInfo[]> {
try {
const output = await this.executeGitCommand(['worktree', 'list', '--porcelain'], {
cwd: this.config.projectRoot
});
const worktrees: WorktreeInfo[] = [];
const lines = output.trim().split('\n');
let currentWorktree: Partial<WorktreeInfo> = {};
for (const line of lines) {
if (line.startsWith('worktree ')) {
if (currentWorktree.path) {
// Complete previous worktree
worktrees.push(this.completeWorktreeInfo(currentWorktree));
}
currentWorktree = { path: line.substring(9) };
} else if (line.startsWith('HEAD ')) {
currentWorktree.commit = line.substring(5);
} else if (line.startsWith('branch ')) {
currentWorktree.branch = line.substring(7).replace('refs/heads/', '');
} else if (line === 'locked') {
currentWorktree.locked = true;
} else if (line.startsWith('locked ')) {
currentWorktree.locked = true;
currentWorktree.lockReason = line.substring(7);
}
}
// Add the last worktree
if (currentWorktree.path) {
worktrees.push(this.completeWorktreeInfo(currentWorktree));
}
// Filter to only our task worktrees
return worktrees.filter(wt =>
wt.path.startsWith(this.config.worktreeBase) &&
wt.branch?.startsWith('task/')
);
} catch (error) {
throw new WorktreeError('Failed to list worktrees', undefined, error as Error);
}
}
/**
* Get worktree info for a specific task
*/
getWorktreeInfo(taskId: string): WorktreeInfo | undefined {
return this.activeWorktrees.get(taskId);
}
/**
* Lock a worktree to prevent cleanup
*/
async lockWorktree(taskId: string, reason?: string): Promise<void> {
const worktreeInfo = this.activeWorktrees.get(taskId);
if (!worktreeInfo) {
throw new WorktreeError(`No active worktree found for task ${taskId}`);
}
try {
const args = ['worktree', 'lock', worktreeInfo.path];
if (reason) {
args.push('--reason', reason);
}
await this.executeGitCommand(args, {
cwd: this.config.projectRoot
});
worktreeInfo.locked = true;
worktreeInfo.lockReason = reason;
} catch (error) {
throw new WorktreeError(
`Failed to lock worktree for task ${taskId}`,
worktreeInfo.path,
error as Error
);
}
}
/**
* Unlock a worktree
*/
async unlockWorktree(taskId: string): Promise<void> {
const worktreeInfo = this.activeWorktrees.get(taskId);
if (!worktreeInfo) {
throw new WorktreeError(`No active worktree found for task ${taskId}`);
}
try {
await this.executeGitCommand(['worktree', 'unlock', worktreeInfo.path], {
cwd: this.config.projectRoot
});
worktreeInfo.locked = false;
delete worktreeInfo.lockReason;
} catch (error) {
throw new WorktreeError(
`Failed to unlock worktree for task ${taskId}`,
worktreeInfo.path,
error as Error
);
}
}
/**
* Clean up all task-related worktrees
*/
async cleanupAll(force = false): Promise<void> {
const worktrees = await this.listWorktrees();
for (const worktree of worktrees) {
if (worktree.taskId) {
try {
await this.removeWorktree(worktree.taskId, force);
} catch (error) {
console.error(`Failed to cleanup worktree for task ${worktree.taskId}:`, error);
}
}
}
}
/**
* Execute git command and return output
*/
private async executeGitCommand(
args: string[],
options: { cwd: string }
): Promise<string> {
return new Promise((resolve, reject) => {
const git = spawn('git', args, {
cwd: options.cwd,
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
git.stdout.on('data', (data) => {
stdout += data.toString();
});
git.stderr.on('data', (data) => {
stderr += data.toString();
});
git.on('close', (code) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`Git command failed (${code}): ${stderr || stdout}`));
}
});
git.on('error', (error) => {
reject(error);
});
});
}
/**
* Sanitize task ID for use in filesystem paths
*/
private sanitizeTaskId(taskId: string): string {
return taskId.replace(/[^a-zA-Z0-9.-]/g, '-');
}
/**
* Check if path exists
*/
private async pathExists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
/**
* Complete worktree info with defaults
*/
private completeWorktreeInfo(partial: Partial<WorktreeInfo>): WorktreeInfo {
const branch = partial.branch || 'unknown';
const taskIdMatch = branch.match(/^task\/(.+?)-/);
return {
path: partial.path || '',
branch,
createdAt: partial.createdAt || new Date(),
taskId: taskIdMatch?.[1] || partial.taskId || 'unknown',
commit: partial.commit,
locked: partial.locked || false,
lockReason: partial.lockReason
};
}
/**
* Setup cleanup handlers for graceful shutdown
*/
private setupCleanupHandlers(): void {
const cleanup = () => {
console.log('Cleaning up worktrees...');
this.cleanupAll(true).catch(console.error);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
}
}

View File

@@ -1,19 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"dist",
"node_modules",
"**/*.test.ts",
"**/*.spec.ts"
]
}

View File

@@ -1,17 +0,0 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: [
'src/index.ts',
'src/task-execution/index.ts',
'src/worktree/index.ts',
'src/process/index.ts',
'src/state/index.ts'
],
format: ['esm'],
dts: true,
sourcemap: true,
clean: true,
splitting: false,
treeshake: true
});

View File

@@ -1,19 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.d.ts',
'**/*.test.ts',
'**/*.spec.ts'
]
}
}
});

View File

@@ -15,8 +15,13 @@ import search from '@inquirer/search';
import ora from 'ora'; // Import ora
import { log, readJSON } from './utils.js';
// Import command registry from @tm/cli
import { registerAllCommands } from '@tm/cli';
// Import new commands from @tm/cli
import {
ListTasksCommand,
ShowCommand,
AuthCommand,
ContextCommand
} from '@tm/cli';
import {
parsePRD,
@@ -1737,9 +1742,21 @@ function registerCommands(programInstance) {
});
});
// Register all commands from @tm/cli using the command registry
// This automatically registers ListTasksCommand, AuthCommand, and any future commands
registerAllCommands(programInstance);
// NEW: Register the new list command from @tm/cli
// This command handles all its own configuration and logic
ListTasksCommand.registerOn(programInstance);
// Register the auth command from @tm/cli
// Handles authentication with tryhamster.com
AuthCommand.registerOn(programInstance);
// Register the context command from @tm/cli
// Manages workspace context (org/brief selection)
ContextCommand.registerOn(programInstance);
// Register the show command from @tm/cli
// Displays detailed information about tasks
ShowCommand.registerOn(programInstance);
// expand command
programInstance
@@ -2559,80 +2576,6 @@ ${result.result}
);
});
// show command
programInstance
.command('show')
.description(
`Display detailed information about one or more tasks${chalk.reset('')}`
)
.argument('[id]', 'Task ID(s) to show (comma-separated for multiple)')
.option(
'-i, --id <id>',
'Task ID(s) to show (comma-separated for multiple)'
)
.option('-s, --status <status>', 'Filter subtasks by status')
.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 (taskId, options) => {
// Initialize TaskMaster
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
// Only pass complexityReportPath if user provided a custom path
if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
initOptions.complexityReportPath = options.report;
}
const taskMaster = initTaskMaster(initOptions);
const idArg = taskId || options.id;
const statusFilter = options.status;
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
process.exit(1);
}
// Check if multiple IDs are provided (comma-separated)
const taskIds = idArg
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0);
if (taskIds.length > 1) {
// Multiple tasks - use compact summary view with interactive drill-down
await displayMultipleTasksSummary(
taskMaster.getTasksPath(),
taskIds,
taskMaster.getComplexityReportPath(),
statusFilter,
{ projectRoot: taskMaster.getProjectRoot(), tag }
);
} else {
// Single task - use detailed view
await displayTaskById(
taskMaster.getTasksPath(),
taskIds[0],
taskMaster.getComplexityReportPath(),
statusFilter,
{ projectRoot: taskMaster.getProjectRoot(), tag }
);
}
});
// add-dependency command
programInstance
.command('add-dependency')

View File

@@ -1,7 +1,4 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from '../../scripts/modules/utils.js';
import packageJson from '../../package.json' with { type: 'json' };
/**
* Reads the version from the nearest package.json relative to this file.
@@ -9,27 +6,5 @@ import { log } from '../../scripts/modules/utils.js';
* @returns {string} The version string or 'unknown'.
*/
export function getTaskMasterVersion() {
let version = 'unknown';
try {
// Get the directory of the current module (getPackageVersion.js)
const currentModuleFilename = fileURLToPath(import.meta.url);
const currentModuleDirname = path.dirname(currentModuleFilename);
// Construct the path to package.json relative to this file (../../package.json)
const packageJsonPath = path.join(
currentModuleDirname,
'..',
'..',
'package.json'
);
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
version = packageJson.version;
}
} catch (error) {
// Silently fall back to default version
log('warn', 'Could not read own package.json for version info.', error);
}
return version;
return packageJson.version || 'unknown';
}

View File

@@ -12,7 +12,15 @@
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"baseUrl": "."
"baseUrl": ".",
"paths": {
"@tm/core": ["./packages/tm-core/src/index.ts"],
"@tm/core/*": ["./packages/tm-core/src/*"],
"@tm/cli": ["./apps/cli/src/index.ts"],
"@tm/cli/*": ["./apps/cli/src/*"],
"@tm/build-config": ["./packages/build-config/src/index.ts"],
"@tm/build-config/*": ["./packages/build-config/src/*"]
}
},
"tsx": {
"tsconfig": {

View File

@@ -1,20 +1,97 @@
import { defineConfig } from 'tsup';
import {
executableConfig,
mergeConfig,
commonExternals
} from '@tm/build-config';
import { baseConfig, mergeConfig } from '@tm/build-config';
import { load as dotenvLoad } from 'dotenv-mono';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenvLoad();
// Get all TM_PUBLIC_* env variables for build-time injection
const getBuildTimeEnvs = () => {
const envs: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('TM_PUBLIC_')) {
// Return the actual value, not JSON.stringify'd
envs[key] = value || '';
}
}
return envs;
};
export default defineConfig(
mergeConfig(executableConfig, {
mergeConfig(baseConfig, {
entry: {
'task-master': 'bin/task-master.js',
'task-master': 'scripts/dev.js',
'mcp-server': 'mcp-server/server.js'
},
outDir: 'dist',
publicDir: 'public',
// Bundle our monorepo packages but keep node_modules external
noExternal: [/@tm\/.*/],
external: commonExternals
// Override the base config's external to bundle our workspace packages
noExternal: [/^@tm\//],
external: [
/^@supabase\//, // Keep Supabase external to avoid dynamic require issues
'marked',
'marked-terminal'
],
env: getBuildTimeEnvs(),
esbuildOptions(options) {
// Set up path aliases for workspace packages
options.alias = {
'@tm/core': path.resolve(__dirname, 'packages/tm-core/src/index.ts'),
'@tm/core/auth': path.resolve(
__dirname,
'packages/tm-core/src/auth/index.ts'
),
'@tm/core/storage': path.resolve(
__dirname,
'packages/tm-core/src/storage/index.ts'
),
'@tm/core/config': path.resolve(
__dirname,
'packages/tm-core/src/config/index.ts'
),
'@tm/core/providers': path.resolve(
__dirname,
'packages/tm-core/src/providers/index.ts'
),
'@tm/core/services': path.resolve(
__dirname,
'packages/tm-core/src/services/index.ts'
),
'@tm/core/errors': path.resolve(
__dirname,
'packages/tm-core/src/errors/index.ts'
),
'@tm/core/logger': path.resolve(
__dirname,
'packages/tm-core/src/logger/index.ts'
),
'@tm/core/types': path.resolve(
__dirname,
'packages/tm-core/src/types/index.ts'
),
'@tm/core/interfaces': path.resolve(
__dirname,
'packages/tm-core/src/interfaces/index.ts'
),
'@tm/core/utils': path.resolve(
__dirname,
'packages/tm-core/src/utils/index.ts'
),
'@tm/cli': path.resolve(__dirname, 'apps/cli/src/index.ts'),
'@tm/cli/commands': path.resolve(
__dirname,
'apps/cli/src/commands/index.ts'
),
'@tm/cli/utils': path.resolve(__dirname, 'apps/cli/src/utils/index.ts'),
'@tm/cli/ui': path.resolve(__dirname, 'apps/cli/src/ui/index.ts'),
'@tm/build-config': path.resolve(
__dirname,
'packages/build-config/src/tsup.base.ts'
)
};
}
})
);

44
turbo.json Normal file
View File

@@ -0,0 +1,44 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"outputLogs": "new-only"
},
"dev": {
"cache": false,
"persistent": true,
"inputs": [
"$TURBO_DEFAULT$",
"!{packages,apps}/**/dist/**",
"!{packages,apps}/**/node_modules/**"
]
},
"test:watch": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
},
"lint": {
"dependsOn": ["^build"],
"inputs": [
"$TURBO_DEFAULT$",
"!{packages,apps}/**/dist/**",
"!{packages,apps}/**/node_modules/**"
],
"outputLogs": "new-only"
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": [
"$TURBO_DEFAULT$",
"!{packages,apps}/**/dist/**",
"!{packages,apps}/**/node_modules/**"
],
"outputLogs": "new-only"
}
},
"globalDependencies": ["turbo.json", "tsconfig.json", ".env*"]
}