@lumenflow/cli 1.6.0 → 2.0.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 (39) hide show
  1. package/dist/__tests__/backlog-prune.test.js +478 -0
  2. package/dist/__tests__/deps-operations.test.js +206 -0
  3. package/dist/__tests__/file-operations.test.js +906 -0
  4. package/dist/__tests__/git-operations.test.js +668 -0
  5. package/dist/__tests__/guards-validation.test.js +416 -0
  6. package/dist/__tests__/init-plan.test.js +340 -0
  7. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  8. package/dist/__tests__/metrics-cli.test.js +619 -0
  9. package/dist/__tests__/rotate-progress.test.js +127 -0
  10. package/dist/__tests__/session-coordinator.test.js +109 -0
  11. package/dist/__tests__/state-bootstrap.test.js +432 -0
  12. package/dist/__tests__/trace-gen.test.js +115 -0
  13. package/dist/backlog-prune.js +299 -0
  14. package/dist/deps-add.js +215 -0
  15. package/dist/deps-remove.js +94 -0
  16. package/dist/file-delete.js +236 -0
  17. package/dist/file-edit.js +247 -0
  18. package/dist/file-read.js +197 -0
  19. package/dist/file-write.js +220 -0
  20. package/dist/git-branch.js +187 -0
  21. package/dist/git-diff.js +177 -0
  22. package/dist/git-log.js +230 -0
  23. package/dist/git-status.js +208 -0
  24. package/dist/guard-locked.js +169 -0
  25. package/dist/guard-main-branch.js +202 -0
  26. package/dist/guard-worktree-commit.js +160 -0
  27. package/dist/init-plan.js +337 -0
  28. package/dist/lumenflow-upgrade.js +178 -0
  29. package/dist/metrics-cli.js +433 -0
  30. package/dist/rotate-progress.js +247 -0
  31. package/dist/session-coordinator.js +300 -0
  32. package/dist/state-bootstrap.js +307 -0
  33. package/dist/trace-gen.js +331 -0
  34. package/dist/validate-agent-skills.js +218 -0
  35. package/dist/validate-agent-sync.js +148 -0
  36. package/dist/validate-backlog-sync.js +152 -0
  37. package/dist/validate-skills-spec.js +206 -0
  38. package/dist/validate.js +230 -0
  39. package/package.json +34 -6
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Session Coordinator CLI Command
4
+ *
5
+ * Manages agent sessions - starting, stopping, status, and handoffs.
6
+ * Sessions track which agent is working on which WU and facilitate
7
+ * coordination between multiple agents.
8
+ *
9
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
10
+ *
11
+ * Usage:
12
+ * pnpm session:start --wu WU-1112 --agent claude-code
13
+ * pnpm session:stop --reason "Completed work"
14
+ * pnpm session:status
15
+ * pnpm session:handoff --wu WU-1112 --agent cursor
16
+ */
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { EXIT_CODES, LUMENFLOW_PATHS, FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
20
+ import { runCLI } from './cli-entry-point.js';
21
+ /** Log prefix for console output */
22
+ const LOG_PREFIX = '[session]';
23
+ /**
24
+ * Session subcommands
25
+ */
26
+ export var SessionCommand;
27
+ (function (SessionCommand) {
28
+ SessionCommand["START"] = "start";
29
+ SessionCommand["STOP"] = "stop";
30
+ SessionCommand["STATUS"] = "status";
31
+ SessionCommand["HANDOFF"] = "handoff";
32
+ })(SessionCommand || (SessionCommand = {}));
33
+ /**
34
+ * Parse command line arguments for session-coordinator
35
+ *
36
+ * @param argv - Process argv array
37
+ * @returns Parsed arguments
38
+ */
39
+ export function parseSessionArgs(argv) {
40
+ const args = {};
41
+ // Skip node and script name
42
+ const cliArgs = argv.slice(2);
43
+ for (let i = 0; i < cliArgs.length; i++) {
44
+ const arg = cliArgs[i];
45
+ if (arg === '--help' || arg === '-h') {
46
+ args.help = true;
47
+ }
48
+ else if (arg === '--wu' || arg === '-w') {
49
+ args.wuId = cliArgs[++i];
50
+ }
51
+ else if (arg === '--agent' || arg === '-a') {
52
+ args.agent = cliArgs[++i];
53
+ }
54
+ else if (arg === '--reason' || arg === '-r') {
55
+ args.reason = cliArgs[++i];
56
+ }
57
+ else if (!arg.startsWith('-')) {
58
+ // Subcommand
59
+ if (Object.values(SessionCommand).includes(arg)) {
60
+ args.command = arg;
61
+ }
62
+ }
63
+ }
64
+ // Default to status if no command given
65
+ if (!args.command && !args.help) {
66
+ args.command = SessionCommand.STATUS;
67
+ }
68
+ return args;
69
+ }
70
+ /**
71
+ * Validate session command arguments
72
+ *
73
+ * @param args - Parsed session arguments
74
+ * @returns Validation result
75
+ */
76
+ export function validateSessionCommand(args) {
77
+ const { command, wuId } = args;
78
+ switch (command) {
79
+ case SessionCommand.START:
80
+ if (!wuId) {
81
+ return {
82
+ valid: false,
83
+ error: 'session start requires --wu <id> to specify which WU to work on',
84
+ };
85
+ }
86
+ break;
87
+ case SessionCommand.HANDOFF:
88
+ if (!wuId) {
89
+ return {
90
+ valid: false,
91
+ error: 'session handoff requires --wu <id> to specify which WU to hand off',
92
+ };
93
+ }
94
+ break;
95
+ case SessionCommand.STOP:
96
+ case SessionCommand.STATUS:
97
+ // No required arguments
98
+ break;
99
+ default:
100
+ return {
101
+ valid: false,
102
+ error: `Unknown command: ${command}`,
103
+ };
104
+ }
105
+ return { valid: true };
106
+ }
107
+ /**
108
+ * Get path to current session file
109
+ */
110
+ function getSessionPath() {
111
+ return join(process.cwd(), LUMENFLOW_PATHS.SESSION_CURRENT);
112
+ }
113
+ /**
114
+ * Read current session state
115
+ */
116
+ function readCurrentSession() {
117
+ const path = getSessionPath();
118
+ if (!existsSync(path)) {
119
+ return null;
120
+ }
121
+ try {
122
+ const content = readFileSync(path, { encoding: FILE_SYSTEM.ENCODING });
123
+ return JSON.parse(content);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Write session state
131
+ */
132
+ function writeSession(session) {
133
+ const path = getSessionPath();
134
+ const dir = dirname(path);
135
+ if (!existsSync(dir)) {
136
+ mkdirSync(dir, { recursive: true });
137
+ }
138
+ if (session === null) {
139
+ // Remove session file if clearing
140
+ if (existsSync(path)) {
141
+ const { unlinkSync } = require('node:fs');
142
+ unlinkSync(path);
143
+ }
144
+ }
145
+ else {
146
+ writeFileSync(path, JSON.stringify(session, null, 2), {
147
+ encoding: FILE_SYSTEM.ENCODING,
148
+ });
149
+ }
150
+ }
151
+ /**
152
+ * Print help message for session-coordinator
153
+ */
154
+ /* istanbul ignore next -- CLI entry point */
155
+ function printHelp() {
156
+ console.log(`
157
+ Usage: session <command> [options]
158
+
159
+ Manage agent sessions for WU work coordination.
160
+
161
+ Commands:
162
+ start Start a new session
163
+ stop Stop current session
164
+ status Show current session status
165
+ handoff Hand off session to another agent
166
+
167
+ Options:
168
+ -w, --wu <id> WU ID to work on (required for start/handoff)
169
+ -a, --agent <type> Agent type (e.g., claude-code, cursor, aider)
170
+ -r, --reason <msg> Reason for stopping session
171
+ -h, --help Show this help message
172
+
173
+ Examples:
174
+ session start --wu WU-1112 --agent claude-code
175
+ session stop --reason "Completed acceptance criteria"
176
+ session status
177
+ session handoff --wu WU-1112 --agent cursor
178
+
179
+ Session files are stored in: ${LUMENFLOW_PATHS.SESSION_CURRENT}
180
+ `);
181
+ }
182
+ /**
183
+ * Handle start command
184
+ */
185
+ /* istanbul ignore next -- CLI entry point */
186
+ function handleStart(args) {
187
+ const current = readCurrentSession();
188
+ if (current) {
189
+ console.log(`${LOG_PREFIX} Active session already exists:`);
190
+ console.log(` WU: ${current.wuId}`);
191
+ console.log(` Agent: ${current.agent}`);
192
+ console.log(` Started: ${current.startedAt}`);
193
+ console.log(`\n${LOG_PREFIX} Stop current session first with: session stop`);
194
+ process.exit(EXIT_CODES.ERROR);
195
+ }
196
+ const session = {
197
+ wuId: args.wuId,
198
+ agent: args.agent || 'unknown',
199
+ startedAt: new Date().toISOString(),
200
+ lastActivity: new Date().toISOString(),
201
+ };
202
+ writeSession(session);
203
+ console.log(`${LOG_PREFIX} ✅ Session started`);
204
+ console.log(` WU: ${session.wuId}`);
205
+ console.log(` Agent: ${session.agent}`);
206
+ console.log(` Started: ${session.startedAt}`);
207
+ }
208
+ /**
209
+ * Handle stop command
210
+ */
211
+ /* istanbul ignore next -- CLI entry point */
212
+ function handleStop(args) {
213
+ const current = readCurrentSession();
214
+ if (!current) {
215
+ console.log(`${LOG_PREFIX} No active session to stop.`);
216
+ process.exit(EXIT_CODES.SUCCESS);
217
+ }
218
+ const duration = Date.now() - new Date(current.startedAt).getTime();
219
+ const durationMin = Math.round(duration / 60000);
220
+ console.log(`${LOG_PREFIX} ✅ Session stopped`);
221
+ console.log(` WU: ${current.wuId}`);
222
+ console.log(` Agent: ${current.agent}`);
223
+ console.log(` Duration: ${durationMin} minutes`);
224
+ if (args.reason) {
225
+ console.log(` Reason: ${args.reason}`);
226
+ }
227
+ writeSession(null);
228
+ }
229
+ /**
230
+ * Handle status command
231
+ */
232
+ /* istanbul ignore next -- CLI entry point */
233
+ function handleStatus() {
234
+ const current = readCurrentSession();
235
+ if (!current) {
236
+ console.log(`${LOG_PREFIX} No active session.`);
237
+ console.log(`\n${LOG_PREFIX} Start a session with: session start --wu WU-XXXX`);
238
+ return;
239
+ }
240
+ const duration = Date.now() - new Date(current.startedAt).getTime();
241
+ const durationMin = Math.round(duration / 60000);
242
+ console.log(`${LOG_PREFIX} Active session:`);
243
+ console.log(` WU: ${current.wuId}`);
244
+ console.log(` Agent: ${current.agent}`);
245
+ console.log(` Started: ${current.startedAt}`);
246
+ console.log(` Duration: ${durationMin} minutes`);
247
+ console.log(` Last activity: ${current.lastActivity}`);
248
+ }
249
+ /**
250
+ * Handle handoff command
251
+ */
252
+ /* istanbul ignore next -- CLI entry point */
253
+ function handleHandoff(args) {
254
+ const current = readCurrentSession();
255
+ if (current) {
256
+ console.log(`${LOG_PREFIX} Stopping current session...`);
257
+ handleStop({ reason: `Handoff to ${args.agent || 'another agent'}` });
258
+ }
259
+ console.log(`\n${LOG_PREFIX} Starting new session for handoff...`);
260
+ handleStart(args);
261
+ }
262
+ /**
263
+ * Main entry point for session-coordinator command
264
+ */
265
+ /* istanbul ignore next -- CLI entry point */
266
+ async function main() {
267
+ const args = parseSessionArgs(process.argv);
268
+ if (args.help) {
269
+ printHelp();
270
+ process.exit(EXIT_CODES.SUCCESS);
271
+ }
272
+ const validation = validateSessionCommand(args);
273
+ if (!validation.valid) {
274
+ console.error(`${LOG_PREFIX} Error: ${validation.error}`);
275
+ printHelp();
276
+ process.exit(EXIT_CODES.ERROR);
277
+ }
278
+ switch (args.command) {
279
+ case SessionCommand.START:
280
+ handleStart(args);
281
+ break;
282
+ case SessionCommand.STOP:
283
+ handleStop(args);
284
+ break;
285
+ case SessionCommand.STATUS:
286
+ handleStatus();
287
+ break;
288
+ case SessionCommand.HANDOFF:
289
+ handleHandoff(args);
290
+ break;
291
+ default:
292
+ console.error(`${LOG_PREFIX} Unknown command: ${args.command}`);
293
+ printHelp();
294
+ process.exit(EXIT_CODES.ERROR);
295
+ }
296
+ }
297
+ // Run main if executed directly
298
+ if (import.meta.main) {
299
+ runCLI(main);
300
+ }
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * State Bootstrap Command
4
+ *
5
+ * One-time migration utility from WU YAMLs to event-sourced state store.
6
+ * Reads all WU YAML files and generates corresponding events in the state store.
7
+ *
8
+ * WU-1107: INIT-003 Phase 3c - Migrate state-bootstrap.mjs from PatientPath
9
+ *
10
+ * Usage:
11
+ * pnpm state:bootstrap # Dry-run mode (shows what would be done)
12
+ * pnpm state:bootstrap --execute # Apply changes
13
+ */
14
+ import { readdirSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { parse as parseYaml } from 'yaml';
17
+ import { readFileSync } from 'node:fs';
18
+ import { CLI_FLAGS, EXIT_CODES, EMOJI, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
19
+ /* eslint-disable security/detect-non-literal-fs-filename */
20
+ /** Log prefix for consistent output */
21
+ const LOG_PREFIX = '[state-bootstrap]';
22
+ /**
23
+ * Default configuration for state bootstrap
24
+ */
25
+ export const STATE_BOOTSTRAP_DEFAULTS = {
26
+ /** Default WU directory path */
27
+ wuDir: 'docs/04-operations/tasks/wu',
28
+ /** Default state directory path */
29
+ stateDir: '.lumenflow/state',
30
+ };
31
+ /**
32
+ * Parse command line arguments for state-bootstrap
33
+ */
34
+ export function parseStateBootstrapArgs(argv) {
35
+ const args = {
36
+ dryRun: true,
37
+ wuDir: STATE_BOOTSTRAP_DEFAULTS.wuDir,
38
+ stateDir: STATE_BOOTSTRAP_DEFAULTS.stateDir,
39
+ force: false,
40
+ help: false,
41
+ };
42
+ for (let i = 2; i < argv.length; i++) {
43
+ const arg = argv[i];
44
+ if (arg === CLI_FLAGS.EXECUTE) {
45
+ args.dryRun = false;
46
+ }
47
+ else if (arg === CLI_FLAGS.DRY_RUN) {
48
+ args.dryRun = true;
49
+ }
50
+ else if (arg === CLI_FLAGS.HELP || arg === CLI_FLAGS.HELP_SHORT) {
51
+ args.help = true;
52
+ }
53
+ else if (arg === '--force') {
54
+ args.force = true;
55
+ }
56
+ else if (arg === '--wu-dir' && argv[i + 1]) {
57
+ args.wuDir = argv[++i];
58
+ }
59
+ else if (arg === '--state-dir' && argv[i + 1]) {
60
+ args.stateDir = argv[++i];
61
+ }
62
+ }
63
+ return args;
64
+ }
65
+ /**
66
+ * Convert a date string to ISO timestamp
67
+ * Falls back to start of day if only date is provided
68
+ */
69
+ function toTimestamp(dateStr, fallback) {
70
+ if (!dateStr) {
71
+ if (fallback) {
72
+ return toTimestamp(fallback);
73
+ }
74
+ return new Date().toISOString();
75
+ }
76
+ // If already ISO format, return as-is
77
+ if (dateStr.includes('T')) {
78
+ return dateStr;
79
+ }
80
+ // Convert date-only to ISO timestamp at midnight UTC
81
+ const date = new Date(dateStr);
82
+ if (isNaN(date.getTime())) {
83
+ return new Date().toISOString();
84
+ }
85
+ return date.toISOString();
86
+ }
87
+ /**
88
+ * Infer events from a WU based on its current status
89
+ *
90
+ * Event generation rules:
91
+ * - ready: No events (WU not yet claimed)
92
+ * - in_progress: Generate claim event
93
+ * - blocked: Generate claim + block events
94
+ * - done/completed: Generate claim + complete events
95
+ */
96
+ export function inferEventsFromWu(wu) {
97
+ const events = [];
98
+ // Ready WUs have no events (not yet in the lifecycle)
99
+ if (wu.status === 'ready' || wu.status === 'backlog' || wu.status === 'todo') {
100
+ return events;
101
+ }
102
+ // All other states start with a claim event
103
+ const claimTimestamp = toTimestamp(wu.claimed_at, wu.created);
104
+ events.push({
105
+ type: 'claim',
106
+ wuId: wu.id,
107
+ lane: wu.lane,
108
+ title: wu.title,
109
+ timestamp: claimTimestamp,
110
+ });
111
+ // Handle completed/done status
112
+ if (wu.status === 'done' || wu.status === 'completed') {
113
+ const completeTimestamp = toTimestamp(wu.completed_at, wu.created);
114
+ events.push({
115
+ type: 'complete',
116
+ wuId: wu.id,
117
+ timestamp: completeTimestamp,
118
+ });
119
+ return events;
120
+ }
121
+ // Handle blocked status
122
+ if (wu.status === 'blocked') {
123
+ // Block event timestamp should be after claim
124
+ // We don't have exact block time, so use claim time + 1 second
125
+ const claimDate = new Date(claimTimestamp);
126
+ claimDate.setSeconds(claimDate.getSeconds() + 1);
127
+ events.push({
128
+ type: 'block',
129
+ wuId: wu.id,
130
+ timestamp: claimDate.toISOString(),
131
+ reason: 'Bootstrapped from WU YAML (original reason unknown)',
132
+ });
133
+ return events;
134
+ }
135
+ // in_progress status already has claim event
136
+ return events;
137
+ }
138
+ /**
139
+ * Generate all bootstrap events from a list of WUs, ordered chronologically
140
+ */
141
+ export function generateBootstrapEvents(wus) {
142
+ const allEvents = [];
143
+ for (const wu of wus) {
144
+ const events = inferEventsFromWu(wu);
145
+ allEvents.push(...events);
146
+ }
147
+ // Sort events chronologically
148
+ allEvents.sort((a, b) => {
149
+ const dateA = new Date(a.timestamp).getTime();
150
+ const dateB = new Date(b.timestamp).getTime();
151
+ return dateA - dateB;
152
+ });
153
+ return allEvents;
154
+ }
155
+ /**
156
+ * Load a WU YAML file and extract bootstrap info
157
+ */
158
+ function loadWuYaml(filePath) {
159
+ try {
160
+ const content = readFileSync(filePath, 'utf-8');
161
+ const doc = parseYaml(content);
162
+ if (!doc || typeof doc !== 'object' || !doc.id) {
163
+ return null;
164
+ }
165
+ return {
166
+ id: String(doc.id),
167
+ status: String(doc.status || 'ready'),
168
+ lane: String(doc.lane || 'Unknown'),
169
+ title: String(doc.title || 'Untitled'),
170
+ created: doc.created ? String(doc.created) : undefined,
171
+ claimed_at: doc.claimed_at ? String(doc.claimed_at) : undefined,
172
+ completed_at: doc.completed_at ? String(doc.completed_at) : undefined,
173
+ };
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ /**
180
+ * Run the state bootstrap migration
181
+ */
182
+ export async function runStateBootstrap(args) {
183
+ const result = {
184
+ success: true,
185
+ eventsGenerated: 0,
186
+ eventsWritten: 0,
187
+ skipped: 0,
188
+ warnings: [],
189
+ };
190
+ // Check if WU directory exists
191
+ if (!existsSync(args.wuDir)) {
192
+ result.warnings.push('WU directory not found');
193
+ return result;
194
+ }
195
+ // Check if state file already exists
196
+ const stateFilePath = path.join(args.stateDir, 'wu-events.jsonl');
197
+ if (existsSync(stateFilePath) && !args.force && !args.dryRun) {
198
+ result.success = false;
199
+ result.error = `State file already exists: ${stateFilePath}. Use --force to overwrite.`;
200
+ return result;
201
+ }
202
+ // Load all WU YAML files
203
+ const wus = [];
204
+ const files = readdirSync(args.wuDir).filter((f) => f.endsWith('.yaml'));
205
+ for (const file of files) {
206
+ const filePath = path.join(args.wuDir, file);
207
+ const wu = loadWuYaml(filePath);
208
+ if (wu) {
209
+ wus.push(wu);
210
+ }
211
+ else {
212
+ result.skipped++;
213
+ }
214
+ }
215
+ // Generate events
216
+ const events = generateBootstrapEvents(wus);
217
+ result.eventsGenerated = events.length;
218
+ // In dry-run mode, don't write anything
219
+ if (args.dryRun) {
220
+ return result;
221
+ }
222
+ // Ensure state directory exists
223
+ mkdirSync(args.stateDir, { recursive: true });
224
+ // Write events to state file
225
+ const lines = events.map((event) => JSON.stringify(event));
226
+ const content = lines.length > 0 ? `${lines.join('\n')}\n` : '';
227
+ writeFileSync(stateFilePath, content, 'utf-8');
228
+ result.eventsWritten = events.length;
229
+ return result;
230
+ }
231
+ /**
232
+ * Print help text
233
+ */
234
+ export function printHelp() {
235
+ console.log(`
236
+ ${LOG_PREFIX} State Bootstrap - One-time migration utility
237
+
238
+ Usage:
239
+ pnpm state:bootstrap # Dry-run mode (default, shows what would be done)
240
+ pnpm state:bootstrap --execute # Apply changes
241
+
242
+ Options:
243
+ --execute Execute migration (default is dry-run)
244
+ --dry-run Show what would be done without making changes
245
+ --wu-dir <path> WU YAML directory (default: ${STATE_BOOTSTRAP_DEFAULTS.wuDir})
246
+ --state-dir <path> State store directory (default: ${STATE_BOOTSTRAP_DEFAULTS.stateDir})
247
+ --force Overwrite existing state file
248
+ --help, -h Show this help message
249
+
250
+ This tool:
251
+ ${EMOJI.SUCCESS} Reads all WU YAML files from the WU directory
252
+ ${EMOJI.SUCCESS} Generates events based on WU status (claim, complete, block)
253
+ ${EMOJI.SUCCESS} Writes events to .lumenflow/state/wu-events.jsonl
254
+ ${EMOJI.WARNING} One-time migration - run only when setting up event-sourced state
255
+
256
+ Supported WU statuses:
257
+ ready -> No events (WU not yet claimed)
258
+ in_progress -> claim event
259
+ blocked -> claim + block events
260
+ done -> claim + complete events
261
+ `);
262
+ }
263
+ /**
264
+ * Main function
265
+ */
266
+ async function main() {
267
+ const args = parseStateBootstrapArgs(process.argv);
268
+ if (args.help) {
269
+ printHelp();
270
+ process.exit(EXIT_CODES.SUCCESS);
271
+ }
272
+ console.log(`${LOG_PREFIX} State Bootstrap Migration`);
273
+ console.log(`${LOG_PREFIX} =========================${STRING_LITERALS.NEWLINE}`);
274
+ if (args.dryRun) {
275
+ console.log(`${LOG_PREFIX} ${EMOJI.INFO} DRY-RUN MODE (use --execute to apply changes)${STRING_LITERALS.NEWLINE}`);
276
+ }
277
+ console.log(`${LOG_PREFIX} WU directory: ${args.wuDir}`);
278
+ console.log(`${LOG_PREFIX} State directory: ${args.stateDir}${STRING_LITERALS.NEWLINE}`);
279
+ const result = await runStateBootstrap(args);
280
+ if (!result.success) {
281
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} ${result.error}`);
282
+ process.exit(EXIT_CODES.ERROR);
283
+ }
284
+ // Report warnings
285
+ for (const warning of result.warnings) {
286
+ console.log(`${LOG_PREFIX} ${EMOJI.WARNING} ${warning}`);
287
+ }
288
+ // Summary
289
+ console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} Summary`);
290
+ console.log(`${LOG_PREFIX} ========`);
291
+ console.log(`${LOG_PREFIX} Events generated: ${result.eventsGenerated}`);
292
+ console.log(`${LOG_PREFIX} Events written: ${result.eventsWritten}`);
293
+ console.log(`${LOG_PREFIX} Files skipped: ${result.skipped}`);
294
+ if (args.dryRun && result.eventsGenerated > 0) {
295
+ console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} ${EMOJI.INFO} This was a dry-run. Use --execute to apply changes.`);
296
+ }
297
+ else if (result.eventsWritten > 0) {
298
+ console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} ${EMOJI.SUCCESS} State bootstrap complete!`);
299
+ }
300
+ process.exit(EXIT_CODES.SUCCESS);
301
+ }
302
+ // Guard main() for testability
303
+ import { fileURLToPath } from 'node:url';
304
+ import { runCLI } from './cli-entry-point.js';
305
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
306
+ runCLI(main);
307
+ }