@westbayberry/dg 2.1.0 → 2.3.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 (148) hide show
  1. package/README.md +216 -226
  2. package/dist/agents/claude-code.js +123 -0
  3. package/dist/agents/codex.js +65 -0
  4. package/dist/agents/copilot-cli.js +115 -0
  5. package/dist/agents/cursor.js +118 -0
  6. package/dist/agents/gate-posture.js +21 -0
  7. package/dist/agents/gemini.js +107 -0
  8. package/dist/agents/persistence.js +351 -0
  9. package/dist/agents/registry.js +128 -0
  10. package/dist/agents/routing.js +118 -0
  11. package/dist/agents/types.js +1 -0
  12. package/dist/agents/windsurf.js +93 -0
  13. package/dist/api/analyze.js +1 -1
  14. package/dist/audit/detectors.js +0 -11
  15. package/dist/audit/events.js +5 -21
  16. package/dist/audit/rules.js +6 -2
  17. package/dist/audit-ui/AuditApp.js +2 -0
  18. package/dist/audit-ui/components/AuditResultsView.js +55 -92
  19. package/dist/audit-ui/export.js +0 -4
  20. package/dist/audit-ui/format.js +0 -3
  21. package/dist/audit-ui/launch.js +10 -1
  22. package/dist/auth/device-login.js +4 -5
  23. package/dist/auth/login-app.js +7 -7
  24. package/dist/auth/store.js +17 -5
  25. package/dist/bin/dg.js +59 -52
  26. package/dist/commands/agents.js +275 -0
  27. package/dist/commands/audit.js +28 -3
  28. package/dist/commands/config.js +15 -7
  29. package/dist/commands/cooldown.js +389 -0
  30. package/dist/commands/decisions.js +23 -9
  31. package/dist/commands/doctor.js +1 -1
  32. package/dist/commands/help.js +1 -1
  33. package/dist/commands/licenses.js +10 -22
  34. package/dist/commands/logout.js +4 -11
  35. package/dist/commands/router.js +6 -4
  36. package/dist/commands/sbom.js +206 -0
  37. package/dist/commands/scan.js +1 -1
  38. package/dist/commands/service.js +32 -13
  39. package/dist/commands/setup.js +196 -27
  40. package/dist/commands/types.js +1 -0
  41. package/dist/commands/uninstall.js +2 -1
  42. package/dist/commands/update.js +17 -8
  43. package/dist/commands/verify.js +8 -5
  44. package/dist/config/settings.js +49 -48
  45. package/dist/decisions/remember-prompt.js +5 -10
  46. package/dist/export-ui/ExportDialog.js +198 -0
  47. package/dist/install-ui/LiveInstall.js +5 -4
  48. package/dist/install-ui/block-render.js +18 -14
  49. package/dist/install-ui/prep-spinner.js +32 -0
  50. package/dist/launcher/agent-check.js +896 -0
  51. package/dist/launcher/agent-hook-exec.js +70 -0
  52. package/dist/launcher/agent-hook-io.js +39 -0
  53. package/dist/launcher/cargo-cache.js +40 -0
  54. package/dist/launcher/classify.js +44 -7
  55. package/dist/launcher/env.js +105 -26
  56. package/dist/launcher/install-preflight.js +64 -18
  57. package/dist/launcher/live-install.js +29 -8
  58. package/dist/launcher/manifest-screen.js +171 -0
  59. package/dist/launcher/output-redaction.js +10 -4
  60. package/dist/launcher/preflight-prompt.js +18 -4
  61. package/dist/launcher/run.js +178 -103
  62. package/dist/launcher/spawn-invocation.js +21 -0
  63. package/dist/policy/cooldown.js +18 -5
  64. package/dist/policy/evaluate.js +5 -6
  65. package/dist/policy/pypi-name.js +17 -0
  66. package/dist/presentation/mode.js +3 -2
  67. package/dist/presentation/package-page.js +9 -0
  68. package/dist/presentation/theme.js +7 -7
  69. package/dist/project/dgfile.js +146 -6
  70. package/dist/project/override-trust.js +0 -0
  71. package/dist/proxy/auth.js +42 -0
  72. package/dist/proxy/ca.js +29 -9
  73. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  74. package/dist/proxy/enforcement.js +55 -16
  75. package/dist/proxy/metadata-map.js +66 -12
  76. package/dist/proxy/preverified.js +55 -0
  77. package/dist/proxy/server.js +568 -53
  78. package/dist/proxy/worker.js +16 -1
  79. package/dist/publish-set/collect.js +1 -4
  80. package/dist/publish-set/npm.js +8 -5
  81. package/dist/publish-set/pack.js +9 -3
  82. package/dist/runtime/cli.js +0 -4
  83. package/dist/runtime/fatal.js +31 -0
  84. package/dist/runtime/first-run.js +14 -12
  85. package/dist/runtime/node-version.js +43 -6
  86. package/dist/runtime/nudges.js +35 -2
  87. package/dist/sbom/cyclonedx.js +211 -0
  88. package/dist/sbom-ui/SbomApp.js +158 -0
  89. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  90. package/dist/sbom-ui/components/SbomList.js +52 -0
  91. package/dist/sbom-ui/inventory.js +132 -0
  92. package/dist/sbom-ui/launch.js +51 -0
  93. package/dist/sbom-ui/run.js +55 -0
  94. package/dist/sbom-ui/store.js +26 -0
  95. package/dist/scan/collect.js +10 -6
  96. package/dist/scan/command.js +17 -10
  97. package/dist/scan/discovery.js +11 -2
  98. package/dist/scan/render.js +28 -4
  99. package/dist/scan/scanner-report.js +15 -9
  100. package/dist/scan/staged.js +33 -9
  101. package/dist/scan-ui/LegacyApp.js +9 -13
  102. package/dist/scan-ui/alt-screen.js +5 -8
  103. package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
  104. package/dist/scan-ui/components/ProgressBar.js +3 -14
  105. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  106. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  107. package/dist/scan-ui/components/SetupBanner.js +0 -6
  108. package/dist/scan-ui/format-helpers.js +61 -5
  109. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  110. package/dist/scan-ui/hooks/useScan.js +19 -2
  111. package/dist/scan-ui/launch.js +7 -4
  112. package/dist/scan-ui/shims.js +11 -4
  113. package/dist/scripts/detect.js +7 -2
  114. package/dist/scripts/gate.js +1 -1
  115. package/dist/security/sanitize.js +8 -4
  116. package/dist/service/state.js +28 -9
  117. package/dist/service/trust-refresh.js +92 -0
  118. package/dist/service/trust-store.js +5 -9
  119. package/dist/service/worker.js +23 -1
  120. package/dist/setup/activate-shell.js +28 -0
  121. package/dist/setup/git-hook.js +49 -4
  122. package/dist/setup/plan.js +227 -49
  123. package/dist/setup/uninstall-standalone.js +25 -0
  124. package/dist/setup-ui/gate.js +39 -0
  125. package/dist/setup-ui/offer.js +42 -0
  126. package/dist/setup-ui/selector.js +27 -0
  127. package/dist/setup-ui/tasks.js +56 -0
  128. package/dist/setup-ui/wizard.js +233 -0
  129. package/dist/standalone/uninstall.mjs +2123 -0
  130. package/dist/state/cooldown-held.js +66 -0
  131. package/dist/state/index.js +1 -0
  132. package/dist/state/locks.js +4 -2
  133. package/dist/state/store.js +2 -1
  134. package/dist/util/external-tool.js +25 -0
  135. package/dist/util/git.js +10 -3
  136. package/dist/util/report-writer.js +57 -0
  137. package/dist/verify/local.js +242 -44
  138. package/dist/verify/package-check.js +87 -29
  139. package/dist/verify/preflight.js +242 -49
  140. package/dist/verify/render.js +15 -1
  141. package/npm-shrinkwrap.json +2383 -0
  142. package/package.json +7 -8
  143. package/NOTICE +0 -5
  144. package/dist/commands/completion.js +0 -117
  145. package/dist/commands/explain.js +0 -236
  146. package/dist/commands/unavailable.js +0 -11
  147. package/dist/scripts/rebuild.js +0 -28
  148. package/dist/telemetry/events.js +0 -40
@@ -3,46 +3,77 @@ import { existsSync, writeFileSync } from "node:fs";
3
3
  import { constants as osConstants } from "node:os";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { EXIT_UNAVAILABLE } from "../commands/types.js";
6
- import { loadUserConfig } from "../config/settings.js";
6
+ import { loadUserConfig, trustsProjectOverrides } from "../config/settings.js";
7
+ import { findProjectRoot, loadDgFile } from "../project/dgfile.js";
8
+ import { honoredOverrides } from "../project/override-trust.js";
9
+ import { writeCooldownExemptionsFile } from "../proxy/cooldown-exemptions-file.js";
10
+ import { writePreverifiedFile } from "../proxy/preverified.js";
7
11
  import { describeBlockedInstall, describeFlaggedWarn, renderInstallDecision } from "../install-ui/block-render.js";
8
12
  import { enforceProtectedInstall } from "../proxy/enforcement.js";
9
13
  import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
14
+ import { maybeSetupNudge } from "../runtime/nudges.js";
10
15
  import { createTheme } from "../presentation/theme.js";
11
16
  import { readProxySessionState } from "../proxy/server.js";
17
+ import { readServiceState } from "../service/state.js";
12
18
  import { cleanupSessionSync, createSessionSync, resolveDgPaths } from "../state/index.js";
