@xagents_in/unmask 0.1.3 → 0.1.4

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 (50) hide show
  1. package/README.md +86 -4
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/cache.mjs +488 -0
  5. package/dist/cli/compare.mjs +261 -0
  6. package/dist/cli/config.mjs +141 -0
  7. package/dist/cli/discovery.mjs +372 -0
  8. package/dist/cli/doctor.mjs +354 -0
  9. package/dist/cli/exceptions.mjs +170 -0
  10. package/dist/cli/file.mjs +271 -0
  11. package/dist/cli/files.mjs +141 -0
  12. package/dist/cli/history.mjs +232 -0
  13. package/dist/cli/hosted.mjs +251 -43
  14. package/dist/cli/info.mjs +125 -0
  15. package/dist/cli/init.mjs +57 -0
  16. package/dist/cli/install-guard.mjs +896 -0
  17. package/dist/cli/lockfile.mjs +330 -0
  18. package/dist/cli/main.mjs +97 -14
  19. package/dist/cli/preflight.mjs +388 -0
  20. package/dist/cli/sarif.mjs +100 -0
  21. package/dist/cli/shell-integration.mjs +122 -0
  22. package/dist/cli/terminal.mjs +396 -0
  23. package/dist/cli/watch.mjs +233 -0
  24. package/dist/cli/why.mjs +256 -0
  25. package/dist/core/advisories.mjs +101 -24
  26. package/dist/core/advisory-cache.mjs +295 -0
  27. package/dist/core/behavior-signals.mjs +1088 -0
  28. package/dist/core/dependency-graph.mjs +332 -0
  29. package/dist/core/evidence-exports.mjs +469 -0
  30. package/dist/core/guard-config.mjs +463 -0
  31. package/dist/core/guard-policy.mjs +308 -0
  32. package/dist/core/package-details.mjs +42 -6
  33. package/dist/core/report.mjs +7 -0
  34. package/dist/core/sarif.mjs +556 -0
  35. package/dist/core/syntax.mjs +296 -15
  36. package/dist/model/client.mjs +204 -56
  37. package/dist/model/diff-evidence.mjs +177 -0
  38. package/dist/model/summary.mjs +183 -0
  39. package/dist/server/advisory-result.mjs +137 -0
  40. package/dist/server/app.mjs +60 -2
  41. package/dist/server/dependency-screening.mjs +386 -0
  42. package/dist/server/exports.mjs +63 -0
  43. package/dist/server/guard-decision.mjs +37 -0
  44. package/dist/server/main.mjs +25 -7
  45. package/dist/server/queue.mjs +95 -53
  46. package/dist/server/store.mjs +21 -1
  47. package/dist/version.json +1 -1
  48. package/dist/viewer/render.mjs +10 -2
  49. package/dist/viewer/shell.mjs +1 -1
  50. package/package.json +1 -1
