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/adoption.js CHANGED
@@ -3,42 +3,26 @@ import {
3
3
  lstat,
4
4
  readFile,
5
5
  readdir,
6
- realpath,
7
6
  } from "node:fs/promises";
8
- import { dirname, relative, resolve, sep } from "node:path";
7
+ import { relative, resolve, sep } from "node:path";
9
8
  import { fileURLToPath } from "node:url";
10
9
 
10
+ import { diagnosticProblem } from "./dialogue.js";
11
11
  import { loadConfig } from "./config.js";
12
12
  import { runGit } from "./git.js";
13
+ import { traceAsync } from "./trace.js";
13
14
 
14
15
  const packageRoot = fileURLToPath(new URL("..", import.meta.url));
15
16
  const packageJson = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
16
- export const SILVERMOON_VERSION = packageJson.version;
17
- export const REPOSITORY_SKILL_PATHS = [
18
- ".agents/skills/silvermoon",
19
- ".github/skills/silvermoon",
20
- ];
21
-
22
- async function metadata(path) {
23
- try {
24
- return await lstat(path);
25
- } catch (error) {
26
- if (error.code === "ENOENT") return null;
27
- throw error;
28
- }
29
- }
17
+ const packagedSkillsRoot = resolve(packageRoot, "skills");
18
+ const packagedSkill = resolve(packagedSkillsRoot, "silvermoon");
30
19
 
31
- async function jsonFile(path) {
32
- try {
33
- return JSON.parse(await readFile(path, "utf8"));
34
- } catch (error) {
35
- if (error.code === "ENOENT") return null;
36
- return { __invalid: error.message };
37
- }
38
- }
20
+ export const SILVERMOON_VERSION = packageJson.version;
21
+ export const REPOSITORY_SKILL_PATH = ".agents/skills/silvermoon";
39
22
 
40
23
  async function directoryDigest(root) {
41
24
  const hash = createHash("sha256");
25
+
42
26
  async function visit(directory) {
43
27
  const entries = (await readdir(directory, { withFileTypes: true }))
44
28
  .sort((left, right) => left.name.localeCompare(right.name));
@@ -51,263 +35,155 @@ async function directoryDigest(root) {
51
35
  hash.update("\0");
52
36
  hash.update(await readFile(path));
53
37
  hash.update("\0");
38
+ } else if (entry.isSymbolicLink()) {
39
+ await visit(path);
54
40
  } else {
55
41
  throw new Error(`Skill contains a non-regular path: ${path}`);
56
42
  }
57
43
  }
58
44
  }
45
+
59
46
  await visit(root);
60
47
  return hash.digest("hex");
61
48
  }
62
49
 
