@westbayberry/dg 2.0.11 → 2.2.0

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 (143) hide show
  1. package/README.md +216 -226
  2. package/dist/agents/claude-code.js +113 -0
  3. package/dist/agents/codex.js +65 -0
  4. package/dist/agents/copilot-cli.js +115 -0
  5. package/dist/agents/cursor.js +113 -0
  6. package/dist/agents/gemini.js +107 -0
  7. package/dist/agents/persistence.js +285 -0
  8. package/dist/agents/registry.js +127 -0
  9. package/dist/agents/types.js +1 -0
  10. package/dist/agents/windsurf.js +93 -0
  11. package/dist/api/analyze.js +6 -4
  12. package/dist/audit/detectors.js +0 -11
  13. package/dist/audit/events.js +5 -21
  14. package/dist/audit-ui/AuditApp.js +2 -0
  15. package/dist/audit-ui/components/AuditResultsView.js +55 -92
  16. package/dist/audit-ui/export.js +0 -4
  17. package/dist/audit-ui/format.js +0 -3
  18. package/dist/audit-ui/launch.js +10 -1
  19. package/dist/auth/device-login.js +4 -5
  20. package/dist/auth/login-app.js +7 -7
  21. package/dist/auth/store.js +8 -3
  22. package/dist/bin/dg.js +59 -52
  23. package/dist/commands/agents.js +231 -0
  24. package/dist/commands/audit.js +22 -3
  25. package/dist/commands/config.js +26 -10
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +169 -0
  28. package/dist/commands/doctor.js +1 -1
  29. package/dist/commands/help.js +1 -1
  30. package/dist/commands/licenses.js +10 -22
  31. package/dist/commands/logout.js +4 -11
  32. package/dist/commands/router.js +8 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +3 -2
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/status.js +5 -2
  38. package/dist/commands/types.js +1 -0
  39. package/dist/commands/update.js +17 -8
  40. package/dist/commands/verify.js +8 -5
  41. package/dist/config/settings.js +154 -65
  42. package/dist/decisions/apply.js +128 -0
  43. package/dist/decisions/remember-prompt.js +92 -0
  44. package/dist/export-ui/ExportDialog.js +198 -0
  45. package/dist/install-ui/LiveInstall.js +2 -2
  46. package/dist/install-ui/block-render.js +21 -4
  47. package/dist/install-ui/prep-spinner.js +32 -0
  48. package/dist/install-ui/prompt.js +14 -0
  49. package/dist/launcher/agent-check.js +466 -0
  50. package/dist/launcher/agent-hook-exec.js +70 -0
  51. package/dist/launcher/agent-hook-io.js +31 -0
  52. package/dist/launcher/cargo-cache.js +40 -0
  53. package/dist/launcher/classify.js +17 -6
  54. package/dist/launcher/env.js +71 -24
  55. package/dist/launcher/install-preflight.js +167 -17
  56. package/dist/launcher/live-install.js +25 -5
  57. package/dist/launcher/output-redaction.js +7 -4
  58. package/dist/launcher/preflight-prompt.js +43 -3
  59. package/dist/launcher/run.js +100 -86
  60. package/dist/launcher/spawn-invocation.js +21 -0
  61. package/dist/policy/cooldown.js +117 -0
  62. package/dist/policy/evaluate.js +5 -21
  63. package/dist/policy/pypi-name.js +17 -0
  64. package/dist/presentation/mode.js +3 -2
  65. package/dist/presentation/package-page.js +9 -0
  66. package/dist/presentation/provenance.js +23 -0
  67. package/dist/presentation/theme.js +7 -7
  68. package/dist/project/dgfile.js +446 -0
  69. package/dist/proxy/auth.js +42 -0
  70. package/dist/proxy/ca.js +29 -9
  71. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  72. package/dist/proxy/enforcement.js +57 -17
  73. package/dist/proxy/metadata-map.js +66 -4
  74. package/dist/proxy/preverified.js +55 -0
  75. package/dist/proxy/server.js +473 -45
  76. package/dist/proxy/worker.js +16 -1
  77. package/dist/publish-set/collect.js +1 -4
  78. package/dist/publish-set/npm.js +8 -5
  79. package/dist/publish-set/pack.js +9 -3
  80. package/dist/runtime/cli.js +0 -4
  81. package/dist/runtime/fatal.js +31 -0
  82. package/dist/runtime/first-run.js +12 -11
  83. package/dist/runtime/node-version.js +43 -6
  84. package/dist/runtime/nudges.js +35 -2
  85. package/dist/sbom/cyclonedx.js +211 -0
  86. package/dist/sbom-ui/SbomApp.js +158 -0
  87. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  88. package/dist/sbom-ui/components/SbomList.js +52 -0
  89. package/dist/sbom-ui/inventory.js +128 -0
  90. package/dist/sbom-ui/launch.js +51 -0
  91. package/dist/sbom-ui/run.js +55 -0
  92. package/dist/sbom-ui/store.js +26 -0
  93. package/dist/scan/collect.js +10 -6
  94. package/dist/scan/command.js +51 -17
  95. package/dist/scan/discovery.js +11 -2
  96. package/dist/scan/render.js +63 -8
  97. package/dist/scan/scanner-report.js +42 -9
  98. package/dist/scan/staged.js +71 -11
  99. package/dist/scan-ui/LegacyApp.js +12 -16
  100. package/dist/scan-ui/alt-screen.js +5 -8
  101. package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
  102. package/dist/scan-ui/components/ProgressBar.js +3 -14
  103. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  104. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  105. package/dist/scan-ui/components/SetupBanner.js +0 -6
  106. package/dist/scan-ui/format-helpers.js +61 -5
  107. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  108. package/dist/scan-ui/hooks/useScan.js +46 -4
  109. package/dist/scan-ui/launch.js +7 -4
  110. package/dist/scan-ui/shims.js +14 -4
  111. package/dist/scripts/detect.js +158 -0
  112. package/dist/scripts/gate.js +170 -0
  113. package/dist/service/state.js +27 -8
  114. package/dist/service/trust-refresh.js +92 -0
  115. package/dist/service/worker.js +23 -1
  116. package/dist/setup/activate-shell.js +28 -0
  117. package/dist/setup/git-hook.js +49 -4
  118. package/dist/setup/plan.js +98 -29
  119. package/dist/setup-ui/gate.js +39 -0
  120. package/dist/setup-ui/offer.js +42 -0
  121. package/dist/setup-ui/selector.js +27 -0
  122. package/dist/setup-ui/tasks.js +56 -0
  123. package/dist/setup-ui/wizard.js +225 -0
  124. package/dist/state/cooldown-held.js +66 -0
  125. package/dist/state/index.js +1 -0
  126. package/dist/state/locks.js +4 -2
  127. package/dist/state/store.js +2 -1
  128. package/dist/util/external-tool.js +25 -0
  129. package/dist/util/git.js +10 -3
  130. package/dist/util/json-file.js +24 -0
  131. package/dist/util/report-writer.js +57 -0
  132. package/dist/util/tty-prompt.js +13 -6
  133. package/dist/verify/local.js +240 -42
  134. package/dist/verify/package-check.js +86 -18
  135. package/dist/verify/preflight.js +242 -49
  136. package/dist/verify/render.js +15 -1
  137. package/npm-shrinkwrap.json +2383 -0
  138. package/package.json +14 -8
  139. package/NOTICE +0 -5
  140. package/dist/commands/completion.js +0 -116
  141. package/dist/commands/explain.js +0 -232
  142. package/dist/commands/unavailable.js +0 -11
  143. package/dist/telemetry/events.js +0 -40
