@codyswann/lisa 3.35.0 → 3.35.1
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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +5 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/workflows/nightly-e2e-health.yml +1 -1
- package/expo/create-only/.github/workflows/nightly-e2e-report.yml +1 -1
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/scripts/check-template-workflow-refs.mjs +363 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic gate that every `uses:` in a shipped caller template resolves
|
|
4
|
+
* at the ref it names (issue #2702).
|
|
5
|
+
*
|
|
6
|
+
* Two expo templates shipped pinned at `@v2.345.1`, a Lisa version that
|
|
7
|
+
* predates the reusable they call. The referenced file does not exist at that
|
|
8
|
+
* tag, so the workflow cannot load: GitHub reports "this run likely failed
|
|
9
|
+
* because of a workflow file issue" and runs ZERO jobs.
|
|
10
|
+
*
|
|
11
|
+
* Nothing caught it, and the reason is the point of this gate:
|
|
12
|
+
*
|
|
13
|
+
* - a workflow that fails to LOAD produces no test output, so no suite goes
|
|
14
|
+
* red and no report shows a failure to read;
|
|
15
|
+
* - `nightly-e2e-report.yml` therefore stayed byte-identical to the broken
|
|
16
|
+
* template in every consumer that received it, and a filename/hash audit
|
|
17
|
+
* scored that as the HEALTHY row — identical-and-never-worked outranks
|
|
18
|
+
* diverged-and-working on every measure except whether it works;
|
|
19
|
+
* - it had never once succeeded: three scheduled runs across two repos,
|
|
20
|
+
* three failures, zero successes, from the day it shipped.
|
|
21
|
+
*
|
|
22
|
+
* The sibling defect was visible and so it was repaired four different ways.
|
|
23
|
+
* `nightly-e2e-health.yml` backs a required gate, so consumers noticed
|
|
24
|
+
* instantly: one repointed to `@v3.27.0`, one to `@v3.14.8`, and two abandoned
|
|
25
|
+
* the reusable and reimplemented it locally. None of those repairs flowed back
|
|
26
|
+
* upstream, so every new consumer inherited the same broken pin.
|
|
27
|
+
*
|
|
28
|
+
* This script reads what the templates actually reference and confirms the
|
|
29
|
+
* target exists. Only that.
|
|
30
|
+
*
|
|
31
|
+
* Determinism guarantees (so the unit test is reproducible and CI is stable):
|
|
32
|
+
* - zero third-party dependencies (Node built-ins only),
|
|
33
|
+
* - no network access,
|
|
34
|
+
* - no `Date` / `Math.random`,
|
|
35
|
+
* - the file list comes from `git ls-files`, so the gate sees exactly what a
|
|
36
|
+
* release would carry.
|
|
37
|
+
*
|
|
38
|
+
* A `@main` reference is checked against the working tree, which needs no
|
|
39
|
+
* history and so is correct in a depth-1 CI clone. A reference pinned to a tag
|
|
40
|
+
* or SHA needs that object locally; when it is missing the gate reports
|
|
41
|
+
* UNVERIFIABLE and exits 2. It never treats "I could not look" as "it is fine"
|
|
42
|
+
* — a gate that passes because it did nothing is the exact failure this file
|
|
43
|
+
* exists to prevent, and it would be perverse to reproduce it here.
|
|
44
|
+
*
|
|
45
|
+
* Discovering zero templates is likewise exit 2, not a clean pass. Finding
|
|
46
|
+
* nothing to check is a broken invocation, not conformance.
|
|
47
|
+
*
|
|
48
|
+
* CLI:
|
|
49
|
+
* node scripts/check-template-workflow-refs.mjs [--root <dir>] [--json]
|
|
50
|
+
*
|
|
51
|
+
* Exit codes (mirroring the sibling parity scripts):
|
|
52
|
+
* 0 — every `uses:` in every caller template resolves at its ref.
|
|
53
|
+
* 1 — ≥1 reference names a target that does not exist at that ref.
|
|
54
|
+
* 2 — operational/usage error: unknown flag, a flag missing its value,
|
|
55
|
+
* `--root` absent or not a git repository, git unavailable, zero
|
|
56
|
+
* templates discovered, or a pinned ref that is not present locally.
|
|
57
|
+
*
|
|
58
|
+
* @module scripts/check-template-workflow-refs
|
|
59
|
+
*/
|
|
60
|
+
import { execFileSync } from "node:child_process";
|
|
61
|
+
import fs from "node:fs";
|
|
62
|
+
import path from "node:path";
|
|
63
|
+
import process from "node:process";
|
|
64
|
+
import { fileURLToPath } from "node:url";
|
|
65
|
+
|
|
66
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
67
|
+
|
|
68
|
+
const REPO_ROOT = path.resolve(
|
|
69
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
70
|
+
".."
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A caller template: `<lane>/<mode>/.github/workflows/<name>.yml`, e.g.
|
|
75
|
+
* `expo/create-only/.github/workflows/maestro-e2e.yml`. The two leading
|
|
76
|
+
* segments are what distinguishes a shipped template from Lisa's OWN
|
|
77
|
+
* `.github/workflows/`, which is not delivered to anyone.
|
|
78
|
+
*/
|
|
79
|
+
const TEMPLATE_RE = /^[^/]+\/[^/]+\/\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
80
|
+
|
|
81
|
+
/** `uses: CodySwannGT/lisa/<path>@<ref>` — the reference this gate verifies. */
|
|
82
|
+
const USES_RE = /uses:\s*CodySwannGT\/lisa\/([^\s@]+)@([^\s"'#]+)/g;
|
|
83
|
+
|
|
84
|
+
/** The moving ref, resolvable against the working tree with no history. */
|
|
85
|
+
const MOVING_REF = "main";
|
|
86
|
+
|
|
87
|
+
/** Max bytes of `git ls-files` output (7k+ tracked paths is ~0.3 MB today). */
|
|
88
|
+
const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Usage error — thrown for an invalid invocation or an unverifiable state so
|
|
92
|
+
* `main` can distinguish it (exit 2) from a finding (exit 1).
|
|
93
|
+
*/
|
|
94
|
+
export class UsageError extends Error {}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract every Lisa workflow reference in `content`.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} content - full text of a template.
|
|
100
|
+
* @returns {{ target: string, ref: string, line: number }[]} one entry per
|
|
101
|
+
* reference, with 1-based line numbers, in file order.
|
|
102
|
+
*/
|
|
103
|
+
export function findWorkflowRefs(content) {
|
|
104
|
+
const lines = String(content).split(/\r?\n/);
|
|
105
|
+
const refs = [];
|
|
106
|
+
for (let index = 0; index < lines.length; index++) {
|
|
107
|
+
// A fresh lastIndex per line: the regex is global and shared.
|
|
108
|
+
USES_RE.lastIndex = 0;
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = USES_RE.exec(lines[index])) !== null) {
|
|
111
|
+
refs.push({ line: index + 1, ref: match[2], target: match[1] });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return refs;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List every tracked file in `root`, relative to it. Throws `UsageError` when
|
|
119
|
+
* git is unavailable or `root` is not a repository.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} root - the repository root.
|
|
122
|
+
* @returns {string[]} tracked paths, relative to `root`.
|
|
123
|
+
*/
|
|
124
|
+
function listTrackedFiles(root) {
|
|
125
|
+
let stdout;
|
|
126
|
+
try {
|
|
127
|
+
stdout = execFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
130
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new UsageError(
|
|
134
|
+
`could not list tracked files in ${root}: ${error.message}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return stdout.split("\0").filter(entry => entry !== "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether `ref` names an object present in this clone.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} root - the repository root.
|
|
144
|
+
* @param {string} ref - a tag, branch, or SHA.
|
|
145
|
+
* @returns {boolean} whether the ref resolves locally.
|
|
146
|
+
*/
|
|
147
|
+
function refExists(root, ref) {
|
|
148
|
+
try {
|
|
149
|
+
execFileSync(
|
|
150
|
+
"git",
|
|
151
|
+
["-C", root, "rev-parse", "--verify", `${ref}^{commit}`],
|
|
152
|
+
{
|
|
153
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Whether `target` exists in the tree named by `ref`.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} root - the repository root.
|
|
166
|
+
* @param {string} ref - a tag, branch, or SHA known to resolve.
|
|
167
|
+
* @param {string} target - a repo-relative path.
|
|
168
|
+
* @returns {boolean} whether the path exists at that ref.
|
|
169
|
+
*/
|
|
170
|
+
function pathExistsAtRef(root, ref, target) {
|
|
171
|
+
try {
|
|
172
|
+
execFileSync("git", ["-C", root, "cat-file", "-e", `${ref}:${target}`], {
|
|
173
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve one reference to a verdict.
|
|
183
|
+
*
|
|
184
|
+
* `@main` is checked against the working tree so the gate is correct in a
|
|
185
|
+
* depth-1 clone, where `main` frequently is not a local ref at all. A pinned
|
|
186
|
+
* ref that is absent locally yields `unverifiable`, never `ok`.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} root - the repository root.
|
|
189
|
+
* @param {{ target: string, ref: string }} reference - the parsed reference.
|
|
190
|
+
* @returns {"ok" | "missing" | "unverifiable"} the verdict.
|
|
191
|
+
*/
|
|
192
|
+
export function classifyRef(root, reference) {
|
|
193
|
+
const { ref, target } = reference;
|
|
194
|
+
if (ref === MOVING_REF) {
|
|
195
|
+
return fs.existsSync(path.join(root, target)) ? "ok" : "missing";
|
|
196
|
+
}
|
|
197
|
+
if (!refExists(root, ref)) {
|
|
198
|
+
return "unverifiable";
|
|
199
|
+
}
|
|
200
|
+
return pathExistsAtRef(root, ref, target) ? "ok" : "missing";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Assemble the machine-readable report.
|
|
205
|
+
*
|
|
206
|
+
* @param {ReadonlyArray<Record<string, unknown>>} results - non-ok references.
|
|
207
|
+
* @param {{ root: string, templates: number, checked: number }} opts - resolved
|
|
208
|
+
* options plus scan size.
|
|
209
|
+
* @returns {Record<string, unknown>} the report object.
|
|
210
|
+
*/
|
|
211
|
+
export function buildReport(results, opts) {
|
|
212
|
+
const missing = results.filter(result => result.verdict === "missing");
|
|
213
|
+
const unverifiable = results.filter(
|
|
214
|
+
result => result.verdict === "unverifiable"
|
|
215
|
+
);
|
|
216
|
+
return {
|
|
217
|
+
results,
|
|
218
|
+
root: opts.root,
|
|
219
|
+
schemaVersion: 1,
|
|
220
|
+
summary: {
|
|
221
|
+
checked: opts.checked,
|
|
222
|
+
missing: missing.length,
|
|
223
|
+
ok: opts.checked - results.length,
|
|
224
|
+
templates: opts.templates,
|
|
225
|
+
unverifiable: unverifiable.length,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render the human-readable report.
|
|
232
|
+
*
|
|
233
|
+
* @param {{ results: ReadonlyArray<Record<string, unknown>>, summary: Record<string, number> }} report
|
|
234
|
+
* the report object.
|
|
235
|
+
* @returns {string} the rendered report.
|
|
236
|
+
*/
|
|
237
|
+
function humanReport(report) {
|
|
238
|
+
const { summary } = report;
|
|
239
|
+
if (summary.missing === 0 && summary.unverifiable === 0) {
|
|
240
|
+
return `✓ ${summary.checked} workflow reference(s) across ${summary.templates} caller template(s) all resolve`;
|
|
241
|
+
}
|
|
242
|
+
const lines = report.results.map(result =>
|
|
243
|
+
result.verdict === "missing"
|
|
244
|
+
? `✗ ${result.file}:${result.line}\n ${result.target}@${result.ref} — target does not exist at that ref`
|
|
245
|
+
: `? ${result.file}:${result.line}\n ${result.target}@${result.ref} — ref not present in this clone, cannot verify`
|
|
246
|
+
);
|
|
247
|
+
const tail = [];
|
|
248
|
+
if (summary.missing > 0) {
|
|
249
|
+
tail.push(
|
|
250
|
+
`${summary.missing} reference(s) name a target that does not exist at the pinned ref.`,
|
|
251
|
+
"A consumer receiving this template gets a workflow that cannot load:",
|
|
252
|
+
"GitHub runs zero jobs and reports a workflow file issue, which produces",
|
|
253
|
+
"no test output and so reads as silence rather than as failure."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (summary.unverifiable > 0) {
|
|
257
|
+
tail.push(
|
|
258
|
+
`${summary.unverifiable} reference(s) could not be verified because the ref`,
|
|
259
|
+
"is absent from this clone. Fetch tags and history, then re-run. This is",
|
|
260
|
+
"reported rather than passed: a gate that cannot look must not claim it saw."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return [...lines, "", ...tail].join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse argv into resolved options. Throws `UsageError` on a bad invocation.
|
|
268
|
+
*
|
|
269
|
+
* @param {readonly string[]} argv - arguments (without node/script prefix).
|
|
270
|
+
* @returns {{ root: string, json: boolean }} options.
|
|
271
|
+
*/
|
|
272
|
+
export function parseArgs(argv) {
|
|
273
|
+
let root = null;
|
|
274
|
+
let json = false;
|
|
275
|
+
for (let i = 0; i < argv.length; i++) {
|
|
276
|
+
const arg = argv[i];
|
|
277
|
+
if (arg === "--json") {
|
|
278
|
+
json = true;
|
|
279
|
+
} else if (arg === "--root") {
|
|
280
|
+
const next = argv[i + 1];
|
|
281
|
+
if (next === undefined || next.startsWith("--")) {
|
|
282
|
+
throw new UsageError("--root requires a value");
|
|
283
|
+
}
|
|
284
|
+
root = next;
|
|
285
|
+
i += 1;
|
|
286
|
+
} else {
|
|
287
|
+
throw new UsageError(`unknown argument: ${arg}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { json, root: path.resolve(root ?? REPO_ROOT) };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Run the gate. Returns the process exit code (does not call `exit`).
|
|
295
|
+
*
|
|
296
|
+
* @param {readonly string[]} argv - arguments (without node/script prefix).
|
|
297
|
+
* @param {{ stdout?: { write(s: string): void }, stderr?: { write(s: string): void } }} [io]
|
|
298
|
+
* injectable streams (defaults to process streams).
|
|
299
|
+
* @returns {number} exit code (0 clean, 1 finding, 2 usage/unverifiable).
|
|
300
|
+
*/
|
|
301
|
+
export function main(argv, io = {}) {
|
|
302
|
+
const out = io.stdout ?? process.stdout;
|
|
303
|
+
const err = io.stderr ?? process.stderr;
|
|
304
|
+
let opts;
|
|
305
|
+
let templates;
|
|
306
|
+
try {
|
|
307
|
+
opts = parseArgs(argv);
|
|
308
|
+
if (!fs.existsSync(opts.root) || !fs.statSync(opts.root).isDirectory()) {
|
|
309
|
+
throw new UsageError(`--root is not a directory: ${opts.root}`);
|
|
310
|
+
}
|
|
311
|
+
templates = listTrackedFiles(opts.root).filter(file =>
|
|
312
|
+
TEMPLATE_RE.test(file)
|
|
313
|
+
);
|
|
314
|
+
if (templates.length === 0) {
|
|
315
|
+
// Finding nothing to check is a broken invocation, not conformance.
|
|
316
|
+
throw new UsageError(
|
|
317
|
+
`no caller templates found under ${opts.root} — expected paths like ` +
|
|
318
|
+
"expo/create-only/.github/workflows/*.yml. Refusing to report a " +
|
|
319
|
+
"clean run for a scan that examined nothing."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
err.write(`error: ${error.message}\n`);
|
|
324
|
+
return 2;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let checked = 0;
|
|
328
|
+
const results = [];
|
|
329
|
+
try {
|
|
330
|
+
for (const file of templates) {
|
|
331
|
+
const content = fs.readFileSync(path.join(opts.root, file), "utf8");
|
|
332
|
+
for (const reference of findWorkflowRefs(content)) {
|
|
333
|
+
checked += 1;
|
|
334
|
+
const verdict = classifyRef(opts.root, reference);
|
|
335
|
+
if (verdict !== "ok") {
|
|
336
|
+
results.push({ ...reference, file, verdict });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
err.write(`error: failed to scan caller templates: ${error.message}\n`);
|
|
342
|
+
return 2;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const report = buildReport(results, {
|
|
346
|
+
checked,
|
|
347
|
+
root: opts.root,
|
|
348
|
+
templates: templates.length,
|
|
349
|
+
});
|
|
350
|
+
out.write(
|
|
351
|
+
`${opts.json ? JSON.stringify(report, null, 2) : humanReport(report)}\n`
|
|
352
|
+
);
|
|
353
|
+
if (report.summary.unverifiable > 0) {
|
|
354
|
+
return 2;
|
|
355
|
+
}
|
|
356
|
+
return report.summary.missing === 0 ? 0 : 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (invokedAsScript(import.meta.url)) {
|
|
360
|
+
// exitCode (not process.exit): when stdout is a pipe, writes are async and
|
|
361
|
+
// process.exit() truncates the report mid-flush.
|
|
362
|
+
process.exitCode = main(process.argv.slice(2));
|
|
363
|
+
}
|