@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
@@ -1,4 +1,6 @@
1
1
  import { createTheme } from "../presentation/theme.js";
2
+ import { packagePageUrl } from "../presentation/package-page.js";
3
+ import { sanitizeDeep } from "../security/sanitize.js";
2
4
  const DETAIL_PROJECT_LIMIT = 8;
3
5
  const CLEAN_PROJECT_PREVIEW_LIMIT = 5;
4
6
  const FINDING_GROUP_PREVIEW_LIMIT = 3;
@@ -16,7 +18,8 @@ const SCANNER_SKIP_MESSAGES = {
16
18
  no_lockfile: "no lockfile found — server verification skipped (local heuristics only)",
17
19
  empty_lockfile: "lockfile contains no scannable packages — server verification skipped (local heuristics only)"
18
20
  };
19
- export function renderTextReport(report, terminalWidth = terminalWidthFromEnv(), theme = createTheme(false), scannerNotice) {
21
+ export function renderTextReport(rawReport, terminalWidth = terminalWidthFromEnv(), theme = createTheme(false), scannerNotice) {
22
+ const report = sanitizeDeep(rawReport);
20
23
  const width = Math.max(48, Math.min(terminalWidth, 140));
21
24
  const cleanProjects = report.projects.filter((project) => project.findings.length === 0);
22
25
  const findingProjects = report.projects.filter((project) => project.findings.length > 0);
@@ -95,11 +98,31 @@ export function renderTextReport(report, terminalWidth = terminalWidthFromEnv(),
95
98
  lines.push("");
96
99
  }
97
100
  }
101
+ lines.push(...packagePageLines(report, theme));
98
102
  for (const error of report.errors) {
99
103
  lines.push(...wrapLine(`ERROR ${error.location}: `, error.message, width));
100
104
  }
101
105
  return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
102
106
  }
107
+ function packagePageLines(report, theme) {
108
+ const urls = [];
109
+ const seen = new Set();
110
+ for (const pkg of report.scanner?.packages ?? []) {
111
+ const action = pkg.action ?? "pass";
112
+ if (action !== "block" && action !== "warn") {
113
+ continue;
114
+ }
115
+ const url = packagePageUrl(pkg.ecosystem ?? "", pkg.name);
116
+ if (url && !seen.has(url)) {
117
+ seen.add(url);
118
+ urls.push(url);
119
+ }
120
+ }
121
+ if (urls.length === 0) {
122
+ return [];
123
+ }
124
+ return ["Full report for each flagged package:", ...urls.map((url) => ` ${theme.paint("muted", url)}`), ""];
125
+ }
103
126
  function provenanceDowngradeLines(packages, theme) {
104
127
  return packages
105
128
  .filter((pkg) => pkg.provenance?.downgrade)
@@ -132,8 +155,8 @@ function formatProject(project, width) {
132
155
  }
133
156
  return lines;
134
157
  }
