@westbayberry/dg 1.0.1 → 1.0.3

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/README.md CHANGED
@@ -57,7 +57,7 @@ dg scan [options]
57
57
 
58
58
  ## CI Examples
59
59
 
60
- ### GitHub Actions
60
+ ### GitHub Actions CI
61
61
 
62
62
  ```yaml
63
63
  - name: Scan dependencies
package/dist/index.js CHANGED
@@ -124,7 +124,10 @@ async function callAnalyzeBatch(packages, config) {
124
124
  if (error instanceof Error && error.name === "AbortError") {
125
125
  throw new APIError("Request timed out after 120s. Try scanning fewer packages.", 408, "");
126
126
  }
127
- throw error;
127
+ // Node.js fetch wraps the real error in .cause
128
+ const cause = error instanceof Error && error.cause;
129
+ const detail = cause ? `: ${cause.message || cause}` : "";
130
+ throw new Error(`fetch failed${detail}`);
128
131
  }
129
132
  clearTimeout(timeoutId);
130
133
  if (response.status === 401) {
@@ -177,15 +180,37 @@ exports.getVersion = getVersion;
177
180
  const node_util_1 = __nccwpck_require__(975);
178
181
  const node_fs_1 = __nccwpck_require__(24);
179
182
  const node_path_1 = __nccwpck_require__(760);
183
+ const node_os_1 = __nccwpck_require__(161);
184
+ function loadDgrc() {
185
+ const candidates = [
186
+ (0, node_path_1.join)(process.cwd(), ".dgrc.json"),
187
+ (0, node_path_1.join)((0, node_os_1.homedir)(), ".dgrc.json"),
188
+ ];
189
+ for (const filepath of candidates) {
190
+ if ((0, node_fs_1.existsSync)(filepath)) {
191
+ try {
192
+ return JSON.parse((0, node_fs_1.readFileSync)(filepath, "utf-8"));
193
+ }
194
+ catch {
195
+ process.stderr.write(`Warning: Failed to parse ${filepath}, ignoring.\n`);
196
+ }
197
+ }
198
+ }
199
+ return {};
200
+ }
180
201
  const USAGE = `
181
202
  Dependency Guardian — Supply chain security scanner
182
203
 
183
204
  Usage:
184
205
  dependency-guardian scan [options]
185
206
  dg scan [options]
207
+ dg npm install <pkg> [npm-flags]
208
+ dg wrap
186
209
 
187
210
  Commands:
188
211
  scan Scan dependencies for security risks (default)
212
+ npm Wrap npm commands — scans packages before installing
213
+ wrap Show instructions to alias npm to dg
189
214
 
190
215
  Options:
191
216
  --api-key <key> API key (or set DG_API_KEY env var)
@@ -198,25 +223,36 @@ const USAGE = `
198
223
  --json Output JSON for CI parsing
199
224
  --scan-all Scan all packages, not just changed
200
225
  --base-lockfile <path> Path to base lockfile for explicit diff
226
+ --debug Show diagnostic output (discovery, batches, timing)
227
+ --no-config Skip loading .dgrc.json config file
201
228
  --help Show this help message
202
229
  --version Show version number
203
230
 
231
+ Config File:
232
+ Place a .dgrc.json in your project root or home directory.
233
+ Precedence: CLI flags > env vars > .dgrc.json > defaults
234
+
204
235
  Environment Variables:
205
236
  DG_API_KEY API key
206
237
  DG_API_URL API base URL
207
238
  DG_MODE Mode (block/warn/off)
208
239
  DG_ALLOWLIST Comma-separated allowlist
240
+ DG_DEBUG Enable debug output (set to 1)
209
241
 
210
242
  Exit Codes:
211
243
  0 pass — No risks detected
212
244
  1 warn — Risks detected (advisory)
213
245
  2 block — High-risk packages detected
246
+ 3 error — Internal error (API failure, config error)
214
247
 
215
248
  Examples:
216
249
  DG_API_KEY=dg_live_xxx dg scan
217
250
  dg scan --api-key dg_live_xxx --json
218
251
  dg scan --scan-all --mode block
219
252
  dg scan --base-lockfile ./main-lockfile.json
253
+ dg npm install express lodash
254
+ dg npm install @scope/pkg@^2.0.0
255
+ dg npm install risky-pkg --dg-force
220
256
  `.trimStart();
