@westbayberry/dg 2.1.0 → 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 (138) 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 +1 -1
  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 +15 -7
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +23 -9
  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 +6 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +1 -1
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/types.js +1 -0
  38. package/dist/commands/update.js +17 -8
  39. package/dist/commands/verify.js +8 -5
  40. package/dist/config/settings.js +14 -44
  41. package/dist/decisions/remember-prompt.js +5 -10
  42. package/dist/export-ui/ExportDialog.js +198 -0
  43. package/dist/install-ui/LiveInstall.js +2 -2
  44. package/dist/install-ui/block-render.js +1 -1
  45. package/dist/install-ui/prep-spinner.js +32 -0
  46. package/dist/launcher/agent-check.js +466 -0
  47. package/dist/launcher/agent-hook-exec.js +70 -0
  48. package/dist/launcher/agent-hook-io.js +31 -0
  49. package/dist/launcher/cargo-cache.js +40 -0
  50. package/dist/launcher/classify.js +17 -6
  51. package/dist/launcher/env.js +71 -24
  52. package/dist/launcher/install-preflight.js +51 -14
  53. package/dist/launcher/live-install.js +25 -5
  54. package/dist/launcher/output-redaction.js +7 -4
  55. package/dist/launcher/preflight-prompt.js +14 -1
  56. package/dist/launcher/run.js +89 -86
  57. package/dist/launcher/spawn-invocation.js +21 -0
  58. package/dist/policy/cooldown.js +18 -5
  59. package/dist/policy/evaluate.js +5 -6
  60. package/dist/policy/pypi-name.js +17 -0
  61. package/dist/presentation/mode.js +3 -2
  62. package/dist/presentation/package-page.js +9 -0
  63. package/dist/presentation/theme.js +7 -7
  64. package/dist/project/dgfile.js +145 -6
  65. package/dist/proxy/auth.js +42 -0
  66. package/dist/proxy/ca.js +29 -9
  67. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  68. package/dist/proxy/enforcement.js +55 -16
  69. package/dist/proxy/metadata-map.js +41 -3
  70. package/dist/proxy/preverified.js +55 -0
  71. package/dist/proxy/server.js +448 -49
  72. package/dist/proxy/worker.js +16 -1
  73. package/dist/publish-set/collect.js +1 -4
  74. package/dist/publish-set/npm.js +8 -5
  75. package/dist/publish-set/pack.js +9 -3
  76. package/dist/runtime/cli.js +0 -4
  77. package/dist/runtime/fatal.js +31 -0
  78. package/dist/runtime/first-run.js +12 -11
  79. package/dist/runtime/node-version.js +43 -6
  80. package/dist/runtime/nudges.js +35 -2
  81. package/dist/sbom/cyclonedx.js +211 -0
  82. package/dist/sbom-ui/SbomApp.js +158 -0
  83. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  84. package/dist/sbom-ui/components/SbomList.js +52 -0
  85. package/dist/sbom-ui/inventory.js +128 -0
  86. package/dist/sbom-ui/launch.js +51 -0
  87. package/dist/sbom-ui/run.js +55 -0
  88. package/dist/sbom-ui/store.js +26 -0
  89. package/dist/scan/collect.js +10 -6
  90. package/dist/scan/command.js +17 -10
  91. package/dist/scan/discovery.js +11 -2
  92. package/dist/scan/render.js +28 -4
  93. package/dist/scan/scanner-report.js +15 -9
  94. package/dist/scan/staged.js +2 -1
  95. package/dist/scan-ui/LegacyApp.js +9 -13
  96. package/dist/scan-ui/alt-screen.js +5 -8
  97. package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
  98. package/dist/scan-ui/components/ProgressBar.js +3 -14
  99. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  100. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  101. package/dist/scan-ui/components/SetupBanner.js +0 -6
  102. package/dist/scan-ui/format-helpers.js +61 -5
  103. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  104. package/dist/scan-ui/hooks/useScan.js +16 -2
  105. package/dist/scan-ui/launch.js +7 -4
  106. package/dist/scan-ui/shims.js +11 -4
  107. package/dist/scripts/detect.js +7 -2
  108. package/dist/scripts/gate.js +1 -1
  109. package/dist/service/state.js +27 -8
  110. package/dist/service/trust-refresh.js +92 -0
  111. package/dist/service/worker.js +23 -1
  112. package/dist/setup/activate-shell.js +28 -0
  113. package/dist/setup/git-hook.js +49 -4
  114. package/dist/setup/plan.js +73 -39
  115. package/dist/setup-ui/gate.js +39 -0
  116. package/dist/setup-ui/offer.js +42 -0
  117. package/dist/setup-ui/selector.js +27 -0
  118. package/dist/setup-ui/tasks.js +56 -0
  119. package/dist/setup-ui/wizard.js +225 -0
  120. package/dist/state/cooldown-held.js +66 -0
  121. package/dist/state/index.js +1 -0
  122. package/dist/state/locks.js +4 -2
  123. package/dist/state/store.js +2 -1
  124. package/dist/util/external-tool.js +25 -0
  125. package/dist/util/git.js +10 -3
  126. package/dist/util/report-writer.js +57 -0
  127. package/dist/verify/local.js +240 -42
  128. package/dist/verify/package-check.js +84 -28
  129. package/dist/verify/preflight.js +242 -49
  130. package/dist/verify/render.js +15 -1
  131. package/npm-shrinkwrap.json +2383 -0
  132. package/package.json +6 -8
  133. package/NOTICE +0 -5
  134. package/dist/commands/completion.js +0 -117
  135. package/dist/commands/explain.js +0 -236
  136. package/dist/commands/unavailable.js +0 -11
  137. package/dist/scripts/rebuild.js +0 -28
  138. package/dist/telemetry/events.js +0 -40
