@quatronic/sdk 0.6.1-rc.4 → 0.7.0-rc.6

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 (40) hide show
  1. package/README.md +26 -0
  2. package/dist/cli/legacy/qdc-sourcemaps.js +237 -0
  3. package/dist/cli/legacy/qdc-test-report.js +200 -0
  4. package/dist/cli/skills-postinstall.js +603 -0
  5. package/dist/cli/skills.js +499 -0
  6. package/dist/cli/sourcemaps.js +9 -9
  7. package/dist/cli/test-report.js +2 -2
  8. package/dist/next/config/index.js +1 -1
  9. package/dist/next/config/index.js.map +1 -1
  10. package/package.json +10 -4
  11. package/postinstall.mjs +9 -0
  12. package/skills/.gitattributes +1 -0
  13. package/skills/catalog.json +24 -0
  14. package/skills/qore-add-api-route/SKILL.md +58 -0
  15. package/skills/qore-add-connection/SKILL.next.md +149 -0
  16. package/skills/qore-add-connection/SKILL.node.md +160 -0
  17. package/skills/qore-add-db-table/SKILL.md +88 -0
  18. package/skills/qore-add-db-table/references/cross-module-reference.md +74 -0
  19. package/skills/qore-add-feature/SKILL.md +77 -0
  20. package/skills/qore-add-integration/SKILL.next.md +217 -0
  21. package/skills/qore-add-integration/SKILL.node.md +216 -0
  22. package/skills/qore-add-page/SKILL.md +53 -0
  23. package/skills/qore-author-skill/SKILL.md +136 -0
  24. package/skills/qore-debug-issue/SKILL.md +48 -0
  25. package/skills/qore-enable-auth-method/SKILL.md +90 -0
  26. package/skills/qore-observability/SKILL.md +89 -0
  27. package/skills/qore-observability/references/errors.md +87 -0
  28. package/skills/qore-observability/references/logs.md +69 -0
  29. package/skills/qore-observability/references/tracing.md +58 -0
  30. package/skills/qore-recommended-tools/SKILL.md +58 -0
  31. package/skills/qore-secure-io/SKILL.md +174 -0
  32. package/skills/qore-ship-checklist/SKILL.md +80 -0
  33. package/skills/qore-translate-strings/SKILL.md +36 -0
  34. package/skills/qore-use-addons/SKILL.md +175 -0
  35. package/skills/qore-use-database/SKILL.md +222 -0
  36. package/skills/qore-use-design-system/SKILL.md +76 -0
  37. package/skills/qore-use-mail-domain/SKILL.next.md +67 -0
  38. package/skills/qore-use-mail-domain/SKILL.node.md +62 -0
  39. package/skills/qore-use-permissions/SKILL.md +116 -0
  40. package/skills/qore-verify-user/SKILL.md +56 -0