221
257
  exports.USAGE = USAGE;
222
258
  function getVersion() {
@@ -242,6 +278,8 @@ function parseConfig(argv) {
242
278
  json: { type: "boolean", default: false },
243
279
  "scan-all": { type: "boolean", default: false },
244
280
  "base-lockfile": { type: "string" },
281
+ debug: { type: "boolean", default: false },
282
+ "no-config": { type: "boolean", default: false },
245
283
  help: { type: "boolean", default: false },
246
284
  version: { type: "boolean", default: false },
247
285
  },
@@ -257,11 +295,14 @@ function parseConfig(argv) {
257
295
  process.exit(0);
258
296
  }
259
297
  const command = positionals[0] ?? "scan";
298
+ const noConfig = values["no-config"];
299
+ const dgrc = noConfig ? {} : loadDgrc();
260
300
  if (values["api-key"]) {
261
301
  process.stderr.write("Warning: --api-key is deprecated (visible in process list). Use DG_API_KEY env var instead.\n");
262
302
  }
263
303
  const apiKey = values["api-key"] ??
264
304
  process.env.DG_API_KEY ??
305
+ dgrc.apiKey ??
265
306
  "";
266
307
  if (!apiKey) {
267
308
  process.stderr.write("Error: API key required. Set DG_API_KEY environment variable.\n" +
@@ -271,6 +312,7 @@ function parseConfig(argv) {
271
312
  }
272
313
  const modeRaw = values.mode ??
273
314
  process.env.DG_MODE ??
315
+ dgrc.mode ??
274
316
  "warn";
275
317
  if (!["block", "warn", "off"].includes(modeRaw)) {
276
318
  process.stderr.write(`Error: Invalid mode "${modeRaw}". Must be block, warn, or off.\n`);
@@ -279,23 +321,40 @@ function parseConfig(argv) {
279
321
  const allowlistRaw = values.allowlist ??
280
322
  process.env.DG_ALLOWLIST ??
281
323
  "";
324
+ const blockThreshold = Number(values["block-threshold"] ?? dgrc.blockThreshold ?? "70");
325
+ const warnThreshold = Number(values["warn-threshold"] ?? dgrc.warnThreshold ?? "60");
326
+ const maxPackages = Number(values["max-packages"] ?? dgrc.maxPackages ?? "200");
327
+ const debug = values.debug || process.env.DG_DEBUG === "1";
328
+ if (isNaN(blockThreshold) || blockThreshold < 0 || blockThreshold > 100) {
329
+ process.stderr.write("Error: --block-threshold must be a number between 0 and 100\n");
330
+ process.exit(1);
331
+ }
332
+ if (isNaN(warnThreshold) || warnThreshold < 0 || warnThreshold > 100) {
333
+ process.stderr.write("Error: --warn-threshold must be a number between 0 and 100\n");
334
+ process.exit(1);
335
+ }
336
+ if (isNaN(maxPackages) || maxPackages < 1 || maxPackages > 10000) {
337
+ process.stderr.write("Error: --max-packages must be a number between 1 and 10000\n");
338
+ process.exit(1);
339
+ }
282
340
  return {
283
341
  apiKey,
284
342
  apiUrl: values["api-url"] ??
285
343
  process.env.DG_API_URL ??
344
+ dgrc.apiUrl ??
286
345
  "https://api.westbayberry.com",
287
346
  mode: modeRaw,
288
- blockThreshold: Number(values["block-threshold"] ?? "70"),
289
- warnThreshold: Number(values["warn-threshold"] ?? "60"),
290
- maxPackages: Number(values["max-packages"] ?? "200"),
347
+ blockThreshold,
348
+ warnThreshold,
349
+ maxPackages,
291
350
  allowlist: allowlistRaw
292
- .split(",")
293
- .map((s) => s.trim())
294
- .filter(Boolean),
351
+ ? allowlistRaw.split(",").map((s) => s.trim()).filter(Boolean)
352
+ : (dgrc.allowlist ?? []),
295
353
  json: values.json,
296
354
  scanAll: values["scan-all"],
297
355
  baseLockfile: values["base-lockfile"] ?? null,
298
356
  command,
357
+ debug,
299
358
  };
300
359
  }
301
360
 
@@ -473,6 +532,273 @@ function toPackageInput(change) {
473
532
  }
474
533
 
475
534
 
535
+ /***/ }),
536
+
537
+ /***/ 124:
538
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
539
+
540
+
541
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
542
+ exports.parseNpmArgs = parseNpmArgs;
543
+ exports.parsePackageSpec = parsePackageSpec;
544
+ exports.resolveVersion = resolveVersion;
545
+ exports.resolvePackages = resolvePackages;
546
+ exports.runNpm = runNpm;
547
+ exports.handleNpmCommand = handleNpmCommand;
548
+ exports.handleWrapCommand = handleWrapCommand;
549
+ const node_child_process_1 = __nccwpck_require__(421);
550
+ const color_1 = __nccwpck_require__(988);
551
+ const api_1 = __nccwpck_require__(879);
552
+ const output_1 = __nccwpck_require__(202);
553
+ /** npm commands that install packages and should trigger a scan */
554
+ const INSTALL_COMMANDS = new Set(["install", "i", "add", "update", "up"]);
555
+ /**
556
+ * Parse the argv after `dg npm ...` to extract the npm command and package specifiers.
557
+ */
558
+ function parseNpmArgs(args) {
559
+ let dgForce = false;
560
+ const filtered = [];
561
+ // Extract dg-specific flags before passing to npm
562
+ for (const arg of args) {
563
+ if (arg === "--dg-force") {
564
+ dgForce = true;
565
+ }
566
+ else {
567
+ filtered.push(arg);
568
+ }
569
+ }
570
+ const command = filtered[0] ?? "";
571
+ const shouldScan = INSTALL_COMMANDS.has(command);
572
+ // Extract package specifiers: anything that's not a flag and not the command itself
573
+ const packages = [];
574
+ if (shouldScan) {
575
+ for (let i = 1; i < filtered.length; i++) {
576
+ const arg = filtered[i];
577
+ // Skip flags and their values
578
+ if (arg.startsWith("-")) {
579
+ // Flags that take a value: skip next arg too
580
+ if (flagTakesValue(arg)) {
581
+ i++;
582
+ }
583
+ continue;
584
+ }
585
+ packages.push(arg);
586
+ }
587
+ }
588
+ return {
589
+ command,
590
+ packages,
591
+ rawArgs: filtered,
592
+ dgForce,
593
+ shouldScan,
594
+ };
595
+ }
596
+ /** npm flags that consume the next argument as a value */
597
+ function flagTakesValue(flag) {
598
+ const valueFlagPrefixes = [
599
+ "--save-prefix",
600
+ "--tag",
601
+ "--registry",
602
+ "--cache",
603
+ "--prefix",
604
+ "--fund",
605
+ "--omit",
606
+ "--install-strategy",
607
+ "--workspace",
608
+ ];
609
+ // Short flags that take values
610
+ if (flag === "-w")
611
+ return true;
612
+ for (const prefix of valueFlagPrefixes) {
613
+ if (flag === prefix)
614
+ return true;
615
+ }
616
+ // --flag=value style doesn't consume next arg
617
+ return false;
618
+ }
619
+ /**
620
+ * Parse a package specifier like "express", "@scope/pkg@^2.0.0", "pkg@latest"
621
+ * into { name, versionSpec }.
622
+ */
623
+ function parsePackageSpec(spec) {
624
+ // Scoped: @scope/pkg@version
625
+ if (spec.startsWith("@")) {
626
+ const slashIdx = spec.indexOf("/");
627
+ if (slashIdx === -1) {
628
+ return { name: spec, versionSpec: null };
629
+ }
630
+ const afterSlash = spec.slice(slashIdx + 1);
631
+ const atIdx = afterSlash.indexOf("@");
632
+ if (atIdx === -1) {
633
+ return { name: spec, versionSpec: null };
634
+ }
635
+ return {
636
+ name: spec.slice(0, slashIdx + 1 + atIdx),
637
+ versionSpec: afterSlash.slice(atIdx + 1),
638
+ };
639
+ }
640
+ // Unscoped: pkg@version
641
+ const atIdx = spec.indexOf("@");
642
+ if (atIdx === -1 || atIdx === 0) {
643
+ return { name: spec, versionSpec: null };
644
+ }
645
+ return {
646
+ name: spec.slice(0, atIdx),
647
+ versionSpec: spec.slice(atIdx + 1),
648
+ };
649
+ }
650
+ /**
651
+ * Resolve what version npm would install for a given package specifier.
652
+ * Uses `npm view <spec> version` to get the resolved version.
653
+ */
654
+ function resolveVersion(spec) {
655
+ try {
656
+ const version = (0, node_child_process_1.execSync)(`npm view "${spec}" version`, {
657
+ encoding: "utf-8",
658
+ stdio: ["pipe", "pipe", "pipe"],
659
+ timeout: 15000,
660
+ }).trim();
661
+ return version || null;
662
+ }
663
+ catch {
664
+ return null;
665
+ }
666
+ }
667
+ /**
668
+ * Build PackageInput[] from package specifiers by resolving versions.
669
+ */
670
+ function resolvePackages(specs) {
671
+ const resolved = [];
672
+ const failed = [];
673
+ for (const spec of specs) {
674
+ const { name, versionSpec } = parsePackageSpec(spec);
675
+ const querySpec = versionSpec ? `${name}@${versionSpec}` : name;
676
+ const version = resolveVersion(querySpec);
677
+ if (version) {
678
+ resolved.push({
679
+ name,
680
+ version,
681
+ previousVersion: null,
682
+ isNew: true,
683
+ });
684
+ }
685
+ else {
686
+ failed.push(spec);
687
+ }
688
+ }
689
+ return { resolved, failed };
690
+ }
691
+ /**
692
+ * Run the actual npm command, inheriting stdio.
693
+ * Returns the npm exit code.
694
+ */
695
+ function runNpm(args) {
696
+ return new Promise((resolve) => {
697
+ const child = (0, node_child_process_1.spawn)("npm", args, {
698
+ stdio: "inherit",
699
+ shell: false,
700
+ });
701
+ child.on("close", (code) => resolve(code ?? 1));
702
+ child.on("error", () => resolve(1));
703
+ });
704
+ }
705
+ /**
706
+ * Main npm wrapper entry point.
707
+ */
708
+ async function handleNpmCommand(npmArgs, config) {
709
+ const parsed = parseNpmArgs(npmArgs);
710
+ // Non-install commands: pass through directly
711
+ if (!parsed.shouldScan) {
712
+ const code = await runNpm(parsed.rawArgs);
713
+ process.exit(code);
714
+ }
715
+ // No packages specified (e.g. bare `npm install` from package.json)
716
+ if (parsed.packages.length === 0) {
717
+ process.stderr.write((0, color_1.dim)(" Dependency Guardian: no new packages specified, passing through to npm.\n"));
718
+ const code = await runNpm(parsed.rawArgs);
719
+ process.exit(code);
720
+ }
721
+ // Resolve versions
722
+ process.stderr.write((0, color_1.dim)(` Resolving ${parsed.packages.length} package${parsed.packages.length !== 1 ? "s" : ""}...\n`));
723
+ const { resolved, failed } = resolvePackages(parsed.packages);
724
+ if (failed.length > 0) {
725
+ process.stderr.write((0, color_1.yellow)(` Warning: Could not resolve versions for: ${failed.join(", ")}\n`));
726
+ }
727
+ if (resolved.length === 0) {
728
+ process.stderr.write((0, color_1.dim)(" No packages to scan. Passing through to npm.\n"));
729
+ const code = await runNpm(parsed.rawArgs);
730
+ process.exit(code);
731
+ }
732
+ // Filter allowlist
733
+ const toScan = resolved.filter((p) => !config.allowlist.includes(p.name));
734
+ if (toScan.length === 0) {
735
+ process.stderr.write((0, color_1.dim)(" All packages are allowlisted. Passing through to npm.\n"));
736
+ const code = await runNpm(parsed.rawArgs);
737
+ process.exit(code);
738
+ }
739
+ // Scan
740
+ process.stderr.write((0, color_1.dim)(` Scanning ${toScan.length} package${toScan.length !== 1 ? "s" : ""}...\n`));
741
+ let result;
742
+ try {
743
+ const startMs = Date.now();
744
+ result = await (0, api_1.callAnalyzeAPI)(toScan, config);
745
+ const elapsed = Date.now() - startMs;
746
+ if (config.debug) {
747
+ process.stderr.write((0, color_1.dim)(` [debug] API responded in ${elapsed}ms, action=${result.action}, score=${result.score}\n`));
748
+ }
749
+ }
750
+ catch (error) {
751
+ // API unavailable — warn and proceed
752
+ const msg = error instanceof Error ? error.message : String(error);
753
+ process.stderr.write((0, color_1.yellow)(` Warning: Scan failed (${msg}). Proceeding with install.\n`));
754
+ const code = await runNpm(parsed.rawArgs);
755
+ process.exit(code);
756
+ return; // unreachable, but helps TypeScript
757
+ }
758
+ // Handle result
759
+ if (result.action === "pass") {
760
+ process.stderr.write((0, color_1.green)(` ${(0, color_1.bold)("\u2713")} ${toScan.length} package${toScan.length !== 1 ? "s" : ""} scanned \u2014 all clear\n\n`));
761
+ const code = await runNpm(parsed.rawArgs);
762
+ process.exit(code);
763
+ }
764
+ // Render findings
765
+ const output = (0, output_1.renderResult)(result, config);
766
+ process.stdout.write(output + "\n");
767
+ if (result.action === "warn") {
768
+ process.stderr.write((0, color_1.yellow)(" Warnings detected. Proceeding with install.\n\n"));
769
+ const code = await runNpm(parsed.rawArgs);
770
+ process.exit(code);
771
+ }
772
+ // Block
773
+ if (result.action === "block") {
774
+ if (parsed.dgForce) {
775
+ process.stderr.write((0, color_1.yellow)((0, color_1.bold)(" --dg-force: Bypassing block. Install at your own risk.\n\n")));
776
+ const code = await runNpm(parsed.rawArgs);
777
+ process.exit(code);
778
+ }
779
+ process.stderr.write((0, color_1.red)((0, color_1.bold)(" BLOCKED: ")) +
780
+ (0, color_1.red)("High-risk packages detected. Install aborted.\n"));
781
+ process.stderr.write((0, color_1.dim)(" Use --dg-force to bypass this check.\n\n"));
782
+ process.exit(2);
783
+ }
784
+ }
785
+ const WRAP_USAGE = `
786
+ Set up dg as your npm wrapper:
787
+
788
+ Option 1 — Shell alias (recommended):
789
+ Add to your ~/.zshrc or ~/.bashrc:
790
+ alias npm='dg npm'
791
+
792
+ Option 2 — Per-project .npmrc:
793
+ Not yet supported.
794
+
795
+ Once set up, every \`npm install\` will be scanned automatically.
796
+ `.trimStart();
797
+ function handleWrapCommand() {
798
+ process.stdout.write(WRAP_USAGE);
799
+ }
800
+
801
+
476
802
  /***/ }),
