@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,499 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/skills.ts
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "fs";
5
+ import { dirname, join, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/cli/skills-core.ts
9
+ import { createHash } from "crypto";
10
+ var MANIFEST_PATH = "qore/manifest.json";
11
+ var INDEX_PATH = "qore/skills.md";
12
+ var README_PATH = "qore/README.md";
13
+ var EXTENSIONS_DIR = "qore/extensions";
14
+ var CLAUDE_MD_IMPORT = "@.claude/qore/skills.md";
15
+ var RUNTIMES = ["next", "node"];
16
+ var AUTHS = ["none", "keycloak", "betterauth"];
17
+ function parseFeatures(raw) {
18
+ const o = raw ?? {};
19
+ if (!RUNTIMES.includes(o.runtime)) throw new Error(`features.runtime must be one of ${RUNTIMES.join("|")}`);
20
+ if (!AUTHS.includes(o.auth)) throw new Error(`features.auth must be one of ${AUTHS.join("|")}`);
21
+ if (typeof o.db !== "boolean") throw new Error("features.db must be a boolean");
22
+ return { runtime: o.runtime, auth: o.auth, db: o.db };
23
+ }
24
+ function inferFeatures(pkg, exists) {
25
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
26
+ const runtime = "next" in deps ? "next" : "node";
27
+ const auth = "better-auth" in deps ? "betterauth" : exists("src/lib/permissions.ts") ? "keycloak" : "none";
28
+ const db = "drizzle-orm" in deps || Boolean(pkg.scripts?.["db:up"]) || exists("docker-compose.yml");
29
+ return { runtime, auth, db };
30
+ }
31
+ function evalCondition(expr, f) {
32
+ const e = expr.trim();
33
+ if (e.startsWith("!")) return !evalCondition(e.slice(1), f);
34
+ const [key, value] = e.split("=", 2);
35
+ const options = value === void 0 ? void 0 : value.split("|");
36
+ switch (key) {
37
+ case "runtime":
38
+ if (!options) throw new Error(`condition "${e}" needs a value`);
39
+ return options.includes(f.runtime);
40
+ case "auth":
41
+ return options ? options.includes(f.auth) : f.auth !== "none";
42
+ case "db":
43
+ if (options) throw new Error(`condition "${e}" takes no value`);
44
+ return f.db;
45
+ default:
46
+ throw new Error(`unknown condition "${e}"`);
47
+ }
48
+ }
49
+ var TAG = /\{\{(#if [^}]+|else|\/if)\}\}/g;
50
+ var LINE_START_TAG = /^[ \t]+(?=\{\{(?:#if [^}]+|else|\/if)\}\})/gm;
51
+ function renderTemplate(src, f) {
52
+ const text = src.replace(/\r\n/g, "\n").replace(LINE_START_TAG, "");
53
+ const root = [];
54
+ const stack = [];
55
+ const push = (n) => {
56
+ const top = stack.at(-1);
57
+ if (!top) root.push(n);
58
+ else (top.inElse ? top.node.else : top.node.then).push(n);
59
+ };
60
+ let last = 0;
61
+ for (const m of text.matchAll(TAG)) {
62
+ push(text.slice(last, m.index));
63
+ last = m.index + m[0].length;
64
+ if (text[last] === "\n") last += 1;
65
+ const tag = m[1];
66
+ if (tag.startsWith("#if ")) {
67
+ const cond = tag.slice(4).trim();
68
+ evalCondition(cond, { runtime: "next", auth: "none", db: false });
69
+ const node = { cond, then: [], else: [] };
70
+ push(node);
71
+ stack.push({ node, inElse: false });
72
+ } else if (tag === "else") {
73
+ const top = stack.at(-1);
74
+ if (!top || top.inElse) throw new Error("{{else}} without a matching {{#if}}");
75
+ top.inElse = true;
76
+ } else {
77
+ if (!stack.pop()) throw new Error("{{/if}} without a matching {{#if}}");
78
+ }
79
+ }
80
+ if (stack.length) throw new Error(`unclosed {{#if ${stack.at(-1).node.cond}}}`);
81
+ push(text.slice(last));
82
+ const emit = (nodes) => nodes.map((n) => typeof n === "string" ? n : emit(evalCondition(n.cond, f) ? n.then : n.else)).join("");
83
+ return emit(root);
84
+ }
85
+ var VARIANT = /^(.*)\.(next|node)(\.[^./]+)$/;
86
+ function skillApplies(entry, f) {
87
+ return (entry.requires ?? []).every((c) => evalCondition(c, f));
88
+ }
89
+ function renderSkills(catalog, sources, f) {
90
+ const out = /* @__PURE__ */ new Map();
91
+ for (const entry of catalog.skills) {
92
+ if (!skillApplies(entry, f)) continue;
93
+ const prefix = `${entry.name}/`;
94
+ for (const [key, src] of [...sources].sort(([a], [b]) => a.localeCompare(b))) {
95
+ if (!key.startsWith(prefix)) continue;
96
+ let rel = key.slice(prefix.length);
97
+ const v = VARIANT.exec(rel);
98
+ if (v) {
99
+ if (v[2] !== f.runtime) continue;
100
+ rel = `${v[1]}${v[3]}`;
101
+ }
102
+ out.set(`skills/${entry.name}/${rel}`, renderTemplate(src, f));
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ function frontmatterField(text, field) {
108
+ const fm = /^---\n([\s\S]*?)\n---\n/.exec(text);
109
+ if (!fm) return void 0;
110
+ const line = fm[1].split("\n").find((l) => l.startsWith(`${field}:`));
111
+ return line?.slice(field.length + 1).trim();
112
+ }
113
+ function extensionFooter(entry) {
114
+ const path = `.claude/${EXTENSIONS_DIR}/${entry.name}.md`;
115
+ 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.";
116
+ return `
117
+ ---
118
+
119
+ *Managed by \`@quatronic/sdk\` \u2014 do not edit this file; the next SDK update overwrites it.*
120
+ If \`${path}\` exists, read it before you start: it is this app team's extension of the skill. ${rule}
121
+ `;
122
+ }
123
+ function renderIndex(catalog, skills) {
124
+ const rows = catalog.skills.filter((e) => skills.has(`skills/${e.name}/SKILL.md`)).map((e) => {
125
+ const desc = frontmatterField(skills.get(`skills/${e.name}/SKILL.md`), "description") ?? "";
126
+ return `| \`/${e.name}\` | ${e.kind} | ${desc.replace(/\|/g, "\\|")} |`;
127
+ });
128
+ return [
129
+ "# Qore skills",
130
+ "",
131
+ "Managed by `@quatronic/sdk`; see `.claude/qore/README.md` for how to disable, extend or detach one.",
132
+ "",
133
+ "**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`.",
134
+ "",
135
+ "| Skill | Kind | When to use |",
136
+ "|---|---|---|",
137
+ ...rows,
138
+ ""
139
+ ].join("\n");
140
+ }
141
+ var README = `# Managed Qore skills
142
+
143
+ The skills under \`.claude/skills/qore-*\` come from \`@quatronic/sdk\` and are
144
+ updated with it: an SDK upgrade PR re-syncs them (\`pnpm exec qore-skills sync\`).
145
+ \`manifest.json\` here records the SDK version, this app's features and a hash
146
+ of every managed file.
147
+
148
+ ## What you can do
149
+
150
+ | You want to\u2026 | Do this |
151
+ |---|---|
152
+ | Turn a skill off | \`"skillOverrides": { "qore-add-page": "off" }\` in \`.claude/settings.json\` |
153
+ | Add your own conventions | Write \`.claude/qore/extensions/<skill>.md\`; the skill tells Claude to read it |
154
+ | Replace a skill | Turn ours off and write your own skill under a name without the \`qore-\` prefix |
155
+ | Own a copy | Add the skill's name to \`detached\` in \`manifest.json\`; sync stops touching it |
156
+
157
+ \`/qore-author-skill\` walks through each of these.
158
+
159
+ Editing a managed file in place does not stick: the next sync overwrites it and
160
+ lists it in the PR, so the change is still in that PR's diff to move into an
161
+ extension file.
162
+
163
+ ## Contract skills
164
+
165
+ \`qore-add-integration\`, \`qore-add-connection\`, \`qore-use-permissions\`,
166
+ \`qore-observability\` and \`qore-use-database\` produce code the portal reads:
167
+ the Integrations tab, connections, roles, Monitoring, the local database. An
168
+ extension may add to them but not relax them, and turning one off does not turn
169
+ off the SDK's lint rules that check the same things.
170
+
171
+ The \`qore-\` prefix is reserved for managed skills. Sync never touches a skill
172
+ it does not ship, so your own skills are safe under any other name.
173
+ `;
174
+ function buildTree(catalog, sources, f) {
175
+ const skills = renderSkills(catalog, sources, f);
176
+ for (const entry of catalog.skills) {
177
+ const key = `skills/${entry.name}/SKILL.md`;
178
+ const body = skills.get(key);
179
+ if (body !== void 0) skills.set(key, body.replace(/\n*$/, "\n") + extensionFooter(entry));
180
+ }
181
+ skills.set(INDEX_PATH, renderIndex(catalog, skills));
182
+ skills.set(README_PATH, README);
183
+ return skills;
184
+ }
185
+ function normalise(text) {
186
+ return text.replace(/\r\n/g, "\n");
187
+ }
188
+ function hashContent(text) {
189
+ return `sha256:${createHash("sha256").update(normalise(text), "utf8").digest("hex")}`;
190
+ }
191
+ function parseManifest(text) {
192
+ const raw = JSON.parse(text);
193
+ if (raw.schemaVersion !== 1) throw new Error(`unsupported manifest schemaVersion ${String(raw.schemaVersion)}`);
194
+ return {
195
+ schemaVersion: 1,
196
+ sdkVersion: String(raw.sdkVersion ?? ""),
197
+ features: parseFeatures(raw.features),
198
+ ...raw.featureOverride === void 0 ? {} : { featureOverride: parseFeatures(raw.featureOverride) },
199
+ detached: Array.isArray(raw.detached) ? raw.detached.map(String) : [],
200
+ files: raw.files ?? {}
201
+ };
202
+ }
203
+ function renderManifest(m) {
204
+ const files = Object.fromEntries(Object.entries(m.files).sort(([a], [b]) => a.localeCompare(b)));
205
+ return `${JSON.stringify({ ...m, files }, null, 2)}
206
+ `;
207
+ }
208
+ var skillOf = (rel) => /^skills\/([^/]+)\//.exec(rel)?.[1];
209
+ function planSync(s) {
210
+ const detached = new Set(s.manifest?.detached ?? []);
211
+ const managed = (rel) => {
212
+ const skill = skillOf(rel);
213
+ return !(skill && detached.has(skill));
214
+ };
215
+ const tree = new Map([...s.tree].filter(([rel]) => managed(rel)));
216
+ const known = s.manifest?.files ?? {};
217
+ const writes = /* @__PURE__ */ new Map();
218
+ const edited = [];
219
+ const adopted = [];
220
+ for (const [rel, content] of tree) {
221
+ const disk = s.read(rel);
222
+ if (disk !== void 0 && normalise(disk) === content) continue;
223
+ if (disk !== void 0) {
224
+ if (!(rel in known)) adopted.push(rel);
225
+ else if (hashContent(disk) !== known[rel]) edited.push(rel);
226
+ }
227
+ writes.set(rel, content);
228
+ }
229
+ const deletes = [];
230
+ for (const rel of Object.keys(known)) {
231
+ if (tree.has(rel) || !managed(rel) || s.read(rel) === void 0) continue;
232
+ deletes.push(rel);
233
+ if (hashContent(s.read(rel)) !== known[rel]) edited.push(rel);
234
+ }
235
+ const legacyRemoved = [];
236
+ for (const entry of s.catalog.skills) {
237
+ if (detached.has(entry.name)) continue;
238
+ for (const legacy of entry.legacyNames ?? []) {
239
+ const files = s.listDir(`skills/${legacy}`);
240
+ if (!files.length) continue;
241
+ legacyRemoved.push(`skills/${legacy}`);
242
+ deletes.push(...files);
243
+ }
244
+ }
245
+ let claudeMd = s.claudeMd;
246
+ const claudeMdRenamed = [];
247
+ let claudeMdImportAdded = false;
248
+ if (claudeMd !== void 0) {
249
+ for (const entry of s.catalog.skills) {
250
+ for (const legacy of entry.legacyNames ?? []) {
251
+ const exact = new RegExp(`(?<![a-z0-9-])${legacy}(?![a-z0-9-])`, "g");
252
+ if (!exact.test(claudeMd)) continue;
253
+ claudeMd = claudeMd.replace(exact, entry.name);
254
+ claudeMdRenamed.push([legacy, entry.name]);
255
+ }
256
+ }
257
+ if (!claudeMd.split(/\r?\n/).some((l) => l.trim() === CLAUDE_MD_IMPORT)) {
258
+ claudeMd = `${claudeMd.replace(/\s*$/, "")}
259
+
260
+ ${CLAUDE_MD_IMPORT}
261
+ `;
262
+ claudeMdImportAdded = true;
263
+ }
264
+ }
265
+ const manifest = {
266
+ schemaVersion: 1,
267
+ sdkVersion: s.sdkVersion,
268
+ features: s.features,
269
+ ...s.featureOverride ? { featureOverride: s.featureOverride } : {},
270
+ detached: [...detached].sort(),
271
+ files: Object.fromEntries([...tree].map(([rel, c]) => [rel, hashContent(c)]))
272
+ };
273
+ const manifestText = renderManifest(manifest);
274
+ if (s.read(MANIFEST_PATH) === void 0 || normalise(s.read(MANIFEST_PATH)) !== manifestText) {
275
+ writes.set(MANIFEST_PATH, manifestText);
276
+ }
277
+ return {
278
+ writes,
279
+ deletes: deletes.sort(),
280
+ edited: edited.sort(),
281
+ adopted: adopted.sort(),
282
+ legacyRemoved: legacyRemoved.sort(),
283
+ manifest,
284
+ claudeMd: claudeMd === s.claudeMd ? void 0 : claudeMd,
285
+ claudeMdRenamed: claudeMdRenamed.sort(([a], [b]) => a.localeCompare(b)),
286
+ claudeMdImportAdded
287
+ };
288
+ }
289
+ function planChanges(p) {
290
+ return p.writes.size > 0 || p.deletes.length > 0 || p.claudeMd !== void 0;
291
+ }
292
+ function renderReport(p, opts) {
293
+ const f = p.manifest.features;
294
+ const lines = [
295
+ "### Qore skills",
296
+ "",
297
+ `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"}).`
298
+ ];
299
+ const managedWrites = [...p.writes.keys()].filter((r) => r !== MANIFEST_PATH);
300
+ if (!managedWrites.length && !p.deletes.length && p.claudeMd === void 0) {
301
+ lines.push("", "No skill content changed.");
302
+ }
303
+ const section = (title, items, note) => {
304
+ if (!items.length) return;
305
+ lines.push("", `**${title}** ${note}`, "", ...items.map((i) => `- \`.claude/${i}\``));
306
+ };
307
+ section(
308
+ "Hand-edited managed files, overwritten",
309
+ p.edited,
310
+ "(the edit is in this change's diff; to keep it, move it into `.claude/qore/extensions/<skill>.md`):"
311
+ );
312
+ section("Existing files now managed", p.adopted, "(written by an older template; check the diff for local changes):");
313
+ section(
314
+ "Legacy skill directories removed",
315
+ p.legacyRemoved,
316
+ "(replaced by their `qore-*` names; a local edit to one is in this change's diff):"
317
+ );
318
+ if (p.claudeMd !== void 0) {
319
+ lines.push("", "**`CLAUDE.md`:**", "");
320
+ for (const [from, to] of p.claudeMdRenamed) lines.push(`- Renamed \`${from}\` to \`${to}\`.`);
321
+ if (p.claudeMdImportAdded) lines.push(`- Imports \`${CLAUDE_MD_IMPORT}\`, the index of managed skills.`);
322
+ }
323
+ return `${lines.join("\n")}
324
+ `;
325
+ }
326
+
327
+ // src/cli/skills.ts
328
+ var USAGE = `usage:
329
+ qore-skills sync [--cwd <app>] [--features <json>|infer] [--report <file.md>]
330
+ qore-skills check [--cwd <app>] [--json]
331
+ qore-skills render --features <json> --out <dir>`;
332
+ var PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
333
+ function parseArgs(argv) {
334
+ const [cmd, ...rest] = argv;
335
+ if (cmd !== "sync" && cmd !== "check" && cmd !== "render") return { error: USAGE };
336
+ const args = { cmd, cwd: process.cwd(), json: false };
337
+ for (let i = 0; i < rest.length; i++) {
338
+ const flag = rest[i];
339
+ const value = () => {
340
+ const v = rest[++i];
341
+ if (v === void 0) throw new Error(`${flag} needs a value`);
342
+ return v;
343
+ };
344
+ try {
345
+ if (flag === "--cwd") args.cwd = resolve(value());
346
+ else if (flag === "--features") {
347
+ const v = value();
348
+ args.features = v === "infer" ? "infer" : parseFeatures(JSON.parse(v));
349
+ } else if (flag === "--report") args.report = resolve(value());
350
+ else if (flag === "--out") args.out = resolve(value());
351
+ else if (flag === "--json") args.json = true;
352
+ else return { error: `unknown option ${flag}
353
+ ${USAGE}` };
354
+ } catch (err) {
355
+ return { error: err instanceof Error ? err.message : String(err) };
356
+ }
357
+ }
358
+ if (cmd === "render" && (!args.features || args.features === "infer" || !args.out)) return { error: `render needs --features and --out
359
+ ${USAGE}` };
360
+ return args;
361
+ }
362
+ function walk(root, prefix = "") {
363
+ const dir = join(root, prefix);
364
+ if (!existsSync(dir)) return [];
365
+ return readdirSync(dir, { withFileTypes: true }).flatMap((d) => {
366
+ const rel = prefix ? `${prefix}/${d.name}` : d.name;
367
+ return d.isDirectory() ? walk(root, rel) : d.isFile() ? [rel] : [];
368
+ });
369
+ }
370
+ function loadSources(root = join(PACKAGE_ROOT, "skills")) {
371
+ const catalog = JSON.parse(readFileSync(join(root, "catalog.json"), "utf8"));
372
+ const sources = new Map(
373
+ walk(root).filter((rel) => rel !== "catalog.json").map((rel) => [rel, readFileSync(join(root, rel), "utf8").replace(/\r\n/g, "\n")])
374
+ );
375
+ return { catalog, sources };
376
+ }
377
+ function sdkVersion() {
378
+ return JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
379
+ }
380
+ function readOptional(path) {
381
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
382
+ }
383
+ function writeFile(path, text) {
384
+ mkdirSync(dirname(path), { recursive: true });
385
+ writeFileSync(path, text);
386
+ }
387
+ function pruneEmpty(root, rel, stop) {
388
+ let dir = dirname(join(root, rel));
389
+ while (dir.startsWith(join(root, stop)) && dir !== join(root, stop)) {
390
+ if (!existsSync(dir) || readdirSync(dir).length) return;
391
+ rmdirSync(dir);
392
+ dir = dirname(dir);
393
+ }
394
+ }
395
+ function plan(app, override) {
396
+ const claude = join(app, ".claude");
397
+ const manifestText = readOptional(join(claude, MANIFEST_PATH));
398
+ const manifest = manifestText ? parseManifest(manifestText) : null;
399
+ const featureOverride = override === "infer" ? void 0 : override ?? manifest?.featureOverride;
400
+ const pkg = JSON.parse(readOptional(join(app, "package.json")) ?? "{}");
401
+ const features = featureOverride ?? inferFeatures(pkg, (rel) => existsSync(join(app, rel)));
402
+ const inferred = !featureOverride;
403
+ const { catalog, sources } = loadSources();
404
+ return {
405
+ inferred,
406
+ plan: planSync({
407
+ catalog,
408
+ tree: buildTree(catalog, sources, features),
409
+ manifest,
410
+ sdkVersion: sdkVersion(),
411
+ features,
412
+ featureOverride,
413
+ read: (rel) => readOptional(join(claude, rel)),
414
+ listDir: (rel) => walk(join(claude, rel)).map((f) => `${rel}/${f}`),
415
+ claudeMd: readOptional(join(app, "CLAUDE.md"))
416
+ })
417
+ };
418
+ }
419
+ function apply(app, p) {
420
+ const claude = join(app, ".claude");
421
+ for (const rel of p.deletes) {
422
+ rmSync(join(claude, rel), { force: true });
423
+ pruneEmpty(claude, rel, "skills");
424
+ }
425
+ for (const [rel, text] of p.writes) writeFile(join(claude, rel), text);
426
+ if (p.claudeMd !== void 0) writeFileSync(join(app, "CLAUDE.md"), p.claudeMd);
427
+ }
428
+ function render(features, out) {
429
+ const { catalog, sources } = loadSources();
430
+ const tree = buildTree(catalog, sources, features);
431
+ for (const [rel, text] of tree) writeFile(join(out, rel), text);
432
+ const manifest = {
433
+ schemaVersion: 1,
434
+ sdkVersion: sdkVersion(),
435
+ features,
436
+ detached: [],
437
+ files: Object.fromEntries([...tree].map(([rel, c]) => [rel, hashContent(c)]))
438
+ };
439
+ writeFile(join(out, MANIFEST_PATH), renderManifest(manifest));
440
+ }
441
+ function main(argv = process.argv.slice(2)) {
442
+ const args = parseArgs(argv);
443
+ if ("error" in args) {
444
+ console.error(`qore-skills: ${args.error}`);
445
+ return 2;
446
+ }
447
+ if (args.cmd === "render") {
448
+ render(args.features, args.out);
449
+ console.log(`qore-skills: rendered into ${args.out}`);
450
+ return 0;
451
+ }
452
+ const { plan: p, inferred } = plan(args.cwd, args.features);
453
+ const report = renderReport(p, { inferred });
454
+ if (args.cmd === "check") {
455
+ const changed = planChanges(p);
456
+ if (args.json) {
457
+ console.log(
458
+ JSON.stringify({
459
+ upToDate: !changed,
460
+ writes: [...p.writes.keys()],
461
+ deletes: p.deletes,
462
+ edited: p.edited,
463
+ claudeMd: p.claudeMd !== void 0
464
+ })
465
+ );
466
+ } else {
467
+ console.log(changed ? report : "qore-skills: up to date");
468
+ }
469
+ return changed ? 1 : 0;
470
+ }
471
+ apply(args.cwd, p);
472
+ if (args.report) writeFile(args.report, report);
473
+ console.log(report);
474
+ return 0;
475
+ }
476
+ function isMainModule() {
477
+ if (!process.argv[1]) return false;
478
+ try {
479
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
480
+ } catch {
481
+ return false;
482
+ }
483
+ }
484
+ if (isMainModule()) {
485
+ try {
486
+ process.exitCode = main();
487
+ } catch (err) {
488
+ console.error(`qore-skills: ${err instanceof Error ? err.message : String(err)}`);
489
+ process.exitCode = 2;
490
+ }
491
+ }
492
+ export {
493
+ apply,
494
+ loadSources,
495
+ main,
496
+ parseArgs,
497
+ plan,
498
+ render
499
+ };
@@ -17,7 +17,7 @@ var MAPPING_URL = /^\/\/# sourceMappingURL=(\S+)[ \t]*$/gm;
17
17
  var STANDALONE_SERVER = "standalone/.next/server/";
18
18
  function parseArgs(argv) {
19
19
  const [cmd, dir, ...rest] = argv;
20
- if (cmd !== "upload" || !dir) return { error: "usage: qdc-sourcemaps upload <dir> [--bundle <name>]" };
20
+ if (cmd !== "upload" || !dir) return { error: "usage: qore-sourcemaps upload <dir> [--bundle <name>]" };
21
21
  let bundle = "app";
22
22
  if (rest[0] === "--bundle") bundle = rest[1] ?? "";
23
23
  if (!BUNDLE.test(bundle)) return { error: `invalid bundle name: ${bundle}` };
@@ -149,13 +149,13 @@ function tryRemove(io, path) {
149
149
  async function main(argv = process.argv.slice(2), io = defaultIo()) {
150
150
  const args = parseArgs(argv);
151
151
  if ("error" in args) {
152
- console.warn(`qdc-sourcemaps: ${args.error}`);
152
+ console.warn(`qore-sourcemaps: ${args.error}`);
153
153
  const dir2 = argv[0] === "upload" ? argv[1] : void 0;
154
154
  if (dir2 && !dir2.startsWith("-")) {
155
155
  try {
156
156
  for (const m of listMaps(io.walk(dir2))) tryRemove(io, join(dir2, m));
157
157
  } catch (err) {
158
- console.warn(`qdc-sourcemaps: cleanup skipped: ${describe(err, io.env.QORE_BUILD_UPLOAD_TOKEN)}`);
158
+ console.warn(`qore-sourcemaps: cleanup skipped: ${describe(err, io.env.QORE_BUILD_UPLOAD_TOKEN)}`);
159
159
  }
160
160
  }
161
161
  return;
@@ -169,7 +169,7 @@ async function main(argv = process.argv.slice(2), io = defaultIo()) {
169
169
  maps = listMaps(paths);
170
170
  const skip = skipReason(io.env);
171
171
  if (skip) {
172
- console.log(`qdc-sourcemaps: skipped (${skip})`);
172
+ console.log(`qore-sourcemaps: skipped (${skip})`);
173
173
  return;
174
174
  }
175
175
  const roots = nextRoots((rel) => io.exists(at(rel)));
@@ -185,9 +185,9 @@ async function main(argv = process.argv.slice(2), io = defaultIo()) {
185
185
  await io.run("sentry-cli", ["sourcemaps", "inject", dir]);
186
186
  }
187
187
  const { files, dropped } = listArtifacts(dir, () => paths, (rel) => io.readText(at(rel)), roots);
188
- if (dropped > 0) console.log(`qdc-sourcemaps: dropped ${dropped} source maps the uploader would reject`);
188
+ if (dropped > 0) console.log(`qore-sourcemaps: dropped ${dropped} source maps the uploader would reject`);
189
189
  if (files.length === 0) {
190
- console.log("qdc-sourcemaps: no source maps found, nothing to upload");
190
+ console.log("qore-sourcemaps: no source maps found, nothing to upload");
191
191
  return;
192
192
  }
193
193
  const tgz = io.tmpFile();
@@ -207,10 +207,10 @@ async function main(argv = process.argv.slice(2), io = defaultIo()) {
207
207
  body: new Uint8Array(body),
208
208
  signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
209
209
  });
210
- if (res.ok) console.log(`qdc-sourcemaps: handed off ${files.length / 2} source maps (${body.byteLength} bytes)`);
211
- else console.warn(`qdc-sourcemaps: upload refused, broker answered ${res.status}`);
210
+ if (res.ok) console.log(`qore-sourcemaps: handed off ${files.length / 2} source maps (${body.byteLength} bytes)`);
211
+ else console.warn(`qore-sourcemaps: upload refused, broker answered ${res.status}`);
212
212
  } catch (err) {
213
- console.warn(`qdc-sourcemaps: upload skipped: ${describe(err, io.env.QORE_BUILD_UPLOAD_TOKEN)}`);
213
+ console.warn(`qore-sourcemaps: upload skipped: ${describe(err, io.env.QORE_BUILD_UPLOAD_TOKEN)}`);
214
214
  } finally {
215
215
  for (const m of maps) tryRemove(io, at(m));
216
216
  for (const s of scratch) tryRemove(io, s);
@@ -168,10 +168,10 @@ function main(argv = process.argv.slice(2)) {
168
168
  });
169
169
  writeReport(out, report);
170
170
  const summary = report.measured ? Object.entries(report.suites).map(([name, s]) => `${name}: ${s.passed} passed, ${s.failed} failed, ${s.skipped} skipped`).join("; ") : `unmeasured (${report.reason})`;
171
- console.log(`qdc-test-report \u2192 ${out} \u2014 ${summary}`);
171
+ console.log(`qore-test-report \u2192 ${out} \u2014 ${summary}`);
172
172
  } catch (err) {
173
173
  console.warn(
174
- `qdc-test-report could not build a report: ${err instanceof Error ? err.message : String(err)}`
174
+ `qore-test-report could not build a report: ${err instanceof Error ? err.message : String(err)}`
175
175
  );
176
176
  try {
177
177
  writeReport(out, unmeasuredReport(commitSha, ranAt, "parse_failed"));
@@ -20,7 +20,7 @@ function sentryBuildOptions(env) {
20
20
  if (qoreUploads(env)) {
21
21
  return {
22
22
  ...common,
23
- // Qore uploads the maps after the build (qdc-sourcemaps); the build holds no Sentry token.
23
+ // Qore uploads the maps after the build (qore-sourcemaps); the build holds no Sentry token.
24
24
  // deleteSourcemapsAfterUpload must stay false: the plugin would otherwise delete the maps anyway.
25
25
  sourcemaps: { disable: true, deleteSourcemapsAfterUpload: false },
26
26
  release: { name: release, create: false }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/next/config/index.ts"],"sourcesContent":["import { withSentryConfig } from \"@sentry/nextjs/config\"\nimport type { NextConfig } from \"next\"\n\nimport { STATIC_SECURITY_HEADERS } from \"../../security-core/headers\"\n\ntype Env = Record<string, string | undefined>\n\n/** Only the Qore build sets this; without it the build behaves as before (portal-web, older repos). */\nfunction qoreUploads(env: Env): boolean {\n return Boolean(env.QORE_BUILD_UPLOAD_URL)\n}\n\nexport function sentryBuildOptions(env: Env): Parameters<typeof withSentryConfig>[1] {\n const release = env.SENTRY_RELEASE || undefined\n const common = {\n org: env.SENTRY_ORG,\n project: env.SENTRY_PROJECT,\n sentryUrl: env.SENTRY_URL || \"https://de.sentry.io\",\n silent: true,\n telemetry: false,\n widenClientFileUpload: true,\n }\n if (qoreUploads(env)) {\n return {\n ...common,\n // Qore uploads the maps after the build (qdc-sourcemaps); the build holds no Sentry token.\n // deleteSourcemapsAfterUpload must stay false: the plugin would otherwise delete the maps anyway.\n sourcemaps: { disable: true, deleteSourcemapsAfterUpload: false },\n release: { name: release, create: false },\n }\n }\n const authToken = env.SENTRY_AUTH_TOKEN || undefined\n return {\n ...common,\n authToken,\n sourcemaps: { disable: !authToken, deleteSourcemapsAfterUpload: true },\n release: {\n name: release,\n create: Boolean(authToken && release),\n setCommits: env.SENTRY_REPO && release ? { repo: env.SENTRY_REPO, commit: release } : undefined,\n },\n }\n}\n\nfunction turbopackDebugIds(config: NextConfig): boolean | undefined {\n return (config.turbopack as { debugIds?: boolean } | undefined)?.debugIds\n}\n\nfunction qoreMapOptions(config: NextConfig): NextConfig {\n return {\n productionBrowserSourceMaps: config.productionBrowserSourceMaps ?? true,\n // With sourcemaps disabled the Sentry plugin no longer turns debug ids on. Next 16 option, not in the Next 15 types.\n turbopack: { ...config.turbopack, debugIds: turbopackDebugIds(config) ?? true } as NextConfig[\"turbopack\"],\n }\n}\n\nexport function withQdcConfig(config: NextConfig = {}, env: Env = process.env): NextConfig {\n const base: NextConfig = {\n ...config,\n ...(qoreUploads(env) ? qoreMapOptions(config) : {}),\n serverExternalPackages: [...(config.serverExternalPackages ?? [])],\n // The static security headers, on every route Next serves. The CSP is\n // deliberately NOT here — it carries a per-response nonce, which a\n // build-time constant cannot have; that comes from `withSecurityHeaders`.\n headers: async () => [\n {\n source: \"/:path*\",\n headers: STATIC_SECURITY_HEADERS.map(([key, value]) => ({ key, value })),\n },\n ...((await config.headers?.()) ?? []),\n ],\n }\n return withSentryConfig(base, sentryBuildOptions(env))\n}\n"],"mappings":";;;;;AAAA,SAAS,wBAAwB;AAQjC,SAAS,YAAY,KAAmB;AACtC,SAAO,QAAQ,IAAI,qBAAqB;AAC1C;AAEO,SAAS,mBAAmB,KAAkD;AACnF,QAAM,UAAU,IAAI,kBAAkB;AACtC,QAAM,SAAS;AAAA,IACb,KAAK,IAAI;AAAA,IACT,SAAS,IAAI;AAAA,IACb,WAAW,IAAI,cAAc;AAAA,IAC7B,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,uBAAuB;AAAA,EACzB;AACA,MAAI,YAAY,GAAG,GAAG;AACpB,WAAO;AAAA,MACL,GAAG;AAAA;AAAA;AAAA,MAGH,YAAY,EAAE,SAAS,MAAM,6BAA6B,MAAM;AAAA,MAChE,SAAS,EAAE,MAAM,SAAS,QAAQ,MAAM;AAAA,IAC1C;AAAA,EACF;AACA,QAAM,YAAY,IAAI,qBAAqB;AAC3C,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,YAAY,EAAE,SAAS,CAAC,WAAW,6BAA6B,KAAK;AAAA,IACrE,SAAS;AAAA,MACP,MAAM;AAAA,MACN,QAAQ,QAAQ,aAAa,OAAO;AAAA,MACpC,YAAY,IAAI,eAAe,UAAU,EAAE,MAAM,IAAI,aAAa,QAAQ,QAAQ,IAAI;AAAA,IACxF;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,QAAyC;AAClE,SAAQ,OAAO,WAAkD;AACnE;AAEA,SAAS,eAAe,QAAgC;AACtD,SAAO;AAAA,IACL,6BAA6B,OAAO,+BAA+B;AAAA;AAAA,IAEnE,WAAW,EAAE,GAAG,OAAO,WAAW,UAAU,kBAAkB,MAAM,KAAK,KAAK;AAAA,EAChF;AACF;AAEO,SAAS,cAAc,SAAqB,CAAC,GAAG,MAAW,QAAQ,KAAiB;AACzF,QAAM,OAAmB;AAAA,IACvB,GAAG;AAAA,IACH,GAAI,YAAY,GAAG,IAAI,eAAe,MAAM,IAAI,CAAC;AAAA,IACjD,wBAAwB,CAAC,GAAI,OAAO,0BAA0B,CAAC,CAAE;AAAA;AAAA;AAAA;AAAA,IAIjE,SAAS,YAAY;AAAA,MACnB;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,wBAAwB,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE;AAAA,MACzE;AAAA,MACA,GAAK,MAAM,OAAO,UAAU,KAAM,CAAC;AAAA,IACrC;AAAA,EACF;AACA,SAAO,iBAAiB,MAAM,mBAAmB,GAAG,CAAC;AACvD;","names":[]}
1
+ {"version":3,"sources":["../../../src/next/config/index.ts"],"sourcesContent":["import { withSentryConfig } from \"@sentry/nextjs/config\"\nimport type { NextConfig } from \"next\"\n\nimport { STATIC_SECURITY_HEADERS } from \"../../security-core/headers\"\n\ntype Env = Record<string, string | undefined>\n\n/** Only the Qore build sets this; without it the build behaves as before (portal-web, older repos). */\nfunction qoreUploads(env: Env): boolean {\n return Boolean(env.QORE_BUILD_UPLOAD_URL)\n}\n\nexport function sentryBuildOptions(env: Env): Parameters<typeof withSentryConfig>[1] {\n const release = env.SENTRY_RELEASE || undefined\n const common = {\n org: env.SENTRY_ORG,\n project: env.SENTRY_PROJECT,\n sentryUrl: env.SENTRY_URL || \"https://de.sentry.io\",\n silent: true,\n telemetry: false,\n widenClientFileUpload: true,\n }\n if (qoreUploads(env)) {\n return {\n ...common,\n // Qore uploads the maps after the build (qore-sourcemaps); the build holds no Sentry token.\n // deleteSourcemapsAfterUpload must stay false: the plugin would otherwise delete the maps anyway.\n sourcemaps: { disable: true, deleteSourcemapsAfterUpload: false },\n release: { name: release, create: false },\n }\n }\n const authToken = env.SENTRY_AUTH_TOKEN || undefined\n return {\n ...common,\n authToken,\n sourcemaps: { disable: !authToken, deleteSourcemapsAfterUpload: true },\n release: {\n name: release,\n create: Boolean(authToken && release),\n setCommits: env.SENTRY_REPO && release ? { repo: env.SENTRY_REPO, commit: release } : undefined,\n },\n }\n}\n\nfunction turbopackDebugIds(config: NextConfig): boolean | undefined {\n return (config.turbopack as { debugIds?: boolean } | undefined)?.debugIds\n}\n\nfunction qoreMapOptions(config: NextConfig): NextConfig {\n return {\n productionBrowserSourceMaps: config.productionBrowserSourceMaps ?? true,\n // With sourcemaps disabled the Sentry plugin no longer turns debug ids on. Next 16 option, not in the Next 15 types.\n turbopack: { ...config.turbopack, debugIds: turbopackDebugIds(config) ?? true } as NextConfig[\"turbopack\"],\n }\n}\n\nexport function withQdcConfig(config: NextConfig = {}, env: Env = process.env): NextConfig {\n const base: NextConfig = {\n ...config,\n ...(qoreUploads(env) ? qoreMapOptions(config) : {}),\n serverExternalPackages: [...(config.serverExternalPackages ?? [])],\n // The static security headers, on every route Next serves. The CSP is\n // deliberately NOT here — it carries a per-response nonce, which a\n // build-time constant cannot have; that comes from `withSecurityHeaders`.\n headers: async () => [\n {\n source: \"/:path*\",\n headers: STATIC_SECURITY_HEADERS.map(([key, value]) => ({ key, value })),\n },\n ...((await config.headers?.()) ?? []),\n ],\n }\n return withSentryConfig(base, sentryBuildOptions(env))\n}\n"],"mappings":";;;;;AAAA,SAAS,wBAAwB;AAQjC,SAAS,YAAY,KAAmB;AACtC,SAAO,QAAQ,IAAI,qBAAqB;AAC1C;AAEO,SAAS,mBAAmB,KAAkD;AACnF,QAAM,UAAU,IAAI,kBAAkB;AACtC,QAAM,SAAS;AAAA,IACb,KAAK,IAAI;AAAA,IACT,SAAS,IAAI;AAAA,IACb,WAAW,IAAI,cAAc;AAAA,IAC7B,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,uBAAuB;AAAA,EACzB;AACA,MAAI,YAAY,GAAG,GAAG;AACpB,WAAO;AAAA,MACL,GAAG;AAAA;AAAA;AAAA,MAGH,YAAY,EAAE,SAAS,MAAM,6BAA6B,MAAM;AAAA,MAChE,SAAS,EAAE,MAAM,SAAS,QAAQ,MAAM;AAAA,IAC1C;AAAA,EACF;AACA,QAAM,YAAY,IAAI,qBAAqB;AAC3C,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,YAAY,EAAE,SAAS,CAAC,WAAW,6BAA6B,KAAK;AAAA,IACrE,SAAS;AAAA,MACP,MAAM;AAAA,MACN,QAAQ,QAAQ,aAAa,OAAO;AAAA,MACpC,YAAY,IAAI,eAAe,UAAU,EAAE,MAAM,IAAI,aAAa,QAAQ,QAAQ,IAAI;AAAA,IACxF;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,QAAyC;AAClE,SAAQ,OAAO,WAAkD;AACnE;AAEA,SAAS,eAAe,QAAgC;AACtD,SAAO;AAAA,IACL,6BAA6B,OAAO,+BAA+B;AAAA;AAAA,IAEnE,WAAW,EAAE,GAAG,OAAO,WAAW,UAAU,kBAAkB,MAAM,KAAK,KAAK;AAAA,EAChF;AACF;AAEO,SAAS,cAAc,SAAqB,CAAC,GAAG,MAAW,QAAQ,KAAiB;AACzF,QAAM,OAAmB;AAAA,IACvB,GAAG;AAAA,IACH,GAAI,YAAY,GAAG,IAAI,eAAe,MAAM,IAAI,CAAC;AAAA,IACjD,wBAAwB,CAAC,GAAI,OAAO,0BAA0B,CAAC,CAAE;AAAA;AAAA;AAAA;AAAA,IAIjE,SAAS,YAAY;AAAA,MACnB;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,wBAAwB,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE;AAAA,MACzE;AAAA,MACA,GAAK,MAAM,OAAO,UAAU,KAAM,CAAC;AAAA,IACrC;AAAA,EACF;AACA,SAAO,iBAAiB,MAAM,mBAAmB,GAAG,CAAC;AACvD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatronic/sdk",
3
- "version": "0.6.1-rc.4",
3
+ "version": "0.7.0-rc.6",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -9,12 +9,17 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "semgrep"
12
+ "semgrep",
13
+ "skills",
14
+ "postinstall.mjs"
13
15
  ],
14
16
  "bin": {
17
+ "qore-skills": "./dist/cli/skills.js",
18
+ "qore-sourcemaps": "./dist/cli/sourcemaps.js",
19
+ "qore-test-report": "./dist/cli/test-report.js",
15
20
  "qdc-setup-env": "./dist/cli/setup-env.js",
16
- "qdc-sourcemaps": "./dist/cli/sourcemaps.js",
17
- "qdc-test-report": "./dist/cli/test-report.js"
21
+ "qdc-sourcemaps": "./dist/cli/legacy/qdc-sourcemaps.js",
22
+ "qdc-test-report": "./dist/cli/legacy/qdc-test-report.js"
18
23
  },
19
24
  "exports": {
20
25
  "./auth": {
@@ -123,6 +128,7 @@
123
128
  "surface": "node scripts/api-surface.mjs",
124
129
  "surface:check": "node scripts/api-surface.mjs --check",
125
130
  "prepublishOnly": "pnpm build",
131
+ "postinstall": "node postinstall.mjs",
126
132
  "test": "vitest run",
127
133
  "test:watch": "vitest",
128
134
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
@@ -0,0 +1,9 @@
1
+ // Syncs an app's managed skills when it installs or bumps this package; see
2
+ // src/cli/skills-postinstall.ts. It must never fail an install, so a missing
3
+ // build (the SDK's own workspace) or any error is swallowed here.
4
+ try {
5
+ const { main } = await import("./dist/cli/skills-postinstall.js")
6
+ main()
7
+ } catch {
8
+ // Nothing to do: the next `qore-skills check` reports skills that lag the SDK.
9
+ }
@@ -0,0 +1 @@
1
+ * text eol=lf