@mmerterden/multi-agent-pipeline 17.1.0 → 17.4.0
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/CHANGELOG.md +194 -0
- package/README.md +7 -0
- package/README.tr.md +7 -0
- package/docs/token-budget-history.md +22 -0
- package/install/_dev-only-files.mjs +1 -0
- package/install/codex.mjs +18 -1
- package/install/copilot.mjs +17 -1
- package/package.json +1 -1
- package/pipeline/agents/code-reviewer.md +35 -1
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +9 -1
- package/pipeline/lib/autopilot-state.sh +34 -0
- package/pipeline/multi-agent-refs/_dev-context.md +10 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
- package/pipeline/multi-agent-refs/analysis/redesign.md +8 -0
- package/pipeline/multi-agent-refs/analysis/render.md +2 -1
- package/pipeline/multi-agent-refs/analysis/review.md +9 -0
- package/pipeline/multi-agent-refs/android-guide.md +14 -0
- package/pipeline/multi-agent-refs/audit-guide.md +12 -0
- package/pipeline/multi-agent-refs/backend-guide.md +10 -0
- package/pipeline/multi-agent-refs/channels/confluence.md +11 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +12 -0
- package/pipeline/multi-agent-refs/channels/jira.md +10 -0
- package/pipeline/multi-agent-refs/channels/pr-review-actions.md +13 -0
- package/pipeline/multi-agent-refs/channels/pr.md +37 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +11 -0
- package/pipeline/multi-agent-refs/component-generation.md +11 -0
- package/pipeline/multi-agent-refs/conventions-defaults.md +15 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +65 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +11 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +40 -0
- package/pipeline/multi-agent-refs/features/design-conformance.md +24 -0
- package/pipeline/multi-agent-refs/features/doctor.md +10 -0
- package/pipeline/multi-agent-refs/features/external-context-injection.md +7 -0
- package/pipeline/multi-agent-refs/features/jira-context.md +9 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +10 -0
- package/pipeline/multi-agent-refs/features/review-file-set.md +132 -0
- package/pipeline/multi-agent-refs/features/skill-conformance.md +13 -0
- package/pipeline/multi-agent-refs/features/url-enrichment.md +9 -0
- package/pipeline/multi-agent-refs/features/visual-evidence.md +42 -0
- package/pipeline/multi-agent-refs/generate-issue.md +7 -0
- package/pipeline/multi-agent-refs/issue-jira-triad.md +9 -0
- package/pipeline/multi-agent-refs/knowledge.md +6 -0
- package/pipeline/multi-agent-refs/multi-repo-integration-build.md +13 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -0
- package/pipeline/multi-agent-refs/phases/operations.md +9 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +31 -23
- package/pipeline/multi-agent-refs/phases.md +11 -0
- package/pipeline/multi-agent-refs/picker-contract.md +12 -0
- package/pipeline/multi-agent-refs/platform-parity.md +10 -0
- package/pipeline/multi-agent-refs/progress-contract.md +10 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +9 -0
- package/pipeline/multi-agent-refs/swiftui-guide.md +17 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +12 -0
- package/pipeline/multi-agent-refs/web-guide.md +10 -0
- package/pipeline/multi-agent-refs/wiki-capture.md +11 -0
- package/pipeline/schemas/prefs.schema.json +4 -0
- package/pipeline/schemas/review-file-exclusions.json +137 -0
- package/pipeline/schemas/reviewer-output.schema.json +27 -1
- package/pipeline/schemas/token-budget.json +10 -19
- package/pipeline/scripts/autopilot-intake.mjs +5 -1
- package/pipeline/scripts/autopilot-runner.mjs +6 -1
- package/pipeline/scripts/autopilot-status.sh +3 -2
- package/pipeline/scripts/capture-evidence.sh +79 -11
- package/pipeline/scripts/diff-risk-score.mjs +1 -36
- package/pipeline/scripts/gen-ref-toc.mjs +279 -0
- package/pipeline/scripts/git-path.mjs +63 -0
- package/pipeline/scripts/glob-match.mjs +62 -0
- package/pipeline/scripts/graph-mermaid.mjs +251 -0
- package/pipeline/scripts/review-file-filter.mjs +180 -0
- package/pipeline/scripts/skill-conformance.mjs +1 -31
- package/pipeline/scripts/validate-analysis-doc.mjs +53 -0
- package/pipeline/scripts/validate-reviewer.mjs +90 -1
- package/pipeline/scripts/verify-citations.mjs +346 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file verify-citations.mjs - a `file:line` that nothing resolves is a guess.
|
|
5
|
+
*
|
|
6
|
+
* Two surfaces in this pipeline require a citation and neither has ever checked
|
|
7
|
+
* one. `reviewer-output.schema.json` makes `file` and `line` required on every
|
|
8
|
+
* finding; `analysis-spec.schema.json` carries `repoEvidence` entries of
|
|
9
|
+
* `{name, file, line, tag}` and `analysis/locked.md` (Locked 37) asks for
|
|
10
|
+
* `repo/file:line` on every current-behaviour row. The only check that existed
|
|
11
|
+
* was `validate-analysis-doc.mjs`'s `/:\d+\b/` - a test that the row LOOKS like
|
|
12
|
+
* a citation, satisfied by `Foo.swift:9999` in a repo with no Foo.swift.
|
|
13
|
+
*
|
|
14
|
+
* The judgement is moved out of the model and into git. A path is resolved at a
|
|
15
|
+
* commit, not on the working tree, so a run that cites a file deleted three
|
|
16
|
+
* commits ago fails instead of passing on whatever happens to be checked out.
|
|
17
|
+
*
|
|
18
|
+
* Inputs:
|
|
19
|
+
* <file> JSON or markdown to read. `-` reads stdin.
|
|
20
|
+
* --repo <path> Repository root. Default: cwd
|
|
21
|
+
* --rev <sha> Commit to resolve against. Default: HEAD
|
|
22
|
+
* --worktree Resolve against the CHECKOUT instead of a commit. For
|
|
23
|
+
* Phase 4, where a reviewer cites lines the dev has not
|
|
24
|
+
* committed yet and a commit-pinned check would call
|
|
25
|
+
* every one of them invented.
|
|
26
|
+
* --json Machine-readable report
|
|
27
|
+
* --quiet Exit code only
|
|
28
|
+
*
|
|
29
|
+
* Accepted shapes, detected rather than declared, so one script serves both
|
|
30
|
+
* callers and neither has to say which it is:
|
|
31
|
+
* - reviewer output `{findings: [{file, line, ...}]}`
|
|
32
|
+
* - triage output `{accepted|deferred|rejected: [{file, line, ...}]}`
|
|
33
|
+
* - analysis spec `{evidence: {repoEvidence: {<repo>: {buckets: ...}}}}`
|
|
34
|
+
* - markdown every `path/to/file.ext:123` in the prose
|
|
35
|
+
*
|
|
36
|
+
* Exit codes:
|
|
37
|
+
* 0 - every citation resolved, or there were none to resolve
|
|
38
|
+
* 1 - at least one citation did not resolve
|
|
39
|
+
* 2 - the input could not be read or is not a repository
|
|
40
|
+
* 64 - usage error
|
|
41
|
+
*
|
|
42
|
+
* @module pipeline/scripts/verify-citations
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
46
|
+
import { resolve as resolvePath, sep } from "node:path";
|
|
47
|
+
import { spawnSync } from "node:child_process";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reject a path before it reaches git.
|
|
51
|
+
*
|
|
52
|
+
* `git show <rev>:<path>` happily resolves `../` out of the tree and an
|
|
53
|
+
* absolute path against the filesystem, so a citation could name something the
|
|
54
|
+
* repository does not contain and still "resolve". The guard is here rather
|
|
55
|
+
* than in the caller because every caller would otherwise need it.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} p
|
|
58
|
+
* @returns {string|null} the reason it is unsafe, or null
|
|
59
|
+
*/
|
|
60
|
+
export function unsafePath(p) {
|
|
61
|
+
if (!p) return "empty path";
|
|
62
|
+
if (p.startsWith("/")) return "absolute path";
|
|
63
|
+
if (p.includes("\\")) return "backslash in path";
|
|
64
|
+
const parts = p.split("/");
|
|
65
|
+
if (parts.includes("..")) return "path escapes the repository";
|
|
66
|
+
if (parts.includes(".git")) return "path reaches into .git";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pull `{file, line}` pairs out of whatever was handed over.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} raw
|
|
74
|
+
* @returns {{file: string, line: number|null, where: string}[]}
|
|
75
|
+
*/
|
|
76
|
+
export function extract(raw) {
|
|
77
|
+
const out = [];
|
|
78
|
+
const push = (file, line, where) => {
|
|
79
|
+
if (typeof file === "string" && file.trim()) {
|
|
80
|
+
out.push({ file: file.trim(), line: Number.isFinite(line) ? line : null, where });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let doc;
|
|
85
|
+
try {
|
|
86
|
+
doc = JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
doc = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (doc && typeof doc === "object") {
|
|
92
|
+
for (const key of ["findings", "accepted", "deferred", "rejected"]) {
|
|
93
|
+
for (const [i, f] of (doc[key] || []).entries()) {
|
|
94
|
+
if (f && typeof f === "object") push(f.file, Number(f.line), `${key}[${i}]`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const repoEvidence = doc?.evidence?.repoEvidence || doc?.analysisSpec?.evidence?.repoEvidence;
|
|
98
|
+
if (repoEvidence && typeof repoEvidence === "object") {
|
|
99
|
+
for (const [repo, body] of Object.entries(repoEvidence)) {
|
|
100
|
+
for (const [bucket, rows] of Object.entries(body?.buckets || {})) {
|
|
101
|
+
for (const [i, r] of (rows || []).entries()) {
|
|
102
|
+
if (r && typeof r === "object") {
|
|
103
|
+
push(r.file, Number(r.line), `repoEvidence.${repo}.${bucket}[${i}]`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Markdown. A citation is a path with an extension followed by a line number.
|
|
113
|
+
//
|
|
114
|
+
// Two things that look exactly like one and are not:
|
|
115
|
+
//
|
|
116
|
+
// - a URL with a port. `https://wiki.example.com:8443/x` hands
|
|
117
|
+
// `wiki.example.com:8443` to the matcher, and an analysis document is
|
|
118
|
+
// full of Confluence, Jira and Figma links. Left in, this gate blocked
|
|
119
|
+
// dispatch on every real document and reported "absolute path" as the
|
|
120
|
+
// reason, which is not even the right complaint. URLs are removed first.
|
|
121
|
+
// - a dotted number. `v1.2:30` has an "extension" of `2`, so the extension
|
|
122
|
+
// must START with a letter. No source file is named `.2`.
|
|
123
|
+
//
|
|
124
|
+
// Fenced blocks are skipped for the same class of reason: a code sample that
|
|
125
|
+
// happens to contain `foo.js:12` is an illustration, not a claim about this
|
|
126
|
+
// repository.
|
|
127
|
+
let fenced = false;
|
|
128
|
+
raw.split("\n").forEach((line, i) => {
|
|
129
|
+
if (/^\s*```/.test(line)) {
|
|
130
|
+
fenced = !fenced;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (fenced) return;
|
|
134
|
+
const cleaned = line.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, " ");
|
|
135
|
+
for (const m of cleaned.matchAll(/([A-Za-z0-9_./-]+\.[A-Za-z][A-Za-z0-9]*):(\d+)/g)) {
|
|
136
|
+
push(m[1], Number(m[2]), `line ${i + 1}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve one citation at a commit, or in the checkout when `rev` is null.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} repo
|
|
146
|
+
* @param {string|null} rev
|
|
147
|
+
* @param {{file: string, line: number|null}} c
|
|
148
|
+
* @param {Map<string, number|null>} lineCache
|
|
149
|
+
* @returns {string|null} the reason it failed, or null
|
|
150
|
+
*/
|
|
151
|
+
export function resolveOne(repo, rev, c, lineCache) {
|
|
152
|
+
const unsafe = unsafePath(c.file);
|
|
153
|
+
if (unsafe) return unsafe;
|
|
154
|
+
|
|
155
|
+
if (!lineCache.has(c.file)) {
|
|
156
|
+
lineCache.set(c.file, rev === null ? worktreeLines(repo, c.file) : revLines(repo, rev, c.file));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const lines = lineCache.get(c.file);
|
|
160
|
+
const at = rev === null ? "in the working tree" : `at ${rev.slice(0, 12)}`;
|
|
161
|
+
if (lines === null) return `no such file ${at}`;
|
|
162
|
+
if (c.line === null) return null;
|
|
163
|
+
if (c.line < 1) return `line ${c.line} is not a line number`;
|
|
164
|
+
if (c.line > lines) return `line ${c.line} is past the end of the file (${lines} lines)`;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Line count of a blob at a commit, or null when the path is not a file there.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} repo
|
|
172
|
+
* @param {string} rev
|
|
173
|
+
* @param {string} file
|
|
174
|
+
* @returns {number|null}
|
|
175
|
+
*/
|
|
176
|
+
function revLines(repo, rev, file) {
|
|
177
|
+
const t = spawnSync("git", ["-C", repo, "cat-file", "-t", `${rev}:${file}`], {
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
});
|
|
180
|
+
if (t.status !== 0 || t.stdout.trim() !== "blob") return null;
|
|
181
|
+
const show = spawnSync("git", ["-C", repo, "show", `${rev}:${file}`], {
|
|
182
|
+
encoding: "utf-8",
|
|
183
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
184
|
+
});
|
|
185
|
+
return countLines(show.status === 0 ? show.stdout : "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Line count in the checkout, or null when there is no such file.
|
|
190
|
+
*
|
|
191
|
+
* `unsafePath` cannot see a symlink: `docs/out -> /etc` is a relative path with
|
|
192
|
+
* no `..` in it, and git never followed it because git reads blobs rather than
|
|
193
|
+
* the filesystem. In worktree mode the filesystem IS the source, so containment
|
|
194
|
+
* is re-checked after the path is resolved.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} repo
|
|
197
|
+
* @param {string} file
|
|
198
|
+
* @returns {number|null}
|
|
199
|
+
*/
|
|
200
|
+
function worktreeLines(repo, file) {
|
|
201
|
+
let real;
|
|
202
|
+
let root;
|
|
203
|
+
try {
|
|
204
|
+
root = realpathSync(repo);
|
|
205
|
+
real = realpathSync(resolvePath(root, file));
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
if (real !== root && !real.startsWith(root + sep)) return null;
|
|
210
|
+
try {
|
|
211
|
+
if (!statSync(real).isFile()) return null;
|
|
212
|
+
return countLines(readFileSync(real, "utf-8"));
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A file with no trailing newline still has its last line.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} text
|
|
222
|
+
* @returns {number}
|
|
223
|
+
*/
|
|
224
|
+
function countLines(text) {
|
|
225
|
+
if (text.length === 0) return 0;
|
|
226
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {object} params
|
|
231
|
+
* @returns {{checked: number, unresolved: object[], rev: string}}
|
|
232
|
+
*/
|
|
233
|
+
export function verify({ raw, repo, rev }) {
|
|
234
|
+
const citations = extract(raw);
|
|
235
|
+
const lineCache = new Map();
|
|
236
|
+
const unresolved = [];
|
|
237
|
+
for (const c of citations) {
|
|
238
|
+
const reason = resolveOne(repo, rev, c, lineCache);
|
|
239
|
+
if (reason) unresolved.push({ ...c, reason });
|
|
240
|
+
}
|
|
241
|
+
return { checked: citations.length, unresolved, rev };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const VALUE_FLAGS = new Set(["--repo", "--rev"]);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* One pass, positionally. `argv.indexOf(a)` finds the FIRST occurrence of a
|
|
248
|
+
* string, so `--repo . file .` would have mis-identified which `.` was the
|
|
249
|
+
* target; walking the list is both simpler and correct.
|
|
250
|
+
*
|
|
251
|
+
* @param {string[]} argv
|
|
252
|
+
* @returns {{target: string|null, repo: string|null, rev: string|null}}
|
|
253
|
+
*/
|
|
254
|
+
export function parseArgs(argv) {
|
|
255
|
+
let target = null;
|
|
256
|
+
let repo = null;
|
|
257
|
+
let rev = null;
|
|
258
|
+
for (let i = 0; i < argv.length; i++) {
|
|
259
|
+
const a = argv[i];
|
|
260
|
+
if (VALUE_FLAGS.has(a)) {
|
|
261
|
+
const v = argv[i + 1];
|
|
262
|
+
if (v !== undefined && !v.startsWith("--")) {
|
|
263
|
+
if (a === "--repo") repo = v;
|
|
264
|
+
else rev = v;
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (a.startsWith("--")) continue;
|
|
270
|
+
if (target === null) target = a;
|
|
271
|
+
}
|
|
272
|
+
return { target, repo, rev };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function main() {
|
|
276
|
+
const argv = process.argv.slice(2);
|
|
277
|
+
const parsed = parseArgs(argv);
|
|
278
|
+
const target = parsed.target;
|
|
279
|
+
if (!target) {
|
|
280
|
+
console.error(
|
|
281
|
+
"usage: verify-citations.mjs <file|-> [--repo <path>] [--rev <sha>|--worktree] [--json] [--quiet]",
|
|
282
|
+
);
|
|
283
|
+
process.exit(64);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const repo = parsed.repo || process.cwd();
|
|
287
|
+
const worktree = argv.includes("--worktree");
|
|
288
|
+
if (worktree && parsed.rev) {
|
|
289
|
+
console.error("verify-citations: --worktree and --rev name two different trees; pick one");
|
|
290
|
+
process.exit(64);
|
|
291
|
+
}
|
|
292
|
+
const rev = parsed.rev || "HEAD";
|
|
293
|
+
const json = argv.includes("--json");
|
|
294
|
+
const quiet = argv.includes("--quiet");
|
|
295
|
+
|
|
296
|
+
let raw;
|
|
297
|
+
try {
|
|
298
|
+
raw = target === "-" ? readFileSync(0, "utf-8") : readFileSync(target, "utf-8");
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error(`verify-citations: cannot read ${target}: ${e.message}`);
|
|
301
|
+
process.exit(2);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const top = spawnSync("git", ["-C", repo, "rev-parse", "--show-toplevel"], { encoding: "utf-8" });
|
|
305
|
+
if (top.status !== 0) {
|
|
306
|
+
console.error(`verify-citations: ${repo} is not a git repository`);
|
|
307
|
+
process.exit(2);
|
|
308
|
+
}
|
|
309
|
+
let sha = null;
|
|
310
|
+
if (!worktree) {
|
|
311
|
+
const head = spawnSync("git", ["-C", repo, "rev-parse", rev], { encoding: "utf-8" });
|
|
312
|
+
if (head.status !== 0) {
|
|
313
|
+
console.error(`verify-citations: ${rev} is not a commit in ${repo}`);
|
|
314
|
+
process.exit(2);
|
|
315
|
+
}
|
|
316
|
+
sha = head.stdout.trim();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const report = verify({ raw, repo, rev: sha });
|
|
320
|
+
const where = sha === null ? "in the working tree" : `at ${sha.slice(0, 12)}`;
|
|
321
|
+
|
|
322
|
+
if (json) {
|
|
323
|
+
console.log(JSON.stringify(report, null, 2));
|
|
324
|
+
} else if (!quiet) {
|
|
325
|
+
if (report.unresolved.length === 0) {
|
|
326
|
+
console.log(`verify-citations: ${report.checked} citation(s) resolve ${where}`);
|
|
327
|
+
} else {
|
|
328
|
+
console.log(
|
|
329
|
+
`verify-citations: ${report.unresolved.length} of ${report.checked} citation(s) do not resolve ${where}:`,
|
|
330
|
+
);
|
|
331
|
+
for (const u of report.unresolved) {
|
|
332
|
+
console.log(` ${u.where}: ${u.file}${u.line === null ? "" : `:${u.line}`} - ${u.reason}`);
|
|
333
|
+
}
|
|
334
|
+
// The cheap way to make this exit 0 is to delete the line number, which
|
|
335
|
+
// turns a wrong claim into an unfalsifiable one. Said here because this
|
|
336
|
+
// is the moment someone is deciding what to do about it.
|
|
337
|
+
console.log(" Re-search and correct the citation; never drop the line number to pass.");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
process.exit(report.unresolved.length === 0 ? 0 : 1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
345
|
+
main();
|
|
346
|
+
}
|