@@ -0,0 +1,396 @@
1
+ const states = ["byte-identical", "changed", "artifact-only", "not-compared"];
2
+ const object = (value) =>
3
+ value !== null && typeof value === "object" && !Array.isArray(value);
4
+ const list = (value) => (Array.isArray(value) ? value : []);
5
+ const count = (value) =>
6
+ Number.isSafeInteger(value) && value >= 0 ? value : 0;
7
+
8
+ export function safeText(value, max = 160) {
9
+ const text = String(value ?? "").replace(
10
+ /[\u0000-\u001f\u007f-\u009f\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g,
11
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
12
+ );
13
+ return text.length > max ? text.slice(0, max - 1) + "..." : text;
14
+ }
15
+
16
+ export function formatBytes(bytes) {
17
+ const value = count(bytes);
18
+ if (value < 1024) return `${value} B`;
19
+ const units = ["KB", "MB", "GB"];
20
+ let scaled = value / 1024;
21
+ let unit = 0;
22
+ while (scaled >= 1024 && unit < units.length - 1) {
23
+ scaled /= 1024;
24
+ unit++;
25
+ }
26
+ return `${scaled.toFixed(1)} ${units[unit]}`;
27
+ }
28
+
29
+ function identity(report) {
30
+ const claim = report.inputs?.claim;
31
+ const manifest = report.manifests?.artifact?.value;
32
+ const name = claim?.name ?? manifest?.name;
33
+ const version = claim?.version ?? manifest?.version;
34
+ return name && version ? `${name}@${version}` : "local artifact";
35
+ }
36
+
37
+ function verdict(exitCode) {
38
+ if (exitCode === 0)
39
+ return "No deterministic findings in the inspected scope (exit 0)";
40
+ if (exitCode === 3) return "Deterministic findings need review (exit 3)";
41
+ return "Incomplete analysis; no verdict is claimed (exit 2)";
42
+ }
43
+
44
+ function coverageLines(report) {
45
+ const coverage = report.coverage ?? {};
46
+ const inventory = coverage.inventory ?? {};
47
+ const totals = states
48
+ .map((state) => `${count(coverage.states?.[state]?.files)} ${state}`)
49
+ .join(", ");
50
+ const lines = [
51
+ `Coverage ${coverage.complete ? "complete" : "incomplete"}: ${inventory.lowerBound ? "at least " : ""}${count(inventory.files)} file${count(inventory.files) === 1 ? "" : "s"}, ${formatBytes(inventory.bytes)}`,
52
+ ` ${totals}`,
53
+ ];
54
+ const source = report.source ?? {};
55
+ const commit =
56
+ typeof source.commit === "string"
57
+ ? ` at ${source.commit.slice(0, 12)}`
58
+ : "";
59
+ lines.push(
60
+ `Source ${safeText(source.status ?? "unknown")} via ${safeText(source.method ?? "unknown")}${safeText(commit)}${coverage.sourceAvailable === false ? " (no source compared)" : ""}`,
61
+ );
62
+ return lines;
63
+ }
64
+
65
+ function findingLines(report) {
66
+ const findings = list(report.policy?.findings);
67
+ const failures = list(report.failures);
68
+ const lines = [];
69
+ if (findings.length) {
70
+ lines.push(`Findings ${findings.length}`);
71
+ for (const finding of findings.slice(0, 10)) {
72
+ const detail = finding.script
73
+ ? ` (script ${finding.script})`
74
+ : finding.fields
75
+ ? ` (${list(finding.fields).join(", ")})`
76
+ : "";
77
+ lines.push(` - ${safeText(finding.ruleId)}${safeText(detail)}`);
78
+ }
79
+ if (findings.length > 10)
80
+ lines.push(` ... ${findings.length - 10} more in the full report`);
81
+ } else lines.push("Findings none");
82
+ if (failures.length) {
83
+ lines.push(`Failures ${failures.length}`);
84
+ for (const failure of failures.slice(0, 5))
85
+ lines.push(
86
+ ` - ${safeText(failure.code ?? "failure")}${failure.path ? ` ${safeText(failure.path, 80)}` : ""}`,
87
+ );
88
+ if (failures.length > 5)
89
+ lines.push(` ... ${failures.length - 5} more in the full report`);
90
+ }
91
+ return lines;
92
+ }
93
+
94
+ function modelLines(model) {
95
+ if (!object(model)) return ["AI not reported"];
96
+ if (model.status === "complete") {
97
+ const hypotheses = list(model.hypotheses);
98
+ const lines = [
99
+ `AI complete, ${hypotheses.length} ${hypotheses.length === 1 ? "hypothesis" : "hypotheses"} (separate from deterministic evidence)`,
100
+ ];
101
+ if (typeof model.summary === "string")
102
+ lines.push(` Summary: ${safeText(model.summary, 600)}`);
103
+ const assessment = model.assessment;
104
+ if (object(assessment) && assessment.source === "model") {
105
+ const scope = assessment.checkedScope;
106
+ if (typeof scope === "string")
107
+ lines.push(` Scope: ${safeText(scope, 200)}`);
108
+ else if (object(scope)) {
109
+ const notChecked = list(scope.notChecked)
110
+ .slice(0, 8)
111
+ .map((item) => safeText(item, 40));
112
+ lines.push(
113
+ ` Scope: ${count(scope.evidenceSupplied)} evidence items supplied, ${count(scope.evidenceOmitted)} omitted${notChecked.length ? `; not checked: ${notChecked.join(", ")}` : ""}`,
114
+ );
115
+ }
116
+ if (typeof assessment.uncertainty === "string")
117
+ lines.push(` Uncertainty: ${safeText(assessment.uncertainty, 200)}`);
118
+ }
119
+ for (const hypothesis of hypotheses.slice(0, 3))
120
+ lines.push(` - ${safeText(hypothesis?.explanation, 200)}`);
121
+ if (hypotheses.length > 3)
122
+ lines.push(` ... ${hypotheses.length - 3} more in the full report`);
123
+ return lines;
124
+ }
125
+ const reason = model.reason ? ` (${safeText(model.reason, 60)})` : "";
126
+ return [
127
+ `AI ${safeText(model.status, 40)}${reason}; no AI summary for this check`,
128
+ ];
129
+ }
130
+
131
+ const behaviorCues = (report) =>
132
+ list(report.cues).filter((cue) => cue?.module === "behavior");
133
+ const highCue = (cue) => cue?.priority === "HIGH";
134
+
135
+ function behaviorLines(report) {
136
+ const cues = behaviorCues(report);
137
+ if (!cues.length) return [];
138
+ const paths = new Map(
139
+ list(report.evidence).map((item) => [item?.id, item?.path]),
140
+ );
141
+ const high = cues.filter(highCue);
142
+ const lines = [
143
+ `Behavior ${high.length} HIGH and ${cues.length - high.length} review-level static cues (suspicion, not proof; exit code unchanged)`,
144
+ ];
145
+ for (const cue of [...high, ...cues.filter((c) => !highCue(c))].slice(0, 5)) {
146
+ const path = paths.get(list(cue.evidenceIds)[0]);
147
+ const hooks = list(cue.installHookTarget)
148
+ .slice(0, 3)
149
+ .map((hook) => safeText(hook, 24));
150
+ lines.push(
151
+ ` - ${highCue(cue) ? "HIGH " : ""}${safeText(cue.ruleId, 64)}${typeof path === "string" ? ` ${safeText(path, 80)}` : ""}${hooks.length ? ` (runs at ${hooks.join(", ")})` : ""}`,
152
+ );
153
+ }
154
+ if (cues.length > 5)
155
+ lines.push(` ... ${cues.length - 5} more in the full report`);
156
+ return lines;
157
+ }
158
+
159
+ function advisoryItems(advisories) {
160
+ const lines = advisories.slice(0, 5).map((item) => {
161
+ const label =
162
+ typeof item?.databaseSeverity === "string"
163
+ ? ` [${item.databaseSeverity}]`
164
+ : "";
165
+ return ` - ${safeText(item?.id, 64)}${safeText(label, 24)} ${safeText(item?.summary ?? "", 120)}`.trimEnd();
166
+ });
167
+ if (advisories.length > 5)
168
+ lines.push(` ... ${advisories.length - 5} more in the full details`);
169
+ return lines;
170
+ }
171
+
172
+ const knownAdvisories = (check) => list(check?.advisories).length > 0;
173
+
174
+ function advisoryLines(details) {
175
+ const check = details?.vulnerabilityCheck;
176
+ if (!object(check)) return ["Advisory not assessed in this check"];
177
+ const advisories = list(check.advisories);
178
+ const source = safeText(check.source ?? "OSV", 20);
179
+ if (check.status === "none-reported")
180
+ return [`Advisory none reported by ${source} for this exact version`];
181
+ const reasons = list(check.reasons)
182
+ .slice(0, 3)
183
+ .map((r) => safeText(r, 40));
184
+ const why = reasons.length ? ` (${reasons.join(", ")})` : "";
185
+ if (check.status === "found")
186
+ return [
187
+ `Advisory ${advisories.length} known for this exact version (${source})`,
188
+ ...advisoryItems(advisories),
189
+ ];
190
+ if (advisories.length)
191
+ return [
192
+ `Advisory ${advisories.length} known for this exact version so far; lookup ${safeText(check.status ?? "unavailable", 30)}${why}, more may exist (${source})`,
193
+ ...advisoryItems(advisories),
194
+ ];
195
+ return [`Advisory ${safeText(check.status ?? "unavailable", 30)}${why}`];
196
+ }
197
+
198
+ function nextActions(report, { details, outDir }) {
199
+ const actions = [];
200
+ const exitCode = report.policy?.exitCode;
201
+ if (knownAdvisories(details?.vulnerabilityCheck))
202
+ actions.push(
203
+ "Known advisories exist: do not install this version until each advisory is reviewed.",
204
+ );
205
+ if (exitCode === 3)
206
+ actions.push(
207
+ "Review each finding and its cited evidence before installing.",
208
+ );
209
+ if (exitCode === 2)
210
+ actions.push(
211
+ "Coverage is incomplete: treat this as unverified and retry or inspect the failures.",
212
+ );
213
+ if (behaviorCues(report).some(highCue))
214
+ actions.push(
215
+ "Review each HIGH behavior cue and its cited file before installing.",
216
+ );
217
+ if (exitCode === 0)
218
+ actions.push(
219
+ "No deterministic finding is not proof of safety; review advisories and AI notes.",
220
+ );
221
+ const assessment = report.model?.assessment;
222
+ if (
223
+ report.model?.status === "complete" &&
224
+ assessment?.source === "model" &&
225
+ typeof assessment.nextAction === "string"
226
+ )
227
+ actions.push(`AI suggests: ${safeText(assessment.nextAction, 200)}`);
228
+ if (report.model?.status !== "complete")
229
+ actions.push(
230
+ "AI explanation was not produced; rely on deterministic evidence only.",
231
+ );
232
+ actions.push(
233
+ outDir
234
+ ? `Open ${safeText(outDir, 200)}/report.html for full evidence.`
235
+ : "Add --out DIR to save the full JSON and HTML evidence.",
236
+ );
237
+ return actions;
238
+ }
239
+
240
+ export function renderAdvisoryOnly(
241
+ result,
242
+ spec,
243
+ { style = terminalStyle() } = {},
244
+ ) {
245
+ const details = { vulnerabilityCheck: result.vulnerabilityCheck };
246
+ const reason = result.artifactVerification?.reason;
247
+ const found = knownAdvisories(result.vulnerabilityCheck);
248
+ const lines = [
249
+ `UNMASK ${safeText(spec, 120)}`,
250
+ style.paint(
251
+ found ? "yellow" : "red",
252
+ "Result Advisory-only: artifact not inspected; no verdict is claimed (exit 2)",
253
+ ),
254
+ `${style.paint("bold", "Artifact")} not inspected${reason ? ` (${safeText(reason, 80)})` : ""}; source comparison not performed`,
255
+ ...advisoryLines(details).map((line) =>
256
+ line.startsWith("Advisory")
257
+ ? style.paint("bold", "Advisory") + line.slice(8)
258
+ : line,
259
+ ),
260
+ `${style.paint("bold", "AI")} not run; no artifact evidence to explain`,
261
+ `${style.paint("bold", "Notice")} ${safeText(result.notice, 400)}`,
262
+ style.paint("bold", "Next"),
263
+ found
264
+ ? " - Known advisories exist: do not install this version until each advisory is reviewed."
265
+ : " - No current advisory is not evidence of safety; the artifact itself was not checked.",
266
+ " - Prefer a different exact version that UNMASK can fully analyze.",
267
+ ];
268
+ return lines.join("\n") + "\n";
269
+ }
270
+
271
+ const codes = { bold: 1, red: 31, yellow: 33, cyan: 36, dim: 2 };
272
+ const label =
273
+ /^(UNMASK|Result|Coverage|Source|Findings|Failures|Behavior|Advisory|AI|Next)\b/;
274
+
275
+ export function terminalStyle(stream, env = process.env) {
276
+ const tty = stream?.isTTY === true && env.TERM !== "dumb";
277
+ const color = tty && !env.NO_COLOR;
278
+ return {
279
+ tty,
280
+ color,
281
+ paint: (name, text) =>
282
+ color ? `\u001b[${codes[name]}m${text}\u001b[0m` : text,
283
+ };
284
+ }
285
+
286
+ function resultColor(report, details) {
287
+ if (report.policy?.exitCode === 2) return "red";
288
+ if (
289
+ report.policy?.exitCode === 3 ||
290
+ knownAdvisories(details?.vulnerabilityCheck) ||
291
+ behaviorCues(report).some(highCue)
292
+ )
293
+ return "yellow";
294
+ return "bold";
295
+ }
296
+
297
+ export function renderCheckSummary(
298
+ report,
299
+ { details, outDir, style = terminalStyle() } = {},
300
+ ) {
301
+ const lines = [
302
+ `UNMASK ${safeText(identity(report), 120)}`,
303
+ `Result ${verdict(report.policy?.exitCode)}`,
304
+ ...coverageLines(report),
305
+ ...findingLines(report),
306
+ ...behaviorLines(report),
307
+ ...advisoryLines(details),
308
+ ...modelLines(report.model),
309
+ "Next",
310
+ ...nextActions(report, { details, outDir }).map(
311
+ (action) => ` - ${action}`,
312
+ ),
313
+ ];
314
+ const tone = resultColor(report, details);
315
+ return (
316
+ lines
317
+ .map((line) => {
318
+ const match = label.exec(line);
319
+ if (!match) return line;
320
+ const rest = line.slice(match[0].length);
321
+ if (match[0] === "Result") return style.paint(tone, line);
322
+ if (match[0] === "Advisory" && / known for /.test(rest))
323
+ return style.paint("yellow", line);
324
+ return style.paint("bold", match[0]) + rest;
325
+ })
326
+ .join("\n") + "\n"
327
+ );
328
+ }
329
+
330
+ const frames = ["|", "/", "-", "\\"];
331
+ const phases = {
332
+ submitting: "Submitting exact package to the hosted service",
333
+ queued: "Queued on the hosted service",
334
+ running: "Hosted analysis running",
335
+ details: "Fetching package details and advisories",
336
+ local: "Analyzing local artifact",
337
+ model: "Requesting AI explanation",
338
+ writing: "Writing report files",
339
+ };
340
+
341
+ export function createProgress(
342
+ stream,
343
+ { env = process.env, now = Date.now, intervalMs = 120, subject = "" } = {},
344
+ ) {
345
+ const style = terminalStyle(stream, env);
346
+ const started = now();
347
+ let phase,
348
+ frame = 0,
349
+ timer;
350
+ const message = () =>
351
+ `${phases[phase] ?? safeText(phase, 60)}${subject ? ` ${safeText(subject, 120)}` : ""}`;
352
+ const draw = () => {
353
+ const seconds = Math.floor((now() - started) / 1000);
354
+ stream.write(
355
+ `\r\u001b[2K${style.paint("cyan", frames[frame++ % frames.length])} ${message()} ${style.paint("dim", `${seconds}s`)}`,
356
+ );
357
+ };
358
+ return {
359
+ update(next) {
360
+ if (next === phase) return;
361
+ phase = next;
362
+ if (!style.tty) {
363
+ stream.write(`unmask: ${message()}\n`);
364
+ return;
365
+ }
366
+ draw();
367
+ if (!timer) {
368
+ timer = setInterval(draw, intervalMs);
369
+ timer.unref?.();
370
+ }
371
+ },
372
+ stop() {
373
+ clearInterval(timer);
374
+ timer = undefined;
375
+ if (style.tty && phase !== undefined) stream.write("\r\u001b[2K");
376
+ },
377
+ };
378
+ }
379
+
380
+ const hints = {
381
+ usage: "Run unmask --help for commands and usage.",
382
+ "unknown-option": "See docs/cli.md for the supported check options.",
383
+ "missing-value": "Each option needs a value, for example --out ./report.",
384
+ "output-write-failed":
385
+ "Choose a fresh --out folder; existing report files are never replaced.",
386
+ "hosted-unavailable": "Check your network or UNMASK_SERVER_URL, then retry.",
387
+ "hosted-timeout": "Retry later; the hosted service may be busy.",
388
+ };
389
+
390
+ export function renderError(code, message, style) {
391
+ const head = style.paint("red", `unmask error ${safeText(code, 60)}`);
392
+ const hint = hints[code]
393
+ ? `\n${style.paint("dim", `Hint: ${hints[code]}`)}`
394
+ : "";
395
+ return `${head}: ${safeText(message, 400)}${hint}\n`;
396
+ }
@@ -0,0 +1,233 @@
1
+ import { createHash } from "node:crypto";
2
+ import { watch as fsWatch } from "node:fs";
3
+ import path from "node:path";
4
+ import { LocalInputError } from "../core/pipeline.mjs";
5
+ import {
6
+ formatPreflight,
7
+ hostedChecker,
8
+ hostedScreener,
9
+ lockIdentities,
10
+ parseCount,
11
+ preflight,
12
+ readLockText,
13
+ } from "./preflight.mjs";
14
+
15
+ const DEFAULT_DEBOUNCE_MS = 750;
16
+ const failure = (code, message) => new LocalInputError(code, message);
17
+ const digest = (text) => createHash("sha256").update(text).digest("hex");
18
+
19
+ export function watchLockfile({
20
+ lockfile,
21
+ debounceMs = DEFAULT_DEBOUNCE_MS,
22
+ maxChecks,
23
+ check = true,
24
+ checkPackage,
25
+ screenAdvisories,
26
+ signal,
27
+ onEvent = () => {},
28
+ watchFactory = (directory, listener) => fsWatch(directory, listener),
29
+ readText = readLockText,
30
+ }) {
31
+ const target = path.basename(lockfile);
32
+ let baselineText = null;
33
+ let lastHash = null;
34
+ let timer = null;
35
+ let running = null;
36
+ let pending = false;
37
+ let runs = 0;
38
+
39
+ async function evaluate() {
40
+ let text;
41
+ try {
42
+ text = await readText(lockfile);
43
+ } catch (error) {
44
+ onEvent({ type: "lockfile-unreadable", code: error.code ?? "unknown" });
45
+ return;
46
+ }
47
+ const hash = digest(text);
48
+ if (hash === lastHash) return;
49
+ lastHash = hash;
50
+ try {
51
+ lockIdentities(text);
52
+ } catch (error) {
53
+ onEvent({ type: "lockfile-invalid", code: error.code });
54
+ return;
55
+ }
56
+ if (baselineText === null) {
57
+ baselineText = text;
58
+ onEvent({ type: "baseline", sha256: hash, audited: false });
59
+ return;
60
+ }
61
+ runs++;
62
+ const result = await preflight({
63
+ baselineText,
64
+ lockfileText: text,
65
+ check,
66
+ maxChecks,
67
+ checkPackage,
68
+ screenAdvisories,
69
+ signal,
70
+ });
71
+ onEvent({ type: "result", sha256: hash, result });
72
+ if (!result.cancelled) baselineText = text;
73
+ }
74
+
75
+ function trigger() {
76
+ if (signal?.aborted) return;
77
+ if (running) {
78
+ pending = true;
79
+ return;
80
+ }
81
+ running = (async () => {
82
+ do {
83
+ pending = false;
84
+ try {
85
+ await evaluate();
86
+ } catch {
87
+ lastHash = null;
88
+ try {
89
+ onEvent({ type: "run-error" });
90
+ } catch {}
91
+ }
92
+ } while (pending && !signal?.aborted);
93
+ })().finally(() => (running = null));
94
+ }
95
+
96
+ let watcher;
97
+ try {
98
+ watcher = watchFactory(path.dirname(lockfile), (_type, filename) => {
99
+ if (filename && path.basename(String(filename)) !== target) return;
100
+ clearTimeout(timer);
101
+ timer = setTimeout(trigger, debounceMs);
102
+ });
103
+ } catch {
104
+ throw failure(
105
+ "watch-unavailable",
106
+ "The lockfile directory cannot be watched.",
107
+ );
108
+ }
109
+ trigger();
110
+ return new Promise((resolve) => {
111
+ let stopped = false;
112
+ const stop = async (failed = false) => {
113
+ if (stopped) return;
114
+ stopped = true;
115
+ clearTimeout(timer);
116
+ watcher.close();
117
+ const interrupted = running !== null;
118
+ await running;
119
+ resolve({ runs, interrupted, failed });
120
+ };
121
+ watcher.on?.("error", () => {
122
+ onEvent({ type: "watch-error" });
123
+ stop(true);
124
+ });
125
+ if (signal?.aborted) stop();
126
+ else signal?.addEventListener("abort", () => stop(), { once: true });
127
+ });
128
+ }
129
+
130
+ function describe(event) {
131
+ if (event.type === "baseline")
132
+ return `watch baseline recorded (sha256 ${event.sha256.slice(0, 12)}); the baseline itself is not audited (use unmask audit); waiting for changes\n`;
133
+ if (event.type === "lockfile-invalid")
134
+ return `watch ${event.code}: lockfile is not a complete npm lockfile yet; no check run, baseline kept\n`;
135
+ if (event.type === "run-error")
136
+ return "watch run-error: this run failed unexpectedly; no result is claimed and the baseline is unchanged\n";
137
+ if (event.type === "watch-error")
138
+ return "watch-error: the file watcher failed; watching stopped\n";
139
+ if (event.type === "lockfile-unreadable")
140
+ return `watch ${event.code}: lockfile could not be read; no check run\n`;
141
+ return formatPreflight(event.result);
142
+ }
143
+
144
+ function parseArgs(argv) {
145
+ const options = { lockfile: "package-lock.json", check: true, json: false };
146
+ const seen = new Set();
147
+ for (let index = 0; index < argv.length; index++) {
148
+ const key = argv[index];
149
+ if (seen.has(key))
150
+ throw failure(
151
+ "duplicate-option",
152
+ "Each option may be supplied only once",
153
+ );
154
+ seen.add(key);
155
+ if (key === "--json") options.json = true;
156
+ else if (key === "--no-check") options.check = false;
157
+ else if (["--lockfile", "--debounce-ms", "--max-checks"].includes(key)) {
158
+ const value = argv[++index];
159
+ if (!value || value.startsWith("--"))
160
+ throw failure("missing-value", "An option is missing its value");
161
+ options[key.slice(2)] = value;
162
+ } else
163
+ throw failure(
164
+ "unknown-option",
165
+ "Only the documented watch options are supported",
166
+ );
167
+ }
168
+ const debounce = options["debounce-ms"] ?? String(DEFAULT_DEBOUNCE_MS);
169
+ if (!/^\d{2,5}$/.test(debounce) || +debounce < 50 || +debounce > 60000)
170
+ throw failure("invalid-value", "--debounce-ms must be from 50 to 60000.");
171
+ options.debounceMs = Number(debounce);
172
+ if (options["max-checks"] !== undefined)
173
+ options.maxChecks = parseCount(options["max-checks"], "--max-checks");
174
+ return options;
175
+ }
176
+
177
+ export async function watchMain(argv, runtime = {}) {
178
+ let options;
179
+ try {
180
+ options = parseArgs(argv);
181
+ } catch (error) {
182
+ process.stderr.write(`${error.code}: ${error.message}\n`);
183
+ return 2;
184
+ }
185
+ const controller = new AbortController();
186
+ const signal = runtime.signal
187
+ ? AbortSignal.any([runtime.signal, controller.signal])
188
+ : controller.signal;
189
+ const onSigint = () => controller.abort();
190
+ process.once("SIGINT", onSigint);
191
+ const lockfile = path.resolve(options.lockfile);
192
+ process.stdout.write(
193
+ options.json
194
+ ? JSON.stringify({ type: "watching", lockfile }) + "\n"
195
+ : `watching ${lockfile}; press Ctrl-C to stop\n`,
196
+ );
197
+ try {
198
+ const summary = await watchLockfile({
199
+ ...options,
200
+ checkPackage: runtime.checkPackage ?? hostedChecker(runtime),
201
+ screenAdvisories: runtime.screenAdvisories ?? hostedScreener(runtime),
202
+ signal,
203
+ watchFactory: runtime.watchFactory,
204
+ onEvent: (event) =>
205
+ process.stdout.write(
206
+ options.json ? JSON.stringify(event) + "\n" : describe(event),
207
+ ),
208
+ });
209
+ const exitCode = summary.failed ? 2 : 130;
210
+ process.stdout.write(
211
+ options.json
212
+ ? JSON.stringify({
213
+ type: "stopped",
214
+ runs: summary.runs,
215
+ interrupted: summary.interrupted,
216
+ exitCode,
217
+ }) + "\n"
218
+ : summary.interrupted
219
+ ? `watch stopped after ${summary.runs} run(s); the in-progress run was cancelled and claims no result\n`
220
+ : `watch stopped after ${summary.runs} run(s)\n`,
221
+ );
222
+ return exitCode;
223
+ } catch (error) {
224
+ process.stderr.write(
225
+ error instanceof LocalInputError
226
+ ? `${error.code}: ${error.message}\n`
227
+ : "Internal watch error; no result is claimed.\n",
228
+ );
229
+ return error instanceof LocalInputError ? 2 : 1;
230
+ } finally {
231
+ process.removeListener("SIGINT", onSigint);
232
+ }
233
+ }