@westbayberry/dg 1.3.3 → 2.0.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 (126) hide show
  1. package/LICENSE +1 -201
  2. package/NOTICE +1 -4
  3. package/README.md +293 -0
  4. package/dist/api/analyze.js +210 -0
  5. package/dist/audit/deep.js +180 -0
  6. package/dist/audit/detectors.js +247 -0
  7. package/dist/audit/events.js +41 -0
  8. package/dist/audit/rules.js +426 -0
  9. package/dist/audit-ui/AuditApp.js +39 -0
  10. package/dist/audit-ui/components/AuditHeader.js +24 -0
  11. package/dist/audit-ui/components/AuditResultsView.js +307 -0
  12. package/dist/audit-ui/components/DeepStatusRow.js +11 -0
  13. package/dist/audit-ui/export.js +85 -0
  14. package/dist/audit-ui/format.js +34 -0
  15. package/dist/audit-ui/launch.js +34 -0
  16. package/dist/auth/device-login.js +271 -0
  17. package/dist/auth/env-token.js +6 -0
  18. package/dist/auth/login-app.js +156 -0
  19. package/dist/auth/store.js +147 -0
  20. package/dist/bin/dg.js +71 -0
  21. package/dist/commands/audit.js +357 -0
  22. package/dist/commands/completion.js +116 -0
  23. package/dist/commands/config.js +99 -0
  24. package/dist/commands/doctor.js +39 -0
  25. package/dist/commands/explain.js +100 -0
  26. package/dist/commands/guard-commit.js +158 -0
  27. package/dist/commands/help.js +74 -0
  28. package/dist/commands/licenses.js +435 -0
  29. package/dist/commands/login.js +81 -0
  30. package/dist/commands/logout.js +37 -0
  31. package/dist/commands/router.js +98 -0
  32. package/dist/commands/scan.js +18 -0
  33. package/dist/commands/service.js +475 -0
  34. package/dist/commands/setup.js +302 -0
  35. package/dist/commands/status.js +115 -0
  36. package/dist/commands/suggest.js +35 -0
  37. package/dist/commands/types.js +4 -0
  38. package/dist/commands/unavailable.js +11 -0
  39. package/dist/commands/uninstall.js +111 -0
  40. package/dist/commands/update.js +210 -0
  41. package/dist/commands/verify.js +151 -0
  42. package/dist/commands/version.js +22 -0
  43. package/dist/commands/wrap.js +55 -0
  44. package/dist/config/settings.js +302 -0
  45. package/dist/install-ui/LiveInstall.js +24 -0
  46. package/dist/install-ui/block-render.js +83 -0
  47. package/dist/install-ui/live-install-app.js +48 -0
  48. package/dist/install-ui/prompt.js +24 -0
  49. package/dist/launcher/classify.js +116 -0
  50. package/dist/launcher/env.js +53 -0
  51. package/dist/launcher/live-install.js +50 -0
  52. package/dist/launcher/output-redaction.js +77 -0
  53. package/dist/launcher/preflight-prompt.js +139 -0
  54. package/dist/launcher/resolve-real-binary.js +73 -0
  55. package/dist/launcher/run.js +417 -0
  56. package/dist/policy/evaluate.js +128 -0
  57. package/dist/presentation/mode.js +52 -0
  58. package/dist/presentation/theme.js +29 -0
  59. package/dist/proxy/buffer-budget.js +64 -0
  60. package/dist/proxy/ca.js +126 -0
  61. package/dist/proxy/classify-host.js +26 -0
  62. package/dist/proxy/enforcement.js +102 -0
  63. package/dist/proxy/metadata-map.js +336 -0
  64. package/dist/proxy/server.js +909 -0
  65. package/dist/proxy/upstream-proxy.js +102 -0
  66. package/dist/proxy/worker.js +39 -0
  67. package/dist/publish-set/collect.js +51 -0
  68. package/dist/publish-set/no-exec-shell.js +19 -0
  69. package/dist/publish-set/npm.js +109 -0
  70. package/dist/publish-set/pack.js +36 -0
  71. package/dist/publish-set/pypi.js +59 -0
  72. package/dist/runtime/cli.js +17 -0
  73. package/dist/runtime/first-run.js +60 -0
  74. package/dist/runtime/node-version.js +58 -0
  75. package/dist/runtime/nudges.js +105 -0
  76. package/dist/scan/analyze-worker.js +21 -0
  77. package/dist/scan/collect.js +153 -0
  78. package/dist/scan/command.js +159 -0
  79. package/dist/scan/discovery.js +209 -0
  80. package/dist/scan/render.js +240 -0
  81. package/dist/scan/scanner-report.js +82 -0
  82. package/dist/scan/staged.js +173 -0
  83. package/dist/scan/types.js +1 -0
  84. package/dist/scan-ui/LegacyApp.js +156 -0
  85. package/dist/scan-ui/alt-screen.js +84 -0
  86. package/dist/scan-ui/api-aliases.js +1 -0
  87. package/dist/scan-ui/components/ErrorView.js +23 -0
  88. package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
  89. package/dist/scan-ui/components/ProgressBar.js +89 -0
  90. package/dist/scan-ui/components/ProjectSelector.js +62 -0
  91. package/dist/scan-ui/components/ScoreHeader.js +20 -0
  92. package/dist/scan-ui/components/SetupBanner.js +13 -0
  93. package/dist/scan-ui/components/Spinner.js +4 -0
  94. package/dist/scan-ui/format-helpers.js +40 -0
  95. package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
  96. package/dist/scan-ui/hooks/useScan.js +113 -0
  97. package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
  98. package/dist/scan-ui/launch.js +27 -0
  99. package/dist/scan-ui/logo.js +91 -0
  100. package/dist/scan-ui/shims.js +30 -0
  101. package/dist/security/sanitize.js +28 -0
  102. package/dist/service/state.js +837 -0
  103. package/dist/service/trust-store.js +234 -0
  104. package/dist/service/worker.js +88 -0
  105. package/dist/setup/git-hook.js +244 -0
  106. package/dist/setup/optional-support.js +58 -0
  107. package/dist/setup/plan.js +899 -0
  108. package/dist/state/cleanup-registry.js +60 -0
  109. package/dist/state/index.js +5 -0
  110. package/dist/state/locks.js +161 -0
  111. package/dist/state/paths.js +24 -0
  112. package/dist/state/sessions.js +170 -0
  113. package/dist/state/store.js +50 -0
  114. package/dist/telemetry/events.js +40 -0
  115. package/dist/util/git.js +20 -0
  116. package/dist/util/tty-prompt.js +43 -0
  117. package/dist/verify/local.js +400 -0
  118. package/dist/verify/package-check.js +240 -0
  119. package/dist/verify/preflight.js +698 -0
  120. package/dist/verify/render.js +184 -0
  121. package/dist/verify/types.js +1 -0
  122. package/package.json +33 -50
  123. package/dist/index.mjs +0 -54116
  124. package/dist/postinstall.mjs +0 -731
  125. package/dist/python-hook/dg_pip_hook.pth +0 -1
  126. package/dist/python-hook/dg_pip_hook.py +0 -130
