@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.
- package/README.md +26 -0
- package/dist/cli/legacy/qdc-sourcemaps.js +237 -0
- package/dist/cli/legacy/qdc-test-report.js +200 -0
- package/dist/cli/skills-postinstall.js +603 -0
- package/dist/cli/skills.js +499 -0
- package/dist/cli/sourcemaps.js +9 -9
- package/dist/cli/test-report.js +2 -2
- package/dist/next/config/index.js +1 -1
- package/dist/next/config/index.js.map +1 -1
- package/package.json +10 -4
- package/postinstall.mjs +9 -0
- package/skills/.gitattributes +1 -0
- package/skills/catalog.json +24 -0
- package/skills/qore-add-api-route/SKILL.md +58 -0
- package/skills/qore-add-connection/SKILL.next.md +149 -0
- package/skills/qore-add-connection/SKILL.node.md +160 -0
- package/skills/qore-add-db-table/SKILL.md +88 -0
- package/skills/qore-add-db-table/references/cross-module-reference.md +74 -0
- package/skills/qore-add-feature/SKILL.md +77 -0
- package/skills/qore-add-integration/SKILL.next.md +217 -0
- package/skills/qore-add-integration/SKILL.node.md +216 -0
- package/skills/qore-add-page/SKILL.md +53 -0
- package/skills/qore-author-skill/SKILL.md +136 -0
- package/skills/qore-debug-issue/SKILL.md +48 -0
- package/skills/qore-enable-auth-method/SKILL.md +90 -0
- package/skills/qore-observability/SKILL.md +89 -0
- package/skills/qore-observability/references/errors.md +87 -0
- package/skills/qore-observability/references/logs.md +69 -0
- package/skills/qore-observability/references/tracing.md +58 -0
- package/skills/qore-recommended-tools/SKILL.md +58 -0
- package/skills/qore-secure-io/SKILL.md +174 -0
- package/skills/qore-ship-checklist/SKILL.md +80 -0
- package/skills/qore-translate-strings/SKILL.md +36 -0
- package/skills/qore-use-addons/SKILL.md +175 -0
- package/skills/qore-use-database/SKILL.md +222 -0
- package/skills/qore-use-design-system/SKILL.md +76 -0
- package/skills/qore-use-mail-domain/SKILL.next.md +67 -0
- package/skills/qore-use-mail-domain/SKILL.node.md +62 -0
- package/skills/qore-use-permissions/SKILL.md +116 -0
- package/skills/qore-verify-user/SKILL.md +56 -0
package/README.md
CHANGED
|
@@ -23,6 +23,32 @@ app runs against its own proxy, and `@quatronic/sdk/eslint` for the fourteen
|
|
|
23
23
|
`qdc/*` lint rules —
|
|
24
24
|
[`docs/security-rules.md`](docs/security-rules.md).
|
|
25
25
|
|
|
26
|
+
## Claude Code skills
|
|
27
|
+
|
|
28
|
+
The SDK ships the app's managed Claude Code skills (`skills/`, one entry per
|
|
29
|
+
skill in `skills/catalog.json`), and `qore-skills sync` writes them into the
|
|
30
|
+
app's `.claude/skills/qore-*`, with `.claude/qore/manifest.json` recording the
|
|
31
|
+
SDK version and a hash per file. The app commits the result.
|
|
32
|
+
|
|
33
|
+
Installing or bumping the SDK runs the sync by itself. The package's
|
|
34
|
+
`postinstall` syncs when the manifest records a different SDK version, and
|
|
35
|
+
prints what it changed so it lands in the same commit as the bump. It never
|
|
36
|
+
fails an install, and it skips CI, `QORE_SKILLS_SYNC=0`, a directory that is not
|
|
37
|
+
a git checkout, and any bump where a file it would overwrite holds an
|
|
38
|
+
uncommitted hand edit. pnpm 11 runs a dependency's script only when the app
|
|
39
|
+
allows it, so each app's `pnpm-workspace.yaml` needs:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
allowBuilds:
|
|
43
|
+
"@quatronic/sdk": true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without it, pnpm 11 fails the install with `ERR_PNPM_IGNORED_BUILDS`. pnpm only
|
|
47
|
+
runs the script when the installed version changes, so after a skipped sync run
|
|
48
|
+
`pnpm exec qore-skills sync` yourself; `qore-skills check` exits 1 while the
|
|
49
|
+
skills are behind. How a team extends, replaces or turns off a managed skill is
|
|
50
|
+
in the app's `.claude/qore/README.md` and the `/qore-author-skill` skill.
|
|
51
|
+
|
|
26
52
|
## Permissions
|
|
27
53
|
|
|
28
54
|
The SDK ships a hybrid role/permission layer. Roles come from the Keycloak JWT;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/legacy/notice.ts
|
|
4
|
+
import { basename } from "path";
|
|
5
|
+
function renamedNotice(oldName, newName) {
|
|
6
|
+
return `\u26A0 ${oldName} is renamed to ${newName}; the old name keeps working until the next major release.`;
|
|
7
|
+
}
|
|
8
|
+
var invoked = basename(process.argv[1] ?? "", ".js");
|
|
9
|
+
if (invoked.startsWith("qdc-")) console.warn(renamedNotice(invoked, `qore-${invoked.slice(4)}`));
|
|
10
|
+
|
|
11
|
+
// src/cli/sourcemaps.ts
|
|
12
|
+
import { execFile } from "child_process";
|
|
13
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { promisify } from "util";
|
|
18
|
+
|
|
19
|
+
// src/cli/sourcemaps-core.ts
|
|
20
|
+
import { posix } from "path";
|
|
21
|
+
var SHA = /^[0-9a-f]{40}$/;
|
|
22
|
+
var BUNDLE = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
23
|
+
var SCRIPT = /\.(js|mjs|cjs)$/;
|
|
24
|
+
var MAPPING_URL = /^\/\/# sourceMappingURL=(\S+)[ \t]*$/gm;
|
|
25
|
+
var STANDALONE_SERVER = "standalone/.next/server/";
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const [cmd, dir, ...rest] = argv;
|
|
28
|
+
if (cmd !== "upload" || !dir) return { error: "usage: qore-sourcemaps upload <dir> [--bundle <name>]" };
|
|
29
|
+
let bundle = "app";
|
|
30
|
+
if (rest[0] === "--bundle") bundle = rest[1] ?? "";
|
|
31
|
+
if (!BUNDLE.test(bundle)) return { error: `invalid bundle name: ${bundle}` };
|
|
32
|
+
return { dir, bundle };
|
|
33
|
+
}
|
|
34
|
+
function nextRoots(has) {
|
|
35
|
+
if (!has("BUILD_ID") || !has("server")) return null;
|
|
36
|
+
return ["static", "server"].filter(has);
|
|
37
|
+
}
|
|
38
|
+
function underNodeModules(p) {
|
|
39
|
+
return p.split("/").includes("node_modules");
|
|
40
|
+
}
|
|
41
|
+
function inRoots(p, roots) {
|
|
42
|
+
if (underNodeModules(p)) return false;
|
|
43
|
+
if (roots) return roots.some((r) => p.startsWith(`${r}/`));
|
|
44
|
+
return !p.startsWith("standalone/");
|
|
45
|
+
}
|
|
46
|
+
function lastMappingUrl(src) {
|
|
47
|
+
return [...src.matchAll(MAPPING_URL)].at(-1);
|
|
48
|
+
}
|
|
49
|
+
function fixMappingUrl(src) {
|
|
50
|
+
const m = lastMappingUrl(src);
|
|
51
|
+
if (!m?.[1] || !/%(5b|5d|40)/i.test(m[1])) return null;
|
|
52
|
+
const fixed = m[1].replace(/%5b/gi, "[").replace(/%5d/gi, "]").replace(/%40/g, "@");
|
|
53
|
+
const at = m.index ?? 0;
|
|
54
|
+
return src.slice(0, at) + `//# sourceMappingURL=${fixed}` + src.slice(at + m[0].length);
|
|
55
|
+
}
|
|
56
|
+
function mapFor(js, src) {
|
|
57
|
+
const url = lastMappingUrl(src)?.[1];
|
|
58
|
+
if (!url || url.startsWith("data:") || /^[a-z]+:/i.test(url)) return null;
|
|
59
|
+
let decoded = url;
|
|
60
|
+
try {
|
|
61
|
+
decoded = decodeURIComponent(url);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
const target = posix.normalize(posix.join(posix.dirname(js), decoded));
|
|
65
|
+
return target.startsWith("../") || posix.isAbsolute(target) ? null : target;
|
|
66
|
+
}
|
|
67
|
+
function listMaps(paths) {
|
|
68
|
+
return paths.filter((p) => p.endsWith(".map") && !underNodeModules(p));
|
|
69
|
+
}
|
|
70
|
+
function hasDebugId(text) {
|
|
71
|
+
try {
|
|
72
|
+
const map = JSON.parse(text);
|
|
73
|
+
const id = map?.debugId ?? map?.debug_id;
|
|
74
|
+
return typeof id === "string" && id.length > 0;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function listArtifacts(dir, walk, readText, roots) {
|
|
80
|
+
const all = walk(dir);
|
|
81
|
+
const set = new Set(all);
|
|
82
|
+
const used = /* @__PURE__ */ new Set();
|
|
83
|
+
const files = [];
|
|
84
|
+
let dropped = 0;
|
|
85
|
+
for (const js of all) {
|
|
86
|
+
if (!SCRIPT.test(js) || !inRoots(js, roots)) continue;
|
|
87
|
+
const map = mapFor(js, readText(js));
|
|
88
|
+
if (!map) continue;
|
|
89
|
+
if (!map.endsWith(".map") || !set.has(map) || !inRoots(map, roots) || used.has(map) || !hasDebugId(readText(map))) {
|
|
90
|
+
dropped++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
used.add(map);
|
|
94
|
+
files.push(js, map);
|
|
95
|
+
}
|
|
96
|
+
return { maps: listMaps(all), files, dropped };
|
|
97
|
+
}
|
|
98
|
+
function standaloneTwins(paths) {
|
|
99
|
+
const set = new Set(paths);
|
|
100
|
+
return paths.filter((p) => p.startsWith("server/") && SCRIPT.test(p)).map((p) => [p, `${STANDALONE_SERVER}${p.slice("server/".length)}`]).filter(([, twin]) => set.has(twin));
|
|
101
|
+
}
|
|
102
|
+
function uploadUrl(base, sha, bundle) {
|
|
103
|
+
return `${base.replace(/\/+$/, "")}/${sha}/${bundle}`;
|
|
104
|
+
}
|
|
105
|
+
function skipReason(env) {
|
|
106
|
+
for (const k of ["QORE_BUILD_UPLOAD_URL", "QORE_BUILD_UPLOAD_TOKEN", "NF_GIT_SHA"]) {
|
|
107
|
+
if (!env[k]) return `${k} is not set`;
|
|
108
|
+
}
|
|
109
|
+
if (!SHA.test(env.NF_GIT_SHA ?? "")) return "NF_GIT_SHA is not a commit sha";
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/cli/sourcemaps.ts
|
|
114
|
+
var execFileP = promisify(execFile);
|
|
115
|
+
var UPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
116
|
+
var RUN_TIMEOUT_MS = 12e4;
|
|
117
|
+
var SCRIPT2 = /\.(js|mjs|cjs)$/;
|
|
118
|
+
async function runCommand(cmd, args) {
|
|
119
|
+
await execFileP(cmd, args, {
|
|
120
|
+
timeout: RUN_TIMEOUT_MS,
|
|
121
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
122
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function walkDir(dir, prefix = "") {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const d of readdirSync(join(dir, prefix), { withFileTypes: true })) {
|
|
128
|
+
const rel = prefix ? `${prefix}/${d.name}` : d.name;
|
|
129
|
+
if (d.isDirectory() && d.name !== "node_modules") out.push(...walkDir(dir, rel));
|
|
130
|
+
else if (d.isFile()) out.push(rel);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
var defaultIo = () => ({
|
|
135
|
+
env: process.env,
|
|
136
|
+
walk: (dir) => walkDir(dir),
|
|
137
|
+
exists: existsSync,
|
|
138
|
+
readText: (p) => readFileSync(p, "utf8"),
|
|
139
|
+
writeText: (p, t) => writeFileSync(p, t),
|
|
140
|
+
copyFile: copyFileSync,
|
|
141
|
+
run: runCommand,
|
|
142
|
+
readFile: (p) => readFileSync(p),
|
|
143
|
+
remove: (p) => rmSync(p, { force: true }),
|
|
144
|
+
fetch,
|
|
145
|
+
tmpFile: () => join(tmpdir(), `qdc-maps-${process.pid}.tgz`)
|
|
146
|
+
});
|
|
147
|
+
function describe(err, token) {
|
|
148
|
+
const msg = (err instanceof Error ? err.message : String(err)).slice(0, 500);
|
|
149
|
+
return token ? msg.split(token).join("***") : msg;
|
|
150
|
+
}
|
|
151
|
+
function tryRemove(io, path) {
|
|
152
|
+
try {
|
|
153
|
+
io.remove(path);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function main(argv = process.argv.slice(2), io = defaultIo()) {
|
|
158
|
+
const args = parseArgs(argv);
|
|
159
|
+
if ("error" in args) {
|
|
160
|
+
console.warn(`qore-sourcemaps: ${args.error}`);
|
|
161
|
+
const dir2 = argv[0] === "upload" ? argv[1] : void 0;
|
|
162
|
+
if (dir2 && !dir2.startsWith("-")) {
|
|
163
|
+
try {
|
|
164
|
+
for (const m of listMaps(io.walk(dir2))) tryRemove(io, join(dir2, m));
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.warn(`qore-sourcemaps: cleanup skipped: ${describe(err, io.env.QORE_BUILD_UPLOAD_TOKEN)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const { dir, bundle } = args;
|
|
172
|
+
const at = (rel) => join(dir, rel);
|
|
173
|
+
let maps = [];
|
|
174
|
+
const scratch = [];
|
|
175
|
+
try {
|
|
176
|
+
const paths = io.walk(dir);
|
|
177
|
+
maps = listMaps(paths);
|
|
178
|
+
const skip = skipReason(io.env);
|
|
179
|
+
if (skip) {
|
|
180
|
+
console.log(`qore-sourcemaps: skipped (${skip})`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const roots = nextRoots((rel) => io.exists(at(rel)));
|
|
184
|
+
if (roots) {
|
|
185
|
+
for (const p of paths) {
|
|
186
|
+
if (!SCRIPT2.test(p) || !roots.some((r) => p.startsWith(`${r}/`))) continue;
|
|
187
|
+
const fixed = fixMappingUrl(io.readText(at(p)));
|
|
188
|
+
if (fixed !== null) io.writeText(at(p), fixed);
|
|
189
|
+
}
|
|
190
|
+
await io.run("sentry-cli", ["sourcemaps", "inject", ...roots.map(at)]);
|
|
191
|
+
for (const [src, twin] of standaloneTwins(paths)) io.copyFile(at(src), at(twin));
|
|
192
|
+
} else {
|
|
193
|
+
await io.run("sentry-cli", ["sourcemaps", "inject", dir]);
|
|
194
|
+
}
|
|
195
|
+
const { files, dropped } = listArtifacts(dir, () => paths, (rel) => io.readText(at(rel)), roots);
|
|
196
|
+
if (dropped > 0) console.log(`qore-sourcemaps: dropped ${dropped} source maps the uploader would reject`);
|
|
197
|
+
if (files.length === 0) {
|
|
198
|
+
console.log("qore-sourcemaps: no source maps found, nothing to upload");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const tgz = io.tmpFile();
|
|
202
|
+
const list = `${tgz}.list`;
|
|
203
|
+
scratch.push(list, tgz);
|
|
204
|
+
io.writeText(list, `${files.join("\n")}
|
|
205
|
+
`);
|
|
206
|
+
await io.run("tar", ["-czf", tgz, "-C", dir, "-T", list]);
|
|
207
|
+
const body = io.readFile(tgz);
|
|
208
|
+
const res = await io.fetch(uploadUrl(io.env.QORE_BUILD_UPLOAD_URL, io.env.NF_GIT_SHA, bundle), {
|
|
209
|
+
method: "PUT",
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${io.env.QORE_BUILD_UPLOAD_TOKEN}`,
|
|
212
|
+
"Content-Type": "application/gzip",
|
|
213
|
+
"Content-Length": String(body.byteLength)
|
|
214
|
+
},
|
|
215
|
+
body: new Uint8Array(body),
|
|
216
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
217
|
+
});
|
|
218
|
+
if (res.ok) console.log(`qore-sourcemaps: handed off ${files.length / 2} source maps (${body.byteLength} bytes)`);
|
|
219
|
+
else console.warn(`qore-sourcemaps: upload refused, broker answered ${res.status}`);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.warn(`qore-sourcemaps: upload skipped: ${describe(err, io.env.QORE_BUILD_UPLOAD_TOKEN)}`);
|
|
222
|
+
} finally {
|
|
223
|
+
for (const m of maps) tryRemove(io, at(m));
|
|
224
|
+
for (const s of scratch) tryRemove(io, s);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function isMainModule() {
|
|
228
|
+
if (!process.argv[1]) return false;
|
|
229
|
+
try {
|
|
230
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (isMainModule()) {
|
|
236
|
+
void main();
|
|
237
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/legacy/notice.ts
|
|
4
|
+
import { basename } from "path";
|
|
5
|
+
function renamedNotice(oldName, newName) {
|
|
6
|
+
return `\u26A0 ${oldName} is renamed to ${newName}; the old name keeps working until the next major release.`;
|
|
7
|
+
}
|
|
8
|
+
var invoked = basename(process.argv[1] ?? "", ".js");
|
|
9
|
+
if (invoked.startsWith("qdc-")) console.warn(renamedNotice(invoked, `qore-${invoked.slice(4)}`));
|
|
10
|
+
|
|
11
|
+
// src/cli/test-report.ts
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
13
|
+
import { dirname, resolve } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
// src/cli/test-report-core.ts
|
|
17
|
+
var SCHEMA_VERSION = 1;
|
|
18
|
+
function asRecord(value) {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function finiteNumber(value) {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function nonNegative(value) {
|
|
26
|
+
return value !== null && value >= 0 ? value : 0;
|
|
27
|
+
}
|
|
28
|
+
function countAssertions(testResults) {
|
|
29
|
+
let passed = 0;
|
|
30
|
+
let failed = 0;
|
|
31
|
+
let skipped = 0;
|
|
32
|
+
for (const file of testResults) {
|
|
33
|
+
const rec = asRecord(file);
|
|
34
|
+
const assertions = rec?.assertionResults;
|
|
35
|
+
if (!Array.isArray(assertions)) continue;
|
|
36
|
+
for (const assertion of assertions) {
|
|
37
|
+
const status = asRecord(assertion)?.status;
|
|
38
|
+
if (status === "passed") passed += 1;
|
|
39
|
+
else if (status === "failed") failed += 1;
|
|
40
|
+
else if (status === "pending" || status === "todo" || status === "skipped") skipped += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { passed, failed, skipped };
|
|
44
|
+
}
|
|
45
|
+
function spanMs(testResults) {
|
|
46
|
+
let earliest = null;
|
|
47
|
+
let latest = null;
|
|
48
|
+
for (const file of testResults) {
|
|
49
|
+
const rec = asRecord(file);
|
|
50
|
+
if (!rec) continue;
|
|
51
|
+
const start = finiteNumber(rec.startTime);
|
|
52
|
+
const end = finiteNumber(rec.endTime);
|
|
53
|
+
if (start !== null && (earliest === null || start < earliest)) earliest = start;
|
|
54
|
+
if (end !== null && (latest === null || end > latest)) latest = end;
|
|
55
|
+
}
|
|
56
|
+
if (earliest === null || latest === null || latest < earliest) return 0;
|
|
57
|
+
return Math.round(latest - earliest);
|
|
58
|
+
}
|
|
59
|
+
function summarizeVitestJson(raw) {
|
|
60
|
+
const root = asRecord(raw);
|
|
61
|
+
if (!root) return null;
|
|
62
|
+
const testResults = Array.isArray(root.testResults) ? root.testResults : [];
|
|
63
|
+
const topPassed = finiteNumber(root.numPassedTests);
|
|
64
|
+
const topFailed = finiteNumber(root.numFailedTests);
|
|
65
|
+
const topPending = finiteNumber(root.numPendingTests);
|
|
66
|
+
const topTodo = finiteNumber(root.numTodoTests);
|
|
67
|
+
const hasTopLevelCounts = topPassed !== null || topFailed !== null;
|
|
68
|
+
if (!hasTopLevelCounts && testResults.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const counted = hasTopLevelCounts ? {
|
|
72
|
+
passed: nonNegative(topPassed),
|
|
73
|
+
failed: nonNegative(topFailed),
|
|
74
|
+
skipped: nonNegative(topPending) + nonNegative(topTodo)
|
|
75
|
+
} : countAssertions(testResults);
|
|
76
|
+
return { ...counted, durationMs: spanMs(testResults) };
|
|
77
|
+
}
|
|
78
|
+
function summarizeCoverage(raw) {
|
|
79
|
+
const total = asRecord(asRecord(raw)?.total);
|
|
80
|
+
if (!total) return null;
|
|
81
|
+
const lines = finiteNumber(asRecord(total.lines)?.pct);
|
|
82
|
+
const branches = finiteNumber(asRecord(total.branches)?.pct);
|
|
83
|
+
if (lines === null && branches === null) return null;
|
|
84
|
+
return { linesPct: lines, branchesPct: branches };
|
|
85
|
+
}
|
|
86
|
+
function buildReport(input) {
|
|
87
|
+
const suites = {};
|
|
88
|
+
if (input.unit.kind === "ok") suites.unit = input.unit.summary;
|
|
89
|
+
if (input.integration.kind === "ok") suites.integration = input.integration.summary;
|
|
90
|
+
const measured = Object.keys(suites).length > 0;
|
|
91
|
+
const anyParseFailure = input.unit.kind === "parse_failed" || input.integration.kind === "parse_failed";
|
|
92
|
+
return {
|
|
93
|
+
schemaVersion: SCHEMA_VERSION,
|
|
94
|
+
commitSha: input.commitSha,
|
|
95
|
+
ranAt: input.ranAt,
|
|
96
|
+
measured,
|
|
97
|
+
reason: measured ? null : anyParseFailure ? "parse_failed" : "no_results",
|
|
98
|
+
suites,
|
|
99
|
+
coverage: input.coverage
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function unmeasuredReport(commitSha, ranAt, reason) {
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: SCHEMA_VERSION,
|
|
105
|
+
commitSha,
|
|
106
|
+
ranAt,
|
|
107
|
+
measured: false,
|
|
108
|
+
reason,
|
|
109
|
+
suites: {},
|
|
110
|
+
coverage: null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/cli/test-report.ts
|
|
115
|
+
var DEFAULT_OUT = ".qdc/test-report.json";
|
|
116
|
+
function parseArgs(argv) {
|
|
117
|
+
const args = argv.slice();
|
|
118
|
+
const opts = {
|
|
119
|
+
unit: null,
|
|
120
|
+
integration: null,
|
|
121
|
+
coverage: null,
|
|
122
|
+
commitSha: null,
|
|
123
|
+
out: DEFAULT_OUT
|
|
124
|
+
};
|
|
125
|
+
while (args.length) {
|
|
126
|
+
const arg = args.shift();
|
|
127
|
+
if (arg === "--unit") opts.unit = args.shift() ?? null;
|
|
128
|
+
else if (arg === "--integration") opts.integration = args.shift() ?? null;
|
|
129
|
+
else if (arg === "--coverage") opts.coverage = args.shift() ?? null;
|
|
130
|
+
else if (arg === "--commit-sha") opts.commitSha = args.shift() ?? null;
|
|
131
|
+
else if (arg === "--out") opts.out = args.shift() ?? opts.out;
|
|
132
|
+
}
|
|
133
|
+
return opts;
|
|
134
|
+
}
|
|
135
|
+
function readJson(path) {
|
|
136
|
+
if (!path) return { kind: "absent" };
|
|
137
|
+
const resolved = resolve(path);
|
|
138
|
+
if (!existsSync(resolved)) return { kind: "absent" };
|
|
139
|
+
try {
|
|
140
|
+
return { kind: "ok", value: JSON.parse(readFileSync(resolved, "utf8")) };
|
|
141
|
+
} catch {
|
|
142
|
+
return { kind: "bad" };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function readSuite(path) {
|
|
146
|
+
const read = readJson(path);
|
|
147
|
+
if (read.kind === "absent") return { kind: "absent" };
|
|
148
|
+
if (read.kind === "bad") return { kind: "parse_failed" };
|
|
149
|
+
const summary = summarizeVitestJson(read.value);
|
|
150
|
+
return summary ? { kind: "ok", summary } : { kind: "parse_failed" };
|
|
151
|
+
}
|
|
152
|
+
function readCoverage(path) {
|
|
153
|
+
const read = readJson(path);
|
|
154
|
+
return read.kind === "ok" ? summarizeCoverage(read.value) : null;
|
|
155
|
+
}
|
|
156
|
+
function writeReport(out, report) {
|
|
157
|
+
const resolved = resolve(out);
|
|
158
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
159
|
+
writeFileSync(resolved, `${JSON.stringify(report, null, 2)}
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
function main(argv = process.argv.slice(2)) {
|
|
163
|
+
let out = DEFAULT_OUT;
|
|
164
|
+
let commitSha = null;
|
|
165
|
+
const ranAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
166
|
+
try {
|
|
167
|
+
const opts = parseArgs(argv);
|
|
168
|
+
out = opts.out;
|
|
169
|
+
commitSha = opts.commitSha;
|
|
170
|
+
const report = buildReport({
|
|
171
|
+
commitSha: opts.commitSha,
|
|
172
|
+
ranAt,
|
|
173
|
+
unit: readSuite(opts.unit),
|
|
174
|
+
integration: readSuite(opts.integration),
|
|
175
|
+
coverage: readCoverage(opts.coverage)
|
|
176
|
+
});
|
|
177
|
+
writeReport(out, report);
|
|
178
|
+
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})`;
|
|
179
|
+
console.log(`qore-test-report \u2192 ${out} \u2014 ${summary}`);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`qore-test-report could not build a report: ${err instanceof Error ? err.message : String(err)}`
|
|
183
|
+
);
|
|
184
|
+
try {
|
|
185
|
+
writeReport(out, unmeasuredReport(commitSha, ranAt, "parse_failed"));
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isMainModule() {
|
|
191
|
+
if (!process.argv[1]) return false;
|
|
192
|
+
try {
|
|
193
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (isMainModule()) {
|
|
199
|
+
main();
|
|
200
|
+
}
|