@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
@@ -4,40 +4,26 @@ import { constants as osConstants } from "node:os";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { EXIT_UNAVAILABLE } from "../commands/types.js";
6
6
  import { loadUserConfig } from "../config/settings.js";
7
+ import { findProjectRoot, loadDgFile } from "../project/dgfile.js";
8
+ import { writeCooldownExemptionsFile } from "../proxy/cooldown-exemptions-file.js";
9
+ import { writePreverifiedFile } from "../proxy/preverified.js";
7
10
  import { describeBlockedInstall, describeFlaggedWarn, renderInstallDecision } from "../install-ui/block-render.js";
8
11
  import { enforceProtectedInstall } from "../proxy/enforcement.js";
9
12
  import { isCiEnv, resolvePresentation } from "../presentation/mode.js";
13
+ import { maybeSetupNudge } from "../runtime/nudges.js";
10
14
  import { createTheme } from "../presentation/theme.js";
11
15
  import { readProxySessionState } from "../proxy/server.js";
12
16
  import { cleanupSessionSync, createSessionSync, resolveDgPaths } from "../state/index.js";
13
17
  import { classifyPackageManagerInvocation } from "./classify.js";
14
18
  import { buildProxyChildEnv } from "./env.js";
19
+ import { prepareCargoHome, userCargoHome } from "./cargo-cache.js";
15
20
  import { cachedPipResolution } from "./install-preflight.js";
16
- import { parsePipReportInstallCount } from "./pip-report.js";
17
21
  import { createStreamRedactor, redactSecrets } from "./output-redaction.js";
22
+ import { resolveSpawnInvocation } from "./spawn-invocation.js";
18
23
  import { resolveRealBinary } from "./resolve-real-binary.js";
24
+ import { runScriptGateAfterInstall } from "../scripts/gate.js";
19
25
  export const EXIT_INSTALL_BLOCKED = 2;