@@ -4,41 +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";
19
24
  import { runScriptGateAfterInstall } from "../scripts/gate.js";
20
25
  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
- }
26
+ export { resolveSpawnInvocation } from "./spawn-invocation.js";
42
27
  export function shimDepth(env) {
43
28
  const parsed = Number.parseInt(env.DG_SHIM_DEPTH ?? "", 10);
44
29
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
@@ -130,7 +115,7 @@ export async function runPackageManager(manager, args, options = {}) {
130
115
  return {
131
116
  exitCode: child.exitCode,
132
117
  stdout: streamedOut(child.stdout, options),
133
- stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}`
118
+ stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
134
119
  };
135
120
  }
136
121
  const child = await spawnPackageManager(plan, args, options);
@@ -152,6 +137,7 @@ async function runWithProductionProxy(plan, args, options) {
152
137
  }
153
138
  const restoreSignalHandlers = installProxySignalHandlers(proxy);
154
139
  try {
140
+ const cacheDir = prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env);
155
141
  const proxiedPlan = {
156
142
  ...plan,
157
143
  childEnv: buildProxyChildEnv({
@@ -159,7 +145,7 @@ async function runWithProductionProxy(plan, args, options) {
159
145
  baseEnv: plan.childEnv,
160
146
  proxyUrl: proxy.proxyUrl,
161
147
  caBundlePath: proxy.session.files.ca,
162
- cacheDir: `${proxy.session.dir}/pm-cache`
148
+ cacheDir
163
149
  })
164
150
  };
165
151
  const child = await spawnPackageManager(proxiedPlan, args, options);
@@ -196,7 +182,7 @@ async function runWithProductionProxy(plan, args, options) {
196
182
  return {
197
183
  exitCode: child.exitCode,
198
184
  stdout: streamedOut(child.stdout, options),
199
- stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}`
185
+ stderr: `${rendered}${streamedErr(child.stderr, options)}${scriptGateLine(plan, child.exitCode, options)}${setupNudgeAfterInstall(plan, child.exitCode, options)}`
200
186
  };
201
187
  }
202
188
  finally {
@@ -214,6 +200,12 @@ function scriptGateLine(plan, exitCode, options) {
214
200
  ...(options.projectDir ? { projectDir: options.projectDir } : {})
215
201
  });
216
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
+ }
217
209
  export function deriveLiveView(state, phase, resolvedTotal) {
218
210
  const verified = state.decisions.filter((decision) => decision.action === "pass").length;
219
211
  const warnDecisions = state.decisions.filter((decision) => decision.action === "warn");
@@ -232,57 +224,43 @@ export function deriveLiveView(state, phase, resolvedTotal) {
232
224
  ...(blocked ? { blocked: describeBlockedInstall(blocked) } : {})
233
225
  };
234
226
  }
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)
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) {
244
234
  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
235
+ }
236
+ discarded = true;
237
+ void worker.then((proxy) => {
238
+ if (!("decision" in proxy)) {
239
+ stopProxyWorker(proxy);
240
+ }
257
241
  });
258
242
  }
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
- });
243
+ };
275
244
  }