13
19
  import { classifyPackageManagerInvocation } from "./classify.js";
14
- import { buildProxyChildEnv } from "./env.js";
20
+ import { buildProxyChildEnv, scrubChildSecrets } from "./env.js";
21
+ import { prepareCargoHome, userCargoHome } from "./cargo-cache.js";
15
22
  import { cachedPipResolution } from "./install-preflight.js";
16
- import { parsePipReportInstallCount } from "./pip-report.js";
17
23
  import { createStreamRedactor, redactSecrets } from "./output-redaction.js";
24
+ import { resolveSpawnInvocation } from "./spawn-invocation.js";
18
25
  import { resolveRealBinary } from "./resolve-real-binary.js";
19
- import { runScriptGateAfterInstall } from "../scripts/gate.js";
26
+ import { runScriptGateAfterInstall, scriptGateChildEnv, scriptGateInstallArgs } from "../scripts/gate.js";
20
27
  export const EXIT_INSTALL_BLOCKED = 2;
21
- const CMD_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
22
- const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
23
- export function resolveSpawnInvocation(binary, args, platform = process.platform) {
24
- if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
25
- return { command: binary, args, windowsVerbatimArguments: false };
26
- }
27
- const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
28
- return {
29
- command: process.env.comspec ?? "cmd.exe",
30
- args: ["/d", "/s", "/c", `"${commandLine}"`],
31
- windowsVerbatimArguments: true
32
- };
33
- }
34
- function escapeCmdCommand(command) {
35
- return command.replace(CMD_META_CHARS, "^$1");
36
- }
37
- // cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
38
- function escapeCmdArgument(argument) {
39
- const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
40
- return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
41
- }
28
+ export { resolveSpawnInvocation } from "./spawn-invocation.js";
42
29
  export function shimDepth(env) {
43
30
  const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
44
31
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
45
32
  }
