@westbayberry/dg 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/dg.js CHANGED
@@ -66,6 +66,11 @@ else {
66
66
  // The auth flows (browser login, paid-verify gate, deep audit upload) already
67
67
  // tell the user exactly what to do; the throttled nudges would just be noise.
68
68
  if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
69
- const { maybeShowNudges } = await import("../runtime/nudges.js");
70
- maybeShowNudges(args);
69
+ try {
70
+ const { maybeShowNudges } = await import("../runtime/nudges.js");
71
+ maybeShowNudges(args);
72
+ }
73
+ catch {
74
+ // dg deleted its own files mid-run (uninstall of itself); nudges are cosmetic.
75
+ }
71
76
  }
@@ -9,12 +9,12 @@ function rule() {
9
9
  return "─".repeat(width);
10
10
  }
11
11
  export const LiveInstall = ({ view }) => {
12
+ if (view.phase === "scanning") {
13
+ return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", view.total === 0 ? "DG starting protection…" : `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
14
+ }
12
15
  if (view.total === 0 && !view.blocked) {
13
16
  return null;
14
17
  }
15
- if (view.phase === "scanning") {
16
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" DG verifying ", packageCount(view.total), "\u2026"] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
17
- }
18
18
  if (view.blocked) {
19
19
  const blocked = view.blocked;
20
20
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: rule() }), blocked.kind === "blocked" ? (_jsxs(Text, { color: "red", children: ["\u2718 DG blocked install \u2014 ", blocked.headline] })) : (_jsxs(Text, { color: "yellow", children: ["? DG could not verify ", blocked.packageName, " \u2014 ", blocked.headline] })), _jsxs(Text, { children: [" ", blocked.packageName, " ", blocked.reason] }), blocked.override ? _jsxs(Text, { dimColor: true, children: [" ", "Override: ", blocked.override] }) : null, blocked.nextStep ? _jsxs(Text, { dimColor: true, children: [" ", "Next: ", blocked.nextStep] }) : null] }));
@@ -1,4 +1,4 @@
1
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
1
+ import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { basename, delimiter, dirname, join, resolve } from "node:path";
3
3
  import { chmodSync } from "node:fs";
4
4
  import { createTheme } from "../presentation/theme.js";
@@ -14,6 +14,8 @@ import { OPTIONAL_SUPPORT_GATES } from "./optional-support.js";
14
14
  export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"]);
15
15
  export const SHIM_SENTINEL = "dg-shim-v1";
16
16
  export const RC_SENTINEL = "dg-shell-rc-v1";
17
+ export const RC_FUNCTIONS_SENTINEL = "dg-shim-functions-v1";
18
+ const RC_SHIM_HELPER = "__dg_shim";
17
19
  export const GUARD_HOOK_SENTINEL = "dg-git-hook-v1";
18
20
  export const RC_BEGIN = "# >>> dg setup >>>";
19
21
  export const RC_END = "# <<< dg setup <<<";
@@ -21,6 +23,9 @@ const LEGACY_RC_MARKERS = [
21
23
  { begin: "# >>> dg-managed >>>", end: "# <<< dg-managed <<<" }
22
24
  ];
23
25
  const LEGACY_RC_CANDIDATES = [".zshrc", ".bashrc", ".bash_profile", ".profile", join(".config", "fish", "config.fish")];
26
+ const LEGACY_PYTHON_HOOK_PY = "dg_pip_hook.py";
27
+ const LEGACY_PYTHON_HOOK_PTH = "dg_pip_hook.pth";
28
+ const LEGACY_PYTHON_HOOK_MARKER = "Dependency Guardian pip-install interceptor";
24
29
  export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
25
30
  export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
26
31
  export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
