dsh-plugin-inspector 0.1.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/lib/source.js ADDED
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Reading the package under analysis without installing, building, or running
3
+ * any part of it.
4
+ *
5
+ * A directory is walked directly. A tarball is decoded **entirely in memory** —
6
+ * nothing is ever written to disk. That is a safety property, not an
7
+ * optimisation: it makes tar path traversal (`../../.ssh/authorized_keys`)
8
+ * structurally impossible rather than something a filter has to catch, and it
9
+ * lets a test assert that analysing a hostile tarball touched no file.
10
+ *
11
+ * Every cap is enforced *while* bytes arrive, not after. A tar entry is
12
+ * abandoned the moment its running total passes {@link MAX_FILE_BYTES}, so a
13
+ * 27 MB tarball holding a 6 GB member costs 27 MB of decompression and no
14
+ * memory at all. Enforcing a cap on an already-materialised buffer is not a
15
+ * cap.
16
+ *
17
+ * Symbolic links are recorded and never followed, for the same reason: a link
18
+ * pointing outside the package is not part of the package.
19
+ * @module dsh-plugin-inspector/source
20
+ */
21
+ import { createReadStream, openSync, readdirSync, readFileSync, readSync, closeSync, statSync } from 'node:fs';
22
+ import { join, posix, relative, resolve, sep } from 'node:path';
23
+ import { PassThrough, Transform } from 'node:stream';
24
+ import { pipeline } from 'node:stream/promises';
25
+ import { createGunzip } from 'node:zlib';
26
+ import { Parser } from 'tar';
27
+ import { publishSet, TARBALL_PUBLISH_SET } from "./publish.js";
28
+ /** Largest single file the analyzer will hold in memory. */
29
+ export const MAX_FILE_BYTES = 4 * 1024 * 1024;
30
+ /** Largest total payload the analyzer will hold in memory. */
31
+ export const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
32
+ /** Largest number of files the analyzer will consider. */
33
+ export const MAX_ENTRIES = 10_000;
34
+ /** The shipping ceilings. Tests substitute smaller ones to exercise each cap. */
35
+ export const DEFAULT_LIMITS = {
36
+ maxFileBytes: MAX_FILE_BYTES,
37
+ maxTotalBytes: MAX_TOTAL_BYTES,
38
+ maxEntries: MAX_ENTRIES,
39
+ };
40
+ /** Directories never descended into: not shipped, and not the package's own code. */
41
+ const SKIPPED_DIRECTORIES = new Set([
42
+ 'node_modules', '.git', '.pnpm-store', '.yarn', 'coverage', '.nyc_output',
43
+ ]);
44
+ /** Thrown when the target cannot be read at all, which is exit code 2, not a finding. */
45
+ export class SourceError extends Error {
46
+ }
47
+ /**
48
+ * Whether a buffer looks like binary content. A NUL byte in the first 8 KiB is
49
+ * the same cheap test `grep` and `git` use, and it is enough here: the point is
50
+ * only to avoid parsing bytes that are not source.
51
+ * @param buffer - the file content.
52
+ * @returns true when the file should be treated as opaque.
53
+ */
54
+ function isBinary(buffer) {
55
+ return buffer.subarray(0, 8192).includes(0);
56
+ }
57
+ /**
58
+ * Charge one file against the entry count.
59
+ * @param collector - the shared accumulator.
60
+ * @param path - package-relative POSIX path.
61
+ * @returns true when the analyzer may go on to read it.
62
+ */
63
+ function countEntry(collector, path) {
64
+ if (collector.entries >= collector.limits.maxEntries) {
65
+ collector.skipped.push({ path, reason: 'entry-cap' });
66
+ return false;
67
+ }
68
+ collector.entries += 1;
69
+ return true;
70
+ }
71
+ /**
72
+ * Record one already-read file against the size caps, storing its text when it
73
+ * is readable.
74
+ * @param collector - the shared accumulator.
75
+ * @param path - package-relative POSIX path.
76
+ * @param buffer - the file content.
77
+ */
78
+ function store(collector, path, buffer) {
79
+ if (buffer.byteLength > collector.limits.maxFileBytes) {
80
+ collector.skipped.push({ path, reason: 'size-cap' });
81
+ return;
82
+ }
83
+ if (collector.bytes + buffer.byteLength > collector.limits.maxTotalBytes) {
84
+ collector.skipped.push({ path, reason: 'total-cap' });
85
+ return;
86
+ }
87
+ if (isBinary(buffer)) {
88
+ collector.skipped.push({ path, reason: 'binary' });
89
+ return;
90
+ }
91
+ collector.bytes += buffer.byteLength;
92
+ collector.files.set(path, buffer.toString('utf8'));
93
+ }
94
+ /**
95
+ * Walk a directory tree, never following symlinks, never descending into build
96
+ * or dependency directories, and reading only what the package would publish.
97
+ * @param root - absolute package root.
98
+ * @param directory - absolute directory currently being read.
99
+ * @param collector - the shared accumulator.
100
+ * @param published - the publish-set membership test.
101
+ */
102
+ function walkDirectory(root, directory, collector, published) {
103
+ let entries;
104
+ try {
105
+ entries = readdirSync(directory, { withFileTypes: true });
106
+ }
107
+ catch {
108
+ collector.skipped.push({ path: toPackagePath(root, directory), reason: 'unreadable' });
109
+ return;
110
+ }
111
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
112
+ const absolute = join(directory, entry.name);
113
+ const path = toPackagePath(root, absolute);
114
+ if (entry.isSymbolicLink()) {
115
+ if (published.includes(path))
116
+ collector.skipped.push({ path, reason: 'unreadable' });
117
+ continue;
118
+ }
119
+ if (entry.isDirectory()) {
120
+ if (SKIPPED_DIRECTORIES.has(entry.name))
121
+ continue;
122
+ walkDirectory(root, absolute, collector, published);
123
+ continue;
124
+ }
125
+ if (!entry.isFile())
126
+ continue;
127
+ if (!published.includes(path)) {
128
+ collector.unpublished += 1;
129
+ continue;
130
+ }
131
+ if (!countEntry(collector, path))
132
+ continue;
133
+ try {
134
+ // Size first, then read. `readFileSync` on an oversized file materialises
135
+ // it before any cap can reject it, which is the same mistake on the
136
+ // directory path that the tar reader had on the tarball path.
137
+ if (statSync(absolute).size > collector.limits.maxFileBytes) {
138
+ collector.skipped.push({ path, reason: 'size-cap' });
139
+ continue;
140
+ }
141
+ store(collector, path, readFileSync(absolute));
142
+ }
143
+ catch {
144
+ collector.skipped.push({ path, reason: 'unreadable' });
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Convert an absolute path inside the package to the POSIX package-relative
150
+ * form every report and finding uses.
151
+ * @param root - absolute package root.
152
+ * @param absolute - absolute path inside it.
153
+ * @returns the package-relative POSIX path.
154
+ */
155
+ function toPackagePath(root, absolute) {
156
+ return relative(root, absolute).split(sep).join(posix.sep);
157
+ }
158
+ /**
159
+ * Drop the tarball's single leading directory, which npm always sets to
160
+ * `package/`, so tarball paths and directory paths are directly comparable, and
161
+ * reject anything that still climbs out afterwards. Nothing is written from a
162
+ * tar entry, but a finding whose `file` reads `../../../../etc/passwd` claims a
163
+ * location the package does not have.
164
+ * @param entryPath - the raw tar entry path.
165
+ * @returns the package-relative path, or `undefined` when there is none.
166
+ */
167
+ function stripRoot(entryPath) {
168
+ const normalized = entryPath.replace(/^\.\//, '');
169
+ const slash = normalized.indexOf('/');
170
+ if (slash < 0)
171
+ return undefined;
172
+ const remainder = normalized.slice(slash + 1);
173
+ if (remainder === '')
174
+ return undefined;
175
+ const segments = [];
176
+ for (const segment of remainder.split('/')) {
177
+ if (segment === '' || segment === '.')
178
+ continue;
179
+ if (segment === '..') {
180
+ if (segments.length === 0)
181
+ return undefined;
182
+ segments.pop();
183
+ continue;
184
+ }
185
+ segments.push(segment);
186
+ }
187
+ return segments.length === 0 ? undefined : segments.join('/');
188
+ }
189
+ /**
190
+ * Decompressed tar bytes one tarball may produce before the read is abandoned.
191
+ *
192
+ * Eight times the in-memory ceiling. A plugin tarball is never this large, and
193
+ * one that is has already answered the only question worth asking about it.
194
+ */
195
+ export const MAX_STREAM_BYTES = 8 * MAX_TOTAL_BYTES;
196
+ /**
197
+ * A stage that fails the pipeline once the decompressed stream passes a
198
+ * ceiling. Nothing downstream keeps the bytes, but *producing* eight gigabytes
199
+ * from a 28 MB file still costs the time to inflate them, and a CI job that
200
+ * hangs for a minute on a hostile input is a denial of service with extra
201
+ * steps.
202
+ * @param limit - the ceiling in bytes.
203
+ * @returns the counting stage.
204
+ */
205
+ function byteCeiling(limit) {
206
+ let total = 0;
207
+ return new Transform({
208
+ transform(chunk, _encoding, callback) {
209
+ total += chunk.byteLength;
210
+ if (total > limit) {
211
+ callback(new SourceError(`decompresses to more than ${limit} bytes`));
212
+ return;
213
+ }
214
+ callback(null, chunk);
215
+ },
216
+ });
217
+ }
218
+ /**
219
+ * Whether a file begins with the gzip magic number, so a plain `.tar` is read
220
+ * as one rather than failing in the inflater.
221
+ * @param file - absolute path.
222
+ * @returns true when the file is gzipped.
223
+ */
224
+ function isGzip(file) {
225
+ const handle = openSync(file, 'r');
226
+ try {
227
+ const header = Buffer.alloc(2);
228
+ readSync(handle, header, 0, 2, 0);
229
+ return header[0] === 0x1f && header[1] === 0x8b;
230
+ }
231
+ finally {
232
+ closeSync(handle);
233
+ }
234
+ }
235
+ /**
236
+ * Decode an npm tarball into memory. The tar reader is only ever a `Parser`;
237
+ * no extraction call exists in this module, so there is no code path that can
238
+ * write a file.
239
+ *
240
+ * The pipeline is assembled by hand rather than through `tar.list({ file })`
241
+ * because that convenience path applies no backpressure between the inflater
242
+ * and the parser: the inflater runs ahead, and a tarball holding one very large
243
+ * member materialises gigabytes of it whatever the entry consumer does. A
244
+ * `stream.pipeline` of Node streams paces the inflater against the parser, and
245
+ * measured on the same 28 MB probe it holds the process at 96 MB instead of
246
+ * 4 GB.
247
+ *
248
+ * Errors thrown from the entry callbacks reach an EventEmitter, not the
249
+ * `await`, so every one of them is captured here and rethrown on the awaited
250
+ * path — an emitter-thrown `RangeError` that escapes becomes an uncaught
251
+ * exception and a raw stack trace on a user's terminal.
252
+ * @param file - absolute path to the `.tgz`.
253
+ * @param collector - the shared accumulator.
254
+ */
255
+ async function readTarball(file, collector) {
256
+ const pending = [];
257
+ let failure = null;
258
+ const parser = new Parser({
259
+ onReadEntry: (entry) => {
260
+ if (entry.type !== 'File') {
261
+ entry.resume();
262
+ return;
263
+ }
264
+ const path = stripRoot(String(entry.path));
265
+ if (path === undefined) {
266
+ collector.skipped.push({ path: String(entry.path), reason: 'unreadable' });
267
+ entry.resume();
268
+ return;
269
+ }
270
+ if (!countEntry(collector, path)) {
271
+ entry.resume();
272
+ return;
273
+ }
274
+ pending.push(new Promise((done) => {
275
+ let chunks = [];
276
+ let size = 0;
277
+ let abandoned = null;
278
+ const abandon = (reason) => {
279
+ abandoned = reason;
280
+ chunks = [];
281
+ entry.resume();
282
+ };
283
+ entry.on('data', (chunk) => {
284
+ if (abandoned !== null)
285
+ return;
286
+ size += chunk.byteLength;
287
+ if (size > collector.limits.maxFileBytes)
288
+ return abandon('size-cap');
289
+ if (collector.bytes + size > collector.limits.maxTotalBytes)
290
+ return abandon('total-cap');
291
+ chunks.push(chunk);
292
+ });
293
+ entry.on('end', () => {
294
+ try {
295
+ if (abandoned === null)
296
+ store(collector, path, Buffer.concat(chunks));
297
+ else
298
+ collector.skipped.push({ path, reason: abandoned });
299
+ }
300
+ catch (error) {
301
+ failure ??= error;
302
+ }
303
+ chunks = [];
304
+ done();
305
+ });
306
+ }));
307
+ },
308
+ });
309
+ const inflate = isGzip(file) ? createGunzip() : new PassThrough();
310
+ await pipeline(createReadStream(file), inflate, byteCeiling(MAX_STREAM_BYTES), parser);
311
+ await Promise.all(pending);
312
+ if (failure !== null)
313
+ throw failure;
314
+ }
315
+ /**
316
+ * Read the `files`, `main`, and ignore rules a working tree publishes under,
317
+ * without trusting the manifest to be well formed — a hostile `package.json`
318
+ * whose `files` is a number must not stop the analysis.
319
+ *
320
+ * A manifest that is missing or is not JSON produces no rules rather than an
321
+ * error. Diagnosing that is `parseManifest`'s job and it says something more
322
+ * useful than this function could; failing here would replace "package.json is
323
+ * not valid JSON, at position 4" with "this is not an npm package".
324
+ * @param root - absolute package root.
325
+ * @returns the publish-set membership test.
326
+ */
327
+ function directoryPublishSet(root) {
328
+ let manifest;
329
+ try {
330
+ manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
331
+ }
332
+ catch {
333
+ manifest = {};
334
+ }
335
+ const record = typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)
336
+ ? manifest
337
+ : {};
338
+ const files = record.files;
339
+ const main = record.main;
340
+ return publishSet({
341
+ files: Array.isArray(files) ? files.filter((entry) => typeof entry === 'string') : null,
342
+ main: typeof main === 'string' ? main : null,
343
+ npmignore: readOptional(join(root, '.npmignore')),
344
+ gitignore: readOptional(join(root, '.gitignore')),
345
+ });
346
+ }
347
+ /**
348
+ * Read a file that may not exist.
349
+ * @param path - absolute path.
350
+ * @returns the text, or `null`.
351
+ */
352
+ function readOptional(path) {
353
+ try {
354
+ return readFileSync(path, 'utf8');
355
+ }
356
+ catch {
357
+ // Absent, a directory, or unreadable: all three mean "no rules from here".
358
+ return null;
359
+ }
360
+ }
361
+ /**
362
+ * Read the package under analysis.
363
+ * @param target - a plugin directory, or a `.tgz` / `.tar.gz` npm tarball.
364
+ * @param limits - resource ceilings; defaults to {@link DEFAULT_LIMITS}.
365
+ * @returns the decoded package.
366
+ * @throws SourceError when the target does not exist, is not a readable
367
+ * tarball, or holds no `package.json`.
368
+ */
369
+ export async function loadSource(target, limits = DEFAULT_LIMITS) {
370
+ const path = resolve(target);
371
+ let stats;
372
+ try {
373
+ stats = statSync(path);
374
+ }
375
+ catch {
376
+ throw new SourceError(`cannot read target: ${path}`);
377
+ }
378
+ const collector = { files: new Map(), skipped: [], limits, bytes: 0, entries: 0, unpublished: 0 };
379
+ const kind = stats.isDirectory() ? 'directory' : 'tarball';
380
+ let published;
381
+ if (kind === 'directory') {
382
+ published = directoryPublishSet(path);
383
+ walkDirectory(path, path, collector, published);
384
+ }
385
+ else {
386
+ published = TARBALL_PUBLISH_SET;
387
+ try {
388
+ await readTarball(path, collector);
389
+ }
390
+ catch (error) {
391
+ const detail = error instanceof Error ? error.message : String(error);
392
+ throw new SourceError(`cannot read tarball ${path}: ${detail}`);
393
+ }
394
+ if (collector.entries === 0) {
395
+ throw new SourceError(`${path} holds no tar entries — this is not a readable npm tarball`);
396
+ }
397
+ }
398
+ if (!collector.files.has('package.json')) {
399
+ throw new SourceError(`no readable package.json in ${path} — this is not an npm package`);
400
+ }
401
+ return {
402
+ kind,
403
+ path,
404
+ files: collector.files,
405
+ skipped: collector.skipped,
406
+ bytesRead: collector.bytes,
407
+ publishBasis: published.basis,
408
+ unpublishedFiles: collector.unpublished,
409
+ };
410
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The decoded package every check tier reads from.
3
+ *
4
+ * Parsing happens once, in `inspect.ts`, and each tier receives the result.
5
+ * That keeps the tiers pure functions of already-parsed data — which is what
6
+ * makes "no analysed code was executed" a property of one small module rather
7
+ * than something every check has to be trusted about.
8
+ * @module dsh-plugin-inspector/checks/input
9
+ */
10
+ import type { PatchDocument, PatchParseError } from '../cordis-yaml.ts';
11
+ import type { PackageManifest } from '../manifest.ts';
12
+ import type { PluginSource } from '../source.ts';
13
+ /** A patch layer that could not be parsed. */
14
+ export interface PatchFailure {
15
+ /** Package-relative path of the YAML file. */
16
+ readonly file: string;
17
+ readonly error: PatchParseError;
18
+ }
19
+ /** Everything the checks see. */
20
+ export interface CheckInput {
21
+ readonly source: PluginSource;
22
+ readonly manifest: PackageManifest;
23
+ /**
24
+ * True when `package.json` declares `dsh.bundle.patch`. False means nothing
25
+ * in this package composes into a profile, which forbids every Tier A
26
+ * patch-row verdict outright — see `runTierA`.
27
+ */
28
+ readonly mountsAsBundle: boolean;
29
+ /**
30
+ * The mounted patch layer, parsed. At most one: `dsh.bundle.patch` names
31
+ * exactly one file and the launcher reads no other.
32
+ */
33
+ readonly patches: readonly PatchDocument[];
34
+ readonly patchFailures: readonly PatchFailure[];
35
+ /** Cordis YAML the package ships but no manifest key mounts. */
36
+ readonly unmountedPatchFiles: readonly string[];
37
+ /** Package-relative paths of shipped JavaScript and TypeScript source. */
38
+ readonly sourceFiles: readonly string[];
39
+ /** Package-relative paths of shipped skill and agent-instruction markdown. */
40
+ readonly modelVisibleFiles: readonly string[];
41
+ }
42
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tier A — decidable checks over structured declarations.
3
+ *
4
+ * Everything here reads a field the harness itself must read literally in order
5
+ * to act on it: a `package.json` key, or a Cordis patch row. That is why Tier A
6
+ * findings carry `certain` confidence and why they are the only findings this
7
+ * tool treats as verdicts. `disabled: true` cannot be obfuscated and still
8
+ * disable anything.
9
+ * @module dsh-plugin-inspector/checks/tier-a
10
+ */
11
+ import type { Finding } from '../model.ts';
12
+ import type { CheckInput } from './input.ts';
13
+ /**
14
+ * Run every Tier A check.
15
+ *
16
+ * The filter is the guard: a package that declares no `dsh.bundle.patch`
17
+ * composes into no profile, so no reading of a Cordis row in it can be a
18
+ * verdict about anything. `patches` is already empty in that case; this makes
19
+ * the property structural rather than a consequence of how the input was built.
20
+ * @param input - the decoded package.
21
+ * @returns findings, unordered.
22
+ */
23
+ export declare function runTierA(input: CheckInput): Finding[];
24
+ //# sourceMappingURL=tier-a.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tier B — capability detection over shipped source.
3
+ *
4
+ * Everything here answers "this plugin CAN do X", never "this plugin DOES X".
5
+ * The distinction is load-bearing for B8: finding a credential read and a
6
+ * network call in the same package is not evidence that the credential reaches
7
+ * the socket, and the finding says so.
8
+ *
9
+ * Parsing is `ts.createSourceFile` — syntax only. No program is created, no
10
+ * type checker is instantiated, no module is resolved, nothing is transpiled,
11
+ * and nothing is executed. Every check is a shape match on one AST node, which
12
+ * is also why every check has a one-line bypass, carried in the finding.
13
+ * @module dsh-plugin-inspector/checks/tier-b
14
+ */
15
+ import type { Finding } from '../model.ts';
16
+ import type { CheckInput } from './input.ts';
17
+ /**
18
+ * Run every Tier B check.
19
+ * @param input - the decoded package.
20
+ * @returns findings, unordered.
21
+ */
22
+ export declare function runTierB(input: CheckInput): Finding[];
23
+ //# sourceMappingURL=tier-b.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Tier C — how much of the package the analyzer could actually read.
3
+ *
4
+ * These checks do not describe the plugin's behavior. They describe the limits
5
+ * of the analysis, and that is why a Tier C hit is a finding rather than a
6
+ * silent internal flag: "we cannot read this" is a legitimate result and the
7
+ * user is entitled to see it.
8
+ *
9
+ * A Tier C hit also has a mechanical consequence. Tier B recognises a whitelist
10
+ * of syntactic shapes, so when code is minified, when identifiers are computed,
11
+ * or when the shipped artifact has no readable source, a Tier B *positive* is
12
+ * still true but a Tier B *negative* means nothing. `inspect.ts` reads the
13
+ * output of this module to lower Tier B confidence and to forbid the report
14
+ * from claiming nothing was found.
15
+ * @module dsh-plugin-inspector/checks/tier-c
16
+ */
17
+ import type { Finding } from '../model.ts';
18
+ import type { CheckInput } from './input.ts';
19
+ /**
20
+ * Tier C checks that do **not** make a Tier B negative unreliable.
21
+ *
22
+ * Every other check here says the analyzer could not read something. C3 says
23
+ * the opposite: the bytes were read exactly as written and exactly as they will
24
+ * run — what cannot be checked is whether they match the repository that
25
+ * claims to have produced them. That is worth reporting and it is not a reason
26
+ * to distrust the parse, and treating it as one marks every ordinary published
27
+ * tarball `degraded`, because shipping built output and no source is what
28
+ * publishing a package *is*.
29
+ */
30
+ export declare const NON_DEGRADING_CHECKS: ReadonlySet<string>;
31
+ /**
32
+ * Run every Tier C check.
33
+ * @param input - the decoded package.
34
+ * @returns findings, unordered.
35
+ */
36
+ export declare function runTierC(input: CheckInput): Finding[];
37
+ //# sourceMappingURL=tier-c.d.ts.map
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `dsh-inspect` — the command line face of the inspector.
4
+ *
5
+ * Exit codes are the CI contract and are deliberately three-valued:
6
+ * `0` clean, `1` findings at or above the threshold, `2` the analysis could not
7
+ * be performed. A job that cannot tell "the analyzer broke" from "the plugin is
8
+ * clean" is the failure mode that split exists to prevent.
9
+ * @module dsh-plugin-inspector/cli
10
+ */
11
+ import { type Severity } from './model.ts';
12
+ /** Exit codes this tool uses. */
13
+ export declare const EXIT: {
14
+ readonly clean: 0;
15
+ readonly findings: 1;
16
+ readonly unanalysable: 2;
17
+ };
18
+ /** One parsed command line. */
19
+ interface Options {
20
+ readonly target: string;
21
+ readonly json: boolean;
22
+ readonly failOn: Severity | 'none';
23
+ readonly color: boolean;
24
+ }
25
+ /** Raised for a malformed command line; the message is printed and the tool exits 2. */
26
+ export declare class UsageError extends Error {
27
+ }
28
+ /**
29
+ * Parse argv.
30
+ * @param argv - arguments after the node binary and script path.
31
+ * @returns the parsed options, or `null` when usage or version was requested.
32
+ * @throws UsageError on an unrecognised or incomplete argument.
33
+ */
34
+ export declare function parseArgs(argv: readonly string[]): Options | null;
35
+ /**
36
+ * Run one invocation.
37
+ * @param argv - arguments after the node binary and script path.
38
+ * @returns the process exit code.
39
+ */
40
+ export declare function main(argv: readonly string[]): Promise<number>;
41
+ /**
42
+ * Report a failure that reached no `try`, and say which exit code it is.
43
+ *
44
+ * Not every failure can be caught where it happens. A `RangeError` raised
45
+ * inside a stream's `'end'` handler is thrown at an EventEmitter, not at the
46
+ * `await`, so it walks past every `catch` in this program and kills the process
47
+ * with Node's default handler — which exits **1**, the code that means "the
48
+ * analysis completed and found something at or above --fail-on". A CI job then
49
+ * reads a crash as a verdict. The whole point of a separate code 2 is that this
50
+ * cannot happen, so the last resort has to be covered too.
51
+ * @param error - whatever was thrown.
52
+ * @returns the exit code to leave with.
53
+ */
54
+ export declare function reportFatal(error: unknown): number;
55
+ export {};
56
+ //# sourceMappingURL=cli.d.ts.map