skyl.dev 0.1.0 → 0.1.2

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.
Files changed (139) hide show
  1. package/README.md +77 -0
  2. package/dist/bin.js +2265 -2
  3. package/package.json +9 -11
  4. package/dist/bin.d.ts +0 -3
  5. package/dist/bin.d.ts.map +0 -1
  6. package/dist/bin.js.map +0 -1
  7. package/dist/cli.d.ts +0 -2
  8. package/dist/cli.d.ts.map +0 -1
  9. package/dist/cli.js +0 -158
  10. package/dist/cli.js.map +0 -1
  11. package/dist/commands/add.d.ts +0 -10
  12. package/dist/commands/add.d.ts.map +0 -1
  13. package/dist/commands/add.js +0 -38
  14. package/dist/commands/add.js.map +0 -1
  15. package/dist/commands/audit.d.ts +0 -16
  16. package/dist/commands/audit.d.ts.map +0 -1
  17. package/dist/commands/audit.js +0 -91
  18. package/dist/commands/audit.js.map +0 -1
  19. package/dist/commands/context.d.ts +0 -17
  20. package/dist/commands/context.d.ts.map +0 -1
  21. package/dist/commands/context.js +0 -47
  22. package/dist/commands/context.js.map +0 -1
  23. package/dist/commands/diff.d.ts +0 -12
  24. package/dist/commands/diff.d.ts.map +0 -1
  25. package/dist/commands/diff.js +0 -79
  26. package/dist/commands/diff.js.map +0 -1
  27. package/dist/commands/learn.d.ts +0 -18
  28. package/dist/commands/learn.d.ts.map +0 -1
  29. package/dist/commands/learn.js +0 -115
  30. package/dist/commands/learn.js.map +0 -1
  31. package/dist/commands/lint.d.ts +0 -14
  32. package/dist/commands/lint.d.ts.map +0 -1
  33. package/dist/commands/lint.js +0 -109
  34. package/dist/commands/lint.js.map +0 -1
  35. package/dist/commands/list.d.ts +0 -6
  36. package/dist/commands/list.d.ts.map +0 -1
  37. package/dist/commands/list.js +0 -60
  38. package/dist/commands/list.js.map +0 -1
  39. package/dist/commands/remove.d.ts +0 -16
  40. package/dist/commands/remove.d.ts.map +0 -1
  41. package/dist/commands/remove.js +0 -73
  42. package/dist/commands/remove.js.map +0 -1
  43. package/dist/commands/scan.d.ts +0 -10
  44. package/dist/commands/scan.d.ts.map +0 -1
  45. package/dist/commands/scan.js +0 -60
  46. package/dist/commands/scan.js.map +0 -1
  47. package/dist/commands/update.d.ts +0 -11
  48. package/dist/commands/update.d.ts.map +0 -1
  49. package/dist/commands/update.js +0 -135
  50. package/dist/commands/update.js.map +0 -1
  51. package/dist/context.d.ts +0 -44
  52. package/dist/context.d.ts.map +0 -1
  53. package/dist/context.js +0 -179
  54. package/dist/context.js.map +0 -1
  55. package/dist/ignore.d.ts +0 -21
  56. package/dist/ignore.d.ts.map +0 -1
  57. package/dist/ignore.js +0 -79
  58. package/dist/ignore.js.map +0 -1
  59. package/dist/install.d.ts +0 -10
  60. package/dist/install.d.ts.map +0 -1
  61. package/dist/install.js +0 -54
  62. package/dist/install.js.map +0 -1
  63. package/dist/learn.d.ts +0 -45
  64. package/dist/learn.d.ts.map +0 -1
  65. package/dist/learn.js +0 -146
  66. package/dist/learn.js.map +0 -1
  67. package/dist/registry.d.ts +0 -23
  68. package/dist/registry.d.ts.map +0 -1
  69. package/dist/registry.js +0 -38
  70. package/dist/registry.js.map +0 -1
  71. package/dist/scan.d.ts +0 -3
  72. package/dist/scan.d.ts.map +0 -1
  73. package/dist/scan.js +0 -104
  74. package/dist/scan.js.map +0 -1
  75. package/dist/state.d.ts +0 -31
  76. package/dist/state.d.ts.map +0 -1
  77. package/dist/state.js +0 -39
  78. package/dist/state.js.map +0 -1
  79. package/dist/ui.d.ts +0 -17
  80. package/dist/ui.d.ts.map +0 -1
  81. package/dist/ui.js +0 -35
  82. package/dist/ui.js.map +0 -1
  83. package/node_modules/@skyl/core/dist/audit.d.ts +0 -48
  84. package/node_modules/@skyl/core/dist/audit.d.ts.map +0 -1
  85. package/node_modules/@skyl/core/dist/audit.js +0 -99
  86. package/node_modules/@skyl/core/dist/audit.js.map +0 -1
  87. package/node_modules/@skyl/core/dist/detect.d.ts +0 -15
  88. package/node_modules/@skyl/core/dist/detect.d.ts.map +0 -1
  89. package/node_modules/@skyl/core/dist/detect.js +0 -62
  90. package/node_modules/@skyl/core/dist/detect.js.map +0 -1
  91. package/node_modules/@skyl/core/dist/diff.d.ts +0 -32
  92. package/node_modules/@skyl/core/dist/diff.d.ts.map +0 -1
  93. package/node_modules/@skyl/core/dist/diff.js +0 -119
  94. package/node_modules/@skyl/core/dist/diff.js.map +0 -1
  95. package/node_modules/@skyl/core/dist/errors.d.ts +0 -21
  96. package/node_modules/@skyl/core/dist/errors.d.ts.map +0 -1
  97. package/node_modules/@skyl/core/dist/errors.js +0 -25
  98. package/node_modules/@skyl/core/dist/errors.js.map +0 -1
  99. package/node_modules/@skyl/core/dist/index.d.ts +0 -14
  100. package/node_modules/@skyl/core/dist/index.d.ts.map +0 -1
  101. package/node_modules/@skyl/core/dist/index.js +0 -14
  102. package/node_modules/@skyl/core/dist/index.js.map +0 -1
  103. package/node_modules/@skyl/core/dist/lint.d.ts +0 -28
  104. package/node_modules/@skyl/core/dist/lint.d.ts.map +0 -1
  105. package/node_modules/@skyl/core/dist/lint.js +0 -169
  106. package/node_modules/@skyl/core/dist/lint.js.map +0 -1
  107. package/node_modules/@skyl/core/dist/lockfile.d.ts +0 -37
  108. package/node_modules/@skyl/core/dist/lockfile.d.ts.map +0 -1
  109. package/node_modules/@skyl/core/dist/lockfile.js +0 -47
  110. package/node_modules/@skyl/core/dist/lockfile.js.map +0 -1
  111. package/node_modules/@skyl/core/dist/match.d.ts +0 -19
  112. package/node_modules/@skyl/core/dist/match.d.ts.map +0 -1
  113. package/node_modules/@skyl/core/dist/match.js +0 -72
  114. package/node_modules/@skyl/core/dist/match.js.map +0 -1
  115. package/node_modules/@skyl/core/dist/names.d.ts +0 -11
  116. package/node_modules/@skyl/core/dist/names.d.ts.map +0 -1
  117. package/node_modules/@skyl/core/dist/names.js +0 -31
  118. package/node_modules/@skyl/core/dist/names.js.map +0 -1
  119. package/node_modules/@skyl/core/dist/parse.d.ts +0 -22
  120. package/node_modules/@skyl/core/dist/parse.d.ts.map +0 -1
  121. package/node_modules/@skyl/core/dist/parse.js +0 -163
  122. package/node_modules/@skyl/core/dist/parse.js.map +0 -1
  123. package/node_modules/@skyl/core/dist/resolve.d.ts +0 -12
  124. package/node_modules/@skyl/core/dist/resolve.d.ts.map +0 -1
  125. package/node_modules/@skyl/core/dist/resolve.js +0 -40
  126. package/node_modules/@skyl/core/dist/resolve.js.map +0 -1
  127. package/node_modules/@skyl/core/dist/source.d.ts +0 -35
  128. package/node_modules/@skyl/core/dist/source.d.ts.map +0 -1
  129. package/node_modules/@skyl/core/dist/source.js +0 -85
  130. package/node_modules/@skyl/core/dist/source.js.map +0 -1
  131. package/node_modules/@skyl/core/dist/targets.d.ts +0 -20
  132. package/node_modules/@skyl/core/dist/targets.d.ts.map +0 -1
  133. package/node_modules/@skyl/core/dist/targets.js +0 -24
  134. package/node_modules/@skyl/core/dist/targets.js.map +0 -1
  135. package/node_modules/@skyl/core/dist/types.d.ts +0 -51
  136. package/node_modules/@skyl/core/dist/types.d.ts.map +0 -1
  137. package/node_modules/@skyl/core/dist/types.js +0 -2
  138. package/node_modules/@skyl/core/dist/types.js.map +0 -1
  139. package/node_modules/@skyl/core/package.json +0 -20
