@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
@@ -0,0 +1,389 @@
1
+ import { describeCooldownSettings, durationToDays, formatCooldownDuration } from "../policy/cooldown.js";
2
+ import { readHeldPackages } from "../state/index.js";
3
+ import { canonicalCooldownName } from "../policy/pypi-name.js";
4
+ import { ConfigError, loadUserConfig, setConfigValue, updateUserConfig } from "../config/settings.js";
5
+ import { appendCooldownExemptions, cooldownExemptionActive, CooldownExemptionCapError, findProjectRoot, loadDgFile, mutateDgFile, removeCooldownExemptions, resolveAcceptedBy } from "../project/dgfile.js";
6
+ import { EXIT_USAGE } from "./types.js";
7
+ export const cooldownCommand = {
8
+ name: "cooldown",
9
+ summary: "Hold new releases for a window before they can install, with per-package exemptions.",
10
+ usage: "dg cooldown [<window>|off] [--json] | dg cooldown exempt <name> [--reason <text>] [--expires <30d>] | dg cooldown rm <name>",
11
+ subcommands: [
12
+ { name: "list", summary: "Show the cooldown window and exemptions (default).", usage: "dg cooldown list [--json]", details: [], handler: () => unreachable() },
13
+ { name: "<window>", summary: "Set the window directly: dg cooldown 7d, dg cooldown 24h, dg cooldown off.", usage: "dg cooldown <7d|24h|off>", details: [], handler: () => unreachable() },
14
+ { name: "exempt", summary: "Always allow a package through cooldown.", usage: "dg cooldown exempt <name> [--reason <text>] [--expires <30d>] [--ecosystem npm|pypi]", details: [], handler: () => unreachable() },
15
+ { name: "rm", summary: "Drop an exemption so cooldown applies again.", usage: "dg cooldown rm <name> [--ecosystem npm|pypi]", details: [], handler: () => unreachable() },
16
+ { name: "prune", summary: "Remove expired exemptions.", usage: "dg cooldown prune", details: [], handler: () => unreachable() }
17
+ ],
18
+ flags: [
19
+ { flag: "--reason", value: "<text>", summary: "Why this package is exempt (stored in dg.json)." },
20
+ { flag: "--expires", value: "<30d>", summary: "Auto-expire the exemption after a duration (e.g. 14d, 90d)." },
21
+ { flag: "--ecosystem", value: "<npm|pypi>", summary: "Ecosystem when the name is not prefixed (default npm)." },
22
+ { flag: "--json", summary: "Machine-readable listing." }
23
+ ],
24
+ examples: [
25
+ "dg cooldown",
26
+ "dg cooldown 7d",
27
+ "dg cooldown off",
28
+ "dg cooldown exempt left-pad --reason 'vendored, pinned'",
29
+ "dg cooldown exempt pypi:requests --expires 30d",
30
+ "dg cooldown rm left-pad"
31
+ ],
32
+ details: [
33
+ "Cooldown quarantines releases younger than your configured window so a freshly-published compromised version can't install before anyone notices. An exemption opts one package out — use it for internal packages you publish and install immediately, or a dependency you've already vetted. Exemptions live in dg.json at the git root; an --expires window makes them self-revoke so the quarantine comes back on its own."
34
+ ],
35
+ handler: (context) => runCooldownCommand(context)
36
+ };
37
+ function unreachable() {
38
+ throw new Error("subcommand handled by the cooldown router");
39
+ }
40
+ export function runCooldownCommand(context, cwd = process.cwd(), env = process.env, now = new Date()) {
41
+ const [first, ...rest] = context.args;
42
+ if (first === undefined || first === "list" || first === "--json") {
43
+ const json = first === "--json" || rest.includes("--json");
44
+ const extras = rest.filter((arg) => arg !== "--json");
45
+ if (extras.length > 0) {
46
+ return usage(`unexpected argument '${extras[0]}'`);
47
+ }
48
+ return listExemptions(cwd, env, json, now);
49
+ }
50
+ if (first === "exempt") {
51
+ return addExemption(rest, cwd, env, now);
52
+ }
53
+ if (first === "rm" || first === "remove") {
54
+ return removeExemption(rest, cwd, env);
55
+ }
56
+ if (first === "prune") {
57
+ return pruneExemptions(rest, cwd, env, now);
58
+ }
59
+ if (/^(0|off|[1-9]\d{0,3}[hd])$/i.test(first)) {
60
+ return setWindow(first.toLowerCase(), rest, cwd, env, now);
61
+ }
62
+ return usage(`unknown subcommand '${first}'`);
63
+ }
64
+ function setWindow(window, rest, cwd, env, now) {
65
+ if (rest.length > 0) {
66
+ return usage(`unexpected argument '${rest[0]}'`);
67
+ }
68
+ try {
69
+ updateUserConfig((current) => setConfigValue(current, "cooldown.age", window), env);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof ConfigError) {
73
+ return usage(error.message);
74
+ }
75
+ throw error;
76
+ }
77
+ return listExemptions(cwd, env, false, now);
78
+ }
79
+ function parseFlags(args) {
80
+ const positionals = [];
81
+ let reason;
82
+ let expires;
83
+ let ecosystem;
84
+ for (let i = 0; i < args.length; i += 1) {
85
+ const arg = args[i];
86
+ if (arg === undefined) {
87
+ continue;
88
+ }
89
+ if (arg === "--reason" || arg === "--expires" || arg === "--ecosystem") {
90
+ const value = args[i + 1];
91
+ if (value === undefined || value.startsWith("--")) {
92
+ return { error: `${arg} needs a value` };
93
+ }
94
+ if (arg === "--reason")
95
+ reason = value;
96
+ else if (arg === "--expires")
97
+ expires = value;
98
+ else
99
+ ecosystem = value;
100
+ i += 1;
101
+ continue;
102
+ }
103
+ if (arg.startsWith("--reason="))
104
+ reason = arg.slice("--reason=".length);
105
+ else if (arg.startsWith("--expires="))
106
+ expires = arg.slice("--expires=".length);
107
+ else if (arg.startsWith("--ecosystem="))
108
+ ecosystem = arg.slice("--ecosystem=".length);
109
+ else if (arg.startsWith("--"))
110
+ return { error: `unknown flag '${arg}'` };
111
+ else
112
+ positionals.push(arg);
113
+ }
114
+ return {
115
+ positionals,
116
+ ...(reason !== undefined ? { reason } : {}),
117
+ ...(expires !== undefined ? { expires } : {}),
118
+ ...(ecosystem !== undefined ? { ecosystem } : {})
119
+ };
120
+ }
121
+ const EXEMPTION_NAME_RE = /^[@A-Za-z0-9][@A-Za-z0-9._/-]*$/u;
122
+ function resolveTarget(token, ecosystemFlag) {
123
+ const colon = token.indexOf(":");
124
+ if (colon > 0) {
125
+ const prefix = token.slice(0, colon);
126
+ if (prefix === "npm" || prefix === "pypi" || prefix === "cargo") {
127
+ return validatedTarget(prefix, token.slice(colon + 1), token);
128
+ }
129
+ return { error: `unknown ecosystem prefix '${prefix}:' (only npm:, pypi:, and cargo: are supported)` };
130
+ }
131
+ const eco = ecosystemFlag ?? "npm";
132
+ if (eco !== "npm" && eco !== "pypi" && eco !== "cargo") {
133
+ return { error: `--ecosystem must be npm, pypi, or cargo, got '${eco}'` };
134
+ }
135
+ return validatedTarget(eco, token, token);
136
+ }
137
+ function validatedTarget(ecosystem, name, token) {
138
+ if (name.length === 0) {
139
+ return { error: `'${token}' has no package name` };
140
+ }
141
+ if (!EXEMPTION_NAME_RE.test(name)) {
142
+ return { error: `'${name}' is not a valid package name (no spaces, control characters, or '*' globs; use config cooldown.exempt for globs)` };
143
+ }
144
+ return { ecosystem, name: canonicalName(ecosystem, name) };
145
+ }
146
+ function canonicalName(ecosystem, name) {
147
+ return canonicalCooldownName(ecosystem, name);
148
+ }
149
+ function expiresAtFrom(expires, now) {
150
+ if (expires === undefined || expires === "off" || expires === "never") {
151
+ return {};
152
+ }
153
+ const days = durationToDays(expires);
154
+ if (days <= 0) {
155
+ if (/^[1-9]\d*(h|d)$/.test(expires)) {
156
+ return { error: `--expires '${expires}' is too large (maximum is 9999d or 9999h)` };
157
+ }
158
+ return { error: `--expires must be a positive duration like 30d or 12h, got '${expires}'` };
159
+ }
160
+ return { value: new Date(now.getTime() + days * 24 * 60 * 60 * 1000).toISOString() };
161
+ }
162
+ function formatExpiryDisplay(expiresAt, acceptedAt) {
163
+ const expiry = Date.parse(expiresAt);
164
+ const accepted = Date.parse(acceptedAt);
165
+ if (Number.isFinite(expiry) && Number.isFinite(accepted) && expiry - accepted < 24 * 60 * 60 * 1000) {
166
+ return `${expiresAt.slice(0, 16).replace("T", " ")}Z`;
167
+ }
168
+ return expiresAt.slice(0, 10);
169
+ }
170
+ function addExemption(args, cwd, env, now) {
171
+ const parsed = parseFlags(args);
172
+ if ("error" in parsed) {
173
+ return usage(parsed.error);
174
+ }
175
+ if (parsed.positionals.length !== 1 || parsed.positionals[0] === undefined) {
176
+ return usage("exempt takes exactly one <name>");
177
+ }
178
+ const target = resolveTarget(parsed.positionals[0], parsed.ecosystem);
179
+ if ("error" in target) {
180
+ return usage(target.error);
181
+ }
182
+ const expiry = expiresAtFrom(parsed.expires, now);
183
+ if ("error" in expiry) {
184
+ return usage(expiry.error);
185
+ }
186
+ const located = locateWritableDgFile(cwd, env);
187
+ if ("error" in located) {
188
+ return located.error;
189
+ }
190
+ const exemption = {
191
+ ecosystem: target.ecosystem,
192
+ name: target.name,
193
+ reason: parsed.reason ?? "",
194
+ acceptedBy: resolveAcceptedBy(located.root, env),
195
+ ...(expiry.value ? { expiresAt: expiry.value } : {})
196
+ };
197
+ try {
198
+ mutateDgFile(located.root, env, (file) => appendCooldownExemptions(file, [exemption], now));
199
+ }
200
+ catch (error) {
201
+ if (error instanceof CooldownExemptionCapError) {
202
+ return { exitCode: 1, stdout: "", stderr: `dg cooldown: ${error.message}.\n` };
203
+ }
204
+ throw error;
205
+ }
206
+ const until = expiry.value ? ` until ${formatExpiryDisplay(expiry.value, now.toISOString())}` : "";
207
+ return {
208
+ exitCode: 0,
209
+ stdout: `Exempted ${target.ecosystem}:${target.name} from cooldown${until}.\n`,
210
+ stderr: ""
211
+ };
212
+ }
213
+ function removeExemption(args, cwd, env) {
214
+ const parsed = parseFlags(args);
215
+ if ("error" in parsed) {
216
+ return usage(parsed.error);
217
+ }
218
+ if (parsed.positionals.length !== 1 || parsed.positionals[0] === undefined) {
219
+ return usage("rm takes exactly one <name>");
220
+ }
221
+ const target = resolveTarget(parsed.positionals[0], parsed.ecosystem);
222
+ if ("error" in target) {
223
+ return usage(target.error);
224
+ }
225
+ const located = locateWritableDgFile(cwd, env);
226
+ if ("error" in located) {
227
+ return located.error;
228
+ }
229
+ const matches = (e) => e.ecosystem === target.ecosystem && e.name === target.name;
230
+ let removed = false;
231
+ mutateDgFile(located.root, env, (file) => {
232
+ if (!file.cooldownExemptions.some(matches)) {
233
+ return file;
234
+ }
235
+ removed = true;
236
+ return removeCooldownExemptions(file, matches);
237
+ });
238
+ if (!removed) {
239
+ return { exitCode: 1, stdout: "", stderr: `dg cooldown: ${target.ecosystem}:${target.name} is not exempt in ${located.file.path}.\n` };
240
+ }
241
+ return { exitCode: 0, stdout: `Removed cooldown exemption for ${target.ecosystem}:${target.name} — cooldown applies again.\n`, stderr: "" };
242
+ }
243
+ function pruneExemptions(args, cwd, env, now) {
244
+ if (args.length > 0) {
245
+ return usage(`unexpected argument '${args[0]}'`);
246
+ }
247
+ const located = locateWritableDgFile(cwd, env);
248
+ if ("error" in located) {
249
+ return located.error;
250
+ }
251
+ let pruned = 0;
252
+ mutateDgFile(located.root, env, (file) => {
253
+ const expired = (e) => !cooldownExemptionActive(e, now);
254
+ pruned = file.cooldownExemptions.filter(expired).length;
255
+ return pruned === 0 ? file : removeCooldownExemptions(file, expired);
256
+ });
257
+ return { exitCode: 0, stdout: `Pruned ${pruned} expired cooldown exemption${pruned === 1 ? "" : "s"}.\n`, stderr: "" };
258
+ }
259
+ function windowStatusLines(env) {
260
+ const config = loadUserConfig(env);
261
+ const window = describeCooldownSettings(config, env);
262
+ const windowLine = window === "off"
263
+ ? "off — new releases install immediately"
264
+ : `${window} set: dg cooldown 3d`;
265
+ const unknownLine = config.cooldown.onUnknown === "block"
266
+ ? "block (no known publish date → held back)"
267
+ : "allow (no known publish date → not held back)";
268
+ return [
269
+ "Cooldown — versions published less than your window ago wait before installing.",
270
+ "",
271
+ ` window ${windowLine}`,
272
+ ` unknown ${unknownLine}`,
273
+ ""
274
+ ];
275
+ }
276
+ const EXEMPTION_FOOTER = [
277
+ " add: dg cooldown exempt <name> [--expires 30d] · remove: dg cooldown rm <name>",
278
+ " An exemption waives only the wait — the package is still scanned. No CVE fast-track yet."
279
+ ];
280
+ function renderTable(columns, rows) {
281
+ const widths = columns.map((label, column) => Math.max(label.length, ...rows.map((row) => row[column]?.length ?? 0)));
282
+ const renderRow = (row) => ` ${row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()}`;
283
+ return [renderRow(columns), ...rows.map(renderRow)];
284
+ }
285
+ function eligibleDisplay(entry, now) {
286
+ if (!entry.eligibleAt) {
287
+ return "unknown";
288
+ }
289
+ const eligible = Date.parse(entry.eligibleAt);
290
+ if (!Number.isFinite(eligible)) {
291
+ return "unknown";
292
+ }
293
+ const relative = formatCooldownDuration(Math.max(0, (eligible - now.getTime()) / 86_400_000));
294
+ return `${entry.eligibleAt.slice(0, 10)} (in ${relative})`;
295
+ }
296
+ function heldSectionLines(env, now) {
297
+ const held = readHeldPackages(env, now);
298
+ if (held.length === 0) {
299
+ return ["Currently held: none.", ""];
300
+ }
301
+ const rows = held.map((entry) => [
302
+ `${entry.ecosystem}:${entry.name}@${entry.version}`,
303
+ entry.publishedAt ? entry.publishedAt.slice(0, 10) : "-",
304
+ eligibleDisplay(entry, now)
305
+ ]);
306
+ return [
307
+ "Currently held:",
308
+ "",
309
+ ...renderTable(["PACKAGE", "PUBLISHED", "ELIGIBLE"], rows),
310
+ "",
311
+ " release now: dg cooldown exempt <name>",
312
+ ""
313
+ ];
314
+ }
315
+ function listExemptions(cwd, env, json, now) {
316
+ const root = findProjectRoot(cwd, env);
317
+ if (!root) {
318
+ return { exitCode: EXIT_USAGE, stdout: "", stderr: "dg cooldown: not inside a git repository.\n" };
319
+ }
320
+ const file = loadDgFile(root);
321
+ if (file.exists && !file.readable) {
322
+ return { exitCode: 1, stdout: "", stderr: `dg cooldown: cannot use ${file.path} — ${file.failure ?? "unreadable"}.\n` };
323
+ }
324
+ const config = loadUserConfig(env);
325
+ if (json) {
326
+ return {
327
+ exitCode: 0,
328
+ stdout: `${JSON.stringify({
329
+ schemaVersion: 1,
330
+ path: file.path,
331
+ window: { effective: describeCooldownSettings(config, env), onUnknown: config.cooldown.onUnknown },
332
+ held: readHeldPackages(env, now),
333
+ cooldownExemptions: file.cooldownExemptions.map((e) => exemptionJson(e, now))
334
+ }, null, 2)}\n`,
335
+ stderr: ""
336
+ };
337
+ }
338
+ const header = [...windowStatusLines(env), ...heldSectionLines(env, now)];
339
+ if (file.cooldownExemptions.length === 0) {
340
+ const note = file.exists ? `No exemptions in ${file.path}.` : "No exemptions yet — add one with the command below.";
341
+ return { exitCode: 0, stdout: `${[...header, note, ...EXEMPTION_FOOTER].join("\n")}\n`, stderr: "" };
342
+ }
343
+ const rows = file.cooldownExemptions.map((e) => [
344
+ `${e.ecosystem}:${e.name}`,
345
+ e.reason || "-",
346
+ e.acceptedBy || "-",
347
+ e.acceptedAt.slice(0, 10) || "-",
348
+ e.expiresAt ? formatExpiryDisplay(e.expiresAt, e.acceptedAt) : "-",
349
+ cooldownExemptionActive(e, now) ? "active" : "expired"
350
+ ]);
351
+ const lines = [
352
+ ...header,
353
+ `Exemptions in ${file.path}:`,
354
+ "",
355
+ ...renderTable(["PACKAGE", "REASON", "BY", "WHEN", "EXPIRES", "STATUS"], rows),
356
+ "",
357
+ ...EXEMPTION_FOOTER
358
+ ];
359
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
360
+ }
361
+ function locateWritableDgFile(cwd, env) {
362
+ const root = findProjectRoot(cwd, env);
363
+ if (!root) {
364
+ return { error: { exitCode: EXIT_USAGE, stdout: "", stderr: "dg cooldown: not inside a git repository.\n" } };
365
+ }
366
+ const file = loadDgFile(root);
367
+ if (!file.readable) {
368
+ return { error: { exitCode: 1, stdout: "", stderr: `dg cooldown: cannot write ${file.path} — ${file.failure ?? "unreadable"}.\n` } };
369
+ }
370
+ return { root, file };
371
+ }
372
+ function exemptionJson(e, now) {
373
+ return {
374
+ ecosystem: e.ecosystem,
375
+ name: e.name,
376
+ reason: e.reason,
377
+ acceptedBy: e.acceptedBy,
378
+ acceptedAt: e.acceptedAt,
379
+ ...(e.expiresAt ? { expiresAt: e.expiresAt } : {}),
380
+ status: cooldownExemptionActive(e, now) ? "active" : "expired"
381
+ };
382
+ }
383
+ function usage(message) {
384
+ return {
385
+ exitCode: EXIT_USAGE,
386
+ stdout: "",
387
+ stderr: `dg cooldown: ${message}. Usage: ${cooldownCommand.usage}\n`
388
+ };
389
+ }
@@ -1,6 +1,6 @@
1
1
  import { recordDecisionEvents } from "../decisions/remember-prompt.js";