@@ -1,34 +1,40 @@
1
1
  import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
2
2
  import { LockBusyError } from "../state/index.js";
3
- import { activationCommand as shellActivationCommand, applySetupPlanWithLock, buildSetupPlan, renderSetupPlan } from "../setup/plan.js";
3
+ import { activationCommand as shellActivationCommand, applySetupPlanWithLock, buildSetupPlan, renderSetupPlan, tildifyPath as tildify } from "../setup/plan.js";
4
+ import { activateShell, activationOffer } from "../setup/activate-shell.js";
4
5
  import { buildServiceSetupPlan, configureService, renderServicePlan } from "../service/state.js";
5
6
  import { optionalSupportGate } from "../setup/optional-support.js";
6
7
  import { resolvePresentation } from "../presentation/mode.js";
7
8
  import { createTheme } from "../presentation/theme.js";
8
9
  import { closeSync, openSync, readSync } from "node:fs";
9
- import { homedir } from "node:os";
10
10
  import { authStatus } from "../auth/store.js";
11
+ import { resolveWebBase } from "../auth/device-login.js";
12
+ import { applyAgentHook, collectAgentOffers } from "../agents/registry.js";
13
+ import { applyGitHook, commitGuardOffer } from "../setup/git-hook.js";
14
+ import { markSecurityNotesShown, securityNotesShown } from "../setup-ui/gate.js";
11
15
  export const setupCommand = {
12
16
  name: "setup",
13
- summary: "Protect plain npm/pip/yarn installs (no dg prefix needed).",
14
- usage: "dg setup [--print] [--yes] [--shell <auto|zsh|bash|fish>] [--service]",
17
+ summary: "Protect this machine — shell installs, AI-agent installs, and a repo's commits.",
18
+ usage: "dg setup [--print] [--yes] [--agents] [--guard-commit] [--shell <auto|zsh|bash|fish>] [--service]",
15
19
  flags: [
16
20
  { flag: "--print", summary: "Preview the exact write plan and change nothing." },
17
- { flag: "--yes", summary: "Apply without the interactive prompt (required when non-interactive)." },
21
+ { flag: "--yes", summary: "Apply the shell firewall only, without prompts (add --agents / --guard-commit to opt in the rest)." },
22
+ { flag: "--agents", summary: "With --yes: also route every detected AI agent's installs through dg." },
23
+ { flag: "--guard-commit", summary: "With --yes: also scan this repo's commits (per-repo)." },
18
24
  { flag: "--shell", value: "<auto|zsh|bash|fish>", summary: "Target shell rc to write (default: auto-detect)." },
19
25
  { flag: "--service", summary: "Set up service mode (Pro/Team; persistent proxy + managed CA)." }
20
26
  ],
21
- examples: ["dg setup --print", "dg setup --yes", "dg setup --shell zsh --yes"],
27
+ examples: ["dg setup", "dg setup --print", "dg setup --yes --agents", "dg setup --shell zsh --yes"],
22
28
  details: [
23
- "In a terminal, shows what it writes and asks before applying, then reloads your shell. Use --yes to apply non-interactively and --print to preview only.",
24
- "Writes only dg-owned reversible files and repairs setup drift; undo with dg uninstall. The per-repo git pre-commit hook is separate see dg guard-commit."
29
+ "In a terminal it shows one consent screen listing everything it will protect your shell's installs plus every detected AI agent, with the exact files it writes — and applies them on a single yes. Commits in the current repo are a separate per-repo question.",
30
+ "Non-interactively, --yes applies just the shell firewall; add --agents and/or --guard-commit to opt those in. --print previews. Writes only dg-owned reversible files; undo it all with dg uninstall."
25
31
  ],
26
32
  handler: (context) => setupHandler(context.args)
27
33
  };
