silvermoon 0.2.1 → 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 +23 -11
- 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/skills/silvermoon/references/adoption.md +3 -2
- package/src/adoption.js +51 -35
- 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) {
|
|
@@ -141,13 +142,13 @@ async function detectPackageManager(root, manifest) {
|
|
|
141
142
|
return packageManagerDescriptor(managers[0] ?? "npm", workspace);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
export function
|
|
145
|
+
export function dependencyInstallCommands(manager, workspace, version = SILVERMOON_VERSION) {
|
|
145
146
|
const packageSpecifier = `silvermoon@^${version}`;
|
|
146
147
|
if (manager === "npm") {
|
|
147
|
-
return { executable: "npm", args: ["install", "--save-dev", packageSpecifier] };
|
|
148
|
+
return [{ executable: "npm", args: ["install", "--save-dev", packageSpecifier] }];
|
|
148
149
|
}
|
|
149
150
|
if (manager === "pnpm") {
|
|
150
|
-
return {
|
|
151
|
+
return [{
|
|
151
152
|
executable: "pnpm",
|
|
152
153
|
args: [
|
|
153
154
|
"add",
|
|
@@ -155,21 +156,19 @@ export function dependencyInstallCommand(manager, workspace, version = SILVERMOO
|
|
|
155
156
|
packageSpecifier,
|
|
156
157
|
...(workspace ? ["--workspace-root"] : []),
|
|
157
158
|
],
|
|
158
|
-
};
|
|
159
|
+
}];
|
|
159
160
|
}
|
|
160
161
|
if (manager === "yarn") {
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
],
|
|
169
|
-
};
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
executable: "npm",
|
|
165
|
+
args: ["pkg", "set", `devDependencies.silvermoon=^${version}`],
|
|
166
|
+
},
|
|
167
|
+
{ executable: "yarn", args: ["install"] },
|
|
168
|
+
];
|
|
170
169
|
}
|
|
171
170
|
if (manager === "bun") {
|
|
172
|
-
return { executable: "bun", args: ["add", packageSpecifier, "--dev"] };
|
|
171
|
+
return [{ executable: "bun", args: ["add", packageSpecifier, "--dev"] }];
|
|
173
172
|
}
|
|
174
173
|
return null;
|
|
175
174
|
}
|
|
@@ -183,12 +182,12 @@ function renderCommand({ executable, args }) {
|
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
export function renderDependencyCommand(manager, workspace, version = SILVERMOON_VERSION) {
|
|
186
|
-
const
|
|
187
|
-
return
|
|
185
|
+
const commands = dependencyInstallCommands(manager, workspace, version);
|
|
186
|
+
return commands ? commands.map(renderCommand).join("\n") : null;
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
function dependencyInstruction(packageManager) {
|
|
191
|
-
const
|
|
190
|
+
const renderedCommands = packageManager.manager
|
|
192
191
|
? renderDependencyCommand(
|
|
193
192
|
packageManager.manager,
|
|
194
193
|
packageManager.workspace,
|
|
@@ -198,10 +197,14 @@ function dependencyInstruction(packageManager) {
|
|
|
198
197
|
if (!expectedDependency) {
|
|
199
198
|
return `The running Silvermoon version ${JSON.stringify(SILVERMOON_VERSION)} is not a supported SemVer version; repair the package before declaring its dependency.`;
|
|
200
199
|
}
|
|
201
|
-
if (!
|
|
200
|
+
if (!renderedCommands) {
|
|
202
201
|
return `Do not guess a package-manager command. ${packageManager.reason} Resolve the root package-manager/workspace configuration, then add silvermoon@${expectedDependency} to the root devDependencies.`;
|
|
203
202
|
}
|
|
204
|
-
|
|
203
|
+
const commands = renderedCommands.split("\n");
|
|
204
|
+
if (commands.length === 1) {
|
|
205
|
+
return `From the repository root, run \`${commands[0]}\` to set root devDependencies.silvermoon to ${expectedDependency}.`;
|
|
206
|
+
}
|
|
207
|
+
return `From the repository root, run these commands in order: ${commands.map((command) => `\`${command}\``).join(", then ")} to set root devDependencies.silvermoon to ${expectedDependency}.`;
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
function skillInstruction(skill, packageManager) {
|
|
@@ -218,12 +221,15 @@ function skillInstruction(skill, packageManager) {
|
|
|
218
221
|
export async function inspectNpmProject(
|
|
219
222
|
snapshotRoot,
|
|
220
223
|
repositoryRoot,
|
|
221
|
-
{
|
|
224
|
+
{
|
|
225
|
+
filesystem = DEFAULT_FILESYSTEM,
|
|
226
|
+
readManifest = filesystem.readFile,
|
|
227
|
+
} = {},
|
|
222
228
|
) {
|
|
223
229
|
const manifestPath = resolve(snapshotRoot, "package.json");
|
|
224
230
|
let metadata;
|
|
225
231
|
try {
|
|
226
|
-
metadata = await lstat(manifestPath);
|
|
232
|
+
metadata = await filesystem.lstat(manifestPath);
|
|
227
233
|
} catch (caught) {
|
|
228
234
|
if (caught.code === "ENOENT") return { npmProject: false, findings: [] };
|
|
229
235
|
return {
|
|
@@ -293,7 +299,11 @@ export async function inspectNpmProject(
|
|
|
293
299
|
};
|
|
294
300
|
}
|
|
295
301
|
|
|
296
|
-
const packageManager = await detectPackageManager(
|
|
302
|
+
const packageManager = await detectPackageManager(
|
|
303
|
+
snapshotRoot,
|
|
304
|
+
manifest,
|
|
305
|
+
filesystem,
|
|
306
|
+
);
|
|
297
307
|
const findings = [];
|
|
298
308
|
const sourceCheckout = isSilvermoonSourceCheckout(repositoryRoot, manifest);
|
|
299
309
|
if (!expectedDependency) {
|
|
@@ -367,11 +377,13 @@ function canonicalSkillBytes(content) {
|
|
|
367
377
|
}
|
|
368
378
|
}
|
|
369
379
|
|
|
370
|
-
async function directoryDigest(root) {
|
|
380
|
+
async function directoryDigest(root, filesystem) {
|
|
371
381
|
const hash = createHash("sha256");
|
|
372
382
|
|
|
373
383
|
async function visit(directory) {
|
|
374
|
-
const entries = (
|
|
384
|
+
const entries = (
|
|
385
|
+
await filesystem.readdir(directory, { withFileTypes: true })
|
|
386
|
+
)
|
|
375
387
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
376
388
|
for (const entry of entries) {
|
|
377
389
|
const path = resolve(directory, entry.name);
|
|
@@ -380,7 +392,7 @@ async function directoryDigest(root) {
|
|
|
380
392
|
} else if (entry.isFile()) {
|
|
381
393
|
hash.update(relative(root, path).split(sep).join("/"));
|
|
382
394
|
hash.update("\0");
|
|
383
|
-
hash.update(canonicalSkillBytes(await readFile(path)));
|
|
395
|
+
hash.update(canonicalSkillBytes(await filesystem.readFile(path)));
|
|
384
396
|
hash.update("\0");
|
|
385
397
|
} else if (entry.isSymbolicLink()) {
|
|
386
398
|
await visit(path);
|
|
@@ -394,16 +406,16 @@ async function directoryDigest(root) {
|
|
|
394
406
|
return hash.digest("hex");
|
|
395
407
|
}
|
|
396
408
|
|
|
397
|
-
async function inspectSkill(contentRoot) {
|
|
409
|
+
async function inspectSkill(contentRoot, filesystem) {
|
|
398
410
|
const path = resolve(contentRoot, ...REPOSITORY_SKILL_PATH.split("/"));
|
|
399
411
|
const expectedDigest = await traceAsync(
|
|
400
412
|
"skill.digest-packaged",
|
|
401
413
|
{},
|
|
402
|
-
() => directoryDigest(packagedSkill),
|
|
414
|
+
() => directoryDigest(packagedSkill, DEFAULT_FILESYSTEM),
|
|
403
415
|
);
|
|
404
416
|
let metadata;
|
|
405
417
|
try {
|
|
406
|
-
metadata = await lstat(path);
|
|
418
|
+
metadata = await filesystem.lstat(path);
|
|
407
419
|
} catch (caught) {
|
|
408
420
|
if (caught.code !== "ENOENT") {
|
|
409
421
|
return {
|
|
@@ -442,7 +454,7 @@ async function inspectSkill(contentRoot) {
|
|
|
442
454
|
actualDigest = await traceAsync(
|
|
443
455
|
"skill.digest-repository",
|
|
444
456
|
{},
|
|
445
|
-
() => directoryDigest(path),
|
|
457
|
+
() => directoryDigest(path, filesystem),
|
|
446
458
|
);
|
|
447
459
|
} catch (caught) {
|
|
448
460
|
return {
|
|
@@ -471,10 +483,14 @@ async function inspectSkill(contentRoot) {
|
|
|
471
483
|
|
|
472
484
|
export async function inspectAdoption({
|
|
473
485
|
contentRoot,
|
|
486
|
+
filesystem = DEFAULT_FILESYSTEM,
|
|
487
|
+
repositoryRoot: knownRepositoryRoot,
|
|
474
488
|
root = process.cwd(),
|
|
475
489
|
} = {}) {
|
|
476
490
|
const requestedRoot = resolve(root);
|
|
477
|
-
const git =
|
|
491
|
+
const git = knownRepositoryRoot === undefined
|
|
492
|
+
? runGit(requestedRoot, ["rev-parse", "--show-toplevel"])
|
|
493
|
+
: { ok: true, stdout: resolve(knownRepositoryRoot) };
|
|
478
494
|
const repositoryRoot = git.ok ? resolve(git.stdout) : requestedRoot;
|
|
479
495
|
const snapshotRoot = resolve(contentRoot ?? repositoryRoot);
|
|
480
496
|
const findings = [];
|
|
@@ -493,7 +509,7 @@ export async function inspectAdoption({
|
|
|
493
509
|
const loadedConfig = await traceAsync(
|
|
494
510
|
"config.load",
|
|
495
511
|
{},
|
|
496
|
-
() => loadConfig({ root: snapshotRoot }),
|
|
512
|
+
() => loadConfig({ filesystem, root: snapshotRoot }),
|
|
497
513
|
);
|
|
498
514
|
for (const diagnostic of loadedConfig.diagnostics) {
|
|
499
515
|
findings.push({
|
|
@@ -507,7 +523,7 @@ export async function inspectAdoption({
|
|
|
507
523
|
const npm = await traceAsync(
|
|
508
524
|
"npm-project.inspect",
|
|
509
525
|
{},
|
|
510
|
-
() => inspectNpmProject(snapshotRoot, repositoryRoot),
|
|
526
|
+
() => inspectNpmProject(snapshotRoot, repositoryRoot, { filesystem }),
|
|
511
527
|
);
|
|
512
528
|
findings.push(...npm.findings);
|
|
513
529
|
const skillSource = npm.npmProject && !npm.sourceCheckout
|
|
@@ -516,7 +532,7 @@ export async function inspectAdoption({
|
|
|
516
532
|
const skill = await traceAsync(
|
|
517
533
|
"skill.inspect",
|
|
518
534
|
{},
|
|
519
|
-
() => inspectSkill(snapshotRoot,
|
|
535
|
+
() => inspectSkill(snapshotRoot, filesystem),
|
|
520
536
|
);
|
|
521
537
|
if (!skill.ok) {
|
|
522
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
|
+
}
|