@westbayberry/dg 2.3.3 → 2.3.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.
Files changed (122) hide show
  1. package/README.md +94 -217
  2. package/dist/agents/copilot-cli.js +7 -2
  3. package/dist/agents/persistence.js +83 -8
  4. package/dist/agents/registry.js +5 -2
  5. package/dist/agents/routing.js +31 -14
  6. package/dist/api/analyze.js +138 -41
  7. package/dist/audit/deep.js +2 -1
  8. package/dist/audit/detectors.js +23 -2
  9. package/dist/audit/rules.js +4 -1
  10. package/dist/audit-ui/export.js +5 -4
  11. package/dist/auth/device-login.js +33 -11
  12. package/dist/auth/login-app.js +17 -12
  13. package/dist/auth/store.js +65 -9
  14. package/dist/bin/dg.js +1 -1
  15. package/dist/commands/audit.js +12 -20
  16. package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
  17. package/dist/commands/cooldown.js +2 -1
  18. package/dist/commands/decisions.js +8 -7
  19. package/dist/commands/help.js +11 -3
  20. package/dist/commands/licenses.js +17 -8
  21. package/dist/commands/login.js +58 -21
  22. package/dist/commands/router.js +12 -2
  23. package/dist/commands/sbom.js +20 -5
  24. package/dist/commands/scan.js +5 -4
  25. package/dist/commands/service.js +1 -1
  26. package/dist/commands/setup.js +12 -12
  27. package/dist/commands/status.js +4 -4
  28. package/dist/commands/uninstall.js +1 -1
  29. package/dist/commands/update.js +20 -0
  30. package/dist/commands/verify.js +4 -4
  31. package/dist/config/settings.js +81 -12
  32. package/dist/export-ui/ExportDialog.js +1 -1
  33. package/dist/gate/cooldown-request.js +22 -0
  34. package/dist/gate/egress-guard.js +185 -0
  35. package/dist/gate/host-match.js +48 -0
  36. package/dist/gate/verdict-core.js +101 -0
  37. package/dist/gate/verdict-fetch.js +214 -0
  38. package/dist/launcher/agent-check.js +1096 -42
  39. package/dist/launcher/agent-hook-exec.js +8 -1
  40. package/dist/launcher/agent-hook-io.js +5 -3
  41. package/dist/launcher/classify.js +110 -26
  42. package/dist/launcher/env.js +84 -18
  43. package/dist/launcher/install-preflight.js +26 -3
  44. package/dist/launcher/live-install.js +4 -2
  45. package/dist/launcher/manifest-screen.js +88 -7
  46. package/dist/launcher/output-redaction.js +153 -15
  47. package/dist/launcher/preflight-prompt.js +1 -1
  48. package/dist/launcher/resolve-real-binary.js +8 -3
  49. package/dist/launcher/run.js +237 -40
  50. package/dist/origin/artifact-cache.js +55 -0
  51. package/dist/origin/cargo-config.js +12 -0
  52. package/dist/origin/gate-mode.js +7 -0
  53. package/dist/origin/platform-paths.js +14 -0
  54. package/dist/origin/precedence.js +345 -0
  55. package/dist/origin/server.js +611 -0
  56. package/dist/origin/spike-worker.js +35 -0
  57. package/dist/origin/worker.js +116 -0
  58. package/dist/presentation/package-page.js +21 -1
  59. package/dist/presentation/safe-version.js +11 -0
  60. package/dist/presentation/sarif.js +109 -0
  61. package/dist/project/dgfile.js +54 -17
  62. package/dist/project/override-trust.js +0 -0
  63. package/dist/proxy/enforcement.js +27 -7
  64. package/dist/proxy/metadata-map.js +33 -7
  65. package/dist/proxy/preverified.js +0 -3
  66. package/dist/proxy/server.js +104 -446
  67. package/dist/proxy/upstream-proxy.js +23 -4
  68. package/dist/proxy/worker.js +3 -2
  69. package/dist/publish-set/collect.js +6 -0
  70. package/dist/runtime/fatal.js +2 -1
  71. package/dist/runtime/first-run.js +5 -1
  72. package/dist/runtime/node-version.js +51 -0
  73. package/dist/runtime/nudges.js +27 -1
  74. package/dist/sbom/cyclonedx.js +120 -33
  75. package/dist/sbom/graph.js +236 -0
  76. package/dist/sbom/spdx.js +53 -0
  77. package/dist/sbom-ui/SbomApp.js +5 -3
  78. package/dist/sbom-ui/inventory.js +8 -10
  79. package/dist/scan/analyze-worker.js +3 -1
  80. package/dist/scan/collect.js +38 -8
  81. package/dist/scan/command.js +77 -27
  82. package/dist/scan/discovery.js +18 -7
  83. package/dist/scan/installed.js +202 -0
  84. package/dist/scan/manifest-coverage.js +128 -0
  85. package/dist/scan/render.js +125 -40
  86. package/dist/scan/scanner-report.js +92 -22
  87. package/dist/scan/staged.js +68 -15
  88. package/dist/scan/types.js +7 -1
  89. package/dist/scan-ui/LegacyApp.js +3 -3
  90. package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
  91. package/dist/scan-ui/hooks/useScan.js +37 -11
  92. package/dist/scan-ui/launch.js +2 -2
  93. package/dist/scan-ui/logo.js +6 -2
  94. package/dist/scan-ui/shims.js +6 -6
  95. package/dist/scripts/gate.js +53 -35
  96. package/dist/security/csv.js +9 -0
  97. package/dist/security/sanitize.js +11 -2
  98. package/dist/service/state.js +120 -37
  99. package/dist/service/trust-refresh.js +12 -1
  100. package/dist/service/trust-store.js +36 -5
  101. package/dist/service/worker.js +5 -7
  102. package/dist/setup/git-hook.js +2 -2
  103. package/dist/setup/install-method.js +36 -0
  104. package/dist/setup/plan.js +119 -57
  105. package/dist/setup/uninstall-standalone.js +7 -2
  106. package/dist/setup-ui/gate.js +1 -8
  107. package/dist/setup-ui/offer.js +14 -7
  108. package/dist/setup-ui/selector.js +1 -1
  109. package/dist/setup-ui/wizard.js +73 -78
  110. package/dist/standalone/uninstall.mjs +1114 -386
  111. package/dist/state/cleanup-registry.js +14 -3
  112. package/dist/state/cooldown-held.js +36 -13
  113. package/dist/state/locks.js +239 -34
  114. package/dist/state/sessions.js +2 -2
  115. package/dist/util/sh-escape.js +6 -0
  116. package/dist/util/tty-prompt.js +13 -4
  117. package/dist/verify/local.js +60 -10
  118. package/dist/verify/package-check.js +35 -49
  119. package/dist/verify/preflight.js +59 -33
  120. package/dist/verify/render.js +11 -12
  121. package/npm-shrinkwrap.json +714 -213
  122. package/package.json +3 -2
