@westbayberry/dg 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -226
- package/dist/agents/claude-code.js +123 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +118 -0
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +351 -0
- package/dist/agents/registry.js +128 -0
- package/dist/agents/routing.js +118 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit/rules.js +6 -2
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +17 -5
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +275 -0
- package/dist/commands/audit.js +28 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +196 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +49 -48
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +5 -4
- package/dist/install-ui/block-render.js +18 -14
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +896 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +39 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +44 -7
- package/dist/launcher/env.js +105 -26
- package/dist/launcher/install-preflight.js +64 -18
- package/dist/launcher/live-install.js +29 -8
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +10 -4
- package/dist/launcher/preflight-prompt.js +18 -4
- package/dist/launcher/run.js +178 -103
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +146 -6
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +55 -16
- package/dist/proxy/metadata-map.js +66 -12
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +568 -53
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +14 -12
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +132 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +33 -9
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +19 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/security/sanitize.js +8 -4
- package/dist/service/state.js +28 -9
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/trust-store.js +5 -9
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +227 -49
- package/dist/setup/uninstall-standalone.js +25 -0
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +233 -0
- package/dist/standalone/uninstall.mjs +2123 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/report-writer.js +57 -0
- package/dist/verify/local.js +242 -44
- package/dist/verify/package-check.js +87 -29
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +7 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
package/dist/setup/git-hook.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { acquireLockSync, CLEANUP_REGISTRY_LOCK, resolveDgPaths } from "../state/index.js";
|
|
6
|
+
import { resolveToolPath } from "../util/external-tool.js";
|
|
6
7
|
import { gitTrimmed } from "../util/git.js";
|
|
7
8
|
import { GUARD_HOOK_SENTINEL, SETUP_UNINSTALL_LOCK, SETUP_UNINSTALL_LOCK_STALE_MS, chainedHookOriginal, guardHookScript, mergeRegistry, readRegistry, reverseGitHookEntry, writeRegistry } from "./plan.js";
|
|
8
9
|
export { GUARD_HOOK_SENTINEL } from "./plan.js";
|
|
@@ -84,7 +85,40 @@ function registryOriginal(context) {
|
|
|
84
85
|
const entry = readRegistry(context.paths).registry.entries.find((candidate) => candidate.kind === "git-hook" && candidate.owner === "dg" && candidate.path === context.hookTarget);
|
|
85
86
|
return entry?.original ?? null;
|
|
86
87
|
}
|
|
88
|
+
function isSymlinkPath(path) {
|
|
89
|
+
try {
|
|
90
|
+
return lstatSync(path).isSymbolicLink();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function writeHookAtomic(target, content) {
|
|
97
|
+
const tmp = join(dirname(target), `.pre-commit.dg-${randomBytes(6).toString("hex")}.tmp`);
|
|
98
|
+
writeFileSync(tmp, content, { encoding: "utf8", mode: 0o755, flag: "wx" });
|
|
99
|
+
try {
|
|
100
|
+
renameSync(tmp, target);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
rmSync(tmp, { force: true });
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
87
107
|
export function applyGitHook(context, now = new Date()) {
|
|
108
|
+
if (gitHookState(context) !== "foreign" && isSymlinkPath(context.hookTarget)) {
|
|
109
|
+
return {
|
|
110
|
+
hookTarget: context.hookTarget,
|
|
111
|
+
chainedOriginal: null,
|
|
112
|
+
checks: [
|
|
113
|
+
{
|
|
114
|
+
name: "hook-writable",
|
|
115
|
+
ok: false,
|
|
116
|
+
detail: `${context.hookTarget} is a symlink; dg refuses to replace it — remove the link or point it at a real file, then re-run dg guard-commit`
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
active: false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
88
122
|
const lock = acquireLockSync(context.paths, SETUP_UNINSTALL_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
|
|
89
123
|
let chainedOriginal = null;
|
|
90
124
|
try {
|
|
@@ -98,7 +132,7 @@ export function applyGitHook(context, now = new Date()) {
|
|
|
98
132
|
else if (state === "managed") {
|
|
99
133
|
chainedOriginal = chainedHookOriginal(hookText(context.hookTarget)) ?? registryOriginal(context);
|
|
100
134
|
}
|
|
101
|
-
|
|
135
|
+
writeHookAtomic(context.hookTarget, guardHookScript(context.dgPath, chainedOriginal));
|
|
102
136
|
chmodSync(context.hookTarget, 0o755);
|
|
103
137
|
const entry = {
|
|
104
138
|
kind: "git-hook",
|
|
@@ -176,7 +210,11 @@ function runSelfTest(context) {
|
|
|
176
210
|
if (!existsSync(context.hookTarget) || !isManaged(context.hookTarget)) {
|
|
177
211
|
return { ok: false, detail: "no dg hook to exercise" };
|
|
178
212
|
}
|
|
179
|
-
const
|
|
213
|
+
const sh = resolveToolPath("sh", context.env);
|
|
214
|
+
if (!sh) {
|
|
215
|
+
return { ok: false, detail: "sh not found on PATH; hook not exercised" };
|
|
216
|
+
}
|
|
217
|
+
const result = spawnSync(sh, [context.hookTarget], {
|
|
180
218
|
cwd: context.root,
|
|
181
219
|
env: { ...context.env, [GUARD_SELFTEST_ENV]: "1" },
|
|
182
220
|
encoding: "utf8",
|
|
@@ -219,6 +257,13 @@ export function gitHookStatusState(options = {}) {
|
|
|
219
257
|
const installedHere = registry.entries.some((entry) => entry.kind === "git-hook" && entry.owner === "dg" && isUnderRoot(entry.path, context.root));
|
|
220
258
|
return installedHere ? "dead" : "off";
|
|
221
259
|
}
|
|
260
|
+
export function commitGuardOffer(options = {}) {
|
|
261
|
+
const resolved = resolveGitRepo(options);
|
|
262
|
+
if ("error" in resolved) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return gitHookStatusState(options) === "off" ? resolved : null;
|
|
266
|
+
}
|
|
222
267
|
export function removeGitHookForRepo(context) {
|
|
223
268
|
const lock = acquireLockSync(context.paths, SETUP_UNINSTALL_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
|
|
224
269
|
const removed = [];
|
package/dist/setup/plan.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
3
3
|
import { chmodSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
4
7
|
import { createTheme } from "../presentation/theme.js";
|
|
8
|
+
import { AGENTS, AGENT_IDS, agentLabel, resolveAgentHookContext, reverseAgentHookEntry } from "../agents/registry.js";
|
|
9
|
+
import { GATE_ENABLE_HINT, readNetworkGatePosture } from "../agents/gate-posture.js";
|
|
5
10
|
import { acquireLockSync, findStaleSessionsSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
6
11
|
import { currentNodeVersion, isSupportedNode } from "../runtime/node-version.js";
|
|
7
12
|
import { dgVersion } from "../commands/version.js";
|
|
8
13
|
import { compareVersions, readLatestVersion } from "../commands/update.js";
|
|
9
14
|
import { AuthError, authStatus, displayTier, readAuthState } from "../auth/store.js";
|
|
10
|
-
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
15
|
+
import { ConfigError, DEFAULT_CONFIG, loadUserConfig } from "../config/settings.js";
|
|
11
16
|
import { describeCooldownSettings } from "../policy/cooldown.js";
|
|
12
17
|
import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
|
|
13
18
|
import { packageManagerNames } from "../launcher/classify.js";
|
|
14
19
|
import { readServiceState } from "../service/state.js";
|
|
15
20
|
import { OPTIONAL_SUPPORT_GATES } from "./optional-support.js";
|
|
16
|
-
export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pipx", "uv", "uvx", "cargo"]);
|
|
21
|
+
export const SHIM_COMMANDS = Object.freeze(["npm", "npx", "pnpm", "pnpx", "yarn", "pip", "pip3", "pipx", "uv", "uvx", "cargo"]);
|
|
17
22
|
export const SHIM_SENTINEL = "dg-shim-v1";
|
|
18
23
|
export const RC_SENTINEL = "dg-shell-rc-v1";
|
|
19
24
|
export const RC_FUNCTIONS_SENTINEL = "dg-shim-functions-v1";
|
|
@@ -28,9 +33,18 @@ const LEGACY_RC_CANDIDATES = [".zshrc", ".bashrc", ".bash_profile", ".profile",
|
|
|
28
33
|
const LEGACY_PYTHON_HOOK_PY = "dg_pip_hook.py";
|
|
29
34
|
const LEGACY_PYTHON_HOOK_PTH = "dg_pip_hook.pth";
|
|
30
35
|
const LEGACY_PYTHON_HOOK_MARKER = "Dependency Guardian pip-install interceptor";
|
|
36
|
+
const LEGACY_PYTHON_HOOK_PTH_MARKER = "import dg_pip_hook";
|
|
31
37
|
export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
|
|
32
38
|
export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
33
39
|
export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
|
|
40
|
+
function readShimFailClosed(env) {
|
|
41
|
+
try {
|
|
42
|
+
return loadUserConfig(env).policy.shimFailClosed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
const DOCTOR_GROUP_BY_NAME = {
|
|
35
49
|
node: "environment",
|
|
36
50
|
package: "environment",
|
|
@@ -44,7 +58,6 @@ const DOCTOR_GROUP_BY_NAME = {
|
|
|
44
58
|
config: "setup",
|
|
45
59
|
policy: "setup",
|
|
46
60
|
"script-gate": "setup",
|
|
47
|
-
telemetry: "setup",
|
|
48
61
|
shims: "setup",
|
|
49
62
|
"shell-rc": "setup",
|
|
50
63
|
"python-hook-drift": "setup",
|
|
@@ -53,6 +66,7 @@ const DOCTOR_GROUP_BY_NAME = {
|
|
|
53
66
|
"commit-guard": "setup",
|
|
54
67
|
"stale-sessions": "setup",
|
|
55
68
|
service: "setup",
|
|
69
|
+
"agent-gate": "setup",
|
|
56
70
|
auth: "account",
|
|
57
71
|
api: "account"
|
|
58
72
|
};
|
|
@@ -64,7 +78,6 @@ const DOCTOR_FIX_BY_NAME = {
|
|
|
64
78
|
"cleanup-registry-stale-entries": "re-run dg setup to refresh",
|
|
65
79
|
config: "fix or remove ~/.dg, then re-run dg setup",
|
|
66
80
|
policy: "fix ~/.dg config",
|
|
67
|
-
telemetry: "fix ~/.dg config",
|
|
68
81
|
shims: "dg setup",
|
|
69
82
|
"shell-rc": "dg setup",
|
|
70
83
|
"python-hook-drift": "dg uninstall, or re-run dg setup, to remove the stale pip hook",
|
|
@@ -100,6 +113,7 @@ export function buildSetupPlan(options) {
|
|
|
100
113
|
shell,
|
|
101
114
|
shimDir,
|
|
102
115
|
rcPath,
|
|
116
|
+
failClosed: readShimFailClosed(env),
|
|
103
117
|
writes: [
|
|
104
118
|
{
|
|
105
119
|
kind: "directory",
|
|
@@ -145,18 +159,19 @@ export function applySetupPlan(plan, now = new Date()) {
|
|
|
145
159
|
});
|
|
146
160
|
for (const command of SHIM_COMMANDS) {
|
|
147
161
|
const path = join(plan.shimDir, command);
|
|
148
|
-
writeFileSync(path, shimSource(command), {
|
|
162
|
+
writeFileSync(path, shimSource(command, { failClosed: plan.failClosed }), {
|
|
149
163
|
encoding: "utf8",
|
|
150
164
|
mode: 0o755
|
|
151
165
|
});
|
|
152
166
|
chmodSync(path, 0o755);
|
|
153
167
|
entries.push(cleanupEntry("shim", path, "mode1", now, SHIM_SENTINEL));
|
|
154
168
|
}
|
|
169
|
+
installStandaloneUninstaller(plan.paths.homeDir);
|
|
155
170
|
mkdirSync(dirname(plan.rcPath), {
|
|
156
171
|
recursive: true,
|
|
157
172
|
mode: 0o700
|
|
158
173
|
});
|
|
159
|
-
|
|
174
|
+
writeRcFileAtomic(plan.rcPath, withRcBlock(readText(plan.rcPath), plan));
|
|
160
175
|
entries.push(cleanupEntry("rc", plan.rcPath, "mode1", now, RC_SENTINEL));
|
|
161
176
|
sweepLegacyPythonHooks(plan.paths.homeDir, [], []);
|
|
162
177
|
const registry = withRegistryLock(plan.paths, () => {
|
|
@@ -180,6 +195,45 @@ export function applySetupPlanWithLock(plan, now = new Date()) {
|
|
|
180
195
|
lock.release();
|
|
181
196
|
}
|
|
182
197
|
}
|
|
198
|
+
// Shims written by a dg that predates the self-cleaning template carry no
|
|
199
|
+
// auto-cleanup, so a plain `npm uninstall -g` leaves them stranded forever.
|
|
200
|
+
// Rewriting the installed shims and reinstalling the standalone uninstaller on
|
|
201
|
+
// the first run after a version bump propagates the current template to setups
|
|
202
|
+
// created by older releases.
|
|
203
|
+
export function refreshSetupOnUpgrade(env = process.env) {
|
|
204
|
+
const paths = resolveDgPaths(env);
|
|
205
|
+
const shimDir = join(paths.homeDir, ".dg", "shims");
|
|
206
|
+
if (!isShimFile(join(shimDir, "npm"))) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const failClosed = readShimFailClosed(env);
|
|
210
|
+
try {
|
|
211
|
+
const lock = acquireLockSync(paths, SETUP_UNINSTALL_LOCK, {
|
|
212
|
+
staleMs: SETUP_UNINSTALL_LOCK_STALE_MS
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
for (const command of SHIM_COMMANDS) {
|
|
216
|
+
const shimPath = join(shimDir, command);
|
|
217
|
+
if (!isShimFile(shimPath)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
writeFileSync(shimPath, shimSource(command, { failClosed }), {
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
mode: 0o755
|
|
223
|
+
});
|
|
224
|
+
chmodSync(shimPath, 0o755);
|
|
225
|
+
}
|
|
226
|
+
installStandaloneUninstaller(paths.homeDir);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
lock.release();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
183
237
|
export function uninstallSetup(options) {
|
|
184
238
|
const paths = resolveDgPaths(options.env ?? process.env);
|
|
185
239
|
const hadCacheDirBeforeLock = existsSync(paths.cacheDir);
|
|
@@ -213,6 +267,9 @@ export function uninstallSetup(options) {
|
|
|
213
267
|
else if (entry.kind === "git-hook") {
|
|
214
268
|
reverseGitHookEntry(entry, removed, missing, warnings);
|
|
215
269
|
}
|
|
270
|
+
else if (entry.kind === "agent-hook") {
|
|
271
|
+
reverseAgentHookEntry(entry, removed, missing, warnings);
|
|
272
|
+
}
|
|
216
273
|
}
|
|
217
274
|
sweepLegacyRcBlocks(paths.homeDir, removed, warnings);
|
|
218
275
|
sweepLegacyPythonHooks(paths.homeDir, removed, warnings);
|
|
@@ -278,7 +335,6 @@ export function doctorReport(options = {}) {
|
|
|
278
335
|
checks.push(authCheck(env));
|
|
279
336
|
checks.push(policyCheck(env));
|
|
280
337
|
checks.push(scriptGateCheck(env));
|
|
281
|
-
checks.push(telemetryCheck(env));
|
|
282
338
|
const missingShims = SHIM_COMMANDS.filter((command) => !validShim(join(shimDir, command), command));
|
|
283
339
|
checks.push({
|
|
284
340
|
name: "shims",
|
|
@@ -324,6 +380,7 @@ export function doctorReport(options = {}) {
|
|
|
324
380
|
});
|
|
325
381
|
checks.push(...unavailableDoctorChecks());
|
|
326
382
|
checks.push(serviceCheck(env));
|
|
383
|
+
checks.push(agentGateCheck(env, paths.homeDir));
|
|
327
384
|
return {
|
|
328
385
|
version: dgVersion(),
|
|
329
386
|
checks: checks.map(enrichDoctorCheck)
|
|
@@ -568,6 +625,9 @@ function shellRcPath(homeDir, shell) {
|
|
|
568
625
|
}
|
|
569
626
|
return join(homeDir, ".zshrc");
|
|
570
627
|
}
|
|
628
|
+
export function tildifyPath(path, home = homedir()) {
|
|
629
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
630
|
+
}
|
|
571
631
|
export function activationCommand(shell, rcDisplay) {
|
|
572
632
|
if (shell === "fish") {
|
|
573
633
|
return `source ${rcDisplay}`;
|
|
@@ -589,10 +649,10 @@ function reloadInstructions(shell) {
|
|
|
589
649
|
// The baked absolute dg path stops a PATH-shadowing dg from hijacking the shim;
|
|
590
650
|
// the fallbacks make it fail open — a removed or moved dg (uninstall, mid-upgrade)
|
|
591
651
|
// runs the real manager instead of bricking it.
|
|
592
|
-
export function shimSource(command) {
|
|
652
|
+
export function shimSource(command, options = {}) {
|
|
593
653
|
const dg = escapeDoubleQuotedSh(dgEntrypoint());
|
|
594
654
|
const nonce = '"${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}' + `${command}:$$"`;
|
|
595
|
-
|
|
655
|
+
const head = [
|
|
596
656
|
"#!/bin/sh",
|
|
597
657
|
`# ${SHIM_SENTINEL}`,
|
|
598
658
|
`if [ -x "${dg}" ]; then`,
|
|
@@ -603,6 +663,27 @@ export function shimSource(command) {
|
|
|
603
663
|
`dg_bin=$(PATH="$dg_path" command -v dg 2>/dev/null)`,
|
|
604
664
|
`if [ -n "$dg_bin" ]; then`,
|
|
605
665
|
` DG_SHIM_ACTIVE=${nonce} exec "$dg_bin" ${command} "$@"`,
|
|
666
|
+
"fi"
|
|
667
|
+
];
|
|
668
|
+
// dg is gone from this machine. fail-closed (managed/CI) refuses the command
|
|
669
|
+
// so an unscanned install can't slip through; the operator removes the guard
|
|
670
|
+
// explicitly via `dg uninstall` while dg is present. fail-open (default)
|
|
671
|
+
// self-cleans dg's footprint and falls back to the real binary, because a
|
|
672
|
+
// plain `npm uninstall -g dg` cannot run cleanup and must not brick npm.
|
|
673
|
+
const failClosedTail = [
|
|
674
|
+
`echo "dg: install firewall required by policy (policy.shimFailClosed) but dg is unavailable; refusing to run ${command}" >&2`,
|
|
675
|
+
`echo "dg: reinstall dg, or run 'dg uninstall' while dg is present, to remove this guard" >&2`,
|
|
676
|
+
"exit 127",
|
|
677
|
+
""
|
|
678
|
+
];
|
|
679
|
+
const failOpenTail = [
|
|
680
|
+
`dg_home=$(dirname "$shim_dir")`,
|
|
681
|
+
`if [ -f "$dg_home/uninstall.mjs" ] && command -v node >/dev/null 2>&1; then`,
|
|
682
|
+
` ( sleep 10`,
|
|
683
|
+
` if [ ! -x "${dg}" ] && ! PATH="$dg_path" command -v dg >/dev/null 2>&1; then`,
|
|
684
|
+
` node "$dg_home/uninstall.mjs" --quiet >/dev/null 2>&1`,
|
|
685
|
+
` fi`,
|
|
686
|
+
` ) >/dev/null 2>&1 &`,
|
|
606
687
|
"fi",
|
|
607
688
|
`real_bin=$(PATH="$dg_path" command -v ${command} 2>/dev/null)`,
|
|
608
689
|
`if [ -n "$real_bin" ]; then`,
|
|
@@ -611,7 +692,8 @@ export function shimSource(command) {
|
|
|
611
692
|
`echo "dg: protection unavailable and no real ${command} found on PATH" >&2`,
|
|
612
693
|
"exit 127",
|
|
613
694
|
""
|
|
614
|
-
]
|
|
695
|
+
];
|
|
696
|
+
return [...head, ...(options.failClosed ? failClosedTail : failOpenTail)].join("\n");
|
|
615
697
|
}
|
|
616
698
|
function escapeDoubleQuotedSh(value) {
|
|
617
699
|
return value.replace(/[\\"$`]/g, "\\$&");
|
|
@@ -623,6 +705,25 @@ function dgEntrypoint() {
|
|
|
623
705
|
const argv1 = process.argv[1];
|
|
624
706
|
return argv1 ? resolve(argv1) : "dg";
|
|
625
707
|
}
|
|
708
|
+
function standaloneUninstallerSource() {
|
|
709
|
+
try {
|
|
710
|
+
return fileURLToPath(new URL("../standalone/uninstall.mjs", import.meta.url));
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function installStandaloneUninstaller(homeDir) {
|
|
717
|
+
const source = standaloneUninstallerSource();
|
|
718
|
+
if (!source || !existsSync(source)) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const dgRoot = join(homeDir, ".dg");
|
|
722
|
+
mkdirSync(dgRoot, { recursive: true, mode: 0o700 });
|
|
723
|
+
const dest = join(dgRoot, "uninstall.mjs");
|
|
724
|
+
copyFileSync(source, dest);
|
|
725
|
+
chmodSync(dest, 0o600);
|
|
726
|
+
}
|
|
626
727
|
function withRcBlock(existing, plan) {
|
|
627
728
|
const withoutExisting = stripRcBlock(existing);
|
|
628
729
|
const block = plan.shell === "fish" ? fishRcBlock(plan.shimDir) : posixRcBlock(plan.shimDir);
|
|
@@ -647,6 +748,26 @@ function fishRcBlock(shimDir) {
|
|
|
647
748
|
function stripRcBlock(existing) {
|
|
648
749
|
return stripRcBlockDetailed(existing).content;
|
|
649
750
|
}
|
|
751
|
+
function writeRcFileAtomic(rcPath, content) {
|
|
752
|
+
let target = rcPath;
|
|
753
|
+
let mode = 0o644;
|
|
754
|
+
try {
|
|
755
|
+
target = realpathSync(rcPath);
|
|
756
|
+
mode = statSync(target).mode & 0o7777;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
// no existing file: write a fresh rc at the requested path
|
|
760
|
+
}
|
|
761
|
+
const tempPath = join(dirname(target), `.${basename(target)}.dg-${randomBytes(6).toString("hex")}.tmp`);
|
|
762
|
+
writeFileSync(tempPath, content, { encoding: "utf8", mode, flag: "wx" });
|
|
763
|
+
try {
|
|
764
|
+
renameSync(tempPath, target);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
rmSync(tempPath, { force: true });
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
650
771
|
// An unterminated begin marker must never strip to EOF: only lines verifiably
|
|
651
772
|
// written by dg are removed, so user content below a stale marker survives.
|
|
652
773
|
function stripRcBlockDetailed(existing) {
|
|
@@ -696,7 +817,7 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
|
|
|
696
817
|
continue;
|
|
697
818
|
}
|
|
698
819
|
try {
|
|
699
|
-
|
|
820
|
+
writeRcFileAtomic(rcPath, next);
|
|
700
821
|
removed.push(`${rcPath} (legacy dg block)`);
|
|
701
822
|
}
|
|
702
823
|
catch (error) {
|
|
@@ -704,19 +825,23 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
|
|
|
704
825
|
}
|
|
705
826
|
}
|
|
706
827
|
}
|
|
828
|
+
function isLegacyPthHook(pthPath) {
|
|
829
|
+
const content = readText(pthPath);
|
|
830
|
+
return content.includes(LEGACY_PYTHON_HOOK_PTH_MARKER) || content.includes(LEGACY_PYTHON_HOOK_MARKER);
|
|
831
|
+
}
|
|
707
832
|
export function legacyPythonHookSites(homeDir) {
|
|
708
|
-
return candidateSitePackagesDirs(homeDir).filter((dir) =>
|
|
833
|
+
return candidateSitePackagesDirs(homeDir).filter((dir) => isLegacyPthHook(join(dir, LEGACY_PYTHON_HOOK_PTH)) || readText(join(dir, LEGACY_PYTHON_HOOK_PY)).includes(LEGACY_PYTHON_HOOK_MARKER));
|
|
709
834
|
}
|
|
710
835
|
export function sweepLegacyPythonHooks(homeDir, removed, warnings) {
|
|
711
836
|
for (const dir of candidateSitePackagesDirs(homeDir)) {
|
|
712
837
|
const pyPath = join(dir, LEGACY_PYTHON_HOOK_PY);
|
|
713
838
|
const pthPath = join(dir, LEGACY_PYTHON_HOOK_PTH);
|
|
714
839
|
const pyIsHook = readText(pyPath).includes(LEGACY_PYTHON_HOOK_MARKER);
|
|
715
|
-
const
|
|
716
|
-
if (!pyIsHook && !
|
|
840
|
+
const pthIsHook = isLegacyPthHook(pthPath);
|
|
841
|
+
if (!pyIsHook && !pthIsHook) {
|
|
717
842
|
continue;
|
|
718
843
|
}
|
|
719
|
-
if (
|
|
844
|
+
if (pthIsHook) {
|
|
720
845
|
removePythonHookFile(pthPath, removed, warnings);
|
|
721
846
|
}
|
|
722
847
|
if (pyIsHook) {
|
|
@@ -815,7 +940,19 @@ function configCheck(paths, env) {
|
|
|
815
940
|
}
|
|
816
941
|
try {
|
|
817
942
|
accessSync(paths.configDir, constants.R_OK);
|
|
818
|
-
loadUserConfig(env);
|
|
943
|
+
const config = loadUserConfig(env);
|
|
944
|
+
if (config.api.baseUrl !== DEFAULT_CONFIG.api.baseUrl) {
|
|
945
|
+
// The verdict API is the firewall's source of truth. A non-default
|
|
946
|
+
// endpoint is legitimate for enterprise self-host, but a silent repoint
|
|
947
|
+
// is also how an attacker would make every verdict come back clean, so
|
|
948
|
+
// surface it on the standard health check rather than trusting it quietly.
|
|
949
|
+
return {
|
|
950
|
+
name: "config",
|
|
951
|
+
status: "warn",
|
|
952
|
+
message: `dg is fetching verdicts from a non-default API endpoint: ${config.api.baseUrl} (default ${DEFAULT_CONFIG.api.baseUrl})`,
|
|
953
|
+
fix: `if you did not set this, your verdict source may be tampered — restore with: dg config set api.baseUrl ${DEFAULT_CONFIG.api.baseUrl}`
|
|
954
|
+
};
|
|
955
|
+
}
|
|
819
956
|
return {
|
|
820
957
|
name: "config",
|
|
821
958
|
status: "pass",
|
|
@@ -873,7 +1010,7 @@ function dgPathCheck(env) {
|
|
|
873
1010
|
};
|
|
874
1011
|
}
|
|
875
1012
|
function currentDgBinaryPath(env) {
|
|
876
|
-
const explicit = env.DG_TEST_CURRENT_DG_PATH;
|
|
1013
|
+
const explicit = env.NODE_ENV === "test" ? env.DG_TEST_CURRENT_DG_PATH : undefined;
|
|
877
1014
|
if (explicit) {
|
|
878
1015
|
return explicit;
|
|
879
1016
|
}
|
|
@@ -931,6 +1068,45 @@ function serviceCheck(env) {
|
|
|
931
1068
|
message: `Service mode is running at ${state.proxy.proxyUrl}; trust installed: ${state.trustInstalled ? "yes" : "no"}`
|
|
932
1069
|
};
|
|
933
1070
|
}
|
|
1071
|
+
function hookedAgentLabels(env, home) {
|
|
1072
|
+
const labels = [];
|
|
1073
|
+
for (const agent of AGENT_IDS) {
|
|
1074
|
+
const integration = AGENTS[agent];
|
|
1075
|
+
if (!integration.detect(home)) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
1079
|
+
const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
|
|
1080
|
+
if (hooked) {
|
|
1081
|
+
labels.push(agentLabel(agent));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return labels;
|
|
1085
|
+
}
|
|
1086
|
+
function agentGateCheck(env, home) {
|
|
1087
|
+
const hooked = hookedAgentLabels(env, home);
|
|
1088
|
+
if (hooked.length === 0) {
|
|
1089
|
+
return {
|
|
1090
|
+
name: "agent-gate",
|
|
1091
|
+
status: "unavailable",
|
|
1092
|
+
message: "No AI agent has the dg install hook; run 'dg agents on' to protect agent-run installs"
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
const posture = readNetworkGatePosture(env);
|
|
1096
|
+
if (posture.live) {
|
|
1097
|
+
return {
|
|
1098
|
+
name: "agent-gate",
|
|
1099
|
+
status: "pass",
|
|
1100
|
+
message: `${hooked.join(", ")} hooked; network gate live at ${posture.proxyUrl} (installs screened at fetch by artifact hash)`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
name: "agent-gate",
|
|
1105
|
+
status: "warn",
|
|
1106
|
+
message: `${hooked.join(", ")} hooked but the fetch-time network gate is OFF — static pre-screen only; absolute-path, manifest-only, dynamic, and unsupported-manager installs are NOT screened`,
|
|
1107
|
+
fix: GATE_ENABLE_HINT
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
934
1110
|
function pathPrecedenceCheck(env, shimDir, functionsPresent) {
|
|
935
1111
|
const pathEntries = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
936
1112
|
const shimIndex = pathEntries.indexOf(shimDir);
|
|
@@ -1117,9 +1293,8 @@ function scriptGateCheck(env) {
|
|
|
1117
1293
|
if (mode === "enforce") {
|
|
1118
1294
|
return {
|
|
1119
1295
|
name: "script-gate",
|
|
1120
|
-
status: "
|
|
1121
|
-
message: "
|
|
1122
|
-
fix: "enforcement ships in an upcoming release; dg config set scriptGate.mode observe clears this warning"
|
|
1296
|
+
status: "pass",
|
|
1297
|
+
message: "Install-script gate enforces: protected npm/yarn installs run with --ignore-scripts so packages cannot run lifecycle scripts (pnpm blocks them natively)"
|
|
1123
1298
|
};
|
|
1124
1299
|
}
|
|
1125
1300
|
return {
|
|
@@ -1136,34 +1311,24 @@ function scriptGateCheck(env) {
|
|
|
1136
1311
|
};
|
|
1137
1312
|
}
|
|
1138
1313
|
}
|
|
1139
|
-
function telemetryCheck(env) {
|
|
1140
|
-
try {
|
|
1141
|
-
const config = loadUserConfig(env);
|
|
1142
|
-
return {
|
|
1143
|
-
name: "telemetry",
|
|
1144
|
-
status: "pass",
|
|
1145
|
-
message: config.telemetry.enabled ? "Telemetry is enabled with minimized event fields" : "Telemetry is disabled"
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
catch (error) {
|
|
1149
|
-
return {
|
|
1150
|
-
name: "telemetry",
|
|
1151
|
-
status: "fail",
|
|
1152
|
-
message: error instanceof ConfigError ? error.message : "Unable to read telemetry config"
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
1314
|
export function writeRegistry(paths, registry) {
|
|
1157
1315
|
mkdirSync(dirname(paths.cleanupRegistryPath), {
|
|
1158
1316
|
recursive: true,
|
|
1159
1317
|
mode: 0o700
|
|
1160
1318
|
});
|
|
1161
|
-
const tempPath = `${paths.cleanupRegistryPath}.${
|
|
1319
|
+
const tempPath = `${paths.cleanupRegistryPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1162
1320
|
writeFileSync(tempPath, `${JSON.stringify(registry, null, 2)}\n`, {
|
|
1163
1321
|
encoding: "utf8",
|
|
1164
|
-
mode: 0o600
|
|
1322
|
+
mode: 0o600,
|
|
1323
|
+
flag: "wx"
|
|
1165
1324
|
});
|
|
1166
|
-
|
|
1325
|
+
try {
|
|
1326
|
+
renameSync(tempPath, paths.cleanupRegistryPath);
|
|
1327
|
+
}
|
|
1328
|
+
catch (error) {
|
|
1329
|
+
rmSync(tempPath, { force: true });
|
|
1330
|
+
throw error;
|
|
1331
|
+
}
|
|
1167
1332
|
}
|
|
1168
1333
|
function withRegistryLock(paths, action) {
|
|
1169
1334
|
const lock = acquireLockSync(paths, CLEANUP_REGISTRY_LOCK, {
|
|
@@ -1204,7 +1369,13 @@ function removeRcBlock(entry, removed, missing, warnings) {
|
|
|
1204
1369
|
return;
|
|
1205
1370
|
}
|
|
1206
1371
|
const stripped = stripRcBlockDetailed(existing);
|
|
1207
|
-
|
|
1372
|
+
try {
|
|
1373
|
+
writeRcFileAtomic(entry.path, stripped.content);
|
|
1374
|
+
}
|
|
1375
|
+
catch (error) {
|
|
1376
|
+
warnings.push(`could not rewrite shell rc ${entry.path}: ${error instanceof Error ? error.message : "write error"}`);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1208
1379
|
if (stripped.repairedLines.length > 0) {
|
|
1209
1380
|
warnings.push(`repaired an unterminated dg block in ${entry.path}: removed only dg-written lines (${stripped.repairedLines.join(", ")})`);
|
|
1210
1381
|
}
|
|
@@ -1214,28 +1385,35 @@ function removeRcBlock(entry, removed, missing, warnings) {
|
|
|
1214
1385
|
// a one-line notice and lets the commit (and any chained hook) proceed instead
|
|
1215
1386
|
// of blocking every commit with exit 127.
|
|
1216
1387
|
export function guardHookScript(dgPath, chainedOriginal) {
|
|
1388
|
+
const dg = escapeDoubleQuotedSh(dgPath);
|
|
1217
1389
|
const lines = [
|
|
1218
1390
|
"#!/bin/sh",
|
|
1219
1391
|
`# ${GUARD_HOOK_SENTINEL}`,
|
|
1220
|
-
`if command -v "${
|
|
1221
|
-
` "${
|
|
1392
|
+
`if command -v "${dg}" >/dev/null 2>&1; then`,
|
|
1393
|
+
` "${dg}" scan --staged --hook || exit $?`,
|
|
1222
1394
|
"else",
|
|
1223
|
-
` echo "dg: pre-commit scan skipped (dg not runnable at ${
|
|
1395
|
+
` echo "dg: pre-commit scan skipped (dg not runnable at ${dg}); commit allowed" >&2`,
|
|
1224
1396
|
"fi"
|
|
1225
1397
|
];
|
|
1226
1398
|
if (chainedOriginal) {
|
|
1227
|
-
|
|
1399
|
+
const chained = escapeDoubleQuotedSh(chainedOriginal);
|
|
1400
|
+
lines.push(`[ -x "${chained}" ] && exec "${chained}" "$@"`);
|
|
1228
1401
|
}
|
|
1229
1402
|
lines.push("exit 0");
|
|
1230
1403
|
return `${lines.join("\n")}\n`;
|
|
1231
1404
|
}
|
|
1405
|
+
function unescapeDoubleQuotedSh(value) {
|
|
1406
|
+
return value.replace(/\\([\\"$`])/g, "$1");
|
|
1407
|
+
}
|
|
1232
1408
|
export function chainedHookOriginal(content) {
|
|
1233
|
-
|
|
1409
|
+
const matched = content.match(/^\[ -x "(.+)" \] && exec "\1" "\$@"$/m)?.[1];
|
|
1410
|
+
return matched ? unescapeDoubleQuotedSh(matched) : null;
|
|
1234
1411
|
}
|
|
1235
1412
|
export function guardHookDgPath(content) {
|
|
1236
|
-
|
|
1413
|
+
const matched = content.match(/^if command -v "(.+)" >\/dev\/null 2>&1; then$/m)?.[1] ??
|
|
1237
1414
|
content.match(/^"(.+)" scan --staged --hook/m)?.[1] ??
|
|
1238
|
-
null
|
|
1415
|
+
null;
|
|
1416
|
+
return matched ? unescapeDoubleQuotedSh(matched) : null;
|
|
1239
1417
|
}
|
|
1240
1418
|
function commitGuardCheck(registry, env) {
|
|
1241
1419
|
const hooks = registry.entries.filter((entry) => entry.owner === "dg" && entry.kind === "git-hook");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { uninstallSetup } from "./plan.js";
|
|
4
|
+
import { LockBusyError, resolveDgPaths } from "../state/index.js";
|
|
5
|
+
function run() {
|
|
6
|
+
const quiet = process.argv.includes("--quiet");
|
|
7
|
+
try {
|
|
8
|
+
const result = uninstallSetup({ keepConfig: false, all: true });
|
|
9
|
+
if (!quiet) {
|
|
10
|
+
process.stderr.write(`Dependency Guardian cleaned up ${result.removed.length} leftover item(s) after the package was removed.\n`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!quiet && !(error instanceof LockBusyError)) {
|
|
15
|
+
process.stderr.write(`dg self-cleanup could not finish: ${error.message}\n`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
rmSync(join(resolveDgPaths().homeDir, ".dg"), { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
run();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { isCiEnv } from "../presentation/mode.js";
|
|
4
|
+
import { resolveDgPaths } from "../state/index.js";
|
|
5
|
+
export function setupApplied(env = process.env) {
|
|
6
|
+
return existsSync(join(resolveDgPaths(env).homeDir, ".dg", "shims", "npm"));
|
|
7
|
+
}
|
|
8
|
+
export function securityNotesMarkerPath(env = process.env) {
|
|
9
|
+
return join(resolveDgPaths(env).stateDir, "security-notes-shown");
|
|
10
|
+
}
|
|
11
|
+
export function wizardSkippedMarkerPath(env = process.env) {
|
|
12
|
+
return join(resolveDgPaths(env).stateDir, "setup-wizard-skipped");
|
|
13
|
+
}
|
|
14
|
+
export function securityNotesShown(env = process.env) {
|
|
15
|
+
return existsSync(securityNotesMarkerPath(env));
|
|
16
|
+
}
|
|
17
|
+
export function markSecurityNotesShown(env = process.env, now = new Date()) {
|
|
18
|
+
writeMarker(securityNotesMarkerPath(env), now);
|
|
19
|
+
}
|
|
20
|
+
export function markWizardSkipped(env = process.env, now = new Date()) {
|
|
21
|
+
writeMarker(wizardSkippedMarkerPath(env), now);
|
|
22
|
+
}
|
|
23
|
+
export function shouldOfferSetupWizard(env = process.env, stdin = process.stdin, stderr = process.stderr) {
|
|
24
|
+
return (process.platform !== "win32" &&
|
|
25
|
+
Boolean(stdin.isTTY) &&
|
|
26
|
+
Boolean(stderr.isTTY) &&
|
|
27
|
+
!isCiEnv(env) &&
|
|
28
|
+
!setupApplied(env) &&
|
|
29
|
+
!existsSync(wizardSkippedMarkerPath(env)));
|
|
30
|
+
}
|
|
31
|
+
function writeMarker(path, now) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
34
|
+
writeFileSync(path, `${now.toISOString()}\n`, { encoding: "utf8", mode: 0o600 });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|