@lumenflow/cli 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # @lumenflow/cli
2
2
 
3
- Command-line interface for LumenFlow workflow management.
3
+ [![npm version](https://img.shields.io/npm/v/@lumenflow/cli.svg)](https://www.npmjs.com/package/@lumenflow/cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@lumenflow/cli.svg)](https://www.npmjs.com/package/@lumenflow/cli)
5
+ [![license](https://img.shields.io/npm/l/@lumenflow/cli.svg)](https://github.com/hellmai/os/blob/main/LICENSE)
6
+ [![node](https://img.shields.io/node/v/@lumenflow/cli.svg)](https://nodejs.org)
7
+
8
+ > Command-line interface for LumenFlow workflow framework
4
9
 
5
10
  ## Installation
6
11
 
@@ -105,6 +110,23 @@ npx wu-claim --id WU-123 --lane operations
105
110
  npx gates
106
111
  ```
107
112
 
113
+ ## Global Flags
114
+
115
+ All commands support these flags:
116
+
117
+ | Flag | Description |
118
+ | ----------------- | ------------------------- |
119
+ | `--help`, `-h` | Show help for the command |
120
+ | `--version`, `-V` | Show version number |
121
+ | `--no-color` | Disable colored output |
122
+
123
+ ## Environment Variables
124
+
125
+ | Variable | Description |
126
+ | ------------- | -------------------------------------------------------------------------------------- |
127
+ | `NO_COLOR` | Disable colored output when set (any value, per [no-color.org](https://no-color.org/)) |
128
+ | `FORCE_COLOR` | Override color level: `0` (disabled), `1` (basic), `2` (256 colors), `3` (16m colors) |
129
+
108
130
  ## Integration
109
131
 
110
132
  The CLI integrates with other LumenFlow packages:
@@ -116,7 +138,7 @@ The CLI integrates with other LumenFlow packages:
116
138
 
117
139
  ## Documentation
118
140
 
119
- For complete documentation, see the [LumenFlow documentation](https://github.com/hellmai/os).
141
+ For complete documentation, see [lumenflow.dev](https://lumenflow.dev/reference/cli).
120
142
 
121
143
  ## License
122
144
 
@@ -10,6 +10,8 @@
10
10
  * path but import.meta.url resolves to the real path - they never match
11
11
  * so main() is never called.
12
12
  *
13
+ * WU-1085: Initializes color support respecting NO_COLOR/FORCE_COLOR/--no-color
14
+ *
13
15
  * @example
14
16
  * ```typescript
15
17
  * // At the bottom of each CLI file:
@@ -21,13 +23,17 @@
21
23
  * ```
22
24
  */
23
25
  import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
26
+ import { initColorSupport } from '@lumenflow/core';
24
27
  /**
25
28
  * Wraps an async main function with proper error handling.
29
+ * WU-1085: Also initializes color support based on NO_COLOR/FORCE_COLOR/--no-color
26
30
  *
27
31
  * @param main - The async main function to execute
28
32
  * @returns Promise that resolves when main completes (or after error handling)
29
33
  */
30
34
  export async function runCLI(main) {
35
+ // WU-1085: Initialize color support before running command
36
+ initColorSupport();
31
37
  try {
32
38
  await main();
33
39
  }
package/dist/docs-sync.js CHANGED
@@ -1,9 +1,38 @@
1
1
  /**
2
2
  * @file docs-sync.ts
3
3
  * LumenFlow docs:sync command for syncing agent docs to existing projects (WU-1083)
4
+ * WU-1085: Added createWUParser for proper --help support
4
5
  */
5
6
  import * as fs from 'node:fs';
6
7
  import * as path from 'node:path';
8
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core';
9
+ /**
10
+ * WU-1085: CLI option definitions for docs-sync command
11
+ */
12
+ const DOCS_SYNC_OPTIONS = {
13
+ vendor: {
14
+ name: 'vendor',
15
+ flags: '--vendor <type>',
16
+ description: 'Vendor type (claude, cursor, aider, all, none)',
17
+ default: 'claude',
18
+ },
19
+ force: WU_OPTIONS.force,
20
+ };
21
+ /**
22
+ * WU-1085: Parse docs-sync command options using createWUParser
23
+ * Provides proper --help, --version, and option parsing
24
+ */
25
+ export function parseDocsSyncOptions() {
26
+ const opts = createWUParser({
27
+ name: 'lumenflow-docs-sync',
28
+ description: 'Sync agent onboarding docs to existing projects (skips existing files by default)',
29
+ options: Object.values(DOCS_SYNC_OPTIONS),
30
+ });
31
+ return {
32
+ force: opts.force ?? false,
33
+ vendor: opts.vendor ?? 'claude',
34
+ };
35
+ }
7
36
  /**
8
37
  * Get current date in YYYY-MM-DD format
9
38
  */
@@ -407,32 +436,18 @@ export async function syncSkills(targetDir, options) {
407
436
  await createFile(path.join(gatesDir, 'SKILL.md'), processTemplate(LUMENFLOW_GATES_SKILL_TEMPLATE, tokens), options.force, result, targetDir);
408
437
  return result;
409
438
  }
410
- /**
411
- * Parse vendor flag from arguments
412
- */
413
- function parseVendorArg(args) {
414
- const vendorIndex = args.findIndex((arg) => arg === '--vendor');
415
- if (vendorIndex !== -1 && args[vendorIndex + 1]) {
416
- const vendor = args[vendorIndex + 1].toLowerCase();
417
- if (['claude', 'cursor', 'aider', 'all', 'none'].includes(vendor)) {
418
- return vendor;
419
- }
420
- }
421
- return undefined;
422
- }
423
439
  /**
424
440
  * CLI entry point for docs:sync command
441
+ * WU-1085: Updated to use parseDocsSyncOptions for proper --help support
425
442
  */
426
443
  export async function main() {
427
- const args = process.argv.slice(2);
428
- const force = args.includes('--force') || args.includes('-f');
429
- const vendor = parseVendorArg(args) ?? 'claude'; // Default to claude
444
+ const opts = parseDocsSyncOptions();
430
445
  const targetDir = process.cwd();
431
446
  console.log('[lumenflow docs:sync] Syncing agent documentation...');
432
- console.log(` Vendor: ${vendor}`);
433
- console.log(` Force: ${force}`);
434
- const docsResult = await syncAgentDocs(targetDir, { force });
435
- const skillsResult = await syncSkills(targetDir, { force, vendor });
447
+ console.log(` Vendor: ${opts.vendor}`);
448
+ console.log(` Force: ${opts.force}`);
449
+ const docsResult = await syncAgentDocs(targetDir, { force: opts.force });
450
+ const skillsResult = await syncSkills(targetDir, { force: opts.force, vendor: opts.vendor });
436
451
  const created = [...docsResult.created, ...skillsResult.created];
437
452
  const skipped = [...docsResult.skipped, ...skillsResult.skipped];
438
453
  if (created.length > 0) {
package/dist/gates.js CHANGED
@@ -54,35 +54,92 @@ import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, }
54
54
  import { detectRiskTier, RISK_TIERS, } from '@lumenflow/core/dist/risk-detector.js';
55
55
  // WU-2252: Import invariants runner for first-check validation
56
56
  import { runInvariants } from '@lumenflow/core/dist/invariants-runner.js';
57
- import { Command } from 'commander';
57
+ import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
58
58
  import { BRANCHES, PACKAGES, PKG_MANAGER, PKG_FLAGS, ESLINT_FLAGS, ESLINT_COMMANDS, ESLINT_DEFAULTS, SCRIPTS, CACHE_STRATEGIES, DIRECTORIES, GATE_NAMES, GATE_COMMANDS, TOOL_PATHS, CLI_MODES, EXIT_CODES, FILE_SYSTEM, PRETTIER_ARGS, PRETTIER_FLAGS, } from '@lumenflow/core/dist/wu-constants.js';
59
- // WU-2457: Add Commander.js for --help support
60
- function parseGatesArgs(argv = process.argv) {
59
+ /**
60
+ * WU-1087: Gates-specific option definitions for createWUParser.
61
+ * Exported for testing and consistency with other CLI commands.
62
+ */
63
+ export const GATES_OPTIONS = {
64
+ docsOnly: {
65
+ name: 'docsOnly',
66
+ flags: '--docs-only',
67
+ description: 'Run docs-only gates (format, spec-linter, prompts-lint, backlog-sync)',
68
+ },
69
+ fullLint: {
70
+ name: 'fullLint',
71
+ flags: '--full-lint',
72
+ description: 'Run full lint instead of incremental',
73
+ },
74
+ fullTests: {
75
+ name: 'fullTests',
76
+ flags: '--full-tests',
77
+ description: 'Run full test suite instead of incremental',
78
+ },
79
+ fullCoverage: {
80
+ name: 'fullCoverage',
81
+ flags: '--full-coverage',
82
+ description: 'Force full test suite and coverage gate (implies --full-tests)',
83
+ },
84
+ coverageMode: {
85
+ name: 'coverageMode',
86
+ flags: '--coverage-mode <mode>',
87
+ description: 'Coverage gate mode: "warn" logs warnings, "block" fails gate',
88
+ default: 'block',
89
+ },
90
+ verbose: {
91
+ name: 'verbose',
92
+ flags: '--verbose',
93
+ description: 'Stream output in agent mode instead of logging to file',
94
+ },
95
+ };
96
+ /**
97
+ * WU-1087: Parse gates options using createWUParser for consistency.
98
+ * Handles pnpm's `--` separator and provides automatic --help support.
99
+ *
100
+ * @returns Parsed options object
101
+ */
102
+ export function parseGatesOptions() {
61
103
  // WU-2465: Pre-filter argv to handle pnpm's `--` separator
62
104
  // When invoked via `pnpm gates -- --docs-only`, pnpm passes ["--", "--docs-only"]
63
- // Commander treats `--` as "everything after is positional", causing errors.
64
- // Solution: Remove standalone `--` from argv before parsing.
65
- const filteredArgv = argv.filter((arg, index, arr) => {
66
- // Keep `--` only if it's followed by a non-option (actual positional arg)
67
- // Remove it if it's followed by an option (starts with -)
105
+ // createWUParser's default filtering removes all `--`, but we need smarter handling:
106
+ // Remove `--` only if it's followed by an option (starts with -)
107
+ const originalArgv = process.argv;
108
+ const filteredArgv = originalArgv.filter((arg, index, arr) => {
68
109
  if (arg === '--') {
69
110
  const nextArg = arr[index + 1];
70
111
  return nextArg && !nextArg.startsWith('-');
71
112
  }
72
113
  return true;
73
114
  });
74
- const program = new Command()
75
- .name('gates')
76
- .description('Run quality gates with support for docs-only mode, incremental linting, and tiered testing')
77
- .option('--docs-only', 'Run docs-only gates (format, spec-linter, prompts-lint, backlog-sync)')
78
- .option('--full-lint', 'Run full lint instead of incremental')
79
- .option('--full-tests', 'Run full test suite instead of incremental')
80
- .option('--full-coverage', 'Force full test suite and coverage gate (implies --full-tests)')
81
- .option('--coverage-mode <mode>', 'Coverage gate mode: "warn" logs warnings, "block" fails gate (default)', 'block')
82
- .option('--verbose', 'Stream output in agent mode instead of logging to file')
83
- .helpOption('-h, --help', 'Display help for command');
84
- program.parse(filteredArgv);
85
- return program.opts();
115
+ // Temporarily replace process.argv for createWUParser
116
+ process.argv = filteredArgv;
117
+ try {
118
+ const opts = createWUParser({
119
+ name: 'gates',
120
+ description: 'Run quality gates with support for docs-only mode, incremental linting, and tiered testing',
121
+ options: Object.values(GATES_OPTIONS),
122
+ });
123
+ return {
124
+ docsOnly: opts.docsOnly,
125
+ fullLint: opts.fullLint,
126
+ fullTests: opts.fullTests,
127
+ fullCoverage: opts.fullCoverage,
128
+ coverageMode: opts.coverageMode ?? 'block',
129
+ verbose: opts.verbose,
130
+ };
131
+ }
132
+ finally {
133
+ // Restore original process.argv
134
+ process.argv = originalArgv;
135
+ }
136
+ }
137
+ /**
138
+ * @deprecated Use parseGatesOptions() instead (WU-1087)
139
+ * Kept for backward compatibility during migration.
140
+ */
141
+ function parseGatesArgs(argv = process.argv) {
142
+ return parseGatesOptions();
86
143
  }
87
144
  /**
88
145
  * Build a pnpm command string
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @lumenflow/cli - Command-line interface for LumenFlow workflow framework
3
+ *
4
+ * This package provides CLI commands for the LumenFlow workflow framework.
5
+ * Most functionality is exposed via bin commands, but the cli-entry-point
6
+ * helper is exported for use in custom CLI wrappers.
7
+ *
8
+ * @see https://lumenflow.dev/reference/cli
9
+ */
10
+ export { runCLI } from './cli-entry-point.js';
package/dist/init.js CHANGED
@@ -3,13 +3,58 @@
3
3
  * LumenFlow project scaffolding command (WU-1045)
4
4
  * WU-1006: Library-First - use core defaults for config generation
5
5
  * WU-1028: Vendor-agnostic core + vendor overlays
6
+ * WU-1085: Added createWUParser for proper --help support
6
7
  */
7
8
  import * as fs from 'node:fs';
8
9
  import * as path from 'node:path';
9
10
  import * as yaml from 'yaml';
10
- import { getDefaultConfig } from '@lumenflow/core';
11
+ import { getDefaultConfig, createWUParser, WU_OPTIONS } from '@lumenflow/core';
11
12
  // WU-1067: Import GATE_PRESETS for --preset support
12
13
  import { GATE_PRESETS } from '@lumenflow/core/dist/gates-config.js';
14
+ /**
15
+ * WU-1085: CLI option definitions for init command
16
+ */
17
+ const INIT_OPTIONS = {
18
+ full: {
19
+ name: 'full',
20
+ flags: '--full',
21
+ description: 'Add docs + agent onboarding + task scaffolding',
22
+ },
23
+ framework: {
24
+ name: 'framework',
25
+ flags: '--framework <name>',
26
+ description: 'Add framework hint + overlay docs',
27
+ },
28
+ vendor: {
29
+ name: 'vendor',
30
+ flags: '--vendor <type>',
31
+ description: 'Vendor type (claude, cursor, aider, all, none)',
32
+ },
33
+ preset: {
34
+ name: 'preset',
35
+ flags: '--preset <preset>',
36
+ description: 'Gate preset for config (node, python, go, rust, dotnet)',
37
+ },
38
+ force: WU_OPTIONS.force,
39
+ };
40
+ /**
41
+ * WU-1085: Parse init command options using createWUParser
42
+ * Provides proper --help, --version, and option parsing
43
+ */
44
+ export function parseInitOptions() {
45
+ const opts = createWUParser({
46
+ name: 'lumenflow-init',
47
+ description: 'Initialize LumenFlow in a project',
48
+ options: Object.values(INIT_OPTIONS),
49
+ });
50
+ return {
51
+ force: opts.force ?? false,
52
+ full: opts.full ?? false,
53
+ framework: opts.framework,
54
+ vendor: opts.vendor,
55
+ preset: opts.preset,
56
+ };
57
+ }
13
58
  const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
14
59
  const FRAMEWORK_HINT_FILE = '.lumenflow.framework.yaml';
15
60
  const LUMENFLOW_DIR = '.lumenflow';
@@ -1100,26 +1145,22 @@ async function createFile(filePath, content, force, result, targetDir) {
1100
1145
  }
1101
1146
  /**
1102
1147
  * CLI entry point
1148
+ * WU-1085: Updated to use parseInitOptions for proper --help support
1103
1149
  */
1104
1150
  export async function main() {
1105
- const args = process.argv.slice(2);
1106
- const force = args.includes('--force') || args.includes('-f');
1107
- const full = args.includes('--full');
1108
- const vendor = parseVendorArg(args);
1109
- const framework = parseFrameworkArg(args);
1110
- const gatePreset = parsePresetArg(args); // WU-1067
1151
+ const opts = parseInitOptions();
1111
1152
  const targetDir = process.cwd();
1112
1153
  console.log('[lumenflow init] Scaffolding LumenFlow project...');
1113
- console.log(` Mode: ${full ? 'full' : 'minimal'}`);
1114
- console.log(` Framework: ${framework ?? 'none'}`);
1115
- console.log(` Vendor overlays: ${vendor ?? 'auto'}`);
1116
- console.log(` Gate preset: ${gatePreset ?? 'none (manual config)'}`);
1154
+ console.log(` Mode: ${opts.full ? 'full' : 'minimal'}`);
1155
+ console.log(` Framework: ${opts.framework ?? 'none'}`);
1156
+ console.log(` Vendor overlays: ${opts.vendor ?? 'auto'}`);
1157
+ console.log(` Gate preset: ${opts.preset ?? 'none (manual config)'}`);
1117
1158
  const result = await scaffoldProject(targetDir, {
1118
- force,
1119
- full,
1120
- vendor,
1121
- framework,
1122
- gatePreset,
1159
+ force: opts.force,
1160
+ full: opts.full,
1161
+ vendor: opts.vendor,
1162
+ framework: opts.framework,
1163
+ gatePreset: opts.preset,
1123
1164
  });
1124
1165
  if (result.created.length > 0) {
1125
1166
  console.log('\nCreated:');
package/dist/release.js CHANGED
@@ -13,9 +13,12 @@
13
13
  * - Creates git tag vX.Y.Z
14
14
  *
15
15
  * Usage:
16
- * pnpm release --version 1.3.0
17
- * pnpm release --version 1.3.0 --dry-run # Preview without making changes
18
- * pnpm release --version 1.3.0 --skip-publish # Bump and tag only (no npm publish)
16
+ * pnpm release --release-version 1.3.0
17
+ * pnpm release --release-version 1.3.0 --dry-run # Preview without making changes
18
+ * pnpm release --release-version 1.3.0 --skip-publish # Bump and tag only (no npm publish)
19
+ *
20
+ * WU-1085: The --release-version flag was renamed from --version to avoid conflict
21
+ * with the standard CLI --version flag that shows the CLI version.
19
22
  *
20
23
  * WU-1074: Add release command for npm publishing
21
24
  */
@@ -262,19 +265,22 @@ export async function pushTagWithForce(git, tagName, reason = 'release: tag push
262
265
  }
263
266
  /**
264
267
  * Main release function
268
+ * WU-1085: Renamed --version to --release-version to avoid conflict with CLI --version flag
265
269
  */
266
270
  async function main() {
267
271
  const program = new Command()
268
- .name('release')
272
+ .name('lumenflow-release')
269
273
  .description('Release @lumenflow/* packages to npm with version bump, tag, and publish')
270
- .requiredOption('-v, --version <version>', 'Semver version to release (e.g., 1.3.0)')
274
+ .version('1.0.0', '-V, --version', 'Output the CLI version')
275
+ .requiredOption('-v, --release-version <version>', 'Semver version to release (e.g., 1.3.0)')
271
276
  .option('--dry-run', 'Preview changes without making them', false)
272
277
  .option('--skip-publish', 'Skip npm publish (only bump and tag)', false)
273
278
  .option('--skip-build', 'Skip build step (use existing dist)', false)
274
279
  .helpOption('-h, --help', 'Display help for command');
275
280
  program.parse();
276
281
  const opts = program.opts();
277
- const { version, dryRun, skipPublish, skipBuild } = opts;
282
+ // WU-1085: Use releaseVersion instead of version (renamed to avoid CLI --version conflict)
283
+ const { releaseVersion: version, dryRun, skipPublish, skipBuild } = opts;
278
284
  console.log(`${LOG_PREFIX} Starting release process for v${version}`);
279
285
  if (dryRun) {
280
286
  console.log(`${LOG_PREFIX} DRY RUN MODE - no changes will be made`);
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Recovery Command
4
+ *
5
+ * WU-1090: Context-aware state machine for WU lifecycle commands
6
+ *
7
+ * Analyzes WU state inconsistencies and offers recovery actions:
8
+ * - resume: Reconcile state and continue working (preserves work)
9
+ * - reset: Discard worktree and reset WU to ready
10
+ * - nuke: Remove all artifacts completely (requires --force)
11
+ * - cleanup: Remove leftover worktree for done WUs
12
+ *
13
+ * Usage:
14
+ * pnpm wu:recover --id WU-123 # Analyze issues
15
+ * pnpm wu:recover --id WU-123 --action resume # Apply fix
16
+ * pnpm wu:recover --id WU-123 --action nuke --force # Destructive
17
+ */
18
+ import { existsSync, rmSync } from 'node:fs';
19
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
20
+ import { computeContext } from '@lumenflow/core/dist/context/index.js';
21
+ import { analyzeRecovery, } from '@lumenflow/core/dist/recovery/recovery-analyzer.js';
22
+ import { die } from '@lumenflow/core/dist/error-handler.js';
23
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
24
+ import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
25
+ import { CONTEXT_VALIDATION, EMOJI, WU_STATUS, DEFAULTS, toKebab, } from '@lumenflow/core/dist/wu-constants.js';
26
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
27
+ import { join } from 'node:path';
28
+ const { RECOVERY_ACTIONS } = CONTEXT_VALIDATION;
29
+ const LOG_PREFIX = '[wu:recover]';
30
+ /**
31
+ * Valid recovery action types
32
+ */
33
+ const VALID_ACTIONS = [
34
+ RECOVERY_ACTIONS.RESUME,
35
+ RECOVERY_ACTIONS.RESET,
36
+ RECOVERY_ACTIONS.NUKE,
37
+ RECOVERY_ACTIONS.CLEANUP,
38
+ ];
39
+ /**
40
+ * Check if action requires --force flag
41
+ */
42
+ export function requiresForceFlag(action) {
43
+ return action === RECOVERY_ACTIONS.NUKE;
44
+ }
45
+ /**
46
+ * Validate recovery action
47
+ */
48
+ export function validateRecoveryAction(action) {
49
+ if (VALID_ACTIONS.includes(action)) {
50
+ return { valid: true };
51
+ }
52
+ return {
53
+ valid: false,
54
+ error: `Invalid action '${action}'. Valid actions: ${VALID_ACTIONS.join(', ')}`,
55
+ };
56
+ }
57
+ /**
58
+ * Format recovery analysis output
59
+ */
60
+ export function formatRecoveryOutput(analysis) {
61
+ const lines = [];
62
+ lines.push(`## Recovery Analysis for ${analysis.wuId || 'unknown'}`);
63
+ lines.push('');
64
+ if (!analysis.hasIssues) {
65
+ lines.push(`${EMOJI.SUCCESS} No issues found - WU state is healthy`);
66
+ return lines.join('\n');
67
+ }
68
+ // Issues section
69
+ lines.push('### Issues Detected');
70
+ for (const issue of analysis.issues) {
71
+ lines.push(` ${EMOJI.FAILURE} ${issue.code}: ${issue.description}`);
72
+ }
73
+ lines.push('');
74
+ // Actions section
75
+ if (analysis.actions.length > 0) {
76
+ lines.push('### Available Recovery Actions');
77
+ for (const action of analysis.actions) {
78
+ lines.push(` **${action.type}**: ${action.description}`);
79
+ lines.push(` Command: ${action.command}`);
80
+ if (action.warning) {
81
+ lines.push(` ${EMOJI.WARNING} Warning: ${action.warning}`);
82
+ }
83
+ if (action.requiresForce) {
84
+ lines.push(` Requires: --force flag`);
85
+ }
86
+ lines.push('');
87
+ }
88
+ }
89
+ return lines.join('\n');
90
+ }
91
+ /**
92
+ * Get exit code for recovery command
93
+ */
94
+ export function getRecoveryExitCode(analysis, actionFailed) {
95
+ if (actionFailed) {
96
+ return 1;
97
+ }
98
+ return 0;
99
+ }
100
+ /**
101
+ * Get expected worktree path for a WU
102
+ */
103
+ function getWorktreePath(wuId, lane) {
104
+ const laneKebab = toKebab(lane);
105
+ const wuIdLower = wuId.toLowerCase();
106
+ return join(process.cwd(), DEFAULTS.WORKTREES_DIR, `${laneKebab}-${wuIdLower}`);
107
+ }
108
+ /**
109
+ * Execute resume action - reconcile state and continue
110
+ */
111
+ async function executeResume(wuId) {
112
+ console.log(`${LOG_PREFIX} Executing resume action for ${wuId}...`);
113
+ const wuPath = WU_PATHS.WU(wuId);
114
+ if (!existsSync(wuPath)) {
115
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} WU file not found: ${wuPath}`);
116
+ return false;
117
+ }
118
+ const doc = readWU(wuPath, wuId);
119
+ // Update status to in_progress if it was ready
120
+ if (doc.status === WU_STATUS.READY) {
121
+ doc.status = WU_STATUS.IN_PROGRESS;
122
+ writeWU(wuPath, doc);
123
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Updated ${wuId} status to in_progress`);
124
+ }
125
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Resume completed - you can continue working in the worktree`);
126
+ return true;
127
+ }
128
+ /**
129
+ * Execute reset action - discard worktree and reset to ready
130
+ */
131
+ async function executeReset(wuId) {
132
+ console.log(`${LOG_PREFIX} Executing reset action for ${wuId}...`);
133
+ const wuPath = WU_PATHS.WU(wuId);
134
+ if (!existsSync(wuPath)) {
135
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} WU file not found: ${wuPath}`);
136
+ return false;
137
+ }
138
+ const doc = readWU(wuPath, wuId);
139
+ const worktreePath = getWorktreePath(wuId, doc.lane || '');
140
+ // Remove worktree if exists
141
+ // WU-1097: Use worktreeRemove() instead of deprecated run() with shell strings
142
+ // This properly handles paths with spaces and special characters
143
+ if (existsSync(worktreePath)) {
144
+ try {
145
+ const git = getGitForCwd();
146
+ await git.worktreeRemove(worktreePath, { force: true });
147
+ console.log(`${LOG_PREFIX} Removed worktree: ${worktreePath}`);
148
+ }
149
+ catch (e) {
150
+ // Try manual removal if git command fails
151
+ try {
152
+ rmSync(worktreePath, { recursive: true, force: true });
153
+ console.log(`${LOG_PREFIX} Manually removed worktree directory: ${worktreePath}`);
154
+ }
155
+ catch (rmError) {
156
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} Failed to remove worktree: ${rmError.message}`);
157
+ return false;
158
+ }
159
+ }
160
+ }
161
+ // Reset WU status to ready
162
+ doc.status = WU_STATUS.READY;
163
+ delete doc.worktree_path;
164
+ delete doc.claimed_at;
165
+ delete doc.session_id;
166
+ writeWU(wuPath, doc);
167
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Reset completed - ${wuId} is now ready for re-claiming`);
168
+ return true;
169
+ }
170
+ /**
171
+ * Execute nuke action - remove all artifacts completely
172
+ */
173
+ async function executeNuke(wuId) {
174
+ console.log(`${LOG_PREFIX} Executing nuke action for ${wuId}...`);
175
+ const wuPath = WU_PATHS.WU(wuId);
176
+ if (!existsSync(wuPath)) {
177
+ console.log(`${LOG_PREFIX} WU file does not exist: ${wuPath}`);
178
+ }
179
+ else {
180
+ const doc = readWU(wuPath, wuId);
181
+ const worktreePath = getWorktreePath(wuId, doc.lane || '');
182
+ // Remove worktree if exists
183
+ // WU-1097: Use worktreeRemove() instead of deprecated run() with shell strings
184
+ if (existsSync(worktreePath)) {
185
+ try {
186
+ const git = getGitForCwd();
187
+ await git.worktreeRemove(worktreePath, { force: true });
188
+ }
189
+ catch {
190
+ rmSync(worktreePath, { recursive: true, force: true });
191
+ }
192
+ console.log(`${LOG_PREFIX} Removed worktree: ${worktreePath}`);
193
+ }
194
+ // Try to delete branch
195
+ // WU-1097: Use deleteBranch() instead of deprecated run() with shell strings
196
+ try {
197
+ const git = getGitForCwd();
198
+ const laneKebab = toKebab(doc.lane || '');
199
+ const branchName = `lane/${laneKebab}/${wuId.toLowerCase()}`;
200
+ await git.deleteBranch(branchName, { force: true });
201
+ console.log(`${LOG_PREFIX} Deleted branch: ${branchName}`);
202
+ }
203
+ catch {
204
+ // Branch may not exist, that's fine
205
+ }
206
+ }
207
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Nuke completed - all artifacts removed for ${wuId}`);
208
+ return true;
209
+ }
210
+ /**
211
+ * Execute cleanup action - remove leftover worktree for done WUs
212
+ */
213
+ async function executeCleanup(wuId) {
214
+ console.log(`${LOG_PREFIX} Executing cleanup action for ${wuId}...`);
215
+ const wuPath = WU_PATHS.WU(wuId);
216
+ if (!existsSync(wuPath)) {
217
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} WU file not found: ${wuPath}`);
218
+ return false;
219
+ }
220
+ const doc = readWU(wuPath, wuId);
221
+ if (doc.status !== WU_STATUS.DONE) {
222
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} Cannot cleanup: WU status is '${doc.status}', expected 'done'`);
223
+ return false;
224
+ }
225
+ const worktreePath = getWorktreePath(wuId, doc.lane || '');
226
+ if (!existsSync(worktreePath)) {
227
+ console.log(`${LOG_PREFIX} Worktree does not exist, nothing to cleanup`);
228
+ return true;
229
+ }
230
+ // WU-1097: Use worktreeRemove() instead of deprecated run() with shell strings
231
+ try {
232
+ const git = getGitForCwd();
233
+ await git.worktreeRemove(worktreePath, { force: true });
234
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Removed leftover worktree: ${worktreePath}`);
235
+ }
236
+ catch (e) {
237
+ rmSync(worktreePath, { recursive: true, force: true });
238
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Manually removed leftover worktree: ${worktreePath}`);
239
+ }
240
+ return true;
241
+ }
242
+ /**
243
+ * Execute recovery action
244
+ */
245
+ async function executeAction(action, wuId) {
246
+ switch (action) {
247
+ case RECOVERY_ACTIONS.RESUME:
248
+ return executeResume(wuId);
249
+ case RECOVERY_ACTIONS.RESET:
250
+ return executeReset(wuId);
251
+ case RECOVERY_ACTIONS.NUKE:
252
+ return executeNuke(wuId);
253
+ case RECOVERY_ACTIONS.CLEANUP:
254
+ return executeCleanup(wuId);
255
+ default:
256
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} Unknown action: ${action}`);
257
+ return false;
258
+ }
259
+ }
260
+ /**
261
+ * Main entry point
262
+ */
263
+ async function main() {
264
+ const args = createWUParser({
265
+ name: 'wu-recover',
266
+ description: 'Analyze and fix WU state inconsistencies (WU-1090)',
267
+ options: [
268
+ WU_OPTIONS.id,
269
+ {
270
+ name: 'action',
271
+ flags: '-a, --action <action>',
272
+ type: 'string',
273
+ description: 'Recovery action: resume, reset, nuke, cleanup',
274
+ },
275
+ {
276
+ name: 'force',
277
+ flags: '-f, --force',
278
+ type: 'boolean',
279
+ description: 'Required for destructive actions (nuke)',
280
+ },
281
+ {
282
+ name: 'json',
283
+ flags: '-j, --json',
284
+ type: 'boolean',
285
+ description: 'Output as JSON',
286
+ },
287
+ ],
288
+ required: ['id'],
289
+ allowPositionalId: true,
290
+ });
291
+ const { id, action, force, json } = args;
292
+ // Compute context for the WU
293
+ const { context } = await computeContext({ wuId: id });
294
+ // Analyze recovery issues
295
+ const analysis = await analyzeRecovery(context);
296
+ // If no action specified, just show analysis
297
+ if (!action) {
298
+ if (json) {
299
+ console.log(JSON.stringify(analysis, null, 2));
300
+ }
301
+ else {
302
+ console.log(formatRecoveryOutput(analysis));
303
+ }
304
+ process.exit(getRecoveryExitCode(analysis, false));
305
+ return;
306
+ }
307
+ // Validate action
308
+ const validation = validateRecoveryAction(action);
309
+ if (!validation.valid) {
310
+ die(validation.error);
311
+ }
312
+ // Check force flag for destructive actions
313
+ if (requiresForceFlag(action) && !force) {
314
+ die(`Action '${action}' requires --force flag`);
315
+ }
316
+ // Execute action
317
+ const success = await executeAction(action, id);
318
+ if (!success) {
319
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} Recovery action failed`);
320
+ process.exit(1);
321
+ }
322
+ process.exit(0);
323
+ }
324
+ // Guard main() for testability
325
+ import { fileURLToPath } from 'node:url';
326
+ import { runCLI } from './cli-entry-point.js';
327
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
328
+ runCLI(main);
329
+ }
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Status Command
4
+ *
5
+ * WU-1090: Context-aware state machine for WU lifecycle commands
6
+ *
7
+ * Shows:
8
+ * - Current location (main checkout vs worktree)
9
+ * - WU state if in worktree or --id provided
10
+ * - Git state (branch, dirty, ahead/behind)
11
+ * - Valid commands for current context
12
+ *
13
+ * Usage:
14
+ * pnpm wu:status # Auto-detect from current directory
15
+ * pnpm wu:status --id WU-123 # Show status for specific WU
16
+ */
17
+ import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
18
+ import { computeContext } from '@lumenflow/core/dist/context/index.js';
19
+ import { getValidCommandsForContext } from '@lumenflow/core/dist/validation/command-registry.js';
20
+ import { CONTEXT_VALIDATION, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
21
+ const { LOCATION_TYPES } = CONTEXT_VALIDATION;
22
+ const LOG_PREFIX = '[wu:status]';
23
+ /**
24
+ * Format location type for display
25
+ */
26
+ function formatLocationType(type) {
27
+ switch (type) {
28
+ case LOCATION_TYPES.MAIN:
29
+ return 'main checkout';
30
+ case LOCATION_TYPES.WORKTREE:
31
+ return 'worktree';
32
+ case LOCATION_TYPES.DETACHED:
33
+ return 'detached HEAD';
34
+ default:
35
+ return 'unknown location';
36
+ }
37
+ }
38
+ /**
39
+ * Format git state for display
40
+ */
41
+ function formatGitState(context) {
42
+ const lines = [];
43
+ const { git } = context;
44
+ if (git.hasError) {
45
+ lines.push(` ${EMOJI.FAILURE} Git error: ${git.errorMessage}`);
46
+ return lines;
47
+ }
48
+ const branchInfo = git.branch || '(detached)';
49
+ lines.push(` Branch: ${branchInfo}`);
50
+ if (git.isDirty) {
51
+ lines.push(` ${EMOJI.WARNING} Working tree: dirty (${git.modifiedFiles.length} files)`);
52
+ }
53
+ else {
54
+ lines.push(` ${EMOJI.SUCCESS} Working tree: clean`);
55
+ }
56
+ if (git.hasStaged) {
57
+ lines.push(` Staged: yes`);
58
+ }
59
+ if (git.ahead > 0 || git.behind > 0) {
60
+ const parts = [];
61
+ if (git.ahead > 0)
62
+ parts.push(`${git.ahead} ahead`);
63
+ if (git.behind > 0)
64
+ parts.push(`${git.behind} behind`);
65
+ lines.push(` Tracking: ${parts.join(', ')}`);
66
+ }
67
+ return lines;
68
+ }
69
+ /**
70
+ * Format WU state for display
71
+ */
72
+ function formatWuState(context) {
73
+ const lines = [];
74
+ const { wu } = context;
75
+ if (!wu) {
76
+ lines.push(` No WU context`);
77
+ return lines;
78
+ }
79
+ lines.push(` ID: ${wu.id}`);
80
+ lines.push(` Title: ${wu.title}`);
81
+ lines.push(` Lane: ${wu.lane}`);
82
+ lines.push(` Status: ${wu.status}`);
83
+ if (!wu.isConsistent) {
84
+ lines.push(` ${EMOJI.WARNING} State inconsistency: ${wu.inconsistencyReason}`);
85
+ }
86
+ return lines;
87
+ }
88
+ /**
89
+ * Format valid commands for display
90
+ */
91
+ function formatValidCommands(context) {
92
+ const lines = [];
93
+ const validCommands = getValidCommandsForContext(context);
94
+ if (validCommands.length === 0) {
95
+ lines.push(` No commands available for current context`);
96
+ return lines;
97
+ }
98
+ for (const cmd of validCommands) {
99
+ lines.push(` ${cmd.name} - ${cmd.description}`);
100
+ }
101
+ return lines;
102
+ }
103
+ /**
104
+ * Format complete status output
105
+ */
106
+ export function formatStatusOutput(context) {
107
+ const lines = [];
108
+ // Location section
109
+ lines.push('## Location');
110
+ lines.push(` Type: ${formatLocationType(context.location.type)}`);
111
+ lines.push(` Path: ${context.location.cwd}`);
112
+ if (context.location.worktreeName) {
113
+ lines.push(` Worktree: ${context.location.worktreeName}`);
114
+ }
115
+ if (context.location.worktreeWuId) {
116
+ lines.push(` WU ID: ${context.location.worktreeWuId}`);
117
+ }
118
+ lines.push('');
119
+ // Git section
120
+ lines.push('## Git State');
121
+ lines.push(...formatGitState(context));
122
+ lines.push('');
123
+ // WU section
124
+ lines.push('## WU State');
125
+ lines.push(...formatWuState(context));
126
+ lines.push('');
127
+ // Valid commands section
128
+ lines.push('## Valid Commands');
129
+ lines.push(...formatValidCommands(context));
130
+ return lines.join('\n');
131
+ }
132
+ /**
133
+ * Get exit code based on context state
134
+ */
135
+ export function getStatusExitCode(context) {
136
+ // Error if git has errors
137
+ if (context.git.hasError) {
138
+ return 1;
139
+ }
140
+ // Error if location is unknown
141
+ if (context.location.type === LOCATION_TYPES.UNKNOWN) {
142
+ return 1;
143
+ }
144
+ return 0;
145
+ }
146
+ /**
147
+ * Main entry point
148
+ */
149
+ async function main() {
150
+ const args = createWUParser({
151
+ name: 'wu-status',
152
+ description: 'Show WU status, location, and valid commands (WU-1090)',
153
+ options: [
154
+ { ...WU_OPTIONS.id, required: false },
155
+ {
156
+ name: 'json',
157
+ flags: '-j, --json',
158
+ type: 'boolean',
159
+ description: 'Output as JSON',
160
+ },
161
+ ],
162
+ required: [],
163
+ allowPositionalId: true,
164
+ });
165
+ const { id, json } = args;
166
+ // Compute context
167
+ const { context, computationMs, exceededBudget } = await computeContext({
168
+ wuId: id,
169
+ });
170
+ if (exceededBudget) {
171
+ console.warn(`${LOG_PREFIX} ${EMOJI.WARNING} Context computation took ${computationMs.toFixed(0)}ms (exceeded 100ms budget)`);
172
+ }
173
+ // Output
174
+ if (json) {
175
+ console.log(JSON.stringify(context, null, 2));
176
+ }
177
+ else {
178
+ console.log(formatStatusOutput(context));
179
+ }
180
+ // Exit with appropriate code
181
+ process.exit(getStatusExitCode(context));
182
+ }
183
+ // Guard main() for testability
184
+ import { fileURLToPath } from 'node:url';
185
+ import { runCLI } from './cli-entry-point.js';
186
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
187
+ runCLI(main);
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/cli",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Command-line interface for LumenFlow workflow framework",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -21,6 +21,18 @@
21
21
  "url": "https://hellm.ai"
22
22
  },
23
23
  "type": "module",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./cli-entry-point": {
32
+ "types": "./dist/cli-entry-point.d.ts",
33
+ "import": "./dist/cli-entry-point.js"
34
+ }
35
+ },
24
36
  "bin": {
25
37
  "wu-claim": "./dist/wu-claim.js",
26
38
  "wu-done": "./dist/wu-done.js",
@@ -39,6 +51,8 @@
39
51
  "wu-delete": "./dist/wu-delete.js",
40
52
  "wu-unlock-lane": "./dist/wu-unlock-lane.js",
41
53
  "wu-release": "./dist/wu-release.js",
54
+ "wu-status": "./dist/wu-status.js",
55
+ "wu-recover": "./dist/wu-recover.js",
42
56
  "mem-init": "./dist/mem-init.js",
43
57
  "mem-checkpoint": "./dist/mem-checkpoint.js",
44
58
  "mem-start": "./dist/mem-start.js",
@@ -88,11 +102,11 @@
88
102
  "pretty-ms": "^9.2.0",
89
103
  "simple-git": "^3.30.0",
90
104
  "yaml": "^2.8.2",
91
- "@lumenflow/core": "1.4.0",
92
- "@lumenflow/metrics": "1.4.0",
93
- "@lumenflow/memory": "1.4.0",
94
- "@lumenflow/initiatives": "1.4.0",
95
- "@lumenflow/agent": "1.4.0"
105
+ "@lumenflow/core": "1.6.0",
106
+ "@lumenflow/initiatives": "1.6.0",
107
+ "@lumenflow/agent": "1.6.0",
108
+ "@lumenflow/metrics": "1.6.0",
109
+ "@lumenflow/memory": "1.6.0"
96
110
  },
97
111
  "devDependencies": {
98
112
  "@vitest/coverage-v8": "^4.0.17",