@@ -38,6 +43,7 @@ const DOCTOR_GROUP_BY_NAME = {
38
43
  telemetry: "setup",
39
44
  shims: "setup",
40
45
  "shell-rc": "setup",
46
+ "python-hook-drift": "setup",
41
47
  path: "setup",
42
48
  "stale-sessions": "setup",
43
49
  service: "setup",
@@ -53,6 +59,7 @@ const DOCTOR_FIX_BY_NAME = {
53
59
  telemetry: "fix ~/.dg config",
54
60
  shims: "dg setup",
55
61
  "shell-rc": "dg setup",
62
+ "python-hook-drift": "dg uninstall, or re-run dg setup, to remove the stale pip hook",
56
63
  path: "reload your shell after setup",
57
64
  "stale-sessions": "clears on the next protected run",
58
65
  auth: "dg login"
@@ -142,6 +149,7 @@ export function applySetupPlan(plan, now = new Date()) {
142
149
  });
143
150
  writeFileSync(plan.rcPath, withRcBlock(readText(plan.rcPath), plan), "utf8");
144
151
  entries.push(cleanupEntry("rc", plan.rcPath, "mode1", now, RC_SENTINEL));
152
+ sweepLegacyPythonHooks(plan.paths.homeDir, [], []);
145
153
  const registry = withRegistryLock(plan.paths, () => {
146
154
  const merged = mergeRegistry(readRegistry(plan.paths).registry, entries);
147
155
  writeRegistry(plan.paths, merged);
@@ -196,6 +204,7 @@ export function uninstallSetup(options) {
196
204
  }
197
205
  }
198
206
  sweepLegacyRcBlocks(paths.homeDir, removed, warnings);
207
+ sweepLegacyPythonHooks(paths.homeDir, removed, warnings);
199
208
  if (!options.all && !registryRead.malformed && options.keepConfig) {
200
209
  writeRegistryWithLock(paths, {
201
210
  version: 1,
@@ -266,12 +275,21 @@ export function doctorReport(options = {}) {
266
275
  });
267
276
  const rcEntries = registryRead.registry.entries.filter((entry) => entry.owner === "dg" && entry.kind === "rc");
268
277
  const missingRc = rcEntries.filter((entry) => !readText(entry.path).includes(RC_SENTINEL));
278
+ const functionsPresent = rcEntries.some((entry) => readText(entry.path).includes(RC_FUNCTIONS_SENTINEL));
269
279
  checks.push({
270
280
  name: "shell-rc",
271
281
  status: rcEntries.length > 0 && missingRc.length === 0 ? "pass" : "warn",
272
282
  message: rcEntries.length === 0 ? "No dg shell rc block is registered" : `Registered shell rc blocks: ${rcEntries.length}`
273
283
  });
274
- checks.push(pathPrecedenceCheck(env, shimDir));
284
+ const staleHookSites = legacyPythonHookSites(paths.homeDir);
285
+ checks.push({
286
+ name: "python-hook-drift",
287
+ status: staleHookSites.length === 0 ? "pass" : "warn",
288
+ message: staleHookSites.length === 0
289
+ ? "No legacy dg pip hooks in user site-packages"
290
+ : `Legacy dg pip hooks break pip in: ${staleHookSites.join(", ")}`
291
+ });
292
+ checks.push(pathPrecedenceCheck(env, shimDir, functionsPresent));
275
293
  checks.push({
276
294
  name: "stale-sessions",
277
295
  status: staleSessions.length === 0 ? "pass" : "warn",
@@ -453,11 +471,31 @@ function reloadInstructions(shell) {
453
471
  }
454
472
  return ["start a new shell or run: source ~/.zshrc", "clear cached command paths with: rehash"];
455
473
  }
456
- // Absolute path: a bare `exec dg` resolves through PATH, so anything that
457
- // shadows dg (npm run prepending node_modules/.bin with a vendored copy)
458
- // hijacks every shimmed package manager.
474
+ // The baked absolute dg path stops a PATH-shadowing dg from hijacking the shim;
475
+ // the fallbacks make it fail open a removed or moved dg (uninstall, mid-upgrade)
476
+ // runs the real manager instead of bricking it.
459
477
  export function shimSource(command) {
460
- return `#!/bin/sh\n# ${SHIM_SENTINEL}\nDG_SHIM_ACTIVE="\${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}${command}:$$" exec "${escapeDoubleQuotedSh(dgEntrypoint())}" ${command} "$@"\n`;
478
+ const dg = escapeDoubleQuotedSh(dgEntrypoint());
479
+ const nonce = '"${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}' + `${command}:$$"`;
480
+ return [
481
+ "#!/bin/sh",
482
+ `# ${SHIM_SENTINEL}`,
483
+ `if [ -x "${dg}" ]; then`,
484
+ ` DG_SHIM_ACTIVE=${nonce} exec "${dg}" ${command} "$@"`,
485
+ "fi",
486
+ `dg_path=$(printf '%s' "$PATH" | awk -v RS=':' -v ORS=':' '$0 != ENVIRON["HOME"] "/.dg/shims"' | sed 's/:$//')`,
487
+ `dg_bin=$(PATH="$dg_path" command -v dg 2>/dev/null)`,
488
+ `if [ -n "$dg_bin" ]; then`,
489
+ ` DG_SHIM_ACTIVE=${nonce} exec "$dg_bin" ${command} "$@"`,
490
+ "fi",
491
+ `real_bin=$(PATH="$dg_path" command -v ${command} 2>/dev/null)`,
492
+ `if [ -n "$real_bin" ]; then`,
493
+ ` exec "$real_bin" "$@"`,
494
+ "fi",
495
+ `echo "dg: protection unavailable and no real ${command} found on PATH" >&2`,
496
+ "exit 127",
497
+ ""
498
+ ].join("\n");
461
499
  }
462
500
  function escapeDoubleQuotedSh(value) {
463
501
  return value.replace(/[\\"$`]/g, "\\$&");
@@ -475,11 +513,20 @@ function withRcBlock(existing, plan) {
475
513
  const prefix = withoutExisting.length > 0 && !withoutExisting.endsWith("\n") ? `${withoutExisting}\n` : withoutExisting;
476
514
  return `${prefix}${block}`;
477
515
  }
516
+ // The PATH export covers child processes that inherit it; the shell functions
517
+ // win even when a virtualenv prepends its own bin ahead of the shim dir, since
518
+ // a function is resolved before PATH. Each delegates to the fail-open shim and
519
+ // falls back to the real command if the shim is gone.
478
520
  function posixRcBlock(shimDir) {
479
- return `${RC_BEGIN}\n# ${RC_SENTINEL}\nexport PATH="${escapeDoubleQuotedSh(shimDir)}:$PATH"\n${RC_END}\n`;
521
+ const dir = escapeDoubleQuotedSh(shimDir);
522
+ const helper = `${RC_SHIM_HELPER}() { local __dg_c="$1"; shift; if [ -x "${dir}/$__dg_c" ]; then "${dir}/$__dg_c" "$@"; else command "$__dg_c" "$@"; fi; }`;
523
+ const fns = SHIM_COMMANDS.map((command) => `${command}() { ${RC_SHIM_HELPER} ${command} "$@"; }`).join("\n");
524
+ return `${RC_BEGIN}\n# ${RC_SENTINEL}\n# ${RC_FUNCTIONS_SENTINEL}\nexport PATH="${dir}:$PATH"\n${helper}\n${fns}\n${RC_END}\n`;
480
525
  }
481
526
  function fishRcBlock(shimDir) {
482
- return `${RC_BEGIN}\n# ${RC_SENTINEL}\nfish_add_path -p "${escapeDoubleQuotedFish(shimDir)}"\n${RC_END}\n`;
527
+ const dir = escapeDoubleQuotedFish(shimDir);
528
+ const fns = SHIM_COMMANDS.map((command) => `function ${command}; if test -x "${dir}/${command}"; "${dir}/${command}" $argv; else; command ${command} $argv; end; end`).join("\n");
529
+ return `${RC_BEGIN}\n# ${RC_SENTINEL}\n# ${RC_FUNCTIONS_SENTINEL}\nfish_add_path -p "${dir}"\n${fns}\n${RC_END}\n`;
483
530
  }
484
531
  function stripRcBlock(existing) {
485
532
  const pattern = new RegExp(`${escapeRegex(RC_BEGIN)}\\n[\\s\\S]*?${escapeRegex(RC_END)}\\n?`, "g");
@@ -517,6 +564,55 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
517
564
  }
518
565
  }
519
566
  }
567
+ export function legacyPythonHookSites(homeDir) {
568
+ return candidateSitePackagesDirs(homeDir).filter((dir) => existsSync(join(dir, LEGACY_PYTHON_HOOK_PTH)) || readText(join(dir, LEGACY_PYTHON_HOOK_PY)).includes(LEGACY_PYTHON_HOOK_MARKER));
569
+ }
570
+ function sweepLegacyPythonHooks(homeDir, removed, warnings) {
571
+ for (const dir of candidateSitePackagesDirs(homeDir)) {
572
+ const pyPath = join(dir, LEGACY_PYTHON_HOOK_PY);
573
+ const pthPath = join(dir, LEGACY_PYTHON_HOOK_PTH);
574
+ const pyIsHook = readText(pyPath).includes(LEGACY_PYTHON_HOOK_MARKER);
575
+ const pthPresent = existsSync(pthPath);
576
+ if (!pyIsHook && !pthPresent) {
577
+ continue;
578
+ }
579
+ if (pthPresent) {
580
+ removePythonHookFile(pthPath, removed, warnings);
581
+ }
582
+ if (pyIsHook) {
583
+ removePythonHookFile(pyPath, removed, warnings);
584
+ }
585
+ }
586
+ }
587
+ function removePythonHookFile(path, removed, warnings) {
588
+ try {
589
+ rmSync(path, { force: true });
590
+ removed.push(`${path} (legacy dg pip hook)`);
591
+ }
592
+ catch (error) {
593
+ warnings.push(`could not remove legacy dg pip hook ${path}: ${error instanceof Error ? error.message : "remove error"}`);
594
+ }
595
+ }
596
+ function candidateSitePackagesDirs(homeDir) {
597
+ const dirs = [];
598
+ for (const version of safeReaddir(join(homeDir, "Library", "Python"))) {
599
+ dirs.push(join(homeDir, "Library", "Python", version, "lib", "python", "site-packages"));
600
+ }
601
+ for (const entry of safeReaddir(join(homeDir, ".local", "lib"))) {
602
+ if (entry.startsWith("python")) {
603
+ dirs.push(join(homeDir, ".local", "lib", entry, "site-packages"));
604
+ }
605
+ }
606
+ return dirs;
607
+ }
608
+ function safeReaddir(dir) {
609
+ try {
610
+ return readdirSync(dir);
611
+ }
612
+ catch {
613
+ return [];
614
+ }
615
+ }
520
616
  export function cleanupEntry(kind, path, mode, now, sentinel) {
521
617
  return {
522
618
  kind,
@@ -694,7 +790,7 @@ function serviceCheck(env) {
694
790
  message: `Service mode is running at ${state.proxy.proxyUrl}; trust installed: ${state.trustInstalled ? "yes" : "no"}`
695
791
  };
696
792
  }
697
- function pathPrecedenceCheck(env, shimDir) {
793
+ function pathPrecedenceCheck(env, shimDir, functionsPresent) {
698
794
  const pathEntries = (env.PATH ?? "").split(delimiter).filter(Boolean);
699
795
  const shimIndex = pathEntries.indexOf(shimDir);
700
796
  const activateFix = `activate this shell: ${currentShellActivation(env)} — or open a new terminal`;
@@ -728,11 +824,18 @@ function pathPrecedenceCheck(env, shimDir) {
728
824
  };
729
825
  }
730
826
  if (offender) {
827
+ if (functionsPresent) {
828
+ return {
829
+ name: "path",
830
+ status: "pass",
831
+ message: `${offender.dir} resolves ${offender.command} first (e.g. an active virtualenv); dg shell functions intercept bare installs regardless`
832
+ };
833
+ }
731
834
  return {
732
835
  name: "path",
733
836
  status: "warn",
734
837
  message: `${shimDir} is on PATH but ${offender.dir} resolves ${offender.command} first`,
735
- fix: activateFix
838
+ fix: `re-run dg setup to intercept inside virtualenvs — or ${activateFix}`
736
839
  };
737
840
  }
738
841
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Dependency Guardian supply-chain firewall CLI",
5
5
  "type": "module",
6
6
  "bin": {