@westbayberry/dg 1.3.2 → 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 -54141
  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,302 @@
1
+ import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
2
+ import { LockBusyError } from "../state/index.js";
3
+ import { activationCommand as shellActivationCommand, applySetupPlanWithLock, buildSetupPlan, renderSetupPlan } from "../setup/plan.js";
4
+ import { buildServiceSetupPlan, configureService, renderServicePlan } from "../service/state.js";
5
+ import { optionalSupportGate } from "../setup/optional-support.js";
6
+ import { resolvePresentation } from "../presentation/mode.js";
7
+ import { createTheme } from "../presentation/theme.js";
8
+ import { closeSync, openSync, readSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { authStatus } from "../auth/store.js";
11
+ export const setupCommand = {
12
+ name: "setup",
13
+ summary: "Protect plain npm/pip/yarn installs (no dg prefix needed).",
14
+ usage: "dg setup [--print] [--yes] [--shell <auto|zsh|bash|fish>] [--service]",
15
+ flags: [
16
+ { flag: "--print", summary: "Preview the exact write plan and change nothing." },
17
+ { flag: "--yes", summary: "Apply without the interactive prompt (required when non-interactive)." },
18
+ { flag: "--shell", value: "<auto|zsh|bash|fish>", summary: "Target shell rc to write (default: auto-detect)." },
19
+ { flag: "--service", summary: "Set up service mode (Pro/Team; persistent proxy + managed CA)." }
20
+ ],
21
+ examples: ["dg setup --print", "dg setup --yes", "dg setup --shell zsh --yes"],
22
+ details: [
23
+ "In a terminal, shows what it writes and asks before applying, then reloads your shell. Use --yes to apply non-interactively and --print to preview only.",
24
+ "Writes only dg-owned reversible files and repairs setup drift; undo with dg uninstall. The per-repo git pre-commit hook is separate — see dg guard-commit."
25
+ ],
26
+ handler: (context) => setupHandler(context.args)
27
+ };
28
+ const WINDOWS_UNSUPPORTED = "dg setup does not support Windows yet — Linux and macOS only.\n";
29
+ function setupHandler(args) {
30
+ const parsed = parseSetupArgs(args);
31
+ if ("error" in parsed) {
32
+ return {
33
+ exitCode: parsed.exitCode,
34
+ stdout: "",
35
+ stderr: parsed.error
36
+ };
37
+ }
38
+ if (process.platform === "win32") {
39
+ return {
40
+ exitCode: EXIT_UNAVAILABLE,
41
+ stdout: "",
42
+ stderr: WINDOWS_UNSUPPORTED
43
+ };
44
+ }
45
+ if (parsed.service) {
46
+ return serviceSetupHandler(parsed);
47
+ }
48
+ const plan = buildSetupPlan({
49
+ shell: parsed.shell
50
+ });
51
+ const renderedPlan = renderSetupPlan(plan);
52
+ if (parsed.printOnly) {
53
+ return {
54
+ exitCode: 0,
55
+ stdout: renderedPlan,
56
+ stderr: ""
57
+ };
58
+ }
59
+ if (!parsed.yes) {
60
+ if (isInteractive()) {
61
+ return runInteractiveSetup(plan);
62
+ }
63
+ return {
64
+ exitCode: EXIT_USAGE,
65
+ stdout: renderedPlan,
66
+ stderr: "dg setup requires --yes to apply this non-interactive write plan.\n"
67
+ };
68
+ }
69
+ try {
70
+ applySetupPlanWithLock(plan);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof LockBusyError) {
74
+ return {
75
+ exitCode: 1,
76
+ stdout: renderedPlan,
77
+ stderr: `dg setup cannot apply while another setup or uninstall is running: ${error.path}\n`
78
+ };
79
+ }
80
+ throw error;
81
+ }
82
+ return {
83
+ exitCode: 0,
84
+ stdout: `${renderedPlan}\nSet up — active in new terminals. Activate this shell now: ${activationCommand(plan)}\n`,
85
+ stderr: ""
86
+ };
87
+ }
88
+ function isInteractive() {
89
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
90
+ }
91
+ function runInteractiveSetup(plan) {
92
+ const theme = createTheme(resolvePresentation().color);
93
+ const accent = (text) => theme.paint("accent", text);
94
+ const muted = (text) => theme.paint("muted", text);
95
+ const err = process.stderr;
96
+ err.write(`\n Scans ${accent("npm")}/${accent("pip")}/${accent("yarn")}/${muted("…")} installs automatically — no ${accent("dg")} prefix.\n`);
97
+ err.write(` ${muted("Writes")} ${accent(tildify(plan.rcPath))}${muted(".")} ${muted("Reversible with")} ${accent("dg uninstall")}${muted(".")}\n\n`);
98
+ const answer = promptYesNoSync(` ${accent("Proceed?")}`, theme);
99
+ if (answer === "no-tty") {
100
+ return {
101
+ exitCode: EXIT_USAGE,
102
+ stdout: "",
103
+ stderr: ` ${muted("no terminal to read consent — re-run with")} ${accent("dg setup --yes")} ${muted("to apply non-interactively")}\n`
104
+ };
105
+ }
106
+ if (answer !== "yes") {
107
+ return { exitCode: 0, stdout: "", stderr: ` ${muted("cancelled — nothing written")}\n` };
108
+ }
109
+ try {
110
+ applySetupPlanWithLock(plan);
111
+ }
112
+ catch (error) {
113
+ if (error instanceof LockBusyError) {
114
+ return {
115
+ exitCode: 1,
116
+ stdout: "",
117
+ stderr: ` dg setup cannot apply while another setup or uninstall is running: ${error.path}\n`
118
+ };
119
+ }
120
+ throw error;
121
+ }
122
+ err.write(`\n ${theme.paint("pass", "✓ set up — active in new terminals")}\n`);
123
+ err.write(` ${muted("Activate this shell now:")} ${accent(activationCommand(plan))}\n`);
124
+ return { exitCode: 0, stdout: "", stderr: "" };
125
+ }
126
+ function activationCommand(plan) {
127
+ return shellActivationCommand(plan.shell, tildify(plan.rcPath));
128
+ }
129
+ function promptYesNoSync(question, theme) {
130
+ process.stderr.write(`${question} ${theme.paint("muted", "[y/N]")} `);
131
+ let tty;
132
+ try {
133
+ tty = openSync("/dev/tty", "rs");
134
+ }
135
+ catch {
136
+ return "no-tty";
137
+ }
138
+ try {
139
+ const byte = Buffer.alloc(1);
140
+ let answer = "";
141
+ for (;;) {
142
+ let read = 0;
143
+ try {
144
+ read = readSync(tty, byte, 0, 1, null);
145
+ }
146
+ catch (error) {
147
+ if (error.code === "EAGAIN") {
148
+ continue;
149
+ }
150
+ break;
151
+ }
152
+ if (read === 0) {
153
+ break;
154
+ }
155
+ const char = byte.toString("utf8");
156
+ if (char === "\n" || char === "\r") {
157
+ break;
158
+ }
159
+ answer += char;
160
+ }
161
+ const normalized = answer.trim().toLowerCase();
162
+ return normalized === "y" || normalized === "yes" ? "yes" : "no";
163
+ }
164
+ finally {
165
+ closeSync(tty);
166
+ }
167
+ }
168
+ function tildify(path) {
169
+ const home = homedir();
170
+ return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
171
+ }
172
+ function parseSetupArgs(args) {
173
+ let printOnly = false;
174
+ let yes = false;
175
+ let shell = "auto";
176
+ let service = false;
177
+ for (let index = 0; index < args.length; index += 1) {
178
+ const arg = args[index];
179
+ if (arg === "--print") {
180
+ printOnly = true;
181
+ }
182
+ else if (arg === "--yes") {
183
+ yes = true;
184
+ }
185
+ else if (arg === "--service") {
186
+ service = true;
187
+ }
188
+ else if (arg === "--python-hook" || arg?.startsWith("--python-hook=")) {
189
+ return {
190
+ exitCode: EXIT_UNAVAILABLE,
191
+ error: `dg setup --python-hook is gated and no files were changed. ${optionalSupportGate("python-hook").message}.\n`
192
+ };
193
+ }
194
+ else if (arg === "--git-hooks") {
195
+ return {
196
+ exitCode: EXIT_USAGE,
197
+ error: "dg setup --git-hooks moved — run 'dg guard-commit' inside the repo you want to protect.\n"
198
+ };
199
+ }
200
+ else if (arg === "--shell") {
201
+ const value = args[index + 1];
202
+ if (!value) {
203
+ return {
204
+ exitCode: EXIT_USAGE,
205
+ error: "dg setup --shell requires one of: auto, zsh, bash, fish.\n"
206
+ };
207
+ }
208
+ const parsedShell = parseShell(value);
209
+ if (!parsedShell) {
210
+ if (value === "powershell") {
211
+ return {
212
+ exitCode: EXIT_UNAVAILABLE,
213
+ error: `dg setup --shell powershell is gated and no files were changed. ${optionalSupportGate("windows").message}.\n`
214
+ };
215
+ }
216
+ return {
217
+ exitCode: EXIT_USAGE,
218
+ error: `dg setup does not support shell '${value}' in this build. Supported shells: auto, zsh, bash, fish.\n`
219
+ };
220
+ }
221
+ shell = parsedShell;
222
+ index += 1;
223
+ }
224
+ else if (arg?.startsWith("--shell=")) {
225
+ const value = arg.slice("--shell=".length);
226
+ const parsedShell = parseShell(value);
227
+ if (!parsedShell) {
228
+ if (value === "powershell") {
229
+ return {
230
+ exitCode: EXIT_UNAVAILABLE,
231
+ error: `dg setup --shell powershell is gated and no files were changed. ${optionalSupportGate("windows").message}.\n`
232
+ };
233
+ }
234
+ return {
235
+ exitCode: EXIT_USAGE,
236
+ error: `dg setup does not support shell '${value}' in this build. Supported shells: auto, zsh, bash, fish.\n`
237
+ };
238
+ }
239
+ shell = parsedShell;
240
+ }
241
+ else {
242
+ return {
243
+ exitCode: EXIT_USAGE,
244
+ error: `dg setup: unknown option '${arg}'. Run 'dg setup --help'.\n`
245
+ };
246
+ }
247
+ }
248
+ return {
249
+ printOnly,
250
+ yes,
251
+ shell,
252
+ service
253
+ };
254
+ }
255
+ function parseShell(value) {
256
+ if (value === "auto" || value === "zsh" || value === "bash" || value === "fish") {
257
+ return value;
258
+ }
259
+ return null;
260
+ }
261
+ function serviceSetupHandler(parsed) {
262
+ const renderedPlan = renderServicePlan("Dependency Guardian service setup write plan", buildServiceSetupPlan());
263
+ if (parsed.printOnly) {
264
+ return {
265
+ exitCode: 0,
266
+ stdout: renderedPlan,
267
+ stderr: ""
268
+ };
269
+ }
270
+ if (!authStatus().authenticated) {
271
+ return {
272
+ exitCode: EXIT_UNAVAILABLE,
273
+ stdout: "",
274
+ stderr: "dg setup --service is a Pro or Team feature. Run 'dg login' to connect a paid account, then 'dg setup --service'. See https://westbayberry.com/pricing\n"
275
+ };
276
+ }
277
+ if (!parsed.yes) {
278
+ return {
279
+ exitCode: EXIT_USAGE,
280
+ stdout: renderedPlan,
281
+ stderr: "dg setup --service requires --yes to apply this non-interactive write plan.\n"
282
+ };
283
+ }
284
+ try {
285
+ const result = configureService();
286
+ return {
287
+ exitCode: 0,
288
+ stdout: `${renderedPlan}\nService mode ${result.changed ? "configured" : "already configured"}. Run 'dg service start' to start it.\n`,
289
+ stderr: ""
290
+ };
291
+ }
292
+ catch (error) {
293
+ if (error instanceof LockBusyError) {
294
+ return {
295
+ exitCode: 1,
296
+ stdout: renderedPlan,
297
+ stderr: `dg setup --service cannot apply while service state is locked: ${error.path}\n`
298
+ };
299
+ }
300
+ throw error;
301
+ }
302
+ }
@@ -0,0 +1,115 @@
1
+ import { authStatus, displayTier } from "../auth/store.js";
2
+ import { loadUserConfig } from "../config/settings.js";
3
+ import { resolvePresentation } from "../presentation/mode.js";
4
+ import { createTheme } from "../presentation/theme.js";
5
+ import { doctorReport } from "../setup/plan.js";
6
+ import { gitHookStatusState } from "../setup/git-hook.js";
7
+ import { EXIT_USAGE } from "./types.js";
8
+ export const statusCommand = {
9
+ name: "status",
10
+ summary: "Show what dg protects on this machine right now.",
11
+ usage: "dg status [--json]",
12
+ flags: [{ flag: "--json", summary: "Machine-readable status snapshot." }],
13
+ examples: ["dg status", "dg status --json"],
14
+ details: [
15
+ "A quick snapshot: account connection, bare-command protection, commit guard (inside a repo), and the active policy.",
16
+ "For full diagnostics with fix commands, run 'dg doctor'."
17
+ ],
18
+ handler: (context) => runStatusCommand(context.args)
19
+ };
20
+ function runStatusCommand(args) {
21
+ const json = args.includes("--json");
22
+ const unknown = args.find((arg) => arg !== "--json");
23
+ if (unknown) {
24
+ return {
25
+ exitCode: EXIT_USAGE,
26
+ stdout: "",
27
+ stderr: `dg status: unknown option '${unknown}'. Run 'dg status --help'.\n`
28
+ };
29
+ }
30
+ const report = buildStatusReport();
31
+ if (json) {
32
+ return {
33
+ exitCode: 0,
34
+ stdout: `${JSON.stringify(report, null, 2)}\n`,
35
+ stderr: ""
36
+ };
37
+ }
38
+ return {
39
+ exitCode: 0,
40
+ stdout: renderStatus(report, createTheme(resolvePresentation().color)),
41
+ stderr: ""
42
+ };
43
+ }
44
+ function buildStatusReport() {
45
+ const checks = doctorReport().checks;
46
+ const checkPassed = (name) => checks.find((check) => check.name === name)?.status === "pass";
47
+ const auth = safeAuthStatus();
48
+ const policy = loadUserConfig().policy;
49
+ return {
50
+ account: auth,
51
+ protection: { shims: checkPassed("shims"), path: checkPassed("path") },
52
+ commitGuard: safeCommitGuard(),
53
+ policy: { mode: policy.mode, trustProjectAllowlists: policy.trustProjectAllowlists }
54
+ };
55
+ }
56
+ function safeCommitGuard() {
57
+ try {
58
+ return gitHookStatusState();
59
+ }
60
+ catch {
61
+ return "not-a-repo";
62
+ }
63
+ }
64
+ function safeAuthStatus() {
65
+ try {
66
+ const status = authStatus();
67
+ return {
68
+ connected: status.authenticated,
69
+ tokenPreview: status.tokenPreview,
70
+ ...(status.email ? { email: status.email } : {}),
71
+ ...(status.tier ? { tier: status.tier } : {})
72
+ };
73
+ }
74
+ catch {
75
+ return { connected: false, tokenPreview: "" };
76
+ }
77
+ }
78
+ function renderStatus(report, theme) {
79
+ const account = report.account.connected
80
+ ? report.account.email && report.account.tier
81
+ ? `${theme.paint("pass", "✓")} ${report.account.email} · ${displayTier(report.account.tier)} plan`
82
+ : `${theme.paint("pass", "✓")} connected (${report.account.tokenPreview})`
83
+ : `${theme.paint("warn", "⚠")} not connected — run ${theme.paint("accent", "dg login")}`;
84
+ const bare = report.protection.shims && report.protection.path
85
+ ? `${theme.paint("pass", "✓")} bare npm/pip installs are protected`
86
+ : report.protection.shims
87
+ ? `${theme.paint("warn", "⚠")} set up — active in new terminals (or prefix commands with ${theme.paint("accent", "dg")})`
88
+ : `${theme.paint("warn", "⚠")} not set up — run ${theme.paint("accent", "dg setup")}, or prefix commands with ${theme.paint("accent", "dg")}`;
89
+ const lines = [
90
+ "Dependency Guardian status",
91
+ "",
92
+ ` Account ${account}`,
93
+ ` Installs ${bare}`
94
+ ];
95
+ const guard = commitGuardLine(report.commitGuard, theme);
96
+ if (guard) {
97
+ lines.push(` Commit guard ${guard}`);
98
+ }
99
+ lines.push(` Policy ${report.policy.mode} mode; project allowlists ${report.policy.trustProjectAllowlists ? "trusted" : "untrusted"}`);
100
+ lines.push("");
101
+ lines.push(`Full diagnostics: ${theme.paint("accent", "dg doctor")}`);
102
+ return `${lines.join("\n")}\n`;
103
+ }
104
+ function commitGuardLine(state, theme) {
105
+ if (state === "not-a-repo") {
106
+ return null;
107
+ }
108
+ if (state === "active") {
109
+ return `${theme.paint("pass", "✓")} this repo's commits are scanned`;
110
+ }
111
+ if (state === "dead") {
112
+ return `${theme.paint("warn", "⚠")} installed but git isn't using it — run ${theme.paint("accent", "dg guard-commit --check")}`;
113
+ }
114
+ return `${theme.paint("warn", "⚠")} off — run ${theme.paint("accent", "dg guard-commit")}`;
115
+ }
@@ -0,0 +1,35 @@
1
+ const MAX_SUGGESTION_DISTANCE = 3;
2
+ function editDistance(a, b) {
3
+ const m = a.length;
4
+ const n = b.length;
5
+ const width = n + 1;
6
+ const dp = new Int32Array((m + 1) * width);
7
+ for (let i = 0; i <= m; i += 1) {
8
+ dp[i * width] = i;
9
+ }
10
+ for (let j = 0; j <= n; j += 1) {
11
+ dp[j] = j;
12
+ }
13
+ for (let i = 1; i <= m; i += 1) {
14
+ for (let j = 1; j <= n; j += 1) {
15
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
16
+ const up = dp[(i - 1) * width + j] ?? 0;
17
+ const left = dp[i * width + (j - 1)] ?? 0;
18
+ const diagonal = dp[(i - 1) * width + (j - 1)] ?? 0;
19
+ dp[i * width + j] = Math.min(up + 1, left + 1, diagonal + cost);
20
+ }
21
+ }
22
+ return dp[m * width + n] ?? 0;
23
+ }
24
+ export function closestCommand(input, commands) {
25
+ let best = null;
26
+ let bestDistance = Number.POSITIVE_INFINITY;
27
+ for (const command of commands) {
28
+ const distance = editDistance(input, command);
29
+ if (distance < bestDistance) {
30
+ bestDistance = distance;
31
+ best = command;
32
+ }
33
+ }
34
+ return best !== null && bestDistance <= MAX_SUGGESTION_DISTANCE ? best : null;
35
+ }
@@ -0,0 +1,4 @@
1
+ export const EXIT_USAGE = 2;
2
+ export const EXIT_NOTHING_TO_SCAN = 10;
3
+ export const EXIT_UNAVAILABLE = 69;
4
+ export const EXIT_TOOL_ERROR = 70;
@@ -0,0 +1,11 @@
1
+ import { EXIT_UNAVAILABLE } from "./types.js";
2
+ export function unavailableCommand(feature, nextStep) {
3
+ return (context) => {
4
+ const invoked = ["dg", ...context.commandPath, ...context.args].join(" ");
5
+ return {
6
+ exitCode: EXIT_UNAVAILABLE,
7
+ stdout: "",
8
+ stderr: `${invoked} is not available in this CLI build. ${feature}. ${nextStep}\n`
9
+ };
10
+ };
11
+ }
@@ -0,0 +1,111 @@
1
+ import { EXIT_USAGE } from "./types.js";
2
+ import { LockBusyError } from "../state/index.js";
3
+ import { uninstallSetup } from "../setup/plan.js";
4
+ import { serviceUninstallHandler } from "./service.js";
5
+ export const uninstallCommand = {
6
+ name: "uninstall",
7
+ summary: "Reverse dg-owned setup writes.",
8
+ usage: "dg uninstall [--yes] [--service] [--all] [--keep-config]",
9
+ flags: [
10
+ { flag: "--yes", summary: "Remove without the confirmation prompt." },
11
+ { flag: "--keep-config", summary: "Keep ~/.dg config and cache." },
12
+ { flag: "--all", summary: "Also remove the config directory — a full wipe." },
13
+ { flag: "--service", summary: "Reverse only service-mode writes." }
14
+ ],
15
+ examples: ["dg uninstall", "dg uninstall --yes --keep-config", "dg uninstall --all --yes"],
16
+ details: [
17
+ "Removes only dg-owned writes (shims, shell-rc block, git hooks), tolerates missing or malformed state, runs twice safely, and preserves user content."
18
+ ],
19
+ handler: (context) => uninstallHandler(context.args)
20
+ };
21
+ function uninstallHandler(args) {
22
+ const parsed = parseUninstallArgs(args);
23
+ if ("error" in parsed) {
24
+ return {
25
+ exitCode: parsed.exitCode,
26
+ stdout: "",
27
+ stderr: parsed.error
28
+ };
29
+ }
30
+ if (parsed.service) {
31
+ return serviceUninstallHandler(args.filter((arg) => arg !== "--service"));
32
+ }
33
+ if (!parsed.yes) {
34
+ return {
35
+ exitCode: EXIT_USAGE,
36
+ stdout: "Dependency Guardian uninstall will remove only registered dg-owned setup writes.\n",
37
+ stderr: "dg uninstall requires --yes to remove files in non-interactive mode.\n"
38
+ };
39
+ }
40
+ let result;
41
+ try {
42
+ result = uninstallSetup({
43
+ keepConfig: parsed.keepConfig,
44
+ all: parsed.all
45
+ });
46
+ }
47
+ catch (error) {
48
+ if (error instanceof LockBusyError) {
49
+ return {
50
+ exitCode: 1,
51
+ stdout: "Dependency Guardian uninstall will remove only registered dg-owned setup writes.\n",
52
+ stderr: `dg uninstall cannot run while another setup or uninstall is running: ${error.path}\n`
53
+ };
54
+ }
55
+ throw error;
56
+ }
57
+ const lines = [
58
+ "Dependency Guardian uninstall",
59
+ "",
60
+ ...result.removed.map((path) => `removed: ${path}`),
61
+ ...result.staleSessions.map((id) => `stale session removed: ${id}`),
62
+ ...result.missing.map((path) => `already absent: ${path}`),
63
+ ...result.warnings.map((warning) => `warning: ${warning}`),
64
+ result.removed.length === 0 && result.staleSessions.length === 0 && result.warnings.length === 0
65
+ ? "No dg-owned setup writes were present."
66
+ : "Uninstall completed."
67
+ ];
68
+ return {
69
+ exitCode: 0,
70
+ stdout: `${lines.join("\n")}\n`,
71
+ stderr: ""
72
+ };
73
+ }
74
+ function parseUninstallArgs(args) {
75
+ let yes = false;
76
+ let keepConfig = false;
77
+ let all = false;
78
+ let service = false;
79
+ for (const arg of args) {
80
+ if (arg === "--yes") {
81
+ yes = true;
82
+ }
83
+ else if (arg === "--keep-config") {
84
+ keepConfig = true;
85
+ }
86
+ else if (arg === "--all") {
87
+ all = true;
88
+ }
89
+ else if (arg === "--service") {
90
+ service = true;
91
+ }
92
+ else {
93
+ return {
94
+ exitCode: EXIT_USAGE,
95
+ error: `dg uninstall: unknown option '${arg}'. Run 'dg uninstall --help'.\n`
96
+ };
97
+ }
98
+ }
99
+ if (all && keepConfig) {
100
+ return {
101
+ exitCode: EXIT_USAGE,
102
+ error: "dg uninstall: --all and --keep-config conflict — choose one. Run 'dg uninstall --help'.\n"
103
+ };
104
+ }
105
+ return {
106
+ yes,
107
+ keepConfig,
108
+ all,
109
+ service
110
+ };
111
+ }