silvermoon 0.0.3 → 0.1.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/src/cli.js CHANGED
@@ -1,116 +1,73 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { Command, CommanderError, Option } from "commander";
3
-
2
+ import {
3
+ Command,
4
+ CommanderError,
5
+ InvalidArgumentError,
6
+ Option,
7
+ } from "commander";
8
+
9
+ import {
10
+ canonicalizeLanguageTag,
11
+ canonicalizeOutputLanguage,
12
+ } from "./language.js";
13
+ import { renderCheck, renderDialogue } from "./dialogue.js";
4
14
  import { checkRepository } from "./index.js";
5
15
  import { createIdea } from "./create-idea.js";
16
+ import { withTraceFile } from "./trace.js";
6
17
  import { whatsNext } from "./whatsnext.js";
7
18
 
8
19
  const { version: VERSION } = JSON.parse(
9
20
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
10
21
  );
11
22
 
23
+ const TRACE_FILE_SUFFIX = ".trace.jsonl";
24
+
12
25
  function write(method, value) {
13
26
  const text = value.replace(/\n$/, "");
14
27
  if (text) method(text);
15
28
  }
16
29
 
30
+ export function normalizeTraceFileName(value) {
31
+ return value.endsWith(TRACE_FILE_SUFFIX)
32
+ ? value
33
+ : `${value}${TRACE_FILE_SUFFIX}`;
34
+ }
35
+
17
36
  function addCommonOptions(command) {
18
37
  return command
19
- .option("--json", "emit the complete machine-readable report")
38
+ .option("--json", "serialize the conversation envelope as JSON")
39
+ .option(
40
+ "--trace <file.trace.jsonl>",
41
+ "write task timing spans as JSONL (suffix is appended when omitted)",
42
+ normalizeTraceFileName,
43
+ )
20
44
  .option("-r, --root <path>", "repository root", process.cwd());
21
45
  }
22
46
 