@@ -0,0 +1,603 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/skills-postinstall.ts
4
+ import { execFileSync } from "child_process";
5
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
6
+ import { dirname as dirname2, join as join2, resolve as resolve2, sep } from "path";
7
+ import { fileURLToPath as fileURLToPath2 } from "url";
8
+
9
+ // src/cli/skills-core.ts
10
+ import { createHash } from "crypto";
11
+ var MANIFEST_PATH = "qore/manifest.json";
12
+ var INDEX_PATH = "qore/skills.md";
13
+ var README_PATH = "qore/README.md";
14
+ var EXTENSIONS_DIR = "qore/extensions";
15
+ var CLAUDE_MD_IMPORT = "@.claude/qore/skills.md";
16
+ var RUNTIMES = ["next", "node"];
17
+ var AUTHS = ["none", "keycloak", "betterauth"];
18
+ function parseFeatures(raw) {
19
+ const o = raw ?? {};
20
+ if (!RUNTIMES.includes(o.runtime)) throw new Error(`features.runtime must be one of ${RUNTIMES.join("|")}`);
21
+ if (!AUTHS.includes(o.auth)) throw new Error(`features.auth must be one of ${AUTHS.join("|")}`);
22
+ if (typeof o.db !== "boolean") throw new Error("features.db must be a boolean");
23
+ return { runtime: o.runtime, auth: o.auth, db: o.db };
24
+ }
25
+ function inferFeatures(pkg, exists) {
26
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
27
+ const runtime = "next" in deps ? "next" : "node";
28
+ const auth = "better-auth" in deps ? "betterauth" : exists("src/lib/permissions.ts") ? "keycloak" : "none";
29
+ const db = "drizzle-orm" in deps || Boolean(pkg.scripts?.["db:up"]) || exists("docker-compose.yml");
30
+ return { runtime, auth, db };
31
+ }
32
+ function evalCondition(expr, f) {
33
+ const e = expr.trim();
34
+ if (e.startsWith("!")) return !evalCondition(e.slice(1), f);
35
+ const [key, value] = e.split("=", 2);
36
+ const options = value === void 0 ? void 0 : value.split("|");
37
+ switch (key) {
38
+ case "runtime":
39
+ if (!options) throw new Error(`condition "${e}" needs a value`);
40
+ return options.includes(f.runtime);
41
+ case "auth":
42
+ return options ? options.includes(f.auth) : f.auth !== "none";
43
+ case "db":
44
+ if (options) throw new Error(`condition "${e}" takes no value`);
45
+ return f.db;
46
+ default:
47
+ throw new Error(`unknown condition "${e}"`);
48
+ }
49
+ }
50
+ var TAG = /\{\{(#if [^}]+|else|\/if)\}\}/g;
51
+ var LINE_START_TAG = /^[ \t]+(?=\{\{(?:#if [^}]+|else|\/if)\}\})/gm;
52
+ function renderTemplate(src, f) {
53
+ const text = src.replace(/\r\n/g, "\n").replace(LINE_START_TAG, "");
54
+ const root = [];
55
+ const stack = [];
56
+ const push = (n) => {
57
+ const top = stack.at(-1);
58
+ if (!top) root.push(n);
59
+ else (top.inElse ? top.node.else : top.node.then).push(n);
60
+ };
61
+ let last = 0;
62
+ for (const m of text.matchAll(TAG)) {
63
+ push(text.slice(last, m.index));
64
+ last = m.index + m[0].length;
65
+ if (text[last] === "\n") last += 1;
66
+ const tag = m[1];
67
+ if (tag.startsWith("#if ")) {
68
+ const cond = tag.slice(4).trim();
69
+ evalCondition(cond, { runtime: "next", auth: "none", db: false });
70
+ const node = { cond, then: [], else: [] };
71
+ push(node);
72
+ stack.push({ node, inElse: false });
73
+ } else if (tag === "else") {
74
+ const top = stack.at(-1);
75
+ if (!top || top.inElse) throw new Error("{{else}} without a matching {{#if}}");
76
+ top.inElse = true;
77
+ } else {
78
+ if (!stack.pop()) throw new Error("{{/if}} without a matching {{#if}}");
79
+ }
80
+ }
81
+ if (stack.length) throw new Error(`unclosed {{#if ${stack.at(-1).node.cond}}}`);
82
+ push(text.slice(last));
83
+ const emit = (nodes) => nodes.map((n) => typeof n === "string" ? n : emit(evalCondition(n.cond, f) ? n.then : n.else)).join("");
84
+ return emit(root);
85
+ }
86
+ var VARIANT = /^(.*)\.(next|node)(\.[^./]+)$/;
87
+ function skillApplies(entry, f) {
88
+ return (entry.requires ?? []).every((c) => evalCondition(c, f));
89
+ }
90
+ function renderSkills(catalog, sources, f) {
91
+ const out = /* @__PURE__ */ new Map();
92
+ for (const entry of catalog.skills) {
93
+ if (!skillApplies(entry, f)) continue;
94
+ const prefix = `${entry.name}/`;
95
+ for (const [key, src] of [...sources].sort(([a], [b]) => a.localeCompare(b))) {
96
+ if (!key.startsWith(prefix)) continue;
97
+ let rel = key.slice(prefix.length);
98
+ const v = VARIANT.exec(rel);
99
+ if (v) {
100
+ if (v[2] !== f.runtime) continue;
101
+ rel = `${v[1]}${v[3]}`;
102
+ }
103
+ out.set(`skills/${entry.name}/${rel}`, renderTemplate(src, f));
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ function frontmatterField(text, field) {
109
+ const fm = /^---\n([\s\S]*?)\n---\n/.exec(text);
110
+ if (!fm) return void 0;
111
+ const line = fm[1].split("\n").find((l) => l.startsWith(`${field}:`));
112
+ return line?.slice(field.length + 1).trim();
113
+ }
114
+ function extensionFooter(entry) {
115
+ const path = `.claude/${EXTENSIONS_DIR}/${entry.name}.md`;
116
+ const rule = entry.kind === "contract" ? "It may add steps and conventions to this skill but cannot relax it: the portal reads what this skill produces." : "Where it conflicts with this skill, it wins.";
117
+ return `
118
+ ---
119
+
120
+ *Managed by \`@quatronic/sdk\` \u2014 do not edit this file; the next SDK update overwrites it.*
121
+ If \`${path}\` exists, read it before you start: it is this app team's extension of the skill. ${rule}
122
+ `;
123
+ }
124
+ function renderIndex(catalog, skills) {
125
+ const rows = catalog.skills.filter((e) => skills.has(`skills/${e.name}/SKILL.md`)).map((e) => {
126
+ const desc = frontmatterField(skills.get(`skills/${e.name}/SKILL.md`), "description") ?? "";
127
+ return `| \`/${e.name}\` | ${e.kind} | ${desc.replace(/\|/g, "\\|")} |`;
128
+ });
129
+ return [
130
+ "# Qore skills",
131
+ "",
132
+ "Managed by `@quatronic/sdk`; see `.claude/qore/README.md` for how to disable, extend or detach one.",
133
+ "",
134
+ "**Never edit `.claude/skills/qore-*` or anything under `.claude/qore/` except `extensions/`** \u2014 the next SDK update overwrites it. To add, change, extend or turn off a skill, use `/qore-author-skill`.",
135
+ "",
136
+ "| Skill | Kind | When to use |",
137
+ "|---|---|---|",
138
+ ...rows,
139
+ ""
140
+ ].join("\n");
141
+ }
142
+ var README = `# Managed Qore skills
143
+
144
+ The skills under \`.claude/skills/qore-*\` come from \`@quatronic/sdk\` and are
145
+ updated with it: an SDK upgrade PR re-syncs them (\`pnpm exec qore-skills sync\`).
146
+ \`manifest.json\` here records the SDK version, this app's features and a hash
147
+ of every managed file.
148
+
149
+ ## What you can do
150
+
151
+ | You want to\u2026 | Do this |
152
+ |---|---|
153
+ | Turn a skill off | \`"skillOverrides": { "qore-add-page": "off" }\` in \`.claude/settings.json\` |
154
+ | Add your own conventions | Write \`.claude/qore/extensions/<skill>.md\`; the skill tells Claude to read it |
155
+ | Replace a skill | Turn ours off and write your own skill under a name without the \`qore-\` prefix |
156
+ | Own a copy | Add the skill's name to \`detached\` in \`manifest.json\`; sync stops touching it |
157
+
158
+ \`/qore-author-skill\` walks through each of these.
159
+
160
+ Editing a managed file in place does not stick: the next sync overwrites it and
161
+ lists it in the PR, so the change is still in that PR's diff to move into an
162
+ extension file.
163
+
164
+ ## Contract skills
165
+
166
+ \`qore-add-integration\`, \`qore-add-connection\`, \`qore-use-permissions\`,
167
+ \`qore-observability\` and \`qore-use-database\` produce code the portal reads:
168
+ the Integrations tab, connections, roles, Monitoring, the local database. An
169
+ extension may add to them but not relax them, and turning one off does not turn
170
+ off the SDK's lint rules that check the same things.
171
+
172
+ The \`qore-\` prefix is reserved for managed skills. Sync never touches a skill
173
+ it does not ship, so your own skills are safe under any other name.
174
+ `;
175
+ function buildTree(catalog, sources, f) {
176
+ const skills = renderSkills(catalog, sources, f);
177
+ for (const entry of catalog.skills) {
178
+ const key = `skills/${entry.name}/SKILL.md`;
179
+ const body = skills.get(key);
180
+ if (body !== void 0) skills.set(key, body.replace(/\n*$/, "\n") + extensionFooter(entry));
181
+ }
182
+ skills.set(INDEX_PATH, renderIndex(catalog, skills));
183
+ skills.set(README_PATH, README);
184
+ return skills;
185
+ }
186
+ function normalise(text) {
187
+ return text.replace(/\r\n/g, "\n");
188
+ }
189
+ function hashContent(text) {
190
+ return `sha256:${createHash("sha256").update(normalise(text), "utf8").digest("hex")}`;
191
+ }
192
+ function parseManifest(text) {
193
+ const raw = JSON.parse(text);
194
+ if (raw.schemaVersion !== 1) throw new Error(`unsupported manifest schemaVersion ${String(raw.schemaVersion)}`);
195
+ return {
196
+ schemaVersion: 1,
197
+ sdkVersion: String(raw.sdkVersion ?? ""),
198
+ features: parseFeatures(raw.features),
199
+ ...raw.featureOverride === void 0 ? {} : { featureOverride: parseFeatures(raw.featureOverride) },
200
+ detached: Array.isArray(raw.detached) ? raw.detached.map(String) : [],
201
+ files: raw.files ?? {}
202
+ };
203
+ }
204
+ function renderManifest(m) {
205
+ const files = Object.fromEntries(Object.entries(m.files).sort(([a], [b]) => a.localeCompare(b)));
206
+ return `${JSON.stringify({ ...m, files }, null, 2)}
207
+ `;
208
+ }
209
+ var skillOf = (rel) => /^skills\/([^/]+)\//.exec(rel)?.[1];
210
+ function planSync(s) {
211
+ const detached = new Set(s.manifest?.detached ?? []);
212
+ const managed = (rel) => {
213
+ const skill = skillOf(rel);
214
+ return !(skill && detached.has(skill));
215
+ };
216
+ const tree = new Map([...s.tree].filter(([rel]) => managed(rel)));
217
+ const known = s.manifest?.files ?? {};
218
+ const writes = /* @__PURE__ */ new Map();
219
+ const edited = [];
220
+ const adopted = [];
221
+ for (const [rel, content] of tree) {
222
+ const disk = s.read(rel);
223
+ if (disk !== void 0 && normalise(disk) === content) continue;
224
+ if (disk !== void 0) {
225
+ if (!(rel in known)) adopted.push(rel);
226
+ else if (hashContent(disk) !== known[rel]) edited.push(rel);
227
+ }
228
+ writes.set(rel, content);
229
+ }
230
+ const deletes = [];
231
+ for (const rel of Object.keys(known)) {
232
+ if (tree.has(rel) || !managed(rel) || s.read(rel) === void 0) continue;
233
+ deletes.push(rel);
234
+ if (hashContent(s.read(rel)) !== known[rel]) edited.push(rel);
235
+ }
236
+ const legacyRemoved = [];
237
+ for (const entry of s.catalog.skills) {
238
+ if (detached.has(entry.name)) continue;
239
+ for (const legacy of entry.legacyNames ?? []) {
240
+ const files = s.listDir(`skills/${legacy}`);
241
+ if (!files.length) continue;
242
+ legacyRemoved.push(`skills/${legacy}`);
243
+ deletes.push(...files);
244
+ }
245
+ }
246
+ let claudeMd = s.claudeMd;
247
+ const claudeMdRenamed = [];
248
+ let claudeMdImportAdded = false;
249
+ if (claudeMd !== void 0) {
250
+ for (const entry of s.catalog.skills) {
251
+ for (const legacy of entry.legacyNames ?? []) {
252
+ const exact = new RegExp(`(?<![a-z0-9-])${legacy}(?![a-z0-9-])`, "g");
253
+ if (!exact.test(claudeMd)) continue;
254
+ claudeMd = claudeMd.replace(exact, entry.name);
255
+ claudeMdRenamed.push([legacy, entry.name]);
256
+ }
257
+ }
258
+ if (!claudeMd.split(/\r?\n/).some((l) => l.trim() === CLAUDE_MD_IMPORT)) {
259
+ claudeMd = `${claudeMd.replace(/\s*$/, "")}
260
+
261
+ ${CLAUDE_MD_IMPORT}
262
+ `;
263
+ claudeMdImportAdded = true;
264
+ }
265
+ }
266
+ const manifest = {
267
+ schemaVersion: 1,
268
+ sdkVersion: s.sdkVersion,
269
+ features: s.features,
270
+ ...s.featureOverride ? { featureOverride: s.featureOverride } : {},
271
+ detached: [...detached].sort(),
272
+ files: Object.fromEntries([...tree].map(([rel, c]) => [rel, hashContent(c)]))
273
+ };
274
+ const manifestText = renderManifest(manifest);
275
+ if (s.read(MANIFEST_PATH) === void 0 || normalise(s.read(MANIFEST_PATH)) !== manifestText) {
276
+ writes.set(MANIFEST_PATH, manifestText);
277
+ }
278
+ return {
279
+ writes,
280
+ deletes: deletes.sort(),
281
+ edited: edited.sort(),
282
+ adopted: adopted.sort(),
283
+ legacyRemoved: legacyRemoved.sort(),
284
+ manifest,
285
+ claudeMd: claudeMd === s.claudeMd ? void 0 : claudeMd,
286
+ claudeMdRenamed: claudeMdRenamed.sort(([a], [b]) => a.localeCompare(b)),
287
+ claudeMdImportAdded
288
+ };
289
+ }
290
+ function planChanges(p) {
291
+ return p.writes.size > 0 || p.deletes.length > 0 || p.claudeMd !== void 0;
292
+ }
293
+ function renderReport(p, opts) {
294
+ const f = p.manifest.features;
295
+ const lines = [
296
+ "### Qore skills",
297
+ "",
298
+ `Synced the managed skills for \`@quatronic/sdk@${p.manifest.sdkVersion}\` (runtime \`${f.runtime}\`, auth \`${f.auth}\`, database \`${f.db ? "yes" : "no"}\`${opts.inferred ? ", inferred from the app \u2014 pin with `qore-skills sync --features`" : ", pinned in the manifest"}).`
299
+ ];
300
+ const managedWrites = [...p.writes.keys()].filter((r) => r !== MANIFEST_PATH);
301
+ if (!managedWrites.length && !p.deletes.length && p.claudeMd === void 0) {
302
+ lines.push("", "No skill content changed.");
303
+ }
304
+ const section = (title, items, note) => {
305
+ if (!items.length) return;
306
+ lines.push("", `**${title}** ${note}`, "", ...items.map((i) => `- \`.claude/${i}\``));
307
+ };
308
+ section(
309
+ "Hand-edited managed files, overwritten",
310
+ p.edited,
311
+ "(the edit is in this change's diff; to keep it, move it into `.claude/qore/extensions/<skill>.md`):"
312
+ );
313
+ section("Existing files now managed", p.adopted, "(written by an older template; check the diff for local changes):");
314
+ section(
315
+ "Legacy skill directories removed",
316
+ p.legacyRemoved,
317
+ "(replaced by their `qore-*` names; a local edit to one is in this change's diff):"
318
+ );
319
+ if (p.claudeMd !== void 0) {
320
+ lines.push("", "**`CLAUDE.md`:**", "");
321
+ for (const [from, to] of p.claudeMdRenamed) lines.push(`- Renamed \`${from}\` to \`${to}\`.`);
322
+ if (p.claudeMdImportAdded) lines.push(`- Imports \`${CLAUDE_MD_IMPORT}\`, the index of managed skills.`);
323
+ }
324
+ return `${lines.join("\n")}
325
+ `;
326
+ }
327
+
328
+ // src/cli/skills.ts
329
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "fs";
330
+ import { dirname, join, resolve } from "path";
331
+ import { fileURLToPath } from "url";
332
+ var USAGE = `usage:
333
+ qore-skills sync [--cwd <app>] [--features <json>|infer] [--report <file.md>]
334
+ qore-skills check [--cwd <app>] [--json]
335
+ qore-skills render --features <json> --out <dir>`;
336
+ var PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
337
+ function parseArgs(argv) {
338
+ const [cmd, ...rest] = argv;
339
+ if (cmd !== "sync" && cmd !== "check" && cmd !== "render") return { error: USAGE };
340
+ const args = { cmd, cwd: process.cwd(), json: false };
341
+ for (let i = 0; i < rest.length; i++) {
342
+ const flag = rest[i];
343
+ const value = () => {
344
+ const v = rest[++i];
345
+ if (v === void 0) throw new Error(`${flag} needs a value`);
346
+ return v;
347
+ };
348
+ try {
349
+ if (flag === "--cwd") args.cwd = resolve(value());
350
+ else if (flag === "--features") {
351
+ const v = value();
352
+ args.features = v === "infer" ? "infer" : parseFeatures(JSON.parse(v));
353
+ } else if (flag === "--report") args.report = resolve(value());
354
+ else if (flag === "--out") args.out = resolve(value());
355
+ else if (flag === "--json") args.json = true;
356
+ else return { error: `unknown option ${flag}
357
+ ${USAGE}` };
358
+ } catch (err) {
359
+ return { error: err instanceof Error ? err.message : String(err) };
360
+ }
361
+ }
362
+ if (cmd === "render" && (!args.features || args.features === "infer" || !args.out)) return { error: `render needs --features and --out
363
+ ${USAGE}` };
364
+ return args;
365
+ }
366
+ function walk(root, prefix = "") {
367
+ const dir = join(root, prefix);
368
+ if (!existsSync(dir)) return [];
369
+ return readdirSync(dir, { withFileTypes: true }).flatMap((d) => {
370
+ const rel = prefix ? `${prefix}/${d.name}` : d.name;
371
+ return d.isDirectory() ? walk(root, rel) : d.isFile() ? [rel] : [];
372
+ });
373
+ }
374
+ function loadSources(root = join(PACKAGE_ROOT, "skills")) {
375
+ const catalog = JSON.parse(readFileSync(join(root, "catalog.json"), "utf8"));
376
+ const sources = new Map(
377
+ walk(root).filter((rel) => rel !== "catalog.json").map((rel) => [rel, readFileSync(join(root, rel), "utf8").replace(/\r\n/g, "\n")])
378
+ );
379
+ return { catalog, sources };
380
+ }
381
+ function sdkVersion() {
382
+ return JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
383
+ }
384
+ function readOptional(path) {
385
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
386
+ }
387
+ function writeFile(path, text) {
388
+ mkdirSync(dirname(path), { recursive: true });
389
+ writeFileSync(path, text);
390
+ }
391
+ function pruneEmpty(root, rel, stop) {
392
+ let dir = dirname(join(root, rel));
393
+ while (dir.startsWith(join(root, stop)) && dir !== join(root, stop)) {
394
+ if (!existsSync(dir) || readdirSync(dir).length) return;
395
+ rmdirSync(dir);
396
+ dir = dirname(dir);
397
+ }
398
+ }
399
+ function plan(app, override) {
400
+ const claude = join(app, ".claude");
401
+ const manifestText = readOptional(join(claude, MANIFEST_PATH));
402
+ const manifest = manifestText ? parseManifest(manifestText) : null;
403
+ const featureOverride = override === "infer" ? void 0 : override ?? manifest?.featureOverride;
404
+ const pkg = JSON.parse(readOptional(join(app, "package.json")) ?? "{}");
405
+ const features = featureOverride ?? inferFeatures(pkg, (rel) => existsSync(join(app, rel)));
406
+ const inferred = !featureOverride;
407
+ const { catalog, sources } = loadSources();
408
+ return {
409
+ inferred,
410
+ plan: planSync({
411
+ catalog,
412
+ tree: buildTree(catalog, sources, features),
413
+ manifest,
414
+ sdkVersion: sdkVersion(),
415
+ features,
416
+ featureOverride,
417
+ read: (rel) => readOptional(join(claude, rel)),
418
+ listDir: (rel) => walk(join(claude, rel)).map((f) => `${rel}/${f}`),
419
+ claudeMd: readOptional(join(app, "CLAUDE.md"))
420
+ })
421
+ };
422
+ }
423
+ function apply(app, p) {
424
+ const claude = join(app, ".claude");
425
+ for (const rel of p.deletes) {
426
+ rmSync(join(claude, rel), { force: true });
427
+ pruneEmpty(claude, rel, "skills");
428
+ }
429
+ for (const [rel, text] of p.writes) writeFile(join(claude, rel), text);
430
+ if (p.claudeMd !== void 0) writeFileSync(join(app, "CLAUDE.md"), p.claudeMd);
431
+ }
432
+ function render(features, out) {
433
+ const { catalog, sources } = loadSources();
434
+ const tree = buildTree(catalog, sources, features);
435
+ for (const [rel, text] of tree) writeFile(join(out, rel), text);
436
+ const manifest = {
437
+ schemaVersion: 1,
438
+ sdkVersion: sdkVersion(),
439
+ features,
440
+ detached: [],
441
+ files: Object.fromEntries([...tree].map(([rel, c]) => [rel, hashContent(c)]))
442
+ };
443
+ writeFile(join(out, MANIFEST_PATH), renderManifest(manifest));
444
+ }
445
+ function main(argv = process.argv.slice(2)) {
446
+ const args = parseArgs(argv);
447
+ if ("error" in args) {
448
+ console.error(`qore-skills: ${args.error}`);
449
+ return 2;
450
+ }
451
+ if (args.cmd === "render") {
452
+ render(args.features, args.out);
453
+ console.log(`qore-skills: rendered into ${args.out}`);
454
+ return 0;
455
+ }
456
+ const { plan: p, inferred } = plan(args.cwd, args.features);
457
+ const report = renderReport(p, { inferred });
458
+ if (args.cmd === "check") {
459
+ const changed = planChanges(p);
460
+ if (args.json) {
461
+ console.log(
462
+ JSON.stringify({
463
+ upToDate: !changed,
464
+ writes: [...p.writes.keys()],
465
+ deletes: p.deletes,
466
+ edited: p.edited,
467
+ claudeMd: p.claudeMd !== void 0
468
+ })
469
+ );
470
+ } else {
471
+ console.log(changed ? report : "qore-skills: up to date");
472
+ }
473
+ return changed ? 1 : 0;
474
+ }
475
+ apply(args.cwd, p);
476
+ if (args.report) writeFile(args.report, report);
477
+ console.log(report);
478
+ return 0;
479
+ }
480
+ function isMainModule() {
481
+ if (!process.argv[1]) return false;
482
+ try {
483
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
488
+ if (isMainModule()) {
489
+ try {
490
+ process.exitCode = main();
491
+ } catch (err) {
492
+ console.error(`qore-skills: ${err instanceof Error ? err.message : String(err)}`);
493
+ process.exitCode = 2;
494
+ }
495
+ }
496
+
497
+ // src/cli/skills-postinstall.ts
498
+ var SDK = "@quatronic/sdk";
499
+ function dependsOnSdk(dir) {
500
+ const path = join2(dir, "package.json");
501
+ if (!existsSync2(path)) return false;
502
+ if (existsSync2(join2(dir, "node_modules", ...SDK.split("/"), "package.json"))) return true;
503
+ try {
504
+ const pkg = JSON.parse(readFileSync2(path, "utf8"));
505
+ return Boolean(pkg.dependencies?.[SDK] ?? pkg.devDependencies?.[SDK]);
506
+ } catch {
507
+ return false;
508
+ }
509
+ }
510
+ function findApp(start) {
511
+ let dir = resolve2(start);
512
+ for (; ; ) {
513
+ if (dependsOnSdk(dir)) return dir;
514
+ const parent = dirname2(dir);
515
+ if (parent === dir) return void 0;
516
+ dir = parent;
517
+ }
518
+ }
519
+ function uncommittedEdits(p, manifestFiles, dirty, read) {
520
+ const touched = [...p.writes.keys(), ...p.deletes].filter((rel) => rel !== MANIFEST_PATH);
521
+ return touched.filter((rel) => dirty.has(`.claude/${rel}`)).filter((rel) => {
522
+ const disk = read(rel);
523
+ return disk !== void 0 && hashContent(disk) !== manifestFiles[rel];
524
+ }).map((rel) => `.claude/${rel}`).sort();
525
+ }
526
+ function runPostinstall(e) {
527
+ try {
528
+ if (e.env.QORE_SKILLS_SYNC === "0") return { kind: "disabled" };
529
+ if (e.env.CI) return { kind: "ci" };
530
+ if (!e.self.split(sep).includes("node_modules")) return { kind: "not-installed" };
531
+ const app = findApp(e.env.INIT_CWD ?? process.cwd());
532
+ if (!app) return { kind: "no-app" };
533
+ if (!existsSync2(join2(app, ".git"))) return { kind: "not-a-checkout" };
534
+ const manifestPath = join2(app, ".claude", MANIFEST_PATH);
535
+ const manifest = existsSync2(manifestPath) ? parseManifest(readFileSync2(manifestPath, "utf8")) : null;
536
+ if (manifest?.sdkVersion === e.sdkVersion) return { kind: "up-to-date" };
537
+ const { plan: p, inferred } = plan(app);
538
+ const dirty = e.gitDirty(app);
539
+ if (!dirty) return { kind: "failed", message: "git is not available to check for uncommitted edits" };
540
+ const read = (rel) => {
541
+ const path = join2(app, ".claude", rel);
542
+ return existsSync2(path) ? readFileSync2(path, "utf8") : void 0;
543
+ };
544
+ const paths = uncommittedEdits(p, manifest?.files ?? {}, dirty, read);
545
+ if (paths.length) return { kind: "dirty", app, paths };
546
+ apply(app, p);
547
+ return { kind: "synced", app, report: renderReport(p, { inferred }) };
548
+ } catch (err) {
549
+ return { kind: "failed", message: err instanceof Error ? err.message : String(err) };
550
+ }
551
+ }
552
+ function parseStatusZ(out) {
553
+ const paths = /* @__PURE__ */ new Set();
554
+ const entries = out.split("\0");
555
+ for (let i = 0; i < entries.length; i++) {
556
+ const entry = entries[i];
557
+ if (entry.length < 4) continue;
558
+ paths.add(entry.slice(3));
559
+ if (entry[0] === "R" || entry[0] === "C") paths.add(entries[++i] ?? "");
560
+ }
561
+ return paths;
562
+ }
563
+ function gitDirty(app) {
564
+ try {
565
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: app, encoding: "utf8" }).trim();
566
+ const out = execFileSync("git", ["status", "--porcelain", "-z", "--untracked-files=all", "--", "."], {
567
+ cwd: app,
568
+ encoding: "utf8"
569
+ });
570
+ const prefix = resolve2(app).slice(resolve2(top).length).replace(/^[\\/]/, "").replace(/\\/g, "/");
571
+ return new Set([...parseStatusZ(out)].map((p) => prefix && p.startsWith(`${prefix}/`) ? p.slice(prefix.length + 1) : p));
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ function message(o) {
577
+ switch (o.kind) {
578
+ case "synced":
579
+ return `${o.report}
580
+ qore-skills: commit .claude/ and CLAUDE.md together with this SDK bump.`;
581
+ case "dirty":
582
+ return "qore-skills: managed skills were NOT synced to this SDK version, because these files hold uncommitted edits:\n" + o.paths.map((p) => ` ${p}`).join("\n") + "\nMove each edit into .claude/qore/extensions/<skill>.md (see /qore-author-skill), then run `pnpm exec qore-skills sync`.";
583
+ case "failed":
584
+ return `qore-skills: skipped syncing managed skills (${o.message}). Run \`pnpm exec qore-skills sync\`.`;
585
+ default:
586
+ return void 0;
587
+ }
588
+ }
589
+ function main2() {
590
+ const self = realpathSync2(fileURLToPath2(import.meta.url));
591
+ const pkg = join2(dirname2(self), "..", "..", "package.json");
592
+ const sdkVersion2 = JSON.parse(readFileSync2(pkg, "utf8")).version;
593
+ const text = message(runPostinstall({ env: process.env, self, sdkVersion: sdkVersion2, gitDirty }));
594
+ if (text) console.error(text);
595
+ }
596
+ export {
597
+ findApp,
598
+ main2 as main,
599
+ message,
600
+ parseStatusZ,
601
+ runPostinstall,
602
+ uncommittedEdits
603
+ };