63
- async function skillDigest(path) {
64
- const entry = await lstat(path);
65
- if (entry.isFile()) {
66
- const pointer = (await readFile(path, "utf8")).trim();
67
- if (!pointer) throw new Error(`Skill pointer is empty: ${path}`);
68
- return directoryDigest(resolve(dirname(path), pointer));
69
- }
70
- if (!entry.isDirectory() && !entry.isSymbolicLink()) {
71
- throw new Error(`Skill discovery path is not a directory: ${path}`);
72
- }
73
- return directoryDigest(path);
74
- }
75
-
76
- function command(executable, args, description) {
77
- return { kind: "command", executable, args, description };
78
- }
79
-
80
- function manual(path, description) {
81
- return { kind: "manual", path, description };
82
- }
83
-
84
- function requirement(id, title, status, blocking, dependencies, observed, remediation = null) {
85
- return { id, title, status, blocking, dependencies, observed, remediation };
86
- }
87
-
88
- async function detectPackageManager(root, manifest) {
89
- for (const [file, name] of [
90
- ["pnpm-lock.yaml", "pnpm"],
91
- ["yarn.lock", "yarn"],
92
- ["bun.lock", "bun"],
93
- ["bun.lockb", "bun"],
94
- ["package-lock.json", "npm"],
95
- ["npm-shrinkwrap.json", "npm"],
96
- ]) {
97
- if (await metadata(resolve(root, file))) return name;
98
- }
99
- const declared = manifest?.packageManager?.split("@")[0];
100
- return ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : "npm";
101
- }
102
-
103
- export async function detectExecutionSource({
104
- root,
105
- runtimeRoot = packageRoot,
106
- } = {}) {
107
- const runtime = await realpath(runtimeRoot);
108
- let repository;
50
+ async function inspectSkill(contentRoot) {
51
+ const path = resolve(contentRoot, ...REPOSITORY_SKILL_PATH.split("/"));
52
+ const expectedDigest = await traceAsync(
53
+ "skill.digest-packaged",
54
+ {},
55
+ () => directoryDigest(packagedSkill),
56
+ );
57
+ let metadata;
109
58
  try {
110
- repository = await realpath(root);
111
- } catch {
112
- repository = resolve(root);
59
+ metadata = await lstat(path);
60
+ } catch (caught) {
61
+ if (caught.code !== "ENOENT") {
62
+ return {
63
+ ok: false,
64
+ problem: {
65
+ type: "canonical-skill-invalid",
66
+ summary: `Cannot inspect ${REPOSITORY_SKILL_PATH}: ${caught.message}`,
67
+ },
68
+ instruction:
69
+ `Repair the path to ${REPOSITORY_SKILL_PATH}, then register the `
70
+ + "canonical skill again.",
71
+ };
72
+ }
73
+ return {
74
+ ok: false,
75
+ problem: {
76
+ type: "canonical-skill-missing",
77
+ summary: `Missing canonical Silvermoon skill at ${REPOSITORY_SKILL_PATH}.`,
78
+ },
79
+ instruction:
80
+ `Run \`npx skills add "${packagedSkillsRoot}" --skill silvermoon `
81
+ + "--agent universal --yes --copy`.",
82
+ };
83
+ }
84
+ if (!metadata.isDirectory() && !metadata.isSymbolicLink()) {
85
+ return {
86
+ ok: false,
87
+ problem: {
88
+ type: "canonical-skill-invalid",
89
+ summary: `${REPOSITORY_SKILL_PATH} must be a directory or directory symlink.`,
90
+ },
91
+ instruction:
92
+ `Preserve the conflicting path, then register the canonical skill at `
93
+ + `${REPOSITORY_SKILL_PATH}.`,
94
+ };
113
95
  }
114
- let local = null;
96
+ let actualDigest;
115
97
  try {
116
- local = await realpath(resolve(repository, "node_modules", "silvermoon"));
117
- } catch (error) {
118
- if (error.code !== "ENOENT") throw error;
98
+ actualDigest = await traceAsync(
99
+ "skill.digest-repository",
100
+ {},
101
+ () => directoryDigest(path),
102
+ );
103
+ } catch (caught) {
104
+ return {
105
+ ok: false,
106
+ problem: {
107
+ type: "canonical-skill-invalid",
108
+ summary: `Cannot inspect ${REPOSITORY_SKILL_PATH}: ${caught.message}`,
109
+ },
110
+ instruction:
111
+ `Repair ${REPOSITORY_SKILL_PATH}, then register the canonical skill again.`,
112
+ };
119
113
  }
120
- const normalizedRuntime = runtime.toLowerCase();
121
- const normalizedRoot = repository.toLowerCase();
122
- const normalizedLocal = local?.toLowerCase() ?? null;
123
- let kind = "unknown";
124
- if (normalizedRuntime === normalizedRoot && await metadata(resolve(runtime, ".git"))) {
125
- kind = "source-checkout";
126
- } else if (normalizedRuntime === normalizedLocal) {
127
- kind = "project-local";
128
- } else if (/(^|[\\/])_npx([\\/]|$)/i.test(runtime)) {
129
- kind = "temporary";
130
- } else if (normalizedRuntime.includes(`${sep}node_modules${sep}`) && !normalizedRuntime.startsWith(normalizedRoot + sep)) {
131
- kind = "global";
114
+ if (actualDigest !== expectedDigest) {
115
+ return {
116
+ ok: false,
117
+ problem: {
118
+ type: "canonical-skill-mismatched",
119
+ summary:
120
+ `${REPOSITORY_SKILL_PATH} does not match the canonical skill bundled `
121
+ + `with Silvermoon ${SILVERMOON_VERSION}.`,
122
+ },
123
+ instruction:
124
+ `Review local skill changes, then run \`npx skills add "${packagedSkillsRoot}" `
125
+ + "--skill silvermoon --agent universal --yes --copy`.",
126
+ };
132
127
  }
133
- return { kind, packageRoot: runtime, version: SILVERMOON_VERSION };
128
+ return { ok: true, digest: actualDigest };
134
129
  }
135
130
 
136
- function installCommand(manager, version) {
137
- if (manager === "pnpm") {
138
- return command("pnpm", ["add", "--save-dev", "--save-exact", `silvermoon@${version}`], `Declare Silvermoon ${version} and install it locally.`);
139
- }
140
- if (manager === "yarn") {
141
- return command("yarn", ["add", "--dev", "--exact", `silvermoon@${version}`], `Declare Silvermoon ${version} and install it locally.`);
142
- }
143
- if (manager === "bun") {
144
- return command("bun", ["add", "--dev", "--exact", `silvermoon@${version}`], `Declare Silvermoon ${version} and install it locally.`);
131
+ export async function inspectAdoption({
132
+ contentRoot,
133
+ root = process.cwd(),
134
+ } = {}) {
135
+ const requestedRoot = resolve(root);
136
+ const git = runGit(requestedRoot, ["rev-parse", "--show-toplevel"]);
137
+ const repositoryRoot = git.ok ? resolve(git.stdout) : requestedRoot;
138
+ const snapshotRoot = resolve(contentRoot ?? repositoryRoot);
139
+ const findings = [];
140
+
141
+ if (!git.ok) {
142
+ findings.push({
143
+ priority: 10,
144
+ problem: {
145
+ type: "git-repository-missing",
146
+ summary: `${requestedRoot} is not an initialized Git repository.`,
147
+ },
148
+ instruction: `Run \`git -C "${requestedRoot}" init\`.`,
149
+ });
145
150
  }
146
- return command("npm", ["install", "--save-dev", "--save-exact", `silvermoon@${version}`], `Declare Silvermoon ${version} and install it locally.`);
147
- }
148
-
149
- export async function inspectAdoption({ root, selector } = {}) {
150
- const repositoryRoot = resolve(root ?? process.cwd());
151
- const source = await detectExecutionSource({ root: repositoryRoot });
152
- const manifest = await jsonFile(resolve(repositoryRoot, "package.json"));
153
- const manager = await detectPackageManager(repositoryRoot, manifest);
154
- const sourceCheckout = source.kind === "source-checkout";
155
- const desiredVersion = source.version;
156
- const declared = manifest?.devDependencies?.silvermoon ?? manifest?.dependencies?.silvermoon ?? null;
157
- const installedManifest = sourceCheckout
158
- ? packageJson
159
- : await jsonFile(resolve(repositoryRoot, "node_modules", "silvermoon", "package.json"));
160
- const installedVersion = installedManifest?.version ?? null;
161
- const requirements = [];
162
151
 
163
- const git = runGit(repositoryRoot, ["rev-parse", "--show-toplevel"]);
164
- requirements.push(requirement(
165
- "repository.git",
166
- "Git repository",
167
- git.ok ? "satisfied" : "missing",
168
- true,
169
- [],
170
- { root: git.ok ? git.stdout : null },
171
- git.ok ? null : command("git", ["init"], "Initialize this directory as a Git repository."),
172
- ));
173
-
174
- requirements.push(requirement(
175
- "runtime.execution-source",
176
- "Authoritative execution source",
177
- ["source-checkout", "project-local", "temporary"].includes(source.kind)
178
- ? "satisfied"
179
- : source.kind === "global" ? "conflicting" : "unknown",
180
- !sourceCheckout && !["project-local", "temporary"].includes(source.kind),
181
- ["global", "unknown"].includes(source.kind)
182
- ? ["repository.git", "package.installed"]
183
- : ["repository.git"],
184
- source,
185
- ["global", "unknown"].includes(source.kind)
186
- ? command("npx", ["--no-install", "silvermoon", "whats-next", ...(selector ? [selector] : []), "--json"], "Re-run with the repository-local CLI.")
187
- : null,
188
- ));
189
-
190
- requirements.push(requirement(
191
- "package.manifest",
192
- "Exact project dependency",
193
- sourceCheckout
194
- ? "inapplicable"
195
- : declared === desiredVersion ? "satisfied" : declared ? "mismatched" : "missing",
196
- !sourceCheckout,
197
- ["repository.git"],
198
- { packageManager: manager, declared, expected: sourceCheckout ? null : desiredVersion },
199
- sourceCheckout ? null : installCommand(manager, desiredVersion),
200
- ));
201
-
202
- const installStatus = sourceCheckout
203
- ? "satisfied"
204
- : installedVersion === desiredVersion ? "satisfied" : installedVersion ? "mismatched" : "missing";
205
- requirements.push(requirement(
206
- "package.installed",
207
- "Installed project CLI",
208
- installStatus,
209
- true,
210
- sourceCheckout ? ["runtime.execution-source"] : ["package.manifest"],
211
- { installedVersion, expected: desiredVersion },
212
- installStatus === "satisfied"
213
- ? null
214
- : command(manager, ["install"], `Install the lockfile-resolved Silvermoon ${desiredVersion} dependency.`),
215
- ));
216
-
217
- const packagedSkill = resolve(packageRoot, "skills", "silvermoon");
218
- let skillStatus = "blocked";
219
- const skillObserved = { paths: [], expectedDigest: null };
220
- if (installStatus === "satisfied") {
221
- const expectedDigest = await directoryDigest(packagedSkill);
222
- skillObserved.expectedDigest = expectedDigest;
223
- for (const path of REPOSITORY_SKILL_PATHS) {
224
- try {
225
- skillObserved.paths.push({
226
- path,
227
- digest: await skillDigest(resolve(repositoryRoot, ...path.split("/"))),
228
- });
229
- } catch (error) {
230
- if (error.code !== "ENOENT") {
231
- skillObserved.paths.push({ path, error: error.message });
232
- }
233
- }
234
- }
235
- skillStatus = skillObserved.paths.length === 0
236
- ? "missing"
237
- : skillObserved.paths.every(({ digest }) => digest === expectedDigest)
238
- ? "satisfied"
239
- : skillObserved.paths.some(({ error }) => error)
240
- ? "conflicting"
241
- : "mismatched";
152
+ const loadedConfig = await traceAsync(
153
+ "config.load",
154
+ {},
155
+ () => loadConfig({ root: snapshotRoot }),
156
+ );
157
+ for (const diagnostic of loadedConfig.diagnostics) {
158
+ findings.push({
159
+ priority: diagnostic.code === "config.unsupported-version" ? 20 : 30,
160
+ problem: diagnosticProblem(diagnostic),
161
+ instruction: diagnostic.remediation,
162
+ sourceDiagnostic: diagnostic,
163
+ });
242
164
  }
243
- requirements.push(requirement(
244
- "skill.repository-local",
245
- "Repository-local canonical skill",
246
- skillStatus,
247
- true,
248
- ["package.installed"],
249
- skillObserved,
250
- skillStatus === "satisfied" || skillStatus === "blocked"
251
- ? null
252
- : command(
253
- "npx",
254
- [
255
- "skills",
256
- "add",
257
- "./node_modules/silvermoon/skills",
258
- "--skill",
259
- "silvermoon",
260
- "--agent",
261
- "github-copilot",
262
- "--yes",
263
- "--copy",
264
- ],
265
- "Register the installed package's canonical skill through the supported npx skills interface.",
266
- ),
267
- ));
268
-
269
- const loadedConfig = await loadConfig({ root: repositoryRoot });
270
- const missingConfig = loadedConfig.diagnostics.some(({ code }) => code === "config.missing");
271
- const configStatus = loadedConfig.config
272
- ? "satisfied"
273
- : missingConfig ? "missing" : "conflicting";
274
- requirements.push(requirement(
275
- "repository.configuration",
276
- "Silvermoon repository configuration",
277
- configStatus,
278
- true,
279
- ["repository.git"],
280
- { path: ".silvermoon/config.yaml", diagnostics: loadedConfig.diagnostics },
281
- configStatus === "satisfied"
282
- ? null
283
- : missingConfig
284
- ? manual(".silvermoon/config.yaml", "Create the documented configuration through ordinary reviewed file editing.")
285
- : manual(".silvermoon/config.yaml", "Repair the reported configuration conflicts without overwriting unrelated content."),
286
- ));
287
165
 
288
- const blockingFindings = requirements.filter(({ blocking, status }) =>
289
- blocking && !["satisfied", "inapplicable"].includes(status)
290
- );
291
- const recommended = requirements.find(({ remediation, status, dependencies }) =>
292
- remediation
293
- && !["satisfied", "inapplicable"].includes(status)
294
- && dependencies.every((id) => {
295
- const dependency = requirements.find((item) => item.id === id);
296
- return dependency && ["satisfied", "inapplicable"].includes(dependency.status);
297
- })
166
+ const skill = await traceAsync(
167
+ "skill.inspect",
168
+ {},
169
+ () => inspectSkill(snapshotRoot),
298
170
  );
171
+ if (!skill.ok) {
172
+ findings.push({
173
+ priority: 40,
174
+ problem: skill.problem,
175
+ instruction: skill.instruction,
176
+ });
177
+ }
178
+ findings.sort((left, right) => left.priority - right.priority);
179
+
299
180
  return {
300
- status: blockingFindings.length === 0 ? "ready" : "blocked",
301
- executionSource: source,
302
- packageManager: manager,
303
- desiredVersion,
304
- requirements,
305
- findings: requirements.filter(({ status }) => !["satisfied", "inapplicable"].includes(status)),
306
- recommendedAction: recommended?.remediation ?? null,
307
- recheck: command(
308
- "npx",
309
- ["--no-install", "silvermoon", "whats-next", ...(selector ? [selector] : []), "--json"],
310
- "Re-run the complete diagnosis after an observable change.",
311
- ),
181
+ config: loadedConfig.config,
182
+ findings,
183
+ gitReady: git.ok,
184
+ instructions: findings.map(({ instruction }) => instruction),
185
+ problems: findings.map(({ problem }) => problem),
186
+ root: repositoryRoot,
187
+ skill,
312
188
  };
313
189
  }