Compare commits

..

27 Commits

Author SHA1 Message Date
Ralph Khreish
cd90b4d65f chore: fix changeset config 2025-09-17 22:53:25 +02:00
Ralph Khreish
33259cc4f8 docs(changeset): Test out the RC 2025-09-17 22:24:22 +02:00
Ralph Khreish
1917e6a01a chore: fix privating certain packages 2025-09-17 22:19:41 +02:00
Ralph Khreish
35f3e71d2d chore: fix pre-release CI 2025-09-17 22:19:41 +02:00
Ralph Khreish
1a3a528bf7 Merge pull request #1212 from eyaltoledano/ralph/main/rebase 2025-09-17 22:05:13 +02:00
Ralph Khreish
c164adc6ff chore: move to tsdown (#1211)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-17 22:00:37 +02:00
Ralph Khreish
9d61e0447d fix: UI list and show (#1210) 2025-09-17 22:00:37 +02:00
Ralph Khreish
ee11b735b3 chore: fix env variables (#1204)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-17 22:00:37 +02:00
losolosol
6d978228d9 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-17 22:00:36 +02:00
Ralph Khreish
ea9341e7af chore: fix CI p2 2025-09-17 22:00:36 +02:00
Ralph Khreish
4296e383ea chore: fix CI 2025-09-17 22:00:36 +02:00
Ralph Khreish
97b2781709 feat: add tm show (#1199) 2025-09-17 22:00:36 +02:00
Ralph Khreish
96553e4a5f chore: fix CI with new typescript setup (#1194)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-17 22:00:36 +02:00
Ralph Khreish
7582219365 chore: fix format 2025-09-17 22:00:35 +02:00
Ralph Khreish
84baedc3d2 feat: implement tm list remote (#1185) 2025-09-17 22:00:35 +02:00
Ralph Khreish
78da39edff chore: address oauth PR concerns (#1184) 2025-09-17 22:00:35 +02:00
Ralph Khreish
4d1416b175 feat: add oauth with remote server (#1178) 2025-09-17 22:00:35 +02:00
Ralph Khreish
dc811eb45e feat: create tm-core and apps/cli (#1093)
- add typescript
- add npm workspaces
2025-09-17 22:00:34 +02:00
Ralph Khreish
3c41a113fe chore: move to tsdown (#1211)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-17 21:55:53 +02: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
github-actions[bot]
e81040def5 Version Packages (#1189)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-09 03:17:22 +02:00
Ralph Khreish
597f6b03b4 Merge pull request #1164 (Release 0.26.0) 2025-09-08 22:17:30 +02:00
95 changed files with 4183 additions and 11909 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples

View File

@@ -8,7 +8,9 @@
],
"commit": false,
"fixed": [],
"linked": [],
"linked": [
["task-master-ai", "@tm/cli", "@tm/core"]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": minor
---
Restore Taskmaster claude-code commands and move clear commands under /remove to avoid collision with the claude-code /clear command.

View File

@@ -1,9 +0,0 @@
---
"task-master-ai": minor
---
Enhanced Gemini CLI provider with codebase-aware task generation
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": minor
---
Test out the RC

View File

@@ -0,0 +1,5 @@
---
"@tm/cli": minor
---
testing this stuff out to see how the release candidate works with monorepo

View File

@@ -1,16 +0,0 @@
{
"mode": "exit",
"tag": "rc",
"initialVersions": {
"task-master-ai": "0.25.1",
"docs": "0.0.1",
"extension": "0.24.1"
},
"changesets": [
"clarify-force-move-docs",
"curvy-moons-dig",
"sour-coins-lay",
"strong-eagles-vanish",
"wet-candies-accept"
]
}

View File

@@ -1,11 +0,0 @@
---
"task-master-ai": minor
---
Add configurable codebase analysis feature flag with multiple configuration sources
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
Priority order: .env > MCP session env > .taskmaster/config.json.
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.

View File

@@ -1,12 +0,0 @@
---
"task-master-ai": minor
---
feat(move): improve cross-tag move UX and safety
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
---

View File

@@ -1,14 +0,0 @@
---
"task-master-ai": minor
---
Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
Commands contextualised:
- add-task
- update-subtask
- update-task
- update

View File

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

View File

@@ -11,6 +11,10 @@ on:
- next
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
@@ -61,7 +65,7 @@ jobs:
timeout-minutes: 5
- name: Typecheck
run: npm run typecheck
run: npm run turbo:typecheck
env:
FORCE_COLOR: 1
@@ -84,11 +88,18 @@ jobs:
timeout-minutes: 5
- name: Build
run: npm run build
run: npm run turbo:build
env:
NODE_ENV: production
FORCE_COLOR: 1
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: dist/
retention-days: 1
test:
name: Test
timeout-minutes: 15
@@ -108,10 +119,11 @@ jobs:
run: npm install --frozen-lockfile --prefer-offline
timeout-minutes: 5
- name: Build packages (required for tests)
run: npm run build:packages
env:
NODE_ENV: production
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-artifacts
path: dist/
- name: Run Tests
run: |

View File

@@ -65,6 +65,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build packages
run: npm run turbo:build
env:
NODE_ENV: production
FORCE_COLOR: 1
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
uses: changesets/action@v1
with:

View File

@@ -41,6 +41,12 @@ jobs:
- name: Check pre-release mode
run: node ./.github/scripts/check-pre-release-mode.mjs "main"
- name: Build packages
run: npm run turbo:build
env:
NODE_ENV: production
FORCE_COLOR: 1
- name: Create Release Pull Request or Publish to npm
uses: changesets/action@v1
with:

3
.gitignore vendored
View File

@@ -94,3 +94,6 @@ apps/extension/.vscode-test/
# apps/extension
apps/extension/vsix-build/
# turbo
.turbo

View File

@@ -1,5 +1,48 @@
# task-master-ai
## 0.26.0
### Minor Changes
- [#1133](https://github.com/eyaltoledano/claude-task-master/pull/1133) [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144) Thanks [@neonwatty](https://github.com/neonwatty)! - Restore Taskmaster claude-code commands and move clear commands under /remove to avoid collision with the claude-code /clear command.
- [#1163](https://github.com/eyaltoledano/claude-task-master/pull/1163) [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Gemini CLI provider with codebase-aware task generation
Added automatic codebase analysis for Gemini CLI provider in parse-prd, and analyze-complexity, add-task, udpate-task, update, update-subtask commands
When using Gemini CLI as the AI provider, Task Master now instructs the AI to analyze the project structure, existing implementations, and patterns before generating tasks or subtasks
Tasks and subtasks generated by Claude Code are now informed by actual codebase analysis, resulting in more accurate and contextual outputs
- [#1165](https://github.com/eyaltoledano/claude-task-master/pull/1165) [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add configurable codebase analysis feature flag with multiple configuration sources
Users can now control whether codebase analysis features (Claude Code and Gemini CLI integration) are enabled through environment variables, MCP configuration, or project config files.
Priority order: .env > MCP session env > .taskmaster/config.json.
Set `TASKMASTER_ENABLE_CODEBASE_ANALYSIS=false` in `.env` to disable codebase analysis prompts and tool integration.
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - feat(move): improve cross-tag move UX and safety
- CLI: print "Next Steps" tips after cross-tag moves that used --ignore-dependencies (validate/fix guidance)
- CLI: show dedicated help block on ID collisions (destination tag already has the ID)
- Core: add structured suggestions to TASK_ALREADY_EXISTS errors
- MCP: map ID collision errors to TASK_ALREADY_EXISTS and include suggestions
- Tests: cover MCP options, error suggestions, CLI tips printing, and integration error payload suggestions
***
- [#1162](https://github.com/eyaltoledano/claude-task-master/pull/1162) [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhanced Claude Code and Google CLI integration with automatic codebase analysis for task operations
When using Claude Code as the AI provider, task management commands now automatically analyze your codebase before generating or updating tasks. This provides more accurate, context-aware implementation details that align with your project's existing architecture and patterns.
Commands contextualised:
- add-task
- update-subtask
- update-task
- update
### Patch Changes
- [#1135](https://github.com/eyaltoledano/claude-task-master/pull/1135) [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f) Thanks [@mm-parthy](https://github.com/mm-parthy)! - docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
## 0.26.0-rc.1
### Minor Changes

View File

@@ -3,18 +3,14 @@
"version": "1.0.0",
"description": "Task Master CLI - Command line interface for task management",
"type": "module",
"private": true,
"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 +24,6 @@
},
"dependencies": {
"@tm/core": "*",
"@tm/workflow-engine": "*",
"boxen": "^7.1.1",
"chalk": "5.6.2",
"cli-table3": "^0.6.5",
@@ -38,10 +33,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 +44,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,45 @@
/**
* @fileoverview Task Master header component
* Displays the banner, version, project info, and file path
*/
import chalk from 'chalk';
/**
* 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,44 @@ 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: '!'
},
completed: {
color: chalk.green,
icon: '✓',
tableIcon: '✓'
}
};
const config = statusConfig[status] || {
@@ -39,18 +64,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 +259,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 +312,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 +360,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,27 +1,36 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"module": "NodeNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"baseUrl": ".",
"rootDir": "./src",
"resolveJsonModule": true,
"allowJs": false,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"types": ["node"]
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"types": ["node"],
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
}

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

@@ -1,3 +1,5 @@
# docs
## 0.0.2
## 0.0.1

View File

@@ -1,14 +1,13 @@
{
"name": "docs",
"version": "0.0.1",
"version": "0.0.2",
"private": true,
"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

@@ -1,5 +1,12 @@
# Change Log
## 0.24.2
### Patch Changes
- Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]:
- task-master-ai@0.26.0
## 0.24.2-rc.1
### Patch Changes

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

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.24.2-rc.1",
"version": "0.24.2",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -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",
@@ -239,7 +240,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "0.26.0-rc.1"
"task-master-ai": "0.26.0"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",

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();

View File

@@ -18,7 +18,17 @@ export default {
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
// Transform files
transform: {},
preset: 'ts-jest/presets/default-esm',
extensionsToTreatAsEsm: ['.ts'],
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true
}
]
},
// Disable transformations for node_modules
transformIgnorePatterns: ['/node_modules/'],
@@ -27,6 +37,7 @@ export default {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1'
},
resolver: '<rootDir>/jest.resolver.cjs',
// Setup module aliases
moduleDirectories: ['node_modules', '<rootDir>'],

19
jest.resolver.cjs Normal file
View File

@@ -0,0 +1,19 @@
const { defaultResolver } = require('jest-resolve');
module.exports = function customResolver(request, options) {
const resolve = options.defaultResolver || defaultResolver;
try {
return resolve(request, options);
} catch (error) {
if (request.startsWith('.') && request.endsWith('.js')) {
try {
return resolve(request.replace(/\.js$/, '.ts'), options);
} catch (tsError) {
tsError.cause = tsError.cause ?? error;
throw tsError;
}
}
throw error;
}
};

9167
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.26.0-rc.1",
"version": "0.26.0",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
@@ -11,19 +11,12 @@
},
"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 && cross-env NODE_ENV=production tsdown",
"dev": "tsdown --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'",
"turbo:dev": "turbo dev",
"turbo:build": "turbo build",
"turbo:typecheck": "turbo typecheck",
"build:build-config": "npm run build -w @tm/build-config",
"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 +29,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 +65,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 +89,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 +106,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,21 +122,23 @@
},
"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",
"prettier": "^3.5.3",
"supertest": "^7.1.0",
"tsup": "^8.5.0",
"ts-jest": "^29.4.2",
"tsdown": "^0.15.2",
"tsx": "^4.16.2",
"turbo": "^2.5.6",
"typescript": "^5.9.2"
}
}

View File

@@ -3,13 +3,13 @@
"version": "1.0.0",
"description": "Shared build configuration for Task Master monorepo",
"type": "module",
"main": "./dist/tsup.base.js",
"types": "./dist/tsup.base.d.ts",
"private": true,
"main": "./dist/tsdown.base.js",
"types": "./src/tsdown.base.ts",
"exports": {
".": {
"types": "./src/tsup.base.ts",
"import": "./dist/tsup.base.js",
"require": "./dist/tsup.base.cjs"
"types": "./src/tsdown.base.ts",
"import": "./dist/tsdown.base.js"
}
},
"files": ["dist", "src"],
@@ -17,15 +17,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

@@ -0,0 +1,46 @@
/**
* Base tsdown configuration for Task Master monorepo
* Provides shared configuration that can be extended by individual packages
*/
import type { UserConfig } from 'tsdown';
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = !isProduction;
/**
* Environment helpers
*/
export const env = {
isProduction,
isDevelopment,
NODE_ENV: process.env.NODE_ENV || 'development'
};
/**
* Base tsdown configuration for all packages
* Since everything gets bundled into root dist/ anyway, use consistent settings
*/
export const baseConfig: Partial<UserConfig> = {
sourcemap: isDevelopment,
format: 'esm',
platform: 'node',
dts: isDevelopment,
minify: isProduction,
treeshake: isProduction,
// Keep all npm dependencies external (available via node_modules)
external: [/^[^@./]/, /^@(?!tm\/)/]
};
/**
* Utility function to merge configurations
* Simplified for tsdown usage
*/
export function mergeConfig(
base: Partial<UserConfig>,
overrides: Partial<UserConfig>
): Partial<UserConfig> {
return {
...base,
...overrides
};
}

View File

@@ -1,151 +0,0 @@
/**
* Base tsup configuration for Task Master monorepo
* Provides shared configuration that can be extended by individual packages
*/
import type { Options } from 'tsup';
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = !isProduction;
/**
* Base configuration for library packages (tm-core, etc.)
*/
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
};
/**
* Base configuration for CLI packages
*/
export const cliConfig: 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
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'
},
esbuildOptions(options) {
options.platform = 'node';
// Allow importing TypeScript from JavaScript
options.resolveExtensions = ['.ts', '.js', '.mjs', '.json'];
// Better source mapping in development only
options.sourcesContent = isDevelopment;
// Keep original names for better debugging in development
options.keepNames = isDevelopment;
}
};
/**
* Common external modules that should not be bundled
*/
export const commonExternals = [
// Native Node.js modules
'fs',
'path',
'child_process',
'crypto',
'os',
'url',
'util',
'stream',
'http',
'https',
'events',
'assert',
'buffer',
'querystring',
'readline',
'zlib',
'tty',
'net',
'dgram',
'dns',
'tls',
'cluster',
'process',
'module'
];
/**
* Utility function to merge configurations
*/
export function mergeConfig(
baseConfig: Partial<Options>,
overrides: Partial<Options>
): Options {
return {
...baseConfig,
...overrides,
// Merge arrays instead of overwriting
external: [...(baseConfig.external || []), ...(overrides.external || [])],
// Merge esbuildOptions
esbuildOptions(options, context) {
if (baseConfig.esbuildOptions) {
baseConfig.esbuildOptions(options, context);
}
if (overrides.esbuildOptions) {
overrides.esbuildOptions(options, context);
}
}
} 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

@@ -53,6 +53,7 @@ export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
*/
export const STATUS_ICONS: Record<TaskStatus, string> = {
done: '✓',
completed: '✓',
'in-progress': '►',
blocked: '⭕',
pending: '○',
@@ -71,5 +72,6 @@ export const STATUS_COLORS: Record<TaskStatus, string> = {
deferred: 'gray',
cancelled: 'red',
blocked: 'magenta',
review: 'cyan'
review: 'cyan',
completed: 'green'
} as const;

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

@@ -24,7 +24,8 @@ export type TaskStatus =
| 'deferred'
| 'cancelled'
| 'blocked'
| 'review';
| 'review'
| 'completed';
/**
* Task priority levels

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

@@ -17,7 +17,7 @@ describe('Complex Cross-Tag Scenarios', () => {
'..',
'..',
'..',
'bin',
'dist',
'task-master.js'
);

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": {

31
tsdown.config.ts Normal file
View File

@@ -0,0 +1,31 @@
import { defineConfig } from 'tsdown';
import { baseConfig, mergeConfig } from '@tm/build-config';
import { load as 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(
mergeConfig(baseConfig, {
entry: {
'task-master': 'scripts/dev.js',
'mcp-server': 'mcp-server/server.js'
},
outDir: 'dist',
copy: ['public'],
// Bundle only our workspace packages, keep npm dependencies external
noExternal: [/^@tm\//],
env: getBuildTimeEnvs()
})
);

View File

@@ -1,20 +0,0 @@
import { defineConfig } from 'tsup';
import {
executableConfig,
mergeConfig,
commonExternals
} from '@tm/build-config';
export default defineConfig(
mergeConfig(executableConfig, {
entry: {
'task-master': 'bin/task-master.js',
'mcp-server': 'mcp-server/server.js'
},
outDir: 'dist',
publicDir: 'public',
// Bundle our monorepo packages but keep node_modules external
noExternal: [/@tm\/.*/],
external: commonExternals
})
);

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*"]
}