2
2
  import { packageKey } from "../decisions/apply.js";
3
- import { findProjectRoot, loadDgFile, removeDecisions, saveDgFile } from "../project/dgfile.js";
3
+ import { findProjectRoot, loadDgFile, mutateDgFile, removeDecisions } from "../project/dgfile.js";
4
4
  import { EXIT_USAGE } from "./types.js";
5
5
  export const decisionsCommand = {
6
6
  name: "decisions",
@@ -24,7 +24,7 @@ export function runDecisionsCommand(context, cwd = process.cwd(), env = process.
24
24
  const [first, ...rest] = context.args;
25
25
  if (first === undefined || first === "list" || first === "--json") {
26
26
  const json = first === "--json" || rest.includes("--json");
27
- const extras = (first === undefined || first === "--json" ? [] : rest).filter((arg) => arg !== "--json");
27
+ const extras = rest.filter((arg) => arg !== "--json");
28
28
  if (extras.length > 0) {
29
29
  return usage(`unexpected argument '${extras[0]}'`);
30
30
  }
@@ -44,11 +44,17 @@ function listDecisions(cwd, env, json) {
44
44
  if ("error" in located) {
45
45
  return located.error;
46
46
  }
47
+ if ("missing" in located) {
48
+ if (json) {
49
+ return { exitCode: 0, stdout: `${JSON.stringify({ schemaVersion: 1, path: null, decisions: [] }, null, 2)}\n`, stderr: "" };
50
+ }
51
+ return { exitCode: 0, stdout: `No dg.json at ${located.missing} — nothing remembered yet.\n`, stderr: "" };
52
+ }
47
53
  const file = located.file;
48
54
  if (json) {
49
55
  return {
50
56
  exitCode: 0,
51
- stdout: `${JSON.stringify({ path: file.path, decisions: file.decisions.map((entry) => entryJson(entry)) }, null, 2)}\n`,
57
+ stdout: `${JSON.stringify({ schemaVersion: 1, path: file.path, decisions: file.decisions.map((entry) => entryJson(entry)) }, null, 2)}\n`,
52
58
  stderr: ""
53
59
  };
54
60
  }
@@ -82,12 +88,20 @@ function revokeDecisions(selector, cwd, env) {
82
88
  if ("error" in located) {
83
89
  return located.error;
84
90
  }
85
- const file = located.file;
86
- const matched = file.decisions.filter((entry) => matchesSelector(entry, selector));
91
+ if ("missing" in located) {
92
+ return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing to revoke — no dg.json at ${located.missing} yet.\n` };
93
+ }
94
+ let matched = [];
95
+ mutateDgFile(located.root, env, (file) => {
96
+ matched = file.decisions.filter((entry) => matchesSelector(entry, selector));
97
+ if (matched.length === 0) {
98
+ return file;
99
+ }
100
+ return removeDecisions(file, new Set(matched.map((entry) => entry.id)));
101
+ });
87
102
  if (matched.length === 0) {
88
- return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing matches '${selector}' in ${file.path}.\n` };
103
+ return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing matches '${selector}' in ${located.file.path}.\n` };
89
104
  }
90
- saveDgFile(removeDecisions(file, new Set(matched.map((entry) => entry.id))));
91
105
  recordDecisionEvents("decision.revoked", matched.map((entry) => `${entry.ecosystem}:${packageKey(entry.name, scopeLabel(entry))}`), `revoked via dg decisions (${selector})`, env);
92
106
  const lines = matched.map((entry) => `Revoked ${entry.ecosystem}:${entry.name}@${scopeLabel(entry)} (${entry.id.slice(0, 8)}) — the warn will surface again.`);
93
107
  return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
@@ -99,12 +113,12 @@ function locateDgFile(cwd, env) {
99
113
  }
100
114
  const file = loadDgFile(root);
101
115
  if (!file.exists) {
102
- return { error: { exitCode: 0, stdout: `No dg.json at ${root} — nothing remembered yet.\n`, stderr: "" } };
116
+ return { missing: root };
103
117
  }
104
118
  if (!file.readable) {
105
119
  return { error: { exitCode: 1, stdout: "", stderr: `dg decisions: cannot use ${file.path} — ${file.failure ?? "unreadable"}.\n` } };
106
120
  }
107
- return { file };
121
+ return { root, file };
108
122
  }
109
123
  function matchesSelector(entry, selector) {
110
124
  if (selector.length >= 4 && entry.id.startsWith(selector)) {
@@ -33,7 +33,7 @@ async function doctorHandler(args) {
33
33
  const theme = createTheme(resolvePresentation().color);
34
34
  return {
35
35
  exitCode: hasFailure ? 1 : 0,
36
- stdout: json ? `${JSON.stringify(report, null, 2)}\n` : renderDoctorReport(report, theme, verbose),
36
+ stdout: json ? `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n` : renderDoctorReport(report, theme, verbose),
37
37
  stderr: ""
38
38
  };
39
39
  }
@@ -1,4 +1,4 @@
1
- const PRODUCT_DESCRIPTION = "Dependency Guardian supply-chain firewall CLI.";
1
+ const PRODUCT_DESCRIPTION = "Dependency Guardian";
2
2
  const COMMON_COMMANDS = ["scan", "verify", "audit", "licenses", "setup", "guard-commit", "doctor", "login"];
3
3
  const GLOBAL_FLAGS_FOOTER = "Global: --help/-h · --version/-v · --json (where supported) · --no-color / --force-color (also NO_COLOR, FORCE_COLOR).";
4
4
  function isWrapper(command) {
@@ -1,9 +1,10 @@
1
- import { mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { readdirSync, statSync } from "node:fs";
2
2
  import { launchScanTui, shouldLaunchScanTui } from "../scan-ui/launch.js";
3
+ import { writeReportAtomic } from "../util/report-writer.js";
3
4
  import { basename, dirname, relative, resolve, sep } from "node:path";
4
5
  import { scanProject } from "../scan/discovery.js";
5
6
  import { isSupportedLockfilePath, verifyLockfile } from "../verify/preflight.js";
6
- import { EXIT_USAGE_VERDICT } from "./types.js";
7
+ import { EXIT_ANALYSIS_INCOMPLETE, EXIT_USAGE_VERDICT } from "./types.js";
7
8
  const LICENSE_RISKS = [
8
9
  "network-copyleft",
9
10
  "no-license",
@@ -39,7 +40,7 @@ export const licensesCommand = {
39
40
  examples: ["dg licenses", "dg licenses --markdown -o licenses.md", "dg licenses --fail-on strong-copyleft,network-copyleft"],
40
41
  details: [
41
42
  "Reports project and lockfile license metadata without running package managers or package code.",
42
- "License exports support text, JSON, CSV, and Markdown with stable exit codes for policy gates."
43
+ "License exports support text, JSON, CSV, and Markdown. Exit codes: 0 pass, 2 policy block, 4 report error, 64 usage error."
43
44
  ],
44
45
  handler: (context) => runLicensesCommand(context.args)
45
46
  };
@@ -60,13 +61,13 @@ async function runLicensesCommand(args) {
60
61
  }
61
62
  catch (error) {
62
63
  return {
63
- exitCode: 1,
64
+ exitCode: EXIT_ANALYSIS_INCOMPLETE,
64
65
  stdout: "",
65
66
  stderr: `dg licenses TUI failed: ${error instanceof Error ? error.message : "unknown error"}\n`
66
67
  };
67
68
  }
68
69
  return {
69
- exitCode: 0,
70
+ exitCode: typeof process.exitCode === "number" ? process.exitCode : 0,
70
71
  stdout: "",
71
72
  stderr: ""
72
73
  };
@@ -78,7 +79,7 @@ async function runLicensesCommand(args) {
78
79
  }
79
80
  catch (error) {
80
81
  return {
81
- exitCode: 1,
82
+ exitCode: EXIT_ANALYSIS_INCOMPLETE,
82
83
  stdout: "",
83
84
  stderr: `dg licenses failed: ${error instanceof Error ? error.message : "unknown license error"}\n`
84
85
  };
@@ -91,7 +92,7 @@ async function runLicensesCommand(args) {
91
92
  }
92
93
  catch (error) {
93
94
  return {
94
- exitCode: 1,
95
+ exitCode: EXIT_ANALYSIS_INCOMPLETE,
95
96
  stdout: "",
96
97
  stderr: `dg licenses could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
97
98
  };
@@ -329,7 +330,7 @@ function classifyLicense(license) {
329
330
  }
330
331
  function renderLicenseReport(report, format) {
331
332
  if (format === "json") {
332
- return `${JSON.stringify(report, null, 2)}\n`;
333
+ return `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n`;
333
334
  }
334
335
  if (format === "csv") {
335
336
  return renderCsv(report);
@@ -392,7 +393,7 @@ function renderMarkdown(report) {
392
393
  return `${lines.join("\n")}\n`;
393
394
  }
394
395
  function exitCodeForReport(report) {
395
- return report.status === "block" ? 1 : 0;
396
+ return report.status === "block" ? 2 : 0;
396
397
  }
397
398
  function usageError(message) {
398
399
  return {
@@ -401,19 +402,6 @@ function usageError(message) {
401
402
  stderr: `dg licenses: ${message}. Usage: dg licenses [path] [--json|--csv|--markdown] [--output <path>] [--fail-on <risk[,risk...]>] [--deny-license <id>]\n`
402
403
  };
403
404
  }
404
- function writeReportAtomic(outputPath, contents) {
405
- const directory = dirname(outputPath);
406
- mkdirSync(directory, { recursive: true });
407
- const temporaryPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
408
- try {
409
- writeFileSync(temporaryPath, contents, "utf8");
410
- renameSync(temporaryPath, outputPath);
411
- }
412
- catch (error) {
413
- rmSync(temporaryPath, { force: true });
414
- throw error;
415
- }
416
- }
417
405
  function dedupeEntries(entries) {
418
406
  const seen = new Set();
419
407
  const deduped = [];
@@ -3,9 +3,9 @@ import { clearAuthState } from "../auth/store.js";
3
3
  export const logoutCommand = {
4
4
  name: "logout",
5
5
  summary: "Remove local Dependency Guardian authentication.",
6
- usage: "dg logout [--yes]",
7
- flags: [{ flag: "--yes", summary: "Required confirmation flag; logout removes the local token only when it is passed." }],
8
- details: ["Removes dg-owned auth material without deleting unrelated user content."],
6
+ usage: "dg logout",
7
+ flags: [{ flag: "--yes", summary: "Accepted for scripts; logout no longer needs confirmation." }],
8
+ details: ["Removes the dg-owned local auth token only. Config and setup files are not changed; log back in with dg login."],
9
9
  handler: (context) => logoutHandler(context.args)
10
10
  };
11
11
  function logoutHandler(args, env = process.env) {
@@ -17,13 +17,6 @@ function logoutHandler(args, env = process.env) {
17
17
  stderr: `dg logout: unknown option '${unknown}'. Run 'dg logout --help'.\n`
18
18
  };
19
19
  }
20
- if (!args.includes("--yes")) {
21
- return {
22
- exitCode: EXIT_USAGE,
23
- stdout: "This removes the dg-owned local auth token only. Config and setup files are not changed.\n",
24
- stderr: "dg logout requires --yes to confirm.\n"
25
- };
26
- }
27
20
  let removed;
28
21
  try {
29
22
  removed = clearAuthState();
@@ -35,7 +28,7 @@ function logoutHandler(args, env = process.env) {
35
28
  stderr: `dg logout: could not remove the local auth token: ${error instanceof Error ? error.message : "unknown error"}\n`
36
29
  };
37
30
  }
38
- const lines = [removed ? "Removed local dg auth token." : "No local dg auth token was present."];
31
+ const lines = [removed ? "Logged out." : "Already logged out."];
39
32
  const activeEnvVars = ["DG_API_KEY", "DG_API_TOKEN"].filter((name) => env[name]);
40
33
  if (activeEnvVars.length > 0) {
41
34
  lines.push(`${activeEnvVars.join(" and ")} ${activeEnvVars.length === 1 ? "is" : "are"} still set, so env-var auth remains active (file token removed only).`);
@@ -1,12 +1,13 @@
1
1
  import { configCommand } from "./config.js";
2
- import { completionCommand } from "./completion.js";
3
2
  import { auditCommand } from "./audit.js";
3
+ import { cooldownCommand } from "./cooldown.js";
4
4
  import { decisionsCommand } from "./decisions.js";
5
5
  import { doctorCommand } from "./doctor.js";
6
- import { explainCommand } from "./explain.js";
7
6
  import { guardCommitCommand } from "./guard-commit.js";
7
+ import { agentsCommand } from "./agents.js";
8
8
  import { renderCommandHelp, renderRootHelp } from "./help.js";
9
9
  import { licensesCommand } from "./licenses.js";
10
+ import { sbomCommand } from "./sbom.js";
10
11
  import { loginCommand } from "./login.js";
11
12
  import { logoutCommand } from "./logout.js";
12
13
  import { packageManagerCommands } from "./wrap.js";
@@ -22,19 +23,20 @@ import { verifyCommand } from "./verify.js";
22
23
  import { dgVersion, versionResult } from "./version.js";
23
24
  export const commandCatalog = [
24
25
  scanCommand,
26
+ sbomCommand,
25
27
  verifyCommand,
26
28
  setupCommand,
27
29
  guardCommitCommand,
30
+ agentsCommand,
28
31
  decisionsCommand,
32
+ cooldownCommand,
29
33
  uninstallCommand,
30
34
  doctorCommand,
31
35
  statusCommand,
32
36
  ...packageManagerCommands(),
33
37
  loginCommand,
34
38
  logoutCommand,
35
- explainCommand,
36
39
  configCommand,
37
- completionCommand,
38
40
  licensesCommand,
39
41
  auditCommand,
40
42
  updateCommand,