@westbayberry/dg 1.0.52 → 1.0.56

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 (64) hide show
  1. package/README.md +5 -1
  2. package/dist/index.mjs +349 -168
  3. package/dist/packages/cli/src/alt-screen.js +36 -0
  4. package/dist/packages/cli/src/api.js +322 -0
  5. package/dist/packages/cli/src/auth.js +218 -0
  6. package/dist/packages/cli/src/bin.js +386 -0
  7. package/dist/packages/cli/src/config.js +228 -0
  8. package/dist/packages/cli/src/discover.js +126 -0
  9. package/dist/packages/cli/src/first-run.js +135 -0
  10. package/dist/packages/cli/src/hook.js +360 -0
  11. package/dist/packages/cli/src/lockfile.js +303 -0
  12. package/dist/packages/cli/src/npm-wrapper.js +218 -0
  13. package/dist/packages/cli/src/pip-wrapper.js +273 -0
  14. package/dist/packages/cli/src/sanitize.js +38 -0
  15. package/dist/packages/cli/src/scan-core.js +144 -0
  16. package/dist/packages/cli/src/setup-status.js +46 -0
  17. package/dist/packages/cli/src/static-output.js +625 -0
  18. package/dist/packages/cli/src/telemetry.js +141 -0
  19. package/dist/packages/cli/src/ui/App.js +137 -0
  20. package/dist/packages/cli/src/ui/InitApp.js +391 -0
  21. package/dist/packages/cli/src/ui/LoginApp.js +51 -0
  22. package/dist/packages/cli/src/ui/NpmWrapperApp.js +73 -0
  23. package/dist/packages/cli/src/ui/PipWrapperApp.js +72 -0
  24. package/dist/packages/cli/src/ui/components/ConfirmPrompt.js +24 -0
  25. package/dist/packages/cli/src/ui/components/DemoScanAnimation.js +26 -0
  26. package/dist/packages/cli/src/ui/components/DurationLine.js +7 -0
  27. package/dist/packages/cli/src/ui/components/ErrorView.js +30 -0
  28. package/dist/packages/cli/src/ui/components/FileSavePrompt.js +210 -0
  29. package/dist/packages/cli/src/ui/components/InteractiveResultsView.js +557 -0
  30. package/dist/packages/cli/src/ui/components/Mascot.js +33 -0
  31. package/dist/packages/cli/src/ui/components/ProgressBar.js +51 -0
  32. package/dist/packages/cli/src/ui/components/ProgressDots.js +35 -0
  33. package/dist/packages/cli/src/ui/components/ProjectSelector.js +60 -0
  34. package/dist/packages/cli/src/ui/components/ResultsView.js +105 -0
  35. package/dist/packages/cli/src/ui/components/ScanResultCard.js +54 -0
  36. package/dist/packages/cli/src/ui/components/ScoreHeader.js +142 -0
  37. package/dist/packages/cli/src/ui/components/SetupBanner.js +17 -0
  38. package/dist/packages/cli/src/ui/components/Spinner.js +11 -0
  39. package/dist/packages/cli/src/ui/hooks/useExpandAnimation.js +44 -0
  40. package/dist/packages/cli/src/ui/hooks/useInit.js +341 -0
  41. package/dist/packages/cli/src/ui/hooks/useLogin.js +121 -0
  42. package/dist/packages/cli/src/ui/hooks/useNpmWrapper.js +192 -0
  43. package/dist/packages/cli/src/ui/hooks/usePipWrapper.js +195 -0
  44. package/dist/packages/cli/src/ui/hooks/useScan.js +202 -0
  45. package/dist/packages/cli/src/ui/hooks/useTerminalSize.js +29 -0
  46. package/dist/packages/cli/src/update-check.js +152 -0
  47. package/dist/packages/cli/src/wizard-demo-data.js +63 -0
  48. package/dist/src/ecosystem.js +2 -0
  49. package/dist/src/lockfile/diff.js +38 -0
  50. package/dist/src/lockfile/parse_package_json.js +41 -0
  51. package/dist/src/lockfile/parse_package_lock.js +55 -0
  52. package/dist/src/lockfile/parse_pipfile_lock.js +69 -0
  53. package/dist/src/lockfile/parse_pnpm_lock.js +62 -0
  54. package/dist/src/lockfile/parse_poetry_lock.js +71 -0
  55. package/dist/src/lockfile/parse_requirements.js +83 -0
  56. package/dist/src/lockfile/parse_yarn_lock.js +66 -0
  57. package/dist/src/logger.js +21 -0
  58. package/dist/src/npm/h2pool.js +161 -0
  59. package/dist/src/npm/registry.js +299 -0
  60. package/dist/src/npm/tarball.js +274 -0
  61. package/dist/src/pypi/registry.js +299 -0
  62. package/dist/src/pypi/tarball.js +361 -0
  63. package/dist/src/types.js +2 -0
  64. package/package.json +6 -3