23
- function renderDiagnostics(report, io) {
24
- for (const item of report.diagnostics) {
25
- const location = item.path ? ` ${item.path}` : "";
26
- const output = item.level === "error" ? io.error : io.log;
27
- output(`${item.level.toUpperCase()} ${item.code}${location}: ${item.message}`);
28
- output(` Fix: ${item.remediation}`);
29
- }
30
- }
31
-
32
- function renderIdeaIdentity(idea) {
33
- return idea.alias === undefined ? idea.id : `${idea.id} (${idea.alias})`;
34
- }
35
-
36
- function renderRemediation(remediation) {
37
- return remediation.kind === "command"
38
- ? `${remediation.executable} ${remediation.args.join(" ")}`
39
- : `${remediation.description} (${remediation.path})`;
40
- }
41
-
42
- function renderWhatsNext(result, io) {
43
- if (result.onboarding) {
44
- io.log(`onboarding ${result.onboarding.status}`);
45
- for (const item of result.onboarding.gaps) {
46
- io.log(
47
- ` ${item.status.padEnd(12)} ${item.id}${item.blocking ? " [blocking]" : " [non-blocking]"}: ${item.title}`,
48
- );
49
- io.log(` depends ${item.dependencies.length ? item.dependencies.join(", ") : "none"}`);
50
- io.log(` observed ${JSON.stringify(item.observed)}`);
51
- if (item.remediation) io.log(` fix ${renderRemediation(item.remediation)}`);
52
- }
53
- if (result.onboarding.recommendedAction) {
54
- io.log(` next ${renderRemediation(result.onboarding.recommendedAction)}`);
55
- }
56
- io.log(` recheck ${renderRemediation(result.onboarding.recheck)}`);
57
- }
58
- io.log(`${result.action.code}: ${result.action.message}`);
59
- io.log(` language ${result.language.tag} (${result.language.source})`);
60
- if (result.observedPrimaryCommit) io.log(` primary ${result.observedPrimaryCommit}`);
61
- if (result.selectedIdea) {
62
- io.log(` idea ${renderIdeaIdentity(result.selectedIdea)}`);
63
- io.log(` state ${result.selectedIdea.state}`);
64
- io.log(` ideal ${result.selectedIdea.idealRevision}`);
65
- io.log(` inner ${result.selectedIdea.implementationRevision}`);
66
- io.log(` outer ${result.selectedIdea.deploymentRevision}`);
67
- }
68
- if (result.action.details?.world) {
69
- const world = result.action.details.world;
70
- io.log(` world ${world.name} (${world.displayName})`);
71
- io.log(` entry ${world.documentPath}`);
72
- io.log(` support ${world.auxiliaryRoot}`);
73
- io.log(` decision ${world.decisionField}: ${world.revision}`);
74
- io.log(` cascade ${world.cascade}`);
75
- }
76
- for (const idea of result.action.details?.ideas ?? []) {
77
- io.log(` option ${renderIdeaIdentity(idea)} ${idea.state}`);
78
- }
47
+ export function render(report, json, io) {
48
+ io.log(
49
+ json
50
+ ? JSON.stringify(report, null, 2)
51
+ : report.intention.command === "check"
52
+ ? renderCheck(report)
53
+ : renderDialogue(report),
54
+ );
79
55
  }
80
56
 
81
- function renderCreateIdea(result, io) {
82
- if (!result.createdIdea) {
83
- renderWhatsNext(result, io);
84
- return;
57
+ function contentLanguageArgument(value) {
58
+ try {
59
+ return canonicalizeLanguageTag(value);
60
+ } catch (caught) {
61
+ throw new InvalidArgumentError(caught.message);
85
62
  }
86
- io.log(`created: ${result.createdIdea.id}`);
87
- io.log(` idea ${result.createdIdea.ideaPath}`);
88
- io.log(` ledger ${result.createdIdea.ledgerPath}`);
89
- io.log(` status ${result.createdIdea.statusPath}`);
90
63
  }
91
64
 
92
- export function render(report, json, io) {
93
- if (json) {
94
- io.log(JSON.stringify(report, null, 2));
95
- return;
96
- }
97
- renderDiagnostics(report, io);
98
- if (!report.ok) {
99
- io.error("FAILED");
100
- return;
101
- }
102
- if (report.command === "whats-next") {
103
- renderWhatsNext(report.result, io);
104
- return;
105
- }
106
- if (report.command === "create-idea") {
107
- renderCreateIdea(report.result, io);
108
- return;
65
+ function outputLanguageArgument(value) {
66
+ try {
67
+ return canonicalizeOutputLanguage(value);
68
+ } catch (caught) {
69
+ throw new InvalidArgumentError(caught.message);
109
70
  }
110
- io.log(`OK: ${report.command}`);
111
- io.log(` target ${report.result.target}`);
112
- if (report.result.commit) io.log(` commit ${report.result.commit}`);
113
- io.log(` ideas ${report.result.checked}`);
114
71
  }
115
72
 
116
73
  export function createProgram(io = console) {
@@ -133,9 +90,12 @@ export function createProgram(io = console) {
133
90
  Examples:
134
91
  $ silvermoon whats-next
135
92
  $ silvermoon whats-next <idea>
93
+ $ silvermoon whats-next <idea> --language zh-CN
136
94
  $ silvermoon whats-next <idea> --json
95
+ $ silvermoon whats-next <idea> --trace whats-next.trace.jsonl
137
96
  $ silvermoon create-idea
138
97
  $ silvermoon check
98
+ $ silvermoon check --language en-US
139
99
  $ silvermoon check --worktree
140
100
  $ silvermoon check --staged
141
101
  $ silvermoon check --commit HEAD
@@ -144,45 +104,94 @@ Examples:
144
104
  addCommonOptions(
145
105
  program
146
106
  .command("whats-next [idea]")
147
- .description("fetch primary and render the highest-priority next action"),
107
+ .description("observe project, repository, and idea readiness")
108
+ .option(
109
+ "--language <tag>",
110
+ "use a built-in output language (en-US or zh-CN) for this invocation",
111
+ outputLanguageArgument,
112
+ ),
148
113
  ).action(async (idea, options) => {
149
- const report = await whatsNext({ enforceOnboarding: true, idea, root: options.root });
114
+ const report = await withTraceFile(
115
+ options.trace,
116
+ "command.whats-next",
117
+ {
118
+ command: "whats-next",
119
+ outputLanguage: options.language ?? null,
120
+ processId: process.pid,
121
+ },
122
+ () => whatsNext({
123
+ idea,
124
+ language: options.language,
125
+ root: options.root,
126
+ }),
127
+ );
150
128
  render(report, options.json, io);
151
- program.setOptionValue("resultCode", report.ok ? 0 : 1);
129
+ program.setOptionValue("resultCode", 0);
152
130
  });
153
131
 
154
132
  addCommonOptions(
155
133
  program
156
134
  .command("create-idea")
157
- .description("create one structured idea scaffold after primary hygiene")
158
- .option("--language <tag>", "persist a canonical language override on the new idea"),
135
+ .description("create one structured idea scaffold after readiness checks")
136
+ .option(
137
+ "--language <tag>",
138
+ "persist a canonical content language override on the new idea",
139
+ contentLanguageArgument,
140
+ ),
159
141
  ).action(async (options) => {
160
- const report = await createIdea({
161
- language: options.language,
162
- root: options.root,
163
- });
142
+ const report = await withTraceFile(
143
+ options.trace,
144
+ "command.create-idea",
145
+ {
146
+ command: "create-idea",
147
+ contentLanguage: options.language ?? null,
148
+ processId: process.pid,
149
+ },
150
+ () => createIdea({
151
+ language: options.language,
152
+ root: options.root,
153
+ }),
154
+ );
164
155
  render(report, options.json, io);
165
- program.setOptionValue("resultCode", report.ok ? 0 : 1);
156
+ program.setOptionValue("resultCode", 0);
166
157
  });
167
158
 
168
159
  addCommonOptions(
169
160
  program
170
161
  .command("check")
171
- .description("validate Silvermoon configuration, idea state, and an optional Git target")
172
- .addOption(new Option("--remote", "fetch and validate the primary tip selected by HEAD").conflicts(["commit", "staged", "worktree"]))
162
+ .description("validate Silvermoon configuration and idea state")
163
+ .option(
164
+ "--language <tag>",
165
+ "use a built-in output language (en-US or zh-CN) for this invocation",
166
+ outputLanguageArgument,
167
+ )
168
+ .addOption(new Option("--remote", "fetch and validate the configured primary tip").conflicts(["commit", "staged", "worktree"]))
173
169
  .addOption(new Option("--commit <revision>", "validate one local commit snapshot").conflicts(["remote", "staged", "worktree"]))
174
170
  .addOption(new Option("--staged", "validate the index snapshot").conflicts(["remote", "commit", "worktree"]))
175
- .addOption(new Option("--worktree", "validate HEAD plus all staged, unstaged, and untracked changes").conflicts(["remote", "commit", "staged"])),
171
+ .addOption(new Option("--worktree", "validate HEAD plus staged, unstaged, and untracked changes").conflicts(["remote", "commit", "staged"])),
176
172
  ).action(async (options) => {
177
- const report = await checkRepository({
178
- commit: options.commit,
179
- remote: options.remote,
180
- root: options.root,
181
- staged: options.staged,
182
- worktree: options.worktree,
183
- });
173
+ const report = await withTraceFile(
174
+ options.trace,
175
+ "command.check",
176
+ {
177
+ command: "check",
178
+ outputLanguage: options.language ?? null,
179
+ processId: process.pid,
180
+ },
181
+ () => checkRepository({
182
+ commit: options.commit,
183
+ language: options.language,
184
+ remote: options.remote,
185
+ root: options.root,
186
+ staged: options.staged,
187
+ worktree: options.worktree,
188
+ }),
189
+ );
184
190
  render(report, options.json, io);
185
- program.setOptionValue("resultCode", report.ok ? 0 : 1);
191
+ program.setOptionValue(
192
+ "resultCode",
193
+ report.observation.state === "project-ready" ? 0 : 1,
194
+ );
186
195
  });
187
196
 
188
197
  return program;
@@ -194,8 +203,12 @@ export async function runCli(args, io = console) {
194
203
  await program.parseAsync(args.length === 0 ? ["--help"] : args, { from: "user" });
195
204
  } catch (caught) {
196
205
  if (caught instanceof CommanderError) return caught.exitCode === 0 ? 0 : 2;
206
+ if (caught.exitCode === 2) {
207
+ io.error(`ERROR usage: ${caught.message}`);
208
+ return 2;
209
+ }
197
210
  io.error(`ERROR command.failed: ${caught.message}`);
198
211
  return 1;
199
212
  }
200
213
  return program.getOptionValue("resultCode") ?? 0;
201
- }
214
+ }