@lumenflow/cli 1.5.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 (41) 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/dist/wu-recover.js +329 -0
  40. package/dist/wu-status.js +188 -0
  41. package/package.json +37 -7
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file validate-backlog-sync.ts
4
+ * @description Validates backlog.md is in sync with WU YAML files (WU-1111)
5
+ *
6
+ * Checks that all WU YAML files are referenced in backlog.md and vice versa.
7
+ * This is the TypeScript replacement for tools/validate-backlog-sync.js.
8
+ *
9
+ * Usage:
10
+ * validate-backlog-sync # Validate sync
11
+ *
12
+ * Exit codes:
13
+ * 0 - Backlog is in sync
14
+ * 1 - Sync issues found
15
+ *
16
+ * @see {@link docs/04-operations/tasks/backlog.md} - Backlog file
17
+ */
18
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
22
+ const LOG_PREFIX = '[validate-backlog-sync]';
23
+ /**
24
+ * Extract WU IDs from backlog.md content
25
+ *
26
+ * @param content - backlog.md content
27
+ * @returns Array of WU IDs found
28
+ */
29
+ function extractWUIDsFromBacklog(content) {
30
+ const wuIds = [];
31
+ const pattern = /WU-\d+/gi;
32
+ let match;
33
+ while ((match = pattern.exec(content)) !== null) {
34
+ const wuId = match[0].toUpperCase();
35
+ if (!wuIds.includes(wuId)) {
36
+ wuIds.push(wuId);
37
+ }
38
+ }
39
+ return wuIds;
40
+ }
41
+ /**
42
+ * Get all WU IDs from YAML files
43
+ *
44
+ * @param wuDir - Path to WU directory
45
+ * @returns Array of WU IDs
46
+ */
47
+ function getWUIDsFromFiles(wuDir) {
48
+ if (!existsSync(wuDir)) {
49
+ return [];
50
+ }
51
+ return readdirSync(wuDir)
52
+ .filter((f) => f.endsWith('.yaml'))
53
+ .map((f) => f.replace('.yaml', '').toUpperCase());
54
+ }
55
+ /**
56
+ * Validate that backlog.md is in sync with WU YAML files
57
+ *
58
+ * @param options - Validation options
59
+ * @param options.cwd - Working directory (default: process.cwd())
60
+ * @returns Validation result
61
+ */
62
+ export async function validateBacklogSync(options = {}) {
63
+ const { cwd = process.cwd() } = options;
64
+ const errors = [];
65
+ const warnings = [];
66
+ // Paths
67
+ const backlogPath = path.join(cwd, 'docs', '04-operations', 'tasks', 'backlog.md');
68
+ const wuDir = path.join(cwd, 'docs', '04-operations', 'tasks', 'wu');
69
+ // Check backlog.md exists
70
+ if (!existsSync(backlogPath)) {
71
+ errors.push(`Backlog file not found: ${backlogPath}`);
72
+ return { valid: false, errors, warnings, wuCount: 0, backlogCount: 0 };
73
+ }
74
+ // Get WU IDs from files
75
+ const wuIdsFromFiles = getWUIDsFromFiles(wuDir);
76
+ // Get WU IDs from backlog
77
+ const backlogContent = readFileSync(backlogPath, {
78
+ encoding: FILE_SYSTEM.UTF8,
79
+ });
80
+ const wuIdsFromBacklog = extractWUIDsFromBacklog(backlogContent);
81
+ // Check for WUs in files but not in backlog
82
+ for (const wuId of wuIdsFromFiles) {
83
+ if (!wuIdsFromBacklog.includes(wuId)) {
84
+ errors.push(`${wuId} not found in backlog.md (exists as ${wuId}.yaml)`);
85
+ }
86
+ }
87
+ // Check for WUs in backlog but not as files (warning only)
88
+ for (const wuId of wuIdsFromBacklog) {
89
+ if (!wuIdsFromFiles.includes(wuId)) {
90
+ warnings.push(`${wuId} referenced in backlog.md but ${wuId}.yaml not found`);
91
+ }
92
+ }
93
+ return {
94
+ valid: errors.length === 0,
95
+ errors,
96
+ warnings,
97
+ wuCount: wuIdsFromFiles.length,
98
+ backlogCount: wuIdsFromBacklog.length,
99
+ };
100
+ }
101
+ /**
102
+ * Main CLI entry point
103
+ */
104
+ async function main() {
105
+ const args = process.argv.slice(2);
106
+ // Parse arguments
107
+ let cwd = process.cwd();
108
+ for (let i = 0; i < args.length; i++) {
109
+ const arg = args[i];
110
+ if (arg === '--cwd' || arg === '-C') {
111
+ cwd = args[++i];
112
+ }
113
+ else if (arg === '--help' || arg === '-h') {
114
+ console.log(`Usage: validate-backlog-sync [options]
115
+
116
+ Validate that backlog.md is in sync with WU YAML files.
117
+
118
+ Options:
119
+ --cwd, -C DIR Working directory (default: current directory)
120
+ -h, --help Show this help message
121
+
122
+ Examples:
123
+ validate-backlog-sync
124
+ `);
125
+ process.exit(0);
126
+ }
127
+ }
128
+ console.log(`${LOG_PREFIX} Validating backlog sync...`);
129
+ const result = await validateBacklogSync({ cwd });
130
+ if (result.errors.length > 0) {
131
+ console.log(`${EMOJI.FAILURE} Sync errors:`);
132
+ result.errors.forEach((e) => console.log(` ${e}`));
133
+ }
134
+ if (result.warnings.length > 0) {
135
+ console.log(`${EMOJI.WARNING} Warnings:`);
136
+ result.warnings.forEach((w) => console.log(` ${w}`));
137
+ }
138
+ console.log(`${LOG_PREFIX} WU files: ${result.wuCount}, Backlog references: ${result.backlogCount}`);
139
+ if (result.valid) {
140
+ console.log(`${EMOJI.SUCCESS} Backlog is in sync`);
141
+ }
142
+ else {
143
+ process.exit(1);
144
+ }
145
+ }
146
+ // Guard main() for testability
147
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
148
+ main().catch((error) => {
149
+ console.error(`${LOG_PREFIX} Unexpected error:`, error);
150
+ process.exit(1);
151
+ });
152
+ }
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file validate-skills-spec.ts
4
+ * @description Validates skills spec format (WU-1111)
5
+ *
6
+ * Validates that skill specification files follow the required format
7
+ * with proper sections and structure.
8
+ *
9
+ * Usage:
10
+ * validate-skills-spec SKILL.md # Validate specific file
11
+ * validate-skills-spec --dir ./skills # Validate all in directory
12
+ *
13
+ * Exit codes:
14
+ * 0 - Validation passed
15
+ * 1 - Validation errors found
16
+ *
17
+ * @see {@link .claude/skills/} - Skill definitions
18
+ */
19
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
23
+ const LOG_PREFIX = '[validate-skills-spec]';
24
+ /**
25
+ * Required sections for a valid skill spec
26
+ */
27
+ const REQUIRED_SECTIONS = ['When to Use'];
28
+ /**
29
+ * Recommended sections (produce warnings if missing)
30
+ */
31
+ const RECOMMENDED_SECTIONS = ['Examples'];
32
+ /**
33
+ * Validate a skill specification file
34
+ *
35
+ * @param skillPath - Path to skill spec file
36
+ * @returns Validation result
37
+ */
38
+ export function validateSkillsSpec(skillPath) {
39
+ const errors = [];
40
+ const warnings = [];
41
+ if (!existsSync(skillPath)) {
42
+ errors.push(`Skill file not found: ${skillPath}`);
43
+ return { valid: false, errors, warnings };
44
+ }
45
+ const content = readFileSync(skillPath, { encoding: FILE_SYSTEM.UTF8 });
46
+ const lines = content.split('\n');
47
+ // Check for title heading (# Title)
48
+ const hasTitle = lines.some((line) => /^#\s+\S/.test(line));
49
+ if (!hasTitle) {
50
+ errors.push('Missing title heading (# Skill Name)');
51
+ }
52
+ // Check for required sections
53
+ for (const section of REQUIRED_SECTIONS) {
54
+ const sectionPattern = new RegExp(`^##\\s+${section}`, 'im');
55
+ if (!sectionPattern.test(content)) {
56
+ errors.push(`Missing required section: "## ${section}"`);
57
+ }
58
+ }
59
+ // Check for recommended sections
60
+ for (const section of RECOMMENDED_SECTIONS) {
61
+ const sectionPattern = new RegExp(`^##\\s+${section}`, 'im');
62
+ if (!sectionPattern.test(content)) {
63
+ warnings.push(`Missing recommended section: "## ${section}"`);
64
+ }
65
+ }
66
+ // Check "When to Use" section has content
67
+ const whenToUseMatch = content.match(/##\s+When to Use\s*\n([\s\S]*?)(?=\n##|$)/i);
68
+ if (whenToUseMatch) {
69
+ const sectionContent = whenToUseMatch[1].trim();
70
+ if (sectionContent.length < 20) {
71
+ warnings.push('"When to Use" section has minimal content');
72
+ }
73
+ }
74
+ // Check for minimum overall content
75
+ if (content.length < 100) {
76
+ warnings.push('Skill spec is very short (< 100 characters)');
77
+ }
78
+ return {
79
+ valid: errors.length === 0,
80
+ errors,
81
+ warnings,
82
+ };
83
+ }
84
+ /**
85
+ * Validate all skill specs in a directory
86
+ *
87
+ * @param dir - Directory to scan
88
+ * @returns Map of file path to validation result
89
+ */
90
+ export function validateSkillsSpecDir(dir) {
91
+ const results = new Map();
92
+ if (!existsSync(dir)) {
93
+ results.set(dir, {
94
+ valid: false,
95
+ errors: [`Directory not found: ${dir}`],
96
+ warnings: [],
97
+ });
98
+ return results;
99
+ }
100
+ const entries = readdirSync(dir);
101
+ for (const entry of entries) {
102
+ const entryPath = path.join(dir, entry);
103
+ const stat = statSync(entryPath);
104
+ if (stat.isDirectory()) {
105
+ // Check for SKILL.md in subdirectory
106
+ const skillFile = path.join(entryPath, 'SKILL.md');
107
+ if (existsSync(skillFile)) {
108
+ results.set(entry, validateSkillsSpec(skillFile));
109
+ }
110
+ }
111
+ else if (entry.endsWith('.md')) {
112
+ results.set(entry, validateSkillsSpec(entryPath));
113
+ }
114
+ }
115
+ return results;
116
+ }
117
+ /**
118
+ * Main CLI entry point
119
+ */
120
+ async function main() {
121
+ const args = process.argv.slice(2);
122
+ // Parse arguments
123
+ let skillPath;
124
+ let dir;
125
+ for (let i = 0; i < args.length; i++) {
126
+ const arg = args[i];
127
+ if (arg === '--dir' || arg === '-d') {
128
+ dir = args[++i];
129
+ }
130
+ else if (arg === '--help' || arg === '-h') {
131
+ console.log(`Usage: validate-skills-spec [file.md] [options]
132
+
133
+ Validate skill specification format.
134
+
135
+ Options:
136
+ --dir, -d DIR Validate all specs in directory
137
+ -h, --help Show this help message
138
+
139
+ Examples:
140
+ validate-skills-spec SKILL.md
141
+ validate-skills-spec --dir .claude/skills
142
+ `);
143
+ process.exit(0);
144
+ }
145
+ else if (!skillPath) {
146
+ skillPath = arg;
147
+ }
148
+ }
149
+ if (!skillPath && !dir) {
150
+ console.error(`${LOG_PREFIX} Error: Provide a skill file or --dir option`);
151
+ process.exit(1);
152
+ }
153
+ if (skillPath) {
154
+ // Validate single file
155
+ console.log(`${LOG_PREFIX} Validating ${skillPath}...`);
156
+ const result = validateSkillsSpec(skillPath);
157
+ if (result.errors.length > 0) {
158
+ console.log(`${EMOJI.FAILURE} Validation failed:`);
159
+ result.errors.forEach((e) => console.log(` ${e}`));
160
+ }
161
+ if (result.warnings.length > 0) {
162
+ console.log(`${EMOJI.WARNING} Warnings:`);
163
+ result.warnings.forEach((w) => console.log(` ${w}`));
164
+ }
165
+ if (result.valid) {
166
+ console.log(`${EMOJI.SUCCESS} Skill spec is valid`);
167
+ }
168
+ else {
169
+ process.exit(1);
170
+ }
171
+ }
172
+ else if (dir) {
173
+ // Validate directory
174
+ console.log(`${LOG_PREFIX} Validating skills in ${dir}...`);
175
+ const results = validateSkillsSpecDir(dir);
176
+ let totalValid = 0;
177
+ let totalInvalid = 0;
178
+ for (const [name, result] of results) {
179
+ if (result.errors.length > 0) {
180
+ console.log(`${EMOJI.FAILURE} ${name}:`);
181
+ result.errors.forEach((e) => console.log(` ${e}`));
182
+ totalInvalid++;
183
+ }
184
+ else {
185
+ totalValid++;
186
+ }
187
+ if (result.warnings.length > 0) {
188
+ console.log(`${EMOJI.WARNING} ${name}: ${result.warnings.length} warning(s)`);
189
+ }
190
+ }
191
+ console.log('');
192
+ console.log(`${LOG_PREFIX} Summary:`);
193
+ console.log(` ${EMOJI.SUCCESS} Valid: ${totalValid}`);
194
+ console.log(` ${EMOJI.FAILURE} Invalid: ${totalInvalid}`);
195
+ if (totalInvalid > 0) {
196
+ process.exit(1);
197
+ }
198
+ }
199
+ }
200
+ // Guard main() for testability
201
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
202
+ main().catch((error) => {
203
+ console.error(`${LOG_PREFIX} Unexpected error:`, error);
204
+ process.exit(1);
205
+ });
206
+ }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file validate.ts
4
+ * @description Main WU YAML validator CLI (WU-1111)
5
+ *
6
+ * Validates WU tasks and status consistency. This is the replacement for
7
+ * tools/validate.js that was previously a stub.
8
+ *
9
+ * Usage:
10
+ * validate # Validate all WUs
11
+ * validate --id WU-123 # Validate specific WU
12
+ * validate --strict # Fail on warnings too
13
+ * validate --done-only # Only validate done WUs
14
+ *
15
+ * Exit codes:
16
+ * 0 - All validations passed
17
+ * 1 - Validation errors found
18
+ *
19
+ * @see {@link wu-validate.ts} - Detailed WU validation with schema
20
+ * @see {@link docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md} - WU lifecycle
21
+ */
22
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
25
+ import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
26
+ import { validateWU, validateWUCompleteness } from '@lumenflow/core/dist/wu-schema.js';
27
+ import { FILE_SYSTEM, EMOJI, PATTERNS } from '@lumenflow/core/dist/wu-constants.js';
28
+ const LOG_PREFIX = '[validate]';
29
+ /**
30
+ * Validate a single WU file
31
+ *
32
+ * @param wuPath - Path to WU YAML file
33
+ * @param options - Validation options
34
+ * @returns Validation result
35
+ */
36
+ export function validateSingleWU(wuPath, options = {}) {
37
+ const { strict = false } = options;
38
+ const errors = [];
39
+ const warnings = [];
40
+ // Check file exists
41
+ if (!existsSync(wuPath)) {
42
+ errors.push(`WU file not found: ${wuPath}`);
43
+ return { valid: false, warnings, errors };
44
+ }
45
+ // Read and parse YAML
46
+ let doc;
47
+ try {
48
+ const text = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
49
+ doc = parseYAML(text);
50
+ }
51
+ catch (e) {
52
+ errors.push(`Failed to parse YAML: ${e.message}`);
53
+ return { valid: false, warnings, errors };
54
+ }
55
+ // Schema validation
56
+ const schemaResult = validateWU(doc);
57
+ if (!schemaResult.success) {
58
+ const schemaErrors = schemaResult.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
59
+ errors.push(...schemaErrors);
60
+ return { valid: false, warnings, errors };
61
+ }
62
+ // Completeness validation (soft warnings)
63
+ const completenessResult = validateWUCompleteness(schemaResult.data);
64
+ warnings.push(...completenessResult.warnings);
65
+ // In strict mode, warnings become errors
66
+ if (strict && warnings.length > 0) {
67
+ errors.push(...warnings.map((w) => `[STRICT] ${w}`));
68
+ return { valid: false, warnings: [], errors };
69
+ }
70
+ return { valid: true, warnings, errors };
71
+ }
72
+ /**
73
+ * Validate all WU files in the WU directory
74
+ *
75
+ * @param options - Validation options
76
+ * @returns Summary of all validations
77
+ */
78
+ export function validateAllWUs(options = {}) {
79
+ const { strict = false, doneOnly = false } = options;
80
+ const wuDir = WU_PATHS.WU_DIR();
81
+ if (!existsSync(wuDir)) {
82
+ return {
83
+ totalValid: 0,
84
+ totalInvalid: 1,
85
+ totalWarnings: 0,
86
+ results: [
87
+ {
88
+ wuId: 'DIRECTORY',
89
+ valid: false,
90
+ warnings: [],
91
+ errors: [`WU directory not found: ${wuDir}`],
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ const files = readdirSync(wuDir).filter((f) => f.endsWith('.yaml'));
97
+ const results = [];
98
+ let totalValid = 0;
99
+ let totalInvalid = 0;
100
+ let totalWarnings = 0;
101
+ for (const file of files) {
102
+ const wuPath = `${wuDir}/${file}`;
103
+ const wuId = file.replace('.yaml', '');
104
+ // Skip if only validating done WUs
105
+ if (doneOnly) {
106
+ try {
107
+ const text = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
108
+ const doc = parseYAML(text);
109
+ if (doc.status !== 'done') {
110
+ continue;
111
+ }
112
+ }
113
+ catch {
114
+ // If we can't read, still validate to catch the error
115
+ }
116
+ }
117
+ const result = validateSingleWU(wuPath, { strict });
118
+ results.push({ wuId, ...result });
119
+ if (result.valid) {
120
+ totalValid++;
121
+ totalWarnings += result.warnings.length;
122
+ }
123
+ else {
124
+ totalInvalid++;
125
+ }
126
+ }
127
+ return { totalValid, totalInvalid, totalWarnings, results };
128
+ }
129
+ /**
130
+ * Main CLI entry point
131
+ */
132
+ async function main() {
133
+ const args = process.argv.slice(2);
134
+ // Parse arguments
135
+ let wuId;
136
+ let strict = false;
137
+ let doneOnly = false;
138
+ for (let i = 0; i < args.length; i++) {
139
+ const arg = args[i];
140
+ if (arg === '--id' || arg === '--wu') {
141
+ wuId = args[++i];
142
+ }
143
+ else if (arg === '--strict' || arg === '-s') {
144
+ strict = true;
145
+ }
146
+ else if (arg === '--done-only') {
147
+ doneOnly = true;
148
+ }
149
+ else if (arg === '--help' || arg === '-h') {
150
+ console.log(`Usage: validate [options]
151
+
152
+ Validate WU YAML files for schema and quality.
153
+
154
+ Options:
155
+ --id, --wu WU-XXX Validate specific WU
156
+ --strict, -s Fail on warnings too
157
+ --done-only Only validate done WUs
158
+ -h, --help Show this help message
159
+
160
+ Examples:
161
+ validate # Validate all WUs
162
+ validate --id WU-123 # Validate specific WU
163
+ validate --strict # Strict mode
164
+ `);
165
+ process.exit(0);
166
+ }
167
+ else if (PATTERNS.WU_ID.test(arg.toUpperCase())) {
168
+ wuId = arg.toUpperCase();
169
+ }
170
+ }
171
+ if (wuId) {
172
+ // Validate single WU
173
+ wuId = wuId.toUpperCase();
174
+ if (!PATTERNS.WU_ID.test(wuId)) {
175
+ console.error(`${LOG_PREFIX} Invalid WU ID: ${wuId}`);
176
+ process.exit(1);
177
+ }
178
+ const wuPath = WU_PATHS.WU(wuId);
179
+ console.log(`${LOG_PREFIX} Validating ${wuId}...`);
180
+ const result = validateSingleWU(wuPath, { strict });
181
+ if (result.errors.length > 0) {
182
+ console.log(`${EMOJI.FAILURE} Validation failed:`);
183
+ result.errors.forEach((e) => console.log(` ${e}`));
184
+ process.exit(1);
185
+ }
186
+ if (result.warnings.length > 0) {
187
+ console.log(`${EMOJI.WARNING} Validation passed with warnings:`);
188
+ result.warnings.forEach((w) => console.log(` ${w}`));
189
+ }
190
+ else {
191
+ console.log(`${EMOJI.SUCCESS} ${wuId} is valid`);
192
+ }
193
+ }
194
+ else {
195
+ // Validate all WUs
196
+ console.log(`${LOG_PREFIX} Validating all WUs${doneOnly ? ' (done only)' : ''}...`);
197
+ const { totalValid, totalInvalid, totalWarnings, results } = validateAllWUs({
198
+ strict,
199
+ doneOnly,
200
+ });
201
+ // Print results
202
+ for (const result of results) {
203
+ if (result.errors.length > 0) {
204
+ console.log(`${EMOJI.FAILURE} ${result.wuId}:`);
205
+ result.errors.forEach((e) => console.log(` ${e}`));
206
+ }
207
+ else if (result.warnings.length > 0) {
208
+ console.log(`${EMOJI.WARNING} ${result.wuId}: ${result.warnings.length} warning(s)`);
209
+ if (process.env.VERBOSE) {
210
+ result.warnings.forEach((w) => console.log(` ${w}`));
211
+ }
212
+ }
213
+ }
214
+ console.log('');
215
+ console.log(`${LOG_PREFIX} Summary:`);
216
+ console.log(` ${EMOJI.SUCCESS} Valid: ${totalValid}`);
217
+ console.log(` ${EMOJI.FAILURE} Invalid: ${totalInvalid}`);
218
+ console.log(` ${EMOJI.WARNING} Warnings: ${totalWarnings}`);
219
+ if (totalInvalid > 0) {
220
+ process.exit(1);
221
+ }
222
+ }
223
+ }
224
+ // Guard main() for testability
225
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
226
+ main().catch((error) => {
227
+ console.error(`${LOG_PREFIX} Unexpected error:`, error);
228
+ process.exit(1);
229
+ });
230
+ }