@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.
- package/README.md +19 -0
- package/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/docs-sync.js +72 -326
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/sync-templates.js +212 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- 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
|
+
}
|