33
+ // A genuine parent dg proxy sets DG_PROXY_ACTIVE=1 and routes the child through a
34
+ // loopback proxy URL that carries the proxy's auth token (buildProxyChildEnv ->
35
+ // proxyUrlWithAuth). All of these must hold for a nested invocation to safely run
36
+ // the real package manager directly (its traffic still flows through the live
37
+ // parent proxy). DG_PROXY_ACTIVE + a bare loopback URL are both forgeable, so:
38
+ // - the URL must carry a dg auth token (a forged HTTPS_PROXY=http://127.0.0.1:1
39
+ // with no credential is rejected), and
40
+ // - when a persistent dg service is the parent (the agent-routing case), the URL
41
+ // must match that live service's proxy, so a forged token cannot impersonate it.
42
+ // If neither matches, we start our own verifying proxy rather than trust the env.
43
+ export function inheritedDgProxyActive(env) {
44
+ if (env.DG_PROXY_ACTIVE !== "1") {
45
+ return false;
46
+ }
47
+ const proxyUrl = env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy;
48
+ if (!proxyUrl) {
49
+ return false;
50
+ }
51
+ let url;
52
+ try {
53
+ url = new URL(proxyUrl);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ const host = url.hostname.replace(/^\[(.*)\]$/, "$1");
59
+ if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
60
+ return false;
61
+ }
62
+ if (!url.username && !url.password) {
63
+ return false;
64
+ }
65
+ try {
66
+ const { state } = readServiceState(env);
67
+ if (state.running && state.proxy) {
68
+ const live = new URL(state.proxy.proxyUrl);
69
+ return live.hostname.replace(/^\[(.*)\]$/, "$1") === host && live.port === url.port;
70
+ }
71
+ }
72
+ catch {
73
+ // No readable service state — fall through to the session-proxy path below.
74
+ }
75
+ return true;
76
+ }
46
77
  export function rootUnprotectedNotice(env, uid = process.getuid?.()) {
47
78
  if (uid !== 0) {
48
79
  return "";
@@ -74,11 +105,11 @@ export function createLaunchPlan(manager, args, env = process.env) {
74
105
  classification,
75
106
  realBinary,
76
107
  startsProxy: classification.kind === "protected",
77
- childEnv: {
108
+ childEnv: scrubChildSecrets({
78
109
  ...env,
79
110
  DG_SHIM_ACTIVE: shimNonce(manager, env),
80
111
  DG_SHIM_DEPTH: String(shimDepth(env) + 1)
81
- }
112
+ })
82
113
  };
83
114
  }
84
115
  export async function runPackageManager(manager, args, options = {}) {
@@ -99,15 +130,20 @@ export async function runPackageManager(manager, args, options = {}) {
99
130
  stderr: `dg: ${manager} shim exec loop detected (DG_SHIM_DEPTH=${depth}) — refusing to re-enter\n`
100
131
  };
101
132
  }
102
- if (depth === 1 && plan.startsProxy) {
103
- const child = await spawnPackageManager(plan, args, options);
104
- return {
105
- exitCode: child.exitCode,
106
- stdout: streamedOut(child.stdout, options),
107
- stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
108
- };
109
- }
110
133
  if (plan.startsProxy) {
134
+ // Reuse a parent dg proxy ONLY when one is genuinely live in this environment
135
+ // (the parent set DG_PROXY_ACTIVE and a loopback proxy URL). An env var such as
136
+ // a forged or stale DG_SHIM_DEPTH must never be what decides to skip
137
+ // verification: if no live dg proxy is detected, start one rather than run the
138
+ // install unproxied. DG_SHIM_DEPTH is only the >=2 infinite-recursion backstop.
139
+ if (inheritedDgProxyActive(options.env ?? process.env)) {
140
+ const child = await spawnPackageManager(plan, args, options);
141
+ return {
142
+ exitCode: child.exitCode,
143
+ stdout: streamedOut(child.stdout, options),
144
+ stderr: `dg: re-entered through its own shim — running the real ${plan.classification.realBinaryName} directly\n${streamedErr(child.stderr, options)}`
145
+ };
146
+ }
111
147
  if (!options.proxyVerdict) {
112
148
  return runWithProductionProxy(plan, args, options);
113
149
  }
@@ -130,7 +166,7 @@ export async function runPackageManager(manager, args, options = {}) {
130
166
  return {
131
167
  exitCode: child.exitCode,
132
168
  stdout: streamedOut(child.stdout, options),
133
- stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}`
169
+ stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
134
170
  };
135
171
  }
136
172
  const child = await spawnPackageManager(plan, args, options);
@@ -152,6 +188,7 @@ async function runWithProductionProxy(plan, args, options) {
152
188
  }
153
189
  const restoreSignalHandlers = installProxySignalHandlers(proxy);
154
190
  try {
191
+ const cacheDir = prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env);
155
192
  const proxiedPlan = {
156
193
  ...plan,
157
194
  childEnv: buildProxyChildEnv({
@@ -159,7 +196,7 @@ async function runWithProductionProxy(plan, args, options) {
159
196
  baseEnv: plan.childEnv,
160
197
  proxyUrl: proxy.proxyUrl,
161
198
  caBundlePath: proxy.session.files.ca,
162
- cacheDir: `${proxy.session.dir}/pm-cache`
199
+ cacheDir
163
200
  })
164
201
  };
165
202
  const child = await spawnPackageManager(proxiedPlan, args, options);
@@ -196,7 +233,7 @@ async function runWithProductionProxy(plan, args, options) {
196
233
  return {
197
234
  exitCode: child.exitCode,
198
235
  stdout: streamedOut(child.stdout, options),
199
- stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}`
236
+ stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
200
237
  };
201
238
  }
202
239
  finally {
@@ -214,6 +251,12 @@ function scriptGateLine(plan, exitCode, options) {
214
251
  ...(options.projectDir ? { projectDir: options.projectDir } : {})
215
252
  });
216
253
  }
254
+ function setupNudgeAfterInstall(plan, exitCode, options) {
255
+ if (exitCode !== 0) {
256
+ return "";
257
+ }
258
+ return maybeSetupNudge(plan.classification.manager, { env: options.env ?? process.env });
259
+ }
217
260
  export function deriveLiveView(state, phase, resolvedTotal) {
218
261
  const verified = state.decisions.filter((decision) => decision.action === "pass").length;
219
262
  const warnDecisions = state.decisions.filter((decision) => decision.action === "warn");
@@ -232,57 +275,43 @@ export function deriveLiveView(state, phase, resolvedTotal) {
232
275
  ...(blocked ? { blocked: describeBlockedInstall(blocked) } : {})
233
276
  };
234
277
  }
