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/index.js CHANGED
@@ -1,248 +1,318 @@
1
1
  import { resolve } from "node:path";
2
2
 
3
+ import {
4
+ diagnosticProblem,
5
+ } from "./dialogue.js";
3
6
  import { loadConfig } from "./config.js";
4
7
  import {
5
8
  fetchPrimary,
6
9
  indexSnapshot,
7
10
  resolveCommit,
8
11
  runGit,
12
+ sanitizeGitMessage,
9
13
  worktreeSnapshot,
10
14
  withTemporaryTree,
11
15
  withTemporaryWorktree,
12
16
  } from "./git.js";
13
- import { inspectIdeaLayout } from "./idea-layout.js";
17
+ import {
18
+ canonicalizeOutputLanguage,
19
+ DEFAULT_LANGUAGE,
20
+ } from "./language.js";
21
+ import {
22
+ observeSnapshot,
23
+ unavailableObservation,
24
+ } from "./observation.js";
14
25
 
15
- function ideaSummary(idea) {
16
- const value = {
17
- id: idea.id,
18
- idealRevision: idea.idealRevision,
19
- implementationRevision: idea.implementationRevision,
20
- deploymentRevision: idea.deploymentRevision,
21
- state: idea.state,
22
- };
23
- if (idea.alias !== undefined) value.alias = idea.alias;
24
- return value;
26
+ function targetArgument({ commit, remote, staged, worktree }) {
27
+ if (remote) return { type: "remote" };
28
+ if (commit !== undefined) return { type: "commit", revision: commit };
29
+ if (staged) return { type: "staged" };
30
+ if (worktree) return { type: "worktree" };
31
+ return { type: "head" };
25
32
  }
26
33
 
27
- async function inspectSnapshot({
28
- baseRevision,
29
- gitRoot = root,
30
- historyCommit,
34
+ function failureReport({
35
+ intention,
36
+ outputLanguage,
37
+ problem,
31
38
  root,
32
- snapshotTree,
33
- target = "local",
34
- validateCandidate = false,
39
+ version,
35
40
  }) {
36
- const repositoryRoot = resolve(root);
37
- const loaded = await loadConfig({ root: repositoryRoot });
38
- const layout = loaded.config
39
- ? await inspectIdeaLayout({
40
- baseRevision,
41
- config: loaded.config,
42
- gitRoot,
43
- historyCommit,
44
- root: repositoryRoot,
45
- snapshotTree,
46
- validateCandidate,
47
- })
48
- : { diagnostics: [], ideas: [] };
49
- const diagnostics = [
50
- ...loaded.diagnostics,
51
- ...layout.diagnostics,
52
- ];
53
-
54
- const report = {
55
- command: "check",
56
- ok: diagnostics.every(({ level }) => level !== "error"),
57
- root: repositoryRoot,
58
- diagnostics,
59
- result: {
60
- target,
61
- checked: layout.ideas.length,
62
- ideas: layout.ideas.map(ideaSummary),
63
- },
41
+ return {
42
+ intention,
43
+ observation: unavailableObservation({
44
+ outputLanguage,
45
+ problem,
46
+ root,
47
+ version,
48
+ }),
64
49
  };
65
- return { config: loaded.config, layout, report };
66
50
  }
67
51
 
68
- function targetFailure(root, code, message, remediation) {
69
- return {
70
- command: "check",
71
- ok: false,
72
- root,
73
- diagnostics: [{ code, level: "error", message, remediation }],
74
- result: null,
75
- };
52
+ async function inspectTree({
53
+ gitRoot,
54
+ outputLanguage,
55
+ root,
56
+ tree,
57
+ userHome,
58
+ version,
59
+ }) {
60
+ return withTemporaryTree(gitRoot, tree, (contentRoot) =>
61
+ observeSnapshot({
62
+ contentRoot,
63
+ gitRoot,
64
+ outputLanguage,
65
+ root,
66
+ projectOnly: true,
67
+ snapshotTree: tree,
68
+ userHome,
69
+ version,
70
+ })
71
+ );
76
72
  }
77
73
 
78
- function finishTarget({ commit, inspected, root, target }) {
79
- const report = inspected.report;
80
- report.root = root;
81
- report.result = { ...report.result, target };
82
- if (commit) report.result.commit = commit;
83
- report.ok = report.diagnostics.every(({ level }) => level !== "error");
84
- return report;
74
+ function finishCheck(intention, observed) {
75
+ return { intention, observation: observed.observation };
85
76
  }
86
77
 
87
78
  export async function checkRepository({
88
79
  commit,
80
+ language,
89
81
  remote = false,
90
82
  root = process.cwd(),
91
83
  staged = false,
84
+ userHome,
92
85
  worktree = false,
93
86
  } = {}) {
94
- const repositoryRoot = resolve(root);
95
- const targetCount = [remote, commit !== undefined, staged, worktree].filter(Boolean).length;
87
+ const canonicalLanguage = language === undefined
88
+ ? undefined
89
+ : canonicalizeOutputLanguage(language);
90
+ const fallbackOutputLanguage = canonicalLanguage ?? DEFAULT_LANGUAGE;
91
+ const requestedRoot = resolve(root);
92
+ const targetCount = [remote, commit !== undefined, staged, worktree]
93
+ .filter(Boolean).length;
96
94
  if (targetCount > 1) {
97
- return targetFailure(
98
- repositoryRoot,
99
- "check.target.conflict",
100
- "Check targets are mutually exclusive.",
101
- "Choose exactly one of remote, commit, staged, or worktree.",
95
+ const error = new Error(
96
+ "Check targets are mutually exclusive; choose one of remote, commit, staged, or worktree.",
102
97
  );
98
+ error.exitCode = 2;
99
+ throw error;
103
100
  }
101
+ const target = targetArgument({ commit, remote, staged, worktree });
102
+ const intention = {
103
+ command: "check",
104
+ args: {
105
+ target,
106
+ language: canonicalLanguage ?? null,
107
+ },
108
+ };
109
+ const repository = runGit(requestedRoot, ["rev-parse", "--show-toplevel"]);
110
+ if (!repository.ok) {
111
+ const observed = await observeSnapshot({
112
+ outputLanguage: canonicalLanguage,
113
+ projectOnly: true,
114
+ root: requestedRoot,
115
+ userHome,
116
+ version: target.type === "commit" || target.type === "head"
117
+ ? { type: "commit", commit: null }
118
+ : target.type === "remote"
119
+ ? { type: "remote", commit: null }
120
+ : { type: target.type },
121
+ });
122
+ return finishCheck(intention, observed);
123
+ }
124
+ const repositoryRoot = resolve(repository.stdout);
104
125
 
105
126
  if (remote) {
127
+ let head;
106
128
  try {
107
- const head = resolveCommit(repositoryRoot, "HEAD");
108
- const bootstrap = await withTemporaryWorktree(repositoryRoot, head, (worktreeRoot, tree) =>
109
- inspectSnapshot({
110
- gitRoot: repositoryRoot,
111
- root: worktreeRoot,
112
- snapshotTree: tree,
113
- target: "remote",
114
- }),
129
+ head = resolveCommit(repositoryRoot, "HEAD");
130
+ } catch (caught) {
131
+ return failureReport({
132
+ intention,
133
+ outputLanguage: fallbackOutputLanguage,
134
+ problem: {
135
+ type: "head-unavailable",
136
+ summary: sanitizeGitMessage(caught.message),
137
+ },
138
+ root: repositoryRoot,
139
+ version: { type: "remote", commit: null },
140
+ });
141
+ }
142
+ let bootstrap;
143
+ try {
144
+ bootstrap = await withTemporaryWorktree(
145
+ repositoryRoot,
146
+ head,
147
+ (contentRoot) => loadConfig({ root: contentRoot }),
115
148
  );
116
- if (!bootstrap.config) {
117
- return finishTarget({ inspected: bootstrap, root: repositoryRoot, target: "remote" });
118
- }
119
- const primary = fetchPrimary(repositoryRoot, bootstrap.config);
120
- const inspected = await withTemporaryWorktree(repositoryRoot, primary, (worktree, tree) =>
121
- inspectSnapshot({
149
+ } catch (caught) {
150
+ return failureReport({
151
+ intention,
152
+ outputLanguage: fallbackOutputLanguage,
153
+ problem: {
154
+ type: "snapshot-inspection-failed",
155
+ summary: sanitizeGitMessage(caught.message),
156
+ },
157
+ root: repositoryRoot,
158
+ version: { type: "remote", commit: null },
159
+ });
160
+ }
161
+ if (!bootstrap.config) {
162
+ return {
163
+ intention,
164
+ observation: {
165
+ state: "project-setup-required",
166
+ observedThrough: "version",
167
+ root: repositoryRoot,
168
+ version: { type: "remote", commit: null },
169
+ outputLanguage: fallbackOutputLanguage,
170
+ problems: bootstrap.diagnostics.map((diagnostic) =>
171
+ diagnosticProblem(diagnostic, fallbackOutputLanguage)
172
+ ),
173
+ },
174
+ };
175
+ }
176
+
177
+ let primary;
178
+ try {
179
+ primary = fetchPrimary(repositoryRoot, bootstrap.config);
180
+ } catch (caught) {
181
+ const summary = sanitizeGitMessage(caught.message);
182
+ return failureReport({
183
+ intention,
184
+ outputLanguage: fallbackOutputLanguage,
185
+ root: repositoryRoot,
186
+ version: { type: "remote", commit: null },
187
+ problem: { type: "primary-fetch-failed", summary },
188
+ });
189
+ }
190
+ try {
191
+ const observed = await withTemporaryWorktree(
192
+ repositoryRoot,
193
+ primary,
194
+ (contentRoot, tree) => observeSnapshot({
195
+ contentRoot,
122
196
  gitRoot: repositoryRoot,
123
- historyCommit: primary,
124
- root: worktree,
197
+ outputLanguage: canonicalLanguage,
198
+ root: repositoryRoot,
199
+ projectOnly: true,
125
200
  snapshotTree: tree,
126
- target: "remote",
201
+ userHome,
202
+ version: { type: "remote", commit: primary },
127
203
  }),
128
204
  );
129
- return finishTarget({
130
- commit: primary,
131
- inspected,
205
+ return finishCheck(intention, observed);
206
+ } catch (caught) {
207
+ return failureReport({
208
+ intention,
209
+ outputLanguage: fallbackOutputLanguage,
210
+ problem: {
211
+ type: "snapshot-inspection-failed",
212
+ summary: sanitizeGitMessage(caught.message),
213
+ },
132
214
  root: repositoryRoot,
133
- target: "remote",
215
+ version: { type: "remote", commit: primary },
134
216
  });
135
- } catch (caught) {
136
- return targetFailure(
137
- repositoryRoot,
138
- "git.fetch.failed",
139
- caught.message,
140
- "Check remote access and retry.",
141
- );
142
217
  }
143
218
  }
144
219
 
145
220
  if (commit !== undefined || (!staged && !worktree)) {
146
221
  const revision = commit ?? "HEAD";
147
- const target = commit === undefined ? "head" : "commit";
148
222
  let resolvedCommit;
149
223
  try {
150
224
  resolvedCommit = resolveCommit(repositoryRoot, revision);
151
225
  } catch (caught) {
152
- return targetFailure(
153
- repositoryRoot,
154
- "git.commit.invalid",
155
- caught.message,
156
- commit === undefined
157
- ? "Create or restore a valid HEAD commit."
158
- : "Choose a commit available in the local repository.",
159
- );
226
+ return failureReport({
227
+ intention,
228
+ outputLanguage: fallbackOutputLanguage,
229
+ problem: {
230
+ type: "commit-unavailable",
231
+ summary: sanitizeGitMessage(caught.message),
232
+ },
233
+ root: repositoryRoot,
234
+ version: { type: "commit", commit: null },
235
+ });
160
236
  }
161
237
  try {
162
- const parent = runGit(repositoryRoot, ["rev-parse", `${resolvedCommit}^1`]);
163
- const inspected = await withTemporaryWorktree(repositoryRoot, resolvedCommit, (worktree, tree) =>
164
- inspectSnapshot({
165
- baseRevision: parent.ok ? parent.stdout : null,
238
+ const observed = await withTemporaryWorktree(
239
+ repositoryRoot,
240
+ resolvedCommit,
241
+ (contentRoot, tree) => observeSnapshot({
242
+ contentRoot,
166
243
  gitRoot: repositoryRoot,
167
- root: worktree,
244
+ outputLanguage: canonicalLanguage,
245
+ root: repositoryRoot,
246
+ projectOnly: true,
168
247
  snapshotTree: tree,
169
- target,
170
- validateCandidate: true,
248
+ userHome,
249
+ version: { type: "commit", commit: resolvedCommit },
171
250
  }),
172
251
  );
173
- return finishTarget({
174
- commit: resolvedCommit,
175
- inspected,
252
+ return finishCheck(intention, observed);
253
+ } catch (caught) {
254
+ return failureReport({
255
+ intention,
256
+ outputLanguage: fallbackOutputLanguage,
257
+ problem: {
258
+ type: "snapshot-inspection-failed",
259
+ summary: sanitizeGitMessage(caught.message),
260
+ },
176
261
  root: repositoryRoot,
177
- target,
262
+ version: { type: "commit", commit: resolvedCommit },
178
263
  });
179
- } catch (caught) {
180
- return targetFailure(
181
- repositoryRoot,
182
- "git.snapshot.failed",
183
- caught.message,
184
- "Resolve the repository state and retry the commit check.",
185
- );
186
264
  }
187
265
  }
188
266
 
189
267
  if (staged) {
190
268
  try {
191
269
  const snapshot = indexSnapshot(repositoryRoot);
192
- const inspected = await withTemporaryTree(repositoryRoot, snapshot.tree, (worktree) =>
193
- inspectSnapshot({
194
- baseRevision: "HEAD",
195
- gitRoot: repositoryRoot,
196
- root: worktree,
197
- snapshotTree: snapshot.tree,
198
- target: "staged",
199
- validateCandidate: true,
200
- }),
201
- );
202
- return finishTarget({
203
- inspected,
270
+ const observed = await inspectTree({
271
+ gitRoot: repositoryRoot,
272
+ outputLanguage: canonicalLanguage,
204
273
  root: repositoryRoot,
205
- target: "staged",
274
+ tree: snapshot.tree,
275
+ userHome,
276
+ version: { type: "staged" },
206
277
  });
278
+ return finishCheck(intention, observed);
207
279
  } catch (caught) {
208
- return targetFailure(
209
- repositoryRoot,
210
- "git.snapshot.failed",
211
- caught.message,
212
- "Resolve the index state and retry the staged check.",
213
- );
214
- }
215
- }
216
-
217
- if (worktree) {
218
- try {
219
- const snapshot = worktreeSnapshot(repositoryRoot);
220
- const inspected = await withTemporaryTree(repositoryRoot, snapshot.tree, (worktree) =>
221
- inspectSnapshot({
222
- baseRevision: "HEAD",
223
- gitRoot: repositoryRoot,
224
- root: worktree,
225
- snapshotTree: snapshot.tree,
226
- target: "worktree",
227
- validateCandidate: true,
228
- }),
229
- );
230
- return finishTarget({
231
- inspected,
280
+ return failureReport({
281
+ intention,
282
+ outputLanguage: fallbackOutputLanguage,
283
+ problem: {
284
+ type: "snapshot-inspection-failed",
285
+ summary: sanitizeGitMessage(caught.message),
286
+ },
232
287
  root: repositoryRoot,
233
- target: "worktree",
288
+ version: { type: "staged" },
234
289
  });
235
- } catch (caught) {
236
- return targetFailure(
237
- repositoryRoot,
238
- "git.snapshot.failed",
239
- caught.message,
240
- "Resolve the worktree state and retry the worktree check.",
241
- );
242
290
  }
243
291
  }
244
292
 
245
- throw new Error("Unreachable check target");
293
+ try {
294
+ const snapshot = worktreeSnapshot(repositoryRoot);
295
+ const observed = await inspectTree({
296
+ gitRoot: repositoryRoot,
297
+ outputLanguage: canonicalLanguage,
298
+ root: repositoryRoot,
299
+ tree: snapshot.tree,
300
+ userHome,
301
+ version: { type: "worktree" },
302
+ });
303
+ return finishCheck(intention, observed);
304
+ } catch (caught) {
305
+ return failureReport({
306
+ intention,
307
+ outputLanguage: fallbackOutputLanguage,
308
+ problem: {
309
+ type: "snapshot-inspection-failed",
310
+ summary: sanitizeGitMessage(caught.message),
311
+ },
312
+ root: repositoryRoot,
313
+ version: { type: "worktree" },
314
+ });
315
+ }
246
316
  }
247
317
 
248
318
  export {
package/src/language.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export const DEFAULT_LANGUAGE = "en-US";
2
+ export const OUTPUT_LANGUAGES = Object.freeze(["en-US", "zh-CN"]);
3
+
4
+ const OUTPUT_LANGUAGE_SET = new Set(OUTPUT_LANGUAGES);
2
5
 
3
6
  export function canonicalizeLanguageTag(value) {
4
7
  if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
@@ -21,9 +24,43 @@ export function isCanonicalLanguageTag(value) {
21
24
  }
22
25
  }
23
26
 
27
+ export function canonicalizeOutputLanguage(value) {
28
+ let canonical;
29
+ try {
30
+ canonical = canonicalizeLanguageTag(value);
31
+ } catch (caught) {
32
+ caught.exitCode = 2;
33
+ throw caught;
34
+ }
35
+ if (!OUTPUT_LANGUAGE_SET.has(canonical)) {
36
+ const error = new Error(
37
+ `unsupported output language: ${canonical}; expected ${OUTPUT_LANGUAGES.join(" or ")}`,
38
+ );
39
+ error.exitCode = 2;
40
+ throw error;
41
+ }
42
+ return canonical;
43
+ }
44
+
24
45
  export function resolveLanguage({ idea, project, global } = {}) {
25
46
  if (idea !== undefined) return { tag: idea, source: "idea" };
26
47
  if (project !== undefined) return { tag: project, source: "project" };
27
48
  if (global !== undefined) return { tag: global, source: "global" };
28
49
  return { tag: DEFAULT_LANGUAGE, source: "default" };
29
50
  }
51
+
52
+ export function resolveOutputLanguage({ content, override } = {}) {
53
+ if (override !== undefined) {
54
+ return {
55
+ tag: canonicalizeOutputLanguage(override),
56
+ source: "override",
57
+ };
58
+ }
59
+ if (content?.toLowerCase().startsWith("zh")) {
60
+ return { tag: "zh-CN", source: "content" };
61
+ }
62
+ return {
63
+ tag: DEFAULT_LANGUAGE,
64
+ source: content === undefined ? "default" : "content",
65
+ };
66
+ }