135
- export function renderJsonReport(report, scannerUnavailable = false) {
136
- return `${JSON.stringify({ ...report, status: displayScanStatus(report), scannerUnavailable }, null, 2)}\n`;
158
+ export function renderJsonReport(report, scannerUnavailable = false, nothingToScan = false) {
159
+ return `${JSON.stringify({ schemaVersion: 1, ...report, status: nothingToScan ? "nothing_to_scan" : displayScanStatus(report), scannerUnavailable }, null, 2)}\n`;
137
160
  }
138
161
  export function renderSarifReport(report) {
139
162
  const rules = uniqueFindings(report.findings).map((finding) => ({
@@ -281,7 +304,8 @@ function wrapLine(prefix, text, width) {
281
304
  return lines.map((line, index) => `${index === 0 ? prefix : continuation}${line}`);
282
305
  }
283
306
  function terminalWidthFromEnv() {
284
- const value = process.env.DG_TEST_TERMINAL_WIDTH ?? String(process.stdout.columns ?? 100);
307
+ const override = process.env.NODE_ENV === "test" ? process.env.DG_TEST_TERMINAL_WIDTH : undefined;
308
+ const value = override ?? String(process.stdout.columns ?? 100);
285
309
  const parsed = Number.parseInt(value, 10);
286
310
  return Number.isFinite(parsed) ? parsed : 100;
287
311
  }
@@ -59,16 +59,16 @@ export function runScannerScan(targetPath, localReport, env = process.env, decis
59
59
  error: { kind: "invalid_response", message: "scanner worker returned unreadable output" }
60
60
  };
61
61
  }
62
- const report = buildScannerReport(localReport, response, total);
63
- if (!decisionsFile?.readable) {
64
- return { kind: "report", report };
65
- }
66
62
  const ecosystems = new Map();
67
63
  for (const group of groups) {
68
64
  for (const pkg of group.packages) {
69
65
  ecosystems.set(packageKey(pkg.name, pkg.version), group.ecosystem);
70
66
  }
71
67
  }
68
+ const report = buildScannerReport(localReport, response, total, ecosystems);
69
+ if (!decisionsFile?.readable) {
70
+ return { kind: "report", report };
71
+ }
72
72
  return { kind: "report", report: annotateReportWithDecisions(report, decisionsFile, ecosystems) };
73
73
  }
74
74
  export function tryScannerScan(targetPath, localReport, env = process.env, decisionsFile = null) {
@@ -130,9 +130,9 @@ function parseWorkerError(stdout) {
130
130
  return null;
131
131
  }
132
132
  function lockfileParseError(projects, collected) {
133
- const parseErrors = collected.parseErrors ?? [];
133
+ const parseErrors = collected.parseErrors;
134
134
  const detail = parseErrors
135
- .map((entry) => [entry.path, entry.message].filter(Boolean).join(": "))
135
+ .map((entry) => [entry.file, entry.reason].filter(Boolean).join(": "))
136
136
  .filter((line) => line.length > 0)
137
137
  .join("; ");
138
138
  const lockfiles = [...new Set(projects.map((project) => project.depFile))].join(", ");
@@ -143,8 +143,14 @@ function lockfileParseError(projects, collected) {
143
143
  : `found ${projects.length} lockfile${projects.length === 1 ? "" : "s"} (${lockfiles}) but no packages could be parsed`
144
144
  };
145
145
  }
146
- export function buildScannerReport(localReport, response, analyzedCount) {
147
- const findings = response.packages
146
+ export function buildScannerReport(localReport, response, analyzedCount, ecosystems) {
147
+ const packages = ecosystems
148
+ ? response.packages.map((pkg) => {
149
+ const ecosystem = ecosystems.get(packageKey(pkg.name, pkg.version));
150
+ return ecosystem ? { ...pkg, ecosystem } : pkg;
151
+ })
152
+ : response.packages;
153
+ const findings = packages
148
154
  .filter((pkg) => (pkg.action ?? "pass") !== "pass")
149
155
  .map((pkg) => scannerFinding(pkg));
150
156
  const warnCount = findings.filter((finding) => finding.severity === "warn").length;
@@ -163,7 +169,7 @@ export function buildScannerReport(localReport, response, analyzedCount) {
163
169
  blockCount,
164
170
  errorCount: localReport.summary.errorCount
165
171
  },
166
- scanner: response
172
+ scanner: { ...response, packages }
167
173
  };
168
174
  }
169
175
  function scannerFinding(pkg) {
@@ -70,7 +70,8 @@ export function runStagedScan(options) {
70
70
  const env = options.env ?? process.env;
71
71
  const cwd = options.cwd ?? process.cwd();
72
72
  const theme = createTheme(resolvePresentation().color);
73
- if (env[GUARD_SELFTEST_ENV] === "1") {
73
+ // git exports GIT_INDEX_FILE to real pre-commit runs; its absence separates dg's hook self-test from a user commit that inherited the env var.
74
+ if (options.hook && env[GUARD_SELFTEST_ENV] === "1" && env.GIT_INDEX_FILE === undefined) {
74
75
  return { exitCode: 2, stdout: "", stderr: "dg guard-commit self-test: synthetic block (exit 2)\n" };
75
76
  }
76
77
  const root = gitTrimmed(["rev-parse", "--show-toplevel"], { cwd, env });
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useCallback } from "react";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
+ import { EXIT_ANALYSIS_INCOMPLETE, EXIT_NOTHING_TO_SCAN } from "../commands/types.js";
4
5
  import { useScan } from "./hooks/useScan.js";
5
6
  import { Spinner } from "./components/Spinner.js";
6
7
  import { ProgressBar } from "./components/ProgressBar.js";
@@ -8,14 +9,15 @@ import { InteractiveResultsView } from "./components/InteractiveResultsView.js";
8
9
  import { ErrorView } from "./components/ErrorView.js";
9
10
  import { ProjectSelector } from "./components/ProjectSelector.js";
10
11
  import { SetupBanner } from "./components/SetupBanner.js";
11
- import { useTerminalSize } from "./hooks/useTerminalSize.js";
12
+ import { useResizeRepaint } from "./hooks/useResizeRepaint.js";
12
13
  import { effectiveScanAction, scanExitCode } from "./shims.js";
13
14
  import { leaveTui, showCursor, tuiIsActive } from "./alt-screen.js";
14
15
  import { formatResetDate } from "../install-ui/block-render.js";
15
- export const App = ({ config, userStatus, scanUsage, setupIssues = [], initialView, updateAvailable }) => {
16
+ import { accountHeaderLine } from "./shims.js";
17
+ export const App = ({ config, setupIssues = [], initialView, updateAvailable }) => {
16
18
  const { state, scanSelectedProjects, restartSelection } = useScan(config);
17
19
  const { exit } = useApp();
18
- useTerminalSize();
20
+ useResizeRepaint();
19
21
  const leaveAltScreen = useCallback(() => {
20
22
  if (tuiIsActive()) {
21
23
  leaveTui();
@@ -33,9 +35,6 @@ export const App = ({ config, userStatus, scanUsage, setupIssues = [], initialVi
33
35
  exit();
34
36
  }, [state, config, exit, leaveAltScreen]);
35
37
  // Exit alt screen BEFORE writing messages so they appear on the main screen.
36
- // `delayMs` keeps the alt-screen panel visible for that long before tearing
37
- // down (default 0 — fire-and-exit). Used by the free_cap_reached path to
38
- // give the free_cap_reached path time to read the "sign in to unlock" panel.
39
38
  const exitWithMessage = useCallback((message, exitCode, delayMs = 0) => {
40
39
  process.exitCode = exitCode;
41
40
  if (delayMs === 0) {
@@ -51,11 +50,11 @@ export const App = ({ config, userStatus, scanUsage, setupIssues = [], initialVi
51
50
  }, [exit, leaveAltScreen]);
52
51
  useEffect(() => {
53
52
  if (state.phase === "empty") {
54
- const timer = exitWithMessage(`${state.message}\n`, 0);
53
+ const timer = exitWithMessage(`${state.message}\n`, EXIT_NOTHING_TO_SCAN);
55
54
  return () => clearTimeout(timer);
56
55
  }
57
56
  if (state.phase === "error") {
58
- const timer = exitWithMessage(`Error: ${state.error.message}\n`, 3);
57
+ const timer = exitWithMessage(`Error: ${state.error.message}\n`, EXIT_ANALYSIS_INCOMPLETE);
59
58
  return () => clearTimeout(timer);
60
59
  }
61
60
  // Reflect the verdict in the process exit code as soon as results render,
@@ -91,13 +90,13 @@ export const App = ({ config, userStatus, scanUsage, setupIssues = [], initialVi
91
90
  return _jsx(Spinner, { label: `Found ${state.found} · ${path}` });
92
91
  }
93
92
  case "selecting":
94
- return (_jsx(ProjectSelector, { projects: state.projects, onConfirm: scanSelectedProjects, onCancel: () => { process.exitCode = 0; leaveAltScreen(); exit(); }, userStatus: userStatus }));
93
+ return (_jsx(ProjectSelector, { projects: state.projects, onConfirm: scanSelectedProjects, onCancel: () => { process.exitCode = 0; leaveAltScreen(); exit(); }, userStatus: accountHeaderLine() }));
95
94
  case "scanning":
96
95
  return (_jsx(ProgressBar, { value: state.done, total: state.total, label: state.batchCount > 1 && state.batchIndex >= 1
97
96
  ? `batch ${state.batchIndex}/${state.batchCount}`
98
97
  : undefined }));
99
98
  case "results":
100
- return (_jsx(InteractiveResultsView, { result: state.result, config: config, durationMs: state.durationMs, onExit: handleResultsExit, onBack: restartSelection ?? undefined, discoveredTotal: state.discoveredTotal, userStatus: userStatus, scanUsage: scanUsage, initialView: initialView, decisions: state.decisions }));
99
+ return (_jsx(InteractiveResultsView, { result: state.result, config: config, durationMs: state.durationMs, onExit: handleResultsExit, onBack: restartSelection ?? undefined, discoveredTotal: state.discoveredTotal, initialView: initialView, decisions: state.decisions }));
101
100
  case "empty":
102
101
  return _jsx(Text, { dimColor: true, children: state.message });
103
102
  case "error":
@@ -107,9 +106,6 @@ export const App = ({ config, userStatus, scanUsage, setupIssues = [], initialVi
107
106
  }
108
107
  }
109
108
  })();
110
- // Show the setup banner above the results — but not during empty/error/
111
- // free_cap_reached phases where it would compete with the message the user
112
- // actually needs to read.
113
109
  const showBanner = state.phase === "selecting" &&
114
110
  setupIssues.length > 0;
115
111
  // Results fill the terminal height exactly; an extra line there overflows the alt screen.
@@ -5,10 +5,12 @@ const HIDE_CURSOR = `${ESC}[?25l`;
5
5
  const SHOW_CURSOR = `${ESC}[?25h`;
6
6
  const MOUSE_1000_OFF = `${ESC}[?1000l`;
7
7
  const MOUSE_1003_OFF = `${ESC}[?1003l`;
8
- const CLEAR_HOME = `${ESC}[2J${ESC}[H`;
8
+ export const CLEAR_HOME = `${ESC}[2J${ESC}[H`;
9
9
  const CLEAR_LINE = `\r${ESC}[2K`;
10
10
  const TITLE = "Dependency Guardian";
11
11
  const TITLE_OSC = `${ESC}]0;${TITLE}\x07`;
12
+ const TITLE_PUSH = `${ESC}[22;0t`;
13
+ const TITLE_POP = `${ESC}[23;0t`;
12
14
  let tuiActive = false;
13
15
  let tuiHandle = null;
14
16
  let signalRegistrations = [];
@@ -43,7 +45,7 @@ function removeRestoreHandlers() {
43
45
  export function enterTui() {
44
46
  if (!process.stdout.isTTY || tuiActive)
45
47
  return;
46
- process.stdout.write(CLEAR_LINE + ALT_SCREEN_ON + HIDE_CURSOR + MOUSE_1000_OFF + MOUSE_1003_OFF + CLEAR_HOME + TITLE_OSC);
48
+ process.stdout.write(CLEAR_LINE + ALT_SCREEN_ON + HIDE_CURSOR + MOUSE_1000_OFF + MOUSE_1003_OFF + CLEAR_HOME + TITLE_PUSH + TITLE_OSC);
47
49
  tuiActive = true;
48
50
  installRestoreHandlers();
49
51
  if (!tuiHandle)
@@ -56,7 +58,7 @@ export function leaveTui() {
56
58
  clearInterval(tuiHandle);
57
59
  tuiHandle = null;
58
60
  }
59
- process.stdout.write(MOUSE_1003_OFF + MOUSE_1000_OFF + SHOW_CURSOR + ALT_SCREEN_OFF);
61
+ process.stdout.write(MOUSE_1003_OFF + MOUSE_1000_OFF + SHOW_CURSOR + ALT_SCREEN_OFF + TITLE_POP);
60
62
  tuiActive = false;
61
63
  removeRestoreHandlers();
62
64
  }
@@ -65,11 +67,6 @@ export function clearScreen() {
65
67
  return;
66
68
  process.stdout.write(CLEAR_HOME);
67
69
  }
68
- export function hideCursor() {
69
- if (!process.stdout.isTTY)
70
- return;
71
- process.stdout.write(HIDE_CURSOR);
72
- }
73
70
  export function showCursor() {
74
71
  if (!process.stdout.isTTY)
75
72
  return;