@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,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
|
+
}
|