@@ -0,0 +1,357 @@
1
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { EXIT_TOOL_ERROR, EXIT_USAGE } from "./types.js";
4
+ import { resolvePresentation } from "../presentation/mode.js";
5
+ import { createTheme } from "../presentation/theme.js";
6
+ import { actionForFindings, detectFindings, findingLocation } from "../audit/detectors.js";
7
+ import { buildAuditFile } from "../publish-set/collect.js";
8
+ import { npmPublishSet } from "../publish-set/npm.js";
9
+ import { pypiPublishSet } from "../publish-set/pypi.js";
10
+ import { deepDecision, runDeepUpload, consentGiven, deepSummary, teamPolicyBlocksUpload } from "../audit/deep.js";
11
+ import { loadUserConfig, saveUserConfig, setConfigValue } from "../config/settings.js";
12
+ import { promptYesNo } from "../util/tty-prompt.js";
13
+ import { authStatus } from "../auth/store.js";
14
+ import { shouldLaunchAuditTui, launchAuditTui } from "../audit-ui/launch.js";
15
+ export const auditCommand = {
16
+ name: "audit",
17
+ summary: "Audit the package you're about to publish for leaked secrets and risky files.",
18
+ usage: "dg audit [path] [--json] [--output <path>] [--local] [--require-deep] [--fail-on <warn|block>]",
19
+ args: [
20
+ { name: "[path]", summary: "Package directory to audit (default: the nearest package from the current directory)." }
21
+ ],
22
+ flags: [
23
+ { flag: "--json", summary: "Machine-readable report." },
24
+ { flag: "--output", value: "<path>", summary: "Write the report to a file (alias -o)." },
25
+ { flag: "--local", summary: "Run only the local audit; skip the paid deep behavioral upload." },
26
+ { flag: "--require-deep", summary: "Fail if the deep behavioral audit could not run (for CI)." },
27
+ { flag: "--fail-on", value: "<warn|block>", summary: "Exit non-zero at or above this level (default: block)." }
28
+ ],
29
+ examples: ["dg audit", "dg audit ./packages/api", "dg audit --json", "dg audit --local"],
30
+ details: [
31
+ "Audits exactly what you're about to publish — the resolved publish set of one package, never the whole repo.",
32
+ "Basic checks run 100% locally and never upload anything. If you're on a paid plan (and your org allows it), it also runs a deep behavioral scan of your package on the scanner; raw bytes are never retained. Exit codes: 0 clean (warn counts as clean under the default --fail-on block), 1 warn with --fail-on warn, 2 block, 3 deep audit required but unavailable (--require-deep), 4 analysis incomplete."
33
+ ],
34
+ handler: (context) => runAuditCommand(context.args)
35
+ };
36
+ function gather(args) {
37
+ const parsed = parseAuditArgs(args);
38
+ if ("error" in parsed) {
39
+ return { error: parsed.error, usage: true };
40
+ }
41
+ const scope = resolveScope(parsed.target);
42
+ if ("error" in scope) {
43
+ return { error: scope.error, usage: false };
44
+ }
45
+ const set = scope.ecosystem === "pypi" ? pypiPublishSet(scope.root) : npmPublishSet(scope.root);
46
+ const files = set.relPaths
47
+ .map((relPath) => buildAuditFile(scope.root, relPath))
48
+ .filter((file) => file !== null);
49
+ const context = {
50
+ packageJson: scope.packageJson,
51
+ ecosystem: scope.ecosystem,
52
+ hasFilesAllowlist: set.hasAllowlist,
53
+ fileCount: files.length
54
+ };
55
+ const findings = detectFindings(files, context);
56
+ return { parsed, scope, localAction: actionForFindings(findings), findings, publishSetSource: set.source, fileCount: files.length };
57
+ }
58
+ function finalize(gathered, deep) {
59
+ const { parsed, scope } = gathered;
60
+ const report = {
61
+ target: displayTarget(scope.root),
62
+ artifact: scope.artifact,
63
+ ecosystem: scope.ecosystem,
64
+ action: combineAction(gathered.localAction, deep),
65
+ fileCount: gathered.fileCount,
66
+ publishSetSource: gathered.publishSetSource,
67
+ findings: gathered.findings,
68
+ deep
69
+ };
70
+ const theme = createTheme(resolvePresentation().color);
71
+ const rendered = parsed.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderReport(report, theme);
72
+ if (parsed.outputPath) {
73
+ try {
74
+ writeFileSync(resolve(parsed.outputPath), rendered, "utf8");
75
+ }
76
+ catch (error) {
77
+ return { exitCode: EXIT_TOOL_ERROR, stdout: "", stderr: `dg audit could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "write error"}\n` };
78
+ }
79
+ return { exitCode: exitCodeFor(report, parsed), stdout: `Wrote ${parsed.format} audit report to ${parsed.outputPath}\n`, stderr: "" };
80
+ }
81
+ return { exitCode: exitCodeFor(report, parsed), stdout: rendered, stderr: "" };
82
+ }
83
+ function gatherError(gathered) {
84
+ return gathered.usage ? usageError(gathered.error) : { exitCode: EXIT_USAGE, stdout: "", stderr: `dg audit: ${gathered.error}.\n` };
85
+ }
86
+ function runAuditCommand(args) {
87
+ const gathered = gather(args);
88
+ if ("error" in gathered) {
89
+ return gatherError(gathered);
90
+ }
91
+ const decision = deepDecision(gathered.scope, gathered.parsed.local);
92
+ return finalize(gathered, { ran: false, reason: decision.reason });
93
+ }
94
+ export async function maybeAudit(args) {
95
+ if (args[0] !== "audit") {
96
+ return { handled: false, result: { exitCode: 0, stdout: "", stderr: "" } };
97
+ }
98
+ const gathered = gather(args.slice(1));
99
+ if ("error" in gathered) {
100
+ return { handled: true, result: gatherError(gathered) };
101
+ }
102
+ let decision = deepDecision(gathered.scope, gathered.parsed.local);
103
+ if (!decision.upload && canPromptConsent(gathered)) {
104
+ if (!(await teamPolicyBlocksUpload())) {
105
+ if (grantConsentInteractively()) {
106
+ decision = deepDecision(gathered.scope, gathered.parsed.local);
107
+ }
108
+ }
109
+ }
110
+ if (shouldLaunchAuditTui({ format: gathered.parsed.format, outputPath: gathered.parsed.outputPath })) {
111
+ const uploadAbort = new AbortController();
112
+ const deepPromise = decision.upload
113
+ ? runDeepUpload(gathered.scope, gathered.scope.packageJson, { signal: uploadAbort.signal })
114
+ : null;
115
+ const initialDeep = decision.upload ? null : { ran: false, reason: decision.reason };
116
+ const exitCode = await launchAuditTui({ gathered, initialDeep, deepPromise });
117
+ uploadAbort.abort();
118
+ return { handled: true, result: { exitCode, stdout: "", stderr: "" } };
119
+ }
120
+ if (!decision.upload) {
121
+ return { handled: true, result: finalize(gathered, { ran: false, reason: decision.reason }) };
122
+ }
123
+ try {
124
+ const deep = await runDeepUpload(gathered.scope, gathered.scope.packageJson);
125
+ return { handled: true, result: finalize(gathered, deep) };
126
+ }
127
+ catch (error) {
128
+ const reason = error instanceof Error ? error.message : "deep audit failed";
129
+ return { handled: true, result: finalize(gathered, { ran: false, reason }) };
130
+ }
131
+ }
132
+ function canPromptConsent(gathered) {
133
+ return (!gathered.parsed.local &&
134
+ gathered.parsed.format !== "json" &&
135
+ !gathered.parsed.outputPath &&
136
+ gathered.scope.ecosystem === "npm" &&
137
+ Boolean(process.stdin.isTTY && process.stderr.isTTY) &&
138
+ !consentGiven() &&
139
+ safeAuthed());
140
+ }
141
+ function safeAuthed() {
142
+ try {
143
+ return authStatus().authenticated;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ function grantConsentInteractively() {
150
+ process.stderr.write("\n dg audit can also run a deep behavioral scan of this package on Dependency Guardian's scanner.\n" +
151
+ " That uploads a packed copy of your package (no lifecycle scripts run); raw bytes are never retained,\n" +
152
+ " only the verdict + redacted findings are recorded to your dashboard.\n");
153
+ const yes = promptYesNo(" Enable the deep upload for this machine?", false);
154
+ if (yes === true) {
155
+ try {
156
+ saveUserConfig(setConfigValue(loadUserConfig(), "audit.upload", "true"));
157
+ return true;
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+ export function combineAction(local, deep) {
166
+ if (deep.ran && deep.action === "block") {
167
+ return "block";
168
+ }
169
+ if (deep.ran && deep.action === "warn" && local === "pass") {
170
+ return "warn";
171
+ }
172
+ return local;
173
+ }
174
+ export function auditExitCode(localAction, deep, policy) {
175
+ const action = combineAction(localAction, deep);
176
+ if (policy.requireDeep && !deep.ran) {
177
+ return 3;
178
+ }
179
+ if (deep.ran && deep.action === "analysis_incomplete" && action !== "block") {
180
+ return 4;
181
+ }
182
+ if (action === "block") {
183
+ return 2;
184
+ }
185
+ if (action === "warn") {
186
+ return policy.failOn === "warn" ? 1 : 0;
187
+ }
188
+ return 0;
189
+ }
190
+ function exitCodeFor(report, parsed) {
191
+ return auditExitCode(report.action, report.deep, { requireDeep: parsed.requireDeep, failOn: parsed.failOn });
192
+ }
193
+ function resolveScope(target) {
194
+ const absolute = resolve(target);
195
+ if (!existsSync(absolute)) {
196
+ return { error: `path does not exist: ${target}` };
197
+ }
198
+ const start = statSync(absolute).isDirectory() ? absolute : dirname(absolute);
199
+ const root = findPackageRoot(start);
200
+ if (!root) {
201
+ return { error: "no package manifest found here — run inside the package you're publishing, or pass its path" };
202
+ }
203
+ if (existsSync(resolve(root, "package.json"))) {
204
+ const packageJson = safeReadJson(resolve(root, "package.json"));
205
+ const name = packageJson && typeof packageJson.name === "string" ? packageJson.name : basename(root);
206
+ const version = packageJson && typeof packageJson.version === "string" ? packageJson.version : "(unknown)";
207
+ return { root, ecosystem: "npm", packageJson, artifact: `${name}@${version}` };
208
+ }
209
+ if (existsSync(resolve(root, "pyproject.toml")) || existsSync(resolve(root, "setup.py")) || existsSync(resolve(root, "setup.cfg"))) {
210
+ return { root, ecosystem: "pypi", packageJson: null, artifact: basename(root) };
211
+ }
212
+ if (existsSync(resolve(root, "Cargo.toml"))) {
213
+ return { root, ecosystem: "cargo", packageJson: null, artifact: basename(root) };
214
+ }
215
+ return { root, ecosystem: "unknown", packageJson: null, artifact: basename(root) };
216
+ }
217
+ function findPackageRoot(start) {
218
+ let current = start;
219
+ for (let depth = 0; depth < 40; depth += 1) {
220
+ if (existsSync(resolve(current, "package.json")) ||
221
+ existsSync(resolve(current, "pyproject.toml")) ||
222
+ existsSync(resolve(current, "setup.py")) ||
223
+ existsSync(resolve(current, "setup.cfg")) ||
224
+ existsSync(resolve(current, "Cargo.toml"))) {
225
+ return current;
226
+ }
227
+ const parent = dirname(current);
228
+ if (parent === current) {
229
+ return null;
230
+ }
231
+ current = parent;
232
+ }
233
+ return null;
234
+ }
235
+ const WARN_GLYPH = "\u26A0\uFE0E";
236
+ function renderReport(report, theme) {
237
+ const muted = (text) => theme.paint("muted", text);
238
+ const accent = (text) => theme.paint("accent", text);
239
+ const role = report.action === "block" ? "block" : report.action === "warn" ? "warn" : "pass";
240
+ const glyph = report.action === "block" ? "✘" : report.action === "warn" ? WARN_GLYPH : "✓";
241
+ const blocking = report.findings.filter((finding) => finding.severity >= 4).length;
242
+ const warnings = report.findings.filter((finding) => finding.severity === 3).length;
243
+ const notes = report.findings.filter((finding) => finding.severity < 3).length;
244
+ const counts = [
245
+ blocking ? `${blocking} blocking` : "",
246
+ warnings ? `${warnings} warning${warnings === 1 ? "" : "s"}` : "",
247
+ notes ? `${notes} note${notes === 1 ? "" : "s"}` : ""
248
+ ].filter(Boolean).join(" · ") || "no issues";
249
+ const fileLabel = `${report.fileCount} file${report.fileCount === 1 ? "" : "s"}`;
250
+ const fallback = report.publishSetSource === "fallback" ? " · publish set approximated" : "";
251
+ const lines = [
252
+ ...verdictBox([
253
+ `${theme.paint(role, `${glyph} ${report.action.toUpperCase()}`)} ${accent(report.artifact)} ${muted(`· ${report.ecosystem}`)}`,
254
+ muted(`${counts} in ${fileLabel}${fallback}`)
255
+ ], theme, role),
256
+ ""
257
+ ];
258
+ for (const finding of report.findings) {
259
+ const tag = finding.severity >= 4
260
+ ? theme.paint("block", "✘")
261
+ : finding.severity >= 3
262
+ ? theme.paint("warn", WARN_GLYPH)
263
+ : theme.paint("muted", "·");
264
+ lines.push(` ${tag} ${accent(findingLocation(finding))}`);
265
+ lines.push(` ${finding.title}`);
266
+ if (finding.evidence && finding.evidence !== `path: ${finding.location}` && finding.evidence !== finding.location) {
267
+ lines.push(` ${muted(finding.evidence)}`);
268
+ }
269
+ lines.push(` ${accent("→")} ${finding.recommendation}`);
270
+ lines.push("");
271
+ }
272
+ lines.push(` ${muted(`Deep behavioral scan · ${deepSummary(report.deep)}`)}`);
273
+ return `${lines.join("\n")}\n`;
274
+ }
275
+ function visibleWidth(text) {
276
+ return text.replace(/\u001b\[[0-9;]*m/g, "").replace(/[\uFE0E\uFE0F]/gu, "").length;
277
+ }
278
+ function verdictBox(content, theme, role) {
279
+ const inner = Math.max(...content.map(visibleWidth));
280
+ const edge = (text) => theme.paint(role, text);
281
+ return [
282
+ ` ${edge(`╭${"─".repeat(inner + 2)}╮`)}`,
283
+ ...content.map((line) => ` ${edge("│")} ${line}${" ".repeat(inner - visibleWidth(line))} ${edge("│")}`),
284
+ ` ${edge(`╰${"─".repeat(inner + 2)}╯`)}`
285
+ ];
286
+ }
287
+ function parseAuditArgs(args) {
288
+ let target = ".";
289
+ let sawTarget = false;
290
+ let format = "text";
291
+ let outputPath = null;
292
+ let local = false;
293
+ let requireDeep = false;
294
+ let failOn = "block";
295
+ for (let index = 0; index < args.length; index += 1) {
296
+ const arg = args[index];
297
+ if (!arg) {
298
+ return { error: "empty argument" };
299
+ }
300
+ if (arg === "--json") {
301
+ format = "json";
302
+ }
303
+ else if (arg === "--local") {
304
+ local = true;
305
+ }
306
+ else if (arg === "--require-deep") {
307
+ requireDeep = true;
308
+ }
309
+ else if (arg === "--output" || arg === "-o") {
310
+ const next = args[index + 1];
311
+ if (!next) {
312
+ return { error: `${arg} requires a path` };
313
+ }
314
+ outputPath = next;
315
+ index += 1;
316
+ }
317
+ else if (arg === "--fail-on") {
318
+ const next = args[index + 1];
319
+ if (next !== "warn" && next !== "block") {
320
+ return { error: "--fail-on requires 'warn' or 'block'" };
321
+ }
322
+ failOn = next;
323
+ index += 1;
324
+ }
325
+ else if (arg.startsWith("-")) {
326
+ return { error: `unknown option '${arg}'` };
327
+ }
328
+ else if (sawTarget) {
329
+ return { error: "audit accepts at most one path" };
330
+ }
331
+ else {
332
+ target = arg;
333
+ sawTarget = true;
334
+ }
335
+ }
336
+ return { target, format, outputPath, local, requireDeep, failOn };
337
+ }
338
+ export function displayTarget(root) {
339
+ const rel = root.startsWith(process.cwd()) ? root.slice(process.cwd().length).replace(/^[/\\]/u, "") : root;
340
+ return rel.length === 0 ? "." : rel;
341
+ }
342
+ function safeReadJson(path) {
343
+ try {
344
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
345
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
346
+ }
347
+ catch {
348
+ return null;
349
+ }
350
+ }
351
+ function usageError(message) {
352
+ return {
353
+ exitCode: EXIT_USAGE,
354
+ stdout: "",
355
+ stderr: `dg audit: ${message}. Usage: ${auditCommand.usage}\n`
356
+ };
357
+ }
@@ -0,0 +1,116 @@
1
+ import { commandCatalog } from "./router.js";
2
+ import { packageManagerCommandNames } from "./wrap.js";
3
+ import { EXIT_USAGE } from "./types.js";
4
+ const SHELLS = ["bash", "zsh", "fish"];
5
+ const COMMON_FLAGS = ["--help", "--version", "--json", "--sarif", "--output", "--yes", "--check", "--staged", "--dg-force-install"];
6
+ const FISH_FLAG_DESCRIPTIONS = {
7
+ "--help": "Show help",
8
+ "--version": "Show version",
9
+ "--json": "Write JSON output",
10
+ "--sarif": "Write SARIF output",
11
+ "--output": "Write report to file",
12
+ "--yes": "Apply a confirmed mutation",
13
+ "--check": "Report without mutating",
14
+ "--staged": "Limit to staged files",
15
+ "--dg-force-install": "Install despite a block, where policy allows"
16
+ };
17
+ function completionCommands() {
18
+ const names = new Set(packageManagerCommandNames);
19
+ for (const command of commandCatalog) {
20
+ names.add(command.name);
21
+ for (const alias of command.aliases ?? []) {
22
+ names.add(alias);
23
+ }
24
+ }
25
+ return [...names];
26
+ }
27
+ export const completionCommand = {
28
+ name: "completion",
29
+ summary: "Print shell completion for bash, zsh, or fish.",
30
+ usage: "dg completion <bash|zsh|fish>",
31
+ args: [{ name: "<shell>", summary: "bash, zsh, or fish." }],
32
+ examples: [
33
+ "dg completion bash > ~/.local/share/bash-completion/completions/dg",
34
+ "dg completion zsh > ~/.zfunc/_dg",
35
+ "dg completion fish > ~/.config/fish/completions/dg.fish"
36
+ ],
37
+ details: ["Prints a completion script to stdout without changing shell files."],
38
+ handler: (context) => completionHandler(context.args)
39
+ };
40
+ function completionHandler(args) {
41
+ const [shell, ...rest] = args;
42
+ if (!shell || rest.length > 0 || !isSupportedShell(shell)) {
43
+ return {
44
+ exitCode: EXIT_USAGE,
45
+ stdout: "",
46
+ stderr: "dg completion: expected one shell: bash, zsh, or fish.\n"
47
+ };
48
+ }
49
+ return {
50
+ exitCode: 0,
51
+ stdout: renderCompletion(shell),
52
+ stderr: ""
53
+ };
54
+ }
55
+ function isSupportedShell(value) {
56
+ return SHELLS.includes(value);
57
+ }
58
+ function renderCompletion(shell) {
59
+ const commandList = completionCommands();
60
+ const commands = commandList.join(" ");
61
+ const flags = COMMON_FLAGS.join(" ");
62
+ if (shell === "bash") {
63
+ return `# dg bash completion
64
+ _dg_completion() {
65
+ local cur prev
66
+ COMPREPLY=()
67
+ cur="\${COMP_WORDS[COMP_CWORD]}"
68
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
69
+ if [ "$prev" = "completion" ]; then
70
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
71
+ return 0
72
+ fi
73
+ if [ "$COMP_CWORD" -eq 1 ]; then
74
+ COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
75
+ return 0
76
+ fi
77
+ COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
78
+ }
79
+ complete -F _dg_completion dg
80
+ `;
81
+ }
82
+ if (shell === "zsh") {
83
+ return `#compdef dg
84
+ _dg() {
85
+ local -a commands flags shells
86
+ commands=(${commandList.map((command) => escapeZsh(command)).join(" ")})
87
+ flags=(${COMMON_FLAGS.map((flag) => escapeZsh(flag)).join(" ")})
88
+ shells=(bash zsh fish)
89
+ if [[ "$words[2]" == "completion" ]]; then
90
+ compadd "$@" -- "$shells[@]"
91
+ return
92
+ fi
93
+ if [[ "$CURRENT" -eq 2 ]]; then
94
+ compadd "$@" -- "$commands[@]"
95
+ return
96
+ fi
97
+ compadd "$@" -- "$flags[@]"
98
+ }
99
+ _dg "$@"
100
+ `;
101
+ }
102
+ const fishFlagLines = COMMON_FLAGS.map((flag) => {
103
+ const option = flag.replace(/^--/u, "");
104
+ const requiresArg = flag === "--output" ? " -r" : "";
105
+ const description = FISH_FLAG_DESCRIPTIONS[flag] ?? option;
106
+ return `complete -c dg -f -l ${option}${requiresArg} -d '${description}'`;
107
+ }).join("\n");
108
+ return `# dg fish completion
109
+ complete -c dg -f -n '__fish_is_first_arg' -a '${commands}' -d 'Dependency Guardian command'
110
+ complete -c dg -f -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell'
111
+ ${fishFlagLines}
112
+ `;
113
+ }
114
+ function escapeZsh(value) {
115
+ return value.replace(/([\\[\]{}()$`"'\s])/gu, "\\$1");
116
+ }
@@ -0,0 +1,99 @@
1
+ import { EXIT_USAGE } from "./types.js";
2
+ import { ConfigError, getConfigValue, isConfigKey, listConfig, loadUserConfig, saveUserConfig, setConfigValue, unsetConfigValue } from "../config/settings.js";
3
+ export const configCommand = {
4
+ name: "config",
5
+ summary: "Inspect or edit trusted dg configuration.",
6
+ usage: "dg config <get|set|unset|list> [key] [value] [--json]",
7
+ args: [
8
+ { name: "<action>", summary: "list | get <key> | set <key> <value> | unset <key>." },
9
+ { name: "[key]", summary: "e.g. policy.mode, gitHook.onWarn, gitHook.onIncomplete, api.baseUrl." }
10
+ ],
11
+ flags: [{ flag: "--json", summary: "Machine-readable output for list/get." }],
12
+ examples: ["dg config list", "dg config get policy.mode", "dg config set gitHook.onWarn allow", "dg config unset api.baseUrl"],
13
+ details: [
14
+ "Reads and writes user-global dg configuration only.",
15
+ "Project-local config and allowlists remain untrusted for install-time firewall enforcement by default."
16
+ ],
17
+ handler: (context) => configHandler(context.args)
18
+ };
19
+ function configHandler(args) {
20
+ const json = args.includes("--json");
21
+ const filtered = args.filter((arg) => arg !== "--json");
22
+ const [action, key, value, extra] = filtered;
23
+ if (!action || extra) {
24
+ return usageError("expected get, set, unset, or list");
25
+ }
26
+ try {
27
+ const config = loadUserConfig();
28
+ if (action === "list") {
29
+ if (key || value) {
30
+ return usageError("list does not accept a key or value");
31
+ }
32
+ const entries = listConfig(config);
33
+ return {
34
+ exitCode: 0,
35
+ stdout: json ? `${JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])), null, 2)}\n` : renderConfigList(entries),
36
+ stderr: ""
37
+ };
38
+ }
39
+ if (!key || !isConfigKey(key)) {
40
+ return usageError(`unknown config key '${key ?? ""}'`);
41
+ }
42
+ if (action === "get") {
43
+ if (value) {
44
+ return usageError("get accepts only a key");
45
+ }
46
+ const result = getConfigValue(config, key);
47
+ return {
48
+ exitCode: 0,
49
+ stdout: json ? `${JSON.stringify({ key, value: result }, null, 2)}\n` : `${result}\n`,
50
+ stderr: ""
51
+ };
52
+ }
53
+ if (action === "set") {
54
+ if (value === undefined) {
55
+ return usageError("set requires a value");
56
+ }
57
+ const next = setConfigValue(config, key, value);
58
+ saveUserConfig(next);
59
+ return {
60
+ exitCode: 0,
61
+ stdout: `${key}=${getConfigValue(next, key)}\n`,
62
+ stderr: ""
63
+ };
64
+ }
65
+ if (action === "unset") {
66
+ if (value) {
67
+ return usageError("unset accepts only a key");
68
+ }
69
+ const next = unsetConfigValue(config, key);
70
+ saveUserConfig(next);
71
+ return {
72
+ exitCode: 0,
73
+ stdout: `${key}=${getConfigValue(next, key)}\n`,
74
+ stderr: ""
75
+ };
76
+ }
77
+ return usageError(`unknown action '${action}'`);
78
+ }
79
+ catch (error) {
80
+ if (error instanceof ConfigError) {
81
+ return {
82
+ exitCode: EXIT_USAGE,
83
+ stdout: "",
84
+ stderr: `dg config: ${error.message}\n`
85
+ };
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ function renderConfigList(entries) {
91
+ return `${entries.map((entry) => `${entry.key}=${entry.value}`).join("\n")}\n`;
92
+ }
93
+ function usageError(message) {
94
+ return {
95
+ exitCode: EXIT_USAGE,
96
+ stdout: "",
97
+ stderr: `dg config: ${message}. Run 'dg config --help'.\n`
98
+ };
99
+ }
@@ -0,0 +1,39 @@
1
+ import { EXIT_USAGE } from "./types.js";
2
+ import { doctorReport, renderDoctorReport } from "../setup/plan.js";
3
+ import { resolvePresentation } from "../presentation/mode.js";
4
+ import { createTheme } from "../presentation/theme.js";
5
+ export const doctorCommand = {
6
+ name: "doctor",
7
+ summary: "Inspect local dg health and setup state.",
8
+ usage: "dg doctor [--verbose] [--json]",
9
+ flags: [
10
+ { flag: "--verbose", summary: "List every check, including passing and gated ones (alias -v)." },
11
+ { flag: "--json", summary: "Machine-readable check results." }
12
+ ],
13
+ examples: ["dg doctor", "dg doctor --verbose"],
14
+ details: [
15
+ "Checks runtime, package, config, auth, PATH, package-manager resolution, stale state, optional support gates, service state, and next fix commands.",
16
+ "Groups checks and collapses passing ones; --verbose lists every check including gated/remote surfaces."
17
+ ],
18
+ handler: (context) => doctorHandler(context.args)
19
+ };
20
+ function doctorHandler(args) {
21
+ const json = args.includes("--json");
22
+ const verbose = args.includes("--verbose") || args.includes("-v");
23
+ const unknown = args.find((arg) => arg !== "--json" && arg !== "--verbose" && arg !== "-v");
24
+ if (unknown) {
25
+ return {
26
+ exitCode: EXIT_USAGE,
27
+ stdout: "",
28
+ stderr: `dg doctor: unknown option '${unknown}'. Run 'dg doctor --help'.\n`
29
+ };
30
+ }
31
+ const report = doctorReport();
32
+ const hasFailure = report.checks.some((check) => check.status === "fail");
33
+ const theme = createTheme(resolvePresentation().color);
34
+ return {
35
+ exitCode: hasFailure ? 1 : 0,
36
+ stdout: json ? `${JSON.stringify(report, null, 2)}\n` : renderDoctorReport(report, theme, verbose),
37
+ stderr: ""
38
+ };
39
+ }