@@ -0,0 +1,36 @@
1
+ import { realpathSync } from "node:fs";
2
+ function normalize(path) {
3
+ return path.replace(/\\/g, "/");
4
+ }
5
+ // Resolve how dg was installed from the realpath of its own entry script, so a
6
+ // self-upgrade dispatches to the package manager that actually owns the binary.
7
+ // dg is published to npm and installed globally, so the npm-family global
8
+ // layouts are auto-handled; bun/volta and anything not under node_modules return
9
+ // null and the caller prints the manual command rather than guessing wrong (an
10
+ // npm install onto a non-npm-owned prefix fails with EACCES/ENOENT).
11
+ export function detectInstallMethod(packageName, version, argv1 = process.argv[1]) {
12
+ if (!argv1) {
13
+ return null;
14
+ }
15
+ let real = argv1;
16
+ try {
17
+ real = realpathSync(argv1);
18
+ }
19
+ catch {
20
+ real = argv1;
21
+ }
22
+ const path = normalize(real);
23
+ if (!path.includes("/node_modules/")) {
24
+ return null;
25
+ }
26
+ const spec = `${packageName}@${version}`;
27
+ const pnpmHome = process.env.PNPM_HOME;
28
+ if (/\/(pnpm|pnpm-global)\//.test(path) || (pnpmHome && path.startsWith(normalize(pnpmHome)))) {
29
+ return { manager: "pnpm", args: ["add", "-g", spec] };
30
+ }
31
+ // bun/volta manage their own toolchain dirs; an npm -g there would be wrong.
32
+ if (/\/\.bun\//.test(path) || /\/\.volta\//.test(path)) {
33
+ return null;
34
+ }
35
+ return { manager: "npm", args: ["install", "-g", spec] };
36
+ }
@@ -6,12 +6,14 @@ import { randomBytes } from "node:crypto";
6
6
  import { homedir } from "node:os";
7
7
  import { createTheme } from "../presentation/theme.js";
8
8
  import { AGENTS, AGENT_IDS, agentLabel, resolveAgentHookContext, reverseAgentHookEntry } from "../agents/registry.js";