20
- const CMD_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
21
- const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
22
- export function resolveSpawnInvocation(binary, args, platform = process.platform) {
23
- if (platform !== "win32" || !CMD_SCRIPT_PATTERN.test(binary)) {
24
- return { command: binary, args, windowsVerbatimArguments: false };
25
- }
26
- const commandLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArgument)].join(" ");
27
- return {
28
- command: process.env.comspec ?? "cmd.exe",
29
- args: ["/d", "/s", "/c", `"${commandLine}"`],
30
- windowsVerbatimArguments: true
31
- };
32
- }
33
- function escapeCmdCommand(command) {
34
- return command.replace(CMD_META_CHARS, "^$1");
35
- }
36
- // cmd shims parse their command line twice, hence the doubled meta-char escape (cross-spawn's algorithm)
37
- function escapeCmdArgument(argument) {
38
- const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
39
- return quoted.replace(CMD_META_CHARS, "^$1").replace(CMD_META_CHARS, "^$1");
40
- }
26
+ export { resolveSpawnInvocation } from "./spawn-invocation.js";
41
27
  export function shimDepth(env) {
42
28
  const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
43
29
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
@@ -129,7 +115,7 @@ export async function runPackageManager(manager, args, options = {}) {
129
115
  return {
130
116
  exitCode: child.exitCode,
131
117
  stdout: streamedOut(child.stdout, options),
132
- stderr: `${rendered}${streamedErr(child.stderr, options)}`
118
+ stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
133
119
  };
134
120
  }
135
121
  const child = await spawnPackageManager(plan, args, options);
@@ -151,6 +137,7 @@ async function runWithProductionProxy(plan, args, options) {
151
137
  }
152
138
  const restoreSignalHandlers = installProxySignalHandlers(proxy);
153
139
  try {
140
+ const cacheDir = prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env);
154
141
  const proxiedPlan = {
155
142
  ...plan,
156
143
  childEnv: buildProxyChildEnv({
@@ -158,7 +145,7 @@ async function runWithProductionProxy(plan, args, options) {
158
145
  baseEnv: plan.childEnv,
159
146
  proxyUrl: proxy.proxyUrl,
160
147
  caBundlePath: proxy.session.files.ca,
161
- cacheDir: `${proxy.session.dir}/pm-cache`
148
+ cacheDir
162
149
  })
163
150
  };
164
151
  const child = await spawnPackageManager(proxiedPlan, args, options);
@@ -195,7 +182,7 @@ async function runWithProductionProxy(plan, args, options) {
195
182
  return {
196
183
  exitCode: child.exitCode,
197
184
  stdout: streamedOut(child.stdout, options),
198
- stderr: `${rendered}${streamedErr(child.stderr, options)}`
185
+ stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
199
186
  };
200
187
  }
201
188
  finally {
@@ -203,6 +190,22 @@ async function runWithProductionProxy(plan, args, options) {
203
190
  stopProxyWorker(proxy);
204
191
  }
205
192
  }
193
+ function scriptGateLine(plan, exitCode, options) {
194
+ if (exitCode !== 0) {
195
+ return "";
196
+ }
197
+ return runScriptGateAfterInstall({
198
+ classification: plan.classification,
199
+ env: options.env ?? process.env,
200
+ ...(options.projectDir ? { projectDir: options.projectDir } : {})
201
+ });
202
+ }
203
+ function setupNudgeAfterInstall(plan, exitCode, options) {
204
+ if (exitCode !== 0) {
205
+ return "";
206
+ }
207
+ return maybeSetupNudge(plan.classification.manager, { env: options.env ?? process.env });
208
+ }
206
209
  export function deriveLiveView(state, phase, resolvedTotal) {
207
210
  const verified = state.decisions.filter((decision) => decision.action === "pass").length;
208
211
  const warnDecisions = state.decisions.filter((decision) => decision.action === "warn");
@@ -221,57 +224,43 @@ export function deriveLiveView(state, phase, resolvedTotal) {
221
224
  ...(blocked ? { blocked: describeBlockedInstall(blocked) } : {})
222
225
  };
223
226
  }
224
- function resolvePipInstallTotal(binary, args, env, register) {
225
- if (!binary)
226
- return Promise.resolve(undefined);
227
- return new Promise((resolve) => {
228
- let stdout = "";
229
- let settled = false;
230
- let timer;
231
- const finish = (value) => {
232
- if (settled)
227
+ export function prepareProxyWorker(classification, env) {
228
+ const worker = startProxyWorker(classification, env, undefined);
229
+ let discarded = false;
230
+ return {
231
+ worker,
232
+ discard: () => {
233
+ if (discarded) {
233
234
  return;
234
- settled = true;
235
- if (timer)
236
- clearTimeout(timer);
237
- resolve(value);
238
- };
239
- let child;
240
- try {
241
- const invocation = resolveSpawnInvocation(binary, [...args, "--dry-run", "--report", "-", "--quiet"]);
242
- child = spawn(invocation.command, [...invocation.args], {
243
- env,
244
- stdio: ["ignore", "pipe", "ignore"],
245
- windowsVerbatimArguments: invocation.windowsVerbatimArguments
235
+ }
236
+ discarded = true;
237
+ void worker.then((proxy) => {
238
+ if (!("decision" in proxy)) {
239
+ stopProxyWorker(proxy);
240
+ }
246
241
  });
247
242
  }
248
- catch {
249
- finish(undefined);
250
- return;
251
- }
252
- register(child);
253
- timer = setTimeout(() => {
254
- try {
255
- child.kill();
256
- }
257
- catch { /* already exited */ }
258
- finish(undefined);
259
- }, 30_000);
260
- child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
261
- child.on("error", () => finish(undefined));
262
- child.on("close", (code) => finish(code === 0 ? parsePipReportInstallCount(stdout) : undefined));
263
- });
243
+ };
264
244
  }
265
- export async function runWithProductionProxyLive(plan, args, options, onView) {
245
+ export async function runWithProductionProxyLive(plan, args, options, onView, prepared) {
266
246
  if (options.onStdout || options.onStderr) {
267
247
  throw new Error("live install mode renders its own UI and owns the terminal; streaming output callbacks are not supported");
268
248
  }
269
249
  const env = options.env ?? process.env;
270
250
  maybeWarnRootWithoutState(env);
271
- const proxy = await startProxyWorker(plan.classification, env, options.forceOverride);
251
+ if (prepared && options.forceOverride) {
252
+ prepared.discard();
253
+ prepared = undefined;
254
+ }
255
+ const proxy = prepared
256
+ ? await prepared.worker
257
+ : await startProxyWorker(plan.classification, env, options.forceOverride);
272
258
  if ("decision" in proxy) {
273
259
  return { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: redactSecrets(renderInstallDecision(proxy.decision)) };
274
260
  }
261
+ if (options.preverified && options.preverified.length > 0) {
262
+ writePreverifiedFile(proxy.session.dir, options.preverified);
263
+ }
275
264
  const restoreSignalHandlers = installProxySignalHandlers(proxy);
276
265
  try {
277
266
  const childEnv = buildProxyChildEnv({
@@ -279,22 +268,10 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
279
268
  baseEnv: plan.childEnv,
280
269
  proxyUrl: proxy.proxyUrl,
281
270
  caBundlePath: proxy.session.files.ca,
282
- cacheDir: `${proxy.session.dir}/pm-cache`
271
+ cacheDir: prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env)
283
272
  });
284
273
  const spawner = options.spawner ?? defaultSpawner;
285
- let resolvedTotal;
286
- let dryRunChild;
287
- if (plan.classification.manager === "pip") {
288
- const cached = cachedPipResolution(plan.realBinary.path ?? "", args);
289
- if (cached) {
290
- resolvedTotal = cached.count;
291
- }
292
- else {
293
- void resolvePipInstallTotal(plan.realBinary.path ?? "", args, childEnv, (child) => { dryRunChild = child; })
294
- .then((count) => { resolvedTotal = count; })
295
- .catch(() => undefined);
296
- }
297
- }
274
+ const resolvedTotal = plan.classification.manager === "pip" ? cachedPipResolution(plan.realBinary.path ?? "", args)?.count : undefined;
298
275
  const poll = setInterval(() => {
299
276
  onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
300
277
  }, 90);
@@ -308,12 +285,6 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
308
285
  }
309
286
  finally {
310
287
  clearInterval(poll);
311
- if (dryRunChild) {
312
- try {
313
- dryRunChild.kill();
314
- }
315
- catch { /* already exited */ }
316
- }
317
288
  }
318
289
  const proxyState = readProxySessionState(proxy.session);
319
290
  onView(deriveLiveView(proxyState, "done", resolvedTotal));
@@ -331,13 +302,20 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
331
302
  const outcome = installOutcome(finished.stdout);
332
303
  return { exitCode: EXIT_INSTALL_BLOCKED, stdout: outcome, stderr: cacheOnlyNotice(outcome.length > 0) };
333
304
  }
334
- return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: "" };
305
+ return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: `${scriptGateLine(plan, finished.exitCode, options)}${setupNudgeAfterInstall(plan, finished.exitCode, options)}` };
335
306
  }
336
307
  finally {
337
308
  restoreSignalHandlers();
338
309
  stopProxyWorker(proxy);
339
310
  }
340
311
  }
312
+ function prepareProxyCacheDir(manager, sessionDir, env) {
313
+ const cacheDir = `${sessionDir}/pm-cache`;
314
+ if (manager === "cargo") {
315
+ prepareCargoHome(cacheDir, userCargoHome(env));
316
+ }
317
+ return cacheDir;
318
+ }
341
319
  function cacheOnlyNotice(installedFresh) {
342
320
  const theme = createTheme(resolvePresentation().color);
343
321
  const reason = installedFresh ? "used a saved copy" : "already installed";
@@ -425,6 +403,38 @@ function renderDecisions(decisions, env) {
425
403
  const visible = isCiEnv(env) ? decisions.filter((decision) => decision.action !== "pass") : decisions;
426
404
  return visible.map((decision) => redactSecrets(renderInstallDecision(decision))).join("");
427
405
  }
406
+ export function loadProjectCooldownExemptions(env, cwd = process.cwd()) {
407
+ try {
408
+ const root = findProjectRoot(cwd, env);
409
+ if (!root) {
410
+ return [];
411
+ }
412
+ const file = loadDgFile(root);
413
+ return file.readable ? file.cooldownExemptions : [];
414
+ }
415
+ catch {
416
+ return [];
417
+ }
418
+ }
419
+ export function dgFileExemptionsNotice(exemptions) {
420
+ if (exemptions.length === 0) {
421
+ return "";
422
+ }
423
+ const plural = exemptions.length === 1 ? "exemption" : "exemptions";
424
+ return `dg: applying ${exemptions.length} cooldown ${plural} from this project's dg.json\n`;
425
+ }
426
+ let dgFileExemptionNoticeWritten = false;
427
+ function maybeNoticeDgFileExemptions(exemptions) {
428
+ if (dgFileExemptionNoticeWritten) {
429
+ return;
430
+ }
431
+ const notice = dgFileExemptionsNotice(exemptions);
432
+ if (!notice) {
433
+ return;
434
+ }
435
+ dgFileExemptionNoticeWritten = true;
436
+ process.stderr.write(notice);
437
+ }
428
438
  async function startProxyWorker(classification, env, forceOverride) {
429
439
  const paths = resolveDgPaths(env);
430
440
  const session = createSessionSync(paths);
@@ -439,11 +449,15 @@ async function startProxyWorker(classification, env, forceOverride) {
439
449
  throw new Error("production proxy worker is not built");
440
450
  }
441
451
  const config = loadUserConfig(env);
452
+ const exemptions = loadProjectCooldownExemptions(env);
453
+ maybeNoticeDgFileExemptions(exemptions);
454
+ const exemptionsEnv = writeCooldownExemptionsFile(session.dir, exemptions);
442
455
  const child = spawn(process.execPath, [workerPath, sessionBootstrapPath, config.api.baseUrl], {
443
456
  env: {
444
457
  ...env,
445
458
  DG_PROXY_CLASSIFICATION: JSON.stringify(classification),
446
- ...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {})
459
+ ...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {}),
460
+ ...exemptionsEnv
447
461
  },
448
462
  stdio: ["pipe", "pipe", "pipe"]
449
463
  });
@@ -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
+ }
@@ -0,0 +1,117 @@
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 };
5
+ export function durationToDays(value) {
6
+ const trimmed = value.trim();
7
+ if (trimmed === "" || trimmed === "0" || trimmed === "off") {
8
+ return 0;
9
+ }
10
+ const match = /^([1-9]\d{0,3})(h|d)$/.exec(trimmed);
11
+ if (!match || !match[1] || !match[2]) {
12
+ return 0;
13
+ }
14
+ const amount = Number.parseInt(match[1], 10);
15
+ return match[2] === "h" ? amount / 24 : amount;
16
+ }
17
+ export function effectiveCooldownAge(config, env, ecosystem) {
18
+ const fromEnv = env.DG_COOLDOWN_AGE;
19
+ if (fromEnv !== undefined) {
20
+ try {
21
+ return parseCooldownAge(fromEnv, "DG_COOLDOWN_AGE", false);
22
+ }
23
+ catch {
24
+ // fall through to config: a malformed env override must not change policy
25
+ }
26
+ }
27
+ const perEcosystem = ecosystem === "npm"
28
+ ? config.cooldown.npmAge
29
+ : ecosystem === "pypi"
30
+ ? config.cooldown.pypiAge
31
+ : config.cooldown.cargoAge;
32
+ return perEcosystem !== "" ? perEcosystem : config.cooldown.age;
33
+ }
34
+ export function isCooldownExempt(packageName, exempt, ecosystem) {
35
+ const name = ecosystem ? canonicalCooldownName(ecosystem, packageName) : packageName;
36
+ return exempt
37
+ .split(",")
38
+ .map((entry) => entry.trim())
39
+ .filter(Boolean)
40
+ .some((pattern) => {
41
+ const candidate = ecosystem ? canonicalCooldownName(ecosystem, pattern) : pattern;
42
+ if (candidate.endsWith("*")) {
43
+ return name.startsWith(candidate.slice(0, -1));
44
+ }
45
+ return name === candidate;
46
+ });
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
+ }
61
+ export function cooldownRequestParam(config, env, ecosystem, packageName) {
62
+ const minAgeDays = durationToDays(effectiveCooldownAge(config, env, ecosystem));
63
+ if (minAgeDays <= 0) {
64
+ return undefined;
65
+ }
66
+ if (packageName && isCooldownExempt(packageName, config.cooldown.exempt, ecosystem)) {
67
+ return undefined;
68
+ }
69
+ return { minAgeDays, onUnknown: config.cooldown.onUnknown };
70
+ }
71
+ export function formatCooldownDuration(days) {
72
+ const hours = days * 24;
73
+ if (hours <= 0) {
74
+ return "0h";
75
+ }
76
+ if (hours < 48) {
77
+ return `${Math.round(hours)}h`;
78
+ }
79
+ return `${Math.floor(days)}d`;
80
+ }
81
+ export function formatPackageAge(ageDays) {
82
+ if (ageDays < 0) {
83
+ return "in the future";
84
+ }
85
+ const hours = ageDays * 24;
86
+ if (hours < 1) {
87
+ return "<1h ago";
88
+ }
89
+ if (hours < 48) {
90
+ return `${Math.floor(hours)}h ago`;
91
+ }
92
+ return `${Math.floor(ageDays)}d ago`;
93
+ }
94
+ export function describeCooldownSettings(config, env) {
95
+ if (env.DG_COOLDOWN_AGE !== undefined) {
96
+ try {
97
+ const envDays = durationToDays(parseCooldownAge(env.DG_COOLDOWN_AGE, "DG_COOLDOWN_AGE", false));
98
+ return `${envDays > 0 ? formatCooldownDuration(envDays) : "off"} (DG_COOLDOWN_AGE)`;
99
+ }
100
+ catch {
101
+ // malformed env override is ignored everywhere; describe the config instead
102
+ }
103
+ }
104
+ const baseDays = durationToDays(config.cooldown.age);
105
+ const base = baseDays > 0 ? formatCooldownDuration(baseDays) : "off";
106
+ const overrides = ["npm", "pypi", "cargo"]
107
+ .map((ecosystem) => {
108
+ const value = ecosystem === "npm" ? config.cooldown.npmAge : ecosystem === "pypi" ? config.cooldown.pypiAge : config.cooldown.cargoAge;
109
+ if (value === "") {
110
+ return undefined;
111
+ }
112
+ const days = durationToDays(value);
113
+ return `${ecosystem} ${days > 0 ? formatCooldownDuration(days) : "off"}`;
114
+ })
115
+ .filter((entry) => entry !== undefined);
116
+ return overrides.length > 0 ? `${base} (${overrides.join(", ")})` : base;
117
+ }
@@ -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,25 +93,12 @@ 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
- export function scriptPolicyArgs(options) {
104
- if (!options.policy.scriptHardening && options.policy.mode !== "strict") {
105
- return options.existingArgs;
106
- }
107
- if (hasScriptDisableFlag(options.existingArgs)) {
108
- return options.existingArgs;
109
- }
110
- if (options.manager === "yarn") {
111
- return [...options.existingArgs, "--ignore-scripts"];
112
- }
113
- return [...options.existingArgs, "--ignore-scripts"];
114
- }
115
102
  function findTrustedAllowlist(packageName, policy, allowlists) {
116
103
  return allowlists.find((entry) => {
117
104
  if (entry.packageName !== packageName) {
@@ -123,6 +110,3 @@ function findTrustedAllowlist(packageName, policy, allowlists) {
123
110
  return true;
124
111
  });
125
112
  }
126
- function hasScriptDisableFlag(args) {
127
- return args.some((arg) => arg === "--ignore-scripts" || arg === "--ignore-scripts=true");
128
- }
@@ -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
+ }
@@ -0,0 +1,23 @@
1
+ export function provenanceLabel(provenance) {
2
+ if (provenance.status === "attested") {
3
+ const tag = slsaTag(provenance.predicateType);
4
+ return tag ? `attested (${tag})` : "attested";
5
+ }
6
+ if (provenance.status === "none") {
7
+ return "none";
8
+ }
9
+ return "unknown (not yet checked)";
10
+ }
11
+ export function provenanceDowngradeLine(version, provenance) {
12
+ if (!provenance.downgrade) {
13
+ return null;
14
+ }
15
+ return `provenance downgraded — ${provenance.downgrade.fromVersion} was attested, ${version} is not`;
16
+ }
17
+ function slsaTag(predicateType) {
18
+ if (!predicateType) {
19
+ return null;
20
+ }
21
+ const match = /slsa\.dev\/provenance\/(v[0-9][0-9.]*)/.exec(predicateType);
22
+ return match ? `slsa ${match[1]}` : null;
23
+ }
@@ -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" },