@llman-sdd/cli 0.1.2 → 0.1.4
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/package.json +4 -3
- package/src/io.ts +3 -2
- package/src/main.ts +79 -119
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llman-sdd/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Spec-driven development workflow CLI (provides both `llman-sdd` and `llmanspec` commands)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
"build": "bun run ./scripts/build-binary.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@llman-sdd/core": "0.1.
|
|
31
|
+
"@llman-sdd/core": "0.1.4",
|
|
32
32
|
"commander": "^15.0.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
|
-
"bun": ">=1.4.0"
|
|
35
|
+
"bun": ">=1.4.0",
|
|
36
|
+
"node": ">=24.0.0"
|
|
36
37
|
}
|
|
37
38
|
}
|
package/src/io.ts
CHANGED
|
@@ -55,8 +55,9 @@ export function makeIo(root: string): CliIo {
|
|
|
55
55
|
try {
|
|
56
56
|
process.kill(pid, 0);
|
|
57
57
|
return true;
|
|
58
|
-
} catch {
|
|
59
|
-
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// EPERM = 进程存在但无权发信号;只有 ESRCH 才能判定死亡。
|
|
60
|
+
return (error as NodeJS.ErrnoException).code !== 'ESRCH';
|
|
60
61
|
}
|
|
61
62
|
},
|
|
62
63
|
mtimeMs: (p) => statSync(full(p)).mtimeMs,
|
package/src/main.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { existsSync,
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
buildReview,
|
|
10
10
|
collectChanges,
|
|
11
11
|
collectSpecs,
|
|
12
|
+
discoverSpecs,
|
|
13
|
+
embeddedTemplates,
|
|
12
14
|
graphMermaid,
|
|
15
|
+
makeEmbeddedTemplateIo,
|
|
13
16
|
nextReqId,
|
|
14
17
|
changeDiff,
|
|
15
18
|
deriveChangeId,
|
|
@@ -17,10 +20,10 @@ import {
|
|
|
17
20
|
loadConfig,
|
|
18
21
|
loadTree,
|
|
19
22
|
newChange,
|
|
20
|
-
parseCapability,
|
|
21
23
|
renderChangesJson,
|
|
22
24
|
runContextRetrieval,
|
|
23
25
|
renderChangesList,
|
|
26
|
+
renderReviewHtml,
|
|
24
27
|
renderSpecsJson,
|
|
25
28
|
renderSpecsList,
|
|
26
29
|
runInit,
|
|
@@ -34,10 +37,10 @@ import {
|
|
|
34
37
|
runList,
|
|
35
38
|
runThaw,
|
|
36
39
|
validateAllSpecs,
|
|
37
|
-
|
|
38
|
-
type GitLike,
|
|
40
|
+
TEMPLATES_ROOT,
|
|
39
41
|
resolveChatConfig,
|
|
40
42
|
unavailableResult,
|
|
43
|
+
type TemplateIo,
|
|
41
44
|
} from '@llman-sdd/core';
|
|
42
45
|
import { Command } from 'commander';
|
|
43
46
|
|
|
@@ -47,23 +50,25 @@ import { makeCliGit, makeIo } from './io.ts';
|
|
|
47
50
|
// package version when running from source.
|
|
48
51
|
const version = process.env.LLMAN_SDD_VERSION ?? VERSION;
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
// Compiled single-file binaries have no on-disk templates and Bun <= 1.4.x has
|
|
54
|
+
// no embedding mechanism, so build-binary.ts injects the template table via
|
|
55
|
+
// define; every non-compiled run keeps reading the real filesystem (npm/source/
|
|
56
|
+
// Node). Both init and `review --export-html` resolve through this one seam.
|
|
57
|
+
const embedded = embeddedTemplates();
|
|
58
|
+
const templateIo: TemplateIo = embedded
|
|
59
|
+
? makeEmbeddedTemplateIo(embedded)
|
|
60
|
+
: {
|
|
61
|
+
exists: (p) => existsSync(p),
|
|
62
|
+
readText: (p) => readFileSync(p, 'utf8'),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Parse all capability specs under llmanspec/specs via core discovery. */
|
|
66
|
+
function loadSpecEntries(): ReturnType<typeof discoverSpecs> {
|
|
67
|
+
return discoverSpecs('llmanspec/specs', makeIo(process.cwd()));
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
function runValidateSpecs(options: { specs?: boolean; check: boolean }): number {
|
|
62
|
-
const
|
|
63
|
-
fileName: path,
|
|
64
|
-
doc: parseCapability(readFileSync(path, 'utf8'), path),
|
|
65
|
-
}));
|
|
66
|
-
const report = validateAllSpecs(entries, makeIo(process.cwd()));
|
|
71
|
+
const report = validateAllSpecs(loadSpecEntries(), makeIo(process.cwd()));
|
|
67
72
|
for (const line of report.lines) console.log(line);
|
|
68
73
|
|
|
69
74
|
let failed = report.failed;
|
|
@@ -88,7 +93,7 @@ program
|
|
|
88
93
|
.option('--update', 'refresh an existing installation')
|
|
89
94
|
.option('--locale <locale>', 'locale for generated templates (defaults to config or en)')
|
|
90
95
|
.action((options: { update?: boolean; locale?: string }) => {
|
|
91
|
-
const result = runInit(process.cwd(), {
|
|
96
|
+
const result = runInit(makeIo(process.cwd()), templateIo, {
|
|
92
97
|
update: options.update ?? false,
|
|
93
98
|
locale: options.locale,
|
|
94
99
|
version,
|
|
@@ -103,7 +108,8 @@ program
|
|
|
103
108
|
.option('--specs', 'validate specs (default and only scope for now)')
|
|
104
109
|
.option('--no-check', 'skip the bdd.run_command check (structural validation only)')
|
|
105
110
|
.action((options: { specs?: boolean; check: boolean }) => {
|
|
106
|
-
process.exit
|
|
111
|
+
// Node 下管道 stdout 写入异步,process.exit 会截断输出;exitCode 等价且安全。
|
|
112
|
+
process.exitCode = runValidateSpecs(options);
|
|
107
113
|
});
|
|
108
114
|
|
|
109
115
|
const change = program
|
|
@@ -118,7 +124,8 @@ change
|
|
|
118
124
|
.action((id: string | undefined, options: { from?: string }) => {
|
|
119
125
|
if ((id === undefined) === (options.from === undefined)) {
|
|
120
126
|
console.error('<CHANGE> and --from are mutually exclusive; pass one or the other');
|
|
121
|
-
process.
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
122
129
|
}
|
|
123
130
|
const io = makeIo(process.cwd());
|
|
124
131
|
const result = newChange(io, { id, from: options.from });
|
|
@@ -132,7 +139,7 @@ change
|
|
|
132
139
|
.argument('<id>')
|
|
133
140
|
.option('--branch-prefix <prefix>', 'feature branch prefix', 'sdd/')
|
|
134
141
|
.action((id: string, options: { branchPrefix: string }) => {
|
|
135
|
-
const git
|
|
142
|
+
const git = makeCliGit(process.cwd());
|
|
136
143
|
const result = startChange(git, makeIo(process.cwd()), id, {
|
|
137
144
|
branchPrefix: options.branchPrefix,
|
|
138
145
|
});
|
|
@@ -175,7 +182,8 @@ change
|
|
|
175
182
|
.action((id: string, options: { into?: string; method: string }) => {
|
|
176
183
|
if (options.method !== 'squash' && options.method !== 'ff') {
|
|
177
184
|
console.error(`invalid --method: ${options.method}`);
|
|
178
|
-
process.
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
return;
|
|
179
187
|
}
|
|
180
188
|
const result = finalizeChange(makeCliGit(process.cwd()), makeIo(process.cwd()), id, {
|
|
181
189
|
into: options.into,
|
|
@@ -194,11 +202,7 @@ program
|
|
|
194
202
|
.option('--json', 'machine-readable output')
|
|
195
203
|
.action((options: { specs?: boolean; json?: boolean }) => {
|
|
196
204
|
if (options.specs) {
|
|
197
|
-
const
|
|
198
|
-
fileName: path,
|
|
199
|
-
doc: parseCapability(readFileSync(path, 'utf8'), path),
|
|
200
|
-
}));
|
|
201
|
-
const summaries = collectSpecs(entries);
|
|
205
|
+
const summaries = collectSpecs(loadSpecEntries());
|
|
202
206
|
console.log(
|
|
203
207
|
options.json ? renderSpecsJson(summaries) : renderSpecsList(summaries).join('\n'),
|
|
204
208
|
);
|
|
@@ -223,13 +227,10 @@ program
|
|
|
223
227
|
const path = join('llmanspec', 'specs', `${item}.feature`);
|
|
224
228
|
if (!existsSync(path)) {
|
|
225
229
|
console.error(`spec not found: ${item}`);
|
|
226
|
-
process.
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
return;
|
|
227
232
|
}
|
|
228
|
-
const
|
|
229
|
-
fileName: specPath,
|
|
230
|
-
doc: parseCapability(readFileSync(specPath, 'utf8'), specPath),
|
|
231
|
-
}));
|
|
232
|
-
const summary = collectSpecs(entries).find((x) => x.id === item);
|
|
233
|
+
const summary = collectSpecs(loadSpecEntries()).find((x) => x.id === item);
|
|
233
234
|
const morphology = summary
|
|
234
235
|
? `\n\n## Morphology\nruleCount=${summary.morphology.ruleCount} enforced=${summary.morphology.ruleEnforcedCount} manual=${summary.morphology.ruleManualCount} pending=${summary.morphology.rulePendingCount} acceptanceCount=${summary.morphology.acceptanceCount}`
|
|
235
236
|
: '';
|
|
@@ -238,16 +239,15 @@ program
|
|
|
238
239
|
}
|
|
239
240
|
if (options.output !== 'json') {
|
|
240
241
|
console.error('only --output json is supported for changes (text format pending)');
|
|
241
|
-
process.
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
242
244
|
}
|
|
243
|
-
const io = makeIo(process.cwd());
|
|
244
245
|
const result = showChangeJson(
|
|
245
246
|
{
|
|
246
|
-
io,
|
|
247
|
-
|
|
247
|
+
io: makeIo(process.cwd()),
|
|
248
|
+
git: makeCliGit(process.cwd()),
|
|
248
249
|
root: process.cwd(),
|
|
249
250
|
specsDir: 'llmanspec/specs',
|
|
250
|
-
now: new Date(),
|
|
251
251
|
},
|
|
252
252
|
item,
|
|
253
253
|
);
|
|
@@ -261,7 +261,8 @@ program
|
|
|
261
261
|
.action((options: { format: string }) => {
|
|
262
262
|
if (options.format !== 'mermaid') {
|
|
263
263
|
console.error(`unsupported format: ${options.format}`);
|
|
264
|
-
process.
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
return;
|
|
265
266
|
}
|
|
266
267
|
console.log(graphMermaid(makeIo(process.cwd()), process.cwd()).join('\n'));
|
|
267
268
|
});
|
|
@@ -314,19 +315,26 @@ archive
|
|
|
314
315
|
.option('--list', 'list entries already in the cold-backup archive')
|
|
315
316
|
.action(
|
|
316
317
|
async (options: { before?: string; keepRecent: string; dryRun?: boolean; list?: boolean }) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
try {
|
|
319
|
+
const sz = await makeWasmSevenZip();
|
|
320
|
+
const root = process.cwd();
|
|
321
|
+
const io = makeIo(root);
|
|
322
|
+
if (options.list) {
|
|
323
|
+
for (const line of await runList(io, sz, root)) console.log(line);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const result = await runFreeze(io, sz, root, {
|
|
327
|
+
before: options.before,
|
|
328
|
+
keepRecent: Number(options.keepRecent),
|
|
329
|
+
dryRun: options.dryRun,
|
|
330
|
+
});
|
|
331
|
+
for (const line of result.lines) console.log(line);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
// Emscripten aborts (e.g. wasm load failure) throw raw RuntimeErrors —
|
|
334
|
+
// keep the CLI surface one-line like thaw does.
|
|
335
|
+
console.error((error as Error).message);
|
|
336
|
+
process.exitCode = 1;
|
|
323
337
|
}
|
|
324
|
-
const result = await runFreeze(io, sz, root, {
|
|
325
|
-
before: options.before,
|
|
326
|
-
keepRecent: Number(options.keepRecent),
|
|
327
|
-
dryRun: options.dryRun,
|
|
328
|
-
});
|
|
329
|
-
for (const line of result.lines) console.log(line);
|
|
330
338
|
},
|
|
331
339
|
);
|
|
332
340
|
|
|
@@ -351,7 +359,7 @@ archive
|
|
|
351
359
|
for (const line of result.lines) console.log(line);
|
|
352
360
|
} catch (error) {
|
|
353
361
|
console.error((error as Error).message);
|
|
354
|
-
process.
|
|
362
|
+
process.exitCode = 1;
|
|
355
363
|
}
|
|
356
364
|
});
|
|
357
365
|
|
|
@@ -364,30 +372,24 @@ review
|
|
|
364
372
|
.option('--json', 'emit structured JSON (signals + summary)')
|
|
365
373
|
.option('--export-html <path>', 'write a self-contained HTML report')
|
|
366
374
|
.action((options: { capability?: string; json?: boolean; exportHtml?: string }) => {
|
|
367
|
-
const entries = collectFeatureFiles('llmanspec/specs').map((path) => ({
|
|
368
|
-
fileName: path,
|
|
369
|
-
doc: parseCapability(readFileSync(path, 'utf8'), path),
|
|
370
|
-
}));
|
|
371
375
|
const config = existsSync('llmanspec/config.yaml')
|
|
372
376
|
? loadConfig(readFileSync('llmanspec/config.yaml', 'utf8'))
|
|
373
377
|
: null;
|
|
374
378
|
const bindings = config?.bdd?.bindings?.filter((b) => b.kind === 'tags') ?? [];
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
(c) => c.hasBinding,
|
|
378
|
-
).length;
|
|
379
|
-
const activeChanges = collectChanges(changeIo, process.cwd(), new Date());
|
|
379
|
+
const io = makeIo(process.cwd());
|
|
380
|
+
const activeChanges = collectChanges(io, process.cwd(), new Date());
|
|
380
381
|
const result = buildReview(
|
|
381
382
|
{
|
|
382
|
-
entries,
|
|
383
|
-
bindings: bindings
|
|
384
|
-
boundChangeCount:
|
|
383
|
+
entries: loadSpecEntries(),
|
|
384
|
+
bindings: bindings.map((b) => ({ kind: 'tags', tags: b.tags })),
|
|
385
|
+
boundChangeCount: activeChanges.filter((c) => c.hasBinding).length,
|
|
385
386
|
activeChanges,
|
|
386
387
|
},
|
|
387
|
-
|
|
388
|
+
io,
|
|
388
389
|
);
|
|
389
390
|
if (options.exportHtml !== undefined) {
|
|
390
|
-
|
|
391
|
+
const template = templateIo.readText(join(TEMPLATES_ROOT, 'shared', 'review.html'));
|
|
392
|
+
writeFileSync(options.exportHtml, renderReviewHtml(template, result));
|
|
391
393
|
console.log(`wrote ${options.exportHtml}`);
|
|
392
394
|
}
|
|
393
395
|
if (options.json) {
|
|
@@ -395,45 +397,8 @@ review
|
|
|
395
397
|
} else {
|
|
396
398
|
console.log(result.lines.join('\n'));
|
|
397
399
|
}
|
|
398
|
-
if (result.exitCode !== 0) process.
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
/** Self-contained HTML report via the v1 shared/review.html template. */
|
|
402
|
-
function renderReviewHtml(result: {
|
|
403
|
-
signals: { kind: string; capability: string; count: number; detail: string }[];
|
|
404
|
-
summary: { criticalCount: number; warningCount: number };
|
|
405
|
-
}): string {
|
|
406
|
-
// main.ts sits at <root>/apps/cli/src — three levels up is the repo root.
|
|
407
|
-
const templatePath = join(
|
|
408
|
-
import.meta.dirname ?? '.',
|
|
409
|
-
'..',
|
|
410
|
-
'..',
|
|
411
|
-
'..',
|
|
412
|
-
'packages',
|
|
413
|
-
'core',
|
|
414
|
-
'templates',
|
|
415
|
-
'shared',
|
|
416
|
-
'review.html',
|
|
417
|
-
);
|
|
418
|
-
const template = readFileSync(templatePath, 'utf8');
|
|
419
|
-
const esc = (input: string): string =>
|
|
420
|
-
input.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
421
|
-
let mermaid = 'graph TD\n';
|
|
422
|
-
const sigJson: unknown[] = [];
|
|
423
|
-
result.signals.forEach((s, idx) => {
|
|
424
|
-
const label = esc(
|
|
425
|
-
`${s.capability} [${s.kind}] = ${s.count} — ${s.detail === '' ? 'ok' : s.detail}`,
|
|
426
|
-
);
|
|
427
|
-
mermaid += ` s${idx}["${label}"]\n`;
|
|
428
|
-
sigJson.push({ kind: s.kind, capability: s.capability, count: s.count, detail: s.detail });
|
|
400
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
429
401
|
});
|
|
430
|
-
return template
|
|
431
|
-
.replaceAll('__CRITICAL__', String(result.summary.criticalCount))
|
|
432
|
-
.replaceAll('__WARNING__', String(result.summary.warningCount))
|
|
433
|
-
.replaceAll('__SIGNALS__', JSON.stringify(sigJson))
|
|
434
|
-
.replaceAll('__MERMAID__', mermaid)
|
|
435
|
-
.replaceAll('__GENERATED__', new Date().toISOString());
|
|
436
|
-
}
|
|
437
402
|
|
|
438
403
|
const indexCmd = program
|
|
439
404
|
.command('index')
|
|
@@ -443,12 +408,7 @@ indexCmd
|
|
|
443
408
|
.command('rebuild')
|
|
444
409
|
.description('Rebuild the pageindex tree from spec IR (no LLM)')
|
|
445
410
|
.action(() => {
|
|
446
|
-
const
|
|
447
|
-
const entries = collectFeatureFiles('llmanspec/specs').map((path) => ({
|
|
448
|
-
fileName: path,
|
|
449
|
-
doc: parseCapability(readFileSync(path, 'utf8'), path),
|
|
450
|
-
}));
|
|
451
|
-
const result = rebuildIndex(indexIo, process.cwd(), 'llmanspec/specs', entries, {
|
|
411
|
+
const result = rebuildIndex(makeIo(process.cwd()), 'llmanspec/specs', loadSpecEntries(), {
|
|
452
412
|
chatModel: process.env.LLMAN_SDD_INDEX_CHAT_MODEL ?? '',
|
|
453
413
|
});
|
|
454
414
|
for (const line of result.lines) console.log(line);
|
|
@@ -458,9 +418,9 @@ indexCmd
|
|
|
458
418
|
.command('check')
|
|
459
419
|
.description('Check index freshness without rebuilding')
|
|
460
420
|
.action(() => {
|
|
461
|
-
const result = checkIndexFreshness(makeIo(process.cwd()),
|
|
421
|
+
const result = checkIndexFreshness(makeIo(process.cwd()), 'llmanspec/specs');
|
|
462
422
|
for (const line of result.lines) console.log(line);
|
|
463
|
-
if (!result.fresh) process.
|
|
423
|
+
if (!result.fresh) process.exitCode = 1;
|
|
464
424
|
});
|
|
465
425
|
|
|
466
426
|
program
|
|
@@ -469,10 +429,11 @@ program
|
|
|
469
429
|
.option('--task <task>', 'natural language task description')
|
|
470
430
|
.option('--paths <paths>', 'comma-separated file paths')
|
|
471
431
|
.option('--top <n>', 'max entries per tier', '5')
|
|
472
|
-
.action((options: { task?: string; paths?: string; top?: string }) => {
|
|
432
|
+
.action(async (options: { task?: string; paths?: string; top?: string }) => {
|
|
473
433
|
if (!options.task && !options.paths) {
|
|
474
434
|
console.error('at least one of --task or --paths is required');
|
|
475
|
-
process.
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
return;
|
|
476
437
|
}
|
|
477
438
|
const config = resolveChatConfig(process.env as Record<string, string | undefined>);
|
|
478
439
|
if (config === null) {
|
|
@@ -480,14 +441,14 @@ program
|
|
|
480
441
|
console.log(JSON.stringify(unavailableResult(), null, 2));
|
|
481
442
|
return;
|
|
482
443
|
}
|
|
483
|
-
const tree = loadTree(makeIo(process.cwd())
|
|
444
|
+
const tree = loadTree(makeIo(process.cwd()));
|
|
484
445
|
if (tree === null) {
|
|
485
446
|
const missing = unavailableResult();
|
|
486
447
|
missing.status.qualityNote = 'index missing — run `llman-sdd index rebuild` first';
|
|
487
448
|
console.log(JSON.stringify(missing, null, 2));
|
|
488
449
|
return;
|
|
489
450
|
}
|
|
490
|
-
|
|
451
|
+
const result = await runContextRetrieval({
|
|
491
452
|
config,
|
|
492
453
|
task: options.task ?? '',
|
|
493
454
|
paths: options.paths,
|
|
@@ -495,9 +456,8 @@ program
|
|
|
495
456
|
tree,
|
|
496
457
|
readFile: (p) => readFileSync(p, 'utf8'),
|
|
497
458
|
root: process.cwd(),
|
|
498
|
-
}).then((resolved) => {
|
|
499
|
-
console.log(JSON.stringify(resolved, null, 2));
|
|
500
459
|
});
|
|
460
|
+
console.log(JSON.stringify(result, null, 2));
|
|
501
461
|
});
|
|
502
462
|
|
|
503
463
|
async function main(): Promise<void> {
|