477
803
 
478
804
  /***/ 202:
@@ -535,9 +861,10 @@ function renderResult(result, config) {
535
861
  lines.push("");
536
862
  return lines.join("\n");
537
863
  }
538
- // Package table
539
- const COL_NAME = 22;
540
- const COL_VER = 22;
864
+ // Package table — adapt to terminal width
865
+ const termWidth = process.stdout.columns || 80;
866
+ const COL_NAME = Math.max(16, Math.min(40, Math.floor(termWidth * 0.3)));
867
+ const COL_VER = Math.max(14, Math.min(30, Math.floor(termWidth * 0.25)));
541
868
  const COL_SCORE = 7;
542
869
  lines.push(` ${(0, color_1.dim)(pad("Package", COL_NAME))}${(0, color_1.dim)(pad("Version", COL_VER))}${(0, color_1.dim)(pad("Score", COL_SCORE))}${(0, color_1.dim)("Action")}`);
543
870
  lines.push(` ${(0, color_1.dim)(pad("\u2500".repeat(COL_NAME - 2), COL_NAME))}${(0, color_1.dim)(pad("\u2500".repeat(COL_VER - 2), COL_VER))}${(0, color_1.dim)(pad("\u2500".repeat(COL_SCORE - 2), COL_SCORE))}${(0, color_1.dim)("\u2500".repeat(6))}`);
