@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
@@ -26,6 +26,7 @@ export function readAuthState(env = process.env) {
26
26
  }
27
27
  const email = typeof parsed.email === "string" && parsed.email.length > 0 ? parsed.email : undefined;
28
28
  const tier = typeof parsed.tier === "string" && parsed.tier.length > 0 ? parsed.tier : undefined;
29
+ const name = typeof parsed.name === "string" && parsed.name.length > 0 ? parsed.name : undefined;
29
30
  return {
30
31
  version: 1,
31
32
  token: parsed.token,
@@ -34,7 +35,8 @@ export function readAuthState(env = process.env) {
34
35
  orgId: parsed.orgId ?? "",
35
36
  loggedInAt: parsed.loggedInAt,
36
37
  ...(email ? { email } : {}),
37
- ...(tier ? { tier } : {})
38
+ ...(tier ? { tier } : {}),
39
+ ...(name ? { name } : {})
38
40
  };
39
41
  }
40
42
  catch (error) {
@@ -63,6 +65,7 @@ export function writeAuthState(options, env = process.env) {
63
65
  }
64
66
  const email = typeof options.email === "string" && options.email.length > 0 ? options.email : undefined;
65
67
  const tier = typeof options.tier === "string" && options.tier.length > 0 ? options.tier : undefined;
68
+ const name = typeof options.name === "string" && options.name.length > 0 ? options.name : undefined;
66
69
  return withUserConfigLock(env, () => {
67
70
  const config = loadUserConfig(env);
68
71
  const apiBaseUrl = options.apiBaseUrl ?? config.api.baseUrl;
@@ -75,7 +78,8 @@ export function writeAuthState(options, env = process.env) {
75
78
  orgId,
76
79
  loggedInAt: (options.now ?? new Date()).toISOString(),
77
80
  ...(email ? { email } : {}),
78
- ...(tier ? { tier } : {})
81
+ ...(tier ? { tier } : {}),
82
+ ...(name ? { name } : {})
79
83
  };
80
84
  const paths = resolveDgPaths(env);
81
85
  writeJsonAtomic(authPath(paths), state);
@@ -120,7 +124,8 @@ export function authStatus(env = process.env) {
120
124
  apiBaseUrl: state.apiBaseUrl,
121
125
  orgId: state.orgId,
122
126
  ...(state.email ? { email: state.email } : {}),
123
- ...(state.tier ? { tier: state.tier } : {})
127
+ ...(state.tier ? { tier: state.tier } : {}),
128
+ ...(state.name ? { name: state.name } : {})
124
129
  };
125
130
  }
126
131
  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
  }
@@ -0,0 +1,231 @@
1
+ import { homedir } from "node:os";
2
+ import { AGENT_IDS, AGENTS, agentLabel, collectAgentOffers, getAgent, isAgentId, resolveAgentHookContext, } from "../agents/registry.js";
3
+ import { AgentHookError } from "../agents/persistence.js";
4
+ import { resolvePresentation } from "../presentation/mode.js";
5
+ import { createTheme } from "../presentation/theme.js";
6
+ import { tildifyPath as tildify } from "../setup/plan.js";
7
+ import { EXIT_USAGE } from "./types.js";
8
+ const VERBS = ["on", "off", "--check", "--print"];
9
+ const LEGACY_VERB_MAP = {
10
+ install: "on",
11
+ on: "on",
12
+ off: "off",
13
+ remove: "off",
14
+ uninstall: "off",
15
+ "--check": "--check",
16
+ "--print": "--print",
17
+ };
18
+ function parseAgentsArgs(args) {
19
+ if (args.length === 0) {
20
+ return { verb: "status", agents: [], recordFixture: false };
21
+ }
22
+ let rest = [...args];
23
+ let verb = null;
24
+ const first = rest[0] ?? "";
25
+ if (isAgentId(first)) {
26
+ const legacy = rest[1] ?? "install";
27
+ const mapped = LEGACY_VERB_MAP[legacy];
28
+ if (!mapped || rest.length > 3 || (rest.length === 3 && (rest[2] !== "--record-fixture" || mapped !== "on"))) {
29
+ return { error: `unknown subcommand '${rest[1] ?? ""}'. Use: dg agents [on|off|--check|--print] [agent]` };
30
+ }
31
+ return { verb: mapped, agents: [first], recordFixture: rest[2] === "--record-fixture" };
32
+ }
33
+ if (VERBS.includes(first)) {
34
+ verb = first;
35
+ rest = rest.slice(1);
36
+ }
37
+ else {
38
+ return { error: `unknown subcommand '${first}'. Use: dg agents [on|off|--check|--print] [agent]` };
39
+ }
40
+ const agents = [];
41
+ let recordFixture = false;
42
+ for (const arg of rest) {
43
+ if (arg === "--record-fixture") {
44
+ recordFixture = true;
45
+ continue;
46
+ }
47
+ if (!isAgentId(arg)) {
48
+ return { error: `unknown agent '${arg}'. Agents: ${AGENT_IDS.join(", ")}` };
49
+ }
50
+ agents.push(arg);
51
+ }
52
+ if (recordFixture && verb !== "on") {
53
+ return { error: "--record-fixture only applies to 'dg agents on'" };
54
+ }
55
+ return { verb, agents, recordFixture };
56
+ }
57
+ function agentRow(agent, home, env) {
58
+ const integration = AGENTS[agent];
59
+ if (!integration.detect(home)) {
60
+ return { agent, state: "not found", detail: "" };
61
+ }
62
+ const probe = integration.probeHookSupport(home);
63
+ if (!probe.supported) {
64
+ return { agent, state: "unsupported", detail: probe.detail };
65
+ }
66
+ const ctx = resolveAgentHookContext(agent, { env, home });
67
+ const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
68
+ if (hooked) {
69
+ return { agent, state: "protected", detail: tildify(ctx.settingsPath, home) };
70
+ }
71
+ return { agent, state: "detected", detail: `dg agents on ${agent}` };
72
+ }
73
+ function renderStatusCard(rows, theme) {
74
+ const accent = (text) => theme.paint("accent", text);
75
+ const muted = (text) => theme.paint("muted", text);
76
+ const glyph = (state) => {
77
+ if (state === "protected") {
78
+ return theme.paint("pass", "✓");
79
+ }
80
+ if (state === "detected") {
81
+ return accent("·");
82
+ }
83
+ if (state === "unsupported") {
84
+ return theme.paint("warn", "✗");
85
+ }
86
+ return muted("–");
87
+ };
88
+ const labelWidth = Math.max(...rows.map((row) => agentLabel(row.agent).length));
89
+ const stateWidth = Math.max(...rows.map((row) => row.state.length));
90
+ const lines = [
91
+ "",
92
+ ` ${accent("AI agents")} ${muted("— installs route through dg before fetching")}`,
93
+ ""
94
+ ];
95
+ for (const row of rows) {
96
+ const label = agentLabel(row.agent).padEnd(labelWidth);
97
+ const state = row.state === "protected" ? theme.paint("pass", row.state.padEnd(stateWidth)) : muted(row.state.padEnd(stateWidth));
98
+ const detail = row.state === "detected" ? accent(row.detail) : muted(row.detail);
99
+ lines.push(` ${glyph(row.state)} ${label} ${state} ${detail}`.trimEnd());
100
+ }
101
+ lines.push("", ` ${muted("protect all:")} ${accent("dg agents on")} ${muted("·")} ${muted("remove:")} ${accent("dg agents off")}`, "");
102
+ return lines.join("\n");
103
+ }
104
+ async function applyAgents(targets, env, home, theme, recordFixture) {
105
+ const accent = (text) => theme.paint("accent", text);
106
+ const muted = (text) => theme.paint("muted", text);
107
+ const lines = [];
108
+ let failures = 0;
109
+ for (const agent of targets) {
110
+ const integration = AGENTS[agent];
111
+ const probe = integration.probeHookSupport(home);
112
+ if (!probe.supported) {
113
+ failures += 1;
114
+ lines.push(` ${theme.paint("warn", `✗ ${integration.label}`)} ${muted(`— ${probe.detail}. The dg PATH shim still covers installs typed in its terminal.`)}`);
115
+ continue;
116
+ }
117
+ const ctx = resolveAgentHookContext(agent, { env, home });
118
+ const dgCommand = recordFixture ? `${ctx.dgCommand} --record-fixture` : ctx.dgCommand;
119
+ try {
120
+ await integration.apply({ ...ctx, dgCommand });
121
+ lines.push(` ${theme.paint("pass", `✓ ${integration.label} installs route through dg`)} ${muted(`(${tildify(ctx.settingsPath, home)})`)}`);
122
+ if (integration.maturity === "unverified") {
123
+ lines.push(` ${muted(` hook installed from ${integration.label}'s documented schema; payload format not yet verified against a live ${integration.label} run`)}`);
124
+ }
125
+ if (integration.postInstallNote) {
126
+ lines.push(` ${muted(` ${integration.postInstallNote}`)}`);
127
+ }
128
+ }
129
+ catch (error) {
130
+ failures += 1;
131
+ const message = error instanceof AgentHookError ? error.message : error instanceof Error ? error.message : "agent hook error";
132
+ lines.push(` ${theme.paint("warn", `✗ ${integration.label}`)} ${muted(`— ${message}`)}`);
133
+ }
134
+ }
135
+ if (lines.length === 0) {
136
+ return { exitCode: 0, stdout: ` ${muted("No unprotected agents detected.")} ${muted("See")} ${accent("dg agents")}\n`, stderr: "" };
137
+ }
138
+ lines.push(` ${muted("Reverse:")} ${accent("dg agents off")}`);
139
+ return { exitCode: failures > 0 && failures === targets.length ? 1 : 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
140
+ }
141
+ async function removeAgents(targets, env, home, theme) {
142
+ const muted = (text) => theme.paint("muted", text);
143
+ const lines = [];
144
+ for (const agent of targets) {
145
+ const integration = AGENTS[agent];
146
+ const ctx = resolveAgentHookContext(agent, { env, home });
147
+ try {
148
+ const result = await integration.remove(ctx);
149
+ lines.push(result.removed
150
+ ? ` ${theme.paint("pass", `✓ ${integration.label} hook removed`)} ${muted(`(${tildify(ctx.settingsPath, home)})`)}`
151
+ : ` ${muted(`· ${integration.label} — no dg hook was installed`)}`);
152
+ }
153
+ catch (error) {
154
+ const message = error instanceof AgentHookError ? error.message : error instanceof Error ? error.message : "agent hook error";
155
+ lines.push(` ${theme.paint("warn", `✗ ${integration.label}`)} ${muted(`— ${message}`)}`);
156
+ }
157
+ }
158
+ if (lines.length === 0) {
159
+ lines.push(` ${muted("No agent hooks to remove.")}`);
160
+ }
161
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
162
+ }
163
+ function checkAgents(targets, env, home) {
164
+ const lines = [];
165
+ let allOk = true;
166
+ for (const agent of targets) {
167
+ const ctx = resolveAgentHookContext(agent, { env, home });
168
+ const checks = getAgent(agent).verify(ctx);
169
+ lines.push(`${agentLabel(agent)}:`);
170
+ for (const check of checks) {
171
+ lines.push(` ${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`);
172
+ allOk = allOk && check.ok;
173
+ }
174
+ }
175
+ return { exitCode: allOk ? 0 : 1, stdout: `${lines.join("\n")}\n`, stderr: "" };
176
+ }
177
+ function printAgents(targets, env, home) {
178
+ const lines = [];
179
+ for (const agent of targets) {
180
+ const integration = AGENTS[agent];
181
+ const ctx = resolveAgentHookContext(agent, { env, home });
182
+ const probe = integration.probeHookSupport(home);
183
+ lines.push(`dg agents on ${agent} would write a dg hook to:`, ` ${ctx.settingsPath}`, "running:", ` ${ctx.dgCommand}`, `probe: ${probe.supported ? "supported" : "unsupported"} — ${probe.detail}`, `Reverse with: dg agents off ${agent} (or dg uninstall)`);
184
+ }
185
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
186
+ }
187
+ export async function runAgentsCommand(args, env = process.env, home) {
188
+ const parsed = parseAgentsArgs(args);
189
+ if ("error" in parsed) {
190
+ return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg agents: ${parsed.error}\n` };
191
+ }
192
+ const resolvedHome = home ?? env.HOME ?? homedir();
193
+ const theme = createTheme(resolvePresentation().color);
194
+ if (parsed.verb === "status") {
195
+ const rows = AGENT_IDS.map((agent) => agentRow(agent, resolvedHome, env));
196
+ return { exitCode: 0, stdout: `${renderStatusCard(rows, theme)}\n`, stderr: "" };
197
+ }
198
+ if (parsed.verb === "on") {
199
+ const targets = parsed.agents.length > 0 ? parsed.agents : collectAgentOffers({ env, home: resolvedHome }).map((offer) => offer.agent);
200
+ return applyAgents(targets, env, resolvedHome, theme, parsed.recordFixture);
201
+ }
202
+ if (parsed.verb === "off") {
203
+ const targets = parsed.agents.length > 0
204
+ ? parsed.agents
205
+ : AGENT_IDS.filter((agent) => agentRow(agent, resolvedHome, env).state === "protected");
206
+ return removeAgents(targets, env, resolvedHome, theme);
207
+ }
208
+ const targets = parsed.agents.length > 0
209
+ ? parsed.agents
210
+ : AGENT_IDS.filter((agent) => AGENTS[agent].detect(resolvedHome));
211
+ if (targets.length === 0) {
212
+ return { exitCode: 1, stdout: "", stderr: "dg agents: no AI agents detected on this machine.\n" };
213
+ }
214
+ return parsed.verb === "--check" ? checkAgents(targets, env, resolvedHome) : printAgents(targets, env, resolvedHome);
215
+ }
216
+ async function handle({ args }) {
217
+ return runAgentsCommand(args);
218
+ }
219
+ export const agentsCommand = {
220
+ name: "agents",
221
+ aliases: ["hook"],
222
+ summary: "Route AI coding agents' package installs through dg's firewall",
223
+ usage: "dg agents [on|off|--check|--print] [agent ...]",
224
+ details: [
225
+ "Bare 'dg agents' shows every supported agent and whether its installs are protected.",
226
+ "'dg agents on' installs a native pre-command hook into every detected agent (or name one, e.g. 'dg agents on claude-code') so the shell installs it runs are checked by dg and blocked before a malicious package is fetched. Reversible via 'dg agents off' or 'dg uninstall'.",
227
+ ],
228
+ args: [{ name: "agent", summary: AGENT_IDS.join(" | ") }],
229
+ examples: ["dg agents", "dg agents on", "dg agents on claude-code", "dg agents --check", "dg agents off"],
230
+ handler: handle,
231
+ };
@@ -1,5 +1,6 @@
1
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { basename, dirname, resolve } from "node:path";
3
+ import { writeReportAtomic } from "../util/report-writer.js";
3
4
  import { EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
4
5
  import { resolvePresentation } from "../presentation/mode.js";
5
6
  import { createTheme } from "../presentation/theme.js";
@@ -34,6 +35,15 @@ export const auditCommand = {
34
35
  ],
35
36
  handler: (context) => runAuditCommand(context.args)
36
37
  };
38
+ function privatePackageResult(scope) {
39
+ const theme = createTheme(resolvePresentation().color);
40
+ const name = typeof scope.packageJson?.name === "string" ? scope.packageJson.name : basename(scope.root);
41
+ return {
42
+ exitCode: 0,
43
+ stdout: `${theme.paint("pass", "✓")} ${name} is private — npm refuses to publish it, so there is nothing to audit ${theme.paint("muted", "(remove \"private\": true to audit it for publish)")}\n`,
44
+ stderr: ""
45
+ };
46
+ }
37
47
  function gather(args) {
38
48
  const parsed = parseAuditArgs(args);
39
49
  if ("error" in parsed) {
@@ -43,6 +53,9 @@ function gather(args) {
43
53
  if ("error" in scope) {
44
54
  return { error: scope.error, usage: false };
45
55
  }
56
+ if (scope.ecosystem === "npm" && scope.packageJson?.private === true) {
57
+ return { privatePackage: scope };
58
+ }
46
59
  const set = scope.ecosystem === "pypi" ? pypiPublishSet(scope.root) : npmPublishSet(scope.root);
47
60
  const files = set.relPaths
48
61
  .map((relPath) => buildAuditFile(scope.root, relPath))
@@ -69,10 +82,10 @@ function finalize(gathered, deep) {
69
82
  deep
70
83
  };
71
84
  const theme = createTheme(resolvePresentation().color);
72
- const rendered = parsed.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderReport(report, theme);
85
+ const rendered = parsed.format === "json" ? `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n` : renderReport(report, theme);
73
86
  if (parsed.outputPath) {
74
87
  try {
75
- writeFileSync(resolve(parsed.outputPath), rendered, "utf8");
88
+ writeReportAtomic(resolve(parsed.outputPath), rendered);
76
89
  }
77
90
  catch (error) {
78
91
  return { exitCode: EXIT_TOOL_ERROR, stdout: "", stderr: `dg audit could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "write error"}\n` };
@@ -89,6 +102,9 @@ function runAuditCommand(args) {
89
102
  if ("error" in gathered) {
90
103
  return gatherError(gathered);
91
104
  }
105
+ if ("privatePackage" in gathered) {
106
+ return privatePackageResult(gathered.privatePackage);
107
+ }
92
108
  const decision = deepDecision(gathered.scope, gathered.parsed.local);
93
109
  return finalize(gathered, { ran: false, reason: decision.reason });
94
110
  }
@@ -104,6 +120,9 @@ export async function maybeAudit(args) {
104
120
  if ("error" in gathered) {
105
121
  return { handled: true, result: gatherError(gathered) };
106
122
  }
123
+ if ("privatePackage" in gathered) {
124
+ return { handled: true, result: privatePackageResult(gathered.privatePackage) };
125
+ }
107
126
  let decision = deepDecision(gathered.scope, gathered.parsed.local);
108
127
  if (!decision.upload && canPromptConsent(gathered)) {
109
128
  if (!(await teamPolicyBlocksUpload())) {
@@ -3,12 +3,15 @@ import { ConfigError, getConfigValue, isConfigKey, listConfig, loadUserConfig, s
3
3
  export const configCommand = {
4
4
  name: "config",
5
5
  summary: "Inspect or edit trusted dg configuration.",
6
- usage: "dg config <get|set|unset|list> [key] [value] [--json]",
6
+ usage: "dg config <get|set|unset|list> [key] [value] [--json] [--all]",
7
7
  args: [
8
8
  { name: "<action>", summary: "list | get <key> | set <key> <value> | unset <key>." },
9
9
  { name: "[key]", summary: "e.g. policy.mode, gitHook.onWarn, cooldown.age, cooldown.exempt, api.baseUrl." }
10
10
  ],
11
- flags: [{ flag: "--json", summary: "Machine-readable output for list/get." }],
11
+ flags: [
12
+ { flag: "--json", summary: "Machine-readable output for list/get." },
13
+ { flag: "--all", summary: "Include advanced keys in list (per-ecosystem cooldowns, internal flags)." }
14
+ ],
12
15
  examples: [
13
16
  "dg config list",
14
17
  "dg config get policy.mode",
@@ -26,7 +29,8 @@ export const configCommand = {
26
29
  };
27
30
  function configHandler(args) {
28
31
  const json = args.includes("--json");
29
- const filtered = args.filter((arg) => arg !== "--json");
32
+ const all = args.includes("--all");
33
+ const filtered = args.filter((arg) => arg !== "--json" && arg !== "--all");
30
34
  const [action, key, value, extra] = filtered;
31
35
  if (!action || extra) {
32
36
  return usageError("expected get, set, unset, or list");
@@ -37,10 +41,10 @@ function configHandler(args) {
37
41
  if (key || value) {
38
42
  return usageError("list does not accept a key or value");
39
43
  }
40
- const entries = listConfig(config);
44
+ const entries = listConfig(config, all);
41
45
  return {
42
46
  exitCode: 0,
43
- stdout: json ? `${JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])), null, 2)}\n` : renderConfigList(entries),
47
+ stdout: json ? `${JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])), null, 2)}\n` : renderConfigList(entries, !all),
44
48
  stderr: ""
45
49
  };
46
50
  }
@@ -93,8 +97,12 @@ function configHandler(args) {
93
97
  throw error;
94
98
  }
95
99
  }
96
- function renderConfigList(entries) {
97
- return `${entries.map((entry) => `${entry.key}=${entry.value}`).join("\n")}\n`;
100
+ function renderConfigList(entries, truncated) {
101
+ const lines = entries.map((entry) => `${entry.key}=${entry.value}`);
102
+ if (truncated) {
103
+ lines.push("(advanced keys hidden — dg config list --all)");
104
+ }
105
+ return `${lines.join("\n")}\n`;
98
106
  }
99
107
  function usageError(message) {
100
108
  return {