package/dist/index.mjs CHANGED
@@ -19192,9 +19192,9 @@ function handleContentBlockStart(event, state) {
19192
19192
  function handleContentBlockDelta(event, state, recordOutputs) {
19193
19193
  if (event.type !== "content_block_delta" || !event.delta) return;
19194
19194
  if (typeof event.index === "number" && "partial_json" in event.delta && typeof event.delta.partial_json === "string") {
19195
- const active = state.activeToolBlocks[event.index];
19196
- if (active) {
19197
- active.inputJsonParts.push(event.delta.partial_json);
19195
+ const active2 = state.activeToolBlocks[event.index];
19196
+ if (active2) {
19197
+ active2.inputJsonParts.push(event.delta.partial_json);
19198
19198
  }
19199
19199
  }
19200
19200
  if (recordOutputs && typeof event.delta.text === "string") {
@@ -19203,9 +19203,9 @@ function handleContentBlockDelta(event, state, recordOutputs) {
19203
19203
  }
19204
19204
  function handleContentBlockStop(event, state) {
19205
19205
  if (event.type !== "content_block_stop" || typeof event.index !== "number") return;
19206
- const active = state.activeToolBlocks[event.index];
19207
- if (!active) return;
19208
- const raw = active.inputJsonParts.join("");
19206
+ const active2 = state.activeToolBlocks[event.index];
19207
+ if (!active2) return;
19208
+ const raw = active2.inputJsonParts.join("");
19209
19209
  let parsedInput;
19210
19210
  try {
19211
19211
  parsedInput = raw ? JSON.parse(raw) : {};
@@ -19214,8 +19214,8 @@ function handleContentBlockStop(event, state) {
19214
19214
  }
19215
19215
  state.toolCalls.push({
19216
19216
  type: "tool_use",
19217
- id: active.id,
19218
- name: active.name,
19217
+ id: active2.id,
19218
+ name: active2.name,
19219
19219
  input: parsedInput
19220
19220
  });
19221
19221
  delete state.activeToolBlocks[event.index];
@@ -46241,9 +46241,10 @@ __export(auth_exports, {
46241
46241
  maskKey: () => maskKey,
46242
46242
  openBrowser: () => openBrowser,
46243
46243
  pollAuthSession: () => pollAuthSession,
46244
- saveCredentials: () => saveCredentials
46244
+ saveCredentials: () => saveCredentials,
46245
+ scrubAuthToken: () => scrubAuthToken
46245
46246
  });
46246
- import { readFileSync as readFileSync2, writeFileSync, unlinkSync, existsSync as existsSync2, chmodSync } from "node:fs";
46247
+ import { readFileSync as readFileSync2, writeFileSync, unlinkSync, existsSync as existsSync2, chmodSync, lstatSync, openSync, fchmodSync, closeSync, constants as fsConstants } from "node:fs";
46247
46248
  import { join as join4 } from "node:path";
46248
46249
  import { homedir } from "node:os";
46249
46250
  import { spawn } from "node:child_process";
@@ -46296,7 +46297,47 @@ async function pollAuthSession(sessionId) {
46296
46297
  function configPath() {
46297
46298
  return join4(homedir(), CONFIG_FILE);
46298
46299
  }
46300
+ function ensureConfigPerms(path2) {
46301
+ let st;
46302
+ try {
46303
+ st = lstatSync(path2);
46304
+ } catch {
46305
+ return;
46306
+ }
46307
+ if (!st || typeof st.isSymbolicLink !== "function") return;
46308
+ if (st.isSymbolicLink()) {
46309
+ process.stderr.write(
46310
+ `Warning: ${path2} is a symlink; refusing to chmod (would affect the symlink target). Replace with a regular file to enforce 0o600.
46311
+ `
46312
+ );
46313
+ return;
46314
+ }
46315
+ if (typeof st.isFile === "function" && !st.isFile()) return;
46316
+ const mode = (st.mode ?? 384) & 511;
46317
+ if (mode === 384) return;
46318
+ process.stderr.write(
46319
+ `Warning: ${path2} has perms 0o${mode.toString(8)} (expected 0o600); re-tightening.
46320
+ `
46321
+ );
46322
+ let fd;
46323
+ try {
46324
+ fd = openSync(path2, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
46325
+ fchmodSync(fd, 384);
46326
+ } catch {
46327
+ } finally {
46328
+ if (fd !== void 0) {
46329
+ try {
46330
+ closeSync(fd);
46331
+ } catch {
46332
+ }
46333
+ }
46334
+ }
46335
+ }
46336
+ function scrubAuthToken(s) {
46337
+ return s.replace(/\bdgk_[A-Za-z0-9]{16,}\b/g, "dgk_<REDACTED>").replace(/\bBearer\s+[A-Za-z0-9_.-]{24,}\b/g, "Bearer <REDACTED>");
46338
+ }
46299
46339
  function readConfig() {
46340
+ ensureConfigPerms(configPath());
46300
46341
  let raw;
46301
46342
  try {
46302
46343
  raw = readFileSync2(configPath(), "utf-8");
@@ -46418,20 +46459,65 @@ var config_exports = {};
46418
46459
  __export(config_exports, {
46419
46460
  USAGE: () => USAGE,
46420
46461
  getVersion: () => getVersion,
46421
- parseConfig: () => parseConfig
46462
+ parseConfig: () => parseConfig,
46463
+ warnUnknownDgrcKeys: () => warnUnknownDgrcKeys
46422
46464
  });
46423
46465
  import { parseArgs } from "node:util";
46424
46466
  import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
46425
46467
  import { join as join5, dirname as dirname3 } from "node:path";
46426
46468
  import { fileURLToPath } from "node:url";
46427
46469
  import { homedir as homedir2 } from "node:os";
46470
+ function levenshtein(a, b) {
46471
+ if (a === b) return 0;
46472
+ if (a.length === 0) return b.length;
46473
+ if (b.length === 0) return a.length;
46474
+ const m = a.length;
46475
+ const n = b.length;
46476
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
46477
+ let curr = new Array(n + 1);
46478
+ for (let i = 1; i <= m; i++) {
46479
+ curr[0] = i;
46480
+ for (let j = 1; j <= n; j++) {
46481
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
46482
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
46483
+ }
46484
+ [prev, curr] = [curr, prev];
46485
+ }
46486
+ return prev[n];
46487
+ }
46488
+ function warnUnknownDgrcKeys(parsed, source) {
46489
+ for (const key of Object.keys(parsed)) {
46490
+ if (KNOWN_DGRC_KEYS.includes(key)) continue;
46491
+ if (INTERNAL_DGRC_KEYS.includes(key)) continue;
46492
+ let bestKey = null;
46493
+ let bestDistance = Infinity;
46494
+ for (const known of KNOWN_DGRC_KEYS) {
46495
+ const d = levenshtein(key, known);
46496
+ if (d < bestDistance) {
46497
+ bestDistance = d;
46498
+ bestKey = known;
46499
+ }
46500
+ }
46501
+ const suggestion = bestKey && bestDistance <= 2 ? ` (did you mean "${bestKey}"?)` : ` (valid keys: ${KNOWN_DGRC_KEYS.join(", ")})`;
46502
+ process.stderr.write(
46503
+ `Warning: unknown key "${key}" in ${source}${suggestion}; ignored.
46504
+ `
46505
+ );
46506
+ }
46507
+ }
46428
46508
  function loadDgrc() {
46429
46509
  const cwdPath = join5(process.cwd(), ".dgrc.json");
46430
46510
  const homePath = join5(homedir2(), ".dgrc.json");
46431
46511
  let config3 = {};
46432
46512
  if (existsSync3(homePath)) {
46433
46513
  try {
46434
- config3 = JSON.parse(readFileSync3(homePath, "utf-8"));
46514
+ const home = JSON.parse(readFileSync3(homePath, "utf-8"));
46515
+ warnUnknownDgrcKeys(home, homePath);
46516
+ const homeFiltered = {};
46517
+ for (const k of KNOWN_DGRC_KEYS) {
46518
+ if (k in home) homeFiltered[k] = home[k];
46519
+ }
46520
+ config3 = homeFiltered;
46435
46521
  } catch {
46436
46522
  process.stderr.write(`Warning: Failed to parse ${homePath}, ignoring.
46437
46523
  `);
@@ -46440,8 +46526,11 @@ function loadDgrc() {
46440
46526
  if (existsSync3(cwdPath) && cwdPath !== homePath) {
46441
46527
  try {
46442
46528
  const cwd2 = JSON.parse(readFileSync3(cwdPath, "utf-8"));
46443
- const { apiKey: _k, apiUrl: _u, ...safeKeys } = cwd2;
46444
- Object.assign(config3, safeKeys);
46529
+ warnUnknownDgrcKeys(cwd2, cwdPath);
46530
+ for (const k of KNOWN_DGRC_KEYS) {
46531
+ if (k === "apiKey" || k === "apiUrl") continue;
46532
+ if (k in cwd2) config3[k] = cwd2[k];
46533
+ }
46445
46534
  } catch {
46446
46535
  process.stderr.write(`Warning: Failed to parse ${cwdPath}, ignoring.
46447
46536
  `);
@@ -46454,6 +46543,14 @@ function validateApiUrl(url) {
46454
46543
  const parsed = new URL(url);
46455
46544
  const rawHost = parsed.hostname;
46456
46545
  const host = rawHost.startsWith("[") && rawHost.endsWith("]") ? rawHost.slice(1, -1) : rawHost;
46546
+ const isLinkLocal = /^169\.254\./.test(host) || /^fe[89ab][0-9a-f]:/i.test(host);
46547
+ if (isLinkLocal) {
46548
+ process.stderr.write(
46549
+ `Error: API URL host ${host} is link-local / metadata; refusing (SSRF defense). If you genuinely need this, set DG_API_URL to the explicit hostname of your test target.
46550
+ `
46551
+ );
46552
+ process.exit(1);
46553
+ }
46457
46554
  const isLocal = host === "localhost" || // IPv4: loopback, RFC 1918, CGNAT. Proper octet regex — not prefix matching,
46458
46555
  // which would wrongly accept `192.1680.0.1` or `100.1.2.3` (public).
46459
46556
  /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(host) || // IPv6: loopback and unique local (fc00::/7 → fc** or fd**)
@@ -46492,7 +46589,8 @@ function parseConfig(argv, strictFlags = true) {
46492
46589
  mode: { type: "string" },
46493
46590
  "max-packages": { type: "string" },
46494
46591
  json: { type: "boolean", default: false },
46495
- "scan-all": { type: "boolean", default: false },
46592
+ "scan-all": { type: "boolean", default: true },
46593
+ "changed-only": { type: "boolean", default: false },
46496
46594
  "base-lockfile": { type: "string" },
46497
46595
  workspace: { type: "string", short: "w" },
46498
46596
  output: { type: "string", short: "o" },
@@ -46533,8 +46631,7 @@ function parseConfig(argv, strictFlags = true) {
46533
46631
  }
46534
46632
  const command = positionals[0] ?? "scan";
46535
46633
  const dgrc = loadDgrc();
46536
- const apiKey = dgrc.apiKey && typeof dgrc.apiKey === "string" && dgrc.apiKey.startsWith("dg_live_") ? dgrc.apiKey : null;
46537
- const deviceId = getOrCreateDeviceId();
46634
+ const apiKey = dgrc.apiKey && typeof dgrc.apiKey === "string" && (dgrc.apiKey.startsWith("dg_live_") || dgrc.apiKey.startsWith("dg_test_")) ? dgrc.apiKey : null;
46538
46635
  const modeRaw = values.mode ?? process.env.DG_MODE ?? dgrc.mode ?? "warn";
46539
46636
  if (!["block", "warn", "off"].includes(modeRaw)) {
46540
46637
  process.stderr.write(
@@ -46549,16 +46646,18 @@ function parseConfig(argv, strictFlags = true) {
46549
46646
  process.stderr.write("Error: --max-packages must be a number between 1 and 10000\n");
46550
46647
  process.exit(1);
46551
46648
  }
46649
+ const apiUrl = validateApiUrl(
46650
+ values["api-url"] ?? process.env.DG_API_URL ?? dgrc.apiUrl ?? "https://api.westbayberry.com"
46651
+ );
46652
+ const deviceId = getOrCreateDeviceId();
46552
46653
  return {
46553
46654
  apiKey,
46554
46655
  deviceId,
46555
- apiUrl: validateApiUrl(
46556
- values["api-url"] ?? process.env.DG_API_URL ?? dgrc.apiUrl ?? "https://api.westbayberry.com"
46557
- ),
46656
+ apiUrl,
46558
46657
  mode: modeRaw,
46559
46658
  maxPackages,
46560
46659
  json: values.json,
46561
- scanAll: values["scan-all"],
46660
+ scanAll: values["changed-only"] ? false : values["scan-all"],
46562
46661
  baseLockfile: values["base-lockfile"] ?? null,
46563
46662
  workspace: values.workspace ?? process.env.DG_WORKSPACE ?? null,
46564
46663
  outputFile: values.output ?? null,
@@ -46566,11 +46665,13 @@ function parseConfig(argv, strictFlags = true) {
46566
46665
  debug: debug2
46567
46666
  };
46568
46667
  }
46569
- var USAGE;
46668
+ var KNOWN_DGRC_KEYS, INTERNAL_DGRC_KEYS, USAGE;
46570
46669
  var init_config = __esm({
46571
46670
  "src/config.ts"() {
46572
46671
  "use strict";
46573
46672
  init_auth();
46673
+ KNOWN_DGRC_KEYS = ["apiKey", "apiUrl", "mode", "maxPackages"];
46674
+ INTERNAL_DGRC_KEYS = ["deviceId", "firstRunCompletedAt"];
46574
46675
  USAGE = `
46575
46676
  Dependency Guardian \u2014 Supply chain security scanner
46576
46677
 
@@ -46603,7 +46704,8 @@ var init_config = __esm({
46603
46704
  --mode <mode> block | warn | off (default: warn)
46604
46705
  --max-packages <n> Max packages per scan (default: 10000)
46605
46706
  --json Output JSON for CI parsing
46606
- --scan-all Scan all packages, not just changed
46707
+ --scan-all Scan all packages (default)
46708
+ --changed-only Only scan packages changed since base lockfile
46607
46709
  --base-lockfile <path> Path to base lockfile for explicit diff
46608
46710
  --workspace <dir> Scan a specific workspace subdirectory
46609
46711
  --output, -o <file> Save JSON results to file (use with --json)
@@ -46718,7 +46820,13 @@ function parsePackageSpec(spec) {
46718
46820
  versionSpec: spec.slice(atIdx + 1)
46719
46821
  };
46720
46822
  }
46823
+ function isFlagLikeSpec(spec) {
46824
+ return spec.startsWith("-");
46825
+ }
46721
46826
  async function resolveVersion(spec) {
46827
+ if (isFlagLikeSpec(spec)) {
46828
+ return null;
46829
+ }
46722
46830
  return new Promise((resolve2) => {
46723
46831
  const child = spawn2("npm", ["view", spec, "version"], {
46724
46832
  stdio: ["pipe", "pipe", "pipe"]
@@ -48378,6 +48486,34 @@ var init_update_check = __esm({
48378
48486
  }
48379
48487
  });
48380
48488
 
48489
+ // src/alt-screen.ts
48490
+ var alt_screen_exports = {};
48491
+ __export(alt_screen_exports, {
48492
+ altScreenActive: () => altScreenActive,
48493
+ enterAltScreen: () => enterAltScreen,
48494
+ leaveAltScreen: () => leaveAltScreen
48495
+ });
48496
+ function enterAltScreen() {
48497
+ if (!process.stdout.isTTY || active) return;
48498
+ process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
48499
+ active = true;
48500
+ }
48501
+ function leaveAltScreen() {
48502
+ if (!active) return;
48503
+ process.stdout.write("\x1B[?1049l\x1B[?25h");
48504
+ active = false;
48505
+ }
48506
+ function altScreenActive() {
48507
+ return active;
48508
+ }
48509
+ var active;
48510
+ var init_alt_screen = __esm({
48511
+ "src/alt-screen.ts"() {
48512
+ "use strict";
48513
+ active = false;
48514
+ }
48515
+ });
48516
+
48381
48517
  // node_modules/react/cjs/react.production.min.js
48382
48518
  var require_react_production_min = __commonJS({
48383
48519
  "node_modules/react/cjs/react.production.min.js"(exports) {
@@ -82374,18 +82510,26 @@ var discover_exports = {};
82374
82510
  __export(discover_exports, {
82375
82511
  discoverProjects: () => discoverProjects
82376
82512
  });
82377
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync, lstatSync } from "node:fs";
82513
+ import { readFileSync as readFileSync7, readdirSync, lstatSync as lstatSync2 } from "node:fs";
82378
82514
  import { join as join8, relative as relative2, basename as basename2 } from "node:path";
82379
82515
  function discoverProjects(root) {
82380
82516
  const projects = [];
82381
82517
  walk(root, root, 0, projects);
82382
82518
  return projects.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
82383
82519
  }
82520
+ function isSafeRegularFile(path2) {
82521
+ try {
82522
+ const st = lstatSync2(path2);
82523
+ return st.isFile() && !st.isSymbolicLink();
82524
+ } catch {
82525
+ return false;
82526
+ }
82527
+ }
82384
82528
  function walk(dir, root, depth, out) {
82385
82529
  if (depth > MAX_DEPTH) return;
82386
82530
  for (const lockfile of NPM_LOCKFILES) {
82387
82531
  const lockPath = join8(dir, lockfile);
82388
- if (existsSync6(lockPath)) {
82532
+ if (isSafeRegularFile(lockPath)) {
82389
82533
  const count = countNpmPackages(lockPath);
82390
82534
  if (count > 0) {
82391
82535
  out.push({
@@ -82401,7 +82545,7 @@ function walk(dir, root, depth, out) {
82401
82545
  }
82402
82546
  for (const depFile of PYTHON_DEPFILES) {
82403
82547
  const depPath = join8(dir, depFile);
82404
- if (existsSync6(depPath)) {
82548
+ if (isSafeRegularFile(depPath)) {
82405
82549
  const count = countPythonPackages(depPath, depFile);
82406
82550
  if (count > 0) {
82407
82551
  out.push({
@@ -82425,7 +82569,7 @@ function walk(dir, root, depth, out) {
82425
82569
  if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
82426
82570
  const full = join8(dir, entry);
82427
82571
  try {
82428
- const st = lstatSync(full);
82572
+ const st = lstatSync2(full);
82429
82573
  if (st.isDirectory() && !st.isSymbolicLink()) {
82430
82574
  walk(full, root, depth + 1, out);
82431
82575
  }
@@ -82518,7 +82662,8 @@ __export(hook_exports, {
82518
82662
  });
82519
82663
  import { execFileSync as execFileSync2 } from "node:child_process";
82520
82664
  import {
82521
- existsSync as existsSync7,
82665
+ existsSync as existsSync6,
82666
+ lstatSync as lstatSync3,
82522
82667
  readFileSync as readFileSync8,
82523
82668
  writeFileSync as writeFileSync3,
82524
82669
  mkdirSync,
@@ -82526,9 +82671,40 @@ import {
82526
82671
  unlinkSync as unlinkSync2
82527
82672
  } from "node:fs";
82528
82673
  import { join as join9, dirname as dirname4 } from "node:path";
82529
- function findGitDir() {
82674
+ function assertSafeWriteTarget(target) {
82675
+ const parent = dirname4(target);
82530
82676
  try {
82531
- return execFileSync2("git", ["rev-parse", "--git-dir"], {
82677
+ const parentStat = lstatSync3(parent);
82678
+ if (parentStat.isSymbolicLink()) {
82679
+ throw new Error(
82680
+ `refusing to write to ${target}: parent dir ${parent} is a symlink (possible path-traversal attack)`
82681
+ );
82682
+ }
82683
+ } catch (e) {
82684
+ if (e instanceof Error && e.message.startsWith("refusing")) {
82685
+ throw e;
82686
+ }
82687
+ }
82688
+ try {
82689
+ const targetStat = lstatSync3(target);
82690
+ if (targetStat.isSymbolicLink()) {
82691
+ throw new Error(
82692
+ `refusing to write to ${target}: target is a symlink (possible path-traversal attack)`
82693
+ );
82694
+ }
82695
+ } catch (e) {
82696
+ if (e instanceof Error && e.message.startsWith("refusing")) {
82697
+ throw e;
82698
+ }
82699
+ }
82700
+ }
82701
+ function safeWriteFileSync(target, content) {
82702
+ assertSafeWriteTarget(target);
82703
+ writeFileSync3(target, content);
82704
+ }
82705
+ function findHooksDir() {
82706
+ try {
82707
+ return execFileSync2("git", ["rev-parse", "--git-path", "hooks"], {
82532
82708
  encoding: "utf-8",
82533
82709
  stdio: ["pipe", "pipe", "pipe"]
82534
82710
  }).trim();
@@ -82551,9 +82727,9 @@ function detectHookFramework(repoRoot) {
82551
82727
  const huskyHook = join9(root, ".husky", "pre-commit");
82552
82728
  const lefthookConfig = join9(root, "lefthook.yml");
82553
82729
  const lefthookConfigYaml = join9(root, "lefthook.yaml");
82554
- const gitDir = findGitDir();
82555
- const bareHook = join9(gitDir, "hooks", "pre-commit");
82556
- if (existsSync7(huskyHook)) {
82730
+ const hooksDir = findHooksDir();
82731
+ const bareHook = join9(hooksDir, "pre-commit");
82732
+ if (existsSync6(huskyHook)) {
82557
82733
  const content = readFileSync8(huskyHook, "utf-8");
82558
82734
  return {
82559
82735
  framework: "husky",
@@ -82562,7 +82738,7 @@ function detectHookFramework(repoRoot) {
82562
82738
  };
82563
82739
  }
82564
82740
  for (const cfg of [lefthookConfig, lefthookConfigYaml]) {
82565
- if (existsSync7(cfg)) {
82741
+ if (existsSync6(cfg)) {
82566
82742
  const content = readFileSync8(cfg, "utf-8");
82567
82743
  const installed2 = /^\s+dependency-guardian\s*:/m.test(content);
82568
82744
  return {
@@ -82573,7 +82749,7 @@ function detectHookFramework(repoRoot) {
82573
82749
  }
82574
82750
  }
82575
82751
  let installed = false;
82576
- if (existsSync7(bareHook)) {
82752
+ if (existsSync6(bareHook)) {
82577
82753
  installed = readFileSync8(bareHook, "utf-8").includes(HOOK_MARKER);
82578
82754
  }
82579
82755
  return {
@@ -82595,7 +82771,7 @@ ${HUSKY_SNIPPET}`;
82595
82771
 
82596
82772
  ${LEFTHOOK_ENTRY}`;
82597
82773
  case "bare":
82598
- if (existsSync7(info.targetFile)) {
82774
+ if (existsSync6(info.targetFile)) {
82599
82775
  return `Will append to existing hook at ${info.targetFile}:
82600
82776
  ${HOOK_SECTION}`;
82601
82777
  }
@@ -82609,7 +82785,7 @@ function installHuskyHook(targetFile) {
82609
82785
  process.stderr.write(" Hook already installed in Husky.\n");
82610
82786
  return;
82611
82787
  }
82612
- writeFileSync3(targetFile, existing.trimEnd() + "\n" + HUSKY_SNIPPET);
82788
+ safeWriteFileSync(targetFile, existing.trimEnd() + "\n" + HUSKY_SNIPPET);
82613
82789
  try {
82614
82790
  chmodSync2(targetFile, 493);
82615
82791
  } catch {
@@ -82631,7 +82807,7 @@ function installLefthookHook(targetFile) {
82631
82807
  /^(\s{2}commands\s*:\s*)$/m,
82632
82808
  `$1
82633
82809
  ${LEFTHOOK_MARKER}:
82634
- glob: "{package-lock.json,yarn.lock,pnpm-lock.yaml,npm-shrinkwrap.json}"
82810
+ glob: "**/{package-lock.json,yarn.lock,pnpm-lock.yaml,npm-shrinkwrap.json,requirements.txt,requirements-*.txt,Pipfile.lock,poetry.lock}"
82635
82811
  run: dg scan --mode block`
82636
82812
  );
82637
82813
  } else {
@@ -82645,20 +82821,20 @@ function installLefthookHook(targetFile) {
82645
82821
  } else {
82646
82822
  updated = existing.trimEnd() + "\n\n" + LEFTHOOK_ENTRY;
82647
82823
  }
82648
- writeFileSync3(targetFile, updated);
82824
+ safeWriteFileSync(targetFile, updated);
82649
82825
  process.stderr.write(` Added Dependency Guardian to ${targetFile}
82650
82826
  `);
82651
82827
  }
82652
82828
  function installBareHook(targetFile) {
82653
82829
  const hooksDir = dirname4(targetFile);
82654
82830
  mkdirSync(hooksDir, { recursive: true });
82655
- if (existsSync7(targetFile)) {
82831
+ if (existsSync6(targetFile)) {
82656
82832
  const existing = readFileSync8(targetFile, "utf-8");
82657
82833
  if (existing.includes(HOOK_MARKER)) {
82658
82834
  process.stderr.write(" Hook already installed.\n");
82659
82835
  return;
82660
82836
  }
82661
- writeFileSync3(targetFile, existing.trimEnd() + "\n" + HOOK_SECTION);
82837
+ safeWriteFileSync(targetFile, existing.trimEnd() + "\n" + HOOK_SECTION);
82662
82838
  chmodSync2(targetFile, 493);
82663
82839
  process.stderr.write(
82664
82840
  ` Appended Dependency Guardian hook to existing ${targetFile}
@@ -82666,7 +82842,7 @@ function installBareHook(targetFile) {
82666
82842
  );
82667
82843
  return;
82668
82844
  }
82669
- writeFileSync3(targetFile, HOOK_SCRIPT);
82845
+ safeWriteFileSync(targetFile, HOOK_SCRIPT);
82670
82846
  chmodSync2(targetFile, 493);
82671
82847
  process.stderr.write(` Installed git pre-commit hook at ${targetFile}
82672
82848
  `);
@@ -82689,12 +82865,12 @@ function installHook() {
82689
82865
  installHookForFramework(info);
82690
82866
  }
82691
82867
  function uninstallHook() {
82692
- const gitDir = findGitDir();
82693
- const hookPath = join9(gitDir, "hooks", "pre-commit");
82868
+ const hooksDir = findHooksDir();
82869
+ const hookPath = join9(hooksDir, "pre-commit");
82694
82870
  try {
82695
82871
  const root = findRepoRoot();
82696
82872
  const huskyPath = join9(root, ".husky", "pre-commit");
82697
- if (existsSync7(huskyPath)) {
82873
+ if (existsSync6(huskyPath)) {
82698
82874
  const content2 = readFileSync8(huskyPath, "utf-8");
82699
82875
  if (content2.includes(HOOK_MARKER)) {
82700
82876
  const startIdx2 = content2.indexOf(MARKER_START);
@@ -82703,7 +82879,7 @@ function uninstallHook() {
82703
82879
  const before = content2.slice(0, startIdx2).trimEnd();
82704
82880
  const after = content2.slice(endIdx2 + MARKER_END.length).trimStart();
82705
82881
  const remaining = (before + (after ? "\n" + after : "")).trimEnd() + "\n";
82706
- writeFileSync3(huskyPath, remaining);
82882
+ safeWriteFileSync(huskyPath, remaining);
82707
82883
  process.stderr.write(
82708
82884
  ` Removed Dependency Guardian section from ${huskyPath}
82709
82885
  `
@@ -82713,14 +82889,14 @@ function uninstallHook() {
82713
82889
  }
82714
82890
  }
82715
82891
  for (const cfg of [join9(root, "lefthook.yml"), join9(root, "lefthook.yaml")]) {
82716
- if (existsSync7(cfg)) {
82892
+ if (existsSync6(cfg)) {
82717
82893
  const content2 = readFileSync8(cfg, "utf-8");
82718
82894
  if (/^\s+dependency-guardian\s*:/m.test(content2)) {
82719
82895
  const stripped = content2.replace(
82720
82896
  /^\s+dependency-guardian\s*:\s*\n(?:\s{6,}.*\n)+/gm,
82721
82897
  ""
82722
82898
  );
82723
- writeFileSync3(cfg, stripped);
82899
+ safeWriteFileSync(cfg, stripped);
82724
82900
  process.stderr.write(
82725
82901
  ` Removed Dependency Guardian section from ${cfg}
82726
82902
  `
@@ -82731,7 +82907,7 @@ function uninstallHook() {
82731
82907
  }
82732
82908
  } catch {
82733
82909
  }
82734
- if (!existsSync7(hookPath)) {
82910
+ if (!existsSync6(hookPath)) {
82735
82911
  process.stderr.write(" No hook to remove.\n");
82736
82912
  return;
82737
82913
  }
@@ -82754,7 +82930,7 @@ function uninstallHook() {
82754
82930
  const before = content.slice(0, startIdx).trimEnd();
82755
82931
  const after = content.slice(endIdx + MARKER_END.length).trimStart();
82756
82932
  const remaining = (before + (after ? "\n" + after : "")).trimEnd() + "\n";
82757
- writeFileSync3(hookPath, remaining);
82933
+ safeWriteFileSync(hookPath, remaining);
82758
82934
  process.stderr.write(
82759
82935
  ` Removed Dependency Guardian section from ${hookPath}
82760
82936
  `
@@ -82800,16 +82976,12 @@ var init_hook = __esm({
82800
82976
  HOOK_SCRIPT = `#!/bin/sh
82801
82977
  ${HOOK_MARKER} \u2014 installed by \`dg hook install\`
82802
82978
 
82803
- # Check if any lockfiles are staged
82804
- STAGED=""
82805
- for f in package-lock.json npm-shrinkwrap.json yarn.lock pnpm-lock.yaml; do
82806
- if git diff --cached --name-only | grep -q "^\${f}$"; then
82807
- STAGED="yes"
82808
- break
82809
- fi
82810
- done
82811
-
82812
- if [ -z "$STAGED" ]; then
82979
+ # Match any of the watched lockfiles, including in subdirectories
82980
+ # (monorepos) \u2014 the regex anchors at start-of-path OR after a slash.
82981
+ # Mirrors the GitHub-App side which is monorepo-aware via basename
82982
+ # matching against NPM_DEPENDENCY_FILES + isPythonDepFile.
82983
+ if ! git diff --cached --name-only | grep -qE \\
82984
+ '(^|/)(package-lock\\.json|npm-shrinkwrap\\.json|yarn\\.lock|pnpm-lock\\.yaml|requirements(-[^/]+)?\\.txt|Pipfile\\.lock|poetry\\.lock)$'; then
82813
82985
  exit 0
82814
82986
  fi
82815
82987
 
@@ -82846,7 +83018,7 @@ ${MARKER_START}
82846
83018
  ${HOOK_MARKER} \u2014 installed by \`dg hook install\`
82847
83019
  DG_BIN=$(command -v dg 2>/dev/null || command -v dependency-guardian 2>/dev/null)
82848
83020
  if [ -n "$DG_BIN" ]; then
82849
- if git diff --cached --name-only | grep -qE '^(package-lock\\.json|npm-shrinkwrap\\.json|yarn\\.lock|pnpm-lock\\.yaml)$'; then
83021
+ if git diff --cached --name-only | grep -qE '(^|/)(package-lock\\.json|npm-shrinkwrap\\.json|yarn\\.lock|pnpm-lock\\.yaml|requirements(-[^/]+)?\\.txt|Pipfile\\.lock|poetry\\.lock)$'; then
82850
83022
  echo "Dependency Guardian: lockfile change detected, scanning..." >&2
82851
83023
  NO_COLOR=1 "$DG_BIN" scan --mode block
82852
83024
  DG_EXIT=$?
@@ -82861,7 +83033,7 @@ ${MARKER_END}
82861
83033
  LEFTHOOK_ENTRY = ` pre-commit:
82862
83034
  commands:
82863
83035
  ${LEFTHOOK_MARKER}:
82864
- glob: "{package-lock.json,yarn.lock,pnpm-lock.yaml,npm-shrinkwrap.json}"
83036
+ glob: "**/{package-lock.json,yarn.lock,pnpm-lock.yaml,npm-shrinkwrap.json,requirements.txt,requirements-*.txt,Pipfile.lock,poetry.lock}"
82865
83037
  run: dg scan --mode block
82866
83038
  `;
82867
83039
  USAGE2 = `
@@ -83126,7 +83298,7 @@ var init_parse_package_json = __esm({
83126
83298
 
83127
83299
  // src/lockfile.ts
83128
83300
  import { execFileSync as execFileSync3 } from "node:child_process";
83129
- import { readFileSync as readFileSync9, existsSync as existsSync8, statSync } from "node:fs";
83301
+ import { readFileSync as readFileSync9, existsSync as existsSync7, statSync } from "node:fs";
83130
83302
  import { join as join10 } from "node:path";
83131
83303
  function readFileSafe(path2) {
83132
83304
  const size = statSync(path2).size;
@@ -83143,7 +83315,7 @@ function discoverChanges(cwd2, config3) {
83143
83315
  const pythonDepFiles = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
83144
83316
  let pythonPackages = [];
83145
83317
  for (const pyFile of pythonDepFiles) {
83146
- if (existsSync8(join10(cwd2, pyFile))) {
83318
+ if (existsSync7(join10(cwd2, pyFile))) {
83147
83319
  const pyPkgs = parsePythonDepFile(cwd2, pyFile);
83148
83320
  for (const p of pyPkgs) {
83149
83321
  if (p.version === "latest") continue;
@@ -83168,66 +83340,53 @@ function discoverChanges(cwd2, config3) {
83168
83340
  const headContent = readFileSafe(lockfileInfo.path);
83169
83341
  const headParsed = parseLockfileByType(headContent, lockfileInfo.type);
83170
83342
  const directDeps = getDirectDeps(cwd2);
83171
- if (config3.scanAll) {
83172
- const packages2 = [];
83173
- for (const [name, entry] of headParsed.packages) {
83174
- if (packages2.length >= config3.maxPackages) break;
83175
- if (entry.optional && entry.hasPlatformRestriction) continue;
83176
- if (name === SELF_PACKAGE) continue;
83177
- packages2.push({
83178
- name,
83179
- version: entry.version,
83180
- previousVersion: null,
83181
- isNew: true
83182
- });
83183
- }
83184
- return { packages: packages2, pythonPackages, method: "scan-all", skipped: [] };
83185
- }
83186
83343
  if (config3.baseLockfile) {
83187
- if (!existsSync8(config3.baseLockfile)) {
83344
+ if (!existsSync7(config3.baseLockfile)) {
83188
83345
  throw new Error(`Base lockfile not found: ${config3.baseLockfile}`);
83189
83346
  }
83190
- const baseContent2 = readFileSafe(config3.baseLockfile);
83191
- const baseParsed = parseLockfile(baseContent2);
83192
- const diff2 = diffLockfiles(baseParsed, headParsed, config3.maxPackages, directDeps);
83193
- return {
83194
- packages: diff2.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83195
- pythonPackages,
83196
- method: "base-lockfile",
83197
- skipped: diff2.skipped
83198
- };
83199
- }
83200
- const baseContent = getGitBaseLockfile(cwd2);
83201
- if (baseContent !== null) {
83347
+ const baseContent = readFileSafe(config3.baseLockfile);
83202
83348
  const baseParsed = parseLockfile(baseContent);
83203
83349
  const diff2 = diffLockfiles(baseParsed, headParsed, config3.maxPackages, directDeps);
83204
83350
  return {
83205
83351
  packages: diff2.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83206
83352
  pythonPackages,
83207
- method: "git-diff",
83353
+ method: "base-lockfile",
83208
83354
  skipped: diff2.skipped
83209
83355
  };
83210
83356
  }
83211
- const pkgJsonPath = join10(cwd2, "package.json");
83212
- if (existsSync8(pkgJsonPath)) {
83213
- const headPkgJson = readFileSafe(pkgJsonPath);
83214
- const basePkgJson = getGitBaseFile(cwd2, "package.json");
83215
- if (basePkgJson !== null) {
83216
- const diff2 = diffPackageJsons(basePkgJson, headPkgJson, config3.maxPackages);
83217
- const resolved = diff2.changes.map((change) => {
83218
- const lockEntry = headParsed.packages.get(change.name);
83219
- return {
83220
- ...change,
83221
- newVersion: lockEntry?.version ?? change.newVersion
83222
- };
83223
- });
83357
+ if (!config3.scanAll) {
83358
+ const baseContent = getGitBaseLockfile(cwd2);
83359
+ if (baseContent !== null) {
83360
+ const baseParsed = parseLockfile(baseContent);
83361
+ const diff2 = diffLockfiles(baseParsed, headParsed, config3.maxPackages, directDeps);
83224
83362
  return {
83225
- packages: resolved.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83363
+ packages: diff2.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83226
83364
  pythonPackages,
83227
- method: "fallback",
83228
- skipped: []
83365
+ method: "git-diff",
83366
+ skipped: diff2.skipped
83229
83367
  };
83230
83368
  }
83369
+ const pkgJsonPath = join10(cwd2, "package.json");
83370
+ if (existsSync7(pkgJsonPath)) {
83371
+ const headPkgJson = readFileSafe(pkgJsonPath);
83372
+ const basePkgJson = getGitBaseFile(cwd2, "package.json");
83373
+ if (basePkgJson !== null) {
83374
+ const diff2 = diffPackageJsons(basePkgJson, headPkgJson, config3.maxPackages);
83375
+ const resolved = diff2.changes.map((change) => {
83376
+ const lockEntry = headParsed.packages.get(change.name);
83377
+ return {
83378
+ ...change,
83379
+ newVersion: lockEntry?.version ?? change.newVersion
83380
+ };
83381
+ });
83382
+ return {
83383
+ packages: resolved.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83384
+ pythonPackages,
83385
+ method: "fallback",
83386
+ skipped: []
83387
+ };
83388
+ }
83389
+ }
83231
83390
  }
83232
83391
  const packages = [];
83233
83392
  for (const [name, entry] of headParsed.packages) {
@@ -83241,7 +83400,7 @@ function discoverChanges(cwd2, config3) {
83241
83400
  isNew: true
83242
83401
  });
83243
83402
  }
83244
- return { packages, pythonPackages, method: "fallback", skipped: [] };
83403
+ return { packages, pythonPackages, method: "scan-all", skipped: [] };
83245
83404
  }
83246
83405
  function findLockfile(cwd2) {
83247
83406
  const candidates = [
@@ -83252,7 +83411,7 @@ function findLockfile(cwd2) {
83252
83411
  ];
83253
83412
  for (const [name, type] of candidates) {
83254
83413
  const p = join10(cwd2, name);
83255
- if (existsSync8(p)) return { path: p, type };
83414
+ if (existsSync7(p)) return { path: p, type };
83256
83415
  }
83257
83416
  return null;
83258
83417
  }
@@ -83514,7 +83673,7 @@ async function callAnalyzeAPI(packages, config3, onProgress) {
83514
83673
  const results = [];
83515
83674
  let completed = 0;
83516
83675
  const tTotal = Date.now();
83517
- if (onProgress) onProgress(0, packages.length, batches[0]?.map((p) => p.name) ?? []);
83676
+ if (onProgress) onProgress(0, packages.length, []);
83518
83677
  for (let i = 0; i < batches.length; i++) {
83519
83678
  const batch = batches[i];
83520
83679
  const tBatch = Date.now();
@@ -83522,7 +83681,7 @@ async function callAnalyzeAPI(packages, config3, onProgress) {
83522
83681
  if (process.env.DG_PERF) console.error(`[CLI-PERF] batch ${i + 1}/${batches.length}: ${batch.length} packages \u2192 ${Date.now() - tBatch}ms`);
83523
83682
  completed += batch.length;
83524
83683
  if (onProgress) {
83525
- onProgress(completed, packages.length, batches[i + 1]?.map((p) => p.name) ?? batch.map((p) => p.name));
83684
+ onProgress(completed, packages.length, batch.map((p) => p.name));
83526
83685
  }
83527
83686
  results.push(result);
83528
83687
  }
@@ -83901,6 +84060,10 @@ function initialState(dryRun, firstRun) {
83901
84060
  };
83902
84061
  }
83903
84062
  function reducer(state, action) {
84063
+ if (process.env.DG_DEBUG_WIZARD && action.type !== "scan_progress") {
84064
+ process.stderr.write(`[wizard] action=${action.type} phase=${state.phase}
84065
+ `);
84066
+ }
83904
84067
  switch (action.type) {
83905
84068
  // ── First-run guided tour transitions ──────────────────────────────────
83906
84069
  case "welcome_yes":
@@ -84119,18 +84282,26 @@ function useInit(opts = {}) {
84119
84282
  const projects = state.projects.length > 0 ? state.projects : [{ path: opts.cwd ?? process.cwd() }];
84120
84283
  const allOutcomes = [];
84121
84284
  for (const proj of projects) {
84285
+ if (process.env.DG_DEBUG_WIZARD) process.stderr.write(`[wizard] scanning ${proj.path}
84286
+ `);
84122
84287
  const outcome = await scanFn(proj.path, config3, (p) => {
84123
84288
  dispatch({ type: "scan_progress", progress: p });
84124
84289
  });
84290
+ if (process.env.DG_DEBUG_WIZARD) process.stderr.write(`[wizard] scan done ${proj.path} status=${outcome.status}
84291
+ `);
84125
84292
  allOutcomes.push(outcome);
84126
84293
  }
84127
- const allPackages = allOutcomes.flatMap(
84128
- (o) => o.result?.result.packages ?? []
84129
- );
84130
- const totalScanned = allOutcomes.reduce(
84131
- (sum, o) => sum + (o.result?.scannedCount ?? 0),
84132
- 0
84133
- );
84294
+ const allPackages = [];
84295
+ const seen = /* @__PURE__ */ new Set();
84296
+ for (const outcome of allOutcomes) {
84297
+ for (const pkg of outcome.result?.result.packages ?? []) {
84298
+ const key = `${pkg.name}@${pkg.version}`;
84299
+ if (seen.has(key)) continue;
84300
+ seen.add(key);
84301
+ allPackages.push(pkg);
84302
+ }
84303
+ }
84304
+ const totalScanned = seen.size;
84134
84305
  const totalDuration = allOutcomes.reduce(
84135
84306
  (sum, o) => sum + (o.result?.durationMs ?? 0),
84136
84307
  0
@@ -87239,20 +87410,14 @@ var init_InitApp = __esm({
87239
87410
  const { exit } = use_app_default();
87240
87411
  useTerminalSize();
87241
87412
  const [yesNoCursor, setYesNoCursor] = (0, import_react27.useState)(defaultYesNoCursor(state.phase));
87242
- const altScreenActiveRef = (0, import_react27.useRef)(false);
87413
+ const prevPhaseRef = (0, import_react27.useRef)(state.phase);
87414
+ const [, forceTick] = (0, import_react27.useState)(0);
87243
87415
  (0, import_react27.useEffect)(() => {
87244
- if (!process.stdout.isTTY) return;
87245
- process.stdout.write("\x1B[?1049h");
87246
- process.stdout.write("\x1B[2J\x1B[H");
87247
- altScreenActiveRef.current = true;
87248
- return () => {
87249
- if (altScreenActiveRef.current) {
87250
- process.stdout.write("\x1B[?1049l");
87251
- altScreenActiveRef.current = false;
87252
- }
87253
- process.stdout.write("\x1B[?25h");
87254
- };
87255
- }, []);
87416
+ if (prevPhaseRef.current === "run_scan" && state.phase === "result_first_scan") {
87417
+ Promise.resolve().then(() => forceTick((n) => n + 1));
87418
+ }
87419
+ prevPhaseRef.current = state.phase;
87420
+ }, [state.phase]);
87256
87421
  (0, import_react27.useEffect)(() => {
87257
87422
  if (YES_NO_PHASES.has(state.phase)) {
87258
87423
  setYesNoCursor(defaultYesNoCursor(state.phase));
@@ -87654,8 +87819,7 @@ var init_InitApp = __esm({
87654
87819
  case "run_scan": {
87655
87820
  const p = state.scanProgress;
87656
87821
  if (p && p.total > 0) {
87657
- const currentLabel = p.current.length > 0 ? p.current[0] : void 0;
87658
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ProgressBar, { value: p.done, total: p.total, label: currentLabel });
87822
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ProgressBar, { value: p.done, total: p.total });
87659
87823
  }
87660
87824
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Scene, { mood: "scanning", color: "cyan", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Spinner2, { label: "Scanning..." }) });
87661
87825
  }
@@ -87907,19 +88071,25 @@ async function maybeOfferFirstRunWizard(opts) {
87907
88071
  const { render: render2 } = await init_build2().then(() => build_exports);
87908
88072
  const React21 = await Promise.resolve().then(() => __toESM(require_react()));
87909
88073
  const { InitApp: InitApp2 } = await init_InitApp().then(() => InitApp_exports);
87910
- const { waitUntilExit } = render2(
87911
- React21.createElement(InitApp2, {
87912
- firstRun: true,
87913
- onReachedDone: () => {
87914
- reachedDone = true;
87915
- },
87916
- onScanRan: () => {
87917
- scanRanInWizard = true;
87918
- }
87919
- })
87920
- );
87921
- mounted = true;
87922
- await waitUntilExit();
88074
+ const { enterAltScreen: enterAltScreen2, leaveAltScreen: leaveAltScreen2 } = await Promise.resolve().then(() => (init_alt_screen(), alt_screen_exports));
88075
+ enterAltScreen2();
88076
+ try {
88077
+ const { waitUntilExit } = render2(
88078
+ React21.createElement(InitApp2, {
88079
+ firstRun: true,
88080
+ onReachedDone: () => {
88081
+ reachedDone = true;
88082
+ },
88083
+ onScanRan: () => {
88084
+ scanRanInWizard = true;
88085
+ }
88086
+ })
88087
+ );
88088
+ mounted = true;
88089
+ await waitUntilExit();
88090
+ } finally {
88091
+ leaveAltScreen2();
88092
+ }
87923
88093
  } catch {
87924
88094
  return;
87925
88095
  }
@@ -88169,7 +88339,7 @@ var init_LoginApp = __esm({
88169
88339
 
88170
88340
  // src/pip-wrapper.ts
88171
88341
  import { spawn as spawn3 } from "node:child_process";
88172
- import { readFileSync as readFileSync10, existsSync as existsSync9 } from "node:fs";
88342
+ import { readFileSync as readFileSync10, existsSync as existsSync8 } from "node:fs";
88173
88343
  function parsePipArgs(args) {
88174
88344
  let dgForce = false;
88175
88345
  const filtered = [];
@@ -88232,7 +88402,7 @@ function pipFlagTakesValue(flag) {
88232
88402
  return false;
88233
88403
  }
88234
88404
  function parseRequirementsFile(filePath) {
88235
- if (!existsSync9(filePath)) return [];
88405
+ if (!existsSync8(filePath)) return [];
88236
88406
  try {
88237
88407
  const content = readFileSync10(filePath, "utf-8");
88238
88408
  const specs = [];
@@ -88372,7 +88542,7 @@ var FileSavePrompt_exports = {};
88372
88542
  __export(FileSavePrompt_exports, {
88373
88543
  FileSavePrompt: () => FileSavePrompt
88374
88544
  });
88375
- import { existsSync as existsSync10, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "node:fs";
88545
+ import { existsSync as existsSync9, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "node:fs";
88376
88546
  import { dirname as dirname5, join as join11 } from "node:path";
88377
88547
  function listDirectory(dir) {
88378
88548
  try {
@@ -88474,7 +88644,7 @@ var init_FileSavePrompt = __esm({
88474
88644
  if (key.return) {
88475
88645
  const fullName = ensureJsonExtension(state.filename);
88476
88646
  const fullPath = join11(state.directory, fullName);
88477
- if (!state.confirmOverwrite && existsSync10(fullPath)) {
88647
+ if (!state.confirmOverwrite && existsSync9(fullPath)) {
88478
88648
  dispatch({ type: "CONFIRM_OVERWRITE" });
88479
88649
  return;
88480
88650
  }
@@ -89735,7 +89905,7 @@ var init_ScoreHeader = __esm({
89735
89905
  });
89736
89906
 
89737
89907
  // src/ui/hooks/useExpandAnimation.ts
89738
- function useExpandAnimation(targetHeight, active, durationMs = 180) {
89908
+ function useExpandAnimation(targetHeight, active2, durationMs = 180) {
89739
89909
  const [visibleLines, setVisibleLines] = (0, import_react32.useState)(0);
89740
89910
  const timerRef = (0, import_react32.useRef)(null);
89741
89911
  (0, import_react32.useEffect)(() => {
@@ -89743,7 +89913,7 @@ function useExpandAnimation(targetHeight, active, durationMs = 180) {
89743
89913
  clearInterval(timerRef.current);
89744
89914
  timerRef.current = null;
89745
89915
  }
89746
- if (!active || targetHeight <= 0) {
89916
+ if (!active2 || targetHeight <= 0) {
89747
89917
  setVisibleLines(0);
89748
89918
  return;
89749
89919
  }
@@ -89766,10 +89936,10 @@ function useExpandAnimation(targetHeight, active, durationMs = 180) {
89766
89936
  timerRef.current = null;
89767
89937
  }
89768
89938
  };
89769
- }, [active, targetHeight, durationMs]);
89939
+ }, [active2, targetHeight, durationMs]);
89770
89940
  return {
89771
- visibleLines: active ? visibleLines : 0,
89772
- isAnimating: active && visibleLines > 0 && visibleLines < targetHeight
89941
+ visibleLines: active2 ? visibleLines : 0,
89942
+ isAnimating: active2 && visibleLines > 0 && visibleLines < targetHeight
89773
89943
  };
89774
89944
  }
89775
89945
  var import_react32;
@@ -90882,7 +91052,7 @@ var init_App2 = __esm({
90882
91052
  (0, import_react35.useEffect)(() => {
90883
91053
  prevPhaseRef.current = state.phase;
90884
91054
  }, [state.phase]);
90885
- const leaveAltScreen = (0, import_react35.useCallback)(() => {
91055
+ const leaveAltScreen2 = (0, import_react35.useCallback)(() => {
90886
91056
  if (altScreenActiveRef.current && process.stdout.isTTY) {
90887
91057
  process.stdout.write("\x1B[?1049l");
90888
91058
  altScreenActiveRef.current = false;
@@ -90900,15 +91070,15 @@ var init_App2 = __esm({
90900
91070
  process.exitCode = 0;
90901
91071
  }
90902
91072
  }
90903
- leaveAltScreen();
91073
+ leaveAltScreen2();
90904
91074
  exit();
90905
- }, [state, config3, exit, leaveAltScreen]);
91075
+ }, [state, config3, exit, leaveAltScreen2]);
90906
91076
  const exitWithMessage = (0, import_react35.useCallback)((message, exitCode) => {
90907
91077
  process.exitCode = exitCode;
90908
- leaveAltScreen();
91078
+ leaveAltScreen2();
90909
91079
  process.stderr.write(message);
90910
91080
  return setTimeout(() => exit(), 0);
90911
- }, [exit, leaveAltScreen]);
91081
+ }, [exit, leaveAltScreen2]);
90912
91082
  (0, import_react35.useEffect)(() => {
90913
91083
  if (state.phase === "empty") {
90914
91084
  const timer = exitWithMessage(`${state.message}
@@ -90936,7 +91106,7 @@ var init_App2 = __esm({
90936
91106
  if (state.phase === "discovering" || state.phase === "scanning") {
90937
91107
  if (input === "q" || key.escape) {
90938
91108
  process.exitCode = 0;
90939
- leaveAltScreen();
91109
+ leaveAltScreen2();
90940
91110
  exit();
90941
91111
  }
90942
91112
  }
@@ -90953,7 +91123,7 @@ var init_App2 = __esm({
90953
91123
  onConfirm: scanSelectedProjects,
90954
91124
  onCancel: () => {
90955
91125
  process.exitCode = 0;
90956
- leaveAltScreen();
91126
+ leaveAltScreen2();
90957
91127
  exit();
90958
91128
  },
90959
91129
  userStatus
@@ -91859,6 +92029,7 @@ init_telemetry();
91859
92029
  init_config();
91860
92030
  init_npm_wrapper();
91861
92031
  init_update_check();
92032
+ init_alt_screen();
91862
92033
  var CLI_VERSION = getVersion();
91863
92034
  initTelemetry(CLI_VERSION);
91864
92035
  function closestCommand(input, commands) {
@@ -91879,9 +92050,14 @@ function closestCommand(input, commands) {
91879
92050
  return bestDist <= 3 ? best : null;
91880
92051
  }
91881
92052
  process.on("SIGINT", () => {
92053
+ leaveAltScreen();
91882
92054
  process.stderr.write("\n");
91883
92055
  process.exit(130);
91884
92056
  });
92057
+ process.on("SIGTERM", () => {
92058
+ leaveAltScreen();
92059
+ process.exit(143);
92060
+ });
91885
92061
  var isInteractive = process.stdout.isTTY === true && !process.env.CI && !process.env.NO_COLOR;
91886
92062
  async function main() {
91887
92063
  const rawCommand = process.argv[2];
@@ -91913,10 +92089,15 @@ async function main() {
91913
92089
  const { render: render3 } = await init_build2().then(() => build_exports);
91914
92090
  const React22 = await Promise.resolve().then(() => __toESM(require_react()));
91915
92091
  const { InitApp: InitApp2 } = await init_InitApp().then(() => InitApp_exports);
91916
- const { waitUntilExit } = render3(
91917
- React22.createElement(InitApp2, { firstRun: true, returning: true })
91918
- );
91919
- await waitUntilExit();
92092
+ enterAltScreen();
92093
+ try {
92094
+ const { waitUntilExit } = render3(
92095
+ React22.createElement(InitApp2, { firstRun: true, returning: true })
92096
+ );
92097
+ await waitUntilExit();
92098
+ } finally {
92099
+ leaveAltScreen();
92100
+ }
91920
92101
  try {
91921
92102
  const { markFirstRunComplete: markFirstRunComplete2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
91922
92103
  markFirstRunComplete2();