@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
@@ -0,0 +1,351 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { dirname } from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+ import { readCleanupRegistry, recordCleanupEntry, removeCleanupEntry, } from "../state/index.js";
6
+ export const LEGACY_AGENT_HOOK_SENTINEL = "dg-agent-hook-v1";
7
+ export function agentHookSentinel(agent) {
8
+ return `${LEGACY_AGENT_HOOK_SENTINEL}:${agent}`;
9
+ }
10
+ export class AgentHookError extends Error {
11
+ }
12
+ export function dirExists(path) {
13
+ try {
14
+ return lstatSync(path).isDirectory();
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ function assertSafeNode(path, role) {
21
+ let stats;
22
+ try {
23
+ stats = lstatSync(path);
24
+ }
25
+ catch {
26
+ return;
27
+ }
28
+ if (stats.isSymbolicLink()) {
29
+ throw new AgentHookError(`${path} is a symlink; refusing to write through it. Edit the link target directly, then retry.`);
30
+ }
31
+ if (typeof process.getuid === "function" && stats.uid !== process.getuid()) {
32
+ throw new AgentHookError(`${path} (${role}) is owned by another user; refusing to write to it.`);
33
+ }
34
+ if ((stats.mode & 0o002) !== 0) {
35
+ throw new AgentHookError(`${path} (${role}) is world-writable; refusing to write to it. Tighten its permissions, then retry.`);
36
+ }
37
+ }
38
+ export function assertSafeWriteTarget(path) {
39
+ assertSafeNode(dirname(path), "directory");
40
+ assertSafeNode(path, "file");
41
+ }
42
+ export function readSettings(path) {
43
+ if (!existsSync(path)) {
44
+ return { settings: {}, existed: false };
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(readFileSync(path, "utf8"));
49
+ }
50
+ catch {
51
+ throw new AgentHookError(`${path} is not valid JSON; refusing to modify it. Fix or remove it, then retry.`);
52
+ }
53
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
54
+ throw new AgentHookError(`${path} is not a JSON object; refusing to modify it.`);
55
+ }
56
+ return { settings: parsed, existed: true };
57
+ }
58
+ export function writeSettingsAtomic(path, settings) {
59
+ mkdirSync(dirname(path), { recursive: true });
60
+ assertSafeWriteTarget(path);
61
+ const tmp = `${path}.dg-${randomBytes(6).toString("hex")}.tmp`;
62
+ writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600, flag: "wx" });
63
+ try {
64
+ renameSync(tmp, path);
65
+ }
66
+ catch (error) {
67
+ rmSync(tmp, { force: true });
68
+ throw error;
69
+ }
70
+ }
71
+ async function wasCreatedByDg(ctx) {
72
+ try {
73
+ const reg = await readCleanupRegistry(ctx.paths);
74
+ return reg.entries.some((e) => e.kind === "agent-hook" && e.path === ctx.settingsPath && e.original === "dg-created");
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ async function recordHookEntry(ctx, legacySentinels, created) {
81
+ for (const sentinel of legacySentinels) {
82
+ await removeCleanupEntry(ctx.paths, { kind: "agent-hook", path: ctx.settingsPath, sentinel });
83
+ }
84
+ // Preserve the dg-created provenance on re-apply: `created` is false the second
85
+ // time the hook is installed, but if dg created the file originally, uninstall
86
+ // must still remove it, so keep the marker.
87
+ const dgCreated = created || (await wasCreatedByDg(ctx));
88
+ await recordCleanupEntry(ctx.paths, {
89
+ kind: "agent-hook",
90
+ path: ctx.settingsPath,
91
+ mode: "mode1",
92
+ sentinel: agentHookSentinel(ctx.agent),
93
+ ...(dgCreated ? { original: "dg-created" } : {}),
94
+ });
95
+ }
96
+ async function removeHookEntries(ctx, legacySentinels) {
97
+ await removeCleanupEntry(ctx.paths, { kind: "agent-hook", path: ctx.settingsPath, sentinel: agentHookSentinel(ctx.agent) });
98
+ for (const sentinel of legacySentinels) {
99
+ await removeCleanupEntry(ctx.paths, { kind: "agent-hook", path: ctx.settingsPath, sentinel });
100
+ }
101
+ }
102
+ function findHookCommand(node, signature) {
103
+ if (typeof node === "string") {
104
+ return node.includes(signature) ? node : null;
105
+ }
106
+ if (Array.isArray(node)) {
107
+ for (const value of node) {
108
+ const found = findHookCommand(value, signature);
109
+ if (found) {
110
+ return found;
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ if (node && typeof node === "object") {
116
+ for (const value of Object.values(node)) {
117
+ const found = findHookCommand(value, signature);
118
+ if (found) {
119
+ return found;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ return null;
125
+ }
126
+ function hookResolvesCheck(settings, agent) {
127
+ const command = findHookCommand(settings, `hook-exec ${agent}`);
128
+ if (!command) {
129
+ return null;
130
+ }
131
+ const head = command.split(/\s+hook-exec\s+/)[0] ?? command;
132
+ const broken = head
133
+ .split(/\s+/)
134
+ .filter((token) => token.startsWith("/"))
135
+ .filter((token) => !existsSync(token));
136
+ return {
137
+ name: "hook command resolves",
138
+ ok: broken.length === 0,
139
+ detail: broken.length === 0
140
+ ? "dg path in the hook resolves"
141
+ : `hook references a missing path (${broken.join(", ")}); re-run 'dg agents on ${agent}'`,
142
+ };
143
+ }
144
+ export function mergedJsonHook(config) {
145
+ const legacy = config.legacySentinels ?? [];
146
+ return {
147
+ isInstalledCheckName: config.checkName,
148
+ async apply(ctx) {
149
+ const { settings, existed } = readSettings(ctx.settingsPath);
150
+ writeSettingsAtomic(ctx.settingsPath, config.insert(settings, ctx.dgCommand));
151
+ await recordHookEntry(ctx, legacy, !existed);
152
+ return { created: !existed };
153
+ },
154
+ async remove(ctx) {
155
+ let removed = false;
156
+ if (existsSync(ctx.settingsPath)) {
157
+ let settings = null;
158
+ try {
159
+ settings = readSettings(ctx.settingsPath).settings;
160
+ }
161
+ catch {
162
+ settings = null;
163
+ }
164
+ if (settings) {
165
+ const result = config.remove(settings);
166
+ if (result.changed) {
167
+ removed = true;
168
+ if (result.empty && (await wasCreatedByDg(ctx))) {
169
+ rmSync(ctx.settingsPath, { force: true });
170
+ }
171
+ else {
172
+ writeSettingsAtomic(ctx.settingsPath, result.settings);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ await removeHookEntries(ctx, legacy);
178
+ return { removed };
179
+ },
180
+ verify(ctx) {
181
+ const checks = [];
182
+ const present = existsSync(ctx.settingsPath);
183
+ checks.push({ name: "settings file", ok: present, detail: present ? ctx.settingsPath : `${ctx.settingsPath} (absent)` });
184
+ if (!present) {
185
+ return checks;
186
+ }
187
+ let settings = null;
188
+ let installed = false;
189
+ try {
190
+ settings = readSettings(ctx.settingsPath).settings;
191
+ installed = config.isInstalled(settings);
192
+ }
193
+ catch {
194
+ installed = false;
195
+ }
196
+ checks.push({ name: config.checkName, ok: installed, detail: installed ? "installed" : "not installed" });
197
+ if (installed && settings) {
198
+ const resolves = hookResolvesCheck(settings, ctx.agent);
199
+ if (resolves) {
200
+ checks.push(resolves);
201
+ }
202
+ }
203
+ return checks;
204
+ },
205
+ reverseEntry(entry, removed, missing, warnings) {
206
+ if (!existsSync(entry.path)) {
207
+ missing.push(entry.path);
208
+ return;
209
+ }
210
+ let settings;
211
+ try {
212
+ const parsed = JSON.parse(readFileSync(entry.path, "utf8"));
213
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
214
+ warnings.push(`${entry.path}: not a JSON object; left untouched`);
215
+ return;
216
+ }
217
+ settings = parsed;
218
+ }
219
+ catch {
220
+ warnings.push(`${entry.path}: unreadable JSON; left untouched`);
221
+ return;
222
+ }
223
+ const result = config.remove(settings);
224
+ if (!result.changed) {
225
+ missing.push(entry.path);
226
+ return;
227
+ }
228
+ if (result.empty && entry.original === "dg-created") {
229
+ rmSync(entry.path, { force: true });
230
+ }
231
+ else {
232
+ try {
233
+ writeSettingsAtomic(entry.path, result.settings);
234
+ }
235
+ catch (error) {
236
+ warnings.push(`${entry.path}: ${error instanceof Error ? error.message : "could not rewrite settings"}`);
237
+ return;
238
+ }
239
+ }
240
+ removed.push(entry.path);
241
+ },
242
+ };
243
+ }
244
+ export function ownedJsonHook(config) {
245
+ const sentinel = agentHookSentinel(config.agent);
246
+ const ownsFile = (path) => {
247
+ try {
248
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
249
+ return parsed?.dgSentinel === sentinel;
250
+ }
251
+ catch {
252
+ return false;
253
+ }
254
+ };
255
+ return {
256
+ isInstalledCheckName: config.checkName,
257
+ async apply(ctx) {
258
+ const existed = existsSync(ctx.settingsPath);
259
+ if (existed && !ownsFile(ctx.settingsPath)) {
260
+ throw new AgentHookError(`${ctx.settingsPath} already exists and was not written by dg; refusing to overwrite it. Merge the output of 'dg agents --print ${ctx.agent}' yourself, or remove the file and retry.`);
261
+ }
262
+ writeSettingsAtomic(ctx.settingsPath, { dgSentinel: sentinel, ...config.render(ctx.dgCommand) });
263
+ await recordHookEntry(ctx, [], !existed);
264
+ return { created: !existed };
265
+ },
266
+ async remove(ctx) {
267
+ let removed = false;
268
+ if (existsSync(ctx.settingsPath) && ownsFile(ctx.settingsPath)) {
269
+ rmSync(ctx.settingsPath, { force: true });
270
+ removed = true;
271
+ }
272
+ await removeHookEntries(ctx, []);
273
+ return { removed };
274
+ },
275
+ verify(ctx) {
276
+ const checks = [];
277
+ const present = existsSync(ctx.settingsPath);
278
+ checks.push({ name: "hook file", ok: present, detail: present ? ctx.settingsPath : `${ctx.settingsPath} (absent)` });
279
+ if (!present) {
280
+ return checks;
281
+ }
282
+ const installed = ownsFile(ctx.settingsPath);
283
+ checks.push({ name: config.checkName, ok: installed, detail: installed ? "installed" : "present but not dg-owned" });
284
+ if (installed) {
285
+ try {
286
+ const settings = JSON.parse(readFileSync(ctx.settingsPath, "utf8"));
287
+ const resolves = hookResolvesCheck(settings, ctx.agent);
288
+ if (resolves) {
289
+ checks.push(resolves);
290
+ }
291
+ }
292
+ catch {
293
+ // unreadable file: the installed check already reflects the broken state
294
+ }
295
+ }
296
+ return checks;
297
+ },
298
+ reverseEntry(entry, removed, missing, warnings) {
299
+ if (!existsSync(entry.path)) {
300
+ missing.push(entry.path);
301
+ return;
302
+ }
303
+ if (!ownsFile(entry.path)) {
304
+ warnings.push(`${entry.path}: not dg-owned; left untouched`);
305
+ return;
306
+ }
307
+ rmSync(entry.path, { force: true });
308
+ removed.push(entry.path);
309
+ },
310
+ };
311
+ }
312
+ function defaultExecVersion(binary, args) {
313
+ try {
314
+ const result = spawnSync(binary, [...args], { encoding: "utf8", timeout: 1500 });
315
+ if (result.status !== 0 || typeof result.stdout !== "string") {
316
+ return null;
317
+ }
318
+ return result.stdout.trim() || null;
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ function parseSemver(text) {
325
+ const match = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(text);
326
+ if (!match) {
327
+ return null;
328
+ }
329
+ return [Number(match[1]), Number(match[2]), Number(match[3] ?? "0")];
330
+ }
331
+ export function probeMinCliVersion(binary, minVersion, label, deps) {
332
+ const execVersion = deps?.execVersion ?? defaultExecVersion;
333
+ const output = execVersion(binary, ["--version"]);
334
+ if (!output) {
335
+ return { supported: false, detail: `cannot confirm ${label} >= ${minVersion} (the ${binary} CLI did not report a version)` };
336
+ }
337
+ const found = parseSemver(output);
338
+ const wanted = parseSemver(minVersion);
339
+ if (!found || !wanted) {
340
+ return { supported: false, detail: `cannot confirm ${label} >= ${minVersion} (unrecognized version '${output}')` };
341
+ }
342
+ for (let index = 0; index < 3; index += 1) {
343
+ if ((found[index] ?? 0) > (wanted[index] ?? 0)) {
344
+ break;
345
+ }
346
+ if ((found[index] ?? 0) < (wanted[index] ?? 0)) {
347
+ return { supported: false, detail: `${label} ${found.join(".")} is older than ${minVersion}, which added the hook dg needs` };
348
+ }
349
+ }
350
+ return { supported: true, detail: `${label} ${found.join(".")}` };
351
+ }
@@ -0,0 +1,128 @@
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
+ // Run the dg script via its own shebang rather than baking process.execPath:
41
+ // a version-pinned node path (e.g. .../Cellar/node/<ver>/bin/node) vanishes on
42
+ // a node upgrade and would silently disable the hook.
43
+ return `${bin} ${agentHookSignature(agent)}`;
44
+ }
45
+ export function resolveAgentHookContext(agent, options = {}) {
46
+ const env = options.env ?? process.env;
47
+ const home = options.home ?? env.HOME ?? homedir();
48
+ return {
49
+ agent,
50
+ settingsPath: AGENTS[agent].configPath(home),
51
+ dgCommand: options.dgCommand ?? defaultDgCommand(agent),
52
+ paths: resolveDgPaths(env),
53
+ };
54
+ }
55
+ export function applyAgentHook(ctx) {
56
+ return AGENTS[ctx.agent].apply(ctx);
57
+ }
58
+ export function removeAgentHookForAgent(ctx) {
59
+ return AGENTS[ctx.agent].remove(ctx);
60
+ }
61
+ export function verifyAgentHook(ctx) {
62
+ return AGENTS[ctx.agent].verify(ctx);
63
+ }
64
+ function agentFromSentinel(sentinel) {
65
+ if (!sentinel || sentinel === LEGACY_AGENT_HOOK_SENTINEL) {
66
+ return "claude-code";
67
+ }
68
+ if (!sentinel.startsWith(`${LEGACY_AGENT_HOOK_SENTINEL}:`)) {
69
+ return null;
70
+ }
71
+ const id = sentinel.slice(LEGACY_AGENT_HOOK_SENTINEL.length + 1);
72
+ return isAgentId(id) ? id : null;
73
+ }
74
+ export function reverseAgentHookEntry(entry, removed, missing, warnings) {
75
+ const agent = agentFromSentinel(entry.sentinel);
76
+ if (!agent) {
77
+ warnings.push(`${entry.path}: unrecognized agent hook entry (${entry.sentinel ?? "no sentinel"}); left untouched`);
78
+ return;
79
+ }
80
+ AGENTS[agent].reverseEntry(entry, removed, missing, warnings);
81
+ }
82
+ export function collectAgentSkips(options) {
83
+ const skips = [];
84
+ const env = options?.env ?? process.env;
85
+ const home = options?.home ?? env.HOME ?? homedir();
86
+ for (const agent of AGENT_IDS) {
87
+ try {
88
+ const integration = AGENTS[agent];
89
+ if (!integration.detect(home)) {
90
+ continue;
91
+ }
92
+ const probe = integration.probeHookSupport(home);
93
+ if (!probe.supported) {
94
+ skips.push({ agent, label: integration.label, detail: probe.detail });
95
+ }
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ }
101
+ return skips;
102
+ }
103
+ export function collectAgentOffers(options) {
104
+ const offers = [];
105
+ const env = options?.env ?? process.env;
106
+ const home = options?.home ?? env.HOME ?? homedir();
107
+ for (const agent of AGENT_IDS) {
108
+ try {
109
+ const integration = AGENTS[agent];
110
+ if (!integration.detect(home)) {
111
+ continue;
112
+ }
113
+ const probe = integration.probeHookSupport(home);
114
+ if (!probe.supported) {
115
+ continue;
116
+ }
117
+ const ctx = resolveAgentHookContext(agent, options ?? {});
118
+ const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
119
+ if (!hooked) {
120
+ offers.push({ agent, label: integration.label, ctx, probe });
121
+ }
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ }
127
+ return offers;
128
+ }
@@ -0,0 +1,118 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { buildAgentRoutingEnv } from "../launcher/env.js";
4
+ import { readServiceState } from "../service/state.js";
5
+ import { resolveDgPaths } from "../state/index.js";
6
+ import { readSettings, writeSettingsAtomic } from "./persistence.js";
7
+ import { getAgent } from "./registry.js";
8
+ // The agent's install hook screens statically; this routes the agent's ACTUAL
9
+ // fetches through dg's proxy so a wrapped/dynamic install (eval, $VAR, python -m
10
+ // pip, …) the static hook can't decode is still screened at fetch time. Only
11
+ // applied when `dg service` is running, so the proxy endpoint baked into the
12
+ // agent's env is always live — never a dead HTTPS_PROXY that breaks installs.
13
+ const CODEX_BEGIN = "# >>> dg routing >>>";
14
+ const CODEX_END = "# <<< dg routing <<<";
15
+ function backupPath(agent, env) {
16
+ return join(resolveDgPaths(env).stateDir, `routing-${agent}.json`);
17
+ }
18
+ export function resolveServiceRoutingEnv(env = process.env) {
19
+ const { state } = readServiceState(env);
20
+ if (!state.running || !state.proxy) {
21
+ return { error: "dg service is not running — run 'dg service start' so the proxy gate is live before routing agents through it" };
22
+ }
23
+ return { env: buildAgentRoutingEnv(state.proxy.proxyUrl, state.proxy.caPath), proxyUrl: state.proxy.proxyUrl };
24
+ }
25
+ function escapeRegex(value) {
26
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ }
28
+ function stripCodexBlock(content) {
29
+ const block = new RegExp(`\\n?${escapeRegex(CODEX_BEGIN)}[\\s\\S]*?${escapeRegex(CODEX_END)}\\n?`, "g");
30
+ return content.replace(block, "\n").replace(/\n{3,}/g, "\n\n");
31
+ }
32
+ function applyClaudeRouting(settingsPath, routing, backup) {
33
+ const { settings } = readSettings(settingsPath);
34
+ const envObj = typeof settings.env === "object" && settings.env !== null ? { ...settings.env } : {};
35
+ const prior = {};
36
+ for (const [k, v] of Object.entries(routing)) {
37
+ prior[k] = k in envObj ? String(envObj[k]) : null;
38
+ envObj[k] = v;
39
+ }
40
+ settings.env = envObj;
41
+ writeSettingsAtomic(settingsPath, settings);
42
+ mkdirSync(dirname(backup), { recursive: true, mode: 0o700 });
43
+ writeFileSync(backup, `${JSON.stringify({ kind: "claude-env", path: settingsPath, prior }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
44
+ }
45
+ function removeClaudeRouting(settingsPath, backup) {
46
+ if (!existsSync(backup)) {
47
+ return;
48
+ }
49
+ let prior = {};
50
+ try {
51
+ prior = JSON.parse(readFileSync(backup, "utf8")).prior ?? {};
52
+ }
53
+ catch {
54
+ prior = {};
55
+ }
56
+ if (existsSync(settingsPath)) {
57
+ const { settings } = readSettings(settingsPath);
58
+ const envObj = typeof settings.env === "object" && settings.env !== null ? { ...settings.env } : {};
59
+ for (const [k, v] of Object.entries(prior)) {
60
+ if (v === null) {
61
+ delete envObj[k];
62
+ }
63
+ else {
64
+ envObj[k] = v;
65
+ }
66
+ }
67
+ if (Object.keys(envObj).length === 0) {
68
+ delete settings.env;
69
+ }
70
+ else {
71
+ settings.env = envObj;
72
+ }
73
+ writeSettingsAtomic(settingsPath, settings);
74
+ }
75
+ rmSync(backup, { force: true });
76
+ }
77
+ function applyCodexRouting(configPath, routing) {
78
+ const content = existsSync(configPath) ? stripCodexBlock(readFileSync(configPath, "utf8")) : "";
79
+ if (/^\s*\[shell_environment_policy\]/m.test(content)) {
80
+ return { applied: false, detail: "~/.codex/config.toml already defines [shell_environment_policy] — add dg's proxy vars (HTTPS_PROXY, NODE_EXTRA_CA_CERTS, …) to its `set` table manually" };
81
+ }
82
+ const set = Object.entries(routing).map(([k, v]) => `${k} = ${JSON.stringify(v)}`).join(", ");
83
+ const block = `${CODEX_BEGIN}\n[shell_environment_policy]\nset = { ${set} }\n${CODEX_END}\n`;
84
+ const next = content === "" || content.endsWith("\n") ? `${content}${block}` : `${content}\n${block}`;
85
+ writeFileSync(configPath, next, { encoding: "utf8", mode: 0o600 });
86
+ return { applied: true, detail: configPath };
87
+ }
88
+ function removeCodexRouting(configPath) {
89
+ if (!existsSync(configPath)) {
90
+ return;
91
+ }
92
+ writeFileSync(configPath, stripCodexBlock(readFileSync(configPath, "utf8")), { encoding: "utf8" });
93
+ }
94
+ function codexConfigPath(home) {
95
+ return join(home, ".codex", "config.toml");
96
+ }
97
+ export function applyAgentRouting(agent, routing, home, env = process.env) {
98
+ if (agent === "codex") {
99
+ return applyCodexRouting(codexConfigPath(home), routing);
100
+ }
101
+ // Every other supported agent reads a JSON settings file with an `env` block.
102
+ applyClaudeRouting(getAgent(agent).configPath(home), routing, backupPath(agent, env));
103
+ return { applied: true, detail: getAgent(agent).configPath(home) };
104
+ }
105
+ export function removeAgentRouting(agent, home, env = process.env) {
106
+ if (agent === "codex") {
107
+ removeCodexRouting(codexConfigPath(home));
108
+ return;
109
+ }
110
+ removeClaudeRouting(getAgent(agent).configPath(home), backupPath(agent, env));
111
+ }
112
+ export function routingInstalled(agent, home, env = process.env) {
113
+ if (agent === "codex") {
114
+ const p = codexConfigPath(home);
115
+ return existsSync(p) && readFileSync(p, "utf8").includes(CODEX_BEGIN);
116
+ }
117
+ return existsSync(backupPath(agent, env));
118
+ }
@@ -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" ? " (dg flagged this for human review — not auto-approved)" : "";
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
+ };