@xagents_in/unmask 0.1.2 → 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 (56) hide show
  1. package/README.md +301 -85
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/batch.mjs +218 -0
  5. package/dist/cli/cache.mjs +488 -0
  6. package/dist/cli/compare.mjs +261 -0
  7. package/dist/cli/config.mjs +141 -0
  8. package/dist/cli/discovery.mjs +372 -0
  9. package/dist/cli/doctor.mjs +354 -0
  10. package/dist/cli/exceptions.mjs +170 -0
  11. package/dist/cli/file.mjs +271 -0
  12. package/dist/cli/files.mjs +141 -0
  13. package/dist/cli/history.mjs +232 -0
  14. package/dist/cli/hosted.mjs +251 -43
  15. package/dist/cli/info.mjs +125 -0
  16. package/dist/cli/init.mjs +57 -0
  17. package/dist/cli/install-guard.mjs +896 -0
  18. package/dist/cli/lockfile.mjs +330 -0
  19. package/dist/cli/main.mjs +101 -14
  20. package/dist/cli/preflight.mjs +388 -0
  21. package/dist/cli/sarif.mjs +100 -0
  22. package/dist/cli/scan.mjs +128 -0
  23. package/dist/cli/shell-integration.mjs +122 -0
  24. package/dist/cli/terminal.mjs +396 -0
  25. package/dist/cli/watch.mjs +233 -0
  26. package/dist/cli/why.mjs +256 -0
  27. package/dist/core/advisories.mjs +101 -24
  28. package/dist/core/advisory-cache.mjs +295 -0
  29. package/dist/core/behavior-signals.mjs +1088 -0
  30. package/dist/core/dependency-graph.mjs +332 -0
  31. package/dist/core/evidence-exports.mjs +469 -0
  32. package/dist/core/guard-config.mjs +463 -0
  33. package/dist/core/guard-policy.mjs +308 -0
  34. package/dist/core/package-details.mjs +42 -6
  35. package/dist/core/registry-facts.mjs +342 -0
  36. package/dist/core/report-exploration.mjs +328 -0
  37. package/dist/core/report.mjs +7 -0
  38. package/dist/core/sarif.mjs +556 -0
  39. package/dist/core/syntax.mjs +296 -15
  40. package/dist/model/client.mjs +204 -56
  41. package/dist/model/diff-evidence.mjs +177 -0
  42. package/dist/model/summary.mjs +183 -0
  43. package/dist/server/advisory-result.mjs +137 -0
  44. package/dist/server/app.mjs +66 -2
  45. package/dist/server/dependency-screening.mjs +386 -0
  46. package/dist/server/exports.mjs +63 -0
  47. package/dist/server/guard-decision.mjs +37 -0
  48. package/dist/server/main.mjs +25 -7
  49. package/dist/server/queue.mjs +95 -53
  50. package/dist/server/registry-facts.mjs +118 -0
  51. package/dist/server/report-exploration.mjs +87 -0
  52. package/dist/server/store.mjs +21 -1
  53. package/dist/version.json +1 -1
  54. package/dist/viewer/render.mjs +10 -2
  55. package/dist/viewer/shell.mjs +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,330 @@
