@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
@@ -328,7 +328,7 @@ function normalizeAction(action) {
328
328
  if (action === "block" || action === "warn" || action === "analysis_incomplete" || action === "pass") {
329
329
  return action;
330
330
  }
331
- return "pass";
331
+ return "analysis_incomplete";
332
332
  }
333
333
  export function mergeAnalyzeResponses(responses) {
334
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), {
@@ -384,7 +384,7 @@ export const CONTENT_RULES = [
384
384
  {
385
385
  id: "db-connection-string",
386
386
  re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:@\s/]+:[^@\s/]+@[^/\s]+/u,
387
- allow: /:\/\/[^:@/]+:(password|pass|user|example|changeme|xxx+|\*+)@|@(localhost|127\.0\.0\.1|example\.|host)/iu,
387
+ allow: /:\/\/[^:@/]+:(password|pass|user|example|changeme|xxx+|\*+)@|@(localhost|127\.0\.0\.1|example\.|host\b)/iu,
388
388
  category: "bundled-secret",
389
389
  severity: 4,
390
390
  title: "Database connection string with password embedded in a published file",
@@ -421,6 +421,10 @@ export const RISKY_SCRIPT_NAMES = [
421
421
  "preuninstall",
422
422
  "uninstall"
423
423
  ];
424
- export const DANGEROUS_SCRIPT_RE = /\|\s*(?:sh|bash|zsh)\b|\beval\s|node\s+-e\b|python[0-9.]*\s+-c\b|base64\s+-d|(?:curl|wget|fetch)\b[^\n]*\|\s*\w*sh\b/iu;
424
+ // Best-effort heuristic on the LOCAL audit only; a public regex is evadable, so
425
+ // the authoritative verdict is the behavioral scanner. Covers pipe-to-shell, eval,
426
+ // node -e, python -c, base64 -d, curl|sh, AND download-then-execute where the fetch
427
+ // is chained (&& / ;) to a shell or a freshly-downloaded file.
428
+ export const DANGEROUS_SCRIPT_RE = /\|\s*(?:sh|bash|zsh)\b|\beval\s|node\s+-e\b|python[0-9.]*\s+-c\b|base64\s+-d|(?:curl|wget|fetch)\b[^\n]*\|\s*\w*sh\b|(?:curl|wget|fetch)\b[^\n]*(?:&&|;)\s*(?:sh|bash|zsh|source|\.\/)/iu;
425
429
  export const INVISIBLE_UNICODE_RE = /[\u200B-\u200F\u2060-\u2064\u202A-\u202E\u2066-\u2069\uFEFF]/u;
426
430
  export const BIDI_OVERRIDE_RE = /[\u202A-\u202E\u2066-\u2069]/u;
@@ -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,
@@ -3,10 +3,10 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
4
  import { Spinner } from "../scan-ui/components/Spinner.js";
5
5
  import { displayTier, writeAuthState } from "./store.js";
6
- import { POLL_INTERVAL_MS, POLL_TIMEOUT_MS, createAuthSession, fetchAccountTier, openBrowser, pollAuthSession, resolveWebBase } from "./device-login.js";
6
+ import { POLL_INTERVAL_MS, POLL_TIMEOUT_MS, createAuthSession, fetchAccountStatus, openBrowser, pollAuthSession, resolveWebBase } from "./device-login.js";
7
7
  const SUCCESS_MIN_MS = 600;
8
8
  const SUCCESS_MAX_MS = 2500;
9
- function useLogin(webBase, env) {
9
+ export function useLogin(webBase, env) {
10
10
  const [state, setState] = useState({ phase: "creating", verifyUrl: "", email: "", plan: "", message: "" });
11
11
  const sessionId = useRef("");
12
12
  const started = useRef(false);
@@ -58,12 +58,12 @@ function useLogin(webBase, env) {
58
58
  const email = result.email ?? "";
59
59
  writeAuthState({ token: apiKey, email: result.email });
60
60
  setState((prev) => ({ ...prev, phase: "success", email }));
61
- void fetchAccountTier(apiKey, env, fetch)
62
- .then((tier) => {
63
- if (tier) {
64
- writeAuthState({ token: apiKey, email: result.email, tier });
61
+ void fetchAccountStatus(apiKey, env, fetch)
62
+ .then((account) => {
63
+ if (account?.tier) {
64
+ writeAuthState({ token: apiKey, email: result.email, tier: account.tier, name: account.name ?? undefined });
65
65
  if (!cancelled.current) {
66
- setState((prev) => ({ ...prev, plan: tier }));
66
+ setState((prev) => ({ ...prev, plan: account.tier ?? "" }));
67
67
  }
68
68
  }
69
69
  })
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { dirname, join } from "node:path";
4
- import { loadUserConfig, saveUserConfig, withUserConfigLock } from "../config/settings.js";
4
+ import { loadUserConfig, parseUrl, saveUserConfig, withUserConfigLock } from "../config/settings.js";
5
5
  import { resolveDgPaths } from "../state/index.js";
6
6
  import { envAuthToken } from "./env-token.js";
7
7
  export class AuthError extends Error {
@@ -24,17 +24,26 @@ export function readAuthState(env = process.env) {
24
24
  if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl || parsed.loggedInAt === undefined) {
25
25
  throw new AuthError("unsupported auth state");
26
26
  }
27
+ let apiBaseUrl;
28
+ try {
29
+ apiBaseUrl = parseUrl(parsed.apiBaseUrl);
30
+ }
31
+ catch {
32
+ throw new AuthError("auth state has an invalid api base URL; run 'dg login' again");
33
+ }
27
34
  const email = typeof parsed.email === "string" && parsed.email.length > 0 ? parsed.email : undefined;
28
35
  const tier = typeof parsed.tier === "string" && parsed.tier.length > 0 ? parsed.tier : undefined;
36
+ const name = typeof parsed.name === "string" && parsed.name.length > 0 ? parsed.name : undefined;
29
37
  return {
30
38
  version: 1,
31
39
  token: parsed.token,
32
40
  tokenPreview: parsed.tokenPreview ?? redactToken(parsed.token),
33
- apiBaseUrl: parsed.apiBaseUrl,
41
+ apiBaseUrl,
34
42
  orgId: parsed.orgId ?? "",
35
43
  loggedInAt: parsed.loggedInAt,
36
44
  ...(email ? { email } : {}),
37
- ...(tier ? { tier } : {})
45
+ ...(tier ? { tier } : {}),
46
+ ...(name ? { name } : {})
38
47
  };
39
48
  }
40
49
  catch (error) {
@@ -63,6 +72,7 @@ export function writeAuthState(options, env = process.env) {
63
72
  }
64
73
  const email = typeof options.email === "string" && options.email.length > 0 ? options.email : undefined;
65
74
  const tier = typeof options.tier === "string" && options.tier.length > 0 ? options.tier : undefined;
75
+ const name = typeof options.name === "string" && options.name.length > 0 ? options.name : undefined;
66
76
  return withUserConfigLock(env, () => {
67
77
  const config = loadUserConfig(env);
68
78
  const apiBaseUrl = options.apiBaseUrl ?? config.api.baseUrl;
@@ -75,7 +85,8 @@ export function writeAuthState(options, env = process.env) {
75
85
  orgId,
76
86
  loggedInAt: (options.now ?? new Date()).toISOString(),
77
87
  ...(email ? { email } : {}),
78
- ...(tier ? { tier } : {})
88
+ ...(tier ? { tier } : {}),
89
+ ...(name ? { name } : {})
79
90
  };
80
91
  const paths = resolveDgPaths(env);
81
92
  writeJsonAtomic(authPath(paths), state);
@@ -120,7 +131,8 @@ export function authStatus(env = process.env) {
120
131
  apiBaseUrl: state.apiBaseUrl,
121
132
  orgId: state.orgId,
122
133
  ...(state.email ? { email: state.email } : {}),
123
- ...(state.tier ? { tier: state.tier } : {})
134
+ ...(state.tier ? { tier: state.tier } : {}),
135
+ ...(state.name ? { name: state.name } : {})
124
136
  };
125
137
  }
126
138
  return {
package/dist/bin/dg.js CHANGED
@@ -1,17 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { assertCurrentNode } from "../runtime/node-version.js";
3
- assertCurrentNode();
4
- const EXIT_TOOL_ERROR = 70;
5
- function exitOnFatal(error) {
6
- const message = error instanceof Error ? error.message : String(error);
7
- try {
8
- process.stderr.write(`dg: unexpected error — ${message}\nRun 'dg doctor' to check your installation; remove ~/.dg/config.json if it is corrupted.\n`);
9
- }
10
- catch {
11
- // stderr is gone; nothing left to report to.
12
- }
13
- process.exit(EXIT_TOOL_ERROR);
14
- }
3
+ await assertCurrentNode();
4
+ const { exitOnFatal } = await import("../runtime/fatal.js");
15
5
  process.on("uncaughtException", (error) => {
16
6
  if (error.code === "EPIPE") {
17
7
  process.exit(process.exitCode ?? 0);
@@ -30,52 +20,69 @@ const { runCli, writeCliResult } = await import("../runtime/cli.js");
30
20
  const { maybeShowFirstRun } = await import("../runtime/first-run.js");
31
21
  const { maybePreflightInstallPrompt } = await import("../launcher/preflight-prompt.js");
32
22
  const args = process.argv.slice(2);
33
- maybeShowFirstRun(args);
34
- import("../state/index.js")
35
- .then(({ pruneDeadSessionsSync, resolveDgPaths }) => {
36
- pruneDeadSessionsSync(resolveDgPaths());
37
- })
38
- .catch(() => { });
39
- const { maybeDeviceLogin } = await import("../auth/device-login.js");
40
- const { maybeVerifyPackage } = await import("../verify/package-check.js");
41
- const { maybeAudit } = await import("../commands/audit.js");
42
- const notHandled = { handled: false };
43
- const deviceLogin = await maybeDeviceLogin(args);
44
- const verifyPackage = deviceLogin.handled ? notHandled : await maybeVerifyPackage(args);
45
- const audit = deviceLogin.handled || verifyPackage.handled ? notHandled : await maybeAudit(args);
46
- if (deviceLogin.handled) {
47
- writeCliResult(deviceLogin.result);
48
- }
49
- else if (verifyPackage.handled) {
50
- writeCliResult(verifyPackage.result);
51
- }
52
- else if (audit.handled) {
53
- writeCliResult(audit.result);
23
+ const { maybeAgentHookExec } = await import("../launcher/agent-hook-exec.js");
24
+ const agentHookExec = await maybeAgentHookExec(args);
25
+ if (agentHookExec.handled) {
26
+ writeCliResult(agentHookExec.result);
54
27
  }
55
28
  else {
56
- const preflight = await maybePreflightInstallPrompt(args, { decisionsCwd: process.cwd() });
57
- if (preflight.handled) {
58
- writeCliResult(preflight.result);
29
+ const { maybeOfferSetupWizard } = await import("../setup-ui/offer.js");
30
+ const wizardOffer = await maybeOfferSetupWizard(args);
31
+ if (wizardOffer.handled) {
32
+ writeCliResult(wizardOffer.result);
59
33
  }
60
34
  else {
61
- const { maybeRunLiveInstall } = await import("../launcher/live-install.js");
62
- const liveInstall = await maybeRunLiveInstall(args);
63
- if (liveInstall.handled) {
64
- writeCliResult(liveInstall.result);
35
+ if (wizardOffer.result.stderr) {
36
+ process.stderr.write(wizardOffer.result.stderr);
37
+ }
38
+ maybeShowFirstRun(args);
39
+ import("../state/index.js")
40
+ .then(({ pruneDeadSessionsSync, resolveDgPaths }) => {
41
+ pruneDeadSessionsSync(resolveDgPaths());
42
+ })
43
+ .catch(() => { });
44
+ const { maybeDeviceLogin } = await import("../auth/device-login.js");
45
+ const { maybeVerifyPackage } = await import("../verify/package-check.js");
46
+ const { maybeAudit } = await import("../commands/audit.js");
47
+ const notHandled = { handled: false };
48
+ const deviceLogin = await maybeDeviceLogin(args);
49
+ const verifyPackage = deviceLogin.handled ? notHandled : await maybeVerifyPackage(args);
50
+ const audit = deviceLogin.handled || verifyPackage.handled ? notHandled : await maybeAudit(args);
51
+ if (deviceLogin.handled) {
52
+ writeCliResult(deviceLogin.result);
53
+ }
54
+ else if (verifyPackage.handled) {
55
+ writeCliResult(verifyPackage.result);
56
+ }
57
+ else if (audit.handled) {
58
+ writeCliResult(audit.result);
65
59
  }
66
60
  else {
67
- writeCliResult(await runCli(args));
61
+ const preflight = await maybePreflightInstallPrompt(args, { decisionsCwd: process.cwd() });
62
+ if (preflight.handled) {
63
+ writeCliResult(preflight.result);
64
+ }
65
+ else {
66
+ const { maybeRunLiveInstall } = await import("../launcher/live-install.js");
67
+ const liveInstall = await maybeRunLiveInstall(args);
68
+ if (liveInstall.handled) {
69
+ writeCliResult(liveInstall.result);
70
+ }
71
+ else {
72
+ writeCliResult(await runCli(args));
73
+ }
74
+ }
75
+ }
76
+ // The auth flows (browser login, paid-verify gate, deep audit upload) already
77
+ // tell the user exactly what to do; the throttled nudges would just be noise.
78
+ if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
79
+ try {
80
+ const { maybeShowNudges } = await import("../runtime/nudges.js");
81
+ maybeShowNudges(args);
82
+ }
83
+ catch {
84
+ // dg deleted its own files mid-run (uninstall of itself); nudges are cosmetic.
85
+ }
68
86
  }
69
- }
70
- }
71
- // The auth flows (browser login, paid-verify gate, deep audit upload) already
72
- // tell the user exactly what to do; the throttled nudges would just be noise.
73
- if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
74
- try {
75
- const { maybeShowNudges } = await import("../runtime/nudges.js");
76
- maybeShowNudges(args);
77
- }
78
- catch {
79
- // dg deleted its own files mid-run (uninstall of itself); nudges are cosmetic.
80
87
  }
81
88
  }