@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 +7 -2
- package/dist/install-ui/LiveInstall.js +3 -3
- package/dist/setup/plan.js +113 -10
- package/package.json +1 -1
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
|
-
|
|
70
|
-
|
|
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] }));
|
package/dist/setup/plan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|