@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
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { startProductionHttpProxy } from "./server.js";
3
3
  import { parseForceOverrideRequest } from "./enforcement.js";
4
4
  import { cleanupSessionSync } from "../state/index.js";
5
+ import { COOLDOWN_EXEMPTIONS_ENV, loadCooldownExemptionsFile } from "./cooldown-exemptions-file.js";
5
6
  const PARENT_POLL_MS = 500;
6
7
  const sessionPath = process.argv[2];
7
8
  const apiBaseUrl = process.argv[3];
@@ -13,6 +14,7 @@ if (!sessionPath || !apiBaseUrl || !classificationJson) {
13
14
  const session = JSON.parse(readFileSync(sessionPath, "utf8"));
14
15
  const classification = JSON.parse(classificationJson);
15
16
  const forceOverride = parseForceOverrideRequest(process.env.DG_FORCE_OVERRIDE_REQUEST);
17
+ const cooldownExemptions = loadCooldownExemptionsFile(process.env[COOLDOWN_EXEMPTIONS_ENV]);
16
18
  writeFileSync(session.files.pid, `${process.pid}\n`, {
17
19
  encoding: "utf8",
18
20
  mode: 0o600
@@ -30,6 +32,18 @@ async function close() {
30
32
  function shutdown() {
31
33
  close().finally(() => process.exit(0));
32
34
  }
35
+ function exitOnFatal(error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ try {
38
+ process.stderr.write(`dg proxy worker: unexpected error — ${message}\n`);
39
+ }
40
+ catch {
41
+ // stderr is gone; nothing left to report to.
42
+ }
43
+ close().finally(() => process.exit(1));
44
+ }
45
+ process.on("uncaughtException", exitOnFatal);
46
+ process.on("unhandledRejection", exitOnFatal);
33
47
  process.stdin.resume();
34
48
  process.stdin.on("end", shutdown);
35
49
  process.stdin.on("error", shutdown);
@@ -46,6 +60,7 @@ handle = await startProductionHttpProxy({
46
60
  apiBaseUrl,
47
61
  classification,
48
62
  env: process.env,
49
- ...(forceOverride ? { forceOverride } : {})
63
+ ...(forceOverride ? { forceOverride } : {}),
64
+ ...(cooldownExemptions.length > 0 ? { cooldownExemptions } : {})
50
65
  });
51
66
  process.stdout.write(`ready ${handle.port}\n`);
@@ -1,5 +1,5 @@
1
1
  import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
2
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
3
  export function buildAuditFile(root, relPath) {
4
4
  const absolute = resolve(root, relPath);
5
5
  let stat;
@@ -46,6 +46,3 @@ export function toDisplay(relPath) {
46
46
  const normalized = relative(".", relPath) || relPath;
47
47
  return normalized.split(sep).join("/");
48
48
  }
49
- export function joinRel(dir, name) {
50
- return toDisplay(join(dir, name));
51
- }
@@ -1,11 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { toolInvocation } from "../util/external-tool.js";
4
5
  import { toDisplay } from "./collect.js";
5
6
  import { noExecPackEnv } from "./no-exec-shell.js";
6
- export function npmExecutable(platform = process.platform) {
7
- return platform === "win32" ? "npm.cmd" : "npm";
8
- }
9
7
  const DEFAULT_EXCLUDED_DIRS = new Set([
10
8
  "node_modules",
11
9
  ".git",
@@ -46,15 +44,20 @@ function npmHasAllowlist(root) {
46
44
  }
47
45
  }
48
46
  function npmPackList(root, env) {
47
+ const invocation = toolInvocation("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], env);
48
+ if (!invocation) {
49
+ return null;
50
+ }
49
51
  const shell = noExecPackEnv(env);
50
52
  let result;
51
53
  try {
52
- result = spawnSync(npmExecutable(), ["pack", "--dry-run", "--json", "--ignore-scripts"], {
54
+ result = spawnSync(invocation.command, [...invocation.args], {
53
55
  cwd: root,
54
56
  env: shell.env,
55
57
  encoding: "utf8",
56
58
  timeout: 60_000,
57
- maxBuffer: 64 * 1024 * 1024
59
+ maxBuffer: 64 * 1024 * 1024,
60
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
58
61
  });
59
62
  }
60
63
  finally {
@@ -3,18 +3,24 @@ import { createHash } from "node:crypto";
3
3
  import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { npmExecutable } from "./npm.js";
6
+ import { toolInvocation } from "../util/external-tool.js";
7
7
  import { noExecPackEnv } from "./no-exec-shell.js";
8
8
  export function packNpmArtifact(root, env = process.env) {
9
9
  const dest = mkdtempSync(join(tmpdir(), "dg-audit-pack-"));
10
+ const invocation = toolInvocation("npm", ["pack", "--ignore-scripts", "--pack-destination", dest], env);
11
+ if (!invocation) {
12
+ rmSync(dest, { recursive: true, force: true });
13
+ return { error: "npm executable not found on PATH" };
14
+ }
10
15
  const shell = noExecPackEnv(env);
11
16
  try {
12
- const result = spawnSync(npmExecutable(), ["pack", "--ignore-scripts", "--pack-destination", dest], {
17
+ const result = spawnSync(invocation.command, [...invocation.args], {
13
18
  cwd: root,
14
19
  env: shell.env,
15
20
  encoding: "utf8",
16
21
  timeout: 120_000,
17
- maxBuffer: 8 * 1024 * 1024
22
+ maxBuffer: 8 * 1024 * 1024,
23
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
18
24
  });
19
25
  if (result.status !== 0) {
20
26
  return { error: result.stderr ? result.stderr.trim().split("\n").pop() ?? "npm pack failed" : "npm pack failed" };
@@ -1,8 +1,4 @@
1
- import { writeFileSync } from "node:fs";
2
1
  import { routeCommand } from "../commands/router.js";
3
- if (process.env.NODE_ENV === "test" && process.env.DG_TEST_RUNTIME_SENTINEL) {
4
- writeFileSync(process.env.DG_TEST_RUNTIME_SENTINEL, "loaded\n", "utf8");
5
- }
6
2
  export function runCli(args) {
7
3
  return routeCommand(args);
8
4
  }
@@ -0,0 +1,31 @@
1
+ import { EXIT_TOOL_ERROR } from "../commands/types.js";
2
+ import { leaveTui, tuiIsActive } from "../scan-ui/alt-screen.js";
3
+ const defaultIo = {
4
+ writeStderr: (text) => {
5
+ process.stderr.write(text);
6
+ },
7
+ exit: (code) => {
8
+ process.exit(code);
9
+ },
10
+ tuiIsActive,
11
+ leaveTui
12
+ };
13
+ // Anything written while the alt screen is active is discarded when the exit handler restores the main screen.
14
+ export function exitOnFatal(error, io = defaultIo) {
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ try {
17
+ if (io.tuiIsActive()) {
18
+ io.leaveTui();
19
+ }
20
+ }
21
+ catch {
22
+ // screen restore must never mask the fatal report
23
+ }
24
+ try {
25
+ io.writeStderr(`dg: unexpected error — ${message}\nRun 'dg doctor' to check your installation; remove ~/.dg/config.json if it is corrupted.\n`);
26
+ }
27
+ catch {
28
+ // stderr is gone; nothing left to report to.
29
+ }
30
+ io.exit(EXIT_TOOL_ERROR);
31
+ }
@@ -3,7 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { dgVersion } from "../commands/version.js";
4
4
  import { isCiEnv } from "../presentation/mode.js";
5
5
  import { createTheme } from "../presentation/theme.js";
6
- import { sweepLegacyPythonHooks } from "../setup/plan.js";
6
+ import { refreshSetupOnUpgrade, sweepLegacyPythonHooks } from "../setup/plan.js";
7
7
  import { resolveDgPaths } from "../state/index.js";
8
8
  const SKIP_COMMANDS = new Set([
9
9
  "help",
@@ -13,14 +13,13 @@ const SKIP_COMMANDS = new Set([
13
13
  "version",
14
14
  "--version",
15
15
  "-v",
16
- "completion",
17
16
  "login",
18
17
  "logout",
19
18
  "update",
20
19
  "upgrade",
21
20
  "uninstall"
22
21
  ]);
23
- const MACHINE_OUTPUT_FLAGS = new Set([
22
+ export const MACHINE_OUTPUT_FLAGS = new Set([
24
23
  "--json",
25
24
  "--sarif",
26
25
  "--csv",
@@ -43,6 +42,7 @@ export function sweepLegacyHooksOnVersionChange(env = process.env, version = dgV
43
42
  return false;
44
43
  }
45
44
  sweepLegacyPythonHooks(resolveDgPaths(env).homeDir, [], []);
45
+ refreshSetupOnUpgrade(env);
46
46
  mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
47
47
  writeFileSync(marker, `${version}\n`, { encoding: "utf8", mode: 0o600 });
48
48
  return true;
@@ -51,6 +51,16 @@ export function sweepLegacyHooksOnVersionChange(env = process.env, version = dgV
51
51
  return false;
52
52
  }
53
53
  }
54
+ export function markFirstRunShown(env = process.env, now = new Date()) {
55
+ try {
56
+ const marker = firstRunMarkerPath(env);
57
+ mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
58
+ writeFileSync(marker, `${now.toISOString()}\n`, { encoding: "utf8", mode: 0o600 });
59
+ }
60
+ catch {
61
+ return;
62
+ }
63
+ }
54
64
  export function maybeShowFirstRun(args, options = {}) {
55
65
  const env = options.env ?? process.env;
56
66
  const stderr = options.stderr ?? process.stderr;
@@ -75,17 +85,9 @@ export function maybeShowFirstRun(args, options = {}) {
75
85
  ` ${theme.paint("muted", "dg npm install / dg pip install scan packages before they run.")}`,
76
86
  ` ${theme.paint("muted", "Run")} ${command_("dg setup")} ${theme.paint("muted", "once to protect bare npm/pip installs too.")}`,
77
87
  "",
78
- ` Next: ${command_("dg login")} to connect your account.`,
79
- "",
80
88
  ""
81
89
  ];
82
90
  stderr.write(lines.join("\n"));
83
- try {
84
- mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
85
- writeFileSync(marker, `${new Date().toISOString()}\n`, { encoding: "utf8", mode: 0o600 });
86
- }
87
- catch {
88
- return true;
89
- }
91
+ markFirstRunShown(env);
90
92
  return true;
91
93
  }
@@ -46,13 +46,50 @@ export function currentNodeVersion() {
46
46
  }
47
47
  return process.version;
48
48
  }
49
- export function assertCurrentNode() {
49
+ // The shims exec dg unconditionally; a hard exit on old Node would brick every shimmed manager.
50
+ export async function resolveShimFallback(env = process.env, argv = process.argv) {
51
+ if (!env.DG_SHIM_ACTIVE) {
52
+ return null;
53
+ }
54
+ const manager = argv[2];
55
+ if (!manager) {
56
+ return null;
57
+ }
50
58
  try {
51
- assertSupportedNode(currentNodeVersion());
59
+ const { resolveRealBinary } = await import("../launcher/resolve-real-binary.js");
60
+ const binary = resolveRealBinary({ name: manager, env }).path;
61
+ if (!binary) {
62
+ return null;
63
+ }
64
+ return {
65
+ binary,
66
+ args: argv.slice(3),
67
+ warning: `dg: protection inactive — dg requires Node.js >=22.14.0 but found ${currentNodeVersion()}; running ${manager} unprotected\n`
68
+ };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ export async function assertCurrentNode() {
75
+ const version = currentNodeVersion();
76
+ if (isSupportedNode(version)) {
77
+ return;
52
78
  }
53
- catch (error) {
54
- const message = error instanceof Error ? error.message : String(error);
55
- process.stderr.write(`${message}\n`);
56
- process.exit(1);
79
+ const fallback = await resolveShimFallback();
80
+ if (fallback) {
81
+ process.stderr.write(fallback.warning);
82
+ const [{ spawnSync }, { resolveSpawnInvocation }] = await Promise.all([
83
+ import("node:child_process"),
84
+ import("../launcher/spawn-invocation.js")
85
+ ]);
86
+ const invocation = resolveSpawnInvocation(fallback.binary, fallback.args);
87
+ const result = spawnSync(invocation.command, [...invocation.args], {
88
+ stdio: "inherit",
89
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
90
+ });
91
+ process.exit(result.status ?? 1);
57
92
  }
93
+ process.stderr.write(`dg requires Node.js >=22.14.0. Current runtime is ${version}. Upgrade Node before running dg.\n`);
94
+ process.exit(1);
58
95
  }
@@ -10,6 +10,7 @@ const UPDATE_THROTTLE_MS = 24 * 60 * 60 * 1000;
10
10
  const LOGIN_THROTTLE_MS = 7 * 24 * 60 * 60 * 1000;
11
11
  const LATEST_LOOKUP_TIMEOUT_MS = 1500;
12
12
  const SKIP_COMMANDS = new Set([
13
+ "",
13
14
  "help",
14
15
  "--help",
15
16
  "-h",
@@ -17,14 +18,15 @@ const SKIP_COMMANDS = new Set([
17
18
  "version",
18
19
  "--version",
19
20
  "-v",
20
- "completion",
21
21
  "login",
22
22
  "logout",
23
+ "setup",
23
24
  "update",
24
25
  "upgrade",
25
26
  "uninstall",
26
27
  "scan",
27
- "licenses"
28
+ "licenses",
29
+ "sbom"
28
30
  ]);
29
31
  export function nudgeStatePath(env = process.env) {
30
32
  return join(resolveDgPaths(env).stateDir, "nudges.json");
@@ -68,6 +70,37 @@ export function maybeShowNudges(args, options = {}) {
68
70
  writeState(statePath, state);
69
71
  }
70
72
  }
73
+ export function maybeSetupNudge(manager, options = {}) {
74
+ const env = options.env ?? process.env;
75
+ if (!(options.stderrIsTTY ?? process.stderr.isTTY)) {
76
+ return "";
77
+ }
78
+ if (isCiEnv(env)) {
79
+ return "";
80
+ }
81
+ if (env.DG_SHIM_ACTIVE) {
82
+ return "";
83
+ }
84
+ const paths = resolveDgPaths(env);
85
+ if (existsSync(join(paths.homeDir, ".dg", "shims", manager))) {
86
+ return "";
87
+ }
88
+ const statePath = nudgeStatePath(env);
89
+ const state = readState(statePath);
90
+ if (state.setupNudgedAt) {
91
+ return "";
92
+ }
93
+ state.setupNudgedAt = (options.now ?? new Date()).toISOString();
94
+ writeState(statePath, state);
95
+ const theme = createTheme(true);
96
+ return `\n ${theme.paint("muted", "Make this automatic —")} ${theme.paint("accent", "dg setup")} ${theme.paint("muted", "protects every install, no prefix.")}\n`;
97
+ }
98
+ export function recordLoginNudge(env = process.env, now = new Date()) {
99
+ const statePath = nudgeStatePath(env);
100
+ const state = readState(statePath);
101
+ state.loginNudgedAt = now.toISOString();
102
+ writeState(statePath, state);
103
+ }
71
104
  export function pendingUpdate(env = process.env) {
72
105
  const latest = readState(nudgeStatePath(env)).updateLatest;
73
106
  if (!latest || compareVersions(latest, dgVersion()) <= 0) {
@@ -0,0 +1,211 @@
1
+ import { join } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { parseLockfilePackages } from "../verify/preflight.js";
4
+ import { normalizePypiName } from "../policy/cooldown.js";
5
+ export function collectSbomComponents(projects) {
6
+ const seen = new Set();
7
+ const droppedSeen = new Set();
8
+ const components = [];
9
+ const dropped = [];
10
+ for (const project of projects) {
11
+ const parsed = parseLockfilePackages(join(project.path, project.depFile));
12
+ for (const identity of parsed.packages) {
13
+ if (identity.ecosystem === "unknown" || !identity.version) {
14
+ const label = `${identity.ecosystem}:${identity.name}`;
15
+ if (identity.name.length > 0 && !droppedSeen.has(label)) {
16
+ droppedSeen.add(label);
17
+ dropped.push(label);
18
+ }
19
+ continue;
20
+ }
21
+ const component = { ...identity, version: identity.version };
22
+ const key = canonicalKey(component);
23
+ if (seen.has(key)) {
24
+ continue;
25
+ }
26
+ seen.add(key);
27
+ components.push(component);
28
+ }
29
+ }
30
+ return { components, dropped };
31
+ }
32
+ export function readRootComponent(root) {
33
+ const manifest = join(root, "package.json");
34
+ if (!existsSync(manifest)) {
35
+ return {};
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
39
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) {
40
+ return {};
41
+ }
42
+ return { component: { name: parsed.name, ...(typeof parsed.version === "string" ? { version: parsed.version } : {}) } };
43
+ }
44
+ catch {
45
+ return { malformed: true };
46
+ }
47
+ }
48
+ export function buildCycloneDxSbom(components, opts) {
49
+ const byRef = new Map();
50
+ for (const component of components) {
51
+ const mapped = toCycloneDxComponent(component);
52
+ if (!byRef.has(mapped["bom-ref"])) {
53
+ byRef.set(mapped["bom-ref"], mapped);
54
+ }
55
+ }
56
+ const mapped = [...byRef.values()].sort((a, b) => compareCodePoints(a["bom-ref"], b["bom-ref"]));
57
+ return {
58
+ bomFormat: "CycloneDX",
59
+ specVersion: "1.5",
60
+ serialNumber: opts.serialNumber,
61
+ version: 1,
62
+ metadata: {
63
+ ...(opts.timestamp ? { timestamp: opts.timestamp } : {}),
64
+ tools: [{ vendor: "WestBayBerry", name: "dg", version: opts.toolVersion }],
65
+ ...(opts.rootComponent
66
+ ? {
67
+ component: {
68
+ type: "application",
69
+ "bom-ref": "root",
70
+ name: opts.rootComponent.name,
71
+ ...(opts.rootComponent.version ? { version: opts.rootComponent.version } : {})
72
+ }
73
+ }
74
+ : {})
75
+ },
76
+ components: mapped
77
+ };
78
+ }
79
+ function compareCodePoints(a, b) {
80
+ return a < b ? -1 : a > b ? 1 : 0;
81
+ }
82
+ export function bomRefOf(component) {
83
+ return purlFor(component) ?? `${component.ecosystem}:${component.name}@${component.version}`;
84
+ }
85
+ function canonicalKey(component) {
86
+ const name = canonicalPurlName(component.ecosystem, component.name);
87
+ return `${component.ecosystem}:${name}@${component.version}`;
88
+ }
89
+ function canonicalPurlName(ecosystem, name) {
90
+ if (ecosystem === "pypi") {
91
+ return normalizePypiName(name);
92
+ }
93
+ if (ecosystem === "npm") {
94
+ return name.toLowerCase();
95
+ }
96
+ return name;
97
+ }
98
+ // The declared license string is emitted verbatim as a CycloneDX `license.name`,
99
+ // which is always schema-valid for any free text. We deliberately do NOT classify
100
+ // it into the SPDX `expression` form: correctly distinguishing a valid SPDX
101
+ // expression from free text requires validating operands against the SPDX license
102
+ // registry, and any heuristic shortcut emits schema-invalid expressions for some
103
+ // inputs (unbalanced parens, operator-only strings, unregistered ids).
104
+ function toCycloneDxComponent(component) {
105
+ const purl = purlFor(component);
106
+ const hashes = hashesFor(component.integrity);
107
+ const license = component.license?.trim();
108
+ const licenses = license ? [{ license: { name: license } }] : undefined;
109
+ return {
110
+ type: "library",
111
+ "bom-ref": bomRefOf(component),
112
+ name: component.name,
113
+ version: component.version,
114
+ ...(purl ? { purl } : {}),
115
+ ...(licenses ? { licenses } : {}),
116
+ ...(hashes.length > 0 ? { hashes } : {})
117
+ };
118
+ }
119
+ export function purlFor(component) {
120
+ const version = encodeURIComponent(component.version);
121
+ if (component.ecosystem === "npm") {
122
+ const lower = component.name.toLowerCase();
123
+ if (lower.startsWith("@") && lower.includes("/")) {
124
+ const slash = lower.indexOf("/");
125
+ const scope = lower.slice(1, slash);
126
+ const bare = lower.slice(slash + 1);
127
+ return `pkg:npm/%40${encodeURIComponent(scope)}/${encodeURIComponent(bare)}@${version}`;
128
+ }
129
+ return `pkg:npm/${encodeURIComponent(lower)}@${version}`;
130
+ }
131
+ if (component.ecosystem === "pypi") {
132
+ return `pkg:pypi/${encodeURIComponent(normalizePypiName(component.name))}@${version}`;
133
+ }
134
+ if (component.ecosystem === "cargo") {
135
+ const base = `pkg:cargo/${encodeURIComponent(component.name.toLowerCase())}@${version}`;
136
+ const repository = cargoAlternateRegistry(component.resolvedUrl);
137
+ return repository ? `${base}?repository_url=${encodeURIComponent(repository)}` : base;
138
+ }
139
+ return null;
140
+ }
141
+ function cargoAlternateRegistry(source) {
142
+ if (!source || (!source.startsWith("registry+") && !source.startsWith("sparse+"))) {
143
+ return null;
144
+ }
145
+ const url = source.replace(/^registry\+/u, "").replace(/^sparse\+/u, "");
146
+ if (/(?:^|\/\/)(?:github\.com\/rust-lang\/crates\.io-index|index\.crates\.io)/u.test(url)) {
147
+ return null;
148
+ }
149
+ return url;
150
+ }
151
+ const SRI_ALG_BYTES = {
152
+ sha1: { alg: "SHA-1", bytes: 20 },
153
+ sha256: { alg: "SHA-256", bytes: 32 },
154
+ sha384: { alg: "SHA-384", bytes: 48 },
155
+ sha512: { alg: "SHA-512", bytes: 64 }
156
+ };
157
+ export function hashesFor(integrity) {
158
+ if (!integrity) {
159
+ return [];
160
+ }
161
+ const hashes = [];
162
+ for (const token of integrity.trim().split(/\s+/)) {
163
+ const sri = /^(sha1|sha256|sha384|sha512)-([^?]+)/u.exec(token);
164
+ if (sri && sri[1] && sri[2]) {
165
+ const spec = SRI_ALG_BYTES[sri[1]];
166
+ const hex = base64ToHex(sri[2]);
167
+ if (spec && hex && hex.length === spec.bytes * 2) {
168
+ hashes.push({ alg: spec.alg, content: hex });
169
+ }
170
+ continue;
171
+ }
172
+ const hex = /^(sha1|sha256|sha384|sha512|md5):([0-9a-fA-F]+)$/u.exec(token);
173
+ if (hex && hex[1] && hex[2]) {
174
+ const spec = SRI_ALG_BYTES[hex[1]];
175
+ if (spec && hex[2].length === spec.bytes * 2) {
176
+ hashes.push({ alg: spec.alg, content: hex[2].toLowerCase() });
177
+ }
178
+ continue;
179
+ }
180
+ const berry = /^[0-9a-z]+\/([0-9a-f]{40,128})$/iu.exec(token);
181
+ if (berry && berry[1]) {
182
+ const content = berry[1].toLowerCase();
183
+ const alg = SHA_HEX_LENGTHS[content.length];
184
+ if (alg) {
185
+ hashes.push({ alg, content });
186
+ }
187
+ continue;
188
+ }
189
+ const bareHex = /^[0-9a-f]{40,128}$/iu.exec(token);
190
+ if (bareHex) {
191
+ const content = token.toLowerCase();
192
+ const alg = SHA_HEX_LENGTHS[content.length];
193
+ if (alg) {
194
+ hashes.push({ alg, content });
195
+ }
196
+ }
197
+ }
198
+ return hashes;
199
+ }
200
+ const SHA_HEX_LENGTHS = { 40: "SHA-1", 64: "SHA-256", 96: "SHA-384", 128: "SHA-512" };
201
+ function base64ToHex(value) {
202
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
203
+ return null;
204
+ }
205
+ try {
206
+ return Buffer.from(value, "base64").toString("hex");
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ }