@llman-sdd/cli 0.7.0 → 0.7.1
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 +2 -2
- package/src/commands/archive.ts +5 -0
- package/src/commands/change.ts +4 -4
- package/src/commands/context.ts +3 -0
- package/src/commands/init.ts +3 -0
- package/src/commands/validate.ts +10 -7
- package/src/io.ts +11 -2
- package/src/progress.ts +59 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llman-sdd/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Spec-driven development workflow CLI (the `llman-sdd` command)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"build": "bun run ./scripts/build-binary.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@llman-sdd/core": "0.7.
|
|
30
|
+
"@llman-sdd/core": "0.7.1",
|
|
31
31
|
"commander": "^15.0.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
package/src/commands/archive.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { Command } from 'commander';
|
|
|
14
14
|
|
|
15
15
|
import { CliError } from '../cli-shared.ts';
|
|
16
16
|
import { makeCliGit, makeIo } from '../io.ts';
|
|
17
|
+
import { progressNote } from '../progress.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* r24: warn (never block) when freeze/thaw runs outside the main checkout —
|
|
@@ -69,6 +70,8 @@ export function registerArchive(program: Command): void {
|
|
|
69
70
|
for (const line of await runList(io, sz, root)) console.log(line);
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
73
|
+
// 7z 压缩可能耗时 — 进度提示防止看上去卡住(B1);dry-run/list 不走压缩
|
|
74
|
+
if (!options.dryRun) progressNote('freeze.pack', root);
|
|
72
75
|
const result = await runFreeze(io, sz, root, {
|
|
73
76
|
before: options.before,
|
|
74
77
|
keepRecent: Number(options.keepRecent),
|
|
@@ -105,6 +108,8 @@ export function registerArchive(program: Command): void {
|
|
|
105
108
|
const sz = await makeEmbeddedSevenZip();
|
|
106
109
|
const io = makeIo(root);
|
|
107
110
|
try {
|
|
111
|
+
// 7z 解包可能耗时 — 进度提示防止看上去卡住(B1)
|
|
112
|
+
progressNote('thaw.unpack', root);
|
|
108
113
|
const result = await runThaw(io, sz, root, options.change, { dest: options.dest });
|
|
109
114
|
for (const line of result.lines) console.log(line);
|
|
110
115
|
} catch (error) {
|
package/src/commands/change.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
CLOSE_OUT_TASK_HINT,
|
|
@@ -291,9 +291,9 @@ export function registerChange(program: Command): void {
|
|
|
291
291
|
skipCleanTree: true,
|
|
292
292
|
today: new Date().toISOString().slice(0, 10),
|
|
293
293
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
294
|
+
// node:path.basename — platform-aware (win32 backslashes would defeat
|
|
295
|
+
// a hand-rolled lastIndexOf('/')).
|
|
296
|
+
const archiveName = basename(result.archiveDir ?? '');
|
|
297
297
|
console.log(`Change '${id}' archived as '${archiveName}'.`);
|
|
298
298
|
if (result.executedIn !== null) {
|
|
299
299
|
console.log(`executed in target worktree ${result.executedIn}`);
|
package/src/commands/context.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
10
|
|
|
11
11
|
import { CliError, loadSpecEntries, newIo, resolveBackend } from '../cli-shared.ts';
|
|
12
|
+
import { progressNote } from '../progress.ts';
|
|
12
13
|
|
|
13
14
|
// (err path below converted to CliError; resolveBackend may also throw CliError)
|
|
14
15
|
|
|
@@ -45,6 +46,8 @@ export function registerContext(program: Command): void {
|
|
|
45
46
|
console.log(JSON.stringify(unavailableResult(), null, 2));
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
49
|
+
// LLM 链路可能是秒级 — 进度提示防止看上去卡住(B1)
|
|
50
|
+
progressNote('context.retrieve', process.cwd());
|
|
48
51
|
const result = await runContextRetrieval({
|
|
49
52
|
config,
|
|
50
53
|
task: options.task ?? '',
|
package/src/commands/init.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Command } from 'commander';
|
|
|
6
6
|
|
|
7
7
|
import { CliError, templateIo, version } from '../cli-shared.ts';
|
|
8
8
|
import { makeCliGit, makeIo } from '../io.ts';
|
|
9
|
+
import { progressNote } from '../progress.ts';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* r93: the repo-root instance (git toplevel, or outside any repo) carries the
|
|
@@ -43,6 +44,8 @@ export function registerInit(program: Command): void {
|
|
|
43
44
|
mkdirSync(root, { recursive: true });
|
|
44
45
|
}
|
|
45
46
|
const repoRootInstance = isRepoRootInstance(root);
|
|
47
|
+
// 模板生成/子根扫块可能耗时 — 进度提示防止看上去卡住(B1)
|
|
48
|
+
progressNote('init.generate', root);
|
|
46
49
|
const result = runInit(makeIo(root), templateIo, {
|
|
47
50
|
update: options.update ?? false,
|
|
48
51
|
locale: options.locale ?? options.lang,
|
package/src/commands/validate.ts
CHANGED
|
@@ -319,24 +319,28 @@ function warnDirtySpecsOnDefaultBranch(): void {
|
|
|
319
319
|
let harnessBannerShown = false;
|
|
320
320
|
|
|
321
321
|
/**
|
|
322
|
-
* r13/r48 trigger state: the nested guard env and the check
|
|
322
|
+
* r13/r48 trigger state: the nested guard env and the check flag are read
|
|
323
323
|
* here (CLI boundary) and injected into core as parameters — core harness
|
|
324
|
-
* logic never touches process.env. Commander
|
|
325
|
-
* --check → check=true,
|
|
324
|
+
* logic never touches process.env. Commander binary state:
|
|
325
|
+
* --check → check=true, absent → undefined.
|
|
326
326
|
*/
|
|
327
327
|
function makeHarnessGate(options: { check?: boolean }): HarnessGate {
|
|
328
328
|
const runCommand = loadCliConfig()?.specs?.check_command ?? null;
|
|
329
329
|
const configured = runCommand !== null && runCommand !== '';
|
|
330
330
|
return {
|
|
331
331
|
nested: process.env.LLMAN_SDD_HARNESS_ACTIVE === '1',
|
|
332
|
-
|
|
332
|
+
// Opt-in harness (2026-10 decision): absent → 'off' — structure/state gate
|
|
333
|
+
// only; --check → 'on' (explicit full harness). The CLI never sends
|
|
334
|
+
// 'default'; core still treats a received 'default' as
|
|
335
|
+
// run-if-configured (its own contract).
|
|
336
|
+
check: options.check === true ? 'on' : 'off',
|
|
333
337
|
runner: configured ? makeCliHarnessRunner() : undefined,
|
|
334
338
|
runCommand,
|
|
335
339
|
cwd: process.cwd(),
|
|
336
340
|
onBeforeFirstRun: (expanded: string): void => {
|
|
337
341
|
if (harnessBannerShown) return;
|
|
338
342
|
harnessBannerShown = true;
|
|
339
|
-
console.error(`running spec check: ${expanded} (
|
|
343
|
+
console.error(`running spec check: ${expanded} (explicit --check harness run)`);
|
|
340
344
|
},
|
|
341
345
|
};
|
|
342
346
|
}
|
|
@@ -361,8 +365,7 @@ export function registerValidate(program: Command): void {
|
|
|
361
365
|
'aggregate: validate every discovered llmanspec root (specs scope, any root red → non-zero exit)',
|
|
362
366
|
)
|
|
363
367
|
.option('--include-info', 'keep INFO-level issues (default: WARNING and above)')
|
|
364
|
-
.option('--
|
|
365
|
-
.option('--check', 'run the spec check (default when specs.check_command is configured)');
|
|
368
|
+
.option('--check', 'run the spec check (full harness) explicitly — opt-in, default skips');
|
|
366
369
|
addReportOutputOptions(validate);
|
|
367
370
|
validate.action(
|
|
368
371
|
(
|
package/src/io.ts
CHANGED
|
@@ -14,10 +14,20 @@ import {
|
|
|
14
14
|
statSync,
|
|
15
15
|
writeFileSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
|
-
import { isAbsolute, join } from 'node:path';
|
|
17
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
18
18
|
|
|
19
19
|
import { makeSpawnGit, type GitLike } from '@llman-sdd/core';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Platform-aware parent directory (node:path.dirname, never hand-rolled
|
|
23
|
+
* '/' slicing): on win32, join() yields backslashes and a lastIndexOf('/')
|
|
24
|
+
* fallback would resolve to '.', so mkdir of a nested new dir would fail with
|
|
25
|
+
* ENOENT. Exported for unit-test guards (A2 platform compat).
|
|
26
|
+
*/
|
|
27
|
+
export function parentOf(p: string): string {
|
|
28
|
+
return dirname(p);
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export interface CliIo {
|
|
22
32
|
exists(path: string): boolean;
|
|
23
33
|
readText(path: string): string;
|
|
@@ -37,7 +47,6 @@ export interface CliIo {
|
|
|
37
47
|
|
|
38
48
|
export function makeIo(root: string): CliIo {
|
|
39
49
|
const full = (p: string): string => (isAbsolute(p) ? p : join(root, p));
|
|
40
|
-
const parentOf = (p: string): string => p.slice(0, p.lastIndexOf('/')) || '.';
|
|
41
50
|
return {
|
|
42
51
|
exists: (p) => existsSync(full(p)),
|
|
43
52
|
readText: (p) => readFileSync(full(p), 'utf8'),
|
package/src/progress.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localized stage-progress notes (B1): genuinely long-running operations print
|
|
3
|
+
* a short one-line stderr note in the project locale (zh → zh-Hans text,
|
|
4
|
+
* everything else English) so neither humans nor agents mistake progress for a
|
|
5
|
+
* hang. Progress ALWAYS goes to stderr (capture contract: results on stdout,
|
|
6
|
+
* progress on stderr); each key fires at most once per process; notes are only
|
|
7
|
+
* emitted before the long op itself, never ahead of domain errors.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
type Locale = 'zh' | 'en';
|
|
13
|
+
|
|
14
|
+
const NOTES: Record<string, Record<Locale, string>> = {
|
|
15
|
+
'context.retrieve': {
|
|
16
|
+
zh: '正在检索 specs 上下文(LLM)...',
|
|
17
|
+
en: 'Retrieving spec context (LLM)...',
|
|
18
|
+
},
|
|
19
|
+
'init.generate': {
|
|
20
|
+
zh: '正在初始化 llmanspec/ 与 skills...',
|
|
21
|
+
en: 'Initializing llmanspec/ and skills...',
|
|
22
|
+
},
|
|
23
|
+
'freeze.pack': {
|
|
24
|
+
zh: '正在压缩冻结归档(7z)...',
|
|
25
|
+
en: 'Packing freeze archive (7z)...',
|
|
26
|
+
},
|
|
27
|
+
'thaw.unpack': {
|
|
28
|
+
zh: '正在解包冻结归档(7z)...',
|
|
29
|
+
en: 'Unpacking freeze archive (7z)...',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Resolve the note text for a locale (pure; exported for unit tests). */
|
|
34
|
+
export function resolveNote(key: string, locale: Locale): string | undefined {
|
|
35
|
+
return NOTES[key]?.[locale];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Detect the project locale from llmanspec/config.yaml (zh-* → zh, else en). */
|
|
39
|
+
export function detectLocale(cwd: string): Locale {
|
|
40
|
+
try {
|
|
41
|
+
const cfg = readFileSync(join(cwd, 'llmanspec', 'config.yaml'), 'utf8');
|
|
42
|
+
const m = cfg.match(/^locale:\s*([A-Za-z-]+)/mu);
|
|
43
|
+
if (m?.[1]?.startsWith('zh') ?? false) return 'zh';
|
|
44
|
+
} catch {
|
|
45
|
+
// No config at cwd — English fallback.
|
|
46
|
+
}
|
|
47
|
+
return 'en';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const shown = new Set<string>();
|
|
51
|
+
|
|
52
|
+
/** Print a localized progress note to stderr once per key per process. */
|
|
53
|
+
export function progressNote(key: string, cwd: string = process.cwd()): void {
|
|
54
|
+
if (shown.has(key)) return;
|
|
55
|
+
const text = resolveNote(key, detectLocale(cwd));
|
|
56
|
+
if (text === undefined) return;
|
|
57
|
+
shown.add(key);
|
|
58
|
+
process.stderr.write(`${text}\n`);
|
|
59
|
+
}
|