@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
@@ -0,0 +1,127 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { resolveDgPaths } from "../state/index.js";
4
+ import { claudeCodeIntegration } from "./claude-code.js";
5
+ import { codexIntegration } from "./codex.js";
6
+ import { copilotCliIntegration } from "./copilot-cli.js";
7
+ import { cursorIntegration } from "./cursor.js";
8
+ import { geminiIntegration } from "./gemini.js";
9
+ import { LEGACY_AGENT_HOOK_SENTINEL } from "./persistence.js";
10
+ import { windsurfIntegration } from "./windsurf.js";
11
+ export const AGENTS = {
12
+ "claude-code": claudeCodeIntegration,
13
+ codex: codexIntegration,
14
+ cursor: cursorIntegration,
15
+ "copilot-cli": copilotCliIntegration,
16
+ gemini: geminiIntegration,
17
+ windsurf: windsurfIntegration,
18
+ };
19
+ export const AGENT_IDS = Object.keys(AGENTS);
20
+ export function isAgentId(value) {
21
+ return value in AGENTS;
22
+ }
23
+ export function getAgent(id) {
24
+ return AGENTS[id];
25
+ }
26
+ export function agentLabel(id) {
27
+ return isAgentId(id) ? AGENTS[id].label : id;
28
+ }
29
+ export function agentHookSignature(agent) {
30
+ return `hook-exec ${agent}`;
31
+ }
32
+ export function defaultDgCommand(agent) {
33
+ let bin = process.argv[1] ?? "dg";
34
+ try {
35
+ bin = realpathSync(bin);
36
+ }
37
+ catch {
38
+ // keep argv[1]; the hook still resolves via PATH as a fallback
39
+ }
40
+ // Absolute node + script path so the agent's non-interactive subprocess
41
+ // can't lose it to a PATH change (the whole point of the hook).
42
+ return `${process.execPath} ${bin} ${agentHookSignature(agent)}`;
43
+ }
44
+ export function resolveAgentHookContext(agent, options = {}) {
45
+ const env = options.env ?? process.env;
46
+ const home = options.home ?? env.HOME ?? homedir();
47
+ return {
48
+ agent,
49
+ settingsPath: AGENTS[agent].configPath(home),
50
+ dgCommand: options.dgCommand ?? defaultDgCommand(agent),
51
+ paths: resolveDgPaths(env),
52
+ };
53
+ }
54
+ export function applyAgentHook(ctx) {
55
+ return AGENTS[ctx.agent].apply(ctx);
56
+ }
57
+ export function removeAgentHookForAgent(ctx) {
58
+ return AGENTS[ctx.agent].remove(ctx);
59
+ }
60
+ export function verifyAgentHook(ctx) {
61
+ return AGENTS[ctx.agent].verify(ctx);
62
+ }
63
+ function agentFromSentinel(sentinel) {
64
+ if (!sentinel || sentinel === LEGACY_AGENT_HOOK_SENTINEL) {
65
+ return "claude-code";
66
+ }
67
+ if (!sentinel.startsWith(`${LEGACY_AGENT_HOOK_SENTINEL}:`)) {
68
+ return null;
69
+ }
70
+ const id = sentinel.slice(LEGACY_AGENT_HOOK_SENTINEL.length + 1);
71
+ return isAgentId(id) ? id : null;
72
+ }
73
+ export function reverseAgentHookEntry(entry, removed, missing, warnings) {
74
+ const agent = agentFromSentinel(entry.sentinel);
75
+ if (!agent) {
76
+ warnings.push(`${entry.path}: unrecognized agent hook entry (${entry.sentinel ?? "no sentinel"}); left untouched`);
77
+ return;
78
+ }
79
+ AGENTS[agent].reverseEntry(entry, removed, missing, warnings);
80
+ }
81
+ export function collectAgentSkips(options) {
82
+ const skips = [];
83
+ const env = options?.env ?? process.env;
84
+ const home = options?.home ?? env.HOME ?? homedir();
85
+ for (const agent of AGENT_IDS) {
86
+ try {
87
+ const integration = AGENTS[agent];
88
+ if (!integration.detect(home)) {
89
+ continue;
90
+ }
91
+ const probe = integration.probeHookSupport(home);
92
+ if (!probe.supported) {
93
+ skips.push({ agent, label: integration.label, detail: probe.detail });
94
+ }
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ }
100
+ return skips;
101
+ }
102
+ export function collectAgentOffers(options) {
103
+ const offers = [];
104
+ const env = options?.env ?? process.env;
105
+ const home = options?.home ?? env.HOME ?? homedir();
106
+ for (const agent of AGENT_IDS) {
107
+ try {
108
+ const integration = AGENTS[agent];
109
+ if (!integration.detect(home)) {
110
+ continue;
111
+ }
112
+ const probe = integration.probeHookSupport(home);
113
+ if (!probe.supported) {
114
+ continue;
115
+ }
116
+ const ctx = resolveAgentHookContext(agent, options ?? {});
117
+ const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
118
+ if (!hooked) {
119
+ offers.push({ agent, label: integration.label, ctx, probe });
120
+ }
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ }
126
+ return offers;
127
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import { join } from "node:path";
2
+ import { dirExists, mergedJsonHook } from "./persistence.js";
3
+ const SIGNATURE = "hook-exec windsurf";
4
+ function configPath(home) {
5
+ return join(home, ".codeium", "windsurf", "hooks.json");
6
+ }
7
+ function isDgEntry(entry) {
8
+ return (typeof entry === "object" &&
9
+ entry !== null &&
10
+ typeof entry.command === "string" &&
11
+ entry.command.includes(SIGNATURE));
12
+ }
13
+ function insertHook(settings, dgCommand) {
14
+ const next = { ...settings };
15
+ const hooks = typeof next.hooks === "object" && next.hooks !== null ? { ...next.hooks } : {};
16
+ const list = Array.isArray(hooks.pre_run_command) ? hooks.pre_run_command : [];
17
+ hooks.pre_run_command = [...list.filter((entry) => !isDgEntry(entry)), { command: dgCommand }];
18
+ next.hooks = hooks;
19
+ return next;
20
+ }
21
+ function removeHook(settings) {
22
+ const next = { ...settings };
23
+ if (typeof next.hooks !== "object" || next.hooks === null) {
24
+ return { settings: next, changed: false, empty: Object.keys(next).length === 0 };
25
+ }
26
+ const hooks = { ...next.hooks };
27
+ const list = Array.isArray(hooks.pre_run_command) ? hooks.pre_run_command : [];
28
+ const withoutDg = list.filter((entry) => !isDgEntry(entry));
29
+ const changed = withoutDg.length !== list.length;
30
+ if (withoutDg.length > 0) {
31
+ hooks.pre_run_command = withoutDg;
32
+ }
33
+ else {
34
+ delete hooks.pre_run_command;
35
+ }
36
+ if (Object.keys(hooks).length > 0) {
37
+ next.hooks = hooks;
38
+ }
39
+ else {
40
+ delete next.hooks;
41
+ }
42
+ return { settings: next, changed, empty: Object.keys(next).length === 0 };
43
+ }
44
+ function hasDgHook(settings) {
45
+ const hooks = settings.hooks?.pre_run_command;
46
+ return Array.isArray(hooks) && hooks.some((entry) => isDgEntry(entry));
47
+ }
48
+ function parseInput(stdin) {
49
+ try {
50
+ const obj = JSON.parse(stdin);
51
+ const command = obj.tool_info?.command_line;
52
+ if (typeof command !== "string") {
53
+ return null;
54
+ }
55
+ const cwd = obj.tool_info?.cwd;
56
+ return typeof cwd === "string" ? { command, cwd } : { command };
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function emitDecision(verdict) {
63
+ if (verdict.decision === "allow") {
64
+ return { stdout: "", exitCode: 0 };
65
+ }
66
+ const reason = verdict.reason ?? "Dependency Guardian firewall";
67
+ const suffix = verdict.decision === "ask" ? " (flagged for review — run the install in a terminal to decide)" : "";
68
+ return { stdout: `${reason}${suffix}\n`, exitCode: 2 };
69
+ }
70
+ function probeHookSupport(home) {
71
+ if (!dirExists(join(home, ".codeium", "windsurf"))) {
72
+ return { supported: false, detail: "~/.codeium/windsurf not found (is Windsurf installed?)" };
73
+ }
74
+ return { supported: true, detail: "Windsurf config directory found; dg cannot read the app version from disk" };
75
+ }
76
+ export const windsurfIntegration = {
77
+ id: "windsurf",
78
+ label: "Windsurf",
79
+ kind: "merged-json",
80
+ maturity: "unverified",
81
+ minVersion: null,
82
+ configPath,
83
+ detect: (home) => dirExists(join(home, ".codeium", "windsurf")),
84
+ probeHookSupport,
85
+ parseInput,
86
+ emitDecision,
87
+ ...mergedJsonHook({
88
+ checkName: "dg pre_run_command hook",
89
+ insert: insertHook,
90
+ remove: removeHook,
91
+ isInstalled: hasDgHook,
92
+ }),
93
+ };
@@ -103,7 +103,8 @@ export async function analyzePackages(packages, options) {
103
103
  scanId: options.scanId ?? randomUUID(),
104
104
  fetchImpl: options.fetchImpl ?? fetch,
105
105
  timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
106
- ...(options.signal ? { signal: options.signal } : {})
106
+ ...(options.signal ? { signal: options.signal } : {}),
107
+ ...(options.cooldown ? { cooldown: options.cooldown } : {})
107
108
  };
108
109
  const total = packages.length;
109
110
  const batches = [];
@@ -178,7 +179,7 @@ function delay(ms) {
178
179
  return new Promise((resolve) => setTimeout(resolve, ms));
179
180
  }
180
181
  async function analyzeBatch(context, batch, onBatchProgress) {
181
- const { url, token, deviceId, scanId, fetchImpl, timeoutMs, signal } = context;
182
+ const { url, token, deviceId, scanId, fetchImpl, timeoutMs, signal, cooldown } = context;
182
183
  const controller = new AbortController();
183
184
  let timedOut = false;
184
185
  let silenceTimer;
@@ -207,7 +208,8 @@ async function analyzeBatch(context, batch, onBatchProgress) {
207
208
  ...(token ? { Authorization: `Bearer ${token}` } : {})
208
209
  },
209
210
  body: JSON.stringify({
210
- packages: batch.map((entry) => ({ name: entry.name, version: entry.version }))
211
+ packages: batch.map((entry) => ({ name: entry.name, version: entry.version })),
212
+ ...(cooldown ? { cooldown } : {})
211
213
  }),
212
214
  signal: controller.signal
213
215
  });
@@ -326,7 +328,7 @@ function normalizeAction(action) {
326
328
  if (action === "block" || action === "warn" || action === "analysis_incomplete" || action === "pass") {
327
329
  return action;
328
330
  }
329
- return "pass";
331
+ return "analysis_incomplete";
330
332
  }
331
333
  export function mergeAnalyzeResponses(responses) {
332
334
  const first = responses[0];
@@ -133,17 +133,6 @@ function contentChecks(file, push) {
133
133
  }
134
134
  function structuralProjectChecks(context, push) {
135
135
  if (context.ecosystem === "npm" && context.packageJson) {
136
- if (context.packageJson.private === true) {
137
- push({
138
- id: "publishing-private",
139
- category: "structural",
140
- severity: 4,
141
- title: "Package is marked private but is being audited for publish",
142
- recommendation: "A private package being published is almost always a mistake — remove \"private\" or do not publish.",
143
- location: "package.json",
144
- evidence: "\"private\": true"
145
- });
146
- }
147
136
  if (!context.hasFilesAllowlist) {
148
137
  push({
149
138
  id: "no-files-allowlist",
@@ -1,33 +1,17 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { authStatus } from "../auth/store.js";
4
- import { loadUserConfig } from "../config/settings.js";
5
3
  import { resolveDgPaths } from "../state/index.js";
6
4
  export function auditLogPath(paths) {
7
5
  return join(paths.stateDir, "audit.jsonl");
8
6
  }
9
- export function webhookOutboxPath(paths) {
10
- return join(paths.stateDir, "webhooks.jsonl");
11
- }
12
7
  export function recordAuditEvent(event, env = process.env) {
13
- const paths = resolveDgPaths(env);
14
- appendJsonLine(auditLogPath(paths), event);
15
- }
16
- export function emitWebhookEvent(event, env = process.env) {
17
- const config = loadUserConfig(env);
18
- const status = authStatus(env);
19
- if (!config.webhooks.enabled || !status.authenticated) {
8
+ try {
9
+ appendJsonLine(auditLogPath(resolveDgPaths(env)), event);
10
+ return true;
11
+ }
12
+ catch {
20
13
  return false;
21
14
  }
22
- const paths = resolveDgPaths(env);
23
- appendJsonLine(webhookOutboxPath(paths), {
24
- ...event,
25
- auth: {
26
- source: status.source,
27
- token: status.tokenPreview
28
- }
29
- });
30
- return true;
31
15
  }
32
16
  function appendJsonLine(path, value) {
33
17
  mkdirSync(dirname(path), {
@@ -3,8 +3,10 @@ import { useCallback, useEffect, useState } from "react";
3
3
  import { Box, useApp } from "ink";
4
4
  import { combineAction, auditExitCode, displayTarget } from "../commands/audit.js";
5
5
  import { AuditResultsView } from "./components/AuditResultsView.js";
6
+ import { useResizeRepaint } from "../scan-ui/hooks/useResizeRepaint.js";
6
7
  export const AuditApp = ({ gathered, initialDeep, deepPromise, onExitCode }) => {
7
8
  const { exit } = useApp();
9
+ useResizeRepaint();
8
10
  const [deep, setDeep] = useState(initialDeep);
9
11
  useEffect(() => {
10
12
  if (!deepPromise)
@@ -1,9 +1,7 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useMemo, useRef, useState } from "react";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import chalk from "chalk";
5
- import { writeFileSync } from "node:fs";
6
- import { resolve as resolvePath } from "node:path";
7
5
  import { findingLocation } from "../../audit/detectors.js";
8
6
  import { isLoggedIn } from "../../scan-ui/shims.js";
9
7
  import { clearScreen } from "../../scan-ui/alt-screen.js";
@@ -12,7 +10,10 @@ import { pad, truncate } from "../../scan-ui/format-helpers.js";
12
10
  import { AuditHeader } from "./AuditHeader.js";
13
11
  import { DeepStatusRow } from "./DeepStatusRow.js";
14
12
  import { countSummary, severityGlyph, severityRole } from "../format.js";
15
- import { buildExport, exportFilename } from "../export.js";
13
+ import { buildExport } from "../export.js";
14
+ import { ExportDialog, loginRequiredToast } from "../../export-ui/ExportDialog.js";
15
+ import { resolvePresentation } from "../../presentation/mode.js";
16
+ import { createTheme } from "../../presentation/theme.js";
16
17
  const ROLE_PAINT = {
17
18
  block: chalk.red,
18
19
  warn: chalk.yellow,
@@ -23,6 +24,11 @@ function glyphPaint(severity) {
23
24
  }
24
25
  const FIXED_CHROME = 12;
25
26
  const EXPAND_CHROME = 10;
27
+ const EXPORT_FORMATS = [
28
+ { format: "json", label: "JSON" },
29
+ { format: "md", label: "Markdown" },
30
+ { format: "txt", label: "Plain text" }
31
+ ];
26
32
  export const AuditResultsView = ({ findings, action, artifact, ecosystem, target, fileCount, publishSetSource, deep, onExit }) => {
27
33
  const { rows: termRows, cols: termCols } = useTerminalSize();
28
34
  const [cursor, setCursor] = useState(0);
@@ -36,17 +42,22 @@ export const AuditResultsView = ({ findings, action, artifact, ecosystem, target
36
42
  const [showHelp, setShowHelp] = useState(false);
37
43
  const showHelpRef = useRef(showHelp);
38
44
  showHelpRef.current = showHelp;
39
- const [exportMenu, setExportMenu] = useState(null);
40
- const exportMenuRef = useRef(exportMenu);
41
- exportMenuRef.current = exportMenu;
45
+ const [exportDialog, setExportDialog] = useState(null);
46
+ const exportDialogRef = useRef(exportDialog);
47
+ exportDialogRef.current = exportDialog;
48
+ const theme = useMemo(() => createTheme(resolvePresentation().color), []);
42
49
  const [exportMsg, setExportMsg] = useState(null);
43
50
  const exportTimer = useRef(null);
44
- const showExportMsg = (message) => {
45
- setExportMsg(message);
51
+ const showExportMsg = (text, tone = "ok") => {
52
+ setExportMsg({ text, tone });
46
53
  if (exportTimer.current)
47
54
  clearTimeout(exportTimer.current);
48
55
  exportTimer.current = setTimeout(() => setExportMsg(null), 4000);
49
56
  };
57
+ useEffect(() => () => {
58
+ if (exportTimer.current)
59
+ clearTimeout(exportTimer.current);
60
+ }, []);
50
61
  const filtered = useMemo(() => {
51
62
  if (!searchQuery)
52
63
  return findings;
@@ -88,70 +99,40 @@ export const AuditResultsView = ({ findings, action, artifact, ecosystem, target
88
99
  setViewport(clamped - listRows + 1);
89
100
  }
90
101
  };
91
- const runExport = (format) => {
102
+ const openExport = () => {
92
103
  if (!isLoggedIn()) {
93
- showExportMsg("Export requires `dg login` (free account)");
104
+ showExportMsg(loginRequiredToast(), "nudge");
94
105
  return;
95
106
  }
96
- try {
97
- const input = {
98
- target,
99
- artifact,
100
- ecosystem,
101
- action,
102
- fileCount,
103
- publishSetSource,
104
- findings,
105
- deep: deep ?? { ran: false, reason: "deep behavioral scan did not run" }
106
- };
107
- const { body, ext } = buildExport(input, format);
108
- const filename = exportFilename(ext);
109
- writeFileSync(resolvePath(process.cwd(), filename), body, "utf-8");
110
- showExportMsg(`✓ Exported to ${filename}`);
111
- }
112
- catch (error) {
113
- showExportMsg(`Export failed: ${error.message}`);
114
- }
107
+ const input = {
108
+ target,
109
+ artifact,
110
+ ecosystem,
111
+ action,
112
+ fileCount,
113
+ publishSetSource,
114
+ findings,
115
+ deep: deep ?? { ran: false, reason: "deep behavioral scan did not run" }
116
+ };
117
+ setExportDialog(EXPORT_FORMATS.map(({ format, label }) => ({
118
+ label,
119
+ defaultName: `dg-audit.${format}`,
120
+ render: () => buildExport(input, format).body
121
+ })));
122
+ };
123
+ const handleExportDone = (result) => {
124
+ setExportDialog(null);
125
+ clearScreen();
126
+ if (result === null)
127
+ return;
128
+ if ("path" in result)
129
+ showExportMsg(`✓ Exported to ${result.path}`);
130
+ else
131
+ showExportMsg(`Export failed: ${result.error}`, "error");
115
132
  };
116
133
  useInput((input, key) => {
117
- if (exportMenuRef.current) {
118
- const menu = exportMenuRef.current;
119
- const FORMATS = ["json", "md", "txt"];
120
- const move = (cur, dir) => {
121
- const i = FORMATS.indexOf(cur);
122
- return FORMATS[Math.max(0, Math.min(FORMATS.length - 1, i + dir))] ?? cur;
123
- };
124
- if (key.escape) {
125
- setExportMenu(null);
126
- return;
127
- }
128
- if (input === "q") {
129
- setExportMenu(null);
130
- return;
131
- }
132
- if (key.return) {
133
- const format = menu.format;
134
- setExportMenu(null);
135
- clearScreen();
136
- runExport(format);
137
- return;
138
- }
139
- if (key.tab || key.leftArrow || key.rightArrow) {
140
- setExportMenu({ ...menu, activeRow: menu.activeRow === "scope" ? "format" : "scope" });
141
- return;
142
- }
143
- if (key.upArrow || input === "k") {
144
- if (menu.activeRow === "format")
145
- setExportMenu({ ...menu, format: move(menu.format, -1) });
146
- return;
147
- }
148
- if (key.downArrow || input === "j") {
149
- if (menu.activeRow === "format")
150
- setExportMenu({ ...menu, format: move(menu.format, 1) });
151
- return;
152
- }
134
+ if (exportDialogRef.current)
153
135
  return;
154
- }
155
136
  if (showHelpRef.current) {
156
137
  if (input === "?" || key.escape)
157
138
  setShowHelp(false);
@@ -219,11 +200,7 @@ export const AuditResultsView = ({ findings, action, artifact, ecosystem, target
219
200
  return;
220
201
  }
221
202
  if (input === "e") {
222
- if (!isLoggedIn()) {
223
- showExportMsg("Export requires `dg login` (free account)");
224
- return;
225
- }
226
- setExportMenu({ format: "json", activeRow: "scope" });
203
+ openExport();
227
204
  return;
228
205
  }
229
206
  return;
@@ -233,11 +210,7 @@ export const AuditResultsView = ({ findings, action, artifact, ecosystem, target
233
210
  return;
234
211
  }
235
212
  if (input === "e") {
236
- if (!isLoggedIn()) {
237
- showExportMsg("Export requires `dg login` (free account)");
238
- return;
239
- }
240
- setExportMenu({ format: "json", activeRow: "scope" });
213
+ openExport();
241
214
  return;
242
215
  }
243
216
  if (input === "q") {
@@ -269,19 +242,9 @@ export const AuditResultsView = ({ findings, action, artifact, ecosystem, target
269
242
  }
270
243
  else if (input === "/")
271
244
  setSearchMode(true);
272
- });
273
- if (exportMenu) {
274
- const FORMATS_RENDER = [
275
- { value: "json", label: "JSON" },
276
- { value: "md", label: "Markdown" },
277
- { value: "txt", label: "Plain text" }
278
- ];
279
- const renderColumn = (title, isActive, rows) => (_jsxs(Box, { flexDirection: "column", marginRight: 4, children: [_jsxs(Text, { children: [isActive ? chalk.cyan("▌ ") : " ", isActive ? chalk.bold(title) : chalk.dim(title)] }), rows] }));
280
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(AuditHeader, { action: action, artifact: artifact, ecosystem: ecosystem, countSummary: countSummary(findings), fileCount: fileCount, fallback: publishSetSource === "fallback" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, children: [_jsx(Text, { bold: true, children: "Export" }), _jsx(Text, { children: "" }), _jsxs(Box, { flexDirection: "row", children: [renderColumn("What", exportMenu.activeRow === "scope", (_jsxs(Box, { children: [_jsx(Box, { width: 5, flexShrink: 0, children: _jsxs(Text, { children: [" ", exportMenu.activeRow === "scope" ? chalk.cyan("●") : chalk.green("●")] }) }), _jsx(Text, { children: chalk.bold("Findings") })] }))), renderColumn("Format", exportMenu.activeRow === "format", (_jsx(_Fragment, { children: FORMATS_RENDER.map((r) => {
281
- const selected = r.value === exportMenu.format;
282
- const bullet = selected ? (exportMenu.activeRow === "format" ? chalk.cyan("●") : chalk.green("●")) : chalk.dim("○");
283
- return (_jsxs(Box, { children: [_jsx(Box, { width: 5, flexShrink: 0, children: _jsxs(Text, { children: [" ", bullet] }) }), _jsx(Text, { children: selected ? chalk.bold(r.label) : r.label })] }, r.value));
284
- }) })))] })] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("↑↓"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("←→/Tab"), " ", chalk.dim("switch row"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim("export"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("cancel")] })] }));
245
+ }, { isActive: exportDialog === null });
246
+ if (exportDialog) {
247
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(AuditHeader, { action: action, artifact: artifact, ecosystem: ecosystem, countSummary: countSummary(findings), fileCount: fileCount, fallback: publishSetSource === "fallback" }), _jsx(ExportDialog, { options: exportDialog, theme: theme, cwd: process.cwd(), onDone: handleExportDone })] }));
285
248
  }
286
249
  if (showHelp) {
287
250
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(AuditHeader, { action: action, artifact: artifact, ecosystem: ecosystem, countSummary: countSummary(findings), fileCount: fileCount, fallback: publishSetSource === "fallback" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: " Navigation" }), _jsxs(Text, { children: [" ", chalk.cyan("↑ k"), " ", chalk.dim("Move up")] }), _jsxs(Text, { children: [" ", chalk.cyan("↓ j"), " ", chalk.dim("Move down")] }), _jsxs(Text, { children: [" ", chalk.cyan("g"), " ", chalk.dim("Jump to top")] }), _jsxs(Text, { children: [" ", chalk.cyan("G"), " ", chalk.dim("Jump to bottom")] }), _jsxs(Text, { children: [" ", chalk.cyan("PgUp"), " ", chalk.dim("Page up")] }), _jsxs(Text, { children: [" ", chalk.cyan("PgDn"), " ", chalk.dim("Page down")] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: " Actions" }), _jsxs(Text, { children: [" ", chalk.cyan("⏎"), " ", chalk.dim("Expand / collapse finding (evidence + recommendation)")] }), _jsxs(Text, { children: [" ", chalk.cyan("/"), " ", chalk.dim("Search findings")] }), _jsxs(Text, { children: [" ", chalk.cyan("e"), " ", chalk.dim("Export menu — Findings as JSON / Markdown / text")] }), _jsxs(Text, { children: [" ", chalk.cyan("q"), " ", chalk.dim("Quit")] }), _jsx(Text, { children: "" }), _jsxs(Text, { dimColor: true, children: [" Press ", chalk.bold.cyan("?"), " or ", chalk.bold.cyan("Esc"), " to close"] })] })] }));
@@ -303,5 +266,5 @@ export const AuditResultsView = ({ findings, action, artifact, ecosystem, target
303
266
  return (_jsxs(Text, { backgroundColor: "#1a1a2e", children: [chalk.cyan("▌"), " ", paint(glyph), " ", chalk.bold(loc), chalk.dim(title)] }, `${finding.id}|${finding.location}`));
304
267
  }
305
268
  return (_jsxs(Text, { children: [" ", paint(glyph), " ", loc, chalk.dim(title)] }, `${finding.id}|${finding.location}`));
306
- }), belowCount > 0 && _jsxs(Text, { dimColor: true, children: [chalk.cyan(" ↓"), " ", belowCount, " more below"] }), expanded && focused && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 4, children: [_jsxs(Text, { bold: true, children: [findingLocation(focused), chalk.dim(" — "), focused.title] }), expandLines.slice(expandScroll, expandScroll + expandViewportRows).map((line, i) => (_jsx(Text, { children: line }, i))), expandLines.length > expandViewportRows && (_jsx(Text, { dimColor: true, children: ` ${expandScroll + 1}-${Math.min(expandScroll + expandViewportRows, expandLines.length)} / ${expandLines.length} (Esc collapse)` }))] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(DeepStatusRow, { deep: deep }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("█"), " ", chalk.dim("Esc clear · Enter confirm")] })) : (_jsxs(Text, { children: [" ", filtered.length > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("↑↓"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim(expanded ? "collapse" : "expand"), " ", chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", chalk.bold.cyan("?"), " ", chalk.dim("help"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] }))] }));
269
+ }), belowCount > 0 && _jsxs(Text, { dimColor: true, children: [chalk.cyan(" ↓"), " ", belowCount, " more below"] }), expanded && focused && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 4, children: [_jsxs(Text, { bold: true, children: [findingLocation(focused), chalk.dim(" — "), focused.title] }), expandLines.slice(expandScroll, expandScroll + expandViewportRows).map((line, i) => (_jsx(Text, { children: line }, i))), expandLines.length > expandViewportRows && (_jsx(Text, { dimColor: true, children: ` ${expandScroll + 1}-${Math.min(expandScroll + expandViewportRows, expandLines.length)} / ${expandLines.length} (Esc collapse)` }))] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(DeepStatusRow, { deep: deep }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("█"), " ", chalk.dim("Esc clear · Enter confirm")] })) : (_jsxs(Text, { children: [" ", filtered.length > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("↑↓"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim(expanded ? "collapse" : "expand"), " ", chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", chalk.bold.cyan("?"), " ", chalk.dim("help"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsg && _jsxs(_Fragment, { children: [" ", exportMsg.tone === "nudge" ? exportMsg.text : (exportMsg.tone === "error" ? chalk.red : chalk.green)(exportMsg.text)] })] }))] }));
307
270
  };
@@ -78,7 +78,3 @@ export function buildExport(input, format) {
78
78
  return { body: buildMd(input), ext: "md" };
79
79
  return { body: buildTxt(input), ext: "txt" };
80
80
  }
81
- export function exportFilename(format, now = new Date()) {
82
- const ts = now.toISOString().replace(/[:T]/g, "-").replace(/\..*$/, "");
83
- return `dg-audit-${ts}-findings.${format}`;
84
- }
@@ -17,9 +17,6 @@ export function severityRole(severity) {
17
17
  export function verdictGlyph(action) {
18
18
  return action === "block" ? "✘" : action === "warn" ? WARN_GLYPH : "✓";
19
19
  }
20
- export function verdictRole(action) {
21
- return action;
22
- }
23
20
  export function countSummary(findings) {
24
21
  const blocking = findings.filter((finding) => finding.severity >= 4).length;
25
22
  const warnings = findings.filter((finding) => finding.severity === 3).length;
@@ -3,11 +3,18 @@ export function shouldLaunchAuditTui(options) {
3
3
  if (options.format !== "text" || options.outputPath) {
4
4
  return false;
5
5
  }
6
+ if (process.env.TERM === "dumb") {
7
+ return false;
8
+ }
9
+ if (!process.stdin.isTTY) {
10
+ return false;
11
+ }
6
12
  return resolvePresentation().mode === "rich";
7
13
  }
8
14
  export async function launchAuditTui(props) {
9
15
  const ci = process.env.CI;
10
- if (ci === "" || ci === "0" || ci === "false") {
16
+ const ciCleared = ci === "" || ci === "0" || ci === "false";
17
+ if (ciCleared) {
11
18
  delete process.env.CI;
12
19
  }
13
20
  const [{ render }, react, app, altScreen] = await Promise.all([
@@ -28,6 +35,8 @@ export async function launchAuditTui(props) {
28
35
  await instance.waitUntilExit();
29
36
  }
30
37
  finally {
38
+ if (ciCleared)
39
+ process.env.CI = ci;
31
40
  altScreen.leaveTui();
32
41
  }
33
42
  return exitCode;
@@ -191,6 +191,7 @@ export async function fetchAccountStatus(token, env, fetchImpl, timeoutMs = 5_00
191
191
  const body = (await response.json());
192
192
  return {
193
193
  tier: typeof body.tier === "string" && body.tier.length > 0 ? body.tier.toLowerCase() : null,
194
+ name: typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : null,
194
195
  scansUsed: typeof body.scansUsed === "number" && Number.isFinite(body.scansUsed) ? body.scansUsed : null,
195
196
  scansLimit: typeof body.scansLimit === "number" && Number.isFinite(body.scansLimit) ? body.scansLimit : null
196
197
  };
@@ -202,9 +203,6 @@ export async function fetchAccountStatus(token, env, fetchImpl, timeoutMs = 5_00
202
203
  clearTimeout(timer);
203
204
  }
204
205
  }
205
- export async function fetchAccountTier(token, env, fetchImpl) {
206
- return (await fetchAccountStatus(token, env, fetchImpl))?.tier ?? null;
207
- }
208
206
  export async function runDeviceLogin(io = {}) {
209
207
  const env = io.env ?? process.env;
210
208
  const fetchImpl = io.fetchImpl ?? fetch;
@@ -236,8 +234,9 @@ export async function runDeviceLogin(io = {}) {
236
234
  for (;;) {
237
235
  const result = await pollAuthSession(webBase, session.sessionId, fetchImpl);
238
236
  if (result.status === "complete" && result.apiKey) {
239
- const tier = await fetchAccountTier(result.apiKey, env, fetchImpl);
240
- writeAuthState({ token: result.apiKey, email: result.email, tier: tier ?? undefined });
237
+ const account = await fetchAccountStatus(result.apiKey, env, fetchImpl);
238
+ const tier = account?.tier ?? null;
239
+ writeAuthState({ token: result.apiKey, email: result.email, tier: tier ?? undefined, name: account?.name ?? undefined });
241
240
  const who = result.email ? ` as ${result.email}` : "";
242
241
  return {
243
242
  exitCode: 0,