Compare commits
25 Commits
docs/auto-
...
task-maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
170d6f2f65 | ||
|
|
137ef36278 | ||
|
|
1a3a528bf7 | ||
|
|
c164adc6ff | ||
|
|
9d61e0447d | ||
|
|
ee11b735b3 | ||
|
|
6d978228d9 | ||
|
|
ea9341e7af | ||
|
|
4296e383ea | ||
|
|
97b2781709 | ||
|
|
96553e4a5f | ||
|
|
7582219365 | ||
|
|
84baedc3d2 | ||
|
|
78da39edff | ||
|
|
4d1416b175 | ||
|
|
dc811eb45e | ||
|
|
3c41a113fe | ||
|
|
0e8c42c7cb | ||
|
|
799d1d2cce | ||
|
|
83af314879 | ||
|
|
dd03374496 | ||
|
|
4ab0affba7 | ||
|
|
77e1ddc237 | ||
|
|
e81040def5 | ||
|
|
597f6b03b4 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
docs(move): clarify cross-tag move docs; deprecate "force"; add explicit --with-dependencies/--ignore-dependencies examples
|
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
],
|
],
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [
|
||||||
|
["task-master-ai", "@tm/cli", "@tm/core"]
|
||||||
|
],
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"docs"
|
"docs"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
5
.changeset/moody-oranges-slide.md
Normal file
5
.changeset/moody-oranges-slide.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Test out the RC
|
||||||
5
.changeset/odd-otters-tan.md
Normal file
5
.changeset/odd-otters-tan.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tm/cli": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
testing this stuff out to see how the release candidate works with monorepo
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
5
.changeset/wild-ears-look.md
Normal file
5
.changeset/wild-ears-look.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"extension": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added a Start Build button to the VSCODE Task Properties Right Panel
|
||||||
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -11,6 +11,10 @@ on:
|
|||||||
- next
|
- next
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@@ -61,7 +65,7 @@ jobs:
|
|||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: npm run typecheck
|
run: npm run turbo:typecheck
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
@@ -84,11 +88,18 @@ jobs:
|
|||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run turbo:build
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-artifacts
|
||||||
|
path: dist/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
@@ -108,10 +119,11 @@ jobs:
|
|||||||
run: npm install --frozen-lockfile --prefer-offline
|
run: npm install --frozen-lockfile --prefer-offline
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
- name: Build packages (required for tests)
|
- name: Download build artifacts
|
||||||
run: npm run build:packages
|
uses: actions/download-artifact@v4
|
||||||
env:
|
with:
|
||||||
NODE_ENV: production
|
name: build-artifacts
|
||||||
|
path: dist/
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
11
.github/workflows/pre-release.yml
vendored
11
.github/workflows/pre-release.yml
vendored
@@ -65,6 +65,17 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run format
|
||||||
|
run: npm run format
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
- 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
|
- name: Create Release Candidate Pull Request or Publish Release Candidate to npm
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -41,6 +41,12 @@ jobs:
|
|||||||
- name: Check pre-release mode
|
- name: Check pre-release mode
|
||||||
run: node ./.github/scripts/check-pre-release-mode.mjs "main"
|
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
|
- name: Create Release Pull Request or Publish to npm
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -93,4 +93,7 @@ dev-debug.log
|
|||||||
apps/extension/.vscode-test/
|
apps/extension/.vscode-test/
|
||||||
|
|
||||||
# apps/extension
|
# apps/extension
|
||||||
apps/extension/vsix-build/
|
apps/extension/vsix-build/
|
||||||
|
|
||||||
|
# turbo
|
||||||
|
.turbo
|
||||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
|||||||
# task-master-ai
|
# 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
|
## 0.26.0-rc.1
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
7
apps/cli/CHANGELOG.md
Normal file
7
apps/cli/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# @tm/cli
|
||||||
|
|
||||||
|
## 1.1.0-rc.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`cd90b4d`](https://github.com/eyaltoledano/claude-task-master/commit/cd90b4d65fc2f04bdad9fb73aba320b58a124240) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/cli",
|
"name": "@tm/cli",
|
||||||
"version": "1.0.0",
|
"version": "0.26.0",
|
||||||
"description": "Task Master CLI - Command line interface for task management",
|
"description": "Task Master CLI - Command line interface for task management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": "./src/index.ts"
|
||||||
"types": "./src/index.ts",
|
|
||||||
"import": "./dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md"],
|
"files": ["dist", "README.md"],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
|
||||||
"dev": "tsup --watch",
|
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "biome check src",
|
"lint": "biome check src",
|
||||||
"format": "biome format --write src",
|
"format": "biome format --write src",
|
||||||
@@ -28,7 +24,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tm/core": "*",
|
"@tm/core": "*",
|
||||||
"@tm/workflow-engine": "*",
|
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"cli-table3": "^0.6.5",
|
"cli-table3": "^0.6.5",
|
||||||
@@ -38,10 +33,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@tm/build-config": "*",
|
|
||||||
"@types/inquirer": "^9.0.3",
|
"@types/inquirer": "^9.0.3",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"tsup": "^8.3.0",
|
|
||||||
"tsx": "^4.20.4",
|
"tsx": "^4.20.4",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^2.1.8"
|
||||||
@@ -51,5 +44,10 @@
|
|||||||
},
|
},
|
||||||
"keywords": ["task-master", "cli", "task-management", "productivity"],
|
"keywords": ["task-master", "cli", "task-management", "productivity"],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,6 +494,17 @@ export class AuthCommand extends Command {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Static method to register this command on an existing program
|
* 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 {
|
static register(program: Command, name?: string): AuthCommand {
|
||||||
const authCommand = new AuthCommand(name);
|
const authCommand = new AuthCommand(name);
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ export class ContextCommand extends Command {
|
|||||||
choices: [
|
choices: [
|
||||||
{ name: '(No brief - organization level)', value: null },
|
{ name: '(No brief - organization level)', value: null },
|
||||||
...briefs.map((brief) => ({
|
...briefs.map((brief) => ({
|
||||||
name: `Brief ${brief.id.slice(0, 8)} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||||
value: brief
|
value: brief
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,18 @@ import {
|
|||||||
} from '@tm/core';
|
} from '@tm/core';
|
||||||
import type { StorageType } from '@tm/core/types';
|
import type { StorageType } from '@tm/core/types';
|
||||||
import * as ui from '../utils/ui.js';
|
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
|
* Options interface for the list command
|
||||||
@@ -173,13 +185,6 @@ export class ListTasksCommand extends Command {
|
|||||||
includeSubtasks: options.withSubtasks
|
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;
|
return result as ListTasksResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,19 +257,16 @@ export class ListTasksCommand extends Command {
|
|||||||
* Display in text format with tables
|
* Display in text format with tables
|
||||||
*/
|
*/
|
||||||
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
|
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
|
||||||
const { tasks, total, filtered, tag, storageType } = data;
|
const { tasks, tag } = data;
|
||||||
|
|
||||||
// Header
|
// Get file path for display
|
||||||
ui.displayBanner(`Task List${tag ? ` (${tag})` : ''}`);
|
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
|
||||||
|
|
||||||
// Statistics
|
// Display header without banner (banner already shown by main CLI)
|
||||||
console.log(chalk.blue.bold('\n📊 Statistics:\n'));
|
displayHeader({
|
||||||
console.log(` Total tasks: ${chalk.cyan(total)}`);
|
tag: tag || 'master',
|
||||||
console.log(` Filtered: ${chalk.cyan(filtered)}`);
|
filePath: filePath
|
||||||
if (tag) {
|
});
|
||||||
console.log(` Tag: ${chalk.cyan(tag)}`);
|
|
||||||
}
|
|
||||||
console.log(` Storage: ${chalk.cyan(storageType)}`);
|
|
||||||
|
|
||||||
// No tasks message
|
// No tasks message
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
@@ -272,21 +274,50 @@ export class ListTasksCommand extends Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task table
|
// Calculate statistics
|
||||||
console.log(chalk.blue.bold(`\n📋 Tasks (${tasks.length}):\n`));
|
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(
|
console.log(
|
||||||
ui.createTaskTable(tasks, {
|
ui.createTaskTable(tasks, {
|
||||||
showSubtasks: withSubtasks,
|
showSubtasks: withSubtasks,
|
||||||
showDependencies: true
|
showDependencies: true,
|
||||||
|
showComplexity: true // Enable complexity column
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Progress bar
|
// Display recommended next task section immediately after table
|
||||||
const completedCount = tasks.filter(
|
if (nextTask) {
|
||||||
(t: Task) => t.status === 'done'
|
// Find the full task object to get description
|
||||||
).length;
|
const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id));
|
||||||
console.log(chalk.blue.bold('\n📊 Overall Progress:\n'));
|
const description = fullTask ? getTaskDescription(fullTask) : undefined;
|
||||||
console.log(` ${ui.createProgressBar(completedCount, tasks.length)}`);
|
|
||||||
|
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;
|
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)
|
* 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
|
* 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 {
|
static register(program: Command, name?: string): ListTasksCommand {
|
||||||
const listCommand = new ListTasksCommand(name);
|
const listCommand = new ListTasksCommand(name);
|
||||||
|
|||||||
318
apps/cli/src/commands/set-status.command.ts
Normal file
318
apps/cli/src/commands/set-status.command.ts
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview SetStatusCommand 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 TaskMasterCore,
|
||||||
|
type TaskStatus
|
||||||
|
} from '@tm/core';
|
||||||
|
import type { StorageType } from '@tm/core/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid task status values for validation
|
||||||
|
*/
|
||||||
|
const VALID_TASK_STATUSES: TaskStatus[] = [
|
||||||
|
'pending',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked',
|
||||||
|
'review'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options interface for the set-status command
|
||||||
|
*/
|
||||||
|
export interface SetStatusCommandOptions {
|
||||||
|
id?: string;
|
||||||
|
status?: TaskStatus;
|
||||||
|
format?: 'text' | 'json';
|
||||||
|
silent?: boolean;
|
||||||
|
project?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result type from set-status command
|
||||||
|
*/
|
||||||
|
export interface SetStatusResult {
|
||||||
|
success: boolean;
|
||||||
|
updatedTasks: Array<{
|
||||||
|
taskId: string;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
}>;
|
||||||
|
storageType: Exclude<StorageType, 'auto'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetStatusCommand extending Commander's Command class
|
||||||
|
* This is a thin presentation layer over @tm/core
|
||||||
|
*/
|
||||||
|
export class SetStatusCommand extends Command {
|
||||||
|
private tmCore?: TaskMasterCore;
|
||||||
|
private lastResult?: SetStatusResult;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name || 'set-status');
|
||||||
|
|
||||||
|
// Configure the command
|
||||||
|
this.description('Update the status of one or more tasks')
|
||||||
|
.requiredOption(
|
||||||
|
'-i, --id <id>',
|
||||||
|
'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)'
|
||||||
|
)
|
||||||
|
.requiredOption(
|
||||||
|
'-s, --status <status>',
|
||||||
|
`New status (${VALID_TASK_STATUSES.join(', ')})`
|
||||||
|
)
|
||||||
|
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||||
|
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||||
|
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||||
|
.action(async (options: SetStatusCommandOptions) => {
|
||||||
|
await this.executeCommand(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the set-status command
|
||||||
|
*/
|
||||||
|
private async executeCommand(
|
||||||
|
options: SetStatusCommandOptions
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Validate required options
|
||||||
|
if (!options.id) {
|
||||||
|
console.error(chalk.red('Error: Task ID is required. Use -i or --id'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.status) {
|
||||||
|
console.error(
|
||||||
|
chalk.red('Error: Status is required. Use -s or --status')
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status
|
||||||
|
if (!VALID_TASK_STATUSES.includes(options.status)) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(
|
||||||
|
`Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize TaskMaster core
|
||||||
|
this.tmCore = await createTaskMasterCore({
|
||||||
|
projectPath: options.project || process.cwd()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse task IDs (handle comma-separated values)
|
||||||
|
const taskIds = options.id.split(',').map((id) => id.trim());
|
||||||
|
|
||||||
|
// Update each task
|
||||||
|
const updatedTasks: Array<{
|
||||||
|
taskId: string;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const taskId of taskIds) {
|
||||||
|
try {
|
||||||
|
const result = await this.tmCore.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
options.status
|
||||||
|
);
|
||||||
|
updatedTasks.push({
|
||||||
|
taskId: result.taskId,
|
||||||
|
oldStatus: result.oldStatus,
|
||||||
|
newStatus: result.newStatus
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.format === 'json') {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
taskId,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store result for potential reuse
|
||||||
|
this.lastResult = {
|
||||||
|
success: true,
|
||||||
|
updatedTasks,
|
||||||
|
storageType: this.tmCore.getStorageType() as Exclude<
|
||||||
|
StorageType,
|
||||||
|
'auto'
|
||||||
|
>
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
this.displayResults(this.lastResult, options);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
console.error(chalk.red(`Error: ${errorMessage}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.format === 'json') {
|
||||||
|
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// Clean up resources
|
||||||
|
if (this.tmCore) {
|
||||||
|
await this.tmCore.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display results based on format
|
||||||
|
*/
|
||||||
|
private displayResults(
|
||||||
|
result: SetStatusResult,
|
||||||
|
options: SetStatusCommandOptions
|
||||||
|
): void {
|
||||||
|
const format = options.format || 'text';
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
if (!options.silent) {
|
||||||
|
this.displayTextResults(result);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display results in text format
|
||||||
|
*/
|
||||||
|
private displayTextResults(result: SetStatusResult): void {
|
||||||
|
if (result.updatedTasks.length === 1) {
|
||||||
|
// Single task update
|
||||||
|
const update = result.updatedTasks[0];
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) +
|
||||||
|
'\n\n' +
|
||||||
|
`${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` +
|
||||||
|
`${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`,
|
||||||
|
{
|
||||||
|
padding: 1,
|
||||||
|
borderColor: 'green',
|
||||||
|
borderStyle: 'round',
|
||||||
|
margin: { top: 1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Multiple task updates
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.white.bold(
|
||||||
|
`✅ Successfully updated ${result.updatedTasks.length} tasks`
|
||||||
|
) +
|
||||||
|
'\n\n' +
|
||||||
|
result.updatedTasks
|
||||||
|
.map(
|
||||||
|
(update) =>
|
||||||
|
`${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}`
|
||||||
|
)
|
||||||
|
.join('\n'),
|
||||||
|
{
|
||||||
|
padding: 1,
|
||||||
|
borderColor: 'green',
|
||||||
|
borderStyle: 'round',
|
||||||
|
margin: { top: 1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show storage info
|
||||||
|
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get colored status display
|
||||||
|
*/
|
||||||
|
private getStatusDisplay(status: TaskStatus): string {
|
||||||
|
const statusColors: Record<TaskStatus, (text: string) => string> = {
|
||||||
|
pending: chalk.yellow,
|
||||||
|
'in-progress': chalk.blue,
|
||||||
|
done: chalk.green,
|
||||||
|
deferred: chalk.gray,
|
||||||
|
cancelled: chalk.red,
|
||||||
|
blocked: chalk.red,
|
||||||
|
review: chalk.magenta,
|
||||||
|
completed: chalk.green
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorFn = statusColors[status] || chalk.white;
|
||||||
|
return colorFn(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last command result (useful for testing or chaining)
|
||||||
|
*/
|
||||||
|
getLastResult(): SetStatusResult | undefined {
|
||||||
|
return this.lastResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 setStatusCommand = new SetStatusCommand();
|
||||||
|
program.addCommand(setStatusCommand);
|
||||||
|
return setStatusCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
|
* Can also configure the command name if needed
|
||||||
|
*/
|
||||||
|
static register(program: Command, name?: string): SetStatusCommand {
|
||||||
|
const setStatusCommand = new SetStatusCommand(name);
|
||||||
|
program.addCommand(setStatusCommand);
|
||||||
|
return setStatusCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create and configure the set-status command
|
||||||
|
*/
|
||||||
|
export function createSetStatusCommand(): SetStatusCommand {
|
||||||
|
return new SetStatusCommand();
|
||||||
|
}
|
||||||
383
apps/cli/src/commands/show.command.ts
Normal file
383
apps/cli/src/commands/show.command.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
@@ -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';
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,10 @@
|
|||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
export { ListTasksCommand } from './commands/list.command.js';
|
export { ListTasksCommand } from './commands/list.command.js';
|
||||||
|
export { ShowCommand } from './commands/show.command.js';
|
||||||
export { AuthCommand } from './commands/auth.command.js';
|
export { AuthCommand } from './commands/auth.command.js';
|
||||||
export { WorkflowCommand } from './commands/workflow.command.js';
|
|
||||||
export { ContextCommand } from './commands/context.command.js';
|
export { ContextCommand } from './commands/context.command.js';
|
||||||
|
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||||
// Command registry
|
|
||||||
export { registerAllCommands } from './commands/index.js';
|
|
||||||
|
|
||||||
// UI utilities (for other commands to use)
|
// UI utilities (for other commands to use)
|
||||||
export * as ui from './utils/ui.js';
|
export * as ui from './utils/ui.js';
|
||||||
|
|||||||
567
apps/cli/src/ui/components/dashboard.component.ts
Normal file
567
apps/cli/src/ui/components/dashboard.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
apps/cli/src/ui/components/header.component.ts
Normal file
45
apps/cli/src/ui/components/header.component.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/cli/src/ui/components/index.ts
Normal file
9
apps/cli/src/ui/components/index.ts
Normal 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';
|
||||||
134
apps/cli/src/ui/components/next-task.component.ts
Normal file
134
apps/cli/src/ui/components/next-task.component.ts
Normal 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;
|
||||||
|
}
|
||||||
31
apps/cli/src/ui/components/suggested-steps.component.ts
Normal file
31
apps/cli/src/ui/components/suggested-steps.component.ts
Normal 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
264
apps/cli/src/ui/components/task-detail.component.ts
Normal file
264
apps/cli/src/ui/components/task-detail.component.ts
Normal 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
9
apps/cli/src/ui/index.ts
Normal 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';
|
||||||
@@ -18,19 +18,44 @@ export function getStatusWithColor(
|
|||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
done: {
|
done: {
|
||||||
color: chalk.green,
|
color: chalk.green,
|
||||||
icon: String.fromCharCode(8730),
|
icon: '✓',
|
||||||
tableIcon: String.fromCharCode(8730)
|
tableIcon: '✓'
|
||||||
}, // √
|
},
|
||||||
pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' },
|
pending: {
|
||||||
|
color: chalk.yellow,
|
||||||
|
icon: '○',
|
||||||
|
tableIcon: '○'
|
||||||
|
},
|
||||||
'in-progress': {
|
'in-progress': {
|
||||||
color: chalk.hex('#FFA500'),
|
color: chalk.hex('#FFA500'),
|
||||||
icon: String.fromCharCode(9654),
|
icon: '▶',
|
||||||
tableIcon: '>'
|
tableIcon: '▶'
|
||||||
}, // ▶
|
},
|
||||||
deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' },
|
deferred: {
|
||||||
blocked: { color: chalk.red, icon: '!', tableIcon: '!' },
|
color: chalk.gray,
|
||||||
review: { color: chalk.magenta, icon: '?', tableIcon: '?' },
|
icon: 'x',
|
||||||
cancelled: { color: chalk.gray, icon: 'X', tableIcon: '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] || {
|
const config = statusConfig[status] || {
|
||||||
@@ -39,18 +64,7 @@ export function getStatusWithColor(
|
|||||||
tableIcon: 'X'
|
tableIcon: 'X'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use simple ASCII characters for stable display
|
const icon = forTable ? config.tableIcon : config.icon;
|
||||||
const simpleIcons = {
|
|
||||||
done: String.fromCharCode(8730), // √
|
|
||||||
pending: 'o',
|
|
||||||
'in-progress': '>',
|
|
||||||
deferred: 'x',
|
|
||||||
blocked: '!',
|
|
||||||
review: '?',
|
|
||||||
cancelled: 'X'
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = forTable ? simpleIcons[status] || 'X' : config.icon;
|
|
||||||
return config.color(`${icon} ${status}`);
|
return config.color(`${icon} ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,10 +259,24 @@ export function createTaskTable(
|
|||||||
} = options || {};
|
} = options || {};
|
||||||
|
|
||||||
// Calculate dynamic column widths based on terminal width
|
// 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
|
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 = [
|
const headers = [
|
||||||
chalk.blue.bold('ID'),
|
chalk.blue.bold('ID'),
|
||||||
@@ -284,11 +312,19 @@ export function createTaskTable(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (showDependencies) {
|
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) {
|
if (showComplexity) {
|
||||||
row.push(getComplexityWithColor(task.complexity as number | string));
|
// Show N/A if no complexity score
|
||||||
|
row.push(chalk.gray('N/A'));
|
||||||
}
|
}
|
||||||
|
|
||||||
table.push(row);
|
table.push(row);
|
||||||
@@ -324,61 +360,3 @@ export function createTaskTable(
|
|||||||
|
|
||||||
return table.toString();
|
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();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "NodeNext",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
"baseUrl": ".",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"resolveJsonModule": true,
|
"strict": true,
|
||||||
"allowJs": false,
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": 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/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "tests"]
|
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { defineConfig } from 'tsup';
|
|
||||||
import { cliConfig, mergeConfig } from '@tm/build-config';
|
|
||||||
|
|
||||||
export default defineConfig(
|
|
||||||
mergeConfig(cliConfig, {
|
|
||||||
entry: ['src/index.ts']
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
# docs
|
# docs
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
## 0.0.1
|
## 0.0.1
|
||||||
|
|||||||
@@ -200,34 +200,6 @@ sidebarTitle: "CLI Commands"
|
|||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="Workflow Management">
|
|
||||||
```bash
|
|
||||||
# Start workflow execution for a task
|
|
||||||
task-master workflow start <task-id>
|
|
||||||
# or use alias
|
|
||||||
task-master workflow run <task-id>
|
|
||||||
|
|
||||||
# List all active workflows
|
|
||||||
task-master workflow list
|
|
||||||
|
|
||||||
# Check status of a specific workflow
|
|
||||||
task-master workflow status <workflow-id>
|
|
||||||
# or use alias
|
|
||||||
task-master workflow info <workflow-id>
|
|
||||||
|
|
||||||
# Stop a running workflow
|
|
||||||
task-master workflow stop <workflow-id>
|
|
||||||
# or use alias
|
|
||||||
task-master workflow kill <workflow-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
The workflow system executes tasks in isolated git worktrees with dedicated Claude Code processes, providing:
|
|
||||||
- **Isolated Execution**: Each task runs in its own git worktree
|
|
||||||
- **Process Management**: Spawns dedicated Claude Code processes
|
|
||||||
- **Real-time Monitoring**: Track progress and output
|
|
||||||
- **Parallel Execution**: Run multiple tasks concurrently
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Initialize a Project">
|
<Accordion title="Initialize a Project">
|
||||||
```bash
|
```bash
|
||||||
# Initialize a new project with Task Master structure
|
# Initialize a new project with Task Master structure
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Workflow Engine"
|
|
||||||
sidebarTitle: "Workflows"
|
|
||||||
---
|
|
||||||
|
|
||||||
The Task Master Workflow Engine provides advanced task execution capabilities with git worktree isolation and Claude Code process management.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The workflow system extends Task Master with powerful execution features:
|
|
||||||
|
|
||||||
- **Git Worktree Isolation**: Each task runs in its own isolated git 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
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Starting a Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start workflow for a specific task
|
|
||||||
task-master workflow start 1.2
|
|
||||||
|
|
||||||
# Using the alias
|
|
||||||
task-master workflow run 1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitoring Workflows
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all active workflows
|
|
||||||
task-master workflow list
|
|
||||||
|
|
||||||
# Check specific workflow status
|
|
||||||
task-master workflow status workflow-1.2-1234567890-abc123
|
|
||||||
|
|
||||||
# Using the alias
|
|
||||||
task-master workflow info workflow-1.2-1234567890-abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stopping Workflows
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop a running workflow
|
|
||||||
task-master workflow stop workflow-1.2-1234567890-abc123
|
|
||||||
|
|
||||||
# Force stop using alias
|
|
||||||
task-master workflow kill workflow-1.2-1234567890-abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow States
|
|
||||||
|
|
||||||
| State | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `pending` | Created but not started |
|
|
||||||
| `initializing` | Setting up worktree and process |
|
|
||||||
| `running` | Active execution in progress |
|
|
||||||
| `paused` | Temporarily stopped |
|
|
||||||
| `completed` | Successfully finished |
|
|
||||||
| `failed` | Error occurred during execution |
|
|
||||||
| `cancelled` | User cancelled the workflow |
|
|
||||||
| `timeout` | Exceeded time limit |
|
|
||||||
|
|
||||||
## Environment Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Set these environment variables to customize workflow behavior:
|
|
||||||
|
|
||||||
- `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
|
|
||||||
|
|
||||||
### Example Configuration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable debug mode
|
|
||||||
export TASKMASTER_WORKFLOW_DEBUG=true
|
|
||||||
|
|
||||||
# Set custom Claude path
|
|
||||||
export TASKMASTER_CLAUDE_PATH=/usr/local/bin/claude
|
|
||||||
|
|
||||||
# Set worktree base directory
|
|
||||||
export TASKMASTER_WORKTREE_BASE=./worktrees
|
|
||||||
|
|
||||||
# Limit concurrent workflows
|
|
||||||
export TASKMASTER_MAX_CONCURRENT=3
|
|
||||||
```
|
|
||||||
|
|
||||||
## Git Worktree Integration
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
When you start a workflow:
|
|
||||||
|
|
||||||
1. **Worktree Creation**: A new git worktree is created for the task
|
|
||||||
2. **Process Spawn**: A dedicated Claude Code process is launched in the worktree
|
|
||||||
3. **Task Execution**: The task runs in complete isolation
|
|
||||||
4. **State Tracking**: Progress is monitored and persisted
|
|
||||||
5. **Cleanup**: Worktree is removed when workflow completes
|
|
||||||
|
|
||||||
### Worktree Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
project/
|
|
||||||
├── .git/ # Main repository
|
|
||||||
├── src/ # Main working directory
|
|
||||||
└── worktrees/ # Workflow worktrees
|
|
||||||
├── task-1.2/ # Worktree for task 1.2
|
|
||||||
├── task-2.1/ # Worktree for task 2.1
|
|
||||||
└── task-3.4/ # Worktree for task 3.4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### When to Use Workflows
|
|
||||||
|
|
||||||
Use workflows for tasks that:
|
|
||||||
|
|
||||||
- Require isolated development environments
|
|
||||||
- Need dedicated Claude Code attention
|
|
||||||
- Benefit from parallel execution
|
|
||||||
- Require process monitoring and state tracking
|
|
||||||
|
|
||||||
### Workflow Management
|
|
||||||
|
|
||||||
- **Start workflows for complex tasks** that need focused execution
|
|
||||||
- **Monitor progress** using `workflow status` command
|
|
||||||
- **Clean up completed workflows** to free resources
|
|
||||||
- **Use meaningful task descriptions** for better workflow tracking
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
|
|
||||||
- **Limit concurrent workflows** based on system resources
|
|
||||||
- **Monitor workflow output** for debugging and progress tracking
|
|
||||||
- **Stop unnecessary workflows** to free up resources
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Worktree Creation Fails**
|
|
||||||
```bash
|
|
||||||
# Check git version (requires 2.5+)
|
|
||||||
git --version
|
|
||||||
|
|
||||||
# Verify project is a git repository
|
|
||||||
git status
|
|
||||||
```
|
|
||||||
|
|
||||||
**Claude Code Not Found**
|
|
||||||
```bash
|
|
||||||
# Check Claude installation
|
|
||||||
which claude
|
|
||||||
|
|
||||||
# Set custom path
|
|
||||||
export TASKMASTER_CLAUDE_PATH=/path/to/claude
|
|
||||||
```
|
|
||||||
|
|
||||||
**Permission Errors**
|
|
||||||
```bash
|
|
||||||
# Check worktree directory permissions
|
|
||||||
chmod -R 755 ./worktrees
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Mode
|
|
||||||
|
|
||||||
Enable debug logging for troubleshooting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export TASKMASTER_WORKFLOW_DEBUG=true
|
|
||||||
task-master workflow start 1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Examples
|
|
||||||
|
|
||||||
### With 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
|
|
||||||
|
|
||||||
### With Task Management
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Typical workflow
|
|
||||||
task-master next # Find next task
|
|
||||||
task-master workflow start 1.2 # Start workflow
|
|
||||||
task-master workflow status <id> # Monitor progress
|
|
||||||
task-master set-status --id=1.2 --status=done # Mark complete
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Features
|
|
||||||
|
|
||||||
### Parallel Execution
|
|
||||||
|
|
||||||
Run multiple workflows simultaneously:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start multiple workflows
|
|
||||||
task-master workflow start 1.2
|
|
||||||
task-master workflow start 2.1
|
|
||||||
task-master workflow start 3.4
|
|
||||||
|
|
||||||
# Monitor all active workflows
|
|
||||||
task-master workflow list
|
|
||||||
```
|
|
||||||
|
|
||||||
### Process Monitoring
|
|
||||||
|
|
||||||
Each workflow provides real-time output monitoring and process management through the workflow engine's event system.
|
|
||||||
|
|
||||||
### State Persistence
|
|
||||||
|
|
||||||
Workflow state is automatically persisted across sessions, allowing you to resume monitoring workflows after restarting the CLI.
|
|
||||||
@@ -49,7 +49,6 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"capabilities/mcp",
|
"capabilities/mcp",
|
||||||
"capabilities/cli-root-commands",
|
"capabilities/cli-root-commands",
|
||||||
"capabilities/workflows",
|
|
||||||
"capabilities/task-structure"
|
"capabilities/task-structure"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "docs",
|
"name": "docs",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Task Master documentation powered by Mintlify",
|
"description": "Task Master documentation powered by Mintlify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "mintlify dev",
|
"dev": "mintlify dev",
|
||||||
"build": "mintlify build",
|
|
||||||
"preview": "mintlify preview"
|
"preview": "mintlify preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mintlify": "^4.0.0"
|
"mintlify": "^4.2.111"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,38 +3,4 @@ title: "What's New"
|
|||||||
sidebarTitle: "What's New"
|
sidebarTitle: "What's New"
|
||||||
---
|
---
|
||||||
|
|
||||||
## New Workflow Engine (Latest)
|
|
||||||
|
|
||||||
Task Master now includes a powerful workflow engine that revolutionizes how tasks are executed:
|
|
||||||
|
|
||||||
### 🚀 Key Features
|
|
||||||
|
|
||||||
- **Git Worktree Isolation**: Each task runs in its own isolated git worktree
|
|
||||||
- **Claude Code Integration**: Spawns dedicated Claude Code processes for task execution
|
|
||||||
- **Real-time Monitoring**: Track workflow progress and process output
|
|
||||||
- **Parallel Execution**: Run multiple tasks concurrently with resource management
|
|
||||||
- **State Persistence**: Workflow state is maintained across sessions
|
|
||||||
|
|
||||||
### 🔧 New CLI Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start workflow execution
|
|
||||||
task-master workflow start <task-id>
|
|
||||||
|
|
||||||
# Monitor active workflows
|
|
||||||
task-master workflow list
|
|
||||||
|
|
||||||
# Check workflow status
|
|
||||||
task-master workflow status <workflow-id>
|
|
||||||
|
|
||||||
# Stop running workflow
|
|
||||||
task-master workflow stop <workflow-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📖 Learn More
|
|
||||||
|
|
||||||
Check out the new [Workflow Documentation](/capabilities/workflows) for comprehensive usage guides and best practices.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
An easy way to see the latest releases
|
An easy way to see the latest releases
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# Change Log
|
# 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
|
## 0.24.2-rc.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ async function main() {
|
|||||||
// This prevents the multiple React instances issue
|
// This prevents the multiple React instances issue
|
||||||
// Ensure React is resolved from the workspace root to avoid duplicates
|
// Ensure React is resolved from the workspace root to avoid duplicates
|
||||||
alias: {
|
alias: {
|
||||||
react: path.resolve(__dirname, 'node_modules/react'),
|
react: path.resolve(__dirname, '../../node_modules/react'),
|
||||||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
|
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
||||||
@@ -135,8 +135,8 @@ async function main() {
|
|||||||
jsxImportSource: 'react',
|
jsxImportSource: 'react',
|
||||||
external: ['*.css'],
|
external: ['*.css'],
|
||||||
alias: {
|
alias: {
|
||||||
react: path.resolve(__dirname, 'node_modules/react'),
|
react: path.resolve(__dirname, '../../node_modules/react'),
|
||||||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
|
'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.24.2-rc.1",
|
"version": "0.24.2",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -229,6 +229,7 @@
|
|||||||
"build": "npm run build:js && npm run build:css",
|
"build": "npm run build:js && npm run build:css",
|
||||||
"build:js": "node ./esbuild.js --production",
|
"build:js": "node ./esbuild.js --production",
|
||||||
"build:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --minify",
|
"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": "npm exec node ./package.mjs",
|
||||||
"package:direct": "node ./package.mjs",
|
"package:direct": "node ./package.mjs",
|
||||||
"debug:env": "node ./debug-env.mjs",
|
"debug:env": "node ./debug-env.mjs",
|
||||||
@@ -239,7 +240,7 @@
|
|||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.26.0-rc.1"
|
"task-master-ai": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2, Play } from 'lucide-react';
|
||||||
import { PriorityBadge } from './PriorityBadge';
|
import { PriorityBadge } from './PriorityBadge';
|
||||||
import type { TaskMasterTask } from '../../webview/types';
|
import type { TaskMasterTask } from '../../webview/types';
|
||||||
|
import { useVSCodeContext } from '../../webview/contexts/VSCodeContext';
|
||||||
|
|
||||||
interface TaskMetadataSidebarProps {
|
interface TaskMetadataSidebarProps {
|
||||||
currentTask: TaskMasterTask;
|
currentTask: TaskMasterTask;
|
||||||
@@ -28,10 +29,12 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
|||||||
isRegenerating = false,
|
isRegenerating = false,
|
||||||
isAppending = false
|
isAppending = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const { vscode } = useVSCodeContext();
|
||||||
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
|
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
|
||||||
const [mcpComplexityScore, setMcpComplexityScore] = useState<
|
const [mcpComplexityScore, setMcpComplexityScore] = useState<
|
||||||
number | undefined
|
number | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
const [isStartingTask, setIsStartingTask] = useState(false);
|
||||||
|
|
||||||
// Get complexity score from task
|
// Get complexity score from task
|
||||||
const currentComplexityScore = complexity?.score;
|
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
|
// Effect to handle complexity on task change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentTask?.id) {
|
if (currentTask?.id) {
|
||||||
@@ -284,6 +310,30 @@ export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
|
|||||||
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
|
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
|
||||||
<div className="border-b border-textSeparator-foreground" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -361,6 +361,30 @@ export class WebviewManager {
|
|||||||
}
|
}
|
||||||
return;
|
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:
|
default:
|
||||||
throw new Error(`Unknown message type: ${type}`);
|
throw new Error(`Unknown message type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,357 +20,8 @@
|
|||||||
* Main entry point for globally installed package
|
* Main entry point for globally installed package
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
// Direct imports instead of spawning child processes
|
||||||
import { dirname, resolve } from 'path';
|
import { runCLI } from '../scripts/modules/commands.js';
|
||||||
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';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
// Simply run the CLI directly
|
||||||
const __dirname = dirname(__filename);
|
runCLI();
|
||||||
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 };
|
|
||||||
|
|||||||
@@ -18,7 +18,17 @@ export default {
|
|||||||
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
|
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
|
||||||
|
|
||||||
// Transform files
|
// 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
|
// Disable transformations for node_modules
|
||||||
transformIgnorePatterns: ['/node_modules/'],
|
transformIgnorePatterns: ['/node_modules/'],
|
||||||
@@ -27,6 +37,7 @@ export default {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@/(.*)$': '<rootDir>/$1'
|
'^@/(.*)$': '<rootDir>/$1'
|
||||||
},
|
},
|
||||||
|
resolver: '<rootDir>/jest.resolver.cjs',
|
||||||
|
|
||||||
// Setup module aliases
|
// Setup module aliases
|
||||||
moduleDirectories: ['node_modules', '<rootDir>'],
|
moduleDirectories: ['node_modules', '<rootDir>'],
|
||||||
|
|||||||
19
jest.resolver.cjs
Normal file
19
jest.resolver.cjs
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
40
output.txt
40
output.txt
File diff suppressed because one or more lines are too long
5280
package-lock.json
generated
5280
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"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.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,19 +11,12 @@
|
|||||||
},
|
},
|
||||||
"workspaces": ["apps/*", "packages/*", "."],
|
"workspaces": ["apps/*", "packages/*", "."],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:packages && tsup",
|
"build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
|
||||||
"dev": "npm run build:packages && npm link && (npm run dev:packages & tsup --watch --onSuccess 'echo Build complete && npm link')",
|
"dev": "tsdown --watch",
|
||||||
"dev:packages": "(cd packages/tm-core && npm run dev) & (cd packages/workflow-engine && npm run dev) & (cd apps/cli && npm run dev) & wait",
|
"turbo:dev": "turbo dev",
|
||||||
"dev:core": "cd packages/tm-core && npm run dev",
|
"turbo:build": "turbo build",
|
||||||
"dev:workflow": "cd packages/workflow-engine && npm run dev",
|
"turbo:typecheck": "turbo typecheck",
|
||||||
"dev:cli": "cd apps/cli && npm run dev",
|
"build:build-config": "npm run build -w @tm/build-config",
|
||||||
"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",
|
|
||||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||||
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
|
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
|
||||||
"test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration",
|
"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",
|
"postpack": "chmod +x dist/task-master.js dist/mcp-server.js",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"release": "changeset publish",
|
"release": "changeset publish",
|
||||||
|
"publish-packages": "turbo run build lint test && changeset version && changeset publish",
|
||||||
"inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js",
|
"inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js",
|
||||||
"mcp-server": "node dist/mcp-server.js",
|
"mcp-server": "node dist/mcp-server.js",
|
||||||
"format-check": "biome format .",
|
"format-check": "biome format .",
|
||||||
@@ -71,6 +65,7 @@
|
|||||||
"@inquirer/search": "^3.0.15",
|
"@inquirer/search": "^3.0.15",
|
||||||
"@openrouter/ai-sdk-provider": "^0.4.5",
|
"@openrouter/ai-sdk-provider": "^0.4.5",
|
||||||
"@streamparser/json": "^0.0.22",
|
"@streamparser/json": "^0.0.22",
|
||||||
|
"@tm/cli": "*",
|
||||||
"ai": "^4.3.10",
|
"ai": "^4.3.10",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
@@ -94,6 +89,8 @@
|
|||||||
"jsonrepair": "^3.13.0",
|
"jsonrepair": "^3.13.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.2.0",
|
||||||
|
"marked": "^15.0.12",
|
||||||
|
"marked-terminal": "^7.3.0",
|
||||||
"ollama-ai-provider": "^1.2.0",
|
"ollama-ai-provider": "^1.2.0",
|
||||||
"openai": "^4.89.0",
|
"openai": "^4.89.0",
|
||||||
"ora": "^8.2.0",
|
"ora": "^8.2.0",
|
||||||
@@ -109,6 +106,7 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
|
"packageManager": "npm@10.9.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/eyaltoledano/claude-task-master.git"
|
"url": "git+https://github.com/eyaltoledano/claude-task-master.git"
|
||||||
@@ -124,21 +122,23 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
|
|
||||||
"@changesets/changelog-github": "^0.5.1",
|
"@changesets/changelog-github": "^0.5.1",
|
||||||
"@changesets/cli": "^2.28.1",
|
"@changesets/cli": "^2.28.1",
|
||||||
"dotenv-mono": "^1.5.1",
|
|
||||||
|
|
||||||
"@types/jest": "^29.5.14",
|
"@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",
|
"execa": "^8.0.1",
|
||||||
"ink": "^5.0.1",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-node": "^29.7.0",
|
"jest-environment-node": "^29.7.0",
|
||||||
"mock-fs": "^5.5.0",
|
"mock-fs": "^5.5.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"supertest": "^7.1.0",
|
"supertest": "^7.1.0",
|
||||||
"tsup": "^8.5.0",
|
"ts-jest": "^29.4.2",
|
||||||
|
"tsdown": "^0.15.2",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.16.2",
|
||||||
|
"turbo": "^2.5.6",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Shared build configuration for Task Master monorepo",
|
"description": "Shared build configuration for Task Master monorepo",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/tsup.base.js",
|
"private": true,
|
||||||
"types": "./dist/tsup.base.d.ts",
|
"main": "./dist/tsdown.base.js",
|
||||||
|
"types": "./src/tsdown.base.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./src/tsup.base.ts",
|
"types": "./src/tsdown.base.ts",
|
||||||
"import": "./dist/tsup.base.js",
|
"import": "./dist/tsdown.base.js"
|
||||||
"require": "./dist/tsup.base.cjs"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": ["dist", "src"],
|
"files": ["dist", "src"],
|
||||||
@@ -17,15 +17,14 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsc",
|
||||||
"dev": "tsup --watch",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsup": "^8.5.0",
|
"dotenv-mono": "^1.5.1",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"dependencies": {
|
||||||
"tsup": "^8.0.0"
|
"tsup": "^8.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
packages/build-config/src/tsdown.base.ts
Normal file
46
packages/build-config/src/tsdown.base.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'
|
|
||||||
};
|
|
||||||
@@ -6,9 +6,10 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,67 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/core",
|
"name": "@tm/core",
|
||||||
"version": "1.0.0",
|
"version": "0.26.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Core library for Task Master - TypeScript task management system",
|
"description": "Core library for Task Master - TypeScript task management system",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"./auth": "./src/auth/index.ts",
|
||||||
"import": "./dist/index.js"
|
"./storage": "./src/storage/index.ts",
|
||||||
},
|
"./config": "./src/config/index.ts",
|
||||||
"./auth": {
|
"./providers": "./src/providers/index.ts",
|
||||||
"types": "./src/auth/index.ts",
|
"./services": "./src/services/index.ts",
|
||||||
"import": "./dist/auth/index.js"
|
"./errors": "./src/errors/index.ts",
|
||||||
},
|
"./logger": "./src/logger/index.ts",
|
||||||
"./storage": {
|
"./types": "./src/types/index.ts",
|
||||||
"types": "./src/storage/index.ts",
|
"./interfaces": "./src/interfaces/index.ts",
|
||||||
"import": "./dist/storage/index.js"
|
"./utils": "./src/utils/index.ts"
|
||||||
},
|
|
||||||
"./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"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
|
||||||
"dev": "tsup --watch",
|
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
@@ -73,9 +31,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.57.0",
|
"@supabase/supabase-js": "^2.57.4",
|
||||||
"@tm/workflow-engine": "*",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -83,15 +39,16 @@
|
|||||||
"@tm/build-config": "*",
|
"@tm/build-config": "*",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@vitest/coverage-v8": "^2.0.5",
|
"@vitest/coverage-v8": "^2.0.5",
|
||||||
|
"dotenv-mono": "^1.3.14",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsup": "^8.0.2",
|
"tsup": "^8.5.0",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.3",
|
||||||
"vitest": "^2.0.5"
|
"vitest": "^2.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||||
"author": "Task Master AI",
|
"author": "Task Master AI",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class AuthManager {
|
|||||||
private organizationService?: OrganizationService;
|
private organizationService?: OrganizationService;
|
||||||
|
|
||||||
private constructor(config?: Partial<AuthConfig>) {
|
private constructor(config?: Partial<AuthConfig>) {
|
||||||
this.credentialStore = new CredentialStore(config);
|
this.credentialStore = CredentialStore.getInstance(config);
|
||||||
this.supabaseClient = new SupabaseAuthClient();
|
this.supabaseClient = new SupabaseAuthClient();
|
||||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||||
|
|
||||||
@@ -73,6 +73,7 @@ export class AuthManager {
|
|||||||
*/
|
*/
|
||||||
static resetInstance(): void {
|
static resetInstance(): void {
|
||||||
AuthManager.instance = null;
|
AuthManager.instance = null;
|
||||||
|
CredentialStore.resetInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,15 +19,39 @@ import { getLogger } from '../logger/index.js';
|
|||||||
* human-readable persisted format in the auth.json file.
|
* human-readable persisted format in the auth.json file.
|
||||||
*/
|
*/
|
||||||
export class CredentialStore {
|
export class CredentialStore {
|
||||||
|
private static instance: CredentialStore | null = null;
|
||||||
private logger = getLogger('CredentialStore');
|
private logger = getLogger('CredentialStore');
|
||||||
private config: AuthConfig;
|
private config: AuthConfig;
|
||||||
// Clock skew tolerance for expiry checks (30 seconds)
|
// Clock skew tolerance for expiry checks (30 seconds)
|
||||||
private readonly CLOCK_SKEW_MS = 30_000;
|
private readonly CLOCK_SKEW_MS = 30_000;
|
||||||
|
|
||||||
constructor(config?: Partial<AuthConfig>) {
|
private constructor(config?: Partial<AuthConfig>) {
|
||||||
this.config = getAuthConfig(config);
|
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
|
* Get stored authentication credentials
|
||||||
* @returns AuthCredentials with expiresAt as number (milliseconds) for runtime use
|
* @returns AuthCredentials with expiresAt as number (milliseconds) for runtime use
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
export { AuthManager } from './auth-manager.js';
|
export { AuthManager } from './auth-manager.js';
|
||||||
export { CredentialStore } from './credential-store.js';
|
export { CredentialStore } from './credential-store.js';
|
||||||
export { OAuthService } from './oauth-service.js';
|
export { OAuthService } from './oauth-service.js';
|
||||||
export { SupabaseSessionStorage } from './supabase-session-storage';
|
export { SupabaseSessionStorage } from './supabase-session-storage.js';
|
||||||
export type {
|
export type {
|
||||||
Organization,
|
Organization,
|
||||||
Brief,
|
Brief,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SupportedStorage } from '@supabase/supabase-js';
|
import { SupportedStorage } from '@supabase/supabase-js';
|
||||||
import { CredentialStore } from './credential-store';
|
import { CredentialStore } from './credential-store.js';
|
||||||
import { AuthCredentials } from './types';
|
import { AuthCredentials } from './types.js';
|
||||||
import { getLogger } from '../logger';
|
import { getLogger } from '../logger/index.js';
|
||||||
|
|
||||||
const STORAGE_KEY = 'sb-taskmaster-auth-token';
|
const STORAGE_KEY = 'sb-taskmaster-auth-token';
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
} from '@supabase/supabase-js';
|
} from '@supabase/supabase-js';
|
||||||
import { AuthenticationError } from '../auth/types.js';
|
import { AuthenticationError } from '../auth/types.js';
|
||||||
import { getLogger } from '../logger/index.js';
|
import { getLogger } from '../logger/index.js';
|
||||||
import { SupabaseSessionStorage } from '../auth/supabase-session-storage';
|
import { SupabaseSessionStorage } from '../auth/supabase-session-storage.js';
|
||||||
import { CredentialStore } from '../auth/credential-store';
|
import { CredentialStore } from '../auth/credential-store.js';
|
||||||
|
|
||||||
export class SupabaseAuthClient {
|
export class SupabaseAuthClient {
|
||||||
private client: SupabaseJSClient | null = null;
|
private client: SupabaseJSClient | null = null;
|
||||||
@@ -19,7 +19,7 @@ export class SupabaseAuthClient {
|
|||||||
private logger = getLogger('SupabaseAuthClient');
|
private logger = getLogger('SupabaseAuthClient');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const credentialStore = new CredentialStore();
|
const credentialStore = CredentialStore.getInstance();
|
||||||
this.sessionStorage = new SupabaseSessionStorage(credentialStore);
|
this.sessionStorage = new SupabaseSessionStorage(credentialStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|||||||
*/
|
*/
|
||||||
export const STATUS_ICONS: Record<TaskStatus, string> = {
|
export const STATUS_ICONS: Record<TaskStatus, string> = {
|
||||||
done: '✓',
|
done: '✓',
|
||||||
|
completed: '✓',
|
||||||
'in-progress': '►',
|
'in-progress': '►',
|
||||||
blocked: '⭕',
|
blocked: '⭕',
|
||||||
pending: '○',
|
pending: '○',
|
||||||
@@ -71,5 +72,6 @@ export const STATUS_COLORS: Record<TaskStatus, string> = {
|
|||||||
deferred: 'gray',
|
deferred: 'gray',
|
||||||
cancelled: 'red',
|
cancelled: 'red',
|
||||||
blocked: 'magenta',
|
blocked: 'magenta',
|
||||||
review: 'cyan'
|
review: 'cyan',
|
||||||
|
completed: 'green'
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -55,7 +55,3 @@ export {
|
|||||||
|
|
||||||
// Re-export logger
|
// Re-export logger
|
||||||
export { getLogger, createLogger, setGlobalLogger } from './logger/index.js';
|
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';
|
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export interface IStorage {
|
|||||||
*/
|
*/
|
||||||
loadTasks(tag?: string): Promise<Task[]>;
|
loadTasks(tag?: string): Promise<Task[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a single task by ID
|
||||||
|
* @param taskId - ID of the task to load
|
||||||
|
* @param tag - Optional tag context for the task
|
||||||
|
* @returns Promise that resolves to the task or null if not found
|
||||||
|
*/
|
||||||
|
loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save tasks to storage, replacing existing tasks
|
* Save tasks to storage, replacing existing tasks
|
||||||
* @param tasks - Array of tasks to save
|
* @param tasks - Array of tasks to save
|
||||||
@@ -175,6 +183,7 @@ export abstract class BaseStorage implements IStorage {
|
|||||||
|
|
||||||
// Abstract methods that must be implemented by concrete classes
|
// Abstract methods that must be implemented by concrete classes
|
||||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||||
|
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
abstract updateTask(
|
abstract updateTask(
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class TaskMapper {
|
|||||||
/**
|
/**
|
||||||
* Maps database status to internal status
|
* Maps database status to internal status
|
||||||
*/
|
*/
|
||||||
private static mapStatus(
|
static mapStatus(
|
||||||
status: Database['public']['Enums']['task_status']
|
status: Database['public']['Enums']['task_status']
|
||||||
): Task['status'] {
|
): Task['status'] {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -3,6 +3,30 @@ import { Task } from '../types/index.js';
|
|||||||
import { Database } from '../types/database.types.js';
|
import { Database } from '../types/database.types.js';
|
||||||
import { TaskMapper } from '../mappers/TaskMapper.js';
|
import { TaskMapper } from '../mappers/TaskMapper.js';
|
||||||
import { AuthManager } from '../auth/auth-manager.js';
|
import { AuthManager } from '../auth/auth-manager.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Zod schema for task status validation
|
||||||
|
const TaskStatusSchema = z.enum([
|
||||||
|
'pending',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'review',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zod schema for task updates
|
||||||
|
const TaskUpdateSchema = z
|
||||||
|
.object({
|
||||||
|
title: z.string().min(1).optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
status: TaskStatusSchema.optional(),
|
||||||
|
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||||
|
details: z.string().optional(),
|
||||||
|
testStrategy: z.string().optional()
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
export class SupabaseTaskRepository {
|
export class SupabaseTaskRepository {
|
||||||
constructor(private supabase: SupabaseClient<Database>) {}
|
constructor(private supabase: SupabaseClient<Database>) {}
|
||||||
@@ -60,12 +84,22 @@ export class SupabaseTaskRepository {
|
|||||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTask(accountId: string, taskId: string): Promise<Task | null> {
|
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||||
|
// Get the current context to determine briefId (projectId not used in Supabase context)
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
const context = authManager.getContext();
|
||||||
|
|
||||||
|
if (!context || !context.briefId) {
|
||||||
|
throw new Error(
|
||||||
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('account_id', accountId)
|
.eq('brief_id', context.briefId)
|
||||||
.eq('id', taskId)
|
.eq('display_id', taskId.toUpperCase())
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -107,4 +141,85 @@ export class SupabaseTaskRepository {
|
|||||||
dependenciesByTaskId
|
dependenciesByTaskId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateTask(
|
||||||
|
projectId: string,
|
||||||
|
taskId: string,
|
||||||
|
updates: Partial<Task>
|
||||||
|
): Promise<Task> {
|
||||||
|
|
||||||
|
// Get the current context to determine briefId
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
const context = authManager.getContext();
|
||||||
|
|
||||||
|
if (!context || !context.briefId) {
|
||||||
|
throw new Error(
|
||||||
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate updates using Zod schema
|
||||||
|
try {
|
||||||
|
TaskUpdateSchema.parse(updates);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
const errorMessages = error.errors
|
||||||
|
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||||
|
.join(', ');
|
||||||
|
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Task fields to database fields - only include fields that actually exist in the database
|
||||||
|
const dbUpdates: any = {};
|
||||||
|
|
||||||
|
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||||
|
if (updates.description !== undefined)
|
||||||
|
dbUpdates.description = updates.description;
|
||||||
|
if (updates.status !== undefined)
|
||||||
|
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||||
|
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
|
||||||
|
// Skip fields that don't exist in database schema: details, testStrategy, etc.
|
||||||
|
|
||||||
|
// Update the task
|
||||||
|
const { error } = await this.supabase
|
||||||
|
.from('tasks')
|
||||||
|
.update(dbUpdates)
|
||||||
|
.eq('brief_id', context.briefId)
|
||||||
|
.eq('display_id', taskId.toUpperCase());
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Failed to update task: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the updated task by fetching it
|
||||||
|
const updatedTask = await this.getTask(projectId, taskId);
|
||||||
|
if (!updatedTask) {
|
||||||
|
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps internal status to database status
|
||||||
|
*/
|
||||||
|
private mapStatusToDatabase(
|
||||||
|
status: string
|
||||||
|
): Database['public']['Enums']['task_status'] {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'todo';
|
||||||
|
case 'in-progress':
|
||||||
|
case 'in_progress': // Accept both formats
|
||||||
|
return 'in_progress';
|
||||||
|
case 'done':
|
||||||
|
return 'done';
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,4 +360,74 @@ export class TaskService {
|
|||||||
async setActiveTag(tag: string): Promise<void> {
|
async setActiveTag(tag: string): Promise<void> {
|
||||||
await this.configManager.setActiveTag(tag);
|
await this.configManager.setActiveTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task status
|
||||||
|
*/
|
||||||
|
async updateTaskStatus(
|
||||||
|
taskId: string | number,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
taskId: string;
|
||||||
|
}> {
|
||||||
|
|
||||||
|
// Ensure we have storage
|
||||||
|
if (!this.storage) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
'Storage not initialized',
|
||||||
|
ERROR_CODES.STORAGE_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use provided tag or get active tag
|
||||||
|
const activeTag = tag || this.getActiveTag();
|
||||||
|
|
||||||
|
const taskIdStr = String(taskId);
|
||||||
|
|
||||||
|
// TODO: For now, assume it's a regular task and just try to update directly
|
||||||
|
// In the future, we can add subtask support if needed
|
||||||
|
if (taskIdStr.includes('.')) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
'Subtask status updates not yet supported in API storage',
|
||||||
|
ERROR_CODES.NOT_IMPLEMENTED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current task to get old status (simple, direct approach)
|
||||||
|
let currentTask: Task | null;
|
||||||
|
try {
|
||||||
|
// Try to get the task directly
|
||||||
|
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
`Failed to load task ${taskIdStr}`,
|
||||||
|
ERROR_CODES.TASK_NOT_FOUND,
|
||||||
|
{ taskId: taskIdStr },
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentTask) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
`Task ${taskIdStr} not found`,
|
||||||
|
ERROR_CODES.TASK_NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStatus = currentTask.status;
|
||||||
|
|
||||||
|
// Simple, direct update - just change the status
|
||||||
|
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
oldStatus,
|
||||||
|
newStatus,
|
||||||
|
taskId: taskIdStr
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,14 +223,6 @@ export class ApiStorage implements IStorage {
|
|||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tag) {
|
|
||||||
// Check if task is in tag
|
|
||||||
const tagData = this.tagsCache.get(tag);
|
|
||||||
if (!tagData || !tagData.tasks.includes(taskId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.retryOperation(() =>
|
return await this.retryOperation(() =>
|
||||||
this.repository.getTask(this.projectId, taskId)
|
this.repository.getTask(this.projectId, taskId)
|
||||||
);
|
);
|
||||||
@@ -477,6 +469,7 @@ export class ApiStorage implements IStorage {
|
|||||||
updates: Partial<Task>,
|
updates: Partial<Task>,
|
||||||
tag?: string
|
tag?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ export class FileStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a single task by ID from the tasks.json file
|
||||||
|
*/
|
||||||
|
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||||
|
const tasks = await this.loadTasks(tag);
|
||||||
|
return tasks.find(task => task.id === taskId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save tasks for a specific tag in the single tasks.json file
|
* Save tasks for a specific tag in the single tasks.json file
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ import type {
|
|||||||
TaskFilter,
|
TaskFilter,
|
||||||
StorageType
|
StorageType
|
||||||
} from './types/index.js';
|
} from './types/index.js';
|
||||||
import {
|
|
||||||
WorkflowService,
|
|
||||||
type WorkflowServiceConfig
|
|
||||||
} from './workflow/index.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating TaskMasterCore instance
|
* Options for creating TaskMasterCore instance
|
||||||
@@ -27,7 +23,6 @@ import {
|
|||||||
export interface TaskMasterCoreOptions {
|
export interface TaskMasterCoreOptions {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
configuration?: Partial<IConfiguration>;
|
configuration?: Partial<IConfiguration>;
|
||||||
workflow?: Partial<WorkflowServiceConfig>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +38,6 @@ export type { GetTaskListOptions } from './services/task-service.js';
|
|||||||
export class TaskMasterCore {
|
export class TaskMasterCore {
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private taskService: TaskService;
|
private taskService: TaskService;
|
||||||
private workflowService: WorkflowService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and initialize a new TaskMasterCore instance
|
* Create and initialize a new TaskMasterCore instance
|
||||||
@@ -66,7 +60,6 @@ export class TaskMasterCore {
|
|||||||
// Services will be initialized in the initialize() method
|
// Services will be initialized in the initialize() method
|
||||||
this.configManager = null as any;
|
this.configManager = null as any;
|
||||||
this.taskService = null as any;
|
this.taskService = null as any;
|
||||||
this.workflowService = null as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,28 +86,6 @@ export class TaskMasterCore {
|
|||||||
// Create task service
|
// Create task service
|
||||||
this.taskService = new TaskService(this.configManager);
|
this.taskService = new TaskService(this.configManager);
|
||||||
await this.taskService.initialize();
|
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) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
'Failed to initialize TaskMasterCore',
|
'Failed to initialize TaskMasterCore',
|
||||||
@@ -205,10 +176,19 @@ export class TaskMasterCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get workflow service for workflow operations
|
* Update task status
|
||||||
*/
|
*/
|
||||||
get workflow(): WorkflowService {
|
async updateTaskStatus(
|
||||||
return this.workflowService;
|
taskId: string | number,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
taskId: string;
|
||||||
|
}> {
|
||||||
|
return this.taskService.updateTaskStatus(taskId, newStatus, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,9 +196,6 @@ export class TaskMasterCore {
|
|||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
// TaskService handles storage cleanup internally
|
// TaskService handles storage cleanup internally
|
||||||
if (this.workflowService) {
|
|
||||||
await this.workflowService.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export type TaskStatus =
|
|||||||
| 'deferred'
|
| 'deferred'
|
||||||
| 'cancelled'
|
| 'cancelled'
|
||||||
| 'blocked'
|
| 'blocked'
|
||||||
| 'review';
|
| 'review'
|
||||||
|
| 'completed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task priority levels
|
* Task priority levels
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "NodeNext",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
@@ -24,11 +24,12 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "NodeNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||||
|
|||||||
@@ -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'];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Workflow Engine Errors
|
|
||||||
* Public error exports
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './workflow.errors.js';
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Process Management
|
|
||||||
* Public exports for process operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './process-sandbox.js';
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview State Management
|
|
||||||
* Public exports for workflow state operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './workflow-state-manager.js';
|
|
||||||
@@ -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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Task Execution Management
|
|
||||||
* Public exports for task execution operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './task-execution-manager.js';
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Workflow Engine Types
|
|
||||||
* Public type exports
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './workflow.types.js';
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Worktree Management
|
|
||||||
* Public exports for worktree operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './worktree-manager.js';
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user