276
- export async function runWithProductionProxyLive(plan, args, options, onView) {
245
+ export async function runWithProductionProxyLive(plan, args, options, onView, prepared) {
277
246
  if (options.onStdout || options.onStderr) {
278
247
  throw new Error("live install mode renders its own UI and owns the terminal; streaming output callbacks are not supported");
279
248
  }
280
249
  const env = options.env ?? process.env;
281
250
  maybeWarnRootWithoutState(env);
282
- 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);
283
258
  if ("decision" in proxy) {
284
259
  return { exitCode: EXIT_INSTALL_BLOCKED, stdout: "", stderr: redactSecrets(renderInstallDecision(proxy.decision)) };
285
260
  }
261
+ if (options.preverified && options.preverified.length > 0) {
262
+ writePreverifiedFile(proxy.session.dir, options.preverified);
263
+ }
286
264
  const restoreSignalHandlers = installProxySignalHandlers(proxy);
287
265
  try {
288
266
  const childEnv = buildProxyChildEnv({
@@ -290,22 +268,10 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
290
268
  baseEnv: plan.childEnv,
291
269
  proxyUrl: proxy.proxyUrl,
292
270
  caBundlePath: proxy.session.files.ca,
293
- cacheDir: `${proxy.session.dir}/pm-cache`
271
+ cacheDir: prepareProxyCacheDir(plan.classification.manager, proxy.session.dir, env)
294
272
  });
295
273
  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
- }
274
+ const resolvedTotal = plan.classification.manager === "pip" ? cachedPipResolution(plan.realBinary.path ?? "", args)?.count : undefined;
309
275
  const poll = setInterval(() => {
310
276
  onView(deriveLiveView(readProxySessionState(proxy.session), "scanning", resolvedTotal));
311
277
  }, 90);
@@ -319,12 +285,6 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
319
285
  }
320
286
  finally {
321
287
  clearInterval(poll);
322
- if (dryRunChild) {
323
- try {
324
- dryRunChild.kill();
325
- }
326
- catch { /* already exited */ }
327
- }
328
288
  }
329
289
  const proxyState = readProxySessionState(proxy.session);
330
290
  onView(deriveLiveView(proxyState, "done", resolvedTotal));
@@ -342,13 +302,20 @@ export async function runWithProductionProxyLive(plan, args, options, onView) {
342
302
  const outcome = installOutcome(finished.stdout);
343
303
  return { exitCode: EXIT_INSTALL_BLOCKED, stdout: outcome, stderr: cacheOnlyNotice(outcome.length > 0) };
344
304
  }
345
- return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: scriptGateLine(plan, finished.exitCode, options) };
305
+ return { exitCode: finished.exitCode, stdout: installOutcome(finished.stdout), stderr: `${scriptGateLine(plan, finished.exitCode, options)}${setupNudgeAfterInstall(plan, finished.exitCode, options)}` };
346
306
  }
347
307
  finally {
348
308
  restoreSignalHandlers();
349
309
  stopProxyWorker(proxy);
350
310
  }
351
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
+ }
352
319
  function cacheOnlyNotice(installedFresh) {
353
320
  const theme = createTheme(resolvePresentation().color);
354
321
  const reason = installedFresh ? "used a saved copy" : "already installed";
@@ -436,6 +403,38 @@ function renderDecisions(decisions, env) {
436
403
  const visible = isCiEnv(env) ? decisions.filter((decision) => decision.action !== "pass") : decisions;
437
404
  return visible.map((decision) => redactSecrets(renderInstallDecision(decision))).join("");
438
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
+ }
439
438
  async function startProxyWorker(classification, env, forceOverride) {
440
439
  const paths = resolveDgPaths(env);
441
440
  const session = createSessionSync(paths);
@@ -450,11 +449,15 @@ async function startProxyWorker(classification, env, forceOverride) {
450
449
  throw new Error("production proxy worker is not built");
451
450
  }
452
451
  const config = loadUserConfig(env);
452
+ const exemptions = loadProjectCooldownExemptions(env);
453
+ maybeNoticeDgFileExemptions(exemptions);
454
+ const exemptionsEnv = writeCooldownExemptionsFile(session.dir, exemptions);
453
455
  const child = spawn(process.execPath, [workerPath, sessionBootstrapPath, config.api.baseUrl], {
454
456
  env: {
455
457
  ...env,
456
458
  DG_PROXY_CLASSIFICATION: JSON.stringify(classification),
457
- ...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {})
459
+ ...(forceOverride ? { DG_FORCE_OVERRIDE_REQUEST: JSON.stringify(forceOverride) } : {}),
460
+ ...exemptionsEnv
458
461
  },
459
462
  stdio: ["pipe", "pipe", "pipe"]
460
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
+ }
@@ -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" },