1
+ import { constants, open } from "node:fs/promises";
2
+ import { LocalInputError } from "../core/pipeline.mjs";
3
+
4
+ export const LOCKFILE_LIMITS = Object.freeze({
5
+ maxBytes: 32 * 1024 * 1024,
6
+ maxEntries: 100000,
7
+ });
8
+ const EXACT =
9
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
10
+ const NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
11
+ const EDGE_TYPES = [
12
+ ["dependencies", "prod"],
13
+ ["optionalDependencies", "optional"],
14
+ ["peerDependencies", "peer"],
15
+ ["devDependencies", "dev"],
16
+ ];
17
+ const invalid = (message) => new LocalInputError("lockfile-invalid", message);
18
+ const isObject = (value) =>
19
+ value !== null && typeof value === "object" && !Array.isArray(value);
20
+
21
+ export const isExactVersion = (value) =>
22
+ typeof value === "string" &&
23
+ value.length <= 256 &&
24
+ EXACT.test(value) &&
25
+ !value
26
+ .split("+")[0]
27
+ .split("-")
28
+ .slice(1)
29
+ .join("-")
30
+ .split(".")
31
+ .some((part) => /^0\d+$/.test(part));
32
+ export const isPackageName = (value) =>
33
+ typeof value === "string" && value.length <= 214 && NAME.test(value);
34
+
35
+ export function nameFromKey(key) {
36
+ const index = key.lastIndexOf("node_modules/");
37
+ return index === -1 ? null : key.slice(index + "node_modules/".length);
38
+ }
39
+
40
+ export function parentKey(key) {
41
+ const index = key.lastIndexOf("/node_modules/");
42
+ return index === -1 ? "" : key.slice(0, index);
43
+ }
44
+
45
+ export const NPM_REGISTRY_HOST = "registry.npmjs.org";
46
+ export function isNpmRegistryTarball(resolved, name, version) {
47
+ if (typeof resolved !== "string") return false;
48
+ try {
49
+ const url = new URL(resolved);
50
+ return (
51
+ url.protocol === "https:" &&
52
+ url.host === NPM_REGISTRY_HOST &&
53
+ !url.username &&
54
+ !url.password &&
55
+ !url.search &&
56
+ !url.hash &&
57
+ url.pathname === `/${name}/-/${name.split("/").pop()}-${version}.tgz`
58
+ );
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function classify(key, raw) {
65
+ if (key === "") return { kind: "root", reason: null };
66
+ if (raw.link === true) return { kind: "link", reason: "workspace-or-link" };
67
+ if (!key.includes("node_modules/"))
68
+ return { kind: "workspace", reason: "workspace-or-link" };
69
+ if (raw.inBundle === true)
70
+ return { kind: "bundled", reason: "bundled-dependency" };
71
+ const name = raw.name ?? nameFromKey(key);
72
+ if (!isPackageName(name)) return { kind: "invalid", reason: "invalid-name" };
73
+ if (typeof raw.version !== "string")
74
+ return { kind: "invalid", reason: "missing-version" };
75
+ if (/^(?:git\+|git:|github:|https?:|file:)/.test(raw.version))
76
+ return { kind: "url", reason: "non-registry-source" };
77
+ if (!isExactVersion(raw.version))
78
+ return { kind: "invalid", reason: "inexact-version" };
79
+ if (
80
+ raw.resolved !== undefined &&
81
+ !isNpmRegistryTarball(raw.resolved, name, raw.version)
82
+ )
83
+ return { kind: "url", reason: "non-registry-source" };
84
+ return { kind: "registry", reason: null };
85
+ }
86
+
87
+ function edges(raw) {
88
+ const result = [];
89
+ const peerMeta = isObject(raw.peerDependenciesMeta)
90
+ ? raw.peerDependenciesMeta
91
+ : {};
92
+ for (const [field, type] of EDGE_TYPES) {
93
+ const value = raw[field];
94
+ if (value === undefined) continue;
95
+ if (!isObject(value)) throw invalid(`Lockfile ${field} must be an object.`);
96
+ for (const [name, range] of Object.entries(value)) {
97
+ // Legacy or unusual names stay as edges; only their own entries are classified invalid.
98
+ if (
99
+ typeof range !== "string" ||
100
+ !name ||
101
+ name.length > 214 ||
102
+ /[\0-\x1f\x7f\\]/.test(name)
103
+ )
104
+ throw invalid(`Lockfile ${field} contains an invalid entry.`);
105
+ result.push({
106
+ name,
107
+ range,
108
+ type:
109
+ type === "peer" && peerMeta[name]?.optional === true
110
+ ? "peer-optional"
111
+ : type,
112
+ });
113
+ }
114
+ }
115
+ return result;
116
+ }
117
+
118
+ export function parseLockfile(text, limits = LOCKFILE_LIMITS) {
119
+ if (typeof text !== "string" || Buffer.byteLength(text) > limits.maxBytes)
120
+ throw invalid("Lockfile is missing or exceeds the size limit.");
121
+ let data;
122
+ try {
123
+ data = JSON.parse(text);
124
+ } catch {
125
+ throw invalid("Lockfile is not valid JSON.");
126
+ }
127
+ if (!isObject(data)) throw invalid("Lockfile must be a JSON object.");
128
+ if (![2, 3].includes(data.lockfileVersion))
129
+ throw new LocalInputError(
130
+ "lockfile-unsupported",
131
+ "Only npm lockfileVersion 2 or 3 (with a packages map) is supported.",
132
+ );
133
+ // npm writes an empty packages map for a project with no dependencies.
134
+ if (isObject(data.packages) && Object.keys(data.packages).length === 0)
135
+ data.packages = { "": {} };
136
+ if (!isObject(data.packages) || !isObject(data.packages[""]))
137
+ throw invalid("Lockfile packages map with a root entry is required.");
138
+ const keys = Object.keys(data.packages);
139
+ if (keys.length > limits.maxEntries)
140
+ throw invalid("Lockfile exceeds the entry limit.");
141
+ const entries = new Map();
142
+ for (const key of keys) {
143
+ const value = data.packages[key];
144
+ const badKey =
145
+ key.length > 4096 ||
146
+ (key !== "" &&
147
+ key
148
+ .split("/")
149
+ .some(
150
+ (part) => ["", ".", ".."].includes(part) || part.includes("\\"),
151
+ ));
152
+ // Malformed entries are kept as invalid so the rest of the graph still parses.
153
+ const raw = isObject(value) && !badKey ? value : {};
154
+ const { kind, reason } = badKey
155
+ ? { kind: "invalid", reason: "invalid-location" }
156
+ : !isObject(value)
157
+ ? { kind: "invalid", reason: "invalid-entry" }
158
+ : classify(key, raw);
159
+ let entryEdges = [];
160
+ let edgesValid = true;
161
+ try {
162
+ entryEdges = kind === "link" ? [] : edges(raw);
163
+ } catch {
164
+ edgesValid = false;
165
+ }
166
+ entries.set(key, {
167
+ key,
168
+ name:
169
+ key === ""
170
+ ? (raw.name ?? data.name ?? null)
171
+ : (raw.name ?? nameFromKey(key) ?? key),
172
+ version: typeof raw.version === "string" ? raw.version : null,
173
+ kind,
174
+ reason,
175
+ dev: raw.dev === true,
176
+ optional: raw.optional === true || raw.devOptional === true,
177
+ bundled: raw.inBundle === true,
178
+ resolved: typeof raw.resolved === "string" ? raw.resolved : null,
179
+ integrity: typeof raw.integrity === "string" ? raw.integrity : null,
180
+ bound:
181
+ kind === "registry" &&
182
+ isNpmRegistryTarball(
183
+ raw.resolved,
184
+ raw.name ?? nameFromKey(key),
185
+ raw.version,
186
+ ) &&
187
+ /^sha512-[A-Za-z0-9+/]{86}==$/.test(raw.integrity ?? ""),
188
+ edges: entryEdges,
189
+ edgesValid,
190
+ });
191
+ }
192
+ // npm omits name on workspace entries; the linking node_modules key carries it.
193
+ for (const entry of entries.values()) {
194
+ const target = entry.kind === "link" && entries.get(entry.resolved ?? "");
195
+ if (target?.kind === "workspace" && target.name === target.key)
196
+ target.name = nameFromKey(entry.key);
197
+ }
198
+ return { lockfileVersion: data.lockfileVersion, entries };
199
+ }
200
+
201
+ export async function readLockfile(filename, limits = LOCKFILE_LIMITS) {
202
+ const unreadable = () =>
203
+ new LocalInputError(
204
+ "lockfile-unreadable",
205
+ "The lockfile must be a readable regular file within the size limit; pass --lockfile PATH to package-lock.json.",
206
+ );
207
+ let handle;
208
+ try {
209
+ // Nonblocking open plus fstat rejects FIFOs/devices before any read.
210
+ handle = await open(filename, constants.O_RDONLY | constants.O_NONBLOCK);
211
+ const info = await handle.stat();
212
+ if (!info.isFile() || info.size > limits.maxBytes) throw unreadable();
213
+ const buffer = Buffer.alloc(info.size + 1);
214
+ let length = 0;
215
+ while (length < buffer.length) {
216
+ const { bytesRead } = await handle.read(
217
+ buffer,
218
+ length,
219
+ buffer.length - length,
220
+ length,
221
+ );
222
+ if (bytesRead === 0) break;
223
+ length += bytesRead;
224
+ }
225
+ if (length !== info.size) throw unreadable();
226
+ return parseLockfile(buffer.subarray(0, length).toString("utf8"), limits);
227
+ } catch (error) {
228
+ if (error instanceof LocalInputError) throw error;
229
+ throw unreadable();
230
+ } finally {
231
+ await handle?.close();
232
+ }
233
+ }
234
+
235
+ export function resolveEdge(lock, fromKey, name) {
236
+ let base = fromKey;
237
+ for (let step = 0; step < 4096; step++) {
238
+ const candidate = (base ? base + "/" : "") + "node_modules/" + name;
239
+ const entry = lock.entries.get(candidate);
240
+ if (entry) {
241
+ if (entry.kind !== "link") return entry;
242
+ return lock.entries.get(entry.resolved ?? "") ?? null;
243
+ }
244
+ if (base === "") return null;
245
+ base = parentKey(base);
246
+ }
247
+ return null;
248
+ }
249
+
250
+ function scanEdges(lock, omitDev) {
251
+ const missing = [];
252
+ const omitted = { optional: 0, peerOptional: 0, devEdges: 0 };
253
+ for (const entry of lock.entries.values()) {
254
+ if (entry.kind === "link" || (omitDev && entry.dev)) continue;
255
+ const project = ["root", "workspace"].includes(entry.kind);
256
+ for (const edge of entry.edges) {
257
+ if (edge.type === "dev" && !project) continue;
258
+ const found = resolveEdge(lock, entry.key, edge.name);
259
+ if (edge.type === "dev" && omitDev) omitted.devEdges++;
260
+ else if (found) continue;
261
+ else if (edge.type === "optional") omitted.optional++;
262
+ else if (edge.type === "peer-optional") omitted.peerOptional++;
263
+ else missing.push({ from: entry.key, name: edge.name, type: edge.type });
264
+ }
265
+ }
266
+ return { missing, omitted };
267
+ }
268
+
269
+ export function unresolvedEdges(lock, { omitDev = false } = {}) {
270
+ return scanEdges(lock, omitDev).missing;
271
+ }
272
+
273
+ export function exactTargets(lock, { omitDev = false } = {}) {
274
+ const unique = new Map();
275
+ const skipped = {};
276
+ let considered = 0;
277
+ const { missing: unresolved, omitted } = scanEdges(lock, omitDev);
278
+ omitted.devEntries = 0;
279
+ if (unresolved.length) skipped["unresolved-dependency"] = unresolved.length;
280
+ const badEdges = [...lock.entries.values()].filter(
281
+ (entry) => !entry.edgesValid && !(omitDev && entry.dev),
282
+ ).length;
283
+ if (badEdges) skipped["invalid-dependencies"] = badEdges;
284
+ for (const entry of lock.entries.values()) {
285
+ if (omitDev && entry.dev) {
286
+ if (!["root", "workspace", "link"].includes(entry.kind))
287
+ omitted.devEntries++;
288
+ continue;
289
+ }
290
+ // A link whose target entry is absent means unlocked local code.
291
+ if (entry.kind === "link" && !lock.entries.has(entry.resolved ?? "")) {
292
+ skipped["dangling-link"] = (skipped["dangling-link"] ?? 0) + 1;
293
+ continue;
294
+ }
295
+ if (["root", "workspace", "link"].includes(entry.kind)) continue;
296
+ considered++;
297
+ if (entry.kind !== "registry") {
298
+ skipped[entry.reason] = (skipped[entry.reason] ?? 0) + 1;
299
+ continue;
300
+ }
301
+ const spec = `${entry.name}@${entry.version}`;
302
+ const target = unique.get(spec) ?? {
303
+ spec,
304
+ name: entry.name,
305
+ version: entry.version,
306
+ locations: [],
307
+ bound: true,
308
+ integrity: [],
309
+ aliases: [],
310
+ };
311
+ target.locations.push(entry.key);
312
+ const alias = nameFromKey(entry.key);
313
+ if (alias !== entry.name && !target.aliases.includes(alias))
314
+ target.aliases.push(alias);
315
+ target.bound &&= entry.bound;
316
+ if (entry.integrity && !target.integrity.includes(entry.integrity))
317
+ target.integrity.push(entry.integrity);
318
+ if (target.integrity.length > 1) target.bound = false;
319
+ unique.set(spec, target);
320
+ }
321
+ return {
322
+ considered,
323
+ targets: [...unique.values()].sort((a, b) =>
324
+ a.spec < b.spec ? -1 : a.spec > b.spec ? 1 : 0,
325
+ ),
326
+ skipped,
327
+ omitted,
328
+ unresolved: unresolved.slice(0, 100),
329
+ };
330
+ }
package/dist/cli/main.mjs CHANGED
@@ -8,7 +8,35 @@ import { renderReport } from "../viewer/render.mjs";
8
8
  import { parsePackageSpec, AcquisitionError } from "../core/acquire.mjs";
9
9
  import { addConfiguredModel } from "../core/online.mjs";
10
10
  import { modelClientFromEnv } from "../model/config.mjs";
11
- import { checkHosted } from "./hosted.mjs";
11
+ import { getHostedDetails } from "./hosted.mjs";
12
+ import { infoCommand } from "./info.mjs";
13
+ import { filesCommand } from "./files.mjs";
14
+ import { fileCommand } from "./file.mjs";
15
+ import {
16
+ createProgress,
17
+ renderAdvisoryOnly,
18
+ renderCheckSummary,
19
+ renderError,
20
+ terminalStyle,
21
+ } from "./terminal.mjs";
22
+ import { discoveryCommand, isDiscoveryRequest } from "./discovery.mjs";
23
+ import { runConfig } from "./config.mjs";
24
+ import { runExceptions } from "./exceptions.mjs";
25
+ import { runInit } from "./init.mjs";
26
+ import { runScan } from "./scan.mjs";
27
+ import { runBatch } from "./batch.mjs";
28
+ import { runInstallGuard } from "./install-guard.mjs";
29
+ import { runShellIntegration } from "./shell-integration.mjs";
30
+ import { runAudit } from "./audit.mjs";
31
+ import { runWhy } from "./why.mjs";
32
+ import { preflightMain } from "./preflight.mjs";
33
+ import { watchMain } from "./watch.mjs";
34
+ import { runCompare } from "./compare.mjs";
35
+ import { runHistory } from "./history.mjs";
36
+ import { doctorCommand } from "./doctor.mjs";
37
+ import { cacheCommand } from "./cache.mjs";
38
+ import { sarifCommand } from "./sarif.mjs";
39
+ import { annotationsCommand } from "./annotations.mjs";
12
40
 
13
41
  const names = {
14
42
  "--tarball": "tarballPath",
@@ -33,7 +61,7 @@ function parse(argv) {
33
61
  if (argv[0] !== "check")
34
62
  throw new LocalInputError(
35
63
  "usage",
36
- "Use check name@version or an explicit local tarball with --offline",
64
+ "Use check name@version, audit, why name, or an explicit local tarball with --offline",
37
65
  );
38
66
  const options = {};
39
67
  let start = 1;
@@ -76,8 +104,6 @@ function parse(argv) {
76
104
  );
77
105
  options[names[key]] = value;
78
106
  }
79
- if (!options.out)
80
- throw new LocalInputError("usage", "Check requires --out DIR");
81
107
  if (options.registrySpec) {
82
108
  if (
83
109
  options.offline ||
@@ -103,7 +129,7 @@ function parse(argv) {
103
129
  if (!options.registrySpec && !options.cacheDir)
104
130
  throw new LocalInputError(
105
131
  "usage",
106
- "Offline check requires --cache-dir DIR and --out DIR",
132
+ "Offline check requires --cache-dir DIR",
107
133
  );
108
134
  if (options.claim !== undefined) options.claim = exactClaim(options.claim);
109
135
  return options;
@@ -161,19 +187,56 @@ async function configuredModel(allowModel, runtime) {
161
187
  }
162
188
 
163
189
  export async function main(argv = process.argv.slice(2), runtime = {}) {
190
+ const stdout = runtime.stdout ?? process.stdout;
191
+ const stderr = runtime.stderr ?? process.stderr;
192
+ const env = runtime.env ?? process.env;
193
+ const errorStyle = terminalStyle(stderr, env);
194
+ let progress;
164
195
  try {
196
+ if (isDiscoveryRequest(argv)) return discoveryCommand(argv, runtime);
197
+ if (argv[0] === "install" || argv[0] === "ci")
198
+ return await runInstallGuard(argv[0], argv.slice(1), runtime);
199
+ if (argv[0] === "enable" || argv[0] === "disable")
200
+ return await runShellIntegration(argv[0], argv.slice(1), runtime);
201
+ if (argv[0] === "scan") return await runScan(argv.slice(1), runtime);
202
+ if (argv[0] === "batch") return await runBatch(argv.slice(1), runtime);
203
+ if (argv[0] === "config") return await runConfig(argv.slice(1), runtime);
204
+ if (argv[0] === "exceptions")
205
+ return await runExceptions(argv.slice(1), runtime);
206
+ if (argv[0] === "init") return await runInit(argv.slice(1), runtime);
207
+ if (argv[0] === "file") return await fileCommand(argv.slice(1), runtime);
208
+ if (argv[0] === "files") return await filesCommand(argv.slice(1), runtime);
209
+ if (argv[0] === "info") return await infoCommand(argv.slice(1), runtime);
210
+ if (argv[0] === "audit") return await runAudit(argv.slice(1), runtime);
211
+ if (argv[0] === "preflight")
212
+ return await preflightMain(argv.slice(1), runtime);
213
+ if (argv[0] === "watch") return await watchMain(argv.slice(1), runtime);
214
+ if (argv[0] === "why") return await runWhy(argv.slice(1));
215
+ if (argv[0] === "compare") return await runCompare(argv.slice(1), runtime);
216
+ if (argv[0] === "history") return await runHistory(argv.slice(1), runtime);
217
+ if (argv[0] === "doctor")
218
+ return await doctorCommand(argv.slice(1), runtime);
219
+ if (argv[0] === "cache") return await cacheCommand(argv.slice(1), runtime);
220
+ if (argv[0] === "sarif") return await sarifCommand(argv.slice(1));
221
+ if (argv[0] === "annotations")
222
+ return await annotationsCommand(argv.slice(1));
165
223
  const { out, offline, allowModel, registrySpec, ...options } = parse(argv);
166
- let report;
224
+ progress = createProgress(stderr, { env, subject: registrySpec });
225
+ let report, details;
167
226
  if (registrySpec)
168
- report = await checkHosted(registrySpec, {
169
- serverUrl: (runtime.env ?? process.env).UNMASK_SERVER_URL,
227
+ ({ report, details } = await getHostedDetails(registrySpec, {
228
+ serverUrl: env.UNMASK_SERVER_URL,
170
229
  fetchImpl: runtime.fetchImpl,
171
230
  signal: runtime.signal,
172
231
  limits: runtime.hostedLimits,
173
- });
232
+ optionalDetails: true,
233
+ onStatus: progress.update,
234
+ }));
174
235
  else {
175
236
  const model = await configuredModel(allowModel, runtime);
237
+ progress.update("local");
176
238
  report = await analyzeLocal({ ...options, limits: runtime.limits });
239
+ if (allowModel) progress.update("model");
177
240
  if (allowModel) report = await addConfiguredModel(report, model);
178
241
  if (!allowModel)
179
242
  report = {
@@ -181,16 +244,40 @@ export async function main(argv = process.argv.slice(2), runtime = {}) {
181
244
  model: { status: "not-sent", reason: "consent-required" },
182
245
  };
183
246
  }
184
- await writeReports(out, report);
185
- process.stdout.write(`Report written to ${path.resolve(out)}\n`);
247
+ const outDir = out === undefined ? undefined : path.resolve(out);
248
+ if (outDir) progress.update("writing");
249
+ if (outDir) await writeReports(outDir, report);
250
+ progress.stop();
251
+ stdout.write(
252
+ renderCheckSummary(report, {
253
+ details,
254
+ outDir,
255
+ style: terminalStyle(stdout, env),
256
+ }),
257
+ );
186
258
  return report.policy.exitCode;
187
259
  } catch (error) {
260
+ progress?.stop();
261
+ if (error instanceof LocalInputError && error.advisoryResult)
262
+ stdout.write(
263
+ renderAdvisoryOnly(
264
+ error.advisoryResult,
265
+ `${error.advisoryResult.package.name}@${error.advisoryResult.package.version}`,
266
+ {
267
+ style: terminalStyle(stdout, env),
268
+ },
269
+ ),
270
+ );
188
271
  if (error instanceof LocalInputError) {
189
- process.stderr.write(`${error.code}: ${error.message}\n`);
272
+ stderr.write(renderError(error.code, error.message, errorStyle));
190
273
  return 2;
191
274
  }
192
- process.stderr.write(
193
- "Internal analysis error; no completed result is claimed.\n",
275
+ stderr.write(
276
+ renderError(
277
+ "internal",
278
+ "Internal analysis error; no completed result is claimed.",
279
+ errorStyle,
280
+ ),
194
281
  );
195
282
  return 1;
196
283
  }