@westbayberry/dg 1.0.53 → 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 +249 -114
  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
@@ -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"]
@@ -82402,18 +82510,26 @@ var discover_exports = {};
82402
82510
  __export(discover_exports, {
82403
82511
  discoverProjects: () => discoverProjects
82404
82512
  });
82405
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync, lstatSync } from "node:fs";
82513
+ import { readFileSync as readFileSync7, readdirSync, lstatSync as lstatSync2 } from "node:fs";
82406
82514
  import { join as join8, relative as relative2, basename as basename2 } from "node:path";
82407
82515
  function discoverProjects(root) {
82408
82516
  const projects = [];
82409
82517
  walk(root, root, 0, projects);
82410
82518
  return projects.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
82411
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
+ }
82412
82528
  function walk(dir, root, depth, out) {
82413
82529
  if (depth > MAX_DEPTH) return;
82414
82530
  for (const lockfile of NPM_LOCKFILES) {
82415
82531
  const lockPath = join8(dir, lockfile);
82416
- if (existsSync6(lockPath)) {
82532
+ if (isSafeRegularFile(lockPath)) {
82417
82533
  const count = countNpmPackages(lockPath);
82418
82534
  if (count > 0) {
82419
82535
  out.push({
@@ -82429,7 +82545,7 @@ function walk(dir, root, depth, out) {
82429
82545
  }
82430
82546
  for (const depFile of PYTHON_DEPFILES) {
82431
82547
  const depPath = join8(dir, depFile);
82432
- if (existsSync6(depPath)) {
82548
+ if (isSafeRegularFile(depPath)) {
82433
82549
  const count = countPythonPackages(depPath, depFile);
82434
82550
  if (count > 0) {
82435
82551
  out.push({
@@ -82453,7 +82569,7 @@ function walk(dir, root, depth, out) {
82453
82569
  if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
82454
82570
  const full = join8(dir, entry);
82455
82571
  try {
82456
- const st = lstatSync(full);
82572
+ const st = lstatSync2(full);
82457
82573
  if (st.isDirectory() && !st.isSymbolicLink()) {
82458
82574
  walk(full, root, depth + 1, out);
82459
82575
  }
@@ -82546,7 +82662,8 @@ __export(hook_exports, {
82546
82662
  });
82547
82663
  import { execFileSync as execFileSync2 } from "node:child_process";
82548
82664
  import {
82549
- existsSync as existsSync7,
82665
+ existsSync as existsSync6,
82666
+ lstatSync as lstatSync3,
82550
82667
  readFileSync as readFileSync8,
82551
82668
  writeFileSync as writeFileSync3,
82552
82669
  mkdirSync,
@@ -82554,9 +82671,40 @@ import {
82554
82671
  unlinkSync as unlinkSync2
82555
82672
  } from "node:fs";
82556
82673
  import { join as join9, dirname as dirname4 } from "node:path";
82557
- function findGitDir() {
82674
+ function assertSafeWriteTarget(target) {
82675
+ const parent = dirname4(target);
82676
+ try {
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() {
82558
82706
  try {
82559
- return execFileSync2("git", ["rev-parse", "--git-dir"], {
82707
+ return execFileSync2("git", ["rev-parse", "--git-path", "hooks"], {
82560
82708
  encoding: "utf-8",
82561
82709
  stdio: ["pipe", "pipe", "pipe"]
82562
82710
  }).trim();
@@ -82579,9 +82727,9 @@ function detectHookFramework(repoRoot) {
82579
82727
  const huskyHook = join9(root, ".husky", "pre-commit");
82580
82728
  const lefthookConfig = join9(root, "lefthook.yml");
82581
82729
  const lefthookConfigYaml = join9(root, "lefthook.yaml");
82582
- const gitDir = findGitDir();
82583
- const bareHook = join9(gitDir, "hooks", "pre-commit");
82584
- if (existsSync7(huskyHook)) {
82730
+ const hooksDir = findHooksDir();
82731
+ const bareHook = join9(hooksDir, "pre-commit");
82732
+ if (existsSync6(huskyHook)) {
82585
82733
  const content = readFileSync8(huskyHook, "utf-8");
82586
82734
  return {
82587
82735
  framework: "husky",
@@ -82590,7 +82738,7 @@ function detectHookFramework(repoRoot) {
82590
82738
  };
82591
82739
  }
82592
82740
  for (const cfg of [lefthookConfig, lefthookConfigYaml]) {
82593
- if (existsSync7(cfg)) {
82741
+ if (existsSync6(cfg)) {
82594
82742
  const content = readFileSync8(cfg, "utf-8");
82595
82743
  const installed2 = /^\s+dependency-guardian\s*:/m.test(content);
82596
82744
  return {
@@ -82601,7 +82749,7 @@ function detectHookFramework(repoRoot) {
82601
82749
  }
82602
82750
  }
82603
82751
  let installed = false;
82604
- if (existsSync7(bareHook)) {
82752
+ if (existsSync6(bareHook)) {
82605
82753
  installed = readFileSync8(bareHook, "utf-8").includes(HOOK_MARKER);
82606
82754
  }
82607
82755
  return {
@@ -82623,7 +82771,7 @@ ${HUSKY_SNIPPET}`;
82623
82771
 
82624
82772
  ${LEFTHOOK_ENTRY}`;
82625
82773
  case "bare":
82626
- if (existsSync7(info.targetFile)) {
82774
+ if (existsSync6(info.targetFile)) {
82627
82775
  return `Will append to existing hook at ${info.targetFile}:
82628
82776
  ${HOOK_SECTION}`;
82629
82777
  }
@@ -82637,7 +82785,7 @@ function installHuskyHook(targetFile) {
82637
82785
  process.stderr.write(" Hook already installed in Husky.\n");
82638
82786
  return;
82639
82787
  }
82640
- writeFileSync3(targetFile, existing.trimEnd() + "\n" + HUSKY_SNIPPET);
82788
+ safeWriteFileSync(targetFile, existing.trimEnd() + "\n" + HUSKY_SNIPPET);
82641
82789
  try {
82642
82790
  chmodSync2(targetFile, 493);
82643
82791
  } catch {
@@ -82659,7 +82807,7 @@ function installLefthookHook(targetFile) {
82659
82807
  /^(\s{2}commands\s*:\s*)$/m,
82660
82808
  `$1
82661
82809
  ${LEFTHOOK_MARKER}:
82662
- 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}"
82663
82811
  run: dg scan --mode block`
82664
82812
  );
82665
82813
  } else {
@@ -82673,20 +82821,20 @@ function installLefthookHook(targetFile) {
82673
82821
  } else {
82674
82822
  updated = existing.trimEnd() + "\n\n" + LEFTHOOK_ENTRY;
82675
82823
  }
82676
- writeFileSync3(targetFile, updated);
82824
+ safeWriteFileSync(targetFile, updated);
82677
82825
  process.stderr.write(` Added Dependency Guardian to ${targetFile}
82678
82826
  `);
82679
82827
  }
82680
82828
  function installBareHook(targetFile) {
82681
82829
  const hooksDir = dirname4(targetFile);
82682
82830
  mkdirSync(hooksDir, { recursive: true });
82683
- if (existsSync7(targetFile)) {
82831
+ if (existsSync6(targetFile)) {
82684
82832
  const existing = readFileSync8(targetFile, "utf-8");
82685
82833
  if (existing.includes(HOOK_MARKER)) {
82686
82834
  process.stderr.write(" Hook already installed.\n");
82687
82835
  return;
82688
82836
  }
82689
- writeFileSync3(targetFile, existing.trimEnd() + "\n" + HOOK_SECTION);
82837
+ safeWriteFileSync(targetFile, existing.trimEnd() + "\n" + HOOK_SECTION);
82690
82838
  chmodSync2(targetFile, 493);
82691
82839
  process.stderr.write(
82692
82840
  ` Appended Dependency Guardian hook to existing ${targetFile}
@@ -82694,7 +82842,7 @@ function installBareHook(targetFile) {
82694
82842
  );
82695
82843
  return;
82696
82844
  }
82697
- writeFileSync3(targetFile, HOOK_SCRIPT);
82845
+ safeWriteFileSync(targetFile, HOOK_SCRIPT);
82698
82846
  chmodSync2(targetFile, 493);
82699
82847
  process.stderr.write(` Installed git pre-commit hook at ${targetFile}
82700
82848
  `);
@@ -82717,12 +82865,12 @@ function installHook() {
82717
82865
  installHookForFramework(info);
82718
82866
  }
82719
82867
  function uninstallHook() {
82720
- const gitDir = findGitDir();
82721
- const hookPath = join9(gitDir, "hooks", "pre-commit");
82868
+ const hooksDir = findHooksDir();
82869
+ const hookPath = join9(hooksDir, "pre-commit");
82722
82870
  try {
82723
82871
  const root = findRepoRoot();
82724
82872
  const huskyPath = join9(root, ".husky", "pre-commit");
82725
- if (existsSync7(huskyPath)) {
82873
+ if (existsSync6(huskyPath)) {
82726
82874
  const content2 = readFileSync8(huskyPath, "utf-8");
82727
82875
  if (content2.includes(HOOK_MARKER)) {
82728
82876
  const startIdx2 = content2.indexOf(MARKER_START);
@@ -82731,7 +82879,7 @@ function uninstallHook() {
82731
82879
  const before = content2.slice(0, startIdx2).trimEnd();
82732
82880
  const after = content2.slice(endIdx2 + MARKER_END.length).trimStart();
82733
82881
  const remaining = (before + (after ? "\n" + after : "")).trimEnd() + "\n";
82734
- writeFileSync3(huskyPath, remaining);
82882
+ safeWriteFileSync(huskyPath, remaining);
82735
82883
  process.stderr.write(
82736
82884
  ` Removed Dependency Guardian section from ${huskyPath}
82737
82885
  `
@@ -82741,14 +82889,14 @@ function uninstallHook() {
82741
82889
  }
82742
82890
  }
82743
82891
  for (const cfg of [join9(root, "lefthook.yml"), join9(root, "lefthook.yaml")]) {
82744
- if (existsSync7(cfg)) {
82892
+ if (existsSync6(cfg)) {
82745
82893
  const content2 = readFileSync8(cfg, "utf-8");
82746
82894
  if (/^\s+dependency-guardian\s*:/m.test(content2)) {
82747
82895
  const stripped = content2.replace(
82748
82896
  /^\s+dependency-guardian\s*:\s*\n(?:\s{6,}.*\n)+/gm,
82749
82897
  ""
82750
82898
  );
82751
- writeFileSync3(cfg, stripped);
82899
+ safeWriteFileSync(cfg, stripped);
82752
82900
  process.stderr.write(
82753
82901
  ` Removed Dependency Guardian section from ${cfg}
82754
82902
  `
@@ -82759,7 +82907,7 @@ function uninstallHook() {
82759
82907
  }
82760
82908
  } catch {
82761
82909
  }
82762
- if (!existsSync7(hookPath)) {
82910
+ if (!existsSync6(hookPath)) {
82763
82911
  process.stderr.write(" No hook to remove.\n");
82764
82912
  return;
82765
82913
  }
@@ -82782,7 +82930,7 @@ function uninstallHook() {
82782
82930
  const before = content.slice(0, startIdx).trimEnd();
82783
82931
  const after = content.slice(endIdx + MARKER_END.length).trimStart();
82784
82932
  const remaining = (before + (after ? "\n" + after : "")).trimEnd() + "\n";
82785
- writeFileSync3(hookPath, remaining);
82933
+ safeWriteFileSync(hookPath, remaining);
82786
82934
  process.stderr.write(
82787
82935
  ` Removed Dependency Guardian section from ${hookPath}
82788
82936
  `
@@ -82828,16 +82976,12 @@ var init_hook = __esm({
82828
82976
  HOOK_SCRIPT = `#!/bin/sh
82829
82977
  ${HOOK_MARKER} \u2014 installed by \`dg hook install\`
82830
82978
 
82831
- # Check if any lockfiles are staged
82832
- STAGED=""
82833
- for f in package-lock.json npm-shrinkwrap.json yarn.lock pnpm-lock.yaml; do
82834
- if git diff --cached --name-only | grep -q "^\${f}$"; then
82835
- STAGED="yes"
82836
- break
82837
- fi
82838
- done
82839
-
82840
- 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
82841
82985
  exit 0
82842
82986
  fi
82843
82987
 
@@ -82874,7 +83018,7 @@ ${MARKER_START}
82874
83018
  ${HOOK_MARKER} \u2014 installed by \`dg hook install\`
82875
83019
  DG_BIN=$(command -v dg 2>/dev/null || command -v dependency-guardian 2>/dev/null)
82876
83020
  if [ -n "$DG_BIN" ]; then
82877
- 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
82878
83022
  echo "Dependency Guardian: lockfile change detected, scanning..." >&2
82879
83023
  NO_COLOR=1 "$DG_BIN" scan --mode block
82880
83024
  DG_EXIT=$?
@@ -82889,7 +83033,7 @@ ${MARKER_END}
82889
83033
  LEFTHOOK_ENTRY = ` pre-commit:
82890
83034
  commands:
82891
83035
  ${LEFTHOOK_MARKER}:
82892
- 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}"
82893
83037
  run: dg scan --mode block
82894
83038
  `;
82895
83039
  USAGE2 = `
@@ -83154,7 +83298,7 @@ var init_parse_package_json = __esm({
83154
83298
 
83155
83299
  // src/lockfile.ts
83156
83300
  import { execFileSync as execFileSync3 } from "node:child_process";
83157
- import { readFileSync as readFileSync9, existsSync as existsSync8, statSync } from "node:fs";
83301
+ import { readFileSync as readFileSync9, existsSync as existsSync7, statSync } from "node:fs";
83158
83302
  import { join as join10 } from "node:path";
83159
83303
  function readFileSafe(path2) {
83160
83304
  const size = statSync(path2).size;
@@ -83171,7 +83315,7 @@ function discoverChanges(cwd2, config3) {
83171
83315
  const pythonDepFiles = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
83172
83316
  let pythonPackages = [];
83173
83317
  for (const pyFile of pythonDepFiles) {
83174
- if (existsSync8(join10(cwd2, pyFile))) {
83318
+ if (existsSync7(join10(cwd2, pyFile))) {
83175
83319
  const pyPkgs = parsePythonDepFile(cwd2, pyFile);
83176
83320
  for (const p of pyPkgs) {
83177
83321
  if (p.version === "latest") continue;
@@ -83196,66 +83340,53 @@ function discoverChanges(cwd2, config3) {
83196
83340
  const headContent = readFileSafe(lockfileInfo.path);
83197
83341
  const headParsed = parseLockfileByType(headContent, lockfileInfo.type);
83198
83342
  const directDeps = getDirectDeps(cwd2);
83199
- if (config3.scanAll) {
83200
- const packages2 = [];
83201
- for (const [name, entry] of headParsed.packages) {
83202
- if (packages2.length >= config3.maxPackages) break;
83203
- if (entry.optional && entry.hasPlatformRestriction) continue;
83204
- if (name === SELF_PACKAGE) continue;
83205
- packages2.push({
83206
- name,
83207
- version: entry.version,
83208
- previousVersion: null,
83209
- isNew: true
83210
- });
83211
- }
83212
- return { packages: packages2, pythonPackages, method: "scan-all", skipped: [] };
83213
- }
83214
83343
  if (config3.baseLockfile) {
83215
- if (!existsSync8(config3.baseLockfile)) {
83344
+ if (!existsSync7(config3.baseLockfile)) {
83216
83345
  throw new Error(`Base lockfile not found: ${config3.baseLockfile}`);
83217
83346
  }
83218
- const baseContent2 = readFileSafe(config3.baseLockfile);
83219
- const baseParsed = parseLockfile(baseContent2);
83220
- const diff2 = diffLockfiles(baseParsed, headParsed, config3.maxPackages, directDeps);
83221
- return {
83222
- packages: diff2.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83223
- pythonPackages,
83224
- method: "base-lockfile",
83225
- skipped: diff2.skipped
83226
- };
83227
- }
83228
- const baseContent = getGitBaseLockfile(cwd2);
83229
- if (baseContent !== null) {
83347
+ const baseContent = readFileSafe(config3.baseLockfile);
83230
83348
  const baseParsed = parseLockfile(baseContent);
83231
83349
  const diff2 = diffLockfiles(baseParsed, headParsed, config3.maxPackages, directDeps);
83232
83350
  return {
83233
83351
  packages: diff2.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83234
83352
  pythonPackages,
83235
- method: "git-diff",
83353
+ method: "base-lockfile",
83236
83354
  skipped: diff2.skipped
83237
83355
  };
83238
83356
  }
83239
- const pkgJsonPath = join10(cwd2, "package.json");
83240
- if (existsSync8(pkgJsonPath)) {
83241
- const headPkgJson = readFileSafe(pkgJsonPath);
83242
- const basePkgJson = getGitBaseFile(cwd2, "package.json");
83243
- if (basePkgJson !== null) {
83244
- const diff2 = diffPackageJsons(basePkgJson, headPkgJson, config3.maxPackages);
83245
- const resolved = diff2.changes.map((change) => {
83246
- const lockEntry = headParsed.packages.get(change.name);
83247
- return {
83248
- ...change,
83249
- newVersion: lockEntry?.version ?? change.newVersion
83250
- };
83251
- });
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);
83252
83362
  return {
83253
- packages: resolved.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83363
+ packages: diff2.changes.map(toPackageInput).filter((p) => p.name !== SELF_PACKAGE),
83254
83364
  pythonPackages,
83255
- method: "fallback",
83256
- skipped: []
83365
+ method: "git-diff",
83366
+ skipped: diff2.skipped
83257
83367
  };
83258
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
+ }
83259
83390
  }
83260
83391
  const packages = [];
83261
83392
  for (const [name, entry] of headParsed.packages) {
@@ -83269,7 +83400,7 @@ function discoverChanges(cwd2, config3) {
83269
83400
  isNew: true
83270
83401
  });
83271
83402
  }
83272
- return { packages, pythonPackages, method: "fallback", skipped: [] };
83403
+ return { packages, pythonPackages, method: "scan-all", skipped: [] };
83273
83404
  }
83274
83405
  function findLockfile(cwd2) {
83275
83406
  const candidates = [
@@ -83280,7 +83411,7 @@ function findLockfile(cwd2) {
83280
83411
  ];
83281
83412
  for (const [name, type] of candidates) {
83282
83413
  const p = join10(cwd2, name);
83283
- if (existsSync8(p)) return { path: p, type };
83414
+ if (existsSync7(p)) return { path: p, type };
83284
83415
  }
83285
83416
  return null;
83286
83417
  }
@@ -84160,13 +84291,17 @@ function useInit(opts = {}) {
84160
84291
  `);
84161
84292
  allOutcomes.push(outcome);
84162
84293
  }
84163
- const allPackages = allOutcomes.flatMap(
84164
- (o) => o.result?.result.packages ?? []
84165
- );
84166
- const totalScanned = allOutcomes.reduce(
84167
- (sum, o) => sum + (o.result?.scannedCount ?? 0),
84168
- 0
84169
- );
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;
84170
84305
  const totalDuration = allOutcomes.reduce(
84171
84306
  (sum, o) => sum + (o.result?.durationMs ?? 0),
84172
84307
  0
@@ -88204,7 +88339,7 @@ var init_LoginApp = __esm({
88204
88339
 
88205
88340
  // src/pip-wrapper.ts
88206
88341
  import { spawn as spawn3 } from "node:child_process";
88207
- import { readFileSync as readFileSync10, existsSync as existsSync9 } from "node:fs";
88342
+ import { readFileSync as readFileSync10, existsSync as existsSync8 } from "node:fs";
88208
88343
  function parsePipArgs(args) {
88209
88344
  let dgForce = false;
88210
88345
  const filtered = [];
@@ -88267,7 +88402,7 @@ function pipFlagTakesValue(flag) {
88267
88402
  return false;
88268
88403
  }
88269
88404
  function parseRequirementsFile(filePath) {
88270
- if (!existsSync9(filePath)) return [];
88405
+ if (!existsSync8(filePath)) return [];
88271
88406
  try {
88272
88407
  const content = readFileSync10(filePath, "utf-8");
88273
88408
  const specs = [];
@@ -88407,7 +88542,7 @@ var FileSavePrompt_exports = {};
88407
88542
  __export(FileSavePrompt_exports, {
88408
88543
  FileSavePrompt: () => FileSavePrompt
88409
88544
  });
88410
- 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";
88411
88546
  import { dirname as dirname5, join as join11 } from "node:path";
88412
88547
  function listDirectory(dir) {
88413
88548
  try {
@@ -88509,7 +88644,7 @@ var init_FileSavePrompt = __esm({
88509
88644
  if (key.return) {
88510
88645
  const fullName = ensureJsonExtension(state.filename);
88511
88646
  const fullPath = join11(state.directory, fullName);
88512
- if (!state.confirmOverwrite && existsSync10(fullPath)) {
88647
+ if (!state.confirmOverwrite && existsSync9(fullPath)) {
88513
88648
  dispatch({ type: "CONFIRM_OVERWRITE" });
88514
88649
  return;
88515
88650
  }