235
- function resolvePipInstallTotal(binary, args, env, register) {
236
- if (!binary)
237
- return Promise.resolve(undefined);
238
- return new Promise((resolve) => {
239
- let stdout = "";
240
- let settled = false;
241
- let timer;
242
- const finish = (value) => {
243
- if (settled)
278
+ export function prepareProxyWorker(classification, env) {
279
+ const worker = startProxyWorker(classification, env, undefined);
280
+ let discarded = false;
281
+ return {
282
+ worker,
283
+ discard: () => {
284
+ if (discarded) {
244
285
  return;
245
- settled = true;
246
- if (timer)
247
- clearTimeout(timer);
248
- resolve(value);
249
- };
250
- let child;
251
- try {
252
- const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
253
- child = spawn(invocation.command, [...invocation.args], {
254
- env,
255
- stdio: ["ignore", "pipe", "ignore"],
256
- windowsVerbatimArguments: invocation.windowsVerbatimArguments
286
+ }
287
+ discarded = true;
288
+ void worker.then((proxy) => {
289
+ if (!("decision" in proxy)) {
290
+ stopProxyWorker(proxy);
291
+ }
257
292
  });
258
293
  }
259
- catch {
260
- finish(undefined);
261
- return;
262
- }
263
- register(child);
264
- timer = setTimeout(() => {
265
- try {
266
- child.kill();
267
- }
268
- catch { /* already exited */ }
269
- finish(undefined);
270
- }, 30_000);
271
- child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
272
- child.on("error", () => finish(undefined));
273
- child.on("close", (code) => finish(code === 0 ? parsePipReportInstallCount(stdout) : undefined));
274
- });
294
+ };
275
295
  }
276
- export async function runWithProductionProxyLive(plan, args, options, onView) {
296
+ export async function runWithProductionProxyLive(plan, args, options, onView, prepared) {
277
297
  if (options.onStdout || options.onStderr) {
278
298
  throw new Error("live install mode renders its own UI and owns the terminal; streaming output callbacks are not supported");
279
299
  }
280
300
  const env = options.env ?? process.env;
281
301
  maybeWarnRootWithoutState(env);
282
- const proxy = await startProxyWorker(plan.classification, env, options.forceOverride);
302
+ if (prepared && options.forceOverride) {
303
+ prepared.discard();
304
+ prepared = undefined;
305
+ }
306
+ const proxy = prepared
307
+ ? await prepared.worker
308
+ : await startProxyWorker(plan.classification, env, options.forceOverride);
283
309
  if ("decision" in proxy) {
284
310
  return { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: redactSecrets(renderInstallDecision(proxy.decision)) };
285
311
  }
312
+ if (options.preverified && options.preverified.length > 0) {
313
+ writePreverifiedFile(proxy.session.dir, options.preverified);
314
+ }
286
315
  const restoreSignalHandlers = installProxySignalHandlers(proxy);
287
316
  try {
288
317
  const childEnv = buildProxyChildEnv({
@@ -290,22 +319,11 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
290
319
  baseEnv: plan.childEnv,
291
320
  proxyUrl: proxy.proxyUrl,
292
321
  caBundlePath: proxy.session.files.ca,
293
- cacheDir: `${proxy.session.dir}/pm-cache`
322
+ cacheDir: prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env)
294
323
  });
295
324
  const spawner = options.spawner ?? defaultSpawner;
296
- let resolvedTotal;
297
- let dryRunChild;
298
- if (plan.classification.manager === "pip") {
299
- const cached = cachedPipResolution(plan.realBinary.path ?? "", args);
300
- if (cached) {
301
- resolvedTotal = cached.count;
302
- }
303
- else {
304
- void resolvePipInstallTotal(plan.realBinary.path ?? "", args, childEnv, (child) => { dryRunChild = child; })
305
- .then((count) => { resolvedTotal = count; })
306
- .catch(() => undefined);
307
- }
308
- }
325
+ const hardened = applyScriptGateHardening(plan, args, childEnv, env);
326
+ const resolvedTotal = plan.classification.manager === "pip" ? cachedPipResolution(plan.realBinary.path ?? "", args)?.count : undefined;
309
327
  const poll = setInterval(() => {
310
328
  onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
311
329
  }, 90);
@@ -313,18 +331,12 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
313
331
  try {
314
332
  finished = await spawner({
315
333
  binary: plan.realBinary.path ?? "",
316
- args,
317
- env: childEnv
334
+ args: hardened.args,
335
+ env: hardened.env
318
336
  });
319
337
  }
320
338
  finally {
321
339
  clearInterval(poll);
322
- if (dryRunChild) {
323
- try {
324
- dryRunChild.kill();
325
- }
326
- catch { /* already exited */ }
327
- }
328
340
  }
329
341
  const proxyState = readProxySessionState(proxy.session);
330
342
  onView(deriveLiveView(proxyState, "done", resolvedTotal));
@@ -342,13 +354,20 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
342
354
  const outcome = installOutcome(finished.stdout);
343
355
  return { exitCode: EXIT_INSTALL_BLOCKED, stdout: outcome, stderr: cacheOnlyNotice(outcome.length > 0) };
344
356
  }
345
- return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: scriptGateLine(plan, finished.exitCode, options) };
357
+ return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: `${scriptGateLine(plan, finished.exitCode, options)}${setupNudgeAfterInstall(plan, finished.exitCode, options)}` };
346
358
  }
347
359
  finally {
348
360
  restoreSignalHandlers();
349
361
  stopProxyWorker(proxy);
350
362
  }
351
363
  }
364
+ function prepareProxyCacheDir(manager, sessionDir, env) {
365
+ const cacheDir = `${sessionDir}/pm-cache`;
366
+ if (manager === "cargo") {
367
+ prepareCargoHome(cacheDir, userCargoHome(env));
368
+ }
369
+ return cacheDir;
370
+ }
352
371
  function cacheOnlyNotice(installedFresh) {
353
372
  const theme = createTheme(resolvePresentation().color);
354
373
  const reason = installedFresh ? "used a saved copy" : "already installed";
@@ -359,6 +378,22 @@ function installOutcome(stdout) {
359
378
  /^(added|changed|removed|updated) \d/.test(line.trim()));
360
379
  return lines.length > 0 ? `${lines.join("\n")}\n` : "";
361
380
  }
381
+ // scriptGate.mode === "enforce" is applied HERE, at the spawn boundary: it
382
+ // rewrites the install args (--ignore-scripts) and child env
383
+ // (npm_config_ignore_scripts) for npm/yarn so a reputation-clean package can't
384
+ // run a lifecycle script. observe/off leave the install untouched. Without this
385
+ // the mode setting was inert — the verdict screens what is fetched, this gates
386
+ // what the fetched package is allowed to execute.
387
+ function applyScriptGateHardening(plan, args, childEnv, configEnv) {
388
+ const mode = loadUserConfig(configEnv).scriptGate.mode;
389
+ const manager = plan.classification.manager;
390
+ const hardenedArgs = scriptGateInstallArgs({ mode, manager, args, env: childEnv });
391
+ const gateEnv = scriptGateChildEnv({ mode, manager, args, env: childEnv });
392
+ return {
393
+ args: hardenedArgs,
394
+ env: Object.keys(gateEnv).length > 0 ? { ...childEnv, ...gateEnv } : childEnv
395
+ };
396
+ }
362
397
  async function spawnPackageManager(plan, args, options) {
363
398
  if (!plan.realBinary.path) {
364
399
  return {
@@ -368,10 +403,11 @@ async function spawnPackageManager(plan, args, options) {
368
403
  };
369
404
  }
370
405
  const spawner = options.spawner ?? defaultSpawner;
406
+ const hardened = applyScriptGateHardening(plan, args, plan.childEnv, options.env ?? process.env);
371
407
  return spawner({
372
408
  binary: plan.realBinary.path,
373
- args,
374
- env: plan.childEnv,
409
+ args: hardened.args,
410
+ env: hardened.env,
375
411
  onStdout: options.onStdout,
376
412
  onStderr: options.onStderr
377
413
  });
@@ -436,6 +472,41 @@ function renderDecisions(decisions, env) {
436
472
  const visible = isCiEnv(env) ? decisions.filter((decision) => decision.action !== "pass") : decisions;
437
473
  return visible.map((decision) => redactSecrets(renderInstallDecision(decision))).join("");
438
474
  }
475
+ export function loadProjectCooldownExemptions(env, cwd = process.cwd()) {
476
+ try {
477
+ const root = findProjectRoot(cwd, env);
478
+ if (!root) {
479
+ return [];
480
+ }
481
+ const file = loadDgFile(root);
482
+ if (!file.readable) {
483
+ return [];
484
+ }
485
+ return honoredOverrides(file, root, env, trustsProjectOverrides(env)).exemptions;
486
+ }
487
+ catch {
488
+ return [];
489
+ }
490
+ }
491
+ export function dgFileExemptionsNotice(exemptions) {
492
+ if (exemptions.length === 0) {
493
+ return "";
494
+ }
495
+ const plural = exemptions.length === 1 ? "exemption" : "exemptions";
496
+ return `dg: applying ${exemptions.length} cooldown ${plural} from this project's dg.json\n`;
497
+ }
498
+ let dgFileExemptionNoticeWritten = false;
499
+ function maybeNoticeDgFileExemptions(exemptions) {
500
+ if (dgFileExemptionNoticeWritten) {
501
+ return;
502
+ }
503
+ const notice = dgFileExemptionsNotice(exemptions);
504
+ if (!notice) {
505
+ return;
506
+ }
507
+ dgFileExemptionNoticeWritten = true;
508
+ process.stderr.write(notice);
509
+ }
439
510
  async function startProxyWorker(classification, env, forceOverride) {
440
511
  const paths = resolveDgPaths(env);
441
512
  const session = createSessionSync(paths);
@@ -450,11 +521,15 @@ async function startProxyWorker(classification, env, forceOverride) {
450
521
  throw new Error("production proxy worker is not built");
451
522
  }
452
523
  const config = loadUserConfig(env);
524
+ const exemptions = loadProjectCooldownExemptions(env);
525
+ maybeNoticeDgFileExemptions(exemptions);
526
+ const exemptionsEnv = writeCooldownExemptionsFile(session.dir, exemptions);
453
527
  const child = spawn(process.execPath, [workerPath, sessionBootstrapPath, config.api.baseUrl], {
454
528
  env: {
455
529
  ...env,
456
530
  DG_PROXY_CLASSIFICATION: JSON.stringify(classification),
457
- ...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {})
531
+ ...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {}),
532
+ ...exemptionsEnv
458
533
  },
459
534
  stdio: ["pipe", "pipe", "pipe"]
460
535
  });
@@ -0,0 +1,21 @@
1
+ const CMD_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
2
+ const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
3
+ export function resolveSpawnInvocation(binary, args, platform = process.platform) {
4
+ if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
5
+ return { command: binary, args, windowsVerbatimArguments: false };
6
+ }
7
+ const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
8
+ return {
9
+ command: process.env.comspec ?? "cmd.exe",
10
+ args: ["/d", "/s", "/c", `"${commandLine}"`],
11
+ windowsVerbatimArguments: true
12
+ };
13
+ }
14
+ function escapeCmdCommand(command) {
15
+ return command.replace(CMD_META_CHARS, "^$1");
16
+ }
17
+ // cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
18
+ function escapeCmdArgument(argument) {
19
+ const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
20
+ return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
21
+ }
@@ -1,4 +1,7 @@
1
1
  import { parseCooldownAge } from "../config/settings.js";
2
+ import { cooldownExemptionActive } from "../project/dgfile.js";
3
+ import { canonicalCooldownName, normalizePypiName } from "./pypi-name.js";
4
+ export { normalizePypiName };
2
5
  export function durationToDays(value) {
3
6
  const trimmed = value.trim();
4
7
  if (trimmed === "" || trimmed === "0" || trimmed === "off") {
@@ -28,23 +31,33 @@ export function effectiveCooldownAge(config, env, ecosystem) {
28
31
  : config.cooldown.cargoAge;
29
32
  return perEcosystem !== "" ? perEcosystem : config.cooldown.age;
30
33
  }
31
- function normalizePypiName(name) {
32
- return name.toLowerCase().replace(/[-_.]+/g, "-");
33
- }
34
34
  export function isCooldownExempt(packageName, exempt, ecosystem) {
35
- const name = ecosystem === "pypi" ? normalizePypiName(packageName) : packageName;
35
+ const name = ecosystem ? canonicalCooldownName(ecosystem, packageName) : packageName;
36
36
  return exempt
37
37
  .split(",")
38
38
  .map((entry) => entry.trim())
39
39
  .filter(Boolean)
40
40
  .some((pattern) => {
41
- const candidate = ecosystem === "pypi" ? normalizePypiName(pattern) : pattern;
41
+ const candidate = ecosystem ? canonicalCooldownName(ecosystem, pattern) : pattern;
42
42
  if (candidate.endsWith("*")) {
43
43
  return name.startsWith(candidate.slice(0, -1));
44
44
  }
45
45
  return name === candidate;
46
46
  });
47
47
  }
48
+ export function isCooldownExemptByDgFile(packageName, ecosystem, exemptions, now = new Date()) {
49
+ if (ecosystem !== "npm" && ecosystem !== "pypi" && ecosystem !== "cargo") {
50
+ return false;
51
+ }
52
+ const target = canonicalCooldownName(ecosystem, packageName);
53
+ return exemptions.some((e) => {
54
+ if (e.ecosystem !== ecosystem) {
55
+ return false;
56
+ }
57
+ const candidate = canonicalCooldownName(ecosystem, e.name);
58
+ return candidate === target && cooldownExemptionActive(e, now);
59
+ });
60
+ }
48
61
  export function cooldownRequestParam(config, env, ecosystem, packageName) {
49
62
  const minAgeDays = durationToDays(effectiveCooldownAge(config, env, ecosystem));
50
63
  if (minAgeDays <= 0) {
@@ -1,4 +1,4 @@
1
- import { emitWebhookEvent, recordAuditEvent } from "../audit/events.js";
1
+ import { recordAuditEvent } from "../audit/events.js";
2
2
  import { DEFAULT_CONFIG } from "../config/settings.js";
3
3
  export function resolveEffectivePolicy(options) {
4
4
  if (options.orgPolicy) {
@@ -69,21 +69,21 @@ export function applyForceOverride(options, env = process.env) {
69
69
  return {
70
70
  allowed: false,
71
71
  reason: "force override was not requested",
72
- webhookQueued: false
72
+ auditRecorded: false
73
73
  };
74
74
  }
75
75
  if (options.currentAction !== "block") {
76
76
  return {
77
77
  allowed: false,
78
78
  reason: "force override is only valid for block decisions",
79
- webhookQueued: false
79
+ auditRecorded: false
80
80
  };
81
81
  }
82
82
  if (!options.policy.allowForceOverride) {
83
83
  return {
84
84
  allowed: false,
85
85
  reason: "force override is disabled by policy",
86
- webhookQueued: false
86
+ auditRecorded: false
87
87
  };
88
88
  }
89
89
  const event = {
@@ -93,11 +93,10 @@ export function applyForceOverride(options, env = process.env) {
93
93
  policyMode: options.policy.mode,
94
94
  createdAt: (options.now ?? new Date()).toISOString()
95
95
  };
96
- recordAuditEvent(event, env);
97
96
  return {
98
97
  allowed: true,
99
98
  reason: event.reason,
100
- webhookQueued: emitWebhookEvent(event, env)
99
+ auditRecorded: recordAuditEvent(event, env)
101
100
  };
102
101
  }
103
102
  function findTrustedAllowlist(packageName, policy, allowlists) {
@@ -0,0 +1,17 @@
1
+ export function normalizePypiName(name) {
2
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
3
+ }
4
+ // crates.io is case-insensitive and treats '-' and '_' as equivalent, so fold
5
+ // both to a single canonical form for exemption matching.
6
+ export function normalizeCargoName(name) {
7
+ return name.toLowerCase().replace(/_/g, "-");
8
+ }
9
+ export function canonicalCooldownName(ecosystem, name) {
10
+ if (ecosystem === "pypi") {
11
+ return normalizePypiName(name);
12
+ }
13
+ if (ecosystem === "cargo") {
14
+ return normalizeCargoName(name);
15
+ }
16
+ return name;
17
+ }
@@ -19,8 +19,9 @@ export function colorEnabled(input) {
19
19
  if (input.forceColorFlag) {
20
20
  return true;
21
21
  }
22
- if (truthyEnv(input.env.FORCE_COLOR)) {
23
- return true;
22
+ const forceColor = input.env.FORCE_COLOR;
23
+ if (forceColor !== undefined) {
24
+ return forceColor !== "0" && forceColor !== "false";
24
25
  }
25
26
  if (input.noColorFlag) {
26
27
  return false;
@@ -0,0 +1,9 @@
1
+ const SITE = "https://westbayberry.com";
2
+ // Only npm and pypi have public, indexable per-package pages today; cargo and
3
+ // other ecosystems have none, so return null rather than link to a 404.
4
+ export function packagePageUrl(ecosystem, name) {
5
+ if (ecosystem !== "npm" && ecosystem !== "pypi") {
6
+ return null;
7
+ }
8
+ return `${SITE}/${ecosystem}/${name}`;
9
+ }
@@ -1,12 +1,12 @@
1
1
  const ANSI = {
2
- block: "",
3
- warn: "",
4
- pass: "",
5
- unknown: "",
6
- muted: "",
7
- accent: ""
2
+ block: "\u001b[31m",
3
+ warn: "\u001b[33m",
4
+ pass: "\u001b[32m",
5
+ unknown: "\u001b[36m",
6
+ muted: "\u001b[2m",
7
+ accent: "\u001b[1m"
8
8
  };
9
- const RESET = "";
9
+ const RESET = "\u001b[0m";
10
10
  const BADGES = {
11
11
  block: { word: "BLOCK", glyph: "✘", role: "block" },
12
12
  warn: { word: "WARN", glyph: "⚠", role: "warn" },