silvermoon 0.0.2 → 0.1.0

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,62 @@
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 { canonicalizeLanguageTag } from "./language.js";
10
+ import { renderCheck, renderDialogue } from "./dialogue.js";
4
11
  import { checkRepository } from "./index.js";
5
12
  import { createIdea } from "./create-idea.js";
13
+ import { withTraceFile } from "./trace.js";
6
14
  import { whatsNext } from "./whatsnext.js";
7
15
 
8
16
  const { version: VERSION } = JSON.parse(
9
17
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
10
18
  );
11
19
 
20
+ const TRACE_FILE_SUFFIX = ".trace.jsonl";
21
+
12
22
  function write(method, value) {
13
23
  const text = value.replace(/\n$/, "");
14
24
  if (text) method(text);
15
25
  }
16
26
 
27
+ export function normalizeTraceFileName(value) {
28
+ return value.endsWith(TRACE_FILE_SUFFIX)
29
+ ? value
30
+ : `${value}${TRACE_FILE_SUFFIX}`;
31
+ }
32
+
17
33
  function addCommonOptions(command) {
18
34
  return command
19
- .option("--json", "emit the complete machine-readable report")
35
+ .option("--json", "serialize the conversation envelope as JSON")
36
+ .option(
37
+ "--trace <file.trace.jsonl>",
38
+ "write task timing spans as JSONL (suffix is appended when omitted)",
39
+ normalizeTraceFileName,
40
+ )
20
41
  .option("-r, --root <path>", "repository root", process.cwd());
21
42
  }
22
43
 
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
- }
79
- }
80
-
81
- function renderCreateIdea(result, io) {
82
- if (!result.createdIdea) {
83
- renderWhatsNext(result, io);
84
- return;
85
- }
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}`);
44
+ export function render(report, json, io) {
45
+ io.log(
46
+ json
47
+ ? JSON.stringify(report, null, 2)
48
+ : report.intention.command === "check"
49
+ ? renderCheck(report)
50
+ : renderDialogue(report),
51
+ );
90
52
  }
91
53
 
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;
54
+ function languageArgument(value) {
55
+ try {
56
+ return canonicalizeLanguageTag(value);
57
+ } catch (caught) {
58
+ throw new InvalidArgumentError(caught.message);
109
59
  }
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
60
  }
115
61
 
116
62
  export function createProgram(io = console) {
@@ -134,6 +80,7 @@ Examples:
134
80
  $ silvermoon whats-next
135
81
  $ silvermoon whats-next <idea>
136
82
  $ silvermoon whats-next <idea> --json
83
+ $ silvermoon whats-next <idea> --trace whats-next.trace.jsonl
137
84
  $ silvermoon create-idea
138
85
  $ silvermoon check
139
86
  $ silvermoon check --worktree
@@ -144,45 +91,67 @@ Examples:
144
91
  addCommonOptions(
145
92
  program
146
93
  .command("whats-next [idea]")
147
- .description("fetch primary and render the highest-priority next action"),
94
+ .description("observe project, repository, and idea readiness"),
148
95
  ).action(async (idea, options) => {
149
- const report = await whatsNext({ enforceOnboarding: true, idea, root: options.root });
96
+ const report = await withTraceFile(
97
+ options.trace,
98
+ "command.whats-next",
99
+ { command: "whats-next", processId: process.pid },
100
+ () => whatsNext({ idea, root: options.root }),
101
+ );
150
102
  render(report, options.json, io);
151
- program.setOptionValue("resultCode", report.ok ? 0 : 1);
103
+ program.setOptionValue("resultCode", 0);
152
104
  });
153
105
 
154
106
  addCommonOptions(
155
107
  program
156
108
  .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"),
109
+ .description("create one structured idea scaffold after readiness checks")
110
+ .option(
111
+ "--language <tag>",
112
+ "persist a canonical language override on the new idea",
113
+ languageArgument,
114
+ ),
159
115
  ).action(async (options) => {
160
- const report = await createIdea({
161
- language: options.language,
162
- root: options.root,
163
- });
116
+ const report = await withTraceFile(
117
+ options.trace,
118
+ "command.create-idea",
119
+ { command: "create-idea", processId: process.pid },
120
+ () => createIdea({
121
+ language: options.language,
122
+ root: options.root,
123
+ }),
124
+ );
164
125
  render(report, options.json, io);
165
- program.setOptionValue("resultCode", report.ok ? 0 : 1);
126
+ program.setOptionValue("resultCode", 0);
166
127
  });
167
128
 
168
129
  addCommonOptions(
169
130
  program
170
131
  .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"]))
132
+ .description("validate Silvermoon configuration and idea state")
133
+ .addOption(new Option("--remote", "fetch and validate the configured primary tip").conflicts(["commit", "staged", "worktree"]))
173
134
  .addOption(new Option("--commit <revision>", "validate one local commit snapshot").conflicts(["remote", "staged", "worktree"]))
174
135
  .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"])),
136
+ .addOption(new Option("--worktree", "validate HEAD plus staged, unstaged, and untracked changes").conflicts(["remote", "commit", "staged"])),
176
137
  ).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
- });
138
+ const report = await withTraceFile(
139
+ options.trace,
140
+ "command.check",
141
+ { command: "check", processId: process.pid },
142
+ () => checkRepository({
143
+ commit: options.commit,
144
+ remote: options.remote,
145
+ root: options.root,
146
+ staged: options.staged,
147
+ worktree: options.worktree,
148
+ }),
149
+ );
184
150
  render(report, options.json, io);
185
- program.setOptionValue("resultCode", report.ok ? 0 : 1);
151
+ program.setOptionValue(
152
+ "resultCode",
153
+ report.observation.state === "project-ready" ? 0 : 1,
154
+ );
186
155
  });
187
156
 
188
157
  return program;
@@ -194,8 +163,12 @@ export async function runCli(args, io = console) {
194
163
  await program.parseAsync(args.length === 0 ? ["--help"] : args, { from: "user" });
195
164
  } catch (caught) {
196
165
  if (caught instanceof CommanderError) return caught.exitCode === 0 ? 0 : 2;
166
+ if (caught.exitCode === 2) {
167
+ io.error(`ERROR usage: ${caught.message}`);
168
+ return 2;
169
+ }
197
170
  io.error(`ERROR command.failed: ${caught.message}`);
198
171
  return 1;
199
172
  }
200
173
  return program.getOptionValue("resultCode") ?? 0;
201
- }
174
+ }