@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,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
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @file sync-templates.ts
3
+ * Sync internal docs to CLI templates for release-cycle maintenance (WU-1123)
4
+ *
5
+ * This script syncs source docs from the hellmai/os repo to the templates
6
+ * directory, applying template variable substitutions:
7
+ * - Onboarding docs -> templates/core/ai/onboarding/
8
+ * - Claude skills -> templates/vendors/claude/.claude/skills/
9
+ * - Core docs (LUMENFLOW.md, constraints.md) -> templates/core/
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { createWUParser } from '@lumenflow/core';
14
+ // Template variable patterns
15
+ const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
16
+ /**
17
+ * CLI option definitions for sync-templates command
18
+ */
19
+ const SYNC_TEMPLATES_OPTIONS = {
20
+ dryRun: {
21
+ name: 'dry-run',
22
+ flags: '--dry-run',
23
+ description: 'Show what would be synced without writing files',
24
+ default: false,
25
+ },
26
+ verbose: {
27
+ name: 'verbose',
28
+ flags: '--verbose',
29
+ description: 'Show detailed output',
30
+ default: false,
31
+ },
32
+ };
33
+ /**
34
+ * Parse sync-templates command options
35
+ */
36
+ export function parseSyncTemplatesOptions() {
37
+ const opts = createWUParser({
38
+ name: 'sync-templates',
39
+ description: 'Sync internal docs to CLI templates for release-cycle maintenance',
40
+ options: Object.values(SYNC_TEMPLATES_OPTIONS),
41
+ });
42
+ return {
43
+ dryRun: opts['dry-run'] ?? false,
44
+ verbose: opts.verbose ?? false,
45
+ };
46
+ }
47
+ /**
48
+ * Convert source content to template format by replacing:
49
+ * - YYYY-MM-DD dates with {{DATE}}
50
+ * - Absolute project paths with {{PROJECT_ROOT}}
51
+ */
52
+ export function convertToTemplate(content, projectRoot) {
53
+ let output = content;
54
+ // Replace dates with {{DATE}}
55
+ output = output.replace(DATE_PATTERN, '{{DATE}}');
56
+ // Replace absolute project paths with {{PROJECT_ROOT}}
57
+ // Escape special regex characters in the path
58
+ const escapedPath = projectRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ const pathPattern = new RegExp(escapedPath, 'g');
60
+ output = output.replace(pathPattern, '{{PROJECT_ROOT}}');
61
+ return output;
62
+ }
63
+ /**
64
+ * Get the templates directory path
65
+ */
66
+ function getTemplatesDir(projectRoot) {
67
+ return path.join(projectRoot, 'packages', '@lumenflow', 'cli', 'templates');
68
+ }
69
+ /**
70
+ * Ensure directory exists
71
+ */
72
+ function ensureDir(dirPath) {
73
+ if (!fs.existsSync(dirPath)) {
74
+ fs.mkdirSync(dirPath, { recursive: true });
75
+ }
76
+ }
77
+ /**
78
+ * Sync a single file to templates
79
+ */
80
+ function syncFile(sourcePath, targetPath, projectRoot, result, dryRun = false) {
81
+ try {
82
+ if (!fs.existsSync(sourcePath)) {
83
+ result.errors.push(`Source not found: ${sourcePath}`);
84
+ return;
85
+ }
86
+ const content = fs.readFileSync(sourcePath, 'utf-8');
87
+ const templateContent = convertToTemplate(content, projectRoot);
88
+ if (!dryRun) {
89
+ ensureDir(path.dirname(targetPath));
90
+ fs.writeFileSync(targetPath, templateContent);
91
+ }
92
+ result.synced.push(path.relative(projectRoot, targetPath));
93
+ }
94
+ catch (error) {
95
+ result.errors.push(`Error syncing ${sourcePath}: ${error.message}`);
96
+ }
97
+ }
98
+ /**
99
+ * Sync onboarding docs to templates/core/ai/onboarding/
100
+ */
101
+ export async function syncOnboardingDocs(projectRoot, dryRun = false) {
102
+ const result = { synced: [], errors: [] };
103
+ const sourceDir = path.join(projectRoot, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
104
+ const targetDir = path.join(getTemplatesDir(projectRoot), 'core', 'ai', 'onboarding');
105
+ if (!fs.existsSync(sourceDir)) {
106
+ result.errors.push(`Onboarding source directory not found: ${sourceDir}`);
107
+ return result;
108
+ }
109
+ const files = fs.readdirSync(sourceDir).filter((f) => f.endsWith('.md'));
110
+ for (const file of files) {
111
+ const sourcePath = path.join(sourceDir, file);
112
+ const targetPath = path.join(targetDir, `${file}.template`);
113
+ syncFile(sourcePath, targetPath, projectRoot, result, dryRun);
114
+ }
115
+ return result;
116
+ }
117
+ /**
118
+ * Sync Claude skills to templates/vendors/claude/.claude/skills/
119
+ */
120
+ export async function syncSkillsToTemplates(projectRoot, dryRun = false) {
121
+ const result = { synced: [], errors: [] };
122
+ const sourceDir = path.join(projectRoot, '.claude', 'skills');
123
+ const targetDir = path.join(getTemplatesDir(projectRoot), 'vendors', 'claude', '.claude', 'skills');
124
+ if (!fs.existsSync(sourceDir)) {
125
+ result.errors.push(`Skills source directory not found: ${sourceDir}`);
126
+ return result;
127
+ }
128
+ // Get all skill directories
129
+ const skillDirs = fs
130
+ .readdirSync(sourceDir, { withFileTypes: true })
131
+ .filter((d) => d.isDirectory())
132
+ .map((d) => d.name);
133
+ for (const skillName of skillDirs) {
134
+ const skillSourceDir = path.join(sourceDir, skillName);
135
+ const skillTargetDir = path.join(targetDir, skillName);
136
+ // Look for SKILL.md file
137
+ const skillFile = path.join(skillSourceDir, 'SKILL.md');
138
+ if (fs.existsSync(skillFile)) {
139
+ const targetPath = path.join(skillTargetDir, 'SKILL.md.template');
140
+ syncFile(skillFile, targetPath, projectRoot, result, dryRun);
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+ /**
146
+ * Sync core docs (LUMENFLOW.md, constraints.md) to templates/core/
147
+ */
148
+ export async function syncCoreDocs(projectRoot, dryRun = false) {
149
+ const result = { synced: [], errors: [] };
150
+ const templatesDir = getTemplatesDir(projectRoot);
151
+ // Sync LUMENFLOW.md
152
+ const lumenflowSource = path.join(projectRoot, 'LUMENFLOW.md');
153
+ const lumenflowTarget = path.join(templatesDir, 'core', 'LUMENFLOW.md.template');
154
+ syncFile(lumenflowSource, lumenflowTarget, projectRoot, result, dryRun);
155
+ // Sync constraints.md
156
+ const constraintsSource = path.join(projectRoot, '.lumenflow', 'constraints.md');
157
+ const constraintsTarget = path.join(templatesDir, 'core', '.lumenflow', 'constraints.md.template');
158
+ syncFile(constraintsSource, constraintsTarget, projectRoot, result, dryRun);
159
+ return result;
160
+ }
161
+ /**
162
+ * Sync all templates
163
+ */
164
+ export async function syncTemplates(projectRoot, dryRun = false) {
165
+ const onboarding = await syncOnboardingDocs(projectRoot, dryRun);
166
+ const skills = await syncSkillsToTemplates(projectRoot, dryRun);
167
+ const core = await syncCoreDocs(projectRoot, dryRun);
168
+ return { onboarding, skills, core };
169
+ }
170
+ /**
171
+ * CLI entry point
172
+ */
173
+ export async function main() {
174
+ const opts = parseSyncTemplatesOptions();
175
+ const projectRoot = process.cwd();
176
+ console.log('[sync-templates] Syncing internal docs to CLI templates...');
177
+ if (opts.dryRun) {
178
+ console.log(' (dry-run mode - no files will be written)');
179
+ }
180
+ const result = await syncTemplates(projectRoot, opts.dryRun);
181
+ // Print results
182
+ const sections = [
183
+ { name: 'Onboarding docs', data: result.onboarding },
184
+ { name: 'Claude skills', data: result.skills },
185
+ { name: 'Core docs', data: result.core },
186
+ ];
187
+ let totalSynced = 0;
188
+ let totalErrors = 0;
189
+ for (const section of sections) {
190
+ if (section.data.synced.length > 0 || section.data.errors.length > 0) {
191
+ console.log(`\n${section.name}:`);
192
+ if (section.data.synced.length > 0) {
193
+ section.data.synced.forEach((f) => console.log(` + ${f}`));
194
+ totalSynced += section.data.synced.length;
195
+ }
196
+ if (section.data.errors.length > 0) {
197
+ section.data.errors.forEach((e) => console.log(` ! ${e}`));
198
+ totalErrors += section.data.errors.length;
199
+ }
200
+ }
201
+ }
202
+ console.log(`\n[sync-templates] Done! Synced ${totalSynced} files.`);
203
+ if (totalErrors > 0) {
204
+ console.log(` ${totalErrors} error(s) occurred.`);
205
+ process.exitCode = 1;
206
+ }
207
+ }
208
+ // CLI entry point
209
+ import { runCLI } from './cli-entry-point.js';
210
+ if (import.meta.main) {
211
+ runCLI(main);
212
+ }