9
+ import { removeAgentRouting, routingInstalled } from "../agents/routing.js";
10
+ import { escapeDoubleQuotedSh, escapeDoubleQuotedFish } from "../util/sh-escape.js";
9
11
  import { GATE_ENABLE_HINT, readNetworkGatePosture } from "../agents/gate-posture.js";
10
- import { acquireLockSync, findStaleSessionsSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
12
+ import { acquireLockSync, findStaleSessionsSync, LockBusyError, preserveCorruptCleanupRegistrySync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
11
13
  import { currentNodeVersion, isSupportedNode } from "../runtime/node-version.js";
12
14
  import { dgVersion } from "../commands/version.js";
13
15
  import { compareVersions, readLatestVersion } from "../commands/update.js";
14
- import { AuthError, authStatus, displayTier, readAuthState } from "../auth/store.js";
16
+ import { AuthError, authPath, authStatus, clearAuthState, displayTier, readAuthState } from "../auth/store.js";
15
17
  import { ConfigError, DEFAULT_CONFIG, loadUserConfig } from "../config/settings.js";
16
18
  import { describeCooldownSettings } from "../policy/cooldown.js";
17
19
  import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
@@ -82,7 +84,7 @@ const DOCTOR_FIX_BY_NAME = {
82
84
  "shell-rc": "dg setup",
83
85
  "python-hook-drift": "dg uninstall, or re-run dg setup, to remove the stale pip hook",
84
86
  path: "reload your shell after setup",
85
- "commit-guard": "dg guard-commit",
87
+ "commit-guard": "dg commit-guard",
86
88
  "stale-sessions": "clears on the next protected run",
87
89
  auth: "dg login"
88
90
  };
@@ -195,44 +197,45 @@ export function applySetupPlanWithLock(plan, now = new Date()) {
195
197
  lock.release();
196
198
  }
197
199
  }
198
- // Shims written by a dg that predates the self-cleaning template carry no
199
- // auto-cleanup, so a plain `npm uninstall -g` leaves them stranded forever.
200
- // Rewriting the installed shims and reinstalling the standalone uninstaller on
201
- // the first run after a version bump propagates the current template to setups
202
- // created by older releases.
203
200
  export function refreshSetupOnUpgrade(env = process.env) {
204
201
  const paths = resolveDgPaths(env);
205
202
  const shimDir = join(paths.homeDir, ".dg", "shims");
206
203
  if (!isShimFile(join(shimDir, "npm"))) {
207
- return false;
204
+ return "skipped";
208
205
  }
209
206
  const failClosed = readShimFailClosed(env);
207
+ let lock;
210
208
  try {
211
- const lock = acquireLockSync(paths, SETUP_UNINSTALL_LOCK, {
209
+ lock = acquireLockSync(paths, SETUP_UNINSTALL_LOCK, {
212
210
  staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
213
211
  });
214
- try {
215
- for (const command of SHIM_COMMANDS) {
216
- const shimPath = join(shimDir, command);
217
- if (!isShimFile(shimPath)) {
218
- continue;
219
- }
220
- writeFileSync(shimPath, shimSource(command, { failClosed }), {
221
- encoding: "utf8",
222
- mode: 0o755
223
- });
224
- chmodSync(shimPath, 0o755);
212
+ }
213
+ catch (error) {
214
+ // A busy lock is transient — report it so the caller can retry on the next
215
+ // run instead of recording the version as refreshed and skipping forever.
216
+ return error instanceof LockBusyError ? "lock-busy" : "skipped";
217
+ }
218
+ try {
219
+ for (const command of SHIM_COMMANDS) {
220
+ const shimPath = join(shimDir, command);
221
+ if (!isShimFile(shimPath)) {
222
+ continue;
225
223
  }
226
- installStandaloneUninstaller(paths.homeDir);
227
- }
228
- finally {
229
- lock.release();
224
+ writeFileSync(shimPath, shimSource(command, { failClosed }), {
225
+ encoding: "utf8",
226
+ mode: 0o755
227
+ });
228
+ chmodSync(shimPath, 0o755);
230
229
  }
230
+ installStandaloneUninstaller(paths.homeDir);
231
231
  }
232
232
  catch {
233
- return false;
233
+ return "skipped";
234
+ }
235
+ finally {
236
+ lock.release();
234
237
  }
235
- return true;
238
+ return "applied";
236
239
  }
237
240
  export function uninstallSetup(options) {
238
241
  const paths = resolveDgPaths(options.env ?? process.env);
@@ -273,6 +276,23 @@ export function uninstallSetup(options) {
273
276
  }
274
277
  sweepLegacyRcBlocks(paths.homeDir, removed, warnings);
275
278
  sweepLegacyPythonHooks(paths.homeDir, removed, warnings);
279
+ // Service-mode `dg agents on` writes fetch-routing (HTTPS_PROXY /
280
+ // NODE_EXTRA_CA_CERTS into the agent's settings, a [shell_environment_policy]
281
+ // block for codex) that is NOT tracked in the cleanup registry. Reverse it
282
+ // here, BEFORE the state dir (which holds the prior-value backup) is removed,
283
+ // so uninstall can't strand an agent pointed at a now-dead proxy.
284
+ const uninstallEnv = options.env ?? process.env;
285
+ for (const agent of AGENT_IDS) {
286
+ try {
287
+ if (routingInstalled(agent, paths.homeDir, uninstallEnv)) {
288
+ removeAgentRouting(agent, paths.homeDir, uninstallEnv);
289
+ removed.push(`agent routing (${agentLabel(agent)})`);
290
+ }
291
+ }
292
+ catch (err) {
293
+ warnings.push(`could not remove ${agentLabel(agent)} routing: ${err.message}`);
294
+ }
295
+ }
276
296
  if (!options.all && !registryRead.malformed && options.keepConfig) {
277
297
  writeRegistryWithLock(paths, {
278
298
  version: 1,
@@ -285,6 +305,9 @@ export function uninstallSetup(options) {
285
305
  if (!options.keepConfig && hadStateDirBeforeLock) {
286
306
  removeDirectory(paths.stateDir, removed, missing);
287
307
  }
308
+ if (!options.all && !options.keepConfig && clearAuthState(uninstallEnv)) {
309
+ removed.push(authPath(paths));
310
+ }
288
311
  if (options.all) {
289
312
  removeDirectory(paths.configDir, removed, missing);
290
313
  }
@@ -371,13 +394,7 @@ export function doctorReport(options = {}) {
371
394
  status: "pass",
372
395
  message: `Real binary resolver skips ${shimDir} and dg shim sentinel files`
373
396
  });
374
- checks.push({
375
- name: "package-manager-discovery",
376
- status: "pass",
377
- message: `Classifiers are registered for ${SHIM_COMMANDS.join(", ")}; gated managers remain ${packageManagerNames()
378
- .filter((name) => !SHIM_COMMANDS.includes(name))
379
- .join(", ")}`
380
- });
397
+ checks.push(packageManagerDiscoveryCheck(env));
381
398
  checks.push(...unavailableDoctorChecks());
382
399
  checks.push(serviceCheck(env));
383
400
  checks.push(agentGateCheck(env, paths.homeDir));
@@ -489,12 +506,14 @@ function describeFetchError(error, timeoutMs) {
489
506
  }
490
507
  const DOCTOR_STATUS_ROLE = {
491
508
  pass: "pass",
509
+ info: "accent",
492
510
  warn: "warn",
493
511
  fail: "block",
494
512
  unavailable: "muted"
495
513
  };
496
514
  const DOCTOR_STATUS_GLYPH = {
497
515
  pass: "✓",
516
+ info: "ℹ",
498
517
  warn: "⚠",
499
518
  fail: "✘",
500
519
  unavailable: "·"
@@ -504,7 +523,7 @@ const DOCTOR_GROUP_ORDER = [
504
523
  { key: "setup", title: "Setup" },
505
524
  { key: "account", title: "Account" }
506
525
  ];
507
- export function renderDoctorReport(report, theme = createTheme(false), verbose = false) {
526
+ export function renderDoctorReport(report, theme = createTheme(false), verbose = false, platform = process.platform) {
508
527
  const failures = report.checks.filter((check) => check.status === "fail").length;
509
528
  const warnings = report.checks.filter((check) => check.status === "warn").length;
510
529
  const role = failures > 0 ? "block" : warnings > 0 ? "warn" : "pass";
@@ -534,14 +553,19 @@ export function renderDoctorReport(report, theme = createTheme(false), verbose =
534
553
  const allWarn = setupCore.every((name) => nonPass.some((check) => check.name === name && check.status === "warn"));
535
554
  if (allWarn) {
536
555
  nonPass = nonPass.filter((check) => !setupCore.includes(check.name) && !(check.name === "config" && check.status === "warn"));
537
- const notSetUp = "not set up — bare npm/pip installs aren't protected";
556
+ const onWindows = platform === "win32";
557
+ const notSetUp = onWindows
558
+ ? "dg setup is unavailable on Windows — run scanned installs through prefix mode: dg npm install <pkg>"
559
+ : "not set up — bare npm/pip installs aren't protected";
560
+ const setupFix = onWindows ? undefined : "dg setup";
538
561
  if (nonPass.length === 0) {
539
- lines.push(rollupInlineLine(title, "warn", "⚠", notSetUp, "dg setup", theme));
562
+ lines.push(rollupInlineLine(title, "warn", "⚠", notSetUp, setupFix, theme));
540
563
  continue;
541
564
  }
542
565
  const worst = worstCheck(nonPass);
543
566
  lines.push(` ${theme.paint(DOCTOR_STATUS_ROLE[worst.status], `${DOCTOR_STATUS_GLYPH[worst.status]} ${title}`)}`);
544
- lines.push(` ${theme.paint("warn", "⚠")} ${notSetUp} ${theme.paint("muted", "→ dg setup")}`);
567
+ const notSetUpFix = setupFix ? ` ${theme.paint("muted", `→ ${setupFix}`)}` : "";
568
+ lines.push(` ${theme.paint("warn", "⚠")} ${notSetUp}${notSetUpFix}`);
545
569
  const width = nameWidth(nonPass);
546
570
  for (const check of nonPass) {
547
571
  lines.push(doctorLine(check, width, theme));
@@ -584,6 +608,7 @@ function nameWidth(checks) {
584
608
  }
585
609
  const DOCTOR_STATUS_SEVERITY = {
586
610
  pass: 0,
611
+ info: 1,
587
612
  unavailable: 1,
588
613
  warn: 2,
589
614
  fail: 3
@@ -679,8 +704,12 @@ export function shimSource(command, options = {}) {
679
704
  const failOpenTail = [
680
705
  `dg_home=$(dirname "$shim_dir")`,
681
706
  `if [ -f "$dg_home/uninstall.mjs" ] && command -v node >/dev/null 2>&1; then`,
682
- ` ( sleep 10`,
683
- ` if [ ! -x "${dg}" ] && ! PATH="$dg_path" command -v dg >/dev/null 2>&1; then`,
707
+ ` ( sleep 60`,
708
+ // Only self-clean when dg is DURABLY gone: re-check after the wait, and skip
709
+ // if a fresh `.upgrading` sentinel marks a self-upgrade in flight (a slow
710
+ // `npm install -g` makes dg unavailable for the window). The 15-min freshness
711
+ // bound means a stale sentinel can't disable cleanup forever.
712
+ ` if [ ! -x "${dg}" ] && ! PATH="$dg_path" command -v dg >/dev/null 2>&1 && [ -z "$(find "$dg_home/.upgrading" -mmin -15 2>/dev/null)" ]; then`,
684
713
  ` node "$dg_home/uninstall.mjs" --quiet >/dev/null 2>&1`,
685
714
  ` fi`,
686
715
  ` ) >/dev/null 2>&1 &`,
@@ -695,12 +724,6 @@ export function shimSource(command, options = {}) {
695
724
  ];
696
725
  return [...head, ...(options.failClosed ? failClosedTail : failOpenTail)].join("\n");
697
726
  }
698
- function escapeDoubleQuotedSh(value) {
699
- return value.replace(/[\\"$`]/g, "\\$&");
700
- }
701
- function escapeDoubleQuotedFish(value) {
702
- return value.replace(/[\\"$]/g, "\\$&");
703
- }
704
727
  function dgEntrypoint() {
705
728
  const argv1 = process.argv[1];
706
729
  return argv1 ? resolve(argv1) : "dg";
@@ -775,7 +798,7 @@ function stripRcBlockDetailed(existing) {
775
798
  const repairedLines = [];
776
799
  for (;;) {
777
800
  const lines = content.split("\n");
778
- const begin = lines.indexOf(RC_BEGIN);
801
+ const begin = lines.findIndex((line) => line.replace(/\r$/, "") === RC_BEGIN);
779
802
  if (begin === -1) {
780
803
  break;
781
804
  }
@@ -790,10 +813,13 @@ function stripRcBlockDetailed(existing) {
790
813
  return { content, repairedLines };
791
814
  }
792
815
  function rcPairPattern(begin, end) {
793
- return new RegExp(`${escapeRegex(begin)}\\n(?:(?!${escapeRegex(begin)})[\\s\\S])*?${escapeRegex(end)}\\n?`, "g");
816
+ // Tolerate CRLF: a block written on (or edited to) Windows line endings must
817
+ // still be matched, or it duplicates on re-setup and survives uninstall.
818
+ return new RegExp(`${escapeRegex(begin)}\\r?\\n(?:(?!${escapeRegex(begin)})[\\s\\S])*?${escapeRegex(end)}\\r?\\n?`, "g");
794
819
  }
795
820
  function isDgWrittenRcLine(line) {
796
- return line === RC_END || line.startsWith("# dg-") || line.includes(RC_SHIM_HELPER) || line.includes(`${sep}.dg${sep}shims`);
821
+ const trimmed = line.replace(/\r$/, "");
822
+ return trimmed === RC_END || trimmed.startsWith("# dg-") || trimmed.includes(RC_SHIM_HELPER) || trimmed.includes(`${sep}.dg${sep}shims`);
797
823
  }
798
824
  function sweepLegacyRcBlocks(homeDir, removed, warnings) {
799
825
  for (const rel of LEGACY_RC_CANDIDATES) {
@@ -942,6 +968,15 @@ function configCheck(paths, env) {
942
968
  accessSync(paths.configDir, constants.R_OK);
943
969
  const config = loadUserConfig(env);
944
970
  if (config.api.baseUrl !== DEFAULT_CONFIG.api.baseUrl) {
971
+ // A loopback endpoint is the local dev stack, not a tamper signal — never
972
+ // alarm on it.
973
+ if (isLoopbackApiHost(config.api.baseUrl)) {
974
+ return {
975
+ name: "config",
976
+ status: "pass",
977
+ message: `using a local dev API endpoint: ${config.api.baseUrl}`
978
+ };
979
+ }
945
980
  // The verdict API is the firewall's source of truth. A non-default
946
981
  // endpoint is legitimate for enterprise self-host, but a silent repoint
947
982
  // is also how an attacker would make every verdict come back clean, so
@@ -967,6 +1002,15 @@ function configCheck(paths, env) {
967
1002
  };
968
1003
  }
969
1004
  }
1005
+ function isLoopbackApiHost(baseUrl) {
1006
+ try {
1007
+ const host = new URL(baseUrl).hostname.toLowerCase();
1008
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0";
1009
+ }
1010
+ catch {
1011
+ return false;
1012
+ }
1013
+ }
970
1014
  function unavailableDoctorChecks() {
971
1015
  const unavailable = [
972
1016
  ["path-cache", "Shell command path cache checks are guidance only. After setup, run 'hash -r' for bash or 'rehash' for zsh."],
@@ -1059,7 +1103,7 @@ function serviceCheck(env) {
1059
1103
  return {
1060
1104
  name: "service",
1061
1105
  status: "warn",
1062
- message: state.lastError ?? "Service mode is running without a persistent proxy"
1106
+ message: state.lastError ?? "Service mode is running but its local proxy is not listening"
1063
1107
  };
1064
1108
  }
1065
1109
  return {
@@ -1102,8 +1146,8 @@ function agentGateCheck(env, home) {
1102
1146
  }
1103
1147
  return {
1104
1148
  name: "agent-gate",
1105
- status: "warn",
1106
- message: `${hooked.join(", ")} hooked but the fetch-time network gate is OFF static pre-screen only; absolute-path, manifest-only, dynamic, and unsupported-manager installs are NOT screened`,
1149
+ status: "info",
1150
+ message: `${hooked.join(", ")} hooked static pre-screen is on. Fetch-time screening of dynamic, absolute-path, and unsupported-manager installs is available with service mode`,
1107
1151
  fix: GATE_ENABLE_HINT
1108
1152
  };
1109
1153
  }
@@ -1232,6 +1276,23 @@ export function currentShellActivation(env = process.env) {
1232
1276
  const rcDisplay = rcPath.startsWith(homeDir) ? `~${rcPath.slice(homeDir.length)}` : rcPath;
1233
1277
  return activationCommand(shell, rcDisplay);
1234
1278
  }
1279
+ function packageManagerDiscoveryCheck(env) {
1280
+ const unprotectedManagers = packageManagerNames().filter((name) => !SHIM_COMMANDS.includes(name));
1281
+ const onPath = unprotectedManagers.filter((name) => resolveRealBinary({ name, env }).path);
1282
+ if (onPath.length === 0) {
1283
+ return {
1284
+ name: "package-manager-discovery",
1285
+ status: "pass",
1286
+ message: `Interception is wired for ${SHIM_COMMANDS.join(", ")}; no unsupported installer (${unprotectedManagers.join(", ")}) is on PATH`
1287
+ };
1288
+ }
1289
+ return {
1290
+ name: "package-manager-discovery",
1291
+ status: "info",
1292
+ message: `${onPath.join(", ")} on PATH ${onPath.length === 1 ? "runs" : "run"} unprotected — dg does not intercept ${onPath.length === 1 ? "it" : "them"} this release; ${SHIM_COMMANDS.join(", ")} are protected`,
1293
+ fix: "install through a supported manager (dg npm/pnpm/yarn/pip/uv/cargo ...) to keep firewall coverage"
1294
+ };
1295
+ }
1235
1296
  function realBinaryResolutionCheck(env) {
1236
1297
  const installed = SHIM_COMMANDS.filter((command) => resolveRealBinary({ name: command, env }).path);
1237
1298
  const absent = SHIM_COMMANDS.filter((command) => !resolveRealBinary({ name: command, env }).path);
@@ -1251,8 +1312,9 @@ function authCheck(env) {
1251
1312
  : `Authenticated from ${status.source} token ${status.tokenPreview}`;
1252
1313
  return {
1253
1314
  name: "auth",
1254
- status: status.authenticated ? "pass" : "warn",
1255
- message: status.authenticated ? connected : "not signed in"
1315
+ status: status.authenticated ? "pass" : "info",
1316
+ message: status.authenticated ? connected : "not signed in — optional; unlocks verdict details and your dashboard",
1317
+ ...(status.authenticated ? {} : { fix: "dg login" })
1256
1318
  };
1257
1319
  }
1258
1320
  catch (error) {
@@ -1421,7 +1483,7 @@ function commitGuardCheck(registry, env) {
1421
1483
  return {
1422
1484
  name: "commit-guard",
1423
1485
  status: "pass",
1424
- message: "No commit guard installed (optional; run dg guard-commit inside a repo)"
1486
+ message: "No commit guard installed (optional; run dg commit-guard inside a repo)"
1425
1487
  };
1426
1488
  }
1427
1489
  const missing = hooks.filter((entry) => !hookOwnedByDg(entry));
@@ -1430,7 +1492,7 @@ function commitGuardCheck(registry, env) {
1430
1492
  name: "commit-guard",
1431
1493
  status: "warn",
1432
1494
  message: `Commit guard hook missing or replaced at: ${missing.map((entry) => entry.path).join(", ")}`,
1433
- fix: "re-run dg guard-commit in that repo, or dg guard-commit off to forget it"
1495
+ fix: "re-run dg commit-guard in that repo, or dg commit-guard off to forget it"
1434
1496
  };
1435
1497
  }
1436
1498
  const broken = hooks.filter((entry) => {
@@ -1442,7 +1504,7 @@ function commitGuardCheck(registry, env) {
1442
1504
  name: "commit-guard",
1443
1505
  status: "warn",
1444
1506
  message: `Commit guard at ${broken.map((entry) => entry.path).join(", ")} points at a dg binary that is not runnable, so commits there fail open with a notice`,
1445
- fix: "reinstall dg, then re-run dg guard-commit"
1507
+ fix: "reinstall dg, then re-run dg commit-guard"
1446
1508
  };
1447
1509
  }
1448
1510
  return {
@@ -5,7 +5,12 @@ import { LockBusyError, resolveDgPaths } from "../state/index.js";
5
5
  function run() {
6
6
  const quiet = process.argv.includes("--quiet");
7
7
  try {
8
- const result = uninstallSetup({ keepConfig: false, all: true });
8
+ // A plain `npm uninstall -g` triggers this courtesy cleanup; it must be LESS
9
+ // destructive than an explicit `dg uninstall`. Remove the activation footprint
10
+ // (shims/rc/hooks via the registry) but keep config, auth, state, and cache so
11
+ // a reinstall keeps the user logged in. The explicit removal below is narrowed
12
+ // to the shim dir only.
13
+ const result = uninstallSetup({ keepConfig: true, all: false });
9
14
  if (!quiet) {
10
15
  process.stderr.write(`Dependency Guardian cleaned up ${result.removed.length} leftover item(s) after the package was removed.\n`);
11
16
  }
@@ -16,7 +21,7 @@ function run() {
16
21
  }
17
22
  }
18
23
  try {
19
- rmSync(join(resolveDgPaths().homeDir, ".dg"), { recursive: true, force: true });
24
+ rmSync(join(resolveDgPaths().homeDir, ".dg", "shims"), { recursive: true, force: true });
20
25
  }
21
26
  catch {
22
27
  return;
@@ -8,25 +8,18 @@ export function setupApplied(env = process.env) {
8
8
  export function securityNotesMarkerPath(env = process.env) {
9
9
  return join(resolveDgPaths(env).stateDir, "security-notes-shown");
10
10
  }
11
- export function wizardSkippedMarkerPath(env = process.env) {
12
- return join(resolveDgPaths(env).stateDir, "setup-wizard-skipped");
13
- }
14
11
  export function securityNotesShown(env = process.env) {
15
12
  return existsSync(securityNotesMarkerPath(env));
16
13
  }
17
14
  export function markSecurityNotesShown(env = process.env, now = new Date()) {
18
15
  writeMarker(securityNotesMarkerPath(env), now);
19
16
  }
20
- export function markWizardSkipped(env = process.env, now = new Date()) {
21
- writeMarker(wizardSkippedMarkerPath(env), now);
22
- }
23
17
  export function shouldOfferSetupWizard(env = process.env, stdin = process.stdin, stderr = process.stderr) {
24
18
  return (process.platform !== "win32" &&
25
19
  Boolean(stdin.isTTY) &&
26
20
  Boolean(stderr.isTTY) &&
27
21
  !isCiEnv(env) &&
28
- !setupApplied(env) &&
29
- !existsSync(wizardSkippedMarkerPath(env)));
22
+ !setupApplied(env));
30
23
  }
31
24
  function writeMarker(path, now) {
32
25
  try {
@@ -1,7 +1,8 @@
1
1
  import { resolvePresentation } from "../presentation/mode.js";
2
+ import { createTheme } from "../presentation/theme.js";
2
3
  import { MACHINE_OUTPUT_FLAGS } from "../runtime/first-run.js";
3
4
  import { buildSetupPlan } from "../setup/plan.js";
4
- import { shouldOfferSetupWizard } from "./gate.js";
5
+ import { setupApplied, shouldOfferSetupWizard } from "./gate.js";
5
6
  const SKIP_COMMANDS = new Set([
6
7
  "help",
7
8
  "--help",
@@ -14,6 +15,7 @@ const SKIP_COMMANDS = new Set([
14
15
  "login",
15
16
  "logout",
16
17
  "setup",
18
+ "doctor",
17
19
  "update",
18
20
  "upgrade",
19
21
  "uninstall"
@@ -25,18 +27,23 @@ export async function maybeOfferSetupWizard(args, options = {}) {
25
27
  if (command && SKIP_COMMANDS.has(command)) {
26
28
  return NOT_RUN;
27
29
  }
28
- if (args.some((arg) => MACHINE_OUTPUT_FLAGS.has(arg))) {
29
- return NOT_RUN;
30
- }
31
30
  if (!shouldOfferSetupWizard(env, options.stdin ?? process.stdin, options.stderr ?? process.stderr)) {
32
31
  return NOT_RUN;
33
32
  }
34
33
  const rich = options.richMode ?? resolvePresentation().mode === "rich";
35
- if (!rich) {
36
- return NOT_RUN;
34
+ if (!rich || args.some((arg) => MACHINE_OUTPUT_FLAGS.has(arg))) {
35
+ const theme = createTheme(resolvePresentation().color);
36
+ return {
37
+ handled: true,
38
+ result: {
39
+ exitCode: 1,
40
+ stdout: "",
41
+ stderr: ` ${theme.paint("muted", "Setup not finished — dg is unavailable until setup completes. Run")} ${theme.paint("accent", "dg setup")} ${theme.paint("muted", "to finish.")}\n`
42
+ }
43
+ };
37
44
  }
38
45
  const plan = buildSetupPlan({ shell: "auto" });
39
46
  const runWizard = options.runWizard ?? (await import("./wizard.js")).runSetupWizard;
40
47
  const result = await runWizard(plan, { env, autoActivate: args.length === 0 });
41
- return { handled: args.length === 0, result };
48
+ return { handled: args.length === 0 || !setupApplied(env), result };
42
49
  }
@@ -23,5 +23,5 @@ export const Selector = ({ options, onSelect, onCancel }) => {
23
23
  }
24
24
  }
25
25
  });
26
- return (_jsxs(Box, { flexDirection: "column", children: [options.map((option, i) => (_jsxs(Text, { ...(i === cursor ? { color: "cyan" } : {}), children: [i === cursor ? "❯" : " ", " ", i + 1, ". ", option.label] }, i))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 arrows or number keys to choose \u00B7 Esc to skip" })] }));
26
+ return (_jsxs(Box, { flexDirection: "column", children: [options.map((option, i) => (_jsxs(Text, { ...(i === cursor ? { color: "cyan" } : {}), children: [i === cursor ? "❯" : " ", " ", i + 1, ". ", option.label] }, i))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to skip" })] }));
27
27
  };