28
34
  const WINDOWS_UNSUPPORTED = "dg setup (automatic interception) does not support Windows yet.\n" +
29
35
  "You can still protect installs on Windows: prefix the command with dg,\n" +
30
36
  "for example `dg pip install <pkg>` or `dg npm install <pkg>`.\n";
31
- function setupHandler(args) {
37
+ async function setupHandler(args) {
32
38
  const parsed = parseSetupArgs(args);
33
39
  if ("error" in parsed) {
34
40
  return {
@@ -54,12 +60,16 @@ function setupHandler(args) {
54
60
  if (parsed.printOnly) {
55
61
  return {
56
62
  exitCode: 0,
57
- stdout: renderedPlan,
63
+ stdout: `${renderedPlan}${printableSurfaceOffers()}`,
58
64
  stderr: ""
59
65
  };
60
66
  }
61
67
  if (!parsed.yes) {
62
68
  if (isInteractive()) {
69
+ if (resolvePresentation().mode === "rich") {
70
+ const { runSetupWizard } = await import("../setup-ui/wizard.js");
71
+ return runSetupWizard(plan, { autoActivate: true });
72
+ }
63
73
  return runInteractiveSetup(plan);
64
74
  }
65
75
  return {
@@ -81,23 +91,34 @@ function setupHandler(args) {
81
91
  }
82
92
  throw error;
83
93
  }
94
+ const extra = await applyRequestedSurfaces(parsed);
95
+ const theme = createTheme(resolvePresentation().color);
84
96
  return {
85
97
  exitCode: 0,
86
- stdout: `${renderedPlan}\nSet up — active in new terminals. Activate this shell now: ${activationCommand(plan)}\n`,
98
+ stdout: `${theme.paint("pass", "✓")} Setup complete — active in new terminals ${theme.paint("muted", `(wrote ${tildify(plan.shimDir)} and ${tildify(plan.rcPath)}; details: dg doctor)`)}\n${extra}Activate this shell now: ${theme.paint("accent", activationCommand(plan))}\n`,
87
99
  stderr: ""
88
100
  };
89
101
  }
90
102
  function isInteractive() {
91
103
  return Boolean(process.stdin.isTTY && process.stderr.isTTY);
92
104
  }
93
- function runInteractiveSetup(plan) {
105
+ async function runInteractiveSetup(plan) {
94
106
  const theme = createTheme(resolvePresentation().color);
95
107
  const accent = (text) => theme.paint("accent", text);
96
108
  const muted = (text) => theme.paint("muted", text);
97
109
  const err = process.stderr;
98
- err.write(`\n Scans ${accent("npm")}/${accent("pip")}/${accent("yarn")}/${muted("…")} installs automatically — no ${accent("dg")} prefix.\n`);
99
- err.write(` ${muted("Writes")} ${accent(tildify(plan.rcPath))}${muted(".")} ${muted("Reversible with")} ${accent("dg uninstall")}${muted(".")}\n\n`);
100
- const answer = promptYesNoSync(` ${accent("Proceed?")}`, theme);
110
+ const agents = collectAgentOffers();
111
+ const repo = commitGuardOffer();
112
+ if (!securityNotesShown()) {
113
+ err.write(renderSecurityNotesText(theme));
114
+ err.write(` ${muted("Press Enter to agree")} `);
115
+ if (readLineFromTty() !== null) {
116
+ markSecurityNotesShown();
117
+ }
118
+ err.write("\n");
119
+ }
120
+ err.write(renderConsentScreen(consentSurfaces(plan, agents, repo !== null), theme));
121
+ const answer = promptYesNoSync(` ${accent("Protect installs on this machine?")}`, theme, true);
101
122
  if (answer === "no-tty") {
102
123
  return {
103
124
  exitCode: EXIT_USAGE,
@@ -121,21 +142,152 @@ function runInteractiveSetup(plan) {
121
142
  }
122
143
  throw error;
123
144
  }
124
- err.write(`\n ${theme.paint("pass", "✓ set up — active in new terminals")}\n`);
125
- err.write(` ${muted("Activate this shell now:")} ${accent(activationCommand(plan))}\n`);
145
+ err.write(`\n ${theme.paint("pass", "✓ shell installs protected — active in new terminals")}\n`);
146
+ await applyAgentOffers(agents, theme, err);
147
+ promptCommitSurface(repo, theme, err);
148
+ return finishWithActivation(plan, theme, err);
149
+ }
150
+ export function consentSurfaces(plan, agents, repoOffered) {
151
+ const surfaces = [
152
+ { label: "shell installs", detail: `writes ${tildify(plan.rcPath)}`, deferred: false }
153
+ ];
154
+ for (const offer of agents) {
155
+ surfaces.push({ label: `${offer.label} installs`, detail: `writes ${tildify(offer.ctx.settingsPath)}`, deferred: false });
156
+ }
157
+ if (repoOffered) {
158
+ surfaces.push({ label: "commits in this repo", detail: "asked separately", deferred: true });
159
+ }
160
+ return surfaces;
161
+ }
162
+ export function renderConsentScreen(surfaces, theme) {
163
+ const accent = (text) => theme.paint("accent", text);
164
+ const muted = (text) => theme.paint("muted", text);
165
+ const width = Math.max(...surfaces.map((surface) => surface.label.length));
166
+ const lines = [
167
+ "",
168
+ ` Scans ${accent("npm")}/${accent("pip")}/${accent("yarn")}/${muted("…")} installs automatically — no ${accent("dg")} prefix.`,
169
+ ` ${muted("Reversible with")} ${accent("dg uninstall")}${muted(".")}`,
170
+ "",
171
+ ` ${muted("Sets up:")}`
172
+ ];
173
+ for (const surface of surfaces) {
174
+ const label = surface.label.padEnd(width);
175
+ lines.push(surface.deferred ? ` ${muted(label)} ${muted(surface.detail)}` : ` ${accent(label)} ${muted(surface.detail)}`);
176
+ }
177
+ lines.push("", "");
178
+ return lines.join("\n");
179
+ }
180
+ function finishWithActivation(plan, theme, err) {
181
+ const offer = activationOffer();
182
+ if (offer === "none") {
183
+ return { exitCode: 0, stdout: "", stderr: "" };
184
+ }
185
+ const accent = (text) => theme.paint("accent", text);
186
+ const muted = (text) => theme.paint("muted", text);
187
+ if (offer === "prompt" && promptYesNoSync(`\n ${accent("Activate in this terminal now?")}`, theme, true) === "yes") {
188
+ err.write(` ${muted("Starting a protected shell — type")} ${accent("exit")} ${muted("to return to your previous one.")}\n`);
189
+ return { exitCode: activateShell(), stdout: "", stderr: "" };
190
+ }
191
+ err.write(`\n ${muted("Activate this shell now:")} ${accent(activationCommand(plan))}\n`);
126
192
  return { exitCode: 0, stdout: "", stderr: "" };
127
193
  }
194
+ async function applyAgentOffers(offers, theme, err) {
195
+ const muted = (text) => theme.paint("muted", text);
196
+ for (const offer of offers) {
197
+ try {
198
+ await applyAgentHook(offer.ctx);
199
+ err.write(` ${theme.paint("pass", `✓ ${offer.label} installs route through dg`)} ${muted(`(${tildify(offer.ctx.settingsPath)})`)}\n`);
200
+ }
201
+ catch (error) {
202
+ err.write(` ${theme.paint("warn", `✗ ${offer.label}`)} ${muted(`— ${error instanceof Error ? error.message : "unknown error"}`)}\n`);
203
+ }
204
+ }
205
+ }
206
+ function promptCommitSurface(repo, theme, err) {
207
+ if (!repo) {
208
+ return;
209
+ }
210
+ const accent = (text) => theme.paint("accent", text);
211
+ const muted = (text) => theme.paint("muted", text);
212
+ err.write(`\n ${muted("This is a git repo. Scan its commits before they land?")}\n`);
213
+ if (promptYesNoSync(` ${accent("Guard commits in this repo?")}`, theme, false) !== "yes") {
214
+ return;
215
+ }
216
+ try {
217
+ applyGitHook(repo);
218
+ err.write(` ${theme.paint("pass", "✓ commits in this repo are now scanned")}\n`);
219
+ }
220
+ catch (error) {
221
+ err.write(` ${theme.paint("warn", `✗ commits in this repo`)} ${muted(`— ${error instanceof Error ? error.message : "unknown error"}`)}\n`);
222
+ }
223
+ }
224
+ async function applyRequestedSurfaces(parsed) {
225
+ const lines = [];
226
+ if (parsed.agents) {
227
+ for (const offer of collectAgentOffers()) {
228
+ try {
229
+ await applyAgentHook(offer.ctx);
230
+ lines.push(`${offer.label} installs now route through dg (${tildify(offer.ctx.settingsPath)}).`);
231
+ }
232
+ catch (error) {
233
+ lines.push(`could not hook ${offer.label}: ${error instanceof Error ? error.message : "unknown error"}`);
234
+ }
235
+ }
236
+ }
237
+ if (parsed.guardCommit) {
238
+ const repo = commitGuardOffer();
239
+ if (repo) {
240
+ try {
241
+ applyGitHook(repo);
242
+ lines.push("commits in this repo are now scanned.");
243
+ }
244
+ catch (error) {
245
+ lines.push(`could not install the commit guard: ${error instanceof Error ? error.message : "unknown error"}`);
246
+ }
247
+ }
248
+ }
249
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
250
+ }
251
+ function printableSurfaceOffers() {
252
+ const lines = [];
253
+ for (const offer of collectAgentOffers()) {
254
+ lines.push(`would offer: route ${offer.label} installs through dg (--agents)`);
255
+ }
256
+ if (commitGuardOffer()) {
257
+ lines.push("would offer: scan this repo's commits (--guard-commit)");
258
+ }
259
+ return lines.length > 0 ? `\n${lines.join("\n")}\n` : "";
260
+ }
128
261
  function activationCommand(plan) {
129
262
  return shellActivationCommand(plan.shell, tildify(plan.rcPath));
130
263
  }
131
- function promptYesNoSync(question, theme) {
132
- process.stderr.write(`${question} ${theme.paint("muted", "[y/N]")} `);
264
+ function renderSecurityNotesText(theme) {
265
+ const muted = (text) => theme.paint("muted", text);
266
+ const webBase = resolveWebBase(process.env);
267
+ return [
268
+ "",
269
+ ` ${theme.paint("accent", "Security notes")}`,
270
+ "",
271
+ " 1. dg can make mistakes.",
272
+ " A PASS verdict does not guarantee a package is safe. You are",
273
+ " responsible for what you install and should review new",
274
+ " dependencies.",
275
+ "",
276
+ " 2. By continuing you confirm you have read and understand the",
277
+ " Terms of Service and Privacy Policy.",
278
+ ` ${muted(`${webBase}/terms`)}`,
279
+ ` ${muted(`${webBase}/privacy`)}`,
280
+ "",
281
+ ""
282
+ ].join("\n");
283
+ }
284
+ function readLineFromTty() {
133
285
  let tty;
134
286
  try {
135
287
  tty = openSync("/dev/tty", "rs");
136
288
  }
137
289
  catch {
138
- return "no-tty";
290
+ return null;
139
291
  }
140
292
  try {
141
293
  const byte = Buffer.alloc(1);
@@ -160,22 +312,31 @@ function promptYesNoSync(question, theme) {
160
312
  }
161
313
  answer += char;
162
314
  }
163
- const normalized = answer.trim().toLowerCase();
164
- return normalized === "y" || normalized === "yes" ? "yes" : "no";
315
+ return answer;
165
316
  }
166
317
  finally {
167
318
  closeSync(tty);
168
319
  }
169
320
  }
170
- function tildify(path) {
171
- const home = homedir();
172
- return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
321
+ function promptYesNoSync(question, theme, defaultYes = false) {
322
+ process.stderr.write(`${question} ${theme.paint("muted", defaultYes ? "[Y/n]" : "[y/N]")} `);
323
+ const answer = readLineFromTty();
324
+ if (answer === null) {
325
+ return "no-tty";
326
+ }
327
+ const normalized = answer.trim().toLowerCase();
328
+ if (normalized === "") {
329
+ return defaultYes ? "yes" : "no";
330
+ }
331
+ return normalized === "y" || normalized === "yes" ? "yes" : "no";
173
332
  }
174
333
  function parseSetupArgs(args) {
175
334
  let printOnly = false;
176
335
  let yes = false;
177
336
  let shell = "auto";
178
337
  let service = false;
338
+ let agents = false;
339
+ let guardCommit = false;
179
340
  for (let index = 0; index < args.length; index += 1) {
180
341
  const arg = args[index];
181
342
  if (arg === "--print") {
@@ -187,6 +348,12 @@ function parseSetupArgs(args) {
187
348
  else if (arg === "--service") {
188
349
  service = true;
189
350
  }
351
+ else if (arg === "--agents") {
352
+ agents = true;
353
+ }
354
+ else if (arg === "--guard-commit") {
355
+ guardCommit = true;
356
+ }
190
357
  else if (arg === "--python-hook" || arg?.startsWith("--python-hook=")) {
191
358
  return {
192
359
  exitCode: EXIT_UNAVAILABLE,
@@ -251,7 +418,9 @@ function parseSetupArgs(args) {
251
418
  printOnly,
252
419
  yes,
253
420
  shell,
254
- service
421
+ service,
422
+ agents,
423
+ guardCommit
255
424
  };
256
425
  }
257
426
  function parseShell(value) {
@@ -270,10 +439,11 @@ function serviceSetupHandler(parsed) {
270
439
  };
271
440
  }
272
441
  if (!authStatus().authenticated) {
442
+ const theme = createTheme(resolvePresentation().color);
273
443
  return {
274
444
  exitCode: EXIT_UNAVAILABLE,
275
445
  stdout: "",
276
- stderr: "dg setup --service is a Pro or Team feature. Run 'dg login' to connect a paid account, then 'dg setup --service'. See https://westbayberry.com/pricing\n"
446
+ stderr: `${theme.paint("warn", "dg setup --service is a Pro or Team feature")} — ${theme.paint("accent", "dg login")} ${theme.paint("muted", "with a paid account, then re-run")} ${theme.paint("muted", westbayberry.com/pricing")}\n`
277
447
  };
278
448
  }
279
449
  if (!parsed.yes) {
@@ -3,6 +3,7 @@ import { envAuthToken } from "../auth/env-token.js";
3
3
  import { fetchAccountStatus } from "../auth/device-login.js";
4
4
  import { formatUsage } from "../scan-ui/format-helpers.js";
5
5
  import { loadUserConfig } from "../config/settings.js";
6
+ import { describeCooldownSettings } from "../policy/cooldown.js";
6
7
  import { resolvePresentation } from "../presentation/mode.js";
7
8
  import { createTheme } from "../presentation/theme.js";
8
9
  import { currentShellActivation, doctorReport } from "../setup/plan.js";
@@ -49,14 +50,15 @@ async function buildStatusReport(io) {
49
50
  const checks = doctorReport().checks;
50
51
  const checkPassed = (name) => checks.find((check) => check.name === name)?.status === "pass";
51
52
  const auth = safeAuthStatus();
52
- const policy = loadUserConfig().policy;
53
+ const config = loadUserConfig();
53
54
  return {
54
55
  account: auth,
55
56
  usage: await fetchStatusUsage(auth.connected, io),
56
57
  protection: { shims: checkPassed("shims"), path: checkPassed("path"), configured: checkPassed("shell-rc") },
57
58
  reloadHint: currentShellActivation(),
58
59
  commitGuard: safeCommitGuard(),
59
- policy: { mode: policy.mode, trustProjectAllowlists: policy.trustProjectAllowlists }
60
+ policy: { mode: config.policy.mode, trustProjectAllowlists: config.policy.trustProjectAllowlists },
61
+ cooldown: describeCooldownSettings(config, process.env)
60
62
  };
61
63
  }
62
64
  async function fetchStatusUsage(connected, io) {
@@ -123,6 +125,7 @@ function renderStatus(report, theme) {
123
125
  lines.push(` Commit guard ${guard}`);
124
126
  }
125
127
  lines.push(` Policy ${report.policy.mode} mode; project allowlists ${report.policy.trustProjectAllowlists ? "trusted" : "untrusted"}`);
128
+ lines.push(` Cooldown ${report.cooldown === "off" ? "off — new releases install immediately" : `${report.cooldown} release-age gate on new installs`}`);
126
129
  lines.push("");
127
130
  lines.push(`Full diagnostics: ${theme.paint("accent", "dg doctor")}`);
128
131
  return `${lines.join("\n")}\n`;
@@ -1,5 +1,6 @@
1
1
  export const EXIT_USAGE = 2;
2
2
  export const EXIT_USAGE_VERDICT = 64;
3
+ export const EXIT_ANALYSIS_INCOMPLETE = 4;
3
4
  export const EXIT_NOTHING_TO_SCAN = 10;
4
5
  export const EXIT_UNAVAILABLE = 69;
5
6
  export const EXIT_TOOL_ERROR = 70;
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { npmExecutable } from "../publish-set/npm.js";
2
+ import { toolInvocation } from "../util/external-tool.js";
3
3
  import { EXIT_USAGE } from "./types.js";
4
4
  import { dgVersion } from "./version.js";
5
5
  const PACKAGE_NAME = "@westbayberry/dg";
6
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/u;
6
7
  export const updateCommand = {
7
8
  name: "update",
8
9
  summary: "Check for dg CLI updates.",
@@ -26,7 +27,7 @@ function runUpdateCommand(args, commandName) {
26
27
  if (parsed.format === "json") {
27
28
  return {
28
29
  exitCode: report.status === "unknown" ? 1 : 0,
29
- stdout: `${JSON.stringify(report, null, 2)}\n`,
30
+ stdout: `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n`,
30
31
  stderr: ""
31
32
  };
32
33
  }
@@ -62,17 +63,22 @@ function parseUpdateArgs(args) {
62
63
  };
63
64
  }
64
65
  export function readLatestVersion(timeoutMs = 5000) {
65
- const injected = process.env.DG_UPDATE_LATEST_VERSION;
66
+ const injected = process.env.NODE_ENV === "test" ? process.env.DG_UPDATE_LATEST_VERSION : undefined;
66
67
  if (injected) {
67
- return injected;
68
+ return validVersion(injected);
68
69
  }
69
- const result = spawnSync(npmExecutable(), ["view", PACKAGE_NAME, "version", "--json"], {
70
+ const invocation = toolInvocation("npm", ["view", PACKAGE_NAME, "version", "--json"]);
71
+ if (!invocation) {
72
+ return null;
73
+ }
74
+ const result = spawnSync(invocation.command, [...invocation.args], {
70
75
  encoding: "utf8",
71
76
  env: {
72
77
  ...process.env,
73
78
  npm_config_ignore_scripts: "true"
74
79
  },
75
- timeout: timeoutMs
80
+ timeout: timeoutMs,
81
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
76
82
  });
77
83
  if (result.status !== 0) {
78
84
  return null;
@@ -83,12 +89,15 @@ export function readLatestVersion(timeoutMs = 5000) {
83
89
  }
84
90
  try {
85
91
  const parsed = JSON.parse(raw);
86
- return typeof parsed === "string" ? parsed : null;
92
+ return typeof parsed === "string" ? validVersion(parsed) : null;
87
93
  }
88
94
  catch {
89
- return raw.replace(/^"|"$/gu, "");
95
+ return validVersion(raw.replace(/^"|"$/gu, ""));
90
96
  }
91
97
  }
98
+ function validVersion(value) {
99
+ return VERSION_PATTERN.test(value) ? value : null;
100
+ }
92
101
  function buildUpdateReport(latestVersion) {
93
102
  if (!latestVersion) {
94
103
  return {
@@ -1,4 +1,4 @@
1
- import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
1
+ import { EXIT_ANALYSIS_INCOMPLETE, EXIT_UNAVAILABLE, EXIT_USAGE_VERDICT } from "./types.js";
2
2
  import { writeFileSync } from "node:fs";
3
3
  import { existsSync } from "node:fs";
4
4
  import { resolve } from "node:path";
@@ -22,14 +22,14 @@ export const verifyCommand = {
22
22
  ],
23
23
  examples: ["dg verify npm:react", "dg verify pypi:requests@2.31.0", "dg verify ./pkg.tgz --verbose", "dg verify package-lock.json --json"],
24
24
  details: [
25
- "dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it. This is a Pro/Team feature and requires dg login.",
25
+ "dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it. Signed-out runs show the verdict and top reasons; dg login unlocks full findings, license info, --json, and --output.",
26
26
  "Local paths, workspaces, tgz/zip/wheel artifacts, and lockfiles are verified offline as advisory preflight (free); proxy enforcement remains authoritative for network artifact fetches."
27
27
  ],
28
28
  handler: (context) => {
29
29
  const parsed = parseVerifyArgs(context.args);
30
30
  if ("error" in parsed) {
31
31
  return {
32
- exitCode: EXIT_USAGE,
32
+ exitCode: EXIT_USAGE_VERDICT,
33
33
  stdout: "",
34
34
  stderr: `dg verify: ${parsed.error}. Usage: dg verify <spec|path|lockfile> [--verbose] [--json|--sarif] [--output <path>]\n`
35
35
  };
@@ -52,7 +52,7 @@ export const verifyCommand = {
52
52
  }
53
53
  catch (error) {
54
54
  return {
55
- exitCode: 1,
55
+ exitCode: EXIT_ANALYSIS_INCOMPLETE,
56
56
  stdout: "",
57
57
  stderr: `dg verify could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
58
58
  };
@@ -147,5 +147,8 @@ function exitCodeForReport(report) {
147
147
  if (report.status === "block") {
148
148
  return 2;
149
149
  }
150
- return report.status === "warn" || report.status === "error" ? 1 : 0;
150
+ if (report.status === "error") {
151
+ return EXIT_ANALYSIS_INCOMPLETE;
152
+ }
153
+ return report.status === "warn" ? 1 : 0;
151
154
  }