silvermoon 0.2.2 → 0.3.0-rc.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/LICENSE +21 -0
- package/README.md +115 -121
- package/README.zh-CN.md +96 -95
- package/bin/silvermoon.js +7 -2
- package/docs/getting-started.md +21 -9
- package/docs/maintaining.md +83 -6
- package/docs/npm-package-releases.md +33 -4
- package/docs/operations.md +11 -7
- package/docs/reference.md +131 -32
- package/docs/repository-tasks.md +43 -15
- package/package.json +46 -8
- package/skills/silvermoon/SKILL.md +224 -123
- package/src/adoption.js +30 -16
- package/src/cli.js +148 -39
- package/src/config.js +76 -16
- package/src/create-idea.js +22 -10
- package/src/git-snapshot.js +212 -0
- package/src/git.js +209 -18
- package/src/guidance.js +65 -23
- package/src/idea-layout.js +59 -23
- package/src/idea-metadata.js +48 -0
- package/src/idea-templates.js +133 -0
- package/src/index.js +64 -53
- package/src/language.js +4 -0
- package/src/list-ideas.js +118 -55
- package/src/observation.js +9 -1
- package/src/repository.js +2 -8
- package/src/response.js +93 -95
- package/src/subprocess.js +27 -0
- package/src/trace.js +37 -2
- package/src/tui-table.js +71 -0
- package/src/tui.js +205 -0
- package/src/whatsnext.js +71 -18
- package/assets/silvermoon-avatar.svg +0 -395
- package/assets/silvermoon.svg +0 -176
- package/docs/assets/silvermoon-avatar.svg +0 -395
package/src/adoption.js
CHANGED
|
@@ -15,6 +15,7 @@ import { traceAsync } from "./trace.js";
|
|
|
15
15
|
|
|
16
16
|
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
17
17
|
const packageJson = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
18
|
+
const DEFAULT_FILESYSTEM = { lstat, readFile, readdir };
|
|
18
19
|
const packagedSkillsRoot = resolve(packageRoot, "skills");
|
|
19
20
|
const packagedSkill = resolve(packagedSkillsRoot, "silvermoon");
|
|
20
21
|
const localSkillsRoot = "./node_modules/silvermoon/skills";
|
|
@@ -95,7 +96,7 @@ function packageManagerDescriptor(manager, workspace, reason = null) {
|
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
async function detectPackageManager(root, manifest) {
|
|
99
|
+
async function detectPackageManager(root, manifest, filesystem) {
|
|
99
100
|
const workspace = workspaceRoot(manifest);
|
|
100
101
|
if (Object.hasOwn(manifest, "packageManager")) {
|
|
101
102
|
const declaration = manifest.packageManager;
|
|
@@ -116,7 +117,7 @@ async function detectPackageManager(root, manifest) {
|
|
|
116
117
|
const uncertain = [];
|
|
117
118
|
for (const [filename, manager] of lockfileManagers) {
|
|
118
119
|
try {
|
|
119
|
-
const metadata = await lstat(resolve(root, filename));
|
|
120
|
+
const metadata = await filesystem.lstat(resolve(root, filename));
|
|
120
121
|
if (metadata.isFile()) detected.push({ filename, manager });
|
|
121
122
|
else uncertain.push(filename);
|
|
122
123
|
} catch (caught) {
|
|
@@ -220,12 +221,15 @@ function skillInstruction(skill, packageManager) {
|
|
|
220
221
|
export async function inspectNpmProject(
|
|
221
222
|
snapshotRoot,
|
|
222
223
|
repositoryRoot,
|
|
223
|
-
{
|
|
224
|
+
{
|
|
225
|
+
filesystem = DEFAULT_FILESYSTEM,
|
|
226
|
+
readManifest = filesystem.readFile,
|
|
227
|
+
} = {},
|
|
224
228
|
) {
|
|
225
229
|
const manifestPath = resolve(snapshotRoot, "package.json");
|
|
226
230
|
let metadata;
|
|
227
231
|
try {
|
|
228
|
-
metadata = await lstat(manifestPath);
|
|
232
|
+
metadata = await filesystem.lstat(manifestPath);
|
|
229
233
|
} catch (caught) {
|
|
230
234
|
if (caught.code === "ENOENT") return { npmProject: false, findings: [] };
|
|
231
235
|
return {
|
|
@@ -295,7 +299,11 @@ export async function inspectNpmProject(
|
|
|
295
299
|
};
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
const packageManager = await detectPackageManager(
|
|
302
|
+
const packageManager = await detectPackageManager(
|
|
303
|
+
snapshotRoot,
|
|
304
|
+
manifest,
|
|
305
|
+
filesystem,
|
|
306
|
+
);
|
|
299
307
|
const findings = [];
|
|
300
308
|
const sourceCheckout = isSilvermoonSourceCheckout(repositoryRoot, manifest);
|
|
301
309
|
if (!expectedDependency) {
|
|
@@ -369,11 +377,13 @@ function canonicalSkillBytes(content) {
|
|
|
369
377
|
}
|
|
370
378
|
}
|
|
371
379
|
|
|
372
|
-
async function directoryDigest(root) {
|
|
380
|
+
async function directoryDigest(root, filesystem) {
|
|
373
381
|
const hash = createHash("sha256");
|
|
374
382
|
|
|
375
383
|
async function visit(directory) {
|
|
376
|
-
const entries = (
|
|
384
|
+
const entries = (
|
|
385
|
+
await filesystem.readdir(directory, { withFileTypes: true })
|
|
386
|
+
)
|
|
377
387
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
378
388
|
for (const entry of entries) {
|
|
379
389
|
const path = resolve(directory, entry.name);
|
|
@@ -382,7 +392,7 @@ async function directoryDigest(root) {
|
|
|
382
392
|
} else if (entry.isFile()) {
|
|
383
393
|
hash.update(relative(root, path).split(sep).join("/"));
|
|
384
394
|
hash.update("\0");
|
|
385
|
-
hash.update(canonicalSkillBytes(await readFile(path)));
|
|
395
|
+
hash.update(canonicalSkillBytes(await filesystem.readFile(path)));
|
|
386
396
|
hash.update("\0");
|
|
387
397
|
} else if (entry.isSymbolicLink()) {
|
|
388
398
|
await visit(path);
|
|
@@ -396,16 +406,16 @@ async function directoryDigest(root) {
|
|
|
396
406
|
return hash.digest("hex");
|
|
397
407
|
}
|
|
398
408
|
|
|
399
|
-
async function inspectSkill(contentRoot) {
|
|
409
|
+
async function inspectSkill(contentRoot, filesystem) {
|
|
400
410
|
const path = resolve(contentRoot, ...REPOSITORY_SKILL_PATH.split("/"));
|
|
401
411
|
const expectedDigest = await traceAsync(
|
|
402
412
|
"skill.digest-packaged",
|
|
403
413
|
{},
|
|
404
|
-
() => directoryDigest(packagedSkill),
|
|
414
|
+
() => directoryDigest(packagedSkill, DEFAULT_FILESYSTEM),
|
|
405
415
|
);
|
|
406
416
|
let metadata;
|
|
407
417
|
try {
|
|
408
|
-
metadata = await lstat(path);
|
|
418
|
+
metadata = await filesystem.lstat(path);
|
|
409
419
|
} catch (caught) {
|
|
410
420
|
if (caught.code !== "ENOENT") {
|
|
411
421
|
return {
|
|
@@ -444,7 +454,7 @@ async function inspectSkill(contentRoot) {
|
|
|
444
454
|
actualDigest = await traceAsync(
|
|
445
455
|
"skill.digest-repository",
|
|
446
456
|
{},
|
|
447
|
-
() => directoryDigest(path),
|
|
457
|
+
() => directoryDigest(path, filesystem),
|
|
448
458
|
);
|
|
449
459
|
} catch (caught) {
|
|
450
460
|
return {
|
|
@@ -473,10 +483,14 @@ async function inspectSkill(contentRoot) {
|
|
|
473
483
|
|
|
474
484
|
export async function inspectAdoption({
|
|
475
485
|
contentRoot,
|
|
486
|
+
filesystem = DEFAULT_FILESYSTEM,
|
|
487
|
+
repositoryRoot: knownRepositoryRoot,
|
|
476
488
|
root = process.cwd(),
|
|
477
489
|
} = {}) {
|
|
478
490
|
const requestedRoot = resolve(root);
|
|
479
|
-
const git =
|
|
491
|
+
const git = knownRepositoryRoot === undefined
|
|
492
|
+
? runGit(requestedRoot, ["rev-parse", "--show-toplevel"])
|
|
493
|
+
: { ok: true, stdout: resolve(knownRepositoryRoot) };
|
|
480
494
|
const repositoryRoot = git.ok ? resolve(git.stdout) : requestedRoot;
|
|
481
495
|
const snapshotRoot = resolve(contentRoot ?? repositoryRoot);
|
|
482
496
|
const findings = [];
|
|
@@ -495,7 +509,7 @@ export async function inspectAdoption({
|
|
|
495
509
|
const loadedConfig = await traceAsync(
|
|
496
510
|
"config.load",
|
|
497
511
|
{},
|
|
498
|
-
() => loadConfig({ root: snapshotRoot }),
|
|
512
|
+
() => loadConfig({ filesystem, root: snapshotRoot }),
|
|
499
513
|
);
|
|
500
514
|
for (const diagnostic of loadedConfig.diagnostics) {
|
|
501
515
|
findings.push({
|
|
@@ -509,7 +523,7 @@ export async function inspectAdoption({
|
|
|
509
523
|
const npm = await traceAsync(
|
|
510
524
|
"npm-project.inspect",
|
|
511
525
|
{},
|
|
512
|
-
() => inspectNpmProject(snapshotRoot, repositoryRoot),
|
|
526
|
+
() => inspectNpmProject(snapshotRoot, repositoryRoot, { filesystem }),
|
|
513
527
|
);
|
|
514
528
|
findings.push(...npm.findings);
|
|
515
529
|
const skillSource = npm.npmProject && !npm.sourceCheckout
|
|
@@ -518,7 +532,7 @@ export async function inspectAdoption({
|
|
|
518
532
|
const skill = await traceAsync(
|
|
519
533
|
"skill.inspect",
|
|
520
534
|
{},
|
|
521
|
-
() => inspectSkill(snapshotRoot,
|
|
535
|
+
() => inspectSkill(snapshotRoot, filesystem),
|
|
522
536
|
);
|
|
523
537
|
if (!skill.ok) {
|
|
524
538
|
findings.push({
|
package/src/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { listIdeas } from "./list-ideas.js";
|
|
|
15
15
|
import { checkRepository } from "./index.js";
|
|
16
16
|
import { createIdea } from "./create-idea.js";
|
|
17
17
|
import { renderResponse } from "./response.js";
|
|
18
|
-
import { withTraceFile } from "./trace.js";
|
|
18
|
+
import { traceAsync, withTraceFile } from "./trace.js";
|
|
19
19
|
import { whatsNext } from "./whatsnext.js";
|
|
20
20
|
|
|
21
21
|
const { version: VERSION } = JSON.parse(
|
|
@@ -23,6 +23,7 @@ const { version: VERSION } = JSON.parse(
|
|
|
23
23
|
);
|
|
24
24
|
|
|
25
25
|
const TRACE_FILE_SUFFIX = ".trace.jsonl";
|
|
26
|
+
const AUDIENCES = ["human", "agent"];
|
|
26
27
|
|
|
27
28
|
function write(method, value) {
|
|
28
29
|
const text = value.replace(/\n$/, "");
|
|
@@ -38,6 +39,14 @@ export function normalizeTraceFileName(value) {
|
|
|
38
39
|
function addCommonOptions(command) {
|
|
39
40
|
return command
|
|
40
41
|
.option("--json", "serialize all four command projections as JSON")
|
|
42
|
+
.addOption(
|
|
43
|
+
new Option(
|
|
44
|
+
"--audience <audience>",
|
|
45
|
+
"select human TUI or agent Markdown output (default: human)",
|
|
46
|
+
)
|
|
47
|
+
.choices(AUDIENCES)
|
|
48
|
+
.conflicts("json"),
|
|
49
|
+
)
|
|
41
50
|
.option(
|
|
42
51
|
"--trace <file.trace.jsonl>",
|
|
43
52
|
"write domain and timing events as JSONL (suffix is appended when omitted)",
|
|
@@ -46,11 +55,106 @@ function addCommonOptions(command) {
|
|
|
46
55
|
.option("-r, --root <path>", "repository root", process.cwd());
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
export function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
export function selectOutputRenderer({
|
|
59
|
+
audience = "human",
|
|
60
|
+
json = false,
|
|
61
|
+
stdinIsTTY = false,
|
|
62
|
+
stdoutIsTTY = false,
|
|
63
|
+
}) {
|
|
64
|
+
if (json) return "json";
|
|
65
|
+
if (audience === "human" && stdinIsTTY && stdoutIsTTY) return "tui";
|
|
66
|
+
return "markdown";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function defaultTuiRenderer(content, terminal) {
|
|
70
|
+
const { renderTuiMarkdown } = await import("./tui.js");
|
|
71
|
+
await renderTuiMarkdown(content, terminal);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function render(
|
|
75
|
+
report,
|
|
76
|
+
{ audience = "human", json = false } = {},
|
|
77
|
+
io = console,
|
|
78
|
+
{
|
|
79
|
+
renderTui = defaultTuiRenderer,
|
|
80
|
+
terminal = { stdin: process.stdin, stdout: process.stdout },
|
|
81
|
+
} = {},
|
|
82
|
+
) {
|
|
83
|
+
const renderer = selectOutputRenderer({
|
|
84
|
+
audience,
|
|
85
|
+
json,
|
|
86
|
+
stdinIsTTY: terminal.stdin.isTTY === true,
|
|
87
|
+
stdoutIsTTY: terminal.stdout.isTTY === true,
|
|
88
|
+
});
|
|
89
|
+
if (renderer === "json") {
|
|
90
|
+
io.log(JSON.stringify(report, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const markdown = renderResponse(report.response);
|
|
95
|
+
if (renderer === "tui") {
|
|
96
|
+
await renderTui(markdown, terminal);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
io.log(markdown);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function outputRenderer(options, runtime) {
|
|
103
|
+
const terminal = runtime.terminal ?? {
|
|
104
|
+
stdin: process.stdin,
|
|
105
|
+
stdout: process.stdout,
|
|
106
|
+
};
|
|
107
|
+
return selectOutputRenderer({
|
|
108
|
+
audience: options.audience,
|
|
109
|
+
json: options.json,
|
|
110
|
+
stdinIsTTY: terminal.stdin.isTTY === true,
|
|
111
|
+
stdoutIsTTY: terminal.stdout.isTTY === true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function bootstrapDuration(runtime) {
|
|
116
|
+
if (typeof runtime.bootstrapStartedAt !== "bigint") return {};
|
|
117
|
+
const elapsed = process.hrtime.bigint() - runtime.bootstrapStartedAt;
|
|
118
|
+
return {
|
|
119
|
+
bootstrapDurationMs: Math.round(Number(elapsed) / 1_000) / 1_000,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runCommand({
|
|
124
|
+
attributes,
|
|
125
|
+
execute,
|
|
126
|
+
name,
|
|
127
|
+
options,
|
|
128
|
+
}, io, runtime) {
|
|
129
|
+
const renderer = outputRenderer(options, runtime);
|
|
130
|
+
const traceAttributes = {
|
|
131
|
+
...attributes,
|
|
132
|
+
outputRenderer: renderer,
|
|
133
|
+
...bootstrapDuration(runtime),
|
|
134
|
+
};
|
|
135
|
+
if (renderer === "tui") {
|
|
136
|
+
const report = await withTraceFile(
|
|
137
|
+
options.trace,
|
|
138
|
+
name,
|
|
139
|
+
traceAttributes,
|
|
140
|
+
execute,
|
|
141
|
+
);
|
|
142
|
+
await render(report, options, io, runtime);
|
|
143
|
+
return report;
|
|
144
|
+
}
|
|
145
|
+
return withTraceFile(
|
|
146
|
+
options.trace,
|
|
147
|
+
name,
|
|
148
|
+
traceAttributes,
|
|
149
|
+
async () => {
|
|
150
|
+
const report = await execute();
|
|
151
|
+
await traceAsync(
|
|
152
|
+
"output.render",
|
|
153
|
+
{ renderer },
|
|
154
|
+
() => render(report, options, io, runtime),
|
|
155
|
+
);
|
|
156
|
+
return report;
|
|
157
|
+
},
|
|
54
158
|
);
|
|
55
159
|
}
|
|
56
160
|
|
|
@@ -74,7 +178,7 @@ function collectOption(value, previous = []) {
|
|
|
74
178
|
return [...previous, value];
|
|
75
179
|
}
|
|
76
180
|
|
|
77
|
-
export function createProgram(io = console) {
|
|
181
|
+
export function createProgram(io = console, runtime = {}) {
|
|
78
182
|
const program = new Command();
|
|
79
183
|
program
|
|
80
184
|
.name("silvermoon")
|
|
@@ -93,10 +197,12 @@ export function createProgram(io = console) {
|
|
|
93
197
|
.addHelpText("after", `
|
|
94
198
|
Examples:
|
|
95
199
|
$ silvermoon list-ideas
|
|
200
|
+
$ silvermoon list-ideas --language zh-CN
|
|
96
201
|
$ silvermoon list-ideas --state completed --sort oldest --limit 10
|
|
97
202
|
$ silvermoon whats-next
|
|
98
203
|
$ silvermoon whats-next <idea>
|
|
99
204
|
$ silvermoon whats-next <idea> --language zh-CN
|
|
205
|
+
$ silvermoon whats-next <idea> --audience agent
|
|
100
206
|
$ silvermoon whats-next <idea> --json
|
|
101
207
|
$ silvermoon whats-next <idea> --trace whats-next.trace.jsonl
|
|
102
208
|
$ silvermoon create-idea
|
|
@@ -111,6 +217,11 @@ Examples:
|
|
|
111
217
|
program
|
|
112
218
|
.command("list-ideas")
|
|
113
219
|
.description("query the current local idea inventory")
|
|
220
|
+
.option(
|
|
221
|
+
"--language <tag>",
|
|
222
|
+
"use a built-in output language (en, en-US, zh, or zh-CN) for this invocation",
|
|
223
|
+
outputLanguageArgument,
|
|
224
|
+
)
|
|
114
225
|
.option(
|
|
115
226
|
"--state <state>",
|
|
116
227
|
"include a lifecycle state or active (repeatable)",
|
|
@@ -138,20 +249,21 @@ Examples:
|
|
|
138
249
|
sort: options.sort,
|
|
139
250
|
states: options.state,
|
|
140
251
|
});
|
|
141
|
-
const report = await
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
252
|
+
const report = await runCommand({
|
|
253
|
+
name: "command.list-ideas",
|
|
254
|
+
options,
|
|
255
|
+
attributes: {
|
|
145
256
|
command: "list-ideas",
|
|
146
257
|
hasQuery: query.query !== null,
|
|
258
|
+
outputLanguage: options.language ?? null,
|
|
147
259
|
stateCount: query.states.length,
|
|
148
260
|
},
|
|
149
|
-
() => listIdeas({
|
|
261
|
+
execute: () => listIdeas({
|
|
150
262
|
...query,
|
|
263
|
+
language: options.language,
|
|
151
264
|
root: options.root,
|
|
152
265
|
}),
|
|
153
|
-
);
|
|
154
|
-
render(report, options.json, io);
|
|
266
|
+
}, io, runtime);
|
|
155
267
|
program.setOptionValue(
|
|
156
268
|
"resultCode",
|
|
157
269
|
report.observation.state === "ideas-listed" ? 0 : 1,
|
|
@@ -164,24 +276,23 @@ Examples:
|
|
|
164
276
|
.description("observe project, repository, and idea readiness")
|
|
165
277
|
.option(
|
|
166
278
|
"--language <tag>",
|
|
167
|
-
"use a built-in output language (en-US or zh-CN) for this invocation",
|
|
279
|
+
"use a built-in output language (en, en-US, zh, or zh-CN) for this invocation",
|
|
168
280
|
outputLanguageArgument,
|
|
169
281
|
),
|
|
170
282
|
).action(async (idea, options) => {
|
|
171
|
-
const report = await
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
283
|
+
const report = await runCommand({
|
|
284
|
+
name: "command.whats-next",
|
|
285
|
+
options,
|
|
286
|
+
attributes: {
|
|
175
287
|
command: "whats-next",
|
|
176
288
|
outputLanguage: options.language ?? null,
|
|
177
289
|
},
|
|
178
|
-
() => whatsNext({
|
|
290
|
+
execute: () => whatsNext({
|
|
179
291
|
idea,
|
|
180
292
|
language: options.language,
|
|
181
293
|
root: options.root,
|
|
182
294
|
}),
|
|
183
|
-
);
|
|
184
|
-
render(report, options.json, io);
|
|
295
|
+
}, io, runtime);
|
|
185
296
|
program.setOptionValue("resultCode", 0);
|
|
186
297
|
});
|
|
187
298
|
|
|
@@ -195,19 +306,18 @@ Examples:
|
|
|
195
306
|
contentLanguageArgument,
|
|
196
307
|
),
|
|
197
308
|
).action(async (options) => {
|
|
198
|
-
const report = await
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{
|
|
309
|
+
const report = await runCommand({
|
|
310
|
+
name: "command.create-idea",
|
|
311
|
+
options,
|
|
312
|
+
attributes: {
|
|
202
313
|
command: "create-idea",
|
|
203
314
|
contentLanguage: options.language ?? null,
|
|
204
315
|
},
|
|
205
|
-
() => createIdea({
|
|
316
|
+
execute: () => createIdea({
|
|
206
317
|
language: options.language,
|
|
207
318
|
root: options.root,
|
|
208
319
|
}),
|
|
209
|
-
);
|
|
210
|
-
render(report, options.json, io);
|
|
320
|
+
}, io, runtime);
|
|
211
321
|
program.setOptionValue("resultCode", 0);
|
|
212
322
|
});
|
|
213
323
|
|
|
@@ -217,7 +327,7 @@ Examples:
|
|
|
217
327
|
.description("validate Silvermoon configuration and idea state")
|
|
218
328
|
.option(
|
|
219
329
|
"--language <tag>",
|
|
220
|
-
"use a built-in output language (en-US or zh-CN) for this invocation",
|
|
330
|
+
"use a built-in output language (en, en-US, zh, or zh-CN) for this invocation",
|
|
221
331
|
outputLanguageArgument,
|
|
222
332
|
)
|
|
223
333
|
.addOption(new Option("--remote", "fetch and validate the configured primary tip").conflicts(["commit", "staged", "worktree"]))
|
|
@@ -225,14 +335,14 @@ Examples:
|
|
|
225
335
|
.addOption(new Option("--staged", "validate the index snapshot").conflicts(["remote", "commit", "worktree"]))
|
|
226
336
|
.addOption(new Option("--worktree", "validate HEAD plus staged, unstaged, and untracked changes").conflicts(["remote", "commit", "staged"])),
|
|
227
337
|
).action(async (options) => {
|
|
228
|
-
const report = await
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{
|
|
338
|
+
const report = await runCommand({
|
|
339
|
+
name: "command.check",
|
|
340
|
+
options,
|
|
341
|
+
attributes: {
|
|
232
342
|
command: "check",
|
|
233
343
|
outputLanguage: options.language ?? null,
|
|
234
344
|
},
|
|
235
|
-
() => checkRepository({
|
|
345
|
+
execute: () => checkRepository({
|
|
236
346
|
commit: options.commit,
|
|
237
347
|
language: options.language,
|
|
238
348
|
remote: options.remote,
|
|
@@ -240,8 +350,7 @@ Examples:
|
|
|
240
350
|
staged: options.staged,
|
|
241
351
|
worktree: options.worktree,
|
|
242
352
|
}),
|
|
243
|
-
);
|
|
244
|
-
render(report, options.json, io);
|
|
353
|
+
}, io, runtime);
|
|
245
354
|
program.setOptionValue(
|
|
246
355
|
"resultCode",
|
|
247
356
|
report.observation.state === "project-ready" ? 0 : 1,
|
|
@@ -251,8 +360,8 @@ Examples:
|
|
|
251
360
|
return program;
|
|
252
361
|
}
|
|
253
362
|
|
|
254
|
-
export async function runCli(args, io = console) {
|
|
255
|
-
const program = createProgram(io);
|
|
363
|
+
export async function runCli(args, io = console, runtime = {}) {
|
|
364
|
+
const program = createProgram(io, runtime);
|
|
256
365
|
try {
|
|
257
366
|
await program.parseAsync(args.length === 0 ? ["--help"] : args, { from: "user" });
|
|
258
367
|
} catch (caught) {
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstat, readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { inspectTreeLineage, readGitBlobs } from "./git.js";
|
|
4
5
|
import { CONFIG_PATH, METADATA_ROOT } from "./layout.js";
|
|
5
6
|
import { isCanonicalLanguageTag } from "./language.js";
|
|
6
7
|
import { validBranchName, validRepository } from "./repository.js";
|
|
@@ -14,28 +15,32 @@ const CONFIG_KEYS = [
|
|
|
14
15
|
"primaryBranch",
|
|
15
16
|
"preferredLanguage",
|
|
16
17
|
];
|
|
18
|
+
const DEFAULT_FILESYSTEM = { lstat, readFile };
|
|
17
19
|
|
|
18
20
|
function configDiagnostic(code, path, message, remediation) {
|
|
19
21
|
return { code, level: "error", path, message, remediation };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
async function regularRepositoryFile(root) {
|
|
24
|
+
async function regularRepositoryFile(root, filesystem) {
|
|
23
25
|
const metadataRoot = resolve(root, METADATA_ROOT);
|
|
24
26
|
const absolutePath = resolve(root, CONFIG_PATH);
|
|
25
27
|
try {
|
|
26
|
-
const directoryMetadata = await lstat(metadataRoot);
|
|
28
|
+
const directoryMetadata = await filesystem.lstat(metadataRoot);
|
|
27
29
|
if (!directoryMetadata.isDirectory() || directoryMetadata.isSymbolicLink()) {
|
|
28
30
|
throw Object.assign(new Error(`${METADATA_ROOT} must be a regular directory`), {
|
|
29
31
|
code: "EINVAL",
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
|
-
const fileMetadata = await lstat(absolutePath);
|
|
34
|
+
const fileMetadata = await filesystem.lstat(absolutePath);
|
|
33
35
|
if (!fileMetadata.isFile() || fileMetadata.isSymbolicLink()) {
|
|
34
36
|
throw Object.assign(new Error("Configuration must be a regular file"), {
|
|
35
37
|
code: "EINVAL",
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
absolutePath,
|
|
42
|
+
source: await filesystem.readFile(absolutePath, "utf8"),
|
|
43
|
+
};
|
|
39
44
|
} catch (error) {
|
|
40
45
|
const missing = error.code === "ENOENT";
|
|
41
46
|
return {
|
|
@@ -70,17 +75,8 @@ export function serializeConfig(config) {
|
|
|
70
75
|
return stringifyCanonicalYaml(canonical);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
if (loaded.diagnostic) {
|
|
76
|
-
return {
|
|
77
|
-
config: null,
|
|
78
|
-
configPath: loaded.absolutePath,
|
|
79
|
-
diagnostics: [loaded.diagnostic],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const normalizedSource = loaded.source.replaceAll("\r\n", "\n");
|
|
78
|
+
function parseConfigSource({ absolutePath, root, source }) {
|
|
79
|
+
const normalizedSource = source.replaceAll("\r\n", "\n");
|
|
84
80
|
let value;
|
|
85
81
|
try {
|
|
86
82
|
value = parseStrictYaml(normalizedSource);
|
|
@@ -187,7 +183,71 @@ export async function loadConfig({ root }) {
|
|
|
187
183
|
|
|
188
184
|
return {
|
|
189
185
|
config: diagnostics.length === 0 ? value : null,
|
|
190
|
-
configPath:
|
|
186
|
+
configPath: absolutePath,
|
|
191
187
|
diagnostics,
|
|
192
188
|
};
|
|
193
189
|
}
|
|
190
|
+
|
|
191
|
+
export async function loadConfig({
|
|
192
|
+
filesystem = DEFAULT_FILESYSTEM,
|
|
193
|
+
root,
|
|
194
|
+
}) {
|
|
195
|
+
const loaded = await regularRepositoryFile(root, filesystem);
|
|
196
|
+
if (loaded.diagnostic) {
|
|
197
|
+
return {
|
|
198
|
+
config: null,
|
|
199
|
+
configPath: loaded.absolutePath,
|
|
200
|
+
diagnostics: [loaded.diagnostic],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return parseConfigSource({
|
|
204
|
+
absolutePath: loaded.absolutePath,
|
|
205
|
+
root,
|
|
206
|
+
source: loaded.source,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function loadConfigSnapshot({ gitRoot, tree }) {
|
|
211
|
+
const absolutePath = resolve(gitRoot, CONFIG_PATH);
|
|
212
|
+
const entries = inspectTreeLineage(gitRoot, tree, CONFIG_PATH);
|
|
213
|
+
const metadataRoot = entries.find(({ name }) => name === METADATA_ROOT);
|
|
214
|
+
const config = entries.find(({ name }) => name === CONFIG_PATH);
|
|
215
|
+
if (metadataRoot === undefined || config === undefined) {
|
|
216
|
+
return {
|
|
217
|
+
config: null,
|
|
218
|
+
configPath: absolutePath,
|
|
219
|
+
diagnostics: [configDiagnostic(
|
|
220
|
+
"config.missing",
|
|
221
|
+
CONFIG_PATH,
|
|
222
|
+
`Missing Silvermoon configuration: ${CONFIG_PATH}`,
|
|
223
|
+
`Create ${CONFIG_PATH}.`,
|
|
224
|
+
)],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (
|
|
228
|
+
metadataRoot.type !== "tree"
|
|
229
|
+
|| metadataRoot.mode !== "040000"
|
|
230
|
+
|| config.type !== "blob"
|
|
231
|
+
|| !["100644", "100755"].includes(config.mode)
|
|
232
|
+
) {
|
|
233
|
+
return {
|
|
234
|
+
config: null,
|
|
235
|
+
configPath: absolutePath,
|
|
236
|
+
diagnostics: [configDiagnostic(
|
|
237
|
+
"config.invalid-file",
|
|
238
|
+
CONFIG_PATH,
|
|
239
|
+
"Cannot read Silvermoon configuration: configuration paths must be regular",
|
|
240
|
+
`Replace ${METADATA_ROOT} and ${CONFIG_PATH} with repository-owned regular paths.`,
|
|
241
|
+
)],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const source = readGitBlobs(gitRoot, [config.object]).get(config.object);
|
|
245
|
+
if (source === undefined) {
|
|
246
|
+
throw new Error(`Cannot read Silvermoon configuration blob ${config.object}`);
|
|
247
|
+
}
|
|
248
|
+
return parseConfigSource({
|
|
249
|
+
absolutePath,
|
|
250
|
+
root: gitRoot,
|
|
251
|
+
source: source.toString("utf8"),
|
|
252
|
+
});
|
|
253
|
+
}
|
package/src/create-idea.js
CHANGED
|
@@ -10,10 +10,7 @@ import {
|
|
|
10
10
|
import { createCommandRun } from "./domain.js";
|
|
11
11
|
import { inspectPhaseGuidance } from "./guidance.js";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
IDEA_TEMPLATE,
|
|
15
|
-
IMPLEMENTATION_TEMPLATE,
|
|
16
|
-
LEDGER_TEMPLATE,
|
|
13
|
+
ideaTemplates,
|
|
17
14
|
} from "./idea-templates.js";
|
|
18
15
|
import { isValidUlid, serializeIdeaStatus } from "./ideas.js";
|
|
19
16
|
import { canonicalizeLanguageTag } from "./language.js";
|
|
@@ -209,6 +206,7 @@ async function cleanupScaffold(cleanupPlan, operations) {
|
|
|
209
206
|
|
|
210
207
|
async function createScaffold({
|
|
211
208
|
canonicalLanguage,
|
|
209
|
+
contentLanguage,
|
|
212
210
|
generateId,
|
|
213
211
|
operations,
|
|
214
212
|
repositoryRoot,
|
|
@@ -263,11 +261,12 @@ async function createScaffold({
|
|
|
263
261
|
paths.innerPath,
|
|
264
262
|
paths.idealPath,
|
|
265
263
|
];
|
|
264
|
+
const templates = ideaTemplates(contentLanguage);
|
|
266
265
|
const files = [
|
|
267
|
-
[paths.ideaDocumentPath,
|
|
268
|
-
[paths.implementationDocumentPath,
|
|
269
|
-
[paths.deploymentDocumentPath,
|
|
270
|
-
[paths.ledgerPath,
|
|
266
|
+
[paths.ideaDocumentPath, templates.idea],
|
|
267
|
+
[paths.implementationDocumentPath, templates.implementation],
|
|
268
|
+
[paths.deploymentDocumentPath, templates.deployment],
|
|
269
|
+
[paths.ledgerPath, templates.ledger],
|
|
271
270
|
[paths.statusPath, serializeIdeaStatus({
|
|
272
271
|
version: 1,
|
|
273
272
|
id,
|
|
@@ -420,6 +419,7 @@ export async function createIdea({
|
|
|
420
419
|
},
|
|
421
420
|
() => createScaffold({
|
|
422
421
|
canonicalLanguage,
|
|
422
|
+
contentLanguage: observed.contentLanguage,
|
|
423
423
|
generateId,
|
|
424
424
|
operations,
|
|
425
425
|
repositoryRoot,
|
|
@@ -467,6 +467,18 @@ export async function createIdea({
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
const { createdIdea, paths } = completion.result;
|
|
470
|
+
const templates = ideaTemplates(observed.contentLanguage);
|
|
471
|
+
const languageInstruction = templates.localized
|
|
472
|
+
? localize(
|
|
473
|
+
observed.outputLanguage,
|
|
474
|
+
`Use ${observed.contentLanguage} for all natural-language content in the canonical idea documents and ledger.`,
|
|
475
|
+
`在 canonical idea 文档和 ledger 的所有自然语言内容中使用 ${observed.contentLanguage}。`,
|
|
476
|
+
)
|
|
477
|
+
: localize(
|
|
478
|
+
observed.outputLanguage,
|
|
479
|
+
`Silvermoon has no built-in ${observed.contentLanguage} scaffold, so the new files contain explicit ${templates.templateLanguage} fallback placeholders. Replace all natural-language placeholders with ${observed.contentLanguage} before continuing.`,
|
|
480
|
+
`Silvermoon 没有内置的 ${observed.contentLanguage} 脚手架,因此新文件包含明确的 ${templates.templateLanguage} fallback 占位。继续前,将所有自然语言占位替换为 ${observed.contentLanguage}。`,
|
|
481
|
+
);
|
|
470
482
|
return runtime.complete(
|
|
471
483
|
dialogueReadyObservation(readiness.observation, "idea-created", {
|
|
472
484
|
createdIdea,
|
|
@@ -477,8 +489,8 @@ export async function createIdea({
|
|
|
477
489
|
{
|
|
478
490
|
nextSteps: localize(
|
|
479
491
|
observed.outputLanguage,
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
`${languageInstruction} Describe the requested Ideal World in ${paths.ideaDocumentPath}; keep canonical headings, stable IDs, and placeholders in ${paths.implementationDocumentPath}, ${paths.deploymentDocumentPath}, and ${paths.ledgerPath} synchronized.`,
|
|
493
|
+
`${languageInstruction} 在 ${paths.ideaDocumentPath} 中描述请求的理想契约;保持 ${paths.implementationDocumentPath}、${paths.deploymentDocumentPath} 与 ${paths.ledgerPath} 中的 canonical 标题、稳定 ID 和占位同步。`,
|
|
482
494
|
),
|
|
483
495
|
},
|
|
484
496
|
);
|