@lumenflow/cli 1.6.0 → 2.1.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.
Files changed (42) hide show
  1. package/README.md +19 -0
  2. package/dist/__tests__/backlog-prune.test.js +478 -0
  3. package/dist/__tests__/deps-operations.test.js +206 -0
  4. package/dist/__tests__/file-operations.test.js +906 -0
  5. package/dist/__tests__/git-operations.test.js +668 -0
  6. package/dist/__tests__/guards-validation.test.js +416 -0
  7. package/dist/__tests__/init-plan.test.js +340 -0
  8. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  9. package/dist/__tests__/metrics-cli.test.js +619 -0
  10. package/dist/__tests__/rotate-progress.test.js +127 -0
  11. package/dist/__tests__/session-coordinator.test.js +109 -0
  12. package/dist/__tests__/state-bootstrap.test.js +432 -0
  13. package/dist/__tests__/trace-gen.test.js +115 -0
  14. package/dist/backlog-prune.js +299 -0
  15. package/dist/deps-add.js +215 -0
  16. package/dist/deps-remove.js +94 -0
  17. package/dist/docs-sync.js +72 -326
  18. package/dist/file-delete.js +236 -0
  19. package/dist/file-edit.js +247 -0
  20. package/dist/file-read.js +197 -0
  21. package/dist/file-write.js +220 -0
  22. package/dist/git-branch.js +187 -0
  23. package/dist/git-diff.js +177 -0
  24. package/dist/git-log.js +230 -0
  25. package/dist/git-status.js +208 -0
  26. package/dist/guard-locked.js +169 -0
  27. package/dist/guard-main-branch.js +202 -0
  28. package/dist/guard-worktree-commit.js +160 -0
  29. package/dist/init-plan.js +337 -0
  30. package/dist/lumenflow-upgrade.js +178 -0
  31. package/dist/metrics-cli.js +433 -0
  32. package/dist/rotate-progress.js +247 -0
  33. package/dist/session-coordinator.js +300 -0
  34. package/dist/state-bootstrap.js +307 -0
  35. package/dist/sync-templates.js +212 -0
  36. package/dist/trace-gen.js +331 -0
  37. package/dist/validate-agent-skills.js +218 -0
  38. package/dist/validate-agent-sync.js +148 -0
  39. package/dist/validate-backlog-sync.js +152 -0
  40. package/dist/validate-skills-spec.js +206 -0
  41. package/dist/validate.js +230 -0
  42. package/package.json +37 -7
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LumenFlow Upgrade CLI Command
4
+ *
5
+ * Updates all @lumenflow/* packages to a specified version or latest.
6
+ * Uses worktree pattern to ensure pnpm install runs in worktree, not main.
7
+ *
8
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
9
+ *
10
+ * Key requirements (from WU acceptance criteria):
11
+ * - Uses worktree pattern (install runs in worktree, not main)
12
+ * - Checks all 7 @lumenflow/* packages (not just 4)
13
+ *
14
+ * Usage:
15
+ * pnpm lumenflow:upgrade --version 1.5.0
16
+ * pnpm lumenflow:upgrade --latest
17
+ * pnpm lumenflow:upgrade --latest --dry-run
18
+ */
19
+ import { execSync } from 'node:child_process';
20
+ import { STDIO_MODES, EXIT_CODES, PKG_MANAGER, } from '@lumenflow/core/dist/wu-constants.js';
21
+ import { runCLI } from './cli-entry-point.js';
22
+ import { validateWorktreeContext } from './deps-add.js';
23
+ /** Log prefix for console output */
24
+ const LOG_PREFIX = '[lumenflow:upgrade]';
25
+ /**
26
+ * All @lumenflow/* packages that should be upgraded together
27
+ *
28
+ * WU-1112: Must include all 7 packages (not just 4 as before)
29
+ * Kept in alphabetical order for consistency
30
+ */
31
+ export const LUMENFLOW_PACKAGES = [
32
+ '@lumenflow/agent',
33
+ '@lumenflow/cli',
34
+ '@lumenflow/core',
35
+ '@lumenflow/initiatives',
36
+ '@lumenflow/memory',
37
+ '@lumenflow/metrics',
38
+ '@lumenflow/shims',
39
+ ];
40
+ /**
41
+ * Parse command line arguments for lumenflow-upgrade
42
+ *
43
+ * @param argv - Process argv array
44
+ * @returns Parsed arguments
45
+ */
46
+ export function parseUpgradeArgs(argv) {
47
+ const args = {};
48
+ // Skip node and script name
49
+ const cliArgs = argv.slice(2);
50
+ for (let i = 0; i < cliArgs.length; i++) {
51
+ const arg = cliArgs[i];
52
+ if (arg === '--help' || arg === '-h') {
53
+ args.help = true;
54
+ }
55
+ else if (arg === '--version' || arg === '-v') {
56
+ args.version = cliArgs[++i];
57
+ }
58
+ else if (arg === '--latest' || arg === '-l') {
59
+ args.latest = true;
60
+ }
61
+ else if (arg === '--dry-run' || arg === '-n') {
62
+ args.dryRun = true;
63
+ }
64
+ }
65
+ return args;
66
+ }
67
+ /**
68
+ * Build the upgrade commands based on arguments
69
+ *
70
+ * Creates pnpm add command for all @lumenflow/* packages.
71
+ * Uses --save-dev since these are development dependencies.
72
+ *
73
+ * @param args - Parsed upgrade arguments
74
+ * @returns Object containing the commands to run
75
+ */
76
+ export function buildUpgradeCommands(args) {
77
+ // Determine version specifier
78
+ const versionSpec = args.latest ? 'latest' : args.version || 'latest';
79
+ // Build package list with version
80
+ const packages = LUMENFLOW_PACKAGES.map((pkg) => `${pkg}@${versionSpec}`);
81
+ // Build pnpm add command
82
+ const addCommand = `${PKG_MANAGER} add --save-dev ${packages.join(' ')}`;
83
+ return {
84
+ addCommand,
85
+ versionSpec,
86
+ };
87
+ }
88
+ /**
89
+ * Print help message for lumenflow-upgrade
90
+ */
91
+ /* istanbul ignore next -- CLI entry point */
92
+ function printHelp() {
93
+ console.log(`
94
+ Usage: lumenflow-upgrade [options]
95
+
96
+ Upgrade all @lumenflow/* packages to a specified version.
97
+ Must be run from inside a worktree to enforce worktree discipline.
98
+
99
+ Options:
100
+ -v, --version <ver> Upgrade to specific version (e.g., 1.5.0)
101
+ -l, --latest Upgrade to latest version
102
+ -n, --dry-run Show commands without executing
103
+ -h, --help Show this help message
104
+
105
+ Packages upgraded (all 7):
106
+ ${LUMENFLOW_PACKAGES.map((p) => ` - ${p}`).join('\n')}
107
+
108
+ Examples:
109
+ lumenflow:upgrade --version 1.5.0 # Upgrade to specific version
110
+ lumenflow:upgrade --latest # Upgrade to latest
111
+ lumenflow:upgrade --latest --dry-run # Preview upgrade commands
112
+
113
+ Worktree Discipline:
114
+ This command only works inside a worktree to prevent lockfile
115
+ conflicts on main checkout. Claim a WU first:
116
+
117
+ pnpm wu:claim --id WU-XXXX --lane "Your Lane"
118
+ cd worktrees/<lane>-wu-<id>/
119
+ lumenflow:upgrade --latest
120
+ `);
121
+ }
122
+ /**
123
+ * Main entry point for lumenflow-upgrade command
124
+ */
125
+ /* istanbul ignore next -- CLI entry point */
126
+ async function main() {
127
+ const args = parseUpgradeArgs(process.argv);
128
+ if (args.help) {
129
+ printHelp();
130
+ process.exit(EXIT_CODES.SUCCESS);
131
+ }
132
+ // Require either --version or --latest
133
+ if (!args.version && !args.latest) {
134
+ console.error(`${LOG_PREFIX} Error: Must specify --version <ver> or --latest`);
135
+ printHelp();
136
+ process.exit(EXIT_CODES.ERROR);
137
+ }
138
+ // Validate worktree context (WU-1112 requirement: must run in worktree)
139
+ const validation = validateWorktreeContext(process.cwd());
140
+ if (!validation.valid) {
141
+ console.error(`${LOG_PREFIX} ${validation.error}`);
142
+ console.error(`\nTo fix:\n${validation.fixCommand}`);
143
+ process.exit(EXIT_CODES.ERROR);
144
+ }
145
+ // Build upgrade commands
146
+ const { addCommand, versionSpec } = buildUpgradeCommands(args);
147
+ console.log(`${LOG_PREFIX} Upgrading @lumenflow/* packages to ${versionSpec}`);
148
+ console.log(`${LOG_PREFIX} Packages: ${LUMENFLOW_PACKAGES.length} packages`);
149
+ if (args.dryRun) {
150
+ console.log(`\n${LOG_PREFIX} DRY RUN - Commands that would be executed:`);
151
+ console.log(` ${addCommand}`);
152
+ console.log(`\n${LOG_PREFIX} No changes made.`);
153
+ process.exit(EXIT_CODES.SUCCESS);
154
+ }
155
+ // Execute upgrade
156
+ console.log(`${LOG_PREFIX} Running: ${addCommand}`);
157
+ try {
158
+ execSync(addCommand, {
159
+ stdio: STDIO_MODES.INHERIT,
160
+ cwd: process.cwd(),
161
+ });
162
+ console.log(`\n${LOG_PREFIX} ✅ Upgrade complete!`);
163
+ console.log(`${LOG_PREFIX} Upgraded to ${versionSpec}`);
164
+ console.log(`\n${LOG_PREFIX} Next steps:`);
165
+ console.log(` 1. Run 'pnpm build' to rebuild with new versions`);
166
+ console.log(` 2. Run 'pnpm gates' to verify everything works`);
167
+ console.log(` 3. Commit the changes`);
168
+ }
169
+ catch (error) {
170
+ console.error(`\n${LOG_PREFIX} ❌ Upgrade failed`);
171
+ console.error(`${LOG_PREFIX} Check the error above and try again.`);
172
+ process.exit(EXIT_CODES.ERROR);
173
+ }
174
+ }
175
+ // Run main if executed directly
176
+ if (import.meta.main) {
177
+ runCLI(main);
178
+ }
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unified Metrics CLI with subcommands (WU-1110)
4
+ *
5
+ * Provides lanes, dora, and flow metrics subcommands in a single CLI.
6
+ *
7
+ * Usage:
8
+ * pnpm metrics # All metrics, JSON output
9
+ * pnpm metrics lanes # Lane health only
10
+ * pnpm metrics dora # DORA metrics only
11
+ * pnpm metrics flow # Flow state only
12
+ * pnpm metrics --format table # Table output
13
+ * pnpm metrics --days 30 # 30 day window
14
+ * pnpm metrics --output metrics.json # Custom output file
15
+ * pnpm metrics --dry-run # Preview without writing
16
+ *
17
+ * @module metrics-cli
18
+ */
19
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
20
+ import { existsSync } from 'node:fs';
21
+ import { join, dirname } from 'node:path';
22
+ import fg from 'fast-glob';
23
+ import { parse as parseYaml } from 'yaml';
24
+ import { Command } from 'commander';
25
+ import { captureMetricsSnapshot, calculateDORAMetrics, calculateFlowState, } from '@lumenflow/metrics';
26
+ import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
27
+ import { die } from '@lumenflow/core/dist/error-handler.js';
28
+ /** Log prefix for console output */
29
+ const LOG_PREFIX = '[metrics]';
30
+ /** Default snapshot output path */
31
+ const DEFAULT_OUTPUT = '.lumenflow/snapshots/metrics-latest.json';
32
+ /** WU directory relative to repo root */
33
+ const WU_DIR = 'docs/04-operations/tasks/wu';
34
+ /** Skip-gates audit file path */
35
+ const SKIP_GATES_PATH = '.lumenflow/skip-gates-audit.ndjson';
36
+ /**
37
+ * Parse command line arguments
38
+ */
39
+ export function parseCommand(argv) {
40
+ let subcommand = 'all';
41
+ let days = 7;
42
+ let format = 'json';
43
+ let output = DEFAULT_OUTPUT;
44
+ let dryRun = false;
45
+ const program = new Command()
46
+ .name('metrics')
47
+ .description('LumenFlow metrics CLI - lanes, dora, flow subcommands')
48
+ .argument('[subcommand]', 'Subcommand: lanes, dora, flow, or all (default)')
49
+ .option('--days <number>', 'Days to analyze (default: 7)', '7')
50
+ .option('--format <type>', 'Output format: json, table (default: json)', 'json')
51
+ .option('--output <path>', `Output file path (default: ${DEFAULT_OUTPUT})`, DEFAULT_OUTPUT)
52
+ .option('--dry-run', 'Preview without writing to file')
53
+ .exitOverride();
54
+ try {
55
+ program.parse(argv);
56
+ const opts = program.opts();
57
+ const args = program.args;
58
+ // Parse subcommand
59
+ if (args.length > 0) {
60
+ const cmd = args[0];
61
+ if (cmd === 'lanes' || cmd === 'dora' || cmd === 'flow' || cmd === 'all') {
62
+ subcommand = cmd;
63
+ }
64
+ }
65
+ days = parseInt(opts.days, 10);
66
+ format = opts.format === 'table' ? 'table' : 'json';
67
+ output = opts.output;
68
+ dryRun = opts.dryRun === true;
69
+ }
70
+ catch (err) {
71
+ const error = err;
72
+ if (error.code === 'commander.helpDisplayed' || error.code === 'commander.version') {
73
+ process.exit(0);
74
+ }
75
+ }
76
+ return { subcommand, days, format, output, dryRun };
77
+ }
78
+ /**
79
+ * Calculate week date range
80
+ */
81
+ function calculateWeekRange(days) {
82
+ const weekEnd = new Date();
83
+ weekEnd.setHours(23, 59, 59, 999);
84
+ const weekStart = new Date(weekEnd);
85
+ weekStart.setDate(weekStart.getDate() - days);
86
+ weekStart.setHours(0, 0, 0, 0);
87
+ return { weekStart, weekEnd };
88
+ }
89
+ /**
90
+ * Calculate cycle time in hours from WU data
91
+ */
92
+ function calculateCycleTime(wu) {
93
+ if (!wu.claimed_at || !wu.completed_at) {
94
+ return undefined;
95
+ }
96
+ const claimed = new Date(wu.claimed_at);
97
+ const completed = new Date(wu.completed_at);
98
+ const diffMs = completed.getTime() - claimed.getTime();
99
+ const diffHours = diffMs / (1000 * 60 * 60);
100
+ return Math.round(diffHours * 10) / 10;
101
+ }
102
+ /**
103
+ * Load WU metrics from YAML files
104
+ */
105
+ async function loadWUMetrics(baseDir) {
106
+ const wuDir = join(baseDir, WU_DIR);
107
+ const wuFiles = await fg('WU-*.yaml', { cwd: wuDir, absolute: true });
108
+ const wuMetrics = [];
109
+ for (const file of wuFiles) {
110
+ try {
111
+ const content = await readFile(file, { encoding: 'utf-8' });
112
+ const wu = parseYaml(content);
113
+ // Map WU status to valid WUMetrics status
114
+ const rawStatus = wu.status;
115
+ let status = 'ready';
116
+ if (rawStatus === 'in_progress')
117
+ status = 'in_progress';
118
+ else if (rawStatus === 'blocked')
119
+ status = 'blocked';
120
+ else if (rawStatus === 'waiting')
121
+ status = 'waiting';
122
+ else if (rawStatus === 'done')
123
+ status = 'done';
124
+ else if (rawStatus === 'ready')
125
+ status = 'ready';
126
+ wuMetrics.push({
127
+ id: wu.id,
128
+ title: wu.title,
129
+ lane: wu.lane,
130
+ status,
131
+ priority: wu.priority,
132
+ claimedAt: wu.claimed_at ? new Date(wu.claimed_at) : undefined,
133
+ completedAt: wu.completed_at ? new Date(wu.completed_at) : undefined,
134
+ cycleTimeHours: calculateCycleTime(wu),
135
+ });
136
+ }
137
+ catch {
138
+ // Skip invalid WU files
139
+ }
140
+ }
141
+ return wuMetrics;
142
+ }
143
+ /**
144
+ * Load git commits from repository
145
+ */
146
+ async function loadGitCommits(weekStart, weekEnd) {
147
+ try {
148
+ const git = getGitForCwd();
149
+ const logResult = await git.log({ maxCount: 500 });
150
+ const commits = [];
151
+ for (const entry of [...logResult.all]) {
152
+ const commitDate = new Date(entry.date);
153
+ if (commitDate < weekStart || commitDate > weekEnd) {
154
+ continue;
155
+ }
156
+ const message = entry.message;
157
+ const wuIdMatch = message.match(/\b(WU-\d+)\b/i);
158
+ const wuId = wuIdMatch ? wuIdMatch[1].toUpperCase() : undefined;
159
+ const typeMatch = message.match(/^(feat|fix|docs|chore|refactor|test|style|perf|ci)[\(:]?/i);
160
+ const type = typeMatch ? typeMatch[1].toLowerCase() : undefined;
161
+ commits.push({
162
+ hash: entry.hash,
163
+ timestamp: commitDate,
164
+ message,
165
+ type,
166
+ wuId,
167
+ });
168
+ }
169
+ return commits;
170
+ }
171
+ catch (err) {
172
+ console.warn(`${LOG_PREFIX} Could not load git commits: ${err.message}`);
173
+ return [];
174
+ }
175
+ }
176
+ /**
177
+ * Load skip-gates audit entries
178
+ */
179
+ async function loadSkipGatesEntries(baseDir) {
180
+ const auditPath = join(baseDir, SKIP_GATES_PATH);
181
+ if (!existsSync(auditPath)) {
182
+ return [];
183
+ }
184
+ try {
185
+ const content = await readFile(auditPath, { encoding: 'utf-8' });
186
+ const lines = content.split('\n').filter((line) => line.trim());
187
+ const entries = [];
188
+ for (const line of lines) {
189
+ try {
190
+ const raw = JSON.parse(line);
191
+ if (raw.timestamp && raw.wu_id && raw.reason && raw.gate) {
192
+ entries.push({
193
+ timestamp: new Date(raw.timestamp),
194
+ wuId: raw.wu_id,
195
+ reason: raw.reason,
196
+ gate: raw.gate,
197
+ });
198
+ }
199
+ }
200
+ catch {
201
+ // Skip invalid JSON lines
202
+ }
203
+ }
204
+ return entries;
205
+ }
206
+ catch {
207
+ return [];
208
+ }
209
+ }
210
+ /**
211
+ * Calculate lane health from WU metrics
212
+ */
213
+ export function calculateLaneHealthFromWUs(wuMetrics) {
214
+ // Use captureMetricsSnapshot with 'lanes' type
215
+ const snapshot = captureMetricsSnapshot({
216
+ commits: [],
217
+ wuMetrics,
218
+ skipGatesEntries: [],
219
+ weekStart: new Date(),
220
+ weekEnd: new Date(),
221
+ type: 'lanes',
222
+ });
223
+ return snapshot.lanes ?? { lanes: [], totalActive: 0, totalBlocked: 0, totalCompleted: 0 };
224
+ }
225
+ /**
226
+ * Calculate DORA metrics from data
227
+ */
228
+ export function calculateDoraFromData(input) {
229
+ return calculateDORAMetrics(input.commits, input.skipGatesEntries, input.wuMetrics, input.weekStart, input.weekEnd);
230
+ }
231
+ /**
232
+ * Calculate flow state from WU metrics
233
+ */
234
+ export function calculateFlowFromWUs(wuMetrics) {
235
+ return calculateFlowState(wuMetrics);
236
+ }
237
+ /**
238
+ * Format lanes output
239
+ */
240
+ export function formatLanesOutput(lanes, format) {
241
+ if (format === 'table') {
242
+ const lines = [];
243
+ lines.push('LANE HEALTH');
244
+ lines.push('═══════════════════════════════════════════════════════════════');
245
+ lines.push(`Total Active: ${lanes.totalActive} | Blocked: ${lanes.totalBlocked} | Completed: ${lanes.totalCompleted}`);
246
+ lines.push('');
247
+ for (const lane of lanes.lanes) {
248
+ const statusIcon = lane.status === 'healthy' ? '[ok]' : lane.status === 'at-risk' ? '[!]' : '[x]';
249
+ lines.push(`${statusIcon} ${lane.lane.padEnd(25)} ${lane.wusCompleted} done, ${lane.wusInProgress} active, ${lane.wusBlocked} blocked`);
250
+ }
251
+ return lines.join('\n');
252
+ }
253
+ return JSON.stringify(lanes, null, 2);
254
+ }
255
+ /**
256
+ * Format DORA output
257
+ */
258
+ export function formatDoraOutput(dora, format) {
259
+ if (format === 'table') {
260
+ const lines = [];
261
+ lines.push('DORA METRICS');
262
+ lines.push('═══════════════════════════════════════════════════════════════');
263
+ lines.push(`Deployment Frequency: ${dora.deploymentFrequency.deploysPerWeek}/week (${dora.deploymentFrequency.status})`);
264
+ lines.push(`Lead Time: ${dora.leadTimeForChanges.medianHours}h median (${dora.leadTimeForChanges.status})`);
265
+ lines.push(`Change Failure Rate: ${dora.changeFailureRate.failurePercentage}% (${dora.changeFailureRate.status})`);
266
+ lines.push(`MTTR: ${dora.meanTimeToRecovery.averageHours}h (${dora.meanTimeToRecovery.status})`);
267
+ return lines.join('\n');
268
+ }
269
+ return JSON.stringify(dora, null, 2);
270
+ }
271
+ /**
272
+ * Format flow output
273
+ */
274
+ export function formatFlowOutput(flow, format) {
275
+ if (format === 'table') {
276
+ const lines = [];
277
+ lines.push('FLOW STATE');
278
+ lines.push('═══════════════════════════════════════════════════════════════');
279
+ lines.push(`Ready: ${flow.ready} | In Progress: ${flow.inProgress}`);
280
+ lines.push(`Blocked: ${flow.blocked} | Waiting: ${flow.waiting}`);
281
+ lines.push(`Done: ${flow.done} | Total Active: ${flow.totalActive}`);
282
+ return lines.join('\n');
283
+ }
284
+ return JSON.stringify(flow, null, 2);
285
+ }
286
+ /**
287
+ * Run lanes subcommand
288
+ */
289
+ export async function runLanesSubcommand(opts) {
290
+ const baseDir = process.cwd();
291
+ console.log(`${LOG_PREFIX} Calculating lane health...`);
292
+ const wuMetrics = await loadWUMetrics(baseDir);
293
+ console.log(`${LOG_PREFIX} Found ${wuMetrics.length} WUs`);
294
+ const lanes = calculateLaneHealthFromWUs(wuMetrics);
295
+ const output = formatLanesOutput(lanes, opts.format);
296
+ console.log('');
297
+ console.log(output);
298
+ if (!opts.dryRun) {
299
+ await writeOutput(baseDir, opts.output, { type: 'lanes', data: lanes });
300
+ }
301
+ }
302
+ /**
303
+ * Run dora subcommand
304
+ */
305
+ export async function runDoraSubcommand(opts) {
306
+ const baseDir = process.cwd();
307
+ const { weekStart, weekEnd } = calculateWeekRange(opts.days);
308
+ console.log(`${LOG_PREFIX} Calculating DORA metrics...`);
309
+ console.log(`${LOG_PREFIX} Date range: ${weekStart.toISOString().split('T')[0]} to ${weekEnd.toISOString().split('T')[0]}`);
310
+ const [wuMetrics, commits, skipGatesEntries] = await Promise.all([
311
+ loadWUMetrics(baseDir),
312
+ loadGitCommits(weekStart, weekEnd),
313
+ loadSkipGatesEntries(baseDir),
314
+ ]);
315
+ console.log(`${LOG_PREFIX} Found ${wuMetrics.length} WUs, ${commits.length} commits`);
316
+ const dora = calculateDoraFromData({ commits, wuMetrics, skipGatesEntries, weekStart, weekEnd });
317
+ const output = formatDoraOutput(dora, opts.format);
318
+ console.log('');
319
+ console.log(output);
320
+ if (!opts.dryRun) {
321
+ await writeOutput(baseDir, opts.output, { type: 'dora', data: dora });
322
+ }
323
+ }
324
+ /**
325
+ * Run flow subcommand
326
+ */
327
+ export async function runFlowSubcommand(opts) {
328
+ const baseDir = process.cwd();
329
+ console.log(`${LOG_PREFIX} Calculating flow state...`);
330
+ const wuMetrics = await loadWUMetrics(baseDir);
331
+ console.log(`${LOG_PREFIX} Found ${wuMetrics.length} WUs`);
332
+ const flow = calculateFlowFromWUs(wuMetrics);
333
+ const output = formatFlowOutput(flow, opts.format);
334
+ console.log('');
335
+ console.log(output);
336
+ if (!opts.dryRun) {
337
+ await writeOutput(baseDir, opts.output, { type: 'flow', data: flow });
338
+ }
339
+ }
340
+ /**
341
+ * Run all metrics (default)
342
+ */
343
+ export async function runAllSubcommand(opts) {
344
+ const baseDir = process.cwd();
345
+ const { weekStart, weekEnd } = calculateWeekRange(opts.days);
346
+ console.log(`${LOG_PREFIX} Capturing all metrics...`);
347
+ console.log(`${LOG_PREFIX} Date range: ${weekStart.toISOString().split('T')[0]} to ${weekEnd.toISOString().split('T')[0]}`);
348
+ const [wuMetrics, commits, skipGatesEntries] = await Promise.all([
349
+ loadWUMetrics(baseDir),
350
+ loadGitCommits(weekStart, weekEnd),
351
+ loadSkipGatesEntries(baseDir),
352
+ ]);
353
+ console.log(`${LOG_PREFIX} Found ${wuMetrics.length} WUs, ${commits.length} commits`);
354
+ const input = {
355
+ commits,
356
+ wuMetrics,
357
+ skipGatesEntries,
358
+ weekStart,
359
+ weekEnd,
360
+ type: 'all',
361
+ };
362
+ const snapshot = captureMetricsSnapshot(input);
363
+ // Format based on output preference
364
+ if (opts.format === 'table') {
365
+ if (snapshot.dora) {
366
+ console.log('');
367
+ console.log(formatDoraOutput(snapshot.dora, 'table'));
368
+ }
369
+ if (snapshot.lanes) {
370
+ console.log('');
371
+ console.log(formatLanesOutput(snapshot.lanes, 'table'));
372
+ }
373
+ if (snapshot.flow) {
374
+ console.log('');
375
+ console.log(formatFlowOutput(snapshot.flow, 'table'));
376
+ }
377
+ }
378
+ else {
379
+ console.log('');
380
+ console.log(JSON.stringify(snapshot, null, 2));
381
+ }
382
+ if (!opts.dryRun) {
383
+ await writeOutput(baseDir, opts.output, {
384
+ type: 'all',
385
+ capturedAt: new Date().toISOString(),
386
+ dateRange: {
387
+ start: weekStart.toISOString(),
388
+ end: weekEnd.toISOString(),
389
+ },
390
+ snapshot,
391
+ });
392
+ }
393
+ }
394
+ /**
395
+ * Write output to file
396
+ */
397
+ async function writeOutput(baseDir, outputPath, data) {
398
+ const fullPath = join(baseDir, outputPath);
399
+ const outputDir = dirname(fullPath);
400
+ if (!existsSync(outputDir)) {
401
+ await mkdir(outputDir, { recursive: true });
402
+ }
403
+ await writeFile(fullPath, JSON.stringify(data, null, 2), { encoding: 'utf-8' });
404
+ console.log(`${LOG_PREFIX} Output written to: ${fullPath}`);
405
+ }
406
+ /**
407
+ * Main entry point
408
+ */
409
+ async function main() {
410
+ const opts = parseCommand(process.argv);
411
+ switch (opts.subcommand) {
412
+ case 'lanes':
413
+ await runLanesSubcommand(opts);
414
+ break;
415
+ case 'dora':
416
+ await runDoraSubcommand(opts);
417
+ break;
418
+ case 'flow':
419
+ await runFlowSubcommand(opts);
420
+ break;
421
+ case 'all':
422
+ default:
423
+ await runAllSubcommand(opts);
424
+ break;
425
+ }
426
+ }
427
+ // Guard main() for testability
428
+ import { fileURLToPath } from 'node:url';
429
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
430
+ main().catch((err) => {
431
+ die(`Metrics command failed: ${err.message}`);
432
+ });
433
+ }