This commit is contained in:
Ralph Khreish
2025-09-08 12:33:43 -07:00
parent b9e3eecafe
commit d4826e0258
39 changed files with 3785 additions and 34 deletions

View File

@@ -24,6 +24,7 @@
},
"dependencies": {
"@tm/core": "*",
"@tm/workflow-engine": "*",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"cli-table3": "^0.6.5",

View File

@@ -494,17 +494,6 @@ 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

@@ -0,0 +1,38 @@
/**
* 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

@@ -307,17 +307,6 @@ 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,58 @@
/**
* @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

@@ -0,0 +1,9 @@
/**
* @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

@@ -0,0 +1,253 @@
/**
* @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

@@ -0,0 +1,239 @@
/**
* @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

@@ -0,0 +1,339 @@
/**
* @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

@@ -0,0 +1,260 @@
/**
* @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

@@ -6,6 +6,10 @@
// Commands
export { ListTasksCommand } from './commands/list.command.js';
export { AuthCommand } from './commands/auth.command.js';
export { WorkflowCommand } from './commands/workflow.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

@@ -324,3 +324,61 @@ 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();
}