package/dist/bin.js CHANGED
@@ -1,4 +1,2267 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "./cli.js";
2
+
3
+ // ../core/src/types.ts
4
+ var AXES = ["core", "language", "framework", "service", "topic"];
5
+
6
+ // ../core/src/errors.ts
7
+ var SkylError = class extends Error {
8
+ detail;
9
+ constructor(message, detail) {
10
+ super(detail ? `${message}
11
+ ${detail}` : message);
12
+ this.name = new.target.name;
13
+ this.detail = detail;
14
+ }
15
+ };
16
+ var SkillFormatError = class extends SkylError {
17
+ };
18
+ var ResolutionError = class extends SkylError {
19
+ };
20
+ var SourceError = class extends SkylError {
21
+ };
22
+
23
+ // ../core/src/parse.ts
24
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
25
+ function stripQuotes(v) {
26
+ const t = v.trim();
27
+ if (t.length >= 2 && (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'"))) {
28
+ return t.slice(1, -1);
29
+ }
30
+ return t;
31
+ }
32
+ function inlineList(v) {
33
+ const inner = v.trim().slice(1, -1).trim();
34
+ if (inner === "") return [];
35
+ return inner.split(",").map(stripQuotes).filter((s) => s !== "");
36
+ }
37
+ function parseFrontmatter(text) {
38
+ const m = FRONTMATTER.exec(text);
39
+ if (!m) throw new SkillFormatError("no frontmatter", "a skill starts with a --- delimited block");
40
+ const out2 = {};
41
+ const lines = m[1].split("\n");
42
+ let currentKey = null;
43
+ let currentSub = null;
44
+ for (const raw of lines) {
45
+ if (raw.trim() === "" || raw.trimStart().startsWith("#")) continue;
46
+ const top = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(raw);
47
+ if (top && !/^\s/.test(raw)) {
48
+ const [, key, rest] = top;
49
+ currentKey = key;
50
+ currentSub = null;
51
+ const value = rest.trim();
52
+ if (value === "") out2[key] = {};
53
+ else if (value.startsWith("[")) out2[key] = inlineList(value);
54
+ else out2[key] = stripQuotes(value);
55
+ continue;
56
+ }
57
+ const nested = /^\s+([A-Za-z_][\w-]*):\s*(.*)$/.exec(raw);
58
+ if (nested && currentKey) {
59
+ const [, sub, rest] = nested;
60
+ const bucket = out2[currentKey] ??= {};
61
+ if (typeof bucket !== "object" || Array.isArray(bucket)) {
62
+ throw new SkillFormatError(`\`${currentKey}\` has both a value and nested keys`);
63
+ }
64
+ const value = rest.trim();
65
+ bucket[sub] = value.startsWith("[") ? inlineList(value) : value === "" ? [] : [stripQuotes(value)];
66
+ currentSub = sub;
67
+ continue;
68
+ }
69
+ const item = /^\s*-\s+(.*)$/.exec(raw);
70
+ if (item && currentKey) {
71
+ const value = stripQuotes(item[1]);
72
+ const bucket = out2[currentKey];
73
+ if (Array.isArray(bucket)) bucket.push(value);
74
+ else if (currentSub && bucket && typeof bucket === "object") {
75
+ (bucket[currentSub] ??= []).push(value);
76
+ } else out2[currentKey] = [value];
77
+ continue;
78
+ }
79
+ }
80
+ return out2;
81
+ }
82
+ function body(text) {
83
+ const m = FRONTMATTER.exec(text);
84
+ return m ? text.slice(m[0].length) : text;
85
+ }
86
+ function sections(markdown) {
87
+ const out2 = /* @__PURE__ */ new Map();
88
+ const parts = markdown.split(/^## (.+)$/m);
89
+ for (let i = 1; i < parts.length; i += 2) {
90
+ out2.set(parts[i].trim(), (parts[i + 1] ?? "").trim());
91
+ }
92
+ return out2;
93
+ }
94
+ var RULE = /^- \*\*([A-Z0-9]+-\d+)\*\*\s+`(must|should)`/gm;
95
+ function rules(markdown) {
96
+ const out2 = [];
97
+ for (const m of markdown.matchAll(RULE)) {
98
+ out2.push({ id: m[1], priority: m[2] });
99
+ }
100
+ return out2;
101
+ }
102
+ function requireString(fm, key) {
103
+ const v = fm[key];
104
+ if (typeof v !== "string" || v === "") {
105
+ throw new SkillFormatError(`frontmatter is missing \`${key}\``);
106
+ }
107
+ return v;
108
+ }
109
+ function list(fm, key) {
110
+ const v = fm[key];
111
+ if (v === void 0) return [];
112
+ if (Array.isArray(v)) return v;
113
+ throw new SkillFormatError(`\`${key}\` must be a list`);
114
+ }
115
+ function parseSkill(text) {
116
+ const fm = parseFrontmatter(text);
117
+ const md = body(text);
118
+ const name = requireString(fm, "name");
119
+ const slash = name.indexOf("/");
120
+ if (slash <= 0 || slash === name.length - 1) {
121
+ throw new SkillFormatError(`name \`${name}\` is not \`family/skill\``);
122
+ }
123
+ const axis = requireString(fm, "axis");
124
+ if (!AXES.includes(axis)) {
125
+ throw new SkillFormatError(`axis \`${axis}\` is not one of ${AXES.join(", ")}`);
126
+ }
127
+ const version = requireString(fm, "version");
128
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
129
+ throw new SkillFormatError(`version \`${version}\` is not semver`);
130
+ }
131
+ const agentSections = list(fm, "agent_sections");
132
+ if (agentSections.length === 0) {
133
+ throw new SkillFormatError("frontmatter is missing `agent_sections`", "nothing would be installed");
134
+ }
135
+ const detectRaw = fm["detect"];
136
+ const detect2 = detectRaw && typeof detectRaw === "object" && !Array.isArray(detectRaw) ? detectRaw : {};
137
+ const all = sections(md);
138
+ const installable = agentSections.map((s) => {
139
+ const heading = s.charAt(0).toUpperCase() + s.slice(1);
140
+ const found = all.get(heading) ?? all.get(s);
141
+ if (found === void 0) {
142
+ throw new SkillFormatError(
143
+ `\`agent_sections\` names \`${s}\` but the file has no \`## ${heading}\``
144
+ );
145
+ }
146
+ return `## ${heading}
147
+
148
+ ${found}`;
149
+ }).join("\n\n");
150
+ return {
151
+ name,
152
+ family: name.slice(0, slash),
153
+ skill: name.slice(slash + 1),
154
+ axis,
155
+ version,
156
+ requires: list(fm, "requires"),
157
+ agentSections,
158
+ retired: list(fm, "retired"),
159
+ detect: detect2,
160
+ authors: list(fm, "authors"),
161
+ rules: rules(md),
162
+ installable,
163
+ installableWords: installable.split(/\s+/).filter(Boolean).length,
164
+ raw: text
165
+ };
166
+ }
167
+
168
+ // ../core/src/match.ts
169
+ function coordinate(value) {
170
+ const parts = value.split(":");
171
+ return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : value;
172
+ }
173
+ function globToRegExp(glob) {
174
+ let out2 = "";
175
+ for (let i = 0; i < glob.length; i += 1) {
176
+ const c = glob[i];
177
+ if (c === "*") {
178
+ if (glob[i + 1] === "*") {
179
+ if (glob[i + 2] === "/") {
180
+ out2 += "(?:.*/)?";
181
+ i += 2;
182
+ } else {
183
+ out2 += ".*";
184
+ i += 1;
185
+ }
186
+ } else {
187
+ out2 += "[^/]*";
188
+ }
189
+ } else if (c === "?") out2 += "[^/]";
190
+ else if (".+^${}()|[]\\".includes(c)) out2 += `\\${c}`;
191
+ else out2 += c;
192
+ }
193
+ return new RegExp(`^${out2}$`);
194
+ }
195
+ var globCache = /* @__PURE__ */ new Map();
196
+ function globMatch(pattern, path) {
197
+ let re = globCache.get(pattern);
198
+ if (!re) {
199
+ re = globToRegExp(pattern);
200
+ globCache.set(pattern, re);
201
+ }
202
+ const normalised = path.replace(/^\.\//, "").replace(/\\/g, "/");
203
+ return re.test(normalised) || re.test(`./${normalised}`.slice(2));
204
+ }
205
+ function strategyFor(key) {
206
+ if (key.endsWith("dependency") || key.endsWith("dependencies")) return "coordinate";
207
+ if (key === "file" || key.endsWith("_file") || key.endsWith("path")) return "glob";
208
+ return "exact";
209
+ }
210
+ function matches(key, wanted, signal) {
211
+ switch (strategyFor(key)) {
212
+ case "coordinate":
213
+ return coordinate(signal) === coordinate(wanted);
214
+ case "glob":
215
+ return globMatch(wanted, signal);
216
+ case "exact":
217
+ return signal === wanted || signal.includes(wanted);
218
+ }
219
+ }
220
+
221
+ // ../core/src/detect.ts
222
+ function hits(detect2, signals) {
223
+ const found = [];
224
+ for (const [key, wanted] of Object.entries(detect2)) {
225
+ const present = signals[key];
226
+ if (!present || present.length === 0) continue;
227
+ for (const w of wanted) {
228
+ if (present.some((p) => matches(key, w, p))) found.push({ key, value: w });
229
+ }
230
+ }
231
+ return found;
232
+ }
233
+ function detect(skills, signals) {
234
+ const matched = [];
235
+ const families = /* @__PURE__ */ new Set();
236
+ for (const s of skills) {
237
+ if (Object.keys(s.detect).length === 0) continue;
238
+ const on2 = hits(s.detect, signals);
239
+ if (on2.length > 0) {
240
+ matched.push({ skill: s.name, on: on2 });
241
+ families.add(s.family);
242
+ }
243
+ }
244
+ for (const s of skills) {
245
+ if (s.axis !== "core" || !families.has(s.family)) continue;
246
+ if (matched.some((m) => m.skill === s.name)) continue;
247
+ matched.push({ skill: s.name, on: [{ key: "family", value: s.family }] });
248
+ }
249
+ return matched.sort((a, b) => a.skill.localeCompare(b.skill));
250
+ }
251
+ function unmatched(skills, matched) {
252
+ const taken = new Set(matched.map((m) => m.skill));
253
+ const families = new Set(matched.map((m) => m.skill.split("/")[0]));
254
+ return skills.filter((s) => !taken.has(s.name) && families.has(s.family)).sort((a, b) => a.name.localeCompare(b.name));
255
+ }
256
+
257
+ // ../core/src/resolve.ts
258
+ function resolve(skills, requested) {
259
+ const byName = new Map(skills.map((s) => [s.name, s]));
260
+ const ordered = [];
261
+ const done = /* @__PURE__ */ new Set();
262
+ const path = [];
263
+ const visit = (name, from) => {
264
+ if (done.has(name)) return;
265
+ if (path.includes(name)) {
266
+ throw new ResolutionError(
267
+ "skills require each other in a cycle",
268
+ [...path.slice(path.indexOf(name)), name].join(" -> ")
269
+ );
270
+ }
271
+ const skill = byName.get(name);
272
+ if (!skill) {
273
+ throw new ResolutionError(
274
+ `unknown skill \`${name}\``,
275
+ from ? `required by \`${from}\`` : "not in the index"
276
+ );
277
+ }
278
+ path.push(name);
279
+ for (const dep of skill.requires) visit(dep, name);
280
+ path.pop();
281
+ done.add(name);
282
+ ordered.push(skill);
283
+ };
284
+ for (const name of requested) visit(name);
285
+ return ordered;
286
+ }
287
+ function implied(resolved, requested) {
288
+ const asked = new Set(requested);
289
+ return resolved.filter((s) => !asked.has(s.name));
290
+ }
291
+
292
+ // ../core/src/source.ts
293
+ function directorySource(root, label = "directory") {
294
+ return {
295
+ name: `${label} (${root})`,
296
+ async load() {
297
+ const { readdir: readdir4, readFile: readFile9 } = await import("node:fs/promises");
298
+ const { join: join9 } = await import("node:path");
299
+ const out2 = [];
300
+ let families;
301
+ try {
302
+ families = (await readdir4(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
303
+ } catch (cause) {
304
+ throw new SourceError(`cannot read ${root}`, String(cause));
305
+ }
306
+ for (const family of families) {
307
+ const dir = join9(root, family);
308
+ const entries = (await readdir4(dir, { withFileTypes: true })).filter((e) => e.isDirectory());
309
+ for (const entry of entries) {
310
+ const file = join9(dir, entry.name, "SKILL.md");
311
+ try {
312
+ out2.push(parseSkill(await readFile9(file, "utf8")));
313
+ } catch (cause) {
314
+ if (cause.code === "ENOENT") continue;
315
+ throw new SourceError(`cannot parse ${file}`, cause.message);
316
+ }
317
+ }
318
+ }
319
+ return out2;
320
+ }
321
+ };
322
+ }
323
+ function bundleSource(load, label) {
324
+ return {
325
+ name: label,
326
+ async load() {
327
+ const bundle = await load();
328
+ return Object.entries(bundle.skills).map(([name, raw]) => {
329
+ try {
330
+ return parseSkill(raw);
331
+ } catch (cause) {
332
+ throw new SourceError(`cannot parse \`${name}\` from ${label}`, cause.message);
333
+ }
334
+ });
335
+ }
336
+ };
337
+ }
338
+ function httpBundleSource(url, label = "remote") {
339
+ return bundleSource(async () => {
340
+ const res = await fetch(url, { headers: { accept: "application/json" } });
341
+ if (!res.ok) throw new SourceError(`${label} returned ${res.status}`, url);
342
+ return await res.json();
343
+ }, `${label} (${url})`);
344
+ }
345
+ async function firstAvailable(sources2) {
346
+ const failures = [];
347
+ for (const source of sources2) {
348
+ try {
349
+ const skills = await source.load();
350
+ if (skills.length > 0) return { skills, from: source };
351
+ failures.push(`${source.name}: empty`);
352
+ } catch (cause) {
353
+ failures.push(`${source.name}: ${cause.message}`);
354
+ }
355
+ }
356
+ throw new SourceError("no source could supply the registry", failures.join("\n "));
357
+ }
358
+
359
+ // ../core/src/targets.ts
360
+ var flat = (name) => name.replace("/", "-");
361
+ var TARGETS = [
362
+ {
363
+ id: "claude",
364
+ dir: ".claude/skills",
365
+ fileFor: (name) => `${flat(name)}/SKILL.md`
366
+ },
367
+ {
368
+ id: "cursor",
369
+ dir: ".cursor/rules",
370
+ fileFor: (name) => `${flat(name)}.mdc`,
371
+ // Cursor decides when to apply a rule from its own frontmatter, so a skill that
372
+ // arrives without one is either always on or never on, depending on the version.
373
+ header: (name) => `---
374
+ description: ${name}
375
+ alwaysApply: false
376
+ ---
377
+
378
+ `
379
+ },
380
+ { id: "windsurf", dir: ".windsurf/rules", fileFor: (name) => `${flat(name)}.md` },
381
+ { id: "continue", dir: ".continue/rules", fileFor: (name) => `${flat(name)}.md` },
382
+ { id: "agents", dir: ".agents/skills", fileFor: (name) => `${flat(name)}.md` }
383
+ ];
384
+ var DEFAULT_TARGET = "claude";
385
+ function targetById(id) {
386
+ return TARGETS.find((t) => t.id === id);
387
+ }
388
+
389
+ // ../core/src/lockfile.ts
390
+ import { createHash } from "node:crypto";
391
+ var LOCKFILE_NAME = "skyl.lock";
392
+ function hashContent(text) {
393
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
394
+ }
395
+ function parseLockfile(text) {
396
+ const data = JSON.parse(text);
397
+ if (data.lockfileVersion !== 1) {
398
+ throw new Error(`unsupported lockfile version ${String(data.lockfileVersion)}`);
399
+ }
400
+ return data;
401
+ }
402
+ function formatLockfile(lock) {
403
+ const skills = {};
404
+ for (const name of Object.keys(lock.skills).sort()) skills[name] = lock.skills[name];
405
+ const ordered = { lockfileVersion: lock.lockfileVersion, target: lock.target, ...lock.source ? { source: lock.source } : {}, skills };
406
+ return `${JSON.stringify(ordered, null, 2)}
407
+ `;
408
+ }
409
+ function entryFor(skill, installed, reason) {
410
+ return { version: skill.version, hash: hashContent(installed), reason };
411
+ }
412
+ function drift(entry, onDisk, upstream) {
413
+ if (!entry || onDisk === void 0) return "missing";
414
+ const localChanged = hashContent(onDisk) !== entry.hash;
415
+ const upstreamChanged = upstream !== void 0 && upstream.version !== entry.version;
416
+ if (localChanged && upstreamChanged) return "both";
417
+ if (localChanged) return "local-edit";
418
+ if (upstreamChanged) return "upstream-change";
419
+ return "unchanged";
420
+ }
421
+
422
+ // ../core/src/names.ts
423
+ function normalise(skills, requested) {
424
+ const known = new Set(skills.map((s) => s.name));
425
+ const families = new Set(requested.filter((r) => r.includes("/")).map((r) => r.split("/")[0]));
426
+ return requested.map((raw) => {
427
+ const name = raw.trim();
428
+ if (name.includes("/")) {
429
+ if (!known.has(name)) throw new ResolutionError(`unknown skill \`${name}\``);
430
+ return name;
431
+ }
432
+ const inNamedFamily = skills.filter((s) => families.has(s.family) && s.skill === name);
433
+ if (inNamedFamily.length === 1) return inNamedFamily[0].name;
434
+ const anywhere = skills.filter((s) => s.skill === name);
435
+ if (anywhere.length === 1) return anywhere[0].name;
436
+ if (anywhere.length === 0) throw new ResolutionError(`unknown skill \`${name}\``);
437
+ throw new ResolutionError(
438
+ `\`${name}\` is ambiguous`,
439
+ `it exists in ${anywhere.map((s) => s.family).join(", ")}. Name the family.`
440
+ );
441
+ });
442
+ }
443
+
444
+ // ../core/src/diff.ts
445
+ var MAX_CELLS = 4e6;
446
+ function diffLines(before, after) {
447
+ const a = before.split("\n");
448
+ const b = after.split("\n");
449
+ if (a.length * b.length > MAX_CELLS) {
450
+ return [
451
+ ...a.map((text) => ({ kind: "-", text })),
452
+ ...b.map((text) => ({ kind: "+", text }))
453
+ ];
454
+ }
455
+ let head = 0;
456
+ while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
457
+ let tail = 0;
458
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail += 1;
459
+ const midA = a.slice(head, a.length - tail);
460
+ const midB = b.slice(head, b.length - tail);
461
+ const table = Array.from({ length: midA.length + 1 }, () => new Array(midB.length + 1).fill(0));
462
+ for (let i2 = midA.length - 1; i2 >= 0; i2 -= 1) {
463
+ for (let j2 = midB.length - 1; j2 >= 0; j2 -= 1) {
464
+ table[i2][j2] = midA[i2] === midB[j2] ? table[i2 + 1][j2 + 1] + 1 : Math.max(table[i2 + 1][j2], table[i2][j2 + 1]);
465
+ }
466
+ }
467
+ const ops = a.slice(0, head).map((text) => ({ kind: " ", text }));
468
+ let i = 0;
469
+ let j = 0;
470
+ while (i < midA.length && j < midB.length) {
471
+ if (midA[i] === midB[j]) {
472
+ ops.push({ kind: " ", text: midA[i] });
473
+ i += 1;
474
+ j += 1;
475
+ } else if (table[i + 1][j] >= table[i][j + 1]) {
476
+ ops.push({ kind: "-", text: midA[i] });
477
+ i += 1;
478
+ } else {
479
+ ops.push({ kind: "+", text: midB[j] });
480
+ j += 1;
481
+ }
482
+ }
483
+ while (i < midA.length) {
484
+ ops.push({ kind: "-", text: midA[i] });
485
+ i += 1;
486
+ }
487
+ while (j < midB.length) {
488
+ ops.push({ kind: "+", text: midB[j] });
489
+ j += 1;
490
+ }
491
+ for (const text of a.slice(a.length - tail)) ops.push({ kind: " ", text });
492
+ return ops;
493
+ }
494
+ function hunks(ops, context2 = 3) {
495
+ const changed = ops.map((o) => o.kind !== " ");
496
+ if (!changed.some(Boolean)) return [];
497
+ const keep = new Array(ops.length).fill(false);
498
+ for (let i = 0; i < ops.length; i += 1) {
499
+ if (!changed[i]) continue;
500
+ for (let j = Math.max(0, i - context2); j <= Math.min(ops.length - 1, i + context2); j += 1) {
501
+ keep[j] = true;
502
+ }
503
+ }
504
+ const out2 = [];
505
+ let beforeLine = 1;
506
+ let afterLine = 1;
507
+ let current;
508
+ for (let i = 0; i < ops.length; i += 1) {
509
+ const op = ops[i];
510
+ if (keep[i]) {
511
+ current ??= { lines: [], beforeStart: beforeLine, afterStart: afterLine };
512
+ current.lines.push(op);
513
+ } else if (current) {
514
+ out2.push(close(current));
515
+ current = void 0;
516
+ }
517
+ if (op.kind !== "+") beforeLine += 1;
518
+ if (op.kind !== "-") afterLine += 1;
519
+ }
520
+ if (current) out2.push(close(current));
521
+ return out2;
522
+ }
523
+ function close(c) {
524
+ return {
525
+ beforeStart: c.beforeStart,
526
+ beforeCount: c.lines.filter((l) => l.kind !== "+").length,
527
+ afterStart: c.afterStart,
528
+ afterCount: c.lines.filter((l) => l.kind !== "-").length,
529
+ lines: c.lines
530
+ };
531
+ }
532
+ function stat(ops) {
533
+ return {
534
+ added: ops.filter((o) => o.kind === "+").length,
535
+ removed: ops.filter((o) => o.kind === "-").length
536
+ };
537
+ }
538
+
539
+ // ../core/src/lint.ts
540
+ var HUMAN_SECTIONS = ["Why", "Pitfalls", "Provenance"];
541
+ var VAGUE = [
542
+ "appropriately",
543
+ "properly",
544
+ "as needed",
545
+ "as appropriate",
546
+ "when appropriate",
547
+ "best practice",
548
+ "best practices",
549
+ "if necessary",
550
+ "where possible",
551
+ "reasonable",
552
+ "clean code",
553
+ "well-structured",
554
+ "avoid unnecessary"
555
+ ];
556
+ var lineOf = (text, needle) => {
557
+ const at = text.indexOf(needle);
558
+ return at < 0 ? void 0 : text.slice(0, at).split("\n").length;
559
+ };
560
+ function lintSkill(text, path) {
561
+ const found = [];
562
+ let skill;
563
+ try {
564
+ skill = parseSkill(text);
565
+ } catch (cause) {
566
+ const e = cause;
567
+ return [{ level: "error", message: e.message.split("\n")[0], ...e.detail ? { detail: e.detail } : {} }];
568
+ }
569
+ if (path) {
570
+ const parts = path.split(/[\\/]/).filter(Boolean);
571
+ const dir = parts.slice(-3, -1).join("/");
572
+ if (dir !== "" && dir !== skill.name) {
573
+ found.push({
574
+ level: "error",
575
+ message: `name \`${skill.name}\` does not match its path`,
576
+ detail: `the file lives in ${dir}/, so it can never be found by name`
577
+ });
578
+ }
579
+ }
580
+ const md = body(text);
581
+ const all = sections(md);
582
+ const offset = text.slice(0, text.length - md.length).split("\n").length - 1;
583
+ for (const s of HUMAN_SECTIONS) {
584
+ if (!all.has(s)) {
585
+ found.push({ level: "warn", message: `no \`## ${s}\` section`, detail: "required for a shipped skill" });
586
+ }
587
+ }
588
+ if (skill.rules.length === 0) {
589
+ found.push({
590
+ level: "error",
591
+ message: "no rules found",
592
+ detail: "a rule is `- **PREFIX-N** `must`: instruction`"
593
+ });
594
+ }
595
+ const seen = /* @__PURE__ */ new Set();
596
+ for (const rule of skill.rules) {
597
+ if (seen.has(rule.id)) {
598
+ found.push({ level: "error", message: `duplicate rule id \`${rule.id}\``, ...lineFor(md, rule.id, offset) });
599
+ }
600
+ seen.add(rule.id);
601
+ }
602
+ for (const id of skill.retired) {
603
+ if (!/^[A-Z0-9]+-\d+$/.test(id)) {
604
+ found.push({
605
+ level: "error",
606
+ message: `retired entry \`${id}\` is not a rule id`,
607
+ detail: "use the bare id, so reuse of it stays detectable"
608
+ });
609
+ continue;
610
+ }
611
+ if (seen.has(id)) {
612
+ found.push({
613
+ level: "error",
614
+ message: `retired id \`${id}\` is in use again`,
615
+ detail: "ids are never reused: published evidence points at the old one",
616
+ ...lineFor(md, id, offset)
617
+ });
618
+ }
619
+ }
620
+ for (const block of md.split(/^- \*\*/m).slice(1)) {
621
+ const id = /^([A-Z0-9]+-\d+)/.exec(block)?.[1];
622
+ if (!id) continue;
623
+ const at = lineFor(md, id, offset);
624
+ if (!block.includes("*Why:*")) {
625
+ found.push({ level: "warn", message: `${id}: no \`*Why:*\``, detail: "the reader cannot recognise the bug in the wild", ...at });
626
+ }
627
+ if (!block.includes("*Not when:*")) {
628
+ found.push({ level: "warn", message: `${id}: no \`*Not when:*\``, detail: "a rule with no stated boundary is applied everywhere", ...at });
629
+ }
630
+ const instruction = block.split("*Why:*")[0].toLowerCase();
631
+ for (const word of VAGUE) {
632
+ if (instruction.includes(word)) {
633
+ found.push({ level: "warn", message: `${id}: "${word}" is not decidable`, detail: "a reader cannot hold it against a file and answer yes or no", ...at });
634
+ break;
635
+ }
636
+ }
637
+ }
638
+ const dash = text.indexOf("\u2014");
639
+ if (dash >= 0) {
640
+ found.push({
641
+ level: "warn",
642
+ message: "em dash in the text",
643
+ detail: "use a comma, a full stop, or a colon",
644
+ line: text.slice(0, dash).split("\n").length
645
+ });
646
+ }
647
+ if (Object.keys(skill.detect).length === 0) {
648
+ found.push({
649
+ level: "warn",
650
+ message: "no `detect` block",
651
+ detail: "nothing will ever propose this skill, it can only be installed by name"
652
+ });
653
+ }
654
+ return found;
655
+ }
656
+ function lineFor(md, id, offset) {
657
+ const line = lineOf(md, `**${id}**`);
658
+ return line === void 0 ? {} : { line: line + offset };
659
+ }
660
+ var XREF = /`([a-z0-9-]+\/[a-z0-9-]+)\s+([A-Z0-9]+-\d+)`/g;
661
+ function lintRegistry(skills) {
662
+ const out2 = /* @__PURE__ */ new Map();
663
+ const byName = new Map(skills.map((s) => [s.name, s]));
664
+ const ruleIds = new Map(
665
+ skills.map((s) => [s.name, /* @__PURE__ */ new Set([...s.rules.map((r) => r.id), ...s.retired])])
666
+ );
667
+ for (const skill of skills) {
668
+ const found = [];
669
+ for (const dep of skill.requires) {
670
+ if (!byName.has(dep)) {
671
+ found.push({ level: "error", message: `requires \`${dep}\`, which is not in the registry` });
672
+ }
673
+ }
674
+ for (const m of skill.raw.matchAll(XREF)) {
675
+ const [, target, rule] = m;
676
+ if (target === skill.name) continue;
677
+ const ids = ruleIds.get(target);
678
+ if (!ids) {
679
+ found.push({ level: "error", message: `cites \`${target} ${rule}\`, and \`${target}\` is not in the registry` });
680
+ } else if (!ids.has(rule)) {
681
+ found.push({ level: "error", message: `cites \`${target} ${rule}\`, which does not exist` });
682
+ }
683
+ }
684
+ if (found.length > 0) out2.set(skill.name, found);
685
+ }
686
+ return out2;
687
+ }
688
+
689
+ // ../core/src/audit.ts
690
+ var STOP = /* @__PURE__ */ new Set([
691
+ "the",
692
+ "a",
693
+ "an",
694
+ "and",
695
+ "or",
696
+ "but",
697
+ "if",
698
+ "then",
699
+ "is",
700
+ "are",
701
+ "was",
702
+ "be",
703
+ "been",
704
+ "to",
705
+ "of",
706
+ "in",
707
+ "on",
708
+ "for",
709
+ "with",
710
+ "as",
711
+ "at",
712
+ "by",
713
+ "from",
714
+ "that",
715
+ "this",
716
+ "it",
717
+ "its",
718
+ "not",
719
+ "no",
720
+ "do",
721
+ "does",
722
+ "use",
723
+ "using",
724
+ "used",
725
+ "when",
726
+ "where",
727
+ "which",
728
+ "you",
729
+ "your",
730
+ "we",
731
+ "our",
732
+ "can",
733
+ "will",
734
+ "should",
735
+ "must",
736
+ "never",
737
+ "always",
738
+ "all",
739
+ "any",
740
+ "each",
741
+ "per",
742
+ "into",
743
+ "out",
744
+ "up",
745
+ "has",
746
+ "have",
747
+ "they",
748
+ "them",
749
+ "their",
750
+ "one",
751
+ "two",
752
+ "more",
753
+ "than",
754
+ "so",
755
+ "only",
756
+ "also"
757
+ ]);
758
+ var words = (text) => text.toLowerCase().match(/[a-z][a-z0-9_.]{2,}/g)?.filter((w) => !STOP.has(w)) ?? [];
759
+ var IMPERATIVE = /\b(must|never|always|do not|don't|avoid|prefer|require[sd]?|should|use\b)/i;
760
+ function distinctive(skills) {
761
+ const perSkill = /* @__PURE__ */ new Map();
762
+ const documents = /* @__PURE__ */ new Map();
763
+ for (const skill of skills) {
764
+ const set = new Set(words(skill.installable));
765
+ perSkill.set(skill.name, set);
766
+ for (const term of set) documents.set(term, (documents.get(term) ?? 0) + 1);
767
+ }
768
+ const limit = Math.max(1, Math.floor(skills.length / 4));
769
+ const out2 = /* @__PURE__ */ new Map();
770
+ for (const [name, set] of perSkill) {
771
+ out2.set(name, new Set([...set].filter((t) => (documents.get(t) ?? 0) <= limit)));
772
+ }
773
+ return out2;
774
+ }
775
+ var SECRET = [
776
+ { what: "private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
777
+ { what: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/ },
778
+ { what: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
779
+ { what: "GitHub token", re: /\bgh[pousr]_[0-9A-Za-z]{36,}\b/ },
780
+ { what: "Slack token", re: /\bxox[abprs]-[0-9A-Za-z-]{10,}\b/ },
781
+ { what: "bearer token", re: /\b(?:bearer|authorization)\s*[:=]\s*["']?[A-Za-z0-9._-]{20,}/i },
782
+ { what: "assignment that looks like a secret", re: /\b(?:api[_-]?key|secret|password|passwd|token|credential)s?\s*[:=]\s*["'][^"'\s]{8,}["']/i }
783
+ ];
784
+ var EXPECTS = [
785
+ { what: "placement, where new code goes", re: /\b(place|placement|goes in|belongs in|live[s]? in|directory structure|module layout)\b/i },
786
+ { what: "build, test or lint commands", re: /\b(\.\/gradlew|npm run|pnpm |yarn |make |pytest|cargo )/ },
787
+ { what: "entry points or flow names", re: /\b(entry point|starts (at|in)|flow|screen|endpoint)\b/i }
788
+ ];
789
+ function scanSecrets(text) {
790
+ const out2 = [];
791
+ text.split("\n").forEach((line, i) => {
792
+ for (const s of SECRET) {
793
+ if (s.re.test(line)) {
794
+ out2.push({ line: i + 1, what: s.what });
795
+ break;
796
+ }
797
+ }
798
+ });
799
+ return out2;
800
+ }
801
+ function audit(text, skills) {
802
+ const secrets = scanSecrets(text);
803
+ const vocab = distinctive(skills);
804
+ const found = sections(text);
805
+ const parts = found.size > 0 ? [...found] : [["(whole file)", text]];
806
+ const reports = parts.map(([heading, content]) => {
807
+ const terms = words(content);
808
+ const unique = new Set(terms);
809
+ const overlaps = [];
810
+ for (const [name, set] of vocab) {
811
+ const hit = [...unique].filter((t) => set.has(t));
812
+ if (hit.length < 4) continue;
813
+ const share = hit.length / Math.max(unique.size, 1);
814
+ if (share < 0.04) continue;
815
+ overlaps.push({ skill: name, share, terms: hit.slice(0, 8) });
816
+ }
817
+ overlaps.sort((a, b) => b.share - a.share);
818
+ return {
819
+ heading,
820
+ words: content.split(/\s+/).filter(Boolean).length,
821
+ overlaps: overlaps.slice(0, 3),
822
+ noInstruction: !IMPERATIVE.test(content)
823
+ };
824
+ });
825
+ return {
826
+ words: text.split(/\s+/).filter(Boolean).length,
827
+ sections: reports,
828
+ missing: EXPECTS.filter((e) => !e.re.test(text)).map((e) => e.what),
829
+ secrets
830
+ };
831
+ }
832
+
833
+ // src/registry.ts
834
+ import { existsSync } from "node:fs";
835
+ import { readFile } from "node:fs/promises";
836
+ import { dirname, join, resolve as resolvePath } from "node:path";
837
+ import { fileURLToPath } from "node:url";
838
+ var HERE = dirname(fileURLToPath(import.meta.url));
839
+ var REMOTE = "https://skyl.dev/registry.json";
840
+ function sources(opts) {
841
+ const list3 = [];
842
+ if (opts.dir) list3.push(directorySource(resolvePath(opts.dir), "local"));
843
+ const bundlePath = join(HERE, "..", "bundle.json");
844
+ const bundled = bundleSource(
845
+ async () => JSON.parse(await readFile(bundlePath, "utf8")),
846
+ "bundled"
847
+ );
848
+ const remote = httpBundleSource(REMOTE, "skyl.dev");
849
+ if (opts.refresh) list3.push(remote, bundled);
850
+ else if (existsSync(bundlePath)) list3.push(bundled, remote);
851
+ else list3.push(remote);
852
+ return list3;
853
+ }
854
+ async function loadRegistry(opts) {
855
+ const { skills, from } = await firstAvailable(sources(opts));
856
+ return { skills, from: from.name };
857
+ }
858
+
859
+ // src/install.ts
860
+ import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
861
+ import { dirname as dirname2, join as join2 } from "node:path";
862
+ async function readLockfile(root) {
863
+ try {
864
+ return parseLockfile(await readFile2(join2(root, LOCKFILE_NAME), "utf8"));
865
+ } catch {
866
+ return void 0;
867
+ }
868
+ }
869
+ async function writeLockfile(root, lock) {
870
+ await writeFile(join2(root, LOCKFILE_NAME), formatLockfile(lock), "utf8");
871
+ }
872
+ function renderFor(target, skill) {
873
+ const header = target.header?.(skill.name, skill.version) ?? "";
874
+ return `${header}${skill.installable}
875
+ `;
876
+ }
877
+ function pathFor(root, target, skill) {
878
+ return join2(root, target.dir, target.fileFor(skill.name));
879
+ }
880
+ async function readInstalled(root, target, skill) {
881
+ try {
882
+ return await readFile2(pathFor(root, target, skill), "utf8");
883
+ } catch {
884
+ return void 0;
885
+ }
886
+ }
887
+ async function install(root, target, ordered, requested, existing) {
888
+ const lock = {
889
+ lockfileVersion: 1,
890
+ target: target.id,
891
+ skills: { ...existing?.target === target.id ? existing.skills : {} }
892
+ };
893
+ for (const skill of ordered) {
894
+ const file = pathFor(root, target, skill);
895
+ const text = renderFor(target, skill);
896
+ await mkdir(dirname2(file), { recursive: true });
897
+ await writeFile(file, text, "utf8");
898
+ lock.skills[skill.name] = entryFor(
899
+ skill,
900
+ text,
901
+ requested.has(skill.name) ? "requested" : "required"
902
+ );
903
+ }
904
+ return lock;
905
+ }
906
+ function resolveTarget(id, lock) {
907
+ const wanted = id ?? lock?.target ?? DEFAULT_TARGET;
908
+ const target = targetById(wanted);
909
+ if (!target) {
910
+ throw new Error(`unknown target \`${wanted}\``);
911
+ }
912
+ return target;
913
+ }
914
+
915
+ // src/ui.ts
916
+ var on = process.stdout.isTTY === true && !process.env["NO_COLOR"];
917
+ var wrap = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
918
+ var bold = wrap("1");
919
+ var dim = wrap("2");
920
+ var green = wrap("32");
921
+ var yellow = wrap("33");
922
+ var red = wrap("31");
923
+ var cyan = wrap("36");
924
+ function out(line = "") {
925
+ process.stdout.write(`${line}
926
+ `);
927
+ }
928
+ function err(line) {
929
+ process.stderr.write(`${line}
930
+ `);
931
+ }
932
+ function tokens(words2) {
933
+ return `~${(Math.round(words2 * 4 / 300) * 100).toLocaleString()} tokens`;
934
+ }
935
+ async function confirm(question, assumeYes) {
936
+ if (assumeYes) return true;
937
+ if (!process.stdin.isTTY) return false;
938
+ process.stdout.write(`${question} [y/N] `);
939
+ const answer = await new Promise((resolve2) => {
940
+ process.stdin.setEncoding("utf8");
941
+ process.stdin.once("data", (d) => resolve2(String(d)));
942
+ process.stdin.resume();
943
+ });
944
+ process.stdin.pause();
945
+ return /^y(es)?$/i.test(answer.trim());
946
+ }
947
+
948
+ // src/commands/add.ts
949
+ async function add(opts) {
950
+ if (opts.names.length === 0) {
951
+ out(`${yellow("Nothing to add.")} Name a skill, or run ${bold("skyl scan")} to see what fits.`);
952
+ return 1;
953
+ }
954
+ const { skills } = await loadRegistry({ dir: opts.dir, refresh: opts.refresh });
955
+ const requested = normalise(skills, opts.names);
956
+ const ordered = resolve(skills, requested);
957
+ const extra = implied(ordered, requested);
958
+ const lock = await readLockfile(opts.root);
959
+ const target = resolveTarget(opts.target, lock);
960
+ const already = new Set(Object.keys(lock?.skills ?? {}));
961
+ out();
962
+ for (const skill of ordered) {
963
+ const state = already.has(skill.name) ? dim("reinstall") : green("install");
964
+ const why = extra.some((e) => e.name === skill.name) ? dim("required by another skill") : "";
965
+ out(` ${bold(skill.name.padEnd(22))} ${skill.version.padEnd(8)} ${state.padEnd(18)} ${why}`);
966
+ }
967
+ const words2 = ordered.reduce((n, s) => n + s.installableWords, 0);
968
+ out();
969
+ out(` ${ordered.length} skills, ${tokens(words2)} ${dim(`into ${target.dir}`)}`);
970
+ out();
971
+ if (!await confirm(" Write them?", opts.yes === true)) {
972
+ out(` ${dim("Nothing written.")}`);
973
+ return 1;
974
+ }
975
+ const next = await install(opts.root, target, ordered, new Set(requested), lock);
976
+ await writeLockfile(opts.root, next);
977
+ out();
978
+ out(` ${green("Done.")} ${dim("Recorded in skyl.lock.")}`);
979
+ out();
980
+ return 0;
981
+ }
982
+
983
+ // src/commands/audit.ts
984
+ import { readFile as readFile3 } from "node:fs/promises";
985
+ import { join as join3, relative } from "node:path";
986
+ var CANDIDATES = [
987
+ "CLAUDE.md",
988
+ ".claude/CLAUDE.md",
989
+ "AGENTS.md",
990
+ ".cursorrules",
991
+ ".windsurfrules",
992
+ ".github/copilot-instructions.md",
993
+ ".continue/rules.md",
994
+ ".aider.conf.yml"
995
+ ];
996
+ async function audit2(opts) {
997
+ let path = opts.file;
998
+ if (!path) {
999
+ for (const c of CANDIDATES) {
1000
+ const full = join3(opts.root, c);
1001
+ if (await readFile3(full, "utf8").then(() => true, () => false)) {
1002
+ path = full;
1003
+ break;
1004
+ }
1005
+ }
1006
+ } else if (!path.startsWith("/")) {
1007
+ path = join3(opts.root, path);
1008
+ }
1009
+ if (!path) {
1010
+ out(` ${yellow("No hand-written context file found.")}`);
1011
+ out(` ${dim(`Looked for ${CANDIDATES.slice(0, 4).join(", ")}`)}`);
1012
+ return 1;
1013
+ }
1014
+ const text = await readFile3(path, "utf8").catch(() => void 0);
1015
+ if (text === void 0) {
1016
+ out(` ${red("Cannot read")} ${path}`);
1017
+ return 1;
1018
+ }
1019
+ const { skills } = await loadRegistry({ dir: opts.dir, refresh: opts.refresh });
1020
+ const lock = await readLockfile(opts.root);
1021
+ const installed = new Set(Object.keys(lock?.skills ?? {}));
1022
+ const report = audit(text, skills);
1023
+ out();
1024
+ out(` ${bold(relative(opts.root, path) || path)} ${dim(`${report.words} words, ${tokens(report.words)}`)}`);
1025
+ out();
1026
+ if (report.secrets.length > 0) {
1027
+ out(` ${red("Possible credentials.")} ${dim("these reach the model with every request")}`);
1028
+ for (const s of report.secrets) out(` ${red("!")} line ${s.line}, ${s.what}`);
1029
+ out();
1030
+ }
1031
+ let flagged = 0;
1032
+ for (const section of report.sections) {
1033
+ const notes = [];
1034
+ for (const o of section.overlaps) {
1035
+ const state = installed.has(o.skill) ? "installed" : "in the registry";
1036
+ notes.push(`${yellow("~")} shares vocabulary with ${bold(o.skill)} ${dim(`(${state}: ${o.terms.slice(0, 6).join(", ")})`)}`);
1037
+ }
1038
+ if (section.noInstruction && section.words > 40) {
1039
+ notes.push(`${yellow("~")} no instruction in it ${dim("nothing to follow, and the agent can read the code itself")}`);
1040
+ }
1041
+ if (notes.length === 0) continue;
1042
+ flagged += 1;
1043
+ out(` ${bold(section.heading)} ${dim(`${section.words} words`)}`);
1044
+ for (const n of notes) out(` ${n}`);
1045
+ out();
1046
+ }
1047
+ if (report.missing.length > 0) {
1048
+ out(` ${dim("Not found in the file")}`);
1049
+ for (const m of report.missing) out(` ${dim("o")} ${m}`);
1050
+ out();
1051
+ }
1052
+ if (flagged === 0 && report.secrets.length === 0) {
1053
+ out(` ${green("Nothing to flag.")} ${dim("no overlap with the registry, and every section carries an instruction")}`);
1054
+ out();
1055
+ return 0;
1056
+ }
1057
+ out(` ${dim("Overlap is word overlap, not a judgement. Read the section before cutting it.")}`);
1058
+ out(` ${cyan("skyl scan")} ${dim("shows what the registry would install for this project.")}`);
1059
+ out();
1060
+ return 0;
1061
+ }
1062
+
1063
+ // src/commands/context.ts
1064
+ import { writeFile as writeFile2 } from "node:fs/promises";
1065
+
1066
+ // src/context.ts
1067
+ import { readdir as readdir2, readFile as readFile6, stat as stat2 } from "node:fs/promises";
1068
+ import { join as join6, relative as relative3, sep as sep2 } from "node:path";
1069
+
1070
+ // src/ignore.ts
1071
+ import { readFile as readFile4 } from "node:fs/promises";
1072
+ import { join as join4 } from "node:path";
1073
+ function toRegExp(glob) {
1074
+ let source = "";
1075
+ for (let i = 0; i < glob.length; i += 1) {
1076
+ const c = glob[i];
1077
+ if (c === "*") {
1078
+ if (glob[i + 1] === "*") {
1079
+ if (glob[i + 2] === "/") {
1080
+ source += "(?:.*/)?";
1081
+ i += 2;
1082
+ } else {
1083
+ source += ".*";
1084
+ i += 1;
1085
+ }
1086
+ } else source += "[^/]*";
1087
+ } else if (c === "?") source += "[^/]";
1088
+ else if ("\\^$.|+()[]{}".includes(c)) source += `\\${c}`;
1089
+ else source += c;
1090
+ }
1091
+ return new RegExp(`^${source}$`);
1092
+ }
1093
+ function parseIgnore(text) {
1094
+ const out2 = [];
1095
+ for (const raw of text.split("\n")) {
1096
+ let line = raw.trim();
1097
+ if (line === "" || line.startsWith("#")) continue;
1098
+ const negated = line.startsWith("!");
1099
+ if (negated) line = line.slice(1);
1100
+ const directoryOnly = line.endsWith("/");
1101
+ if (directoryOnly) line = line.slice(0, -1);
1102
+ const anchored = line.includes("/");
1103
+ if (line.startsWith("/")) line = line.slice(1);
1104
+ out2.push({ re: toRegExp(anchored ? line : `**/${line}`), negated, directoryOnly });
1105
+ }
1106
+ return out2;
1107
+ }
1108
+ async function loadIgnore(root, extra = []) {
1109
+ const patterns = [];
1110
+ for (const file of [".gitignore", ".skylignore"]) {
1111
+ const text = await readFile4(join4(root, file), "utf8").catch(() => void 0);
1112
+ if (text !== void 0) patterns.push(...parseIgnore(text));
1113
+ }
1114
+ patterns.push(...parseIgnore(extra.join("\n")));
1115
+ return {
1116
+ ignored(path, isDirectory) {
1117
+ const parents = segments(path);
1118
+ let result = false;
1119
+ for (const p of patterns) {
1120
+ const self = (!p.directoryOnly || isDirectory) && p.re.test(path);
1121
+ const under = parents.some((s) => p.re.test(s));
1122
+ if (self || under) result = !p.negated;
1123
+ }
1124
+ return result;
1125
+ }
1126
+ };
1127
+ }
1128
+ function segments(path) {
1129
+ const parts = path.split("/");
1130
+ return parts.slice(0, -1).map((_, i) => parts.slice(0, i + 1).join("/"));
1131
+ }
1132
+
1133
+ // src/scan.ts
1134
+ import { readdir, readFile as readFile5 } from "node:fs/promises";
1135
+ import { join as join5, relative as relative2, sep } from "node:path";
1136
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", "build", ".git", ".gradle", ".idea", "dist", "out", ".kotlin"]);
1137
+ var MAX_DEPTH = 6;
1138
+ var BUILD_FILES = ["build.gradle.kts", "build.gradle", "settings.gradle.kts", "settings.gradle"];
1139
+ var CATALOGS = ["libs.versions.toml"];
1140
+ async function walk(root, depth = 0, acc = []) {
1141
+ if (depth > MAX_DEPTH) return acc;
1142
+ let entries;
1143
+ try {
1144
+ entries = await readdir(root, { withFileTypes: true });
1145
+ } catch {
1146
+ return acc;
1147
+ }
1148
+ for (const e of entries) {
1149
+ if (e.name.startsWith(".") && e.name !== ".claude") {
1150
+ if (SKIP.has(e.name)) continue;
1151
+ }
1152
+ if (SKIP.has(e.name)) continue;
1153
+ const full = join5(root, e.name);
1154
+ if (e.isDirectory()) await walk(full, depth + 1, acc);
1155
+ else acc.push(full);
1156
+ }
1157
+ return acc;
1158
+ }
1159
+ var DEP = /["']([a-zA-Z][\w.-]*(?:\.[\w-]+)+:[\w.-]+)(?::[^"']*)?["']/g;
1160
+ var PLUGIN_ID = /id\s*\(?\s*["']([\w.-]+)["']/g;
1161
+ var PLUGIN_ALIAS = /^\s*([\w-]+)\s*=\s*\{[^}]*id\s*=\s*["']([\w.-]+)["']/gm;
1162
+ var TOML_DEP = /^\s*[\w-]+\s*=\s*\{[^}]*module\s*=\s*["']([\w.:-]+)["']/gm;
1163
+ var TOML_GROUP = /^\s*[\w-]+\s*=\s*\{[^}]*group\s*=\s*["']([\w.-]+)["'][^}]*name\s*=\s*["']([\w.-]+)["']/gm;
1164
+ async function scanProject(root) {
1165
+ const files = await walk(root);
1166
+ const rel = files.map((f) => relative2(root, f).split(sep).join("/"));
1167
+ const dependencies = /* @__PURE__ */ new Set();
1168
+ const plugins = /* @__PURE__ */ new Set();
1169
+ const manifestElements = /* @__PURE__ */ new Set();
1170
+ const manifestAttributes = /* @__PURE__ */ new Set();
1171
+ const properties = /* @__PURE__ */ new Set();
1172
+ for (const f of files) {
1173
+ const name = f.split(sep).pop();
1174
+ const isBuild = BUILD_FILES.includes(name);
1175
+ const isCatalog = CATALOGS.includes(name);
1176
+ const isManifest = name === "AndroidManifest.xml";
1177
+ const isProps = name === "gradle.properties";
1178
+ if (!isBuild && !isCatalog && !isManifest && !isProps) continue;
1179
+ let text;
1180
+ try {
1181
+ text = await readFile5(f, "utf8");
1182
+ } catch {
1183
+ continue;
1184
+ }
1185
+ if (isBuild || isCatalog) {
1186
+ for (const m of text.matchAll(DEP)) dependencies.add(m[1]);
1187
+ for (const m of text.matchAll(TOML_DEP)) dependencies.add(m[1]);
1188
+ for (const m of text.matchAll(TOML_GROUP)) dependencies.add(`${m[1]}:${m[2]}`);
1189
+ for (const m of text.matchAll(PLUGIN_ID)) plugins.add(m[1]);
1190
+ for (const m of text.matchAll(PLUGIN_ALIAS)) plugins.add(m[2]);
1191
+ }
1192
+ if (isManifest) {
1193
+ for (const m of text.matchAll(/<([a-zA-Z-]+)[\s>]/g)) manifestElements.add(m[1]);
1194
+ for (const m of text.matchAll(/\s(android:[\w]+)\s*=/g)) manifestAttributes.add(m[1]);
1195
+ }
1196
+ if (isProps) {
1197
+ for (const line of text.split("\n")) {
1198
+ const eq = line.indexOf("=");
1199
+ if (eq > 0 && !line.trimStart().startsWith("#")) properties.add(line.slice(0, eq).trim());
1200
+ }
1201
+ }
1202
+ }
1203
+ return {
1204
+ file: rel,
1205
+ gradle_dependency: [...dependencies],
1206
+ gradle_plugin: [...plugins],
1207
+ manifest_element: [...manifestElements],
1208
+ manifest_attribute: [...manifestAttributes],
1209
+ gradle_property: [...properties]
1210
+ };
1211
+ }
1212
+
1213
+ // src/context.ts
1214
+ var MAX_FILES = 40;
1215
+ var MAX_BYTES_PER_FILE = 16e3;
1216
+ var MAX_TOTAL_BYTES = 4e5;
1217
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "build", ".git", ".gradle", ".idea", "dist", "out", "target", "vendor", ".next", ".venv"]);
1218
+ var DOC = /\.(md|mdx|adoc|rst)$/i;
1219
+ var SCHEMA = /\.(sql|graphql|gql|prisma|proto)$/i;
1220
+ var CODE = /\.(kt|kts|java|swift|ts|tsx|js|jsx|py|go|rb|rs|php|cs|dart|vue|svelte)$/i;
1221
+ var LOCKISH = /(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Podfile\.lock|\.min\.js|\.map)$/i;
1222
+ var REASONS = [
1223
+ { why: "documentation", weight: 100, test: (p, b) => DOC.test(b) && !/changelog|license|contributing|code_of_conduct/i.test(b) && (p.split("/").length <= 3 || /docs?\//i.test(p)) },
1224
+ { why: "schema", weight: 90, test: (_p, b) => SCHEMA.test(b) },
1225
+ // `AppModule.kt` starts like an entry point and is wiring, so the exclusion is explicit
1226
+ { why: "entry point", weight: 80, test: (_p, b) => /^(Main|App|Application|index|main)[A-Za-z]*\.(kt|java|ts|tsx|js|jsx|py|go|swift|dart)$/.test(b) && !/(Module|Component|Config|Test)\./.test(b) || /Activity\.(kt|java)$/.test(b) },
1227
+ { why: "dependency wiring", weight: 70, test: (p, b) => /(Module|Component|Injection|di)\.(kt|java|ts)$/.test(b) || /\/di\//.test(p) },
1228
+ { why: "shared foundation", weight: 60, test: (p) => /(^|\/)(core|common|base|shared|lib)\//i.test(p) },
1229
+ { why: "navigation or routing", weight: 55, test: (_p, b) => /(Nav|Navigation|Router|Routes|Screen)[A-Za-z]*\.(kt|java|ts|tsx)$/.test(b) },
1230
+ { why: "data model", weight: 50, test: (p, b) => /(Entity|Dto|Model|Repository|Dao|Api|Service)\.(kt|java|ts)$/.test(b) || /\/(model|domain|entity)\//i.test(p) },
1231
+ { why: "configuration", weight: 40, test: (_p, b) => /^(build\.gradle(\.kts)?|settings\.gradle(\.kts)?|libs\.versions\.toml|package\.json|pyproject\.toml|Cargo\.toml|go\.mod|Makefile|docker-compose\.ya?ml)$/.test(b) }
1232
+ ];
1233
+ async function walk2(root, ignore) {
1234
+ const out2 = [];
1235
+ const step = async (dir, depth) => {
1236
+ if (depth > 8) return;
1237
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
1238
+ for (const entry of entries) {
1239
+ if (SKIP_DIRS.has(entry.name)) continue;
1240
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
1241
+ const full = join6(dir, entry.name);
1242
+ const rel = relative3(root, full).split(sep2).join("/");
1243
+ if (ignore.ignored(rel, entry.isDirectory())) continue;
1244
+ if (entry.isDirectory()) await step(full, depth + 1);
1245
+ else if (!LOCKISH.test(entry.name)) {
1246
+ const info = await stat2(full).catch(() => void 0);
1247
+ if (info) out2.push({ path: rel, bytes: info.size });
1248
+ }
1249
+ }
1250
+ };
1251
+ await step(root, 0);
1252
+ return out2;
1253
+ }
1254
+ function renderTree(paths, limit = 500, perDirectory = 12) {
1255
+ const byDir = /* @__PURE__ */ new Map();
1256
+ for (const p of paths) {
1257
+ const at = p.lastIndexOf("/");
1258
+ const dir = at < 0 ? "" : p.slice(0, at);
1259
+ (byDir.get(dir) ?? byDir.set(dir, []).get(dir)).push(p.slice(at + 1));
1260
+ for (let d = dir; d.includes("/"); d = d.slice(0, d.lastIndexOf("/"))) {
1261
+ if (!byDir.has(d.slice(0, d.lastIndexOf("/")))) byDir.set(d.slice(0, d.lastIndexOf("/")), []);
1262
+ }
1263
+ if (dir !== "" && !byDir.has("")) byDir.set("", []);
1264
+ }
1265
+ const lines = [];
1266
+ let shown = 0;
1267
+ for (const dir of [...byDir.keys()].sort()) {
1268
+ const depth = dir === "" ? 0 : dir.split("/").length;
1269
+ if (dir !== "") lines.push(`${" ".repeat(depth - 1)}${dir.split("/").pop()}/`);
1270
+ const files = byDir.get(dir).sort();
1271
+ for (const file of files.slice(0, perDirectory)) {
1272
+ if (shown >= limit) break;
1273
+ lines.push(`${" ".repeat(depth)}${file}`);
1274
+ shown += 1;
1275
+ }
1276
+ if (files.length > perDirectory) lines.push(`${" ".repeat(depth)}... ${files.length - perDirectory} more files`);
1277
+ if (shown >= limit) {
1278
+ lines.push(`... ${paths.length - shown} more files`);
1279
+ break;
1280
+ }
1281
+ }
1282
+ return lines.join("\n");
1283
+ }
1284
+ async function buildContext(root, opts = {}) {
1285
+ const ignore = await loadIgnore(root);
1286
+ const all = await walk2(root, ignore);
1287
+ const scored = [];
1288
+ for (const file of all) {
1289
+ const base = file.path.split("/").pop();
1290
+ const reason = REASONS.find((r) => r.test(file.path, base));
1291
+ if (!reason) continue;
1292
+ if (!DOC.test(base) && !SCHEMA.test(base) && !CODE.test(base) && reason.why !== "configuration") continue;
1293
+ scored.push({ path: file.path, why: reason.why, bytes: file.bytes, weight: reason.weight });
1294
+ }
1295
+ scored.sort((a, b) => b.weight - a.weight || b.bytes - a.bytes);
1296
+ const limit = opts.maxFiles ?? MAX_FILES;
1297
+ const files = [];
1298
+ const withheld = [];
1299
+ let total = 0;
1300
+ for (const pick of scored) {
1301
+ if (files.length >= limit || total >= MAX_TOTAL_BYTES) break;
1302
+ const text = await readFile6(join6(root, pick.path), "utf8").catch(() => void 0);
1303
+ if (text === void 0) continue;
1304
+ const secrets = scanSecrets(text);
1305
+ if (secrets.length > 0) {
1306
+ withheld.push({ path: pick.path, why: `${secrets[0].what} on line ${secrets[0].line}` });
1307
+ continue;
1308
+ }
1309
+ const truncated = text.length > MAX_BYTES_PER_FILE;
1310
+ const body2 = truncated ? `${text.slice(0, MAX_BYTES_PER_FILE)}
1311
+ ... truncated` : text;
1312
+ total += body2.length;
1313
+ files.push({ path: pick.path, why: pick.why, bytes: pick.bytes, text: body2, truncated });
1314
+ }
1315
+ return {
1316
+ root,
1317
+ tree: renderTree(all.map((f) => f.path)),
1318
+ signals: await scanProject(root),
1319
+ installed: opts.installed ?? [],
1320
+ files,
1321
+ withheld
1322
+ };
1323
+ }
1324
+ function renderContext(bundle) {
1325
+ const parts = [];
1326
+ parts.push("# Project context bundle\n");
1327
+ parts.push(`Collected by \`skyl context\` from \`${bundle.root}\`. Files were selected by rule, not by a model.
1328
+ `);
1329
+ parts.push("## Directory tree\n");
1330
+ parts.push("```\n" + bundle.tree + "\n```\n");
1331
+ const signals = Object.entries(bundle.signals).filter(([, v]) => v.length > 0);
1332
+ if (signals.length > 0) {
1333
+ parts.push("## What the build declares\n");
1334
+ for (const [key, values] of signals) {
1335
+ parts.push(`**${key}**
1336
+ `);
1337
+ parts.push(values.map((v) => `- ${v}`).join("\n") + "\n");
1338
+ }
1339
+ }
1340
+ if (bundle.installed.length > 0) {
1341
+ parts.push("## Skills already installed\n");
1342
+ parts.push("Do not restate anything these cover.\n");
1343
+ parts.push(bundle.installed.map((s) => `- ${s}`).join("\n") + "\n");
1344
+ }
1345
+ parts.push("## Files\n");
1346
+ for (const file of bundle.files) {
1347
+ parts.push(`### ${file.path}
1348
+ `);
1349
+ parts.push(`_selected as ${file.why}${file.truncated ? ", truncated" : ""}_
1350
+ `);
1351
+ parts.push("```\n" + file.text.replace(/```/g, "``\u200B`") + "\n```\n");
1352
+ }
1353
+ if (bundle.withheld.length > 0) {
1354
+ parts.push("## Withheld\n");
1355
+ parts.push("These matched the selection rules and were not included.\n");
1356
+ parts.push(bundle.withheld.map((w) => `- ${w.path}, ${w.why}`).join("\n") + "\n");
1357
+ }
1358
+ return parts.join("\n");
1359
+ }
1360
+
1361
+ // src/commands/context.ts
1362
+ async function context(opts) {
1363
+ const lock = await readLockfile(opts.root);
1364
+ const bundle = await buildContext(opts.root, {
1365
+ installed: Object.keys(lock?.skills ?? {}),
1366
+ ...opts.maxFiles === void 0 ? {} : { maxFiles: opts.maxFiles }
1367
+ });
1368
+ if (opts.dryRun === true) {
1369
+ out();
1370
+ out(` ${bold("Would send")} ${dim(`${bundle.files.length} files`)}`);
1371
+ out();
1372
+ for (const file of bundle.files) {
1373
+ out(` ${file.path.padEnd(52)} ${dim(`${file.why}${file.truncated ? ", truncated" : ""}`)}`);
1374
+ }
1375
+ if (bundle.withheld.length > 0) {
1376
+ out();
1377
+ out(` ${yellow("Withheld")}`);
1378
+ for (const w of bundle.withheld) out(` ${w.path.padEnd(52)} ${dim(w.why)}`);
1379
+ }
1380
+ const words2 = bundle.files.reduce((n, f) => n + f.text.split(/\s+/).length, 0);
1381
+ out();
1382
+ out(` ${dim(`plus the directory tree and what the build declares, ${tokens(words2)}`)}`);
1383
+ out();
1384
+ return 0;
1385
+ }
1386
+ const text = opts.json === true ? `${JSON.stringify(bundle, null, 2)}
1387
+ ` : renderContext(bundle);
1388
+ if (opts.outFile) {
1389
+ await writeFile2(opts.outFile, text, "utf8");
1390
+ out(` ${green("Written.")} ${opts.outFile} ${dim(`${bundle.files.length} files`)}`);
1391
+ return 0;
1392
+ }
1393
+ process.stdout.write(text);
1394
+ return 0;
1395
+ }
1396
+
1397
+ // src/state.ts
1398
+ async function installedState(root, lock, target, opts) {
1399
+ let registry;
1400
+ let from;
1401
+ try {
1402
+ const reg = await loadRegistry(opts);
1403
+ registry = new Map(reg.skills.map((s) => [s.name, s]));
1404
+ from = reg.from;
1405
+ } catch {
1406
+ }
1407
+ const items = [];
1408
+ for (const name of Object.keys(lock.skills).sort()) {
1409
+ const entry = lock.skills[name];
1410
+ const skill = registry?.get(name);
1411
+ const onDisk = skill ? await readInstalled(root, target, skill) : void 0;
1412
+ items.push({
1413
+ name,
1414
+ entry,
1415
+ skill,
1416
+ onDisk,
1417
+ upstream: skill ? renderFor(target, skill) : void 0,
1418
+ state: registry === void 0 ? "unchanged" : drift(entry, onDisk, skill)
1419
+ });
1420
+ }
1421
+ return { items, from };
1422
+ }
1423
+ function changedOnDisk(item) {
1424
+ return item.upstream !== void 0 && item.onDisk !== item.upstream;
1425
+ }
1426
+
1427
+ // src/commands/diff.ts
1428
+ function printDiff(item, full) {
1429
+ if (item.upstream === void 0 || item.onDisk === void 0) return false;
1430
+ const ops = diffLines(item.onDisk, item.upstream);
1431
+ const { added, removed } = stat(ops);
1432
+ if (added === 0 && removed === 0) return false;
1433
+ const versions = item.skill && item.skill.version !== item.entry.version ? dim(`${item.entry.version} to ${item.skill.version}`) : dim(`${item.entry.version}, edited here`);
1434
+ out();
1435
+ out(` ${bold(item.name)} ${versions} ${green(`+${added}`)} ${red(`-${removed}`)}`);
1436
+ out(` ${dim("- what is in your file, + what upstream has")}`);
1437
+ out();
1438
+ for (const hunk of hunks(ops, full ? 999 : 3)) {
1439
+ out(` ${cyan(`@@ -${hunk.beforeStart},${hunk.beforeCount} +${hunk.afterStart},${hunk.afterCount} @@`)}`);
1440
+ for (const line of hunk.lines) {
1441
+ const text = `${line.kind} ${line.text}`;
1442
+ out(` ${line.kind === "+" ? green(text) : line.kind === "-" ? red(text) : dim(text)}`);
1443
+ }
1444
+ }
1445
+ return true;
1446
+ }
1447
+ async function diff(opts) {
1448
+ const lock = await readLockfile(opts.root);
1449
+ if (!lock || Object.keys(lock.skills).length === 0) {
1450
+ out(`${yellow("Nothing installed.")} Run ${bold("skyl scan")} to see what fits this project.`);
1451
+ return 0;
1452
+ }
1453
+ const target = resolveTarget(void 0, lock);
1454
+ const { items, from } = await installedState(opts.root, lock, target, {
1455
+ dir: opts.dir,
1456
+ refresh: opts.refresh ?? opts.dir === void 0
1457
+ });
1458
+ if (from === void 0) {
1459
+ out(` ${yellow("No registry reachable.")} ${dim("Nothing to compare against.")}`);
1460
+ return 1;
1461
+ }
1462
+ const wanted = new Set(opts.names ?? []);
1463
+ const subject = wanted.size === 0 ? items : items.filter((i) => wanted.has(i.name) || wanted.has(i.name.split("/")[1]));
1464
+ if (subject.length === 0) {
1465
+ out(` ${yellow("Not installed here.")} ${dim([...wanted].join(", "))}`);
1466
+ return 1;
1467
+ }
1468
+ let shown = 0;
1469
+ for (const item of subject) {
1470
+ if (item.skill === void 0) {
1471
+ out();
1472
+ out(` ${bold(item.name)} ${yellow("is no longer in the registry")} ${dim("installed at " + item.entry.version)}`);
1473
+ shown += 1;
1474
+ continue;
1475
+ }
1476
+ if (item.onDisk === void 0) {
1477
+ out();
1478
+ out(` ${bold(item.name)} ${yellow("file is gone")} ${dim(`skyl add ${item.name} puts it back`)}`);
1479
+ shown += 1;
1480
+ continue;
1481
+ }
1482
+ if (printDiff(item, opts.full === true)) shown += 1;
1483
+ }
1484
+ out();
1485
+ if (shown === 0) {
1486
+ out(` ${green("Up to date.")} ${dim(`${subject.length} skills, nothing changed here or upstream`)}`);
1487
+ } else {
1488
+ const editable = subject.filter((i) => changedOnDisk(i) && i.state !== "local-edit").length;
1489
+ if (editable > 0) out(` ${cyan("skyl update")} ${dim("applies the upstream side")}`);
1490
+ }
1491
+ out();
1492
+ return 0;
1493
+ }
1494
+
1495
+ // src/commands/learn.ts
1496
+ import { spawn } from "node:child_process";
1497
+ import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile3 } from "node:fs/promises";
1498
+ import { dirname as dirname3, join as join7 } from "node:path";
1499
+
1500
+ // src/learn.ts
1501
+ var LEARNED_NAME = "project/core";
1502
+ var MANUAL_SECTION = "Manual";
1503
+ var INSTRUCTIONS = `You are writing a project knowledge skill for an AI coding agent that will work in this
1504
+ repository. The context bundle below was selected mechanically, not by a model.
1505
+
1506
+ Write **product knowledge**, not code quality advice. What the product is, what its terms mean, what
1507
+ its parts are called, where things live. A rule about Compose or coroutines belongs in a community
1508
+ skill and must not appear here.
1509
+
1510
+ Include only what you can point at in the bundle:
1511
+
1512
+ - **Glossary.** Domain terms and what they mean, with where each is defined. Usually the most
1513
+ valuable section, and the one nothing else can supply.
1514
+ - **Features.** Modules and screens, and what each is called.
1515
+ - **Entities.** The data model and how the pieces relate.
1516
+ - **Integrations.** Third party services, and what each is used for.
1517
+ - **Flows.** Named flows and their entry points, such as "withdrawal starts at WithdrawFragment and
1518
+ hits /api/withdraw".
1519
+ - **Placement.** Where a new screen, a new model, a new test goes.
1520
+ - **Commands.** Build, test, lint.
1521
+
1522
+ Leave out:
1523
+
1524
+ - Explanations of how code works. The agent reads the code faster than it reads a summary.
1525
+ - Architecture justification. One sentence for the choice, none for the argument.
1526
+ - Anything the installed skills already cover.
1527
+ - Secrets, credentials, environment structure. If you see one, say so and do not copy it.
1528
+
1529
+ **Ask rather than assert.** Anything you cannot observe in the bundle is a question, never a claim.
1530
+ Collect the questions at the end of the file under \`## Questions\`, each as a line ending in
1531
+ \`<!-- unanswered -->\`. A business rule you inferred from a status enum is a question. This is the
1532
+ part that keeps the file from decaying: it improves each run instead of drifting.
1533
+
1534
+ Output one markdown file and nothing else. No preamble, no explanation of what you did. Start at the
1535
+ frontmatter and use this shape:
1536
+
1537
+ \`\`\`
1538
+ ---
1539
+ name: project/core
1540
+ axis: topic
1541
+ family: project
1542
+ version: 1.0.0
1543
+ agent_sections: [rules]
1544
+ generated: <date>
1545
+ ---
1546
+
1547
+ ## Rules
1548
+
1549
+ - **PROJ-1** \`must\`: <instruction>
1550
+ *Why:* <the failure it prevents>
1551
+ *Not when:* <the boundary>
1552
+
1553
+ ## Glossary
1554
+ ...
1555
+ ## Questions
1556
+ - Can a withdrawal be cancelled after PROCESSING? <!-- unanswered -->
1557
+ \`\`\`
1558
+ `;
1559
+ function learnPrompt(bundle, keep) {
1560
+ const parts = [INSTRUCTIONS];
1561
+ if (keep.manual) {
1562
+ parts.push(`
1563
+ ## Text to carry over unchanged
1564
+
1565
+ This was written by hand. Reproduce it verbatim as a \`## ${MANUAL_SECTION}\` section, and do not
1566
+ edit, summarise, or reorder it.
1567
+
1568
+ \`\`\`
1569
+ ${keep.manual}
1570
+ \`\`\`
1571
+ `);
1572
+ }
1573
+ if (keep.questions) {
1574
+ parts.push(`
1575
+ ## Questions still open from the last run
1576
+
1577
+ Carry any that are still unanswered. Drop one only if the bundle now answers it.
1578
+
1579
+ \`\`\`
1580
+ ${keep.questions}
1581
+ \`\`\`
1582
+ `);
1583
+ }
1584
+ parts.push("\n---\n");
1585
+ parts.push(renderContext(bundle));
1586
+ return parts.join("\n");
1587
+ }
1588
+ function preserved(existing) {
1589
+ if (!existing) return {};
1590
+ const found = sections(existing);
1591
+ const manual = found.get(MANUAL_SECTION);
1592
+ const questions = found.get("Questions");
1593
+ return {
1594
+ ...manual ? { manual } : {},
1595
+ ...questions ? { questions: questions.split("\n").filter((l) => l.includes("<!-- unanswered -->")).join("\n") } : {}
1596
+ };
1597
+ }
1598
+ function extractMarkdown(output) {
1599
+ const fenced = /```(?:markdown|md)?\n([\s\S]*?)```/.exec(output);
1600
+ const text = (fenced?.[1] ?? output).trim();
1601
+ const start = text.indexOf("---");
1602
+ if (start < 0) return void 0;
1603
+ return `${text.slice(start).trim()}
1604
+ `;
1605
+ }
1606
+ function reattach(generated, keep) {
1607
+ let out2 = generated;
1608
+ const found = sections(out2);
1609
+ if (keep.manual && !found.has(MANUAL_SECTION)) {
1610
+ out2 = `${out2.trimEnd()}
1611
+
1612
+ ## ${MANUAL_SECTION}
1613
+
1614
+ ${keep.manual}
1615
+ `;
1616
+ }
1617
+ if (keep.questions) {
1618
+ const already = found.get("Questions") ?? "";
1619
+ const missing = keep.questions.split("\n").filter((q) => q.trim() !== "" && !already.includes(q.trim()));
1620
+ if (missing.length > 0) {
1621
+ out2 = found.has("Questions") ? out2.replace(/^## Questions$/m, `## Questions
1622
+
1623
+ ${missing.join("\n")}`) : `${out2.trimEnd()}
1624
+
1625
+ ## Questions
1626
+
1627
+ ${missing.join("\n")}
1628
+ `;
1629
+ }
1630
+ }
1631
+ return out2;
1632
+ }
1633
+ function caution(date, sha) {
1634
+ return `<!--
1635
+ Generated by \`skyl learn\` on ${date}${sha ? ` from commit ${sha}` : ""}.
1636
+ Partly inferred from the repository. Correct anything wrong.
1637
+ Text under \`## ${MANUAL_SECTION}\` is written by hand and is preserved across refreshes.
1638
+ -->`;
1639
+ }
1640
+ function withCaution(markdown, date, sha) {
1641
+ const note = caution(date, sha);
1642
+ const end = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(markdown);
1643
+ if (!end) return `${note}
1644
+
1645
+ ${markdown}`;
1646
+ return `${markdown.slice(0, end[0].length)}
1647
+ ${note}
1648
+ ${markdown.slice(end[0].length)}`;
1649
+ }
1650
+
1651
+ // src/commands/learn.ts
1652
+ var SHORTHAND = { claude: ["claude", "-p"] };
1653
+ async function learn(opts) {
1654
+ const lock = await readLockfile(opts.root);
1655
+ const target = resolveTarget(opts.target, lock);
1656
+ const file = join7(opts.root, target.dir, target.fileFor(LEARNED_NAME));
1657
+ const existing = await readFile7(file, "utf8").catch(() => void 0);
1658
+ const keep = preserved(existing);
1659
+ const bundle = await buildContext(opts.root, { installed: Object.keys(lock?.skills ?? {}) });
1660
+ if (bundle.files.length === 0) {
1661
+ err(`${yellow("Nothing to read here.")} No documentation, schema or source files were selected.`);
1662
+ return 1;
1663
+ }
1664
+ const prompt = learnPrompt(bundle, keep);
1665
+ if (opts.printPrompt === true || !opts.agent) {
1666
+ if (opts.outFile) {
1667
+ await writeFile3(opts.outFile, prompt, "utf8");
1668
+ out(` ${green("Written.")} ${opts.outFile}`);
1669
+ } else {
1670
+ process.stdout.write(prompt);
1671
+ }
1672
+ if (!opts.agent && opts.printPrompt !== true) {
1673
+ err("");
1674
+ err(` ${dim("Paste this into any agent, or run")} ${cyan("skyl learn --agent claude")} ${dim("to do it here.")}`);
1675
+ }
1676
+ return 0;
1677
+ }
1678
+ const command = SHORTHAND[opts.agent] ?? opts.agent.split(" ");
1679
+ out();
1680
+ out(` ${bold("Reading this project")} ${dim(`${bundle.files.length} files, ${tokens(prompt.split(/\s+/).length)}`)}`);
1681
+ for (const w of bundle.withheld) out(` ${yellow("withheld")} ${w.path} ${dim(w.why)}`);
1682
+ out(` ${dim(`through ${command.join(" ")}, which uses your own account`)}`);
1683
+ out();
1684
+ if (!await confirm(` Send ${bundle.files.length} files to ${command[0]}?`, opts.yes === true)) {
1685
+ out(` ${dim("Nothing sent.")}`);
1686
+ return 1;
1687
+ }
1688
+ const output = await pipeTo(command, prompt).catch((cause) => {
1689
+ err(` ${red("Could not run")} ${command.join(" ")}`);
1690
+ err(` ${dim(cause.message)}`);
1691
+ return void 0;
1692
+ });
1693
+ if (output === void 0) return 1;
1694
+ const generated = extractMarkdown(output);
1695
+ if (!generated) {
1696
+ err(` ${red("No skill in the output.")} ${dim("Re-run with --print-prompt and paste it somewhere you can see the answer.")}`);
1697
+ return 1;
1698
+ }
1699
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1700
+ const sha = await gitSha(opts.root);
1701
+ const text = withCaution(reattach(generated, keep), date, sha);
1702
+ if (existing !== void 0) {
1703
+ const history = join7(opts.root, ".skyl", "history", `project-core.${sha ?? date}.md`);
1704
+ await mkdir2(dirname3(history), { recursive: true });
1705
+ await writeFile3(history, existing, "utf8");
1706
+ }
1707
+ await mkdir2(dirname3(file), { recursive: true });
1708
+ await writeFile3(file, text, "utf8");
1709
+ const open = (text.match(/<!-- unanswered -->/g) ?? []).length;
1710
+ out();
1711
+ out(` ${green(existing ? "Refreshed." : "Written.")} ${dim(file.replace(opts.root + "/", ""))}`);
1712
+ if (keep.manual) {
1713
+ const lines = keep.manual.split("\n").length;
1714
+ out(` ${dim(`## Manual carried over, ${lines} line${lines === 1 ? "" : "s"}`)}`);
1715
+ }
1716
+ if (open > 0) out(` ${yellow(`${open} question${open === 1 ? "" : "s"}`)} ${dim("left for you, marked <!-- unanswered -->")}`);
1717
+ out();
1718
+ return 0;
1719
+ }
1720
+ function pipeTo(command, input) {
1721
+ return new Promise((resolve2, reject) => {
1722
+ const child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "inherit"] });
1723
+ let stdout = "";
1724
+ child.stdout.setEncoding("utf8");
1725
+ child.stdout.on("data", (d) => {
1726
+ stdout += d;
1727
+ });
1728
+ child.on("error", reject);
1729
+ child.on("close", (code) => {
1730
+ if (code === 0) resolve2(stdout);
1731
+ else reject(new Error(`exited with code ${String(code)}`));
1732
+ });
1733
+ child.stdin.end(input, "utf8");
1734
+ });
1735
+ }
1736
+ async function gitSha(root) {
1737
+ return new Promise((resolve2) => {
1738
+ const child = spawn("git", ["-C", root, "rev-parse", "--short", "HEAD"], { stdio: ["ignore", "pipe", "ignore"] });
1739
+ let out2 = "";
1740
+ child.stdout.setEncoding("utf8");
1741
+ child.stdout.on("data", (d) => {
1742
+ out2 += d;
1743
+ });
1744
+ child.on("error", () => resolve2(void 0));
1745
+ child.on("close", (code) => resolve2(code === 0 && out2.trim() !== "" ? out2.trim() : void 0));
1746
+ });
1747
+ }
1748
+
1749
+ // src/commands/lint.ts
1750
+ import { readdir as readdir3, readFile as readFile8, stat as statFile } from "node:fs/promises";
1751
+ import { join as join8, relative as relative4 } from "node:path";
1752
+ async function skillFiles(path) {
1753
+ const info = await statFile(path).catch(() => void 0);
1754
+ if (!info) return [];
1755
+ if (info.isFile()) return [path];
1756
+ const out2 = [];
1757
+ const walk3 = async (dir, depth) => {
1758
+ if (depth > 3) return;
1759
+ for (const entry of await readdir3(dir, { withFileTypes: true })) {
1760
+ if (entry.name.startsWith(".")) continue;
1761
+ const full = join8(dir, entry.name);
1762
+ if (entry.isDirectory()) await walk3(full, depth + 1);
1763
+ else if (entry.name === "SKILL.md") out2.push(full);
1764
+ }
1765
+ };
1766
+ await walk3(path, 0);
1767
+ return out2.sort();
1768
+ }
1769
+ async function lint(opts) {
1770
+ const targets = opts.paths.length > 0 ? opts.paths : await defaultPaths(opts.root);
1771
+ if (targets.length === 0) {
1772
+ err(`${yellow("Nothing to lint.")} Point at a SKILL.md or a skills directory.`);
1773
+ return 1;
1774
+ }
1775
+ const files = [];
1776
+ for (const t of targets) files.push(...await skillFiles(t));
1777
+ if (files.length === 0) {
1778
+ err(`${yellow("No SKILL.md found in")} ${targets.join(", ")}`);
1779
+ return 1;
1780
+ }
1781
+ let errors = 0;
1782
+ let warnings = 0;
1783
+ const parsed = [];
1784
+ const perFile = /* @__PURE__ */ new Map();
1785
+ const fileOf = /* @__PURE__ */ new Map();
1786
+ for (const file of files) {
1787
+ const text = await readFile8(file, "utf8");
1788
+ perFile.set(file, lintSkill(text, file));
1789
+ try {
1790
+ const skill = parseSkill(text);
1791
+ parsed.push(skill);
1792
+ fileOf.set(skill.name, file);
1793
+ } catch {
1794
+ }
1795
+ }
1796
+ for (const [name, found] of lintRegistry(parsed)) {
1797
+ const file = fileOf.get(name);
1798
+ if (!file) continue;
1799
+ perFile.set(file, [...perFile.get(file) ?? [], ...found]);
1800
+ }
1801
+ out();
1802
+ for (const file of files) {
1803
+ const found = perFile.get(file) ?? [];
1804
+ const where = relative4(opts.root, file) || file;
1805
+ if (found.length === 0) {
1806
+ out(` ${green("ok")} ${dim(where)}`);
1807
+ continue;
1808
+ }
1809
+ out(` ${found.some((f) => f.level === "error") ? red("fail") : yellow("warn")} ${bold(where)}`);
1810
+ for (const f of found) {
1811
+ if (f.level === "error") errors += 1;
1812
+ else warnings += 1;
1813
+ const at = f.line === void 0 ? "" : dim(`:${f.line}`);
1814
+ out(` ${f.level === "error" ? red("error") : yellow("warn ")}${at} ${f.message}`);
1815
+ if (f.detail) out(` ${dim(f.detail)}`);
1816
+ }
1817
+ }
1818
+ out();
1819
+ out(` ${files.length} skill${files.length === 1 ? "" : "s"}, ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`);
1820
+ if (errors === 0 && warnings > 0 && opts.strict !== true) {
1821
+ out(` ${dim("warnings do not fail this run,")} ${cyan("--strict")} ${dim("makes them")}`);
1822
+ }
1823
+ out();
1824
+ return errors > 0 || opts.strict === true && warnings > 0 ? 1 : 0;
1825
+ }
1826
+ async function defaultPaths(root) {
1827
+ for (const candidate of ["skills", "."]) {
1828
+ const path = join8(root, candidate);
1829
+ const found = await skillFiles(path);
1830
+ if (found.length > 0) return [path];
1831
+ }
1832
+ return [];
1833
+ }
1834
+
1835
+ // src/commands/list.ts
1836
+ var LABEL = {
1837
+ unchanged: dim,
1838
+ "local-edit": yellow,
1839
+ "upstream-change": cyan,
1840
+ both: yellow,
1841
+ missing: yellow
1842
+ };
1843
+ var SAYS = {
1844
+ unchanged: "",
1845
+ "local-edit": "edited here",
1846
+ "upstream-change": "update available",
1847
+ both: "edited here, and updated upstream",
1848
+ missing: "file is gone"
1849
+ };
1850
+ async function list2(opts) {
1851
+ const lock = await readLockfile(opts.root);
1852
+ if (!lock || Object.keys(lock.skills).length === 0) {
1853
+ out(`${yellow("Nothing installed.")} Run ${bold("skyl scan")} to see what fits this project.`);
1854
+ return 0;
1855
+ }
1856
+ const target = resolveTarget(void 0, lock);
1857
+ let upstream = /* @__PURE__ */ new Map();
1858
+ try {
1859
+ const reg = await loadRegistry({ dir: opts.dir, refresh: opts.refresh });
1860
+ upstream = new Map(reg.skills.map((s) => [s.name, s]));
1861
+ } catch {
1862
+ }
1863
+ out();
1864
+ out(` ${bold("Installed")} ${dim(`for ${target.id}, in ${target.dir}`)}`);
1865
+ out();
1866
+ let words2 = 0;
1867
+ for (const name of Object.keys(lock.skills).sort()) {
1868
+ const entry = lock.skills[name];
1869
+ const remote = upstream.get(name);
1870
+ const onDisk = remote ? await readInstalled(opts.root, target, remote) : void 0;
1871
+ const state = upstream.size === 0 ? "unchanged" : drift(entry, onDisk, remote);
1872
+ const note = SAYS[state];
1873
+ if (remote) words2 += remote.installableWords;
1874
+ out(` ${bold(name.padEnd(22))} ${entry.version.padEnd(8)} ${dim(entry.reason.padEnd(10))} ${LABEL[state](note)}`);
1875
+ }
1876
+ out();
1877
+ if (words2 > 0) out(` ${Object.keys(lock.skills).length} skills, ${tokens(words2)}`);
1878
+ const stale = Object.keys(lock.skills).some((n) => {
1879
+ const r = upstream.get(n);
1880
+ return r && r.version !== lock.skills[n].version;
1881
+ });
1882
+ if (stale) out(` ${cyan("skyl add")} ${dim("reinstalls at the current version")}`);
1883
+ out();
1884
+ return 0;
1885
+ }
1886
+
1887
+ // src/commands/remove.ts
1888
+ import { rm } from "node:fs/promises";
1889
+ import { dirname as dirname4 } from "node:path";
1890
+ async function remove(opts) {
1891
+ const lock = await readLockfile(opts.root);
1892
+ if (!lock || Object.keys(lock.skills).length === 0) {
1893
+ out(`${yellow("Nothing installed.")}`);
1894
+ return 1;
1895
+ }
1896
+ if (opts.names.length === 0) {
1897
+ out(`${yellow("Name a skill to remove.")}`);
1898
+ return 1;
1899
+ }
1900
+ const target = resolveTarget(void 0, lock);
1901
+ const { skills } = await loadRegistry({ dir: opts.dir, refresh: opts.refresh });
1902
+ const byName = new Map(skills.map((s) => [s.name, s]));
1903
+ const installed = new Set(Object.keys(lock.skills));
1904
+ const asked = normalise(skills.filter((s) => installed.has(s.name)), opts.names);
1905
+ const going = new Set(asked);
1906
+ const orphaned = [];
1907
+ for (const name of installed) {
1908
+ if (going.has(name)) continue;
1909
+ for (const dep of byName.get(name)?.requires ?? []) {
1910
+ if (going.has(dep)) orphaned.push(`${name} requires ${dep}`);
1911
+ }
1912
+ }
1913
+ if (orphaned.length > 0) {
1914
+ out(` ${red("Still required.")}`);
1915
+ for (const line of orphaned) out(` ${dim(line)}`);
1916
+ out();
1917
+ out(` ${dim("Remove those first, or keep this one.")}`);
1918
+ return 1;
1919
+ }
1920
+ out();
1921
+ for (const name of asked) out(` ${bold(name.padEnd(22))} ${red("remove")}`);
1922
+ out();
1923
+ if (!await confirm(" Delete them?", opts.yes === true)) {
1924
+ out(` ${dim("Nothing deleted.")}`);
1925
+ return 1;
1926
+ }
1927
+ const skills2 = { ...lock.skills };
1928
+ for (const name of asked) {
1929
+ const skill = byName.get(name);
1930
+ if (skill) {
1931
+ const file = pathFor(opts.root, target, skill);
1932
+ await rm(file, { force: true });
1933
+ if (target.fileFor(name).includes("/")) await rm(dirname4(file), { recursive: true, force: true });
1934
+ }
1935
+ delete skills2[name];
1936
+ }
1937
+ await writeLockfile(opts.root, { ...lock, skills: skills2 });
1938
+ out();
1939
+ out(` ${green("Removed.")} ${dim(`${asked.length} gone, ${Object.keys(skills2).length} left`)}`);
1940
+ out();
1941
+ return 0;
1942
+ }
1943
+
1944
+ // src/commands/scan.ts
1945
+ async function scan(opts) {
1946
+ const { skills } = await loadRegistry({ dir: opts.dir, refresh: opts.refresh });
1947
+ const signals = await scanProject(opts.root);
1948
+ const matched = detect(skills, signals);
1949
+ if (matched.length === 0) {
1950
+ out(`${yellow("Nothing detected.")} No skill in the registry matches this project.`);
1951
+ out(dim(" Skyl currently covers Android. If this is an Android project, please open an issue."));
1952
+ return 0;
1953
+ }
1954
+ const chosen = resolve(skills, matched.map((m) => m.skill));
1955
+ const lock = await readLockfile(opts.root);
1956
+ const target = resolveTarget(void 0, lock);
1957
+ const installed = new Set(Object.keys(lock?.skills ?? {}));
1958
+ if (opts.json) {
1959
+ out(JSON.stringify({
1960
+ detected: matched,
1961
+ install: chosen.map((s) => ({ name: s.name, version: s.version, words: s.installableWords, installed: installed.has(s.name) })),
1962
+ target: target.id
1963
+ }, null, 2));
1964
+ return 0;
1965
+ }
1966
+ const evidence = /* @__PURE__ */ new Map();
1967
+ for (const m of matched) evidence.set(m.skill, m.on.map((o) => o.value));
1968
+ out();
1969
+ out(` ${bold("Detected")} ${dim(`in ${opts.root}`)}`);
1970
+ out();
1971
+ for (const skill of chosen) {
1972
+ const why = evidence.get(skill.name);
1973
+ const mark = installed.has(skill.name) ? dim("installed") : green("new");
1974
+ const reason = why?.length ? why[0] : skill.axis === "core" ? `every ${skill.family} project` : "required";
1975
+ out(` ${bold(skill.name.padEnd(22))} ${mark.padEnd(18)} ${dim(reason)}`);
1976
+ }
1977
+ const rest = unmatched(skills, matched).filter((s) => !chosen.some((c) => c.name === s.name));
1978
+ if (rest.length > 0) {
1979
+ out();
1980
+ out(` ${dim("Considered and not detected")}`);
1981
+ for (const s of rest) out(` ${dim(s.name)}`);
1982
+ }
1983
+ const fresh = chosen.filter((s) => !installed.has(s.name));
1984
+ const words2 = fresh.reduce((n, s) => n + s.installableWords, 0);
1985
+ out();
1986
+ if (fresh.length === 0) {
1987
+ out(` ${green("Up to date.")} ${dim(`${chosen.length} skills installed for ${target.id}.`)}`);
1988
+ } else {
1989
+ out(` ${fresh.length} to install, ${tokens(words2)} ${dim(`into ${target.dir}`)}`);
1990
+ out();
1991
+ out(` ${cyan(`skyl add ${fresh.map((s) => s.name).join(" ")}`)}`);
1992
+ }
1993
+ out();
1994
+ return 0;
1995
+ }
1996
+
1997
+ // src/commands/update.ts
1998
+ var SAYS2 = {
1999
+ apply: "update",
2000
+ restore: "file is gone, put it back",
2001
+ new: "newly required",
2002
+ kept: "edited here, left alone",
2003
+ conflict: "edited here, and changed upstream",
2004
+ gone: "no longer in the registry"
2005
+ };
2006
+ async function update(opts) {
2007
+ const lock = await readLockfile(opts.root);
2008
+ if (!lock || Object.keys(lock.skills).length === 0) {
2009
+ out(`${yellow("Nothing installed.")} Run ${bold("skyl scan")} to see what fits this project.`);
2010
+ return 0;
2011
+ }
2012
+ const target = resolveTarget(void 0, lock);
2013
+ const load = { dir: opts.dir, refresh: opts.refresh ?? opts.dir === void 0 };
2014
+ const { items, from } = await installedState(opts.root, lock, target, load);
2015
+ if (from === void 0) {
2016
+ out(` ${yellow("No registry reachable.")} ${dim("Nothing to update from.")}`);
2017
+ return 1;
2018
+ }
2019
+ const { skills } = await loadRegistry(load);
2020
+ const requested = Object.entries(lock.skills).filter(([, e]) => e.reason === "requested").map(([n]) => n);
2021
+ const known = new Set(skills.map((s) => s.name));
2022
+ let ordered = [];
2023
+ try {
2024
+ ordered = resolve(skills, requested.filter((n) => known.has(n)));
2025
+ } catch {
2026
+ ordered = [];
2027
+ }
2028
+ const held = new Set(Object.keys(lock.skills));
2029
+ const arrivals = ordered.filter((s) => !held.has(s.name));
2030
+ const plan = [];
2031
+ for (const item of items) {
2032
+ if (!item.skill) {
2033
+ plan.push({ skill: void 0, name: item.name, action: "gone", version: item.entry.version });
2034
+ continue;
2035
+ }
2036
+ if (item.onDisk === void 0) {
2037
+ plan.push({ skill: item.skill, name: item.name, action: "restore", version: item.skill.version });
2038
+ continue;
2039
+ }
2040
+ if (!changedOnDisk(item)) {
2041
+ continue;
2042
+ }
2043
+ if (item.state === "local-edit") {
2044
+ plan.push({ skill: item.skill, name: item.name, action: "kept", version: item.entry.version });
2045
+ continue;
2046
+ }
2047
+ if (item.state === "both") {
2048
+ plan.push({ skill: item.skill, name: item.name, action: opts.force === true ? "apply" : "conflict", version: item.skill.version });
2049
+ continue;
2050
+ }
2051
+ plan.push({ skill: item.skill, name: item.name, action: "apply", version: item.skill.version });
2052
+ }
2053
+ for (const skill of arrivals) plan.push({ skill, name: skill.name, action: "new", version: skill.version });
2054
+ const wanted = new Set(opts.names ?? []);
2055
+ const chosen = plan.filter((p) => (p.action === "apply" || p.action === "restore" || p.action === "new") && (wanted.size === 0 || wanted.has(p.name) || wanted.has(p.name.split("/")[1])));
2056
+ const stale = items.filter((i) => i.skill && i.skill.version !== i.entry.version && !changedOnDisk(i));
2057
+ if (plan.length === 0 && stale.length === 0) {
2058
+ out();
2059
+ out(` ${green("Up to date.")} ${dim(`${items.length} skills, from ${from}`)}`);
2060
+ out();
2061
+ return 0;
2062
+ }
2063
+ out();
2064
+ for (const p of plan) {
2065
+ const colour = p.action === "conflict" || p.action === "kept" || p.action === "gone" ? yellow : green;
2066
+ out(` ${bold(p.name.padEnd(22))} ${p.version.padEnd(8)} ${colour(SAYS2[p.action])}`);
2067
+ }
2068
+ out();
2069
+ if (chosen.length === 0) {
2070
+ if (plan.some((p) => p.action === "conflict")) {
2071
+ out(` ${dim("Nothing applied.")} ${cyan("skyl diff")} shows both sides, ${cyan("--force")} takes the upstream one.`);
2072
+ } else {
2073
+ out(` ${dim("Nothing to apply.")}`);
2074
+ }
2075
+ if (stale.length > 0) await recordVersions(opts.root, lock, stale.map((i) => [i.name, i.skill.version]));
2076
+ out();
2077
+ return 0;
2078
+ }
2079
+ if (opts.showDiff === true) {
2080
+ for (const item of items) {
2081
+ if (chosen.some((c) => c.name === item.name)) printDiff(item, false);
2082
+ }
2083
+ out();
2084
+ }
2085
+ const words2 = chosen.reduce((n, c) => n + (c.skill?.installableWords ?? 0), 0);
2086
+ out(` ${chosen.length} to write, ${tokens(words2)} ${dim(`into ${target.dir}`)}`);
2087
+ out();
2088
+ if (!await confirm(" Apply?", opts.yes === true)) {
2089
+ out(` ${dim("Nothing written.")}`);
2090
+ return 1;
2091
+ }
2092
+ const requestedSet = new Set(requested);
2093
+ const next = await install(
2094
+ opts.root,
2095
+ target,
2096
+ chosen.map((c) => c.skill),
2097
+ requestedSet,
2098
+ lock
2099
+ );
2100
+ for (const [name, version] of stale.map((i) => [i.name, i.skill.version])) {
2101
+ const entry = next.skills[name];
2102
+ if (entry) next.skills[name] = { ...entry, version };
2103
+ }
2104
+ await writeLockfile(opts.root, next);
2105
+ out();
2106
+ out(` ${green("Done.")} ${dim("skyl.lock records what is here now.")}`);
2107
+ out();
2108
+ return 0;
2109
+ }
2110
+ async function recordVersions(root, lock, pairs) {
2111
+ if (pairs.length === 0) return;
2112
+ const skills = { ...lock.skills };
2113
+ for (const [name, version] of pairs) {
2114
+ const entry = skills[name];
2115
+ if (entry) skills[name] = { ...entry, version };
2116
+ }
2117
+ await writeLockfile(root, { ...lock, skills });
2118
+ }
2119
+
2120
+ // src/cli.ts
2121
+ var VERSION = "0.1.2";
2122
+ var HELP = `
2123
+ ${bold("skyl")} ${dim("curated agent skills for what your project actually uses")}
2124
+
2125
+ ${dim("Install")}
2126
+ ${bold("skyl scan")} read the project and propose an install
2127
+ ${bold("skyl add")} <skill...> install skills and what they require
2128
+ ${bold("skyl remove")} <skill...> take skills back out
2129
+ ${bold("skyl list")} what is installed, and what has moved since
2130
+
2131
+ ${dim("Keep current")}
2132
+ ${bold("skyl diff")} [skill...] what changed here, and what changed upstream
2133
+ ${bold("skyl update")} [skill...] apply the upstream side, keeping local edits
2134
+
2135
+ ${dim("Your own files")}
2136
+ ${bold("skyl audit")} [file] read a hand-written CLAUDE.md and say what is in it
2137
+ ${bold("skyl context")} emit what a model needs to describe this project
2138
+ ${bold("skyl learn")} derive a project knowledge skill from this repository
2139
+
2140
+ ${dim("Authoring")}
2141
+ ${bold("skyl lint")} [path...] check skills against the spec
2142
+
2143
+ ${dim("Options")}
2144
+ --target <${TARGETS.map((t) => t.id).join("|")}>
2145
+ --dir <path> read skills from a local checkout instead
2146
+ --refresh prefer the network over the bundled copy
2147
+ --json machine-readable output (scan, context)
2148
+ --full whole file rather than hunks (diff)
2149
+ --force take the upstream side of a conflict (update)
2150
+ --strict warnings fail the run (lint)
2151
+ --dry-run list what would be sent, and send nothing (context)
2152
+ --agent <cmd> the agent to pipe a prompt to (learn)
2153
+ --print-prompt print the prompt instead of running anything (learn)
2154
+ --out <file> write to a file instead of stdout
2155
+ -y, --yes do not ask before writing
2156
+ -C <path> run as if in this directory
2157
+ -h, --help -v, --version
2158
+
2159
+ ${dim("Skills are proposed, never installed without asking.")}
2160
+ `;
2161
+ function parse(argv) {
2162
+ const positional = [];
2163
+ const flags = {};
2164
+ const takesValue = /* @__PURE__ */ new Set(["target", "dir", "C", "out", "agent", "max-files"]);
2165
+ for (let i = 0; i < argv.length; i += 1) {
2166
+ const arg = argv[i];
2167
+ if (arg === "--") {
2168
+ positional.push(...argv.slice(i + 1));
2169
+ break;
2170
+ }
2171
+ if (arg.startsWith("--")) {
2172
+ const [name, inline] = arg.slice(2).split("=", 2);
2173
+ if (inline !== void 0) flags[name] = inline;
2174
+ else if (takesValue.has(name)) {
2175
+ flags[name] = argv[i + 1] ?? "";
2176
+ i += 1;
2177
+ } else flags[name] = true;
2178
+ } else if (arg.startsWith("-") && arg.length > 1) {
2179
+ const short = arg.slice(1);
2180
+ if (takesValue.has(short)) {
2181
+ flags[short] = argv[i + 1] ?? "";
2182
+ i += 1;
2183
+ } else for (const c of short) flags[c] = true;
2184
+ } else positional.push(arg);
2185
+ }
2186
+ return { command: positional.shift(), positional, flags };
2187
+ }
2188
+ var str = (v) => typeof v === "string" && v !== "" ? v : void 0;
2189
+ var yes = (flags) => flags["yes"] === true || flags["y"] === true;
2190
+ async function run(argv) {
2191
+ const { command, positional, flags } = parse(argv);
2192
+ if (flags["version"] === true || flags["v"] === true) {
2193
+ out(`skyl ${VERSION}`);
2194
+ return 0;
2195
+ }
2196
+ if (flags["help"] === true || flags["h"] === true || command === "help") {
2197
+ out(HELP);
2198
+ return 0;
2199
+ }
2200
+ if (command === void 0) {
2201
+ out(HELP);
2202
+ return 1;
2203
+ }
2204
+ const root = str(flags["C"]) ?? process.cwd();
2205
+ const dir = str(flags["dir"]);
2206
+ const refresh = flags["refresh"] === true;
2207
+ try {
2208
+ switch (command) {
2209
+ case "scan":
2210
+ return await scan({ root, dir, refresh, json: flags["json"] === true });
2211
+ case "add":
2212
+ return await add({ root, names: positional, target: str(flags["target"]), dir, refresh, yes: yes(flags) });
2213
+ case "list":
2214
+ return await list2({ root, dir, refresh });
2215
+ case "remove":
2216
+ case "rm":
2217
+ return await remove({ root, names: positional, dir, refresh, yes: yes(flags) });
2218
+ case "diff":
2219
+ return await diff({ root, names: positional, dir, refresh, full: flags["full"] === true });
2220
+ case "update":
2221
+ case "upgrade":
2222
+ return await update({
2223
+ root,
2224
+ names: positional,
2225
+ dir,
2226
+ refresh,
2227
+ yes: yes(flags),
2228
+ force: flags["force"] === true,
2229
+ showDiff: flags["diff"] === true
2230
+ });
2231
+ case "audit":
2232
+ return await audit2({ root, file: positional[0], dir, refresh });
2233
+ case "context":
2234
+ return await context({
2235
+ root,
2236
+ dryRun: flags["dry-run"] === true,
2237
+ json: flags["json"] === true,
2238
+ outFile: str(flags["out"]),
2239
+ ...str(flags["max-files"]) === void 0 ? {} : { maxFiles: Number(str(flags["max-files"])) }
2240
+ });
2241
+ case "learn":
2242
+ return await learn({
2243
+ root,
2244
+ agent: str(flags["agent"]),
2245
+ printPrompt: flags["print-prompt"] === true,
2246
+ outFile: str(flags["out"]),
2247
+ yes: yes(flags),
2248
+ target: str(flags["target"])
2249
+ });
2250
+ case "lint":
2251
+ return await lint({ root, paths: positional, strict: flags["strict"] === true });
2252
+ default:
2253
+ err(`${red("Unknown command")} ${bold(command)}`);
2254
+ err(`Try ${cyan("skyl --help")}`);
2255
+ return 1;
2256
+ }
2257
+ } catch (cause) {
2258
+ if (cause instanceof SkylError) {
2259
+ err(`${red("Error")} ${cause.message}`);
2260
+ return 1;
2261
+ }
2262
+ throw cause;
2263
+ }
2264
+ }
2265
+
2266
+ // src/bin.ts
3
2267
  process.exitCode = await run(process.argv.slice(2));
4
- //# sourceMappingURL=bin.js.map