@@ -718,7 +1045,7 @@ function parseLockfile(content) {
718
1045
  if (path === "")
719
1046
  continue;
720
1047
  const name = extractPackageName(path);
721
- if (name) {
1048
+ if (name && !packages.has(name)) {
722
1049
  const e = entry;
723
1050
  packages.set(name, {
724
1051
  version: e.version ?? "",
@@ -775,6 +1102,13 @@ module.exports = require("node:fs");
775
1102
 
776
1103
  /***/ }),
777
1104
 
1105
+ /***/ 161:
1106
+ /***/ ((module) => {
1107
+
1108
+ module.exports = require("node:os");
1109
+
1110
+ /***/ }),
1111
+
778
1112
  /***/ 760:
779
1113
  /***/ ((module) => {
780
1114
 
@@ -838,15 +1172,35 @@ const lockfile_1 = __nccwpck_require__(746);
838
1172
  const api_1 = __nccwpck_require__(879);
839
1173
  const output_1 = __nccwpck_require__(202);
840
1174
  const color_1 = __nccwpck_require__(988);
1175
+ const npm_wrapper_1 = __nccwpck_require__(124);
841
1176
  async function main() {
1177
+ // Check for `dg npm ...` or `dg wrap` before full config parsing
1178
+ const rawCommand = process.argv[2];
1179
+ if (rawCommand === "npm") {
1180
+ // Parse config but don't require lockfile discovery
1181
+ const config = (0, config_1.parseConfig)(process.argv);
1182
+ await (0, npm_wrapper_1.handleNpmCommand)(process.argv.slice(3), config);
1183
+ return;
1184
+ }
1185
+ if (rawCommand === "wrap") {
1186
+ (0, npm_wrapper_1.handleWrapCommand)();
1187
+ return;
1188
+ }
842
1189
  const config = (0, config_1.parseConfig)(process.argv);
1190
+ const dbg = (msg) => {
1191
+ if (config.debug)
1192
+ process.stderr.write((0, color_1.dim)(` [debug] ${msg}\n`));
1193
+ };
843
1194
  if (config.mode === "off") {
844
1195
  process.stderr.write((0, color_1.dim)(" Dependency Guardian: mode is off — skipping.\n"));
845
1196
  process.exit(0);
846
1197
  }
1198
+ dbg(`mode=${config.mode} block=${config.blockThreshold} warn=${config.warnThreshold} max=${config.maxPackages}`);
1199
+ dbg(`api=${config.apiUrl}`);
847
1200
  // Discover packages
848
1201
  process.stderr.write((0, color_1.dim)(" Discovering package changes...\n"));
849
1202
  const discovery = (0, lockfile_1.discoverChanges)(process.cwd(), config);
1203
+ dbg(`discovery method: ${discovery.method}`);
850
1204
  if (discovery.packages.length === 0) {
851
1205
  process.stderr.write((0, color_1.dim)(" No package changes detected.\n"));
852
1206
  process.exit(0);
@@ -861,10 +1215,13 @@ async function main() {
861
1215
  if (discovery.skipped.length > 0) {
862
1216
  process.stderr.write((0, color_1.yellow)(` Warning: ${discovery.skipped.length} package(s) skipped (max-packages=${config.maxPackages})\n`));
863
1217
  }
1218
+ dbg(`packages to scan: ${packages.map(p => `${p.name}@${p.version}`).slice(0, 10).join(", ")}${packages.length > 10 ? ` (+${packages.length - 10} more)` : ""}`);
864
1219
  // Call Detection API
1220
+ const startMs = Date.now();
865
1221
  const result = await (0, api_1.callAnalyzeAPI)(packages, config, (done, total) => {
866
1222
  process.stderr.write((0, color_1.dim)(` Analyzed ${done}/${total}...\n`));
867
1223
  });
1224
+ dbg(`API responded in ${Date.now() - startMs}ms, action=${result.action}, score=${result.score}`);
868
1225
  // Render output
869
1226
  const output = (0, output_1.renderResult)(result, config);
870
1227
  process.stdout.write(output + "\n");
@@ -879,7 +1236,7 @@ async function main() {
879
1236
  }
880
1237
  main().catch((err) => {
881
1238
  process.stderr.write(`\n ${(0, color_1.bold)((0, color_1.red)("Error:"))} ${err.message}\n\n`);
882
- process.exit(1);
1239
+ process.exit(3); // Exit 3 = internal error (distinct from exit 1 = warnings)
883
1240
  });
884
1241
 
885
1242
  })();