@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.
- package/LICENSE +1 -201
- package/NOTICE +1 -4
- package/README.md +293 -0
- package/dist/api/analyze.js +210 -0
- package/dist/audit/deep.js +180 -0
- package/dist/audit/detectors.js +247 -0
- package/dist/audit/events.js +41 -0
- package/dist/audit/rules.js +426 -0
- package/dist/audit-ui/AuditApp.js +39 -0
- package/dist/audit-ui/components/AuditHeader.js +24 -0
- package/dist/audit-ui/components/AuditResultsView.js +307 -0
- package/dist/audit-ui/components/DeepStatusRow.js +11 -0
- package/dist/audit-ui/export.js +85 -0
- package/dist/audit-ui/format.js +34 -0
- package/dist/audit-ui/launch.js +34 -0
- package/dist/auth/device-login.js +271 -0
- package/dist/auth/env-token.js +6 -0
- package/dist/auth/login-app.js +156 -0
- package/dist/auth/store.js +147 -0
- package/dist/bin/dg.js +71 -0
- package/dist/commands/audit.js +357 -0
- package/dist/commands/completion.js +116 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/doctor.js +39 -0
- package/dist/commands/explain.js +100 -0
- package/dist/commands/guard-commit.js +158 -0
- package/dist/commands/help.js +74 -0
- package/dist/commands/licenses.js +435 -0
- package/dist/commands/login.js +81 -0
- package/dist/commands/logout.js +37 -0
- package/dist/commands/router.js +98 -0
- package/dist/commands/scan.js +18 -0
- package/dist/commands/service.js +475 -0
- package/dist/commands/setup.js +302 -0
- package/dist/commands/status.js +115 -0
- package/dist/commands/suggest.js +35 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/unavailable.js +11 -0
- package/dist/commands/uninstall.js +111 -0
- package/dist/commands/update.js +210 -0
- package/dist/commands/verify.js +151 -0
- package/dist/commands/version.js +22 -0
- package/dist/commands/wrap.js +55 -0
- package/dist/config/settings.js +302 -0
- package/dist/install-ui/LiveInstall.js +24 -0
- package/dist/install-ui/block-render.js +83 -0
- package/dist/install-ui/live-install-app.js +48 -0
- package/dist/install-ui/prompt.js +24 -0
- package/dist/launcher/classify.js +116 -0
- package/dist/launcher/env.js +53 -0
- package/dist/launcher/live-install.js +50 -0
- package/dist/launcher/output-redaction.js +77 -0
- package/dist/launcher/preflight-prompt.js +139 -0
- package/dist/launcher/resolve-real-binary.js +73 -0
- package/dist/launcher/run.js +417 -0
- package/dist/policy/evaluate.js +128 -0
- package/dist/presentation/mode.js +52 -0
- package/dist/presentation/theme.js +29 -0
- package/dist/proxy/buffer-budget.js +64 -0
- package/dist/proxy/ca.js +126 -0
- package/dist/proxy/classify-host.js +26 -0
- package/dist/proxy/enforcement.js +102 -0
- package/dist/proxy/metadata-map.js +336 -0
- package/dist/proxy/server.js +909 -0
- package/dist/proxy/upstream-proxy.js +102 -0
- package/dist/proxy/worker.js +39 -0
- package/dist/publish-set/collect.js +51 -0
- package/dist/publish-set/no-exec-shell.js +19 -0
- package/dist/publish-set/npm.js +109 -0
- package/dist/publish-set/pack.js +36 -0
- package/dist/publish-set/pypi.js +59 -0
- package/dist/runtime/cli.js +17 -0
- package/dist/runtime/first-run.js +60 -0
- package/dist/runtime/node-version.js +58 -0
- package/dist/runtime/nudges.js +105 -0
- package/dist/scan/analyze-worker.js +21 -0
- package/dist/scan/collect.js +153 -0
- package/dist/scan/command.js +159 -0
- package/dist/scan/discovery.js +209 -0
- package/dist/scan/render.js +240 -0
- package/dist/scan/scanner-report.js +82 -0
- package/dist/scan/staged.js +173 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan-ui/LegacyApp.js +156 -0
- package/dist/scan-ui/alt-screen.js +84 -0
- package/dist/scan-ui/api-aliases.js +1 -0
- package/dist/scan-ui/components/ErrorView.js +23 -0
- package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
- package/dist/scan-ui/components/ProgressBar.js +89 -0
- package/dist/scan-ui/components/ProjectSelector.js +62 -0
- package/dist/scan-ui/components/ScoreHeader.js +20 -0
- package/dist/scan-ui/components/SetupBanner.js +13 -0
- package/dist/scan-ui/components/Spinner.js +4 -0
- package/dist/scan-ui/format-helpers.js +40 -0
- package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
- package/dist/scan-ui/hooks/useScan.js +113 -0
- package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
- package/dist/scan-ui/launch.js +27 -0
- package/dist/scan-ui/logo.js +91 -0
- package/dist/scan-ui/shims.js +30 -0
- package/dist/security/sanitize.js +28 -0
- package/dist/service/state.js +837 -0
- package/dist/service/trust-store.js +234 -0
- package/dist/service/worker.js +88 -0
- package/dist/setup/git-hook.js +244 -0
- package/dist/setup/optional-support.js +58 -0
- package/dist/setup/plan.js +899 -0
- package/dist/state/cleanup-registry.js +60 -0
- package/dist/state/index.js +5 -0
- package/dist/state/locks.js +161 -0
- package/dist/state/paths.js +24 -0
- package/dist/state/sessions.js +170 -0
- package/dist/state/store.js +50 -0
- package/dist/telemetry/events.js +40 -0
- package/dist/util/git.js +20 -0
- package/dist/util/tty-prompt.js +43 -0
- package/dist/verify/local.js +400 -0
- package/dist/verify/package-check.js +240 -0
- package/dist/verify/preflight.js +698 -0
- package/dist/verify/render.js +184 -0
- package/dist/verify/types.js +1 -0
- package/package.json +33 -50
- package/dist/index.mjs +0 -54141
- package/dist/postinstall.mjs +0 -731
- package/dist/python-hook/dg_pip_hook.pth +0 -1
- package/dist/python-hook/dg_pip_hook.py +0 -130
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { chmodSync } from "node:fs";
|
|
4
|
+
import { createTheme } from "../presentation/theme.js";
|
|
5
|
+
import { acquireLockSync, findStaleSessionsSync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
6
|
+
import { currentNodeVersion, isSupportedNode } from "../runtime/node-version.js";
|
|
7
|
+
import { dgVersion } from "../commands/version.js";
|
|
8
|
+
import { AuthError, authStatus, displayTier } from "../auth/store.js";
|
|
9
|
+
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
10
|
+
import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
|
|
11
|
+
import { packageManagerNames } from "../launcher/classify.js";
|
|
12
|
+
import { readServiceState } from "../service/state.js";
|
|
13
|
+
import { OPTIONAL_SUPPORT_GATES } from "./optional-support.js";
|
|
14
|
+
export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"]);
|
|
15
|
+
export const SHIM_SENTINEL = "dg-shim-v1";
|
|
16
|
+
export const RC_SENTINEL = "dg-shell-rc-v1";
|
|
17
|
+
export const GUARD_HOOK_SENTINEL = "dg-git-hook-v1";
|
|
18
|
+
export const RC_BEGIN = "# >>> dg setup >>>";
|
|
19
|
+
export const RC_END = "# <<< dg setup <<<";
|
|
20
|
+
export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
|
|
21
|
+
export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
22
|
+
export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
const DOCTOR_GROUP_BY_NAME = {
|
|
24
|
+
node: "environment",
|
|
25
|
+
package: "environment",
|
|
26
|
+
"dg-binary-path": "environment",
|
|
27
|
+
"real-binary-resolution": "environment",
|
|
28
|
+
"recursive-shim-guard": "environment",
|
|
29
|
+
"package-manager-discovery": "environment",
|
|
30
|
+
"cleanup-registry": "setup",
|
|
31
|
+
"cleanup-registry-stale-entries": "setup",
|
|
32
|
+
config: "setup",
|
|
33
|
+
policy: "setup",
|
|
34
|
+
telemetry: "setup",
|
|
35
|
+
shims: "setup",
|
|
36
|
+
"shell-rc": "setup",
|
|
37
|
+
path: "setup",
|
|
38
|
+
"stale-sessions": "setup",
|
|
39
|
+
service: "setup",
|
|
40
|
+
auth: "account"
|
|
41
|
+
};
|
|
42
|
+
const DOCTOR_FIX_BY_NAME = {
|
|
43
|
+
node: "upgrade Node to >=22.14.0",
|
|
44
|
+
"dg-binary-path": "put the dg bin directory first on PATH",
|
|
45
|
+
"cleanup-registry": "re-run dg setup",
|
|
46
|
+
"cleanup-registry-stale-entries": "re-run dg setup to refresh",
|
|
47
|
+
config: "fix or remove ~/.dg, then re-run dg setup",
|
|
48
|
+
policy: "fix ~/.dg config",
|
|
49
|
+
telemetry: "fix ~/.dg config",
|
|
50
|
+
shims: "dg setup",
|
|
51
|
+
"shell-rc": "dg setup",
|
|
52
|
+
path: "reload your shell after setup",
|
|
53
|
+
"stale-sessions": "clears on the next protected run",
|
|
54
|
+
auth: "dg login"
|
|
55
|
+
};
|
|
56
|
+
function enrichDoctorCheck(check) {
|
|
57
|
+
const group = check.status === "unavailable" ? "gated" : DOCTOR_GROUP_BY_NAME[check.name] ?? "setup";
|
|
58
|
+
const fix = check.status === "pass" || check.status === "unavailable" ? undefined : check.fix ?? DOCTOR_FIX_BY_NAME[check.name];
|
|
59
|
+
return fix ? { ...check, group, fix } : { ...check, group };
|
|
60
|
+
}
|
|
61
|
+
export class SetupUnsupportedPlatformError extends Error {
|
|
62
|
+
platform;
|
|
63
|
+
constructor(platform) {
|
|
64
|
+
super(`dg setup does not support ${platform} — Linux and macOS only`);
|
|
65
|
+
this.platform = platform;
|
|
66
|
+
this.name = "SetupUnsupportedPlatformError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function buildSetupPlan(options) {
|
|
70
|
+
if (process.platform === "win32") {
|
|
71
|
+
throw new SetupUnsupportedPlatformError(process.platform);
|
|
72
|
+
}
|
|
73
|
+
const env = options.env ?? process.env;
|
|
74
|
+
const paths = resolveDgPaths(env);
|
|
75
|
+
const shell = resolveShell(options.shell, env);
|
|
76
|
+
const shimDir = join(paths.homeDir, ".dg", "shims");
|
|
77
|
+
const rcPath = shellRcPath(paths.homeDir, shell);
|
|
78
|
+
return {
|
|
79
|
+
paths,
|
|
80
|
+
shell,
|
|
81
|
+
shimDir,
|
|
82
|
+
rcPath,
|
|
83
|
+
writes: [
|
|
84
|
+
{
|
|
85
|
+
kind: "directory",
|
|
86
|
+
path: shimDir,
|
|
87
|
+
action: "create dg-owned shim directory"
|
|
88
|
+
},
|
|
89
|
+
...SHIM_COMMANDS.map((command) => ({
|
|
90
|
+
kind: "shim",
|
|
91
|
+
path: join(shimDir, command),
|
|
92
|
+
action: `write ${command} shim that dispatches to dg ${command}`
|
|
93
|
+
})),
|
|
94
|
+
{
|
|
95
|
+
kind: "shell-rc",
|
|
96
|
+
path: rcPath,
|
|
97
|
+
action: `insert or replace ${RC_SENTINEL} PATH block`
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
kind: "state",
|
|
101
|
+
path: paths.cleanupRegistryPath,
|
|
102
|
+
action: "record dg-owned writes for uninstall"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
reloadInstructions: reloadInstructions(shell)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function renderSetupPlan(plan) {
|
|
109
|
+
const lines = [
|
|
110
|
+
"Dependency Guardian setup write plan",
|
|
111
|
+
"",
|
|
112
|
+
"No files are changed until this plan is confirmed.",
|
|
113
|
+
...plan.writes.map((write) => `- ${write.action}: ${write.path}`),
|
|
114
|
+
"",
|
|
115
|
+
"After setup, reload your shell:",
|
|
116
|
+
...plan.reloadInstructions.map((line) => `- ${line}`)
|
|
117
|
+
];
|
|
118
|
+
return `${lines.join("\n")}\n`;
|
|
119
|
+
}
|
|
120
|
+
export function applySetupPlan(plan, now = new Date()) {
|
|
121
|
+
const entries = [];
|
|
122
|
+
mkdirSync(plan.shimDir, {
|
|
123
|
+
recursive: true,
|
|
124
|
+
mode: 0o700
|
|
125
|
+
});
|
|
126
|
+
for (const command of SHIM_COMMANDS) {
|
|
127
|
+
const path = join(plan.shimDir, command);
|
|
128
|
+
writeFileSync(path, shimSource(command), {
|
|
129
|
+
encoding: "utf8",
|
|
130
|
+
mode: 0o755
|
|
131
|
+
});
|
|
132
|
+
chmodSync(path, 0o755);
|
|
133
|
+
entries.push(cleanupEntry("shim", path, "mode1", now, SHIM_SENTINEL));
|
|
134
|
+
}
|
|
135
|
+
mkdirSync(dirname(plan.rcPath), {
|
|
136
|
+
recursive: true,
|
|
137
|
+
mode: 0o700
|
|
138
|
+
});
|
|
139
|
+
writeFileSync(plan.rcPath, withRcBlock(readText(plan.rcPath), plan), "utf8");
|
|
140
|
+
entries.push(cleanupEntry("rc", plan.rcPath, "mode1", now, RC_SENTINEL));
|
|
141
|
+
const registry = withRegistryLock(plan.paths, () => {
|
|
142
|
+
const merged = mergeRegistry(readRegistry(plan.paths).registry, entries);
|
|
143
|
+
writeRegistry(plan.paths, merged);
|
|
144
|
+
return merged;
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
plan,
|
|
148
|
+
registry
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function applySetupPlanWithLock(plan, now = new Date()) {
|
|
152
|
+
const lock = acquireLockSync(plan.paths, SETUP_UNINSTALL_LOCK, {
|
|
153
|
+
staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
return applySetupPlan(plan, now);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
lock.release();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function uninstallSetup(options) {
|
|
163
|
+
const paths = resolveDgPaths(options.env ?? process.env);
|
|
164
|
+
const hadCacheDirBeforeLock = existsSync(paths.cacheDir);
|
|
165
|
+
const hadStateDirBeforeLock = existsSync(paths.stateDir);
|
|
166
|
+
const lock = acquireLockSync(paths, SETUP_UNINSTALL_LOCK, {
|
|
167
|
+
staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
const registryRead = readRegistry(paths);
|
|
171
|
+
const removed = [];
|
|
172
|
+
const missing = [];
|
|
173
|
+
const warnings = [];
|
|
174
|
+
const staleSessions = sweepStaleSessionsSync(paths, {
|
|
175
|
+
olderThanMs: STALE_SESSION_OLDER_THAN_MS
|
|
176
|
+
}).removed;
|
|
177
|
+
if (registryRead.malformed) {
|
|
178
|
+
warnings.push(`cleanup registry is malformed: ${paths.cleanupRegistryPath}`);
|
|
179
|
+
}
|
|
180
|
+
for (const entry of registryRead.registry.entries) {
|
|
181
|
+
if (entry.owner !== "dg") {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (entry.kind === "shim") {
|
|
185
|
+
removeShim(entry, removed, missing, warnings);
|
|
186
|
+
}
|
|
187
|
+
else if (entry.kind === "rc") {
|
|
188
|
+
removeRcBlock(entry, removed, missing, warnings);
|
|
189
|
+
}
|
|
190
|
+
else if (entry.kind === "git-hook") {
|
|
191
|
+
reverseGitHookEntry(entry, removed, missing, warnings);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!options.all && !registryRead.malformed && options.keepConfig) {
|
|
195
|
+
writeRegistryWithLock(paths, {
|
|
196
|
+
version: 1,
|
|
197
|
+
entries: registryRead.registry.entries.filter((entry) => entry.owner !== "dg")
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (!options.keepConfig && hadCacheDirBeforeLock) {
|
|
201
|
+
removeDirectory(paths.cacheDir, removed, missing);
|
|
202
|
+
}
|
|
203
|
+
if (!options.keepConfig && hadStateDirBeforeLock) {
|
|
204
|
+
removeDirectory(paths.stateDir, removed, missing);
|
|
205
|
+
}
|
|
206
|
+
if (options.all) {
|
|
207
|
+
removeDirectory(paths.configDir, removed, missing);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
removed,
|
|
211
|
+
missing,
|
|
212
|
+
warnings,
|
|
213
|
+
staleSessions
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
lock.release();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export function doctorReport(options = {}) {
|
|
221
|
+
const env = options.env ?? process.env;
|
|
222
|
+
const paths = resolveDgPaths(env);
|
|
223
|
+
const shimDir = join(paths.homeDir, ".dg", "shims");
|
|
224
|
+
const registryRead = readRegistry(paths);
|
|
225
|
+
const staleSessions = findStaleSessionsSync(paths, {
|
|
226
|
+
olderThanMs: STALE_SESSION_OLDER_THAN_MS
|
|
227
|
+
}).stale;
|
|
228
|
+
const checks = [];
|
|
229
|
+
checks.push({
|
|
230
|
+
name: "node",
|
|
231
|
+
status: isSupportedNode(currentNodeVersion()) ? "pass" : "fail",
|
|
232
|
+
message: `Node ${currentNodeVersion()} with required >=22.14.0`
|
|
233
|
+
});
|
|
234
|
+
checks.push({
|
|
235
|
+
name: "package",
|
|
236
|
+
status: "pass",
|
|
237
|
+
message: `dg ${dgVersion()}`
|
|
238
|
+
});
|
|
239
|
+
checks.push(dgPathCheck(env));
|
|
240
|
+
checks.push({
|
|
241
|
+
name: "cleanup-registry",
|
|
242
|
+
status: registryRead.malformed ? "fail" : "pass",
|
|
243
|
+
message: registryRead.malformed ? `Malformed cleanup registry at ${paths.cleanupRegistryPath}` : paths.cleanupRegistryPath
|
|
244
|
+
});
|
|
245
|
+
checks.push({
|
|
246
|
+
name: "cleanup-registry-stale-entries",
|
|
247
|
+
status: staleRegistryEntries(registryRead.registry).length === 0 ? "pass" : "warn",
|
|
248
|
+
message: staleRegistryEntries(registryRead.registry).length === 0
|
|
249
|
+
? "No stale cleanup registry entries detected"
|
|
250
|
+
: `Missing registered paths: ${staleRegistryEntries(registryRead.registry).join(", ")}`
|
|
251
|
+
});
|
|
252
|
+
checks.push(configCheck(paths, env));
|
|
253
|
+
checks.push(authCheck(env));
|
|
254
|
+
checks.push(policyCheck(env));
|
|
255
|
+
checks.push(telemetryCheck(env));
|
|
256
|
+
const missingShims = SHIM_COMMANDS.filter((command) => !validShim(join(shimDir, command), command));
|
|
257
|
+
checks.push({
|
|
258
|
+
name: "shims",
|
|
259
|
+
status: missingShims.length === 0 ? "pass" : "warn",
|
|
260
|
+
message: missingShims.length === 0 ? `All setup shims exist in ${shimDir}` : `Missing or drifted shims: ${missingShims.join(", ")}`
|
|
261
|
+
});
|
|
262
|
+
const rcEntries = registryRead.registry.entries.filter((entry) => entry.owner === "dg" && entry.kind === "rc");
|
|
263
|
+
const missingRc = rcEntries.filter((entry) => !readText(entry.path).includes(RC_SENTINEL));
|
|
264
|
+
checks.push({
|
|
265
|
+
name: "shell-rc",
|
|
266
|
+
status: rcEntries.length > 0 && missingRc.length === 0 ? "pass" : "warn",
|
|
267
|
+
message: rcEntries.length === 0 ? "No dg shell rc block is registered" : `Registered shell rc blocks: ${rcEntries.length}`
|
|
268
|
+
});
|
|
269
|
+
checks.push(pathPrecedenceCheck(env, shimDir));
|
|
270
|
+
checks.push({
|
|
271
|
+
name: "stale-sessions",
|
|
272
|
+
status: staleSessions.length === 0 ? "pass" : "warn",
|
|
273
|
+
message: staleSessions.length === 0 ? "No stale sessions detected" : `Stale sessions detected: ${staleSessions.join(", ")}`
|
|
274
|
+
});
|
|
275
|
+
checks.push(realBinaryResolutionCheck(env));
|
|
276
|
+
checks.push({
|
|
277
|
+
name: "recursive-shim-guard",
|
|
278
|
+
status: "pass",
|
|
279
|
+
message: `Real binary resolver skips ${shimDir} and dg shim sentinel files`
|
|
280
|
+
});
|
|
281
|
+
checks.push({
|
|
282
|
+
name: "package-manager-discovery",
|
|
283
|
+
status: "pass",
|
|
284
|
+
message: `Classifiers are registered for ${SHIM_COMMANDS.join(", ")}; gated managers remain ${packageManagerNames()
|
|
285
|
+
.filter((name) => !SHIM_COMMANDS.includes(name))
|
|
286
|
+
.join(", ")}`
|
|
287
|
+
});
|
|
288
|
+
checks.push(...unavailableDoctorChecks());
|
|
289
|
+
checks.push(serviceCheck(env));
|
|
290
|
+
return {
|
|
291
|
+
version: dgVersion(),
|
|
292
|
+
checks: checks.map(enrichDoctorCheck)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const DOCTOR_STATUS_ROLE = {
|
|
296
|
+
pass: "pass",
|
|
297
|
+
warn: "warn",
|
|
298
|
+
fail: "block",
|
|
299
|
+
unavailable: "muted"
|
|
300
|
+
};
|
|
301
|
+
const DOCTOR_STATUS_GLYPH = {
|
|
302
|
+
pass: "✓",
|
|
303
|
+
warn: "⚠",
|
|
304
|
+
fail: "✘",
|
|
305
|
+
unavailable: "·"
|
|
306
|
+
};
|
|
307
|
+
const DOCTOR_GROUP_ORDER = [
|
|
308
|
+
{ key: "environment", title: "Environment" },
|
|
309
|
+
{ key: "setup", title: "Setup" },
|
|
310
|
+
{ key: "account", title: "Account" }
|
|
311
|
+
];
|
|
312
|
+
export function renderDoctorReport(report, theme = createTheme(false), verbose = false) {
|
|
313
|
+
const failures = report.checks.filter((check) => check.status === "fail").length;
|
|
314
|
+
const warnings = report.checks.filter((check) => check.status === "warn").length;
|
|
315
|
+
const role = failures > 0 ? "block" : warnings > 0 ? "warn" : "pass";
|
|
316
|
+
const glyph = failures > 0 ? "✘" : warnings > 0 ? "⚠" : "✓";
|
|
317
|
+
const summary = failures > 0
|
|
318
|
+
? `${failures} ${failures === 1 ? "issue needs" : "issues need"} attention`
|
|
319
|
+
: warnings > 0
|
|
320
|
+
? `${warnings} ${warnings === 1 ? "warning" : "warnings"}`
|
|
321
|
+
: "all good";
|
|
322
|
+
const lines = [`${theme.paint(role, `${glyph} DG doctor`)} — ${summary}`];
|
|
323
|
+
for (const { key, title } of DOCTOR_GROUP_ORDER) {
|
|
324
|
+
const groupChecks = report.checks.filter((check) => check.group === key);
|
|
325
|
+
if (groupChecks.length === 0) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (verbose) {
|
|
329
|
+
lines.push("", title);
|
|
330
|
+
const width = nameWidth(groupChecks);
|
|
331
|
+
for (const check of groupChecks) {
|
|
332
|
+
lines.push(doctorLine(check, width, theme));
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let nonPass = groupChecks.filter((check) => check.status !== "pass");
|
|
337
|
+
if (key === "setup") {
|
|
338
|
+
const trio = ["shims", "shell-rc", "path"];
|
|
339
|
+
const allWarn = trio.every((name) => nonPass.some((check) => check.name === name && check.status === "warn"));
|
|
340
|
+
if (allWarn) {
|
|
341
|
+
nonPass = nonPass.filter((check) => !trio.includes(check.name) && !(check.name === "config" && check.status === "warn"));
|
|
342
|
+
const notSetUp = "not set up — bare npm/pip installs aren't protected";
|
|
343
|
+
if (nonPass.length === 0) {
|
|
344
|
+
lines.push(rollupInlineLine(title, "warn", "⚠", notSetUp, "dg setup", theme));
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const worst = worstCheck(nonPass);
|
|
348
|
+
lines.push(` ${theme.paint(DOCTOR_STATUS_ROLE[worst.status], `${DOCTOR_STATUS_GLYPH[worst.status]} ${title}`)}`);
|
|
349
|
+
lines.push(` ${theme.paint("warn", "⚠")} ${notSetUp} ${theme.paint("muted", "→ dg setup")}`);
|
|
350
|
+
const width = nameWidth(nonPass);
|
|
351
|
+
for (const check of nonPass) {
|
|
352
|
+
lines.push(doctorLine(check, width, theme));
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (nonPass.length === 0) {
|
|
358
|
+
lines.push(` ${theme.paint("pass", `✓ ${title}`)}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const worst = worstCheck(nonPass);
|
|
362
|
+
if (nonPass.length === 1) {
|
|
363
|
+
lines.push(rollupInlineLine(title, worst.status, DOCTOR_STATUS_GLYPH[worst.status], worst.message, worst.fix, theme));
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
lines.push(` ${theme.paint(DOCTOR_STATUS_ROLE[worst.status], `${DOCTOR_STATUS_GLYPH[worst.status]} ${title}`)}`);
|
|
367
|
+
const width = nameWidth(nonPass);
|
|
368
|
+
for (const check of nonPass) {
|
|
369
|
+
lines.push(doctorLine(check, width, theme));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const gated = report.checks.filter((check) => check.group === "gated");
|
|
373
|
+
if (gated.length > 0) {
|
|
374
|
+
if (verbose) {
|
|
375
|
+
lines.push("", "Gated / remote");
|
|
376
|
+
const width = nameWidth(gated);
|
|
377
|
+
for (const check of gated) {
|
|
378
|
+
lines.push(doctorLine(check, width, theme));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
lines.push("", theme.paint("muted", `${gated.length} gated/remote checks hidden · dg doctor --verbose`));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return `${lines.join("\n")}\n`;
|
|
386
|
+
}
|
|
387
|
+
function nameWidth(checks) {
|
|
388
|
+
return checks.reduce((max, check) => Math.max(max, check.name.length), 0);
|
|
389
|
+
}
|
|
390
|
+
const DOCTOR_STATUS_SEVERITY = {
|
|
391
|
+
pass: 0,
|
|
392
|
+
unavailable: 1,
|
|
393
|
+
warn: 2,
|
|
394
|
+
fail: 3
|
|
395
|
+
};
|
|
396
|
+
function worstCheck(checks) {
|
|
397
|
+
return checks.reduce((worst, check) => DOCTOR_STATUS_SEVERITY[check.status] > DOCTOR_STATUS_SEVERITY[worst.status] ? check : worst);
|
|
398
|
+
}
|
|
399
|
+
function rollupInlineLine(title, status, glyph, message, fix, theme) {
|
|
400
|
+
const head = theme.paint(DOCTOR_STATUS_ROLE[status], `${glyph} ${title}`);
|
|
401
|
+
const fixSuffix = fix ? ` ${theme.paint("muted", `→ ${fix}`)}` : "";
|
|
402
|
+
return ` ${head} ${message}${fixSuffix}`;
|
|
403
|
+
}
|
|
404
|
+
function doctorLine(check, width, theme) {
|
|
405
|
+
const glyph = theme.paint(DOCTOR_STATUS_ROLE[check.status], DOCTOR_STATUS_GLYPH[check.status]);
|
|
406
|
+
const name = check.name.padEnd(width);
|
|
407
|
+
const message = check.status === "unavailable" ? theme.paint("muted", check.message) : check.message;
|
|
408
|
+
const fix = check.fix ? ` ${theme.paint("muted", `→ ${check.fix}`)}` : "";
|
|
409
|
+
return ` ${glyph} ${name} ${message}${fix}`;
|
|
410
|
+
}
|
|
411
|
+
function resolveShell(shell, env) {
|
|
412
|
+
if (shell !== "auto") {
|
|
413
|
+
return shell;
|
|
414
|
+
}
|
|
415
|
+
const detected = basename(env.SHELL ?? "");
|
|
416
|
+
if (detected === "fish") {
|
|
417
|
+
return "fish";
|
|
418
|
+
}
|
|
419
|
+
if (detected === "bash") {
|
|
420
|
+
return "bash";
|
|
421
|
+
}
|
|
422
|
+
return "zsh";
|
|
423
|
+
}
|
|
424
|
+
function shellRcPath(homeDir, shell) {
|
|
425
|
+
if (shell === "fish") {
|
|
426
|
+
return join(homeDir, ".config", "fish", "config.fish");
|
|
427
|
+
}
|
|
428
|
+
if (shell === "bash") {
|
|
429
|
+
return join(homeDir, ".bashrc");
|
|
430
|
+
}
|
|
431
|
+
return join(homeDir, ".zshrc");
|
|
432
|
+
}
|
|
433
|
+
export function activationCommand(shell, rcDisplay) {
|
|
434
|
+
if (shell === "fish") {
|
|
435
|
+
return `source ${rcDisplay}`;
|
|
436
|
+
}
|
|
437
|
+
if (shell === "bash") {
|
|
438
|
+
return `source ${rcDisplay} && hash -r`;
|
|
439
|
+
}
|
|
440
|
+
return `source ${rcDisplay} && rehash`;
|
|
441
|
+
}
|
|
442
|
+
function reloadInstructions(shell) {
|
|
443
|
+
if (shell === "fish") {
|
|
444
|
+
return ["start a new shell or run: source ~/.config/fish/config.fish"];
|
|
445
|
+
}
|
|
446
|
+
if (shell === "bash") {
|
|
447
|
+
return ["start a new shell or run: source ~/.bashrc", "clear cached command paths with: hash -r"];
|
|
448
|
+
}
|
|
449
|
+
return ["start a new shell or run: source ~/.zshrc", "clear cached command paths with: rehash"];
|
|
450
|
+
}
|
|
451
|
+
// Absolute path: a bare `exec dg` resolves through PATH, so anything that
|
|
452
|
+
// shadows dg (npm run prepending node_modules/.bin with a vendored copy)
|
|
453
|
+
// hijacks every shimmed package manager.
|
|
454
|
+
export function shimSource(command) {
|
|
455
|
+
return `#!/bin/sh\n# ${SHIM_SENTINEL}\nDG_SHIM_ACTIVE="\${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}${command}:$$" exec "${escapeDoubleQuotedSh(dgEntrypoint())}" ${command} "$@"\n`;
|
|
456
|
+
}
|
|
457
|
+
function escapeDoubleQuotedSh(value) {
|
|
458
|
+
return value.replace(/[\\"$`]/g, "\\$&");
|
|
459
|
+
}
|
|
460
|
+
function escapeDoubleQuotedFish(value) {
|
|
461
|
+
return value.replace(/[\\"$]/g, "\\$&");
|
|
462
|
+
}
|
|
463
|
+
function dgEntrypoint() {
|
|
464
|
+
const argv1 = process.argv[1];
|
|
465
|
+
return argv1 ? resolve(argv1) : "dg";
|
|
466
|
+
}
|
|
467
|
+
function withRcBlock(existing, plan) {
|
|
468
|
+
const withoutExisting = stripRcBlock(existing);
|
|
469
|
+
const block = plan.shell === "fish" ? fishRcBlock(plan.shimDir) : posixRcBlock(plan.shimDir);
|
|
470
|
+
const prefix = withoutExisting.length > 0 && !withoutExisting.endsWith("\n") ? `${withoutExisting}\n` : withoutExisting;
|
|
471
|
+
return `${prefix}${block}`;
|
|
472
|
+
}
|
|
473
|
+
function posixRcBlock(shimDir) {
|
|
474
|
+
return `${RC_BEGIN}\n# ${RC_SENTINEL}\nexport PATH="${escapeDoubleQuotedSh(shimDir)}:$PATH"\n${RC_END}\n`;
|
|
475
|
+
}
|
|
476
|
+
function fishRcBlock(shimDir) {
|
|
477
|
+
return `${RC_BEGIN}\n# ${RC_SENTINEL}\nfish_add_path -p "${escapeDoubleQuotedFish(shimDir)}"\n${RC_END}\n`;
|
|
478
|
+
}
|
|
479
|
+
function stripRcBlock(existing) {
|
|
480
|
+
const pattern = new RegExp(`${escapeRegex(RC_BEGIN)}\\n[\\s\\S]*?${escapeRegex(RC_END)}\\n?`, "g");
|
|
481
|
+
const unterminatedPattern = new RegExp(`${escapeRegex(RC_BEGIN)}\\n[\\s\\S]*$`, "g");
|
|
482
|
+
return existing.replace(pattern, "").replace(unterminatedPattern, "");
|
|
483
|
+
}
|
|
484
|
+
export function cleanupEntry(kind, path, mode, now, sentinel) {
|
|
485
|
+
return {
|
|
486
|
+
kind,
|
|
487
|
+
path,
|
|
488
|
+
mode,
|
|
489
|
+
sentinel,
|
|
490
|
+
installedAt: now.toISOString(),
|
|
491
|
+
owner: "dg"
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
export function mergeRegistry(registry, entries) {
|
|
495
|
+
const retained = registry.entries.filter((entry) => !entries.some((next) => entry.kind === next.kind && entry.path === next.path && entry.sentinel === next.sentinel));
|
|
496
|
+
return {
|
|
497
|
+
version: 1,
|
|
498
|
+
entries: [...retained, ...entries]
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
export function readRegistry(paths) {
|
|
502
|
+
try {
|
|
503
|
+
if (!existsSync(paths.cleanupRegistryPath)) {
|
|
504
|
+
return {
|
|
505
|
+
registry: {
|
|
506
|
+
version: 1,
|
|
507
|
+
entries: []
|
|
508
|
+
},
|
|
509
|
+
malformed: false
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const registry = JSON.parse(readFileSync(paths.cleanupRegistryPath, "utf8"));
|
|
513
|
+
if (registry.version !== 1 || !Array.isArray(registry.entries)) {
|
|
514
|
+
throw new Error("unsupported cleanup registry");
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
registry,
|
|
518
|
+
malformed: false
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return {
|
|
523
|
+
registry: {
|
|
524
|
+
version: 1,
|
|
525
|
+
entries: []
|
|
526
|
+
},
|
|
527
|
+
malformed: true
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function staleRegistryEntries(registry) {
|
|
532
|
+
return registry.entries.filter((entry) => entry.owner === "dg" && !existsSync(entry.path)).map((entry) => entry.path);
|
|
533
|
+
}
|
|
534
|
+
function configCheck(paths, env) {
|
|
535
|
+
if (!existsSync(paths.configDir)) {
|
|
536
|
+
return {
|
|
537
|
+
name: "config",
|
|
538
|
+
status: "warn",
|
|
539
|
+
message: `No dg config directory exists at ${paths.configDir}`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
accessSync(paths.configDir, constants.R_OK);
|
|
544
|
+
loadUserConfig(env);
|
|
545
|
+
return {
|
|
546
|
+
name: "config",
|
|
547
|
+
status: "pass",
|
|
548
|
+
message: `${paths.configDir} is readable and config is valid`
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
return {
|
|
553
|
+
name: "config",
|
|
554
|
+
status: "fail",
|
|
555
|
+
message: error instanceof ConfigError ? error.message : `${paths.configDir} is not readable`
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function unavailableDoctorChecks() {
|
|
560
|
+
const unavailable = [
|
|
561
|
+
["api", "API connectivity is not checked until this machine is authenticated. Run 'dg login', then run 'dg doctor' again."],
|
|
562
|
+
["path-cache", "Shell command path cache checks are guidance only. After setup, run 'hash -r' for bash or 'rehash' for zsh."],
|
|
563
|
+
["proxy", "Per-command proxy health is checked when a protected prefix command runs, for example 'dg npm install <package>'."],
|
|
564
|
+
["ca", "CA health is checked during protected HTTPS artifact fetches or explicit service startup."],
|
|
565
|
+
["upstream-proxy", "Corporate proxy chain health is checked during protected fetches when proxy environment variables are configured."],
|
|
566
|
+
...OPTIONAL_SUPPORT_GATES.map((gate) => [gate.id, gate.message]),
|
|
567
|
+
["dashboard", "Dashboard setup status is checked after authentication. Run 'dg login' and open the Dependency Guardian dashboard."],
|
|
568
|
+
["docs-api", "Docs/OpenAPI health is a remote service check. Run 'dg login', then use 'dg verify <target>' or a protected prefix command."]
|
|
569
|
+
];
|
|
570
|
+
return unavailable.map(([name, message]) => ({
|
|
571
|
+
name,
|
|
572
|
+
status: "unavailable",
|
|
573
|
+
message
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
function dgPathCheck(env) {
|
|
577
|
+
const current = currentDgBinaryPath(env);
|
|
578
|
+
const candidates = findDgExecutables(env);
|
|
579
|
+
const first = candidates[0] ?? null;
|
|
580
|
+
if (!first) {
|
|
581
|
+
return {
|
|
582
|
+
name: "dg-binary-path",
|
|
583
|
+
status: "warn",
|
|
584
|
+
message: current
|
|
585
|
+
? `The running dg binary is ${current}, but no dg executable is on PATH. Add ${dirname(current)} to PATH or invoke this path directly.`
|
|
586
|
+
: "No dg executable was found on PATH. Add the installed dg bin directory to PATH, then run 'dg doctor' again."
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (current && resolve(first) !== resolve(current)) {
|
|
590
|
+
return {
|
|
591
|
+
name: "dg-binary-path",
|
|
592
|
+
status: "warn",
|
|
593
|
+
message: `Another dg executable is earlier on PATH: ${first}. This command is running ${current}. Run 'which -a dg' and put ${dirname(current)} before older dg entries.`
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
name: "dg-binary-path",
|
|
598
|
+
status: "pass",
|
|
599
|
+
message: `dg on PATH resolves to ${first}`
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function currentDgBinaryPath(env) {
|
|
603
|
+
const explicit = env.DG_TEST_CURRENT_DG_PATH;
|
|
604
|
+
if (explicit) {
|
|
605
|
+
return explicit;
|
|
606
|
+
}
|
|
607
|
+
const invoked = process.argv[1];
|
|
608
|
+
if (!invoked) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const invokedName = basename(invoked);
|
|
612
|
+
return invokedName === "dg" || invokedName === "dg.js" ? invoked : null;
|
|
613
|
+
}
|
|
614
|
+
function findDgExecutables(env) {
|
|
615
|
+
const extensions = process.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
|
|
616
|
+
const matches = [];
|
|
617
|
+
for (const rawDir of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
618
|
+
for (const extension of extensions) {
|
|
619
|
+
const candidate = join(rawDir, `dg${extension}`);
|
|
620
|
+
try {
|
|
621
|
+
accessSync(candidate, constants.X_OK);
|
|
622
|
+
matches.push(candidate);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
// Keep scanning PATH entries.
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return matches;
|
|
631
|
+
}
|
|
632
|
+
function serviceCheck(env) {
|
|
633
|
+
const state = readServiceState(env).state;
|
|
634
|
+
if (!state.configured) {
|
|
635
|
+
return {
|
|
636
|
+
name: "service",
|
|
637
|
+
status: "pass",
|
|
638
|
+
message: "Off (optional Team feature for CI / private registries)"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (!state.running) {
|
|
642
|
+
return {
|
|
643
|
+
name: "service",
|
|
644
|
+
status: "warn",
|
|
645
|
+
message: `Service mode is configured but stopped; trust installed: ${state.trustInstalled ? "yes" : "no"}`
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (!state.proxy) {
|
|
649
|
+
return {
|
|
650
|
+
name: "service",
|
|
651
|
+
status: "warn",
|
|
652
|
+
message: state.lastError ?? "Service mode is running without a persistent proxy"
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
name: "service",
|
|
657
|
+
status: "pass",
|
|
658
|
+
message: `Service mode is running at ${state.proxy.proxyUrl}; trust installed: ${state.trustInstalled ? "yes" : "no"}`
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function pathPrecedenceCheck(env, shimDir) {
|
|
662
|
+
const pathEntries = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
663
|
+
const shimIndex = pathEntries.indexOf(shimDir);
|
|
664
|
+
const activateFix = `activate this shell: ${currentShellActivation(env)} — or open a new terminal`;
|
|
665
|
+
if (shimIndex === -1) {
|
|
666
|
+
return {
|
|
667
|
+
name: "path",
|
|
668
|
+
status: "warn",
|
|
669
|
+
message: `${shimDir} is not on PATH`,
|
|
670
|
+
fix: activateFix
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
let offender = null;
|
|
674
|
+
let resolvedAny = false;
|
|
675
|
+
for (const command of SHIM_COMMANDS) {
|
|
676
|
+
const real = resolveRealBinary({ name: command, env }).path;
|
|
677
|
+
if (!real) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
resolvedAny = true;
|
|
681
|
+
const dir = dirname(real);
|
|
682
|
+
const dirIndex = pathEntries.indexOf(dir);
|
|
683
|
+
if (dirIndex !== -1 && dirIndex < shimIndex && !offender) {
|
|
684
|
+
offender = { dir, command };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!resolvedAny) {
|
|
688
|
+
return {
|
|
689
|
+
name: "path",
|
|
690
|
+
status: "pass",
|
|
691
|
+
message: `${shimDir} is on PATH; no shimmed package managers found to shadow`
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (offender) {
|
|
695
|
+
return {
|
|
696
|
+
name: "path",
|
|
697
|
+
status: "warn",
|
|
698
|
+
message: `${shimDir} is on PATH but ${offender.dir} resolves ${offender.command} first`,
|
|
699
|
+
fix: activateFix
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
name: "path",
|
|
704
|
+
status: "pass",
|
|
705
|
+
message: `${shimDir} precedes the real package-manager directories on PATH`
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function currentShellActivation(env) {
|
|
709
|
+
const shell = resolveShell("auto", env);
|
|
710
|
+
const homeDir = resolveDgPaths(env).homeDir;
|
|
711
|
+
const rcPath = shellRcPath(homeDir, shell);
|
|
712
|
+
const rcDisplay = rcPath.startsWith(homeDir) ? `~${rcPath.slice(homeDir.length)}` : rcPath;
|
|
713
|
+
return activationCommand(shell, rcDisplay);
|
|
714
|
+
}
|
|
715
|
+
function realBinaryResolutionCheck(env) {
|
|
716
|
+
const installed = SHIM_COMMANDS.filter((command) => resolveRealBinary({ name: command, env }).path);
|
|
717
|
+
const absent = SHIM_COMMANDS.filter((command) => !resolveRealBinary({ name: command, env }).path);
|
|
718
|
+
return {
|
|
719
|
+
name: "real-binary-resolution",
|
|
720
|
+
status: "pass",
|
|
721
|
+
message: absent.length === 0
|
|
722
|
+
? `Protecting ${installed.join(", ")}`
|
|
723
|
+
: `Protecting ${installed.join(", ")} (${absent.join(", ")} not installed — nothing to protect there)`
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function authCheck(env) {
|
|
727
|
+
try {
|
|
728
|
+
const status = authStatus(env);
|
|
729
|
+
const connected = status.email && status.tier
|
|
730
|
+
? `${status.email} · ${displayTier(status.tier)} plan`
|
|
731
|
+
: `Authenticated from ${status.source} token ${status.tokenPreview}`;
|
|
732
|
+
return {
|
|
733
|
+
name: "auth",
|
|
734
|
+
status: status.authenticated ? "pass" : "warn",
|
|
735
|
+
message: status.authenticated ? connected : "not signed in"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
return {
|
|
740
|
+
name: "auth",
|
|
741
|
+
status: "fail",
|
|
742
|
+
message: error instanceof AuthError ? error.message : "Unable to read auth state"
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function policyCheck(env) {
|
|
747
|
+
try {
|
|
748
|
+
const config = loadUserConfig(env);
|
|
749
|
+
return {
|
|
750
|
+
name: "policy",
|
|
751
|
+
status: "pass",
|
|
752
|
+
message: `Local policy mode ${config.policy.mode}; project allowlists trusted: ${config.policy.trustProjectAllowlists}`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
return {
|
|
757
|
+
name: "policy",
|
|
758
|
+
status: "fail",
|
|
759
|
+
message: error instanceof ConfigError ? error.message : "Unable to read policy config"
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function telemetryCheck(env) {
|
|
764
|
+
try {
|
|
765
|
+
const config = loadUserConfig(env);
|
|
766
|
+
return {
|
|
767
|
+
name: "telemetry",
|
|
768
|
+
status: "pass",
|
|
769
|
+
message: config.telemetry.enabled ? "Telemetry is enabled with minimized event fields" : "Telemetry is disabled"
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
return {
|
|
774
|
+
name: "telemetry",
|
|
775
|
+
status: "fail",
|
|
776
|
+
message: error instanceof ConfigError ? error.message : "Unable to read telemetry config"
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
export function writeRegistry(paths, registry) {
|
|
781
|
+
mkdirSync(dirname(paths.cleanupRegistryPath), {
|
|
782
|
+
recursive: true,
|
|
783
|
+
mode: 0o700
|
|
784
|
+
});
|
|
785
|
+
const tempPath = `${paths.cleanupRegistryPath}.${process.pid}.tmp`;
|
|
786
|
+
writeFileSync(tempPath, `${JSON.stringify(registry, null, 2)}\n`, {
|
|
787
|
+
encoding: "utf8",
|
|
788
|
+
mode: 0o600
|
|
789
|
+
});
|
|
790
|
+
renameSync(tempPath, paths.cleanupRegistryPath);
|
|
791
|
+
}
|
|
792
|
+
function withRegistryLock(paths, action) {
|
|
793
|
+
const lock = acquireLockSync(paths, CLEANUP_REGISTRY_LOCK, {
|
|
794
|
+
staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
|
|
795
|
+
});
|
|
796
|
+
try {
|
|
797
|
+
return action();
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
lock.release();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function writeRegistryWithLock(paths, registry) {
|
|
804
|
+
withRegistryLock(paths, () => writeRegistry(paths, registry));
|
|
805
|
+
}
|
|
806
|
+
function removeShim(entry, removed, missing, warnings) {
|
|
807
|
+
if (!existsSync(entry.path)) {
|
|
808
|
+
missing.push(entry.path);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (!validShim(entry.path, basename(entry.path))) {
|
|
812
|
+
warnings.push(`refused to remove drifted shim: ${entry.path}`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
rmSync(entry.path, {
|
|
816
|
+
force: true
|
|
817
|
+
});
|
|
818
|
+
removed.push(entry.path);
|
|
819
|
+
}
|
|
820
|
+
function removeRcBlock(entry, removed, missing, warnings) {
|
|
821
|
+
const existing = readText(entry.path);
|
|
822
|
+
if (!existing) {
|
|
823
|
+
missing.push(entry.path);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (!existing.includes(RC_SENTINEL)) {
|
|
827
|
+
warnings.push(`refused to edit shell rc without dg sentinel: ${entry.path}`);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
writeFileSync(entry.path, stripRcBlock(existing), "utf8");
|
|
831
|
+
removed.push(entry.path);
|
|
832
|
+
}
|
|
833
|
+
export function reverseGitHookEntry(entry, removed, missing, warnings) {
|
|
834
|
+
const sentinel = entry.sentinel ?? GUARD_HOOK_SENTINEL;
|
|
835
|
+
let ownsTarget = false;
|
|
836
|
+
if (!existsSync(entry.path)) {
|
|
837
|
+
missing.push(entry.path);
|
|
838
|
+
ownsTarget = true;
|
|
839
|
+
}
|
|
840
|
+
else if (readText(entry.path).split("\n", 2)[1]?.includes(sentinel)) {
|
|
841
|
+
rmSync(entry.path, { force: true });
|
|
842
|
+
removed.push(entry.path);
|
|
843
|
+
ownsTarget = true;
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
warnings.push(`refused to remove git hook without dg sentinel: ${entry.path}`);
|
|
847
|
+
}
|
|
848
|
+
if (!ownsTarget) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (entry.original) {
|
|
852
|
+
if (existsSync(entry.original)) {
|
|
853
|
+
try {
|
|
854
|
+
renameSync(entry.original, entry.path);
|
|
855
|
+
removed.push(entry.original);
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
warnings.push(`could not restore chained hook ${entry.original}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
missing.push(entry.original);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function removeDirectory(path, removed, missing) {
|
|
867
|
+
if (!existsSync(path)) {
|
|
868
|
+
missing.push(path);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
rmSync(path, {
|
|
872
|
+
force: true,
|
|
873
|
+
recursive: true
|
|
874
|
+
});
|
|
875
|
+
removed.push(path);
|
|
876
|
+
}
|
|
877
|
+
function validShim(path, command) {
|
|
878
|
+
if (!existsSync(path)) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
return isValidShimSource(readText(path), command);
|
|
882
|
+
}
|
|
883
|
+
export function isValidShimSource(content, command) {
|
|
884
|
+
if (!content.includes(SHIM_SENTINEL)) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
return content.includes("DG_SHIM_ACTIVE=") && content.includes('exec "') && content.includes(`" ${command} "$@"`);
|
|
888
|
+
}
|
|
889
|
+
function readText(path) {
|
|
890
|
+
try {
|
|
891
|
+
return readFileSync(path, "utf8");
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return "";
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function escapeRegex(value) {
|
|
898
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
899
|
+
}
|