@westbayberry/dg 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -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-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 +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -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 +197 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +14 -44
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +1 -1
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +51 -14
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +14 -1
- package/dist/launcher/run.js +89 -86
- 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 +145 -6
- 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 +41 -3
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +448 -49
- 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 +12 -11
- 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 +128 -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 +2 -1
- 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 +16 -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/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- 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 +73 -39
- 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 +225 -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 +240 -42
- package/dist/verify/package-check.js +84 -28
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +6 -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,7 +1,10 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, 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 { randomBytes } from "node:crypto";
|
|
5
|
+
import { homedir } from "node:os";
|
|
4
6
|
import { createTheme } from "../presentation/theme.js";
|
|
7
|
+
import { reverseAgentHookEntry } from "../agents/registry.js";
|
|
5
8
|
import { acquireLockSync, findStaleSessionsSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, sweepStaleSessionsSync, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
6
9
|
import { currentNodeVersion, isSupportedNode } from "../runtime/node-version.js";
|
|
7
10
|
import { dgVersion } from "../commands/version.js";
|
|
@@ -28,6 +31,7 @@ const LEGACY_RC_CANDIDATES = [".zshrc", ".bashrc", ".bash_profile", ".profile",
|
|
|
28
31
|
const LEGACY_PYTHON_HOOK_PY = "dg_pip_hook.py";
|
|
29
32
|
const LEGACY_PYTHON_HOOK_PTH = "dg_pip_hook.pth";
|
|
30
33
|
const LEGACY_PYTHON_HOOK_MARKER = "Dependency Guardian pip-install interceptor";
|
|
34
|
+
const LEGACY_PYTHON_HOOK_PTH_MARKER = "import dg_pip_hook";
|
|
31
35
|
export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
|
|
32
36
|
export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
33
37
|
export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
|
|
@@ -44,7 +48,6 @@ const DOCTOR_GROUP_BY_NAME = {
|
|
|
44
48
|
config: "setup",
|
|
45
49
|
policy: "setup",
|
|
46
50
|
"script-gate": "setup",
|
|
47
|
-
telemetry: "setup",
|
|
48
51
|
shims: "setup",
|
|
49
52
|
"shell-rc": "setup",
|
|
50
53
|
"python-hook-drift": "setup",
|
|
@@ -64,7 +67,6 @@ const DOCTOR_FIX_BY_NAME = {
|
|
|
64
67
|
"cleanup-registry-stale-entries": "re-run dg setup to refresh",
|
|
65
68
|
config: "fix or remove ~/.dg, then re-run dg setup",
|
|
66
69
|
policy: "fix ~/.dg config",
|
|
67
|
-
telemetry: "fix ~/.dg config",
|
|
68
70
|
shims: "dg setup",
|
|
69
71
|
"shell-rc": "dg setup",
|
|
70
72
|
"python-hook-drift": "dg uninstall, or re-run dg setup, to remove the stale pip hook",
|
|
@@ -156,7 +158,7 @@ export function applySetupPlan(plan, now = new Date()) {
|
|
|
156
158
|
recursive: true,
|
|
157
159
|
mode: 0o700
|
|
158
160
|
});
|
|
159
|
-
|
|
161
|
+
writeRcFileAtomic(plan.rcPath, withRcBlock(readText(plan.rcPath), plan));
|
|
160
162
|
entries.push(cleanupEntry("rc", plan.rcPath, "mode1", now, RC_SENTINEL));
|
|
161
163
|
sweepLegacyPythonHooks(plan.paths.homeDir, [], []);
|
|
162
164
|
const registry = withRegistryLock(plan.paths, () => {
|
|
@@ -213,6 +215,9 @@ export function uninstallSetup(options) {
|
|
|
213
215
|
else if (entry.kind === "git-hook") {
|
|
214
216
|
reverseGitHookEntry(entry, removed, missing, warnings);
|
|
215
217
|
}
|
|
218
|
+
else if (entry.kind === "agent-hook") {
|
|
219
|
+
reverseAgentHookEntry(entry, removed, missing, warnings);
|
|
220
|
+
}
|
|
216
221
|
}
|
|
217
222
|
sweepLegacyRcBlocks(paths.homeDir, removed, warnings);
|
|
218
223
|
sweepLegacyPythonHooks(paths.homeDir, removed, warnings);
|
|
@@ -278,7 +283,6 @@ export function doctorReport(options = {}) {
|
|
|
278
283
|
checks.push(authCheck(env));
|
|
279
284
|
checks.push(policyCheck(env));
|
|
280
285
|
checks.push(scriptGateCheck(env));
|
|
281
|
-
checks.push(telemetryCheck(env));
|
|
282
286
|
const missingShims = SHIM_COMMANDS.filter((command) => !validShim(join(shimDir, command), command));
|
|
283
287
|
checks.push({
|
|
284
288
|
name: "shims",
|
|
@@ -568,6 +572,9 @@ function shellRcPath(homeDir, shell) {
|
|
|
568
572
|
}
|
|
569
573
|
return join(homeDir, ".zshrc");
|
|
570
574
|
}
|
|
575
|
+
export function tildifyPath(path, home = homedir()) {
|
|
576
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
577
|
+
}
|
|
571
578
|
export function activationCommand(shell, rcDisplay) {
|
|
572
579
|
if (shell === "fish") {
|
|
573
580
|
return `source ${rcDisplay}`;
|
|
@@ -647,6 +654,26 @@ function fishRcBlock(shimDir) {
|
|
|
647
654
|
function stripRcBlock(existing) {
|
|
648
655
|
return stripRcBlockDetailed(existing).content;
|
|
649
656
|
}
|
|
657
|
+
function writeRcFileAtomic(rcPath, content) {
|
|
658
|
+
let target = rcPath;
|
|
659
|
+
let mode = 0o644;
|
|
660
|
+
try {
|
|
661
|
+
target = realpathSync(rcPath);
|
|
662
|
+
mode = statSync(target).mode & 0o7777;
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
// no existing file: write a fresh rc at the requested path
|
|
666
|
+
}
|
|
667
|
+
const tempPath = join(dirname(target), `.${basename(target)}.dg-${randomBytes(6).toString("hex")}.tmp`);
|
|
668
|
+
writeFileSync(tempPath, content, { encoding: "utf8", mode, flag: "wx" });
|
|
669
|
+
try {
|
|
670
|
+
renameSync(tempPath, target);
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
rmSync(tempPath, { force: true });
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
650
677
|
// An unterminated begin marker must never strip to EOF: only lines verifiably
|
|
651
678
|
// written by dg are removed, so user content below a stale marker survives.
|
|
652
679
|
function stripRcBlockDetailed(existing) {
|
|
@@ -696,7 +723,7 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
|
|
|
696
723
|
continue;
|
|
697
724
|
}
|
|
698
725
|
try {
|
|
699
|
-
|
|
726
|
+
writeRcFileAtomic(rcPath, next);
|
|
700
727
|
removed.push(`${rcPath} (legacy dg block)`);
|
|
701
728
|
}
|
|
702
729
|
catch (error) {
|
|
@@ -704,19 +731,23 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
|
|
|
704
731
|
}
|
|
705
732
|
}
|
|
706
733
|
}
|
|
734
|
+
function isLegacyPthHook(pthPath) {
|
|
735
|
+
const content = readText(pthPath);
|
|
736
|
+
return content.includes(LEGACY_PYTHON_HOOK_PTH_MARKER) || content.includes(LEGACY_PYTHON_HOOK_MARKER);
|
|
737
|
+
}
|
|
707
738
|
export function legacyPythonHookSites(homeDir) {
|
|
708
|
-
return candidateSitePackagesDirs(homeDir).filter((dir) =>
|
|
739
|
+
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
740
|
}
|
|
710
741
|
export function sweepLegacyPythonHooks(homeDir, removed, warnings) {
|
|
711
742
|
for (const dir of candidateSitePackagesDirs(homeDir)) {
|
|
712
743
|
const pyPath = join(dir, LEGACY_PYTHON_HOOK_PY);
|
|
713
744
|
const pthPath = join(dir, LEGACY_PYTHON_HOOK_PTH);
|
|
714
745
|
const pyIsHook = readText(pyPath).includes(LEGACY_PYTHON_HOOK_MARKER);
|
|
715
|
-
const
|
|
716
|
-
if (!pyIsHook && !
|
|
746
|
+
const pthIsHook = isLegacyPthHook(pthPath);
|
|
747
|
+
if (!pyIsHook && !pthIsHook) {
|
|
717
748
|
continue;
|
|
718
749
|
}
|
|
719
|
-
if (
|
|
750
|
+
if (pthIsHook) {
|
|
720
751
|
removePythonHookFile(pthPath, removed, warnings);
|
|
721
752
|
}
|
|
722
753
|
if (pyIsHook) {
|
|
@@ -873,7 +904,7 @@ function dgPathCheck(env) {
|
|
|
873
904
|
};
|
|
874
905
|
}
|
|
875
906
|
function currentDgBinaryPath(env) {
|
|
876
|
-
const explicit = env.DG_TEST_CURRENT_DG_PATH;
|
|
907
|
+
const explicit = env.NODE_ENV === "test" ? env.DG_TEST_CURRENT_DG_PATH : undefined;
|
|
877
908
|
if (explicit) {
|
|
878
909
|
return explicit;
|
|
879
910
|
}
|
|
@@ -1136,34 +1167,24 @@ function scriptGateCheck(env) {
|
|
|
1136
1167
|
};
|
|
1137
1168
|
}
|
|
1138
1169
|
}
|
|
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
1170
|
export function writeRegistry(paths, registry) {
|
|
1157
1171
|
mkdirSync(dirname(paths.cleanupRegistryPath), {
|
|
1158
1172
|
recursive: true,
|
|
1159
1173
|
mode: 0o700
|
|
1160
1174
|
});
|
|
1161
|
-
const tempPath = `${paths.cleanupRegistryPath}.${
|
|
1175
|
+
const tempPath = `${paths.cleanupRegistryPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1162
1176
|
writeFileSync(tempPath, `${JSON.stringify(registry, null, 2)}\n`, {
|
|
1163
1177
|
encoding: "utf8",
|
|
1164
|
-
mode: 0o600
|
|
1178
|
+
mode: 0o600,
|
|
1179
|
+
flag: "wx"
|
|
1165
1180
|
});
|
|
1166
|
-
|
|
1181
|
+
try {
|
|
1182
|
+
renameSync(tempPath, paths.cleanupRegistryPath);
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
rmSync(tempPath, { force: true });
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1167
1188
|
}
|
|
1168
1189
|
function withRegistryLock(paths, action) {
|
|
1169
1190
|
const lock = acquireLockSync(paths, CLEANUP_REGISTRY_LOCK, {
|
|
@@ -1204,7 +1225,13 @@ function removeRcBlock(entry, removed, missing, warnings) {
|
|
|
1204
1225
|
return;
|
|
1205
1226
|
}
|
|
1206
1227
|
const stripped = stripRcBlockDetailed(existing);
|
|
1207
|
-
|
|
1228
|
+
try {
|
|
1229
|
+
writeRcFileAtomic(entry.path, stripped.content);
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
warnings.push(`could not rewrite shell rc ${entry.path}: ${error instanceof Error ? error.message : "write error"}`);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1208
1235
|
if (stripped.repairedLines.length > 0) {
|
|
1209
1236
|
warnings.push(`repaired an unterminated dg block in ${entry.path}: removed only dg-written lines (${stripped.repairedLines.join(", ")})`);
|
|
1210
1237
|
}
|
|
@@ -1214,28 +1241,35 @@ function removeRcBlock(entry, removed, missing, warnings) {
|
|
|
1214
1241
|
// a one-line notice and lets the commit (and any chained hook) proceed instead
|
|
1215
1242
|
// of blocking every commit with exit 127.
|
|
1216
1243
|
export function guardHookScript(dgPath, chainedOriginal) {
|
|
1244
|
+
const dg = escapeDoubleQuotedSh(dgPath);
|
|
1217
1245
|
const lines = [
|
|
1218
1246
|
"#!/bin/sh",
|
|
1219
1247
|
`# ${GUARD_HOOK_SENTINEL}`,
|
|
1220
|
-
`if command -v "${
|
|
1221
|
-
` "${
|
|
1248
|
+
`if command -v "${dg}" >/dev/null 2>&1; then`,
|
|
1249
|
+
` "${dg}" scan --staged --hook || exit $?`,
|
|
1222
1250
|
"else",
|
|
1223
|
-
` echo "dg: pre-commit scan skipped (dg not runnable at ${
|
|
1251
|
+
` echo "dg: pre-commit scan skipped (dg not runnable at ${dg}); commit allowed" >&2`,
|
|
1224
1252
|
"fi"
|
|
1225
1253
|
];
|
|
1226
1254
|
if (chainedOriginal) {
|
|
1227
|
-
|
|
1255
|
+
const chained = escapeDoubleQuotedSh(chainedOriginal);
|
|
1256
|
+
lines.push(`[ -x "${chained}" ] && exec "${chained}" "$@"`);
|
|
1228
1257
|
}
|
|
1229
1258
|
lines.push("exit 0");
|
|
1230
1259
|
return `${lines.join("\n")}\n`;
|
|
1231
1260
|
}
|
|
1261
|
+
function unescapeDoubleQuotedSh(value) {
|
|
1262
|
+
return value.replace(/\\([\\"$`])/g, "$1");
|
|
1263
|
+
}
|
|
1232
1264
|
export function chainedHookOriginal(content) {
|
|
1233
|
-
|
|
1265
|
+
const matched = content.match(/^\[ -x "(.+)" \] && exec "\1" "\$@"$/m)?.[1];
|
|
1266
|
+
return matched ? unescapeDoubleQuotedSh(matched) : null;
|
|
1234
1267
|
}
|
|
1235
1268
|
export function guardHookDgPath(content) {
|
|
1236
|
-
|
|
1269
|
+
const matched = content.match(/^if command -v "(.+)" >\/dev\/null 2>&1; then$/m)?.[1] ??
|
|
1237
1270
|
content.match(/^"(.+)" scan --staged --hook/m)?.[1] ??
|
|
1238
|
-
null
|
|
1271
|
+
null;
|
|
1272
|
+
return matched ? unescapeDoubleQuotedSh(matched) : null;
|
|
1239
1273
|
}
|
|
1240
1274
|
function commitGuardCheck(registry, env) {
|
|
1241
1275
|
const hooks = registry.entries.filter((entry) => entry.owner === "dg" && entry.kind === "git-hook");
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { resolvePresentation } from "../presentation/mode.js";
|
|
2
|
+
import { MACHINE_OUTPUT_FLAGS } from "../runtime/first-run.js";
|
|
3
|
+
import { buildSetupPlan } from "../setup/plan.js";
|
|
4
|
+
import { shouldOfferSetupWizard } from "./gate.js";
|
|
5
|
+
const SKIP_COMMANDS = new Set([
|
|
6
|
+
"help",
|
|
7
|
+
"--help",
|
|
8
|
+
"-h",
|
|
9
|
+
"--help-all",
|
|
10
|
+
"help-all",
|
|
11
|
+
"version",
|
|
12
|
+
"--version",
|
|
13
|
+
"-v",
|
|
14
|
+
"login",
|
|
15
|
+
"logout",
|
|
16
|
+
"setup",
|
|
17
|
+
"update",
|
|
18
|
+
"upgrade",
|
|
19
|
+
"uninstall"
|
|
20
|
+
]);
|
|
21
|
+
const NOT_RUN = { handled: false, result: { exitCode: 0, stdout: "", stderr: "" } };
|
|
22
|
+
export async function maybeOfferSetupWizard(args, options = {}) {
|
|
23
|
+
const env = options.env ?? process.env;
|
|
24
|
+
const command = args[0] ?? "";
|
|
25
|
+
if (command && SKIP_COMMANDS.has(command)) {
|
|
26
|
+
return NOT_RUN;
|
|
27
|
+
}
|
|
28
|
+
if (args.some((arg) => MACHINE_OUTPUT_FLAGS.has(arg))) {
|
|
29
|
+
return NOT_RUN;
|
|
30
|
+
}
|
|
31
|
+
if (!shouldOfferSetupWizard(env, options.stdin ?? process.stdin, options.stderr ?? process.stderr)) {
|
|
32
|
+
return NOT_RUN;
|
|
33
|
+
}
|
|
34
|
+
const rich = options.richMode ?? resolvePresentation().mode === "rich";
|
|
35
|
+
if (!rich) {
|
|
36
|
+
return NOT_RUN;
|
|
37
|
+
}
|
|
38
|
+
const plan = buildSetupPlan({ shell: "auto" });
|
|
39
|
+
const runWizard = options.runWizard ?? (await import("./wizard.js")).runSetupWizard;
|
|
40
|
+
const result = await runWizard(plan, { env, autoActivate: args.length === 0 });
|
|
41
|
+
return { handled: args.length === 0, result };
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
export const Selector = ({ options, onSelect, onCancel }) => {
|
|
5
|
+
const [cursor, setCursor] = useState(0);
|
|
6
|
+
useInput((input, key) => {
|
|
7
|
+
if (key.upArrow || input === "k") {
|
|
8
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
9
|
+
}
|
|
10
|
+
else if (key.downArrow || input === "j") {
|
|
11
|
+
setCursor((c) => Math.min(options.length - 1, c + 1));
|
|
12
|
+
}
|
|
13
|
+
else if (key.return) {
|
|
14
|
+
onSelect(cursor);
|
|
15
|
+
}
|
|
16
|
+
else if (key.escape) {
|
|
17
|
+
onCancel();
|
|
18
|
+
}
|
|
19
|
+
else if (/^[1-9]$/.test(input)) {
|
|
20
|
+
const index = Number(input) - 1;
|
|
21
|
+
if (index < options.length) {
|
|
22
|
+
onSelect(index);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", children: [options.map((option, i) => (_jsxs(Text, { ...(i === cursor ? { color: "cyan" } : {}), children: [i === cursor ? "❯" : " ", " ", i + 1, ". ", option.label] }, i))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 arrows or number keys to choose \u00B7 Esc to skip" })] }));
|
|
27
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { LockBusyError } from "../state/index.js";
|
|
2
|
+
import { applyGitHook } from "../setup/git-hook.js";
|
|
3
|
+
import { applySetupPlanWithLock } from "../setup/plan.js";
|
|
4
|
+
import { applyAgentHook } from "../agents/registry.js";
|
|
5
|
+
export function buildWizardTasks(plan, agents) {
|
|
6
|
+
const tasks = [
|
|
7
|
+
{
|
|
8
|
+
label: "shell installs route through dg",
|
|
9
|
+
run: async () => {
|
|
10
|
+
try {
|
|
11
|
+
applySetupPlanWithLock(plan);
|
|
12
|
+
return { ok: true };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof LockBusyError) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
fatal: true,
|
|
19
|
+
detail: `dg setup cannot apply while another setup or uninstall is running: ${error.path}`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
for (const offer of agents) {
|
|
28
|
+
tasks.push({
|
|
29
|
+
label: `${offer.label} installs route through dg`,
|
|
30
|
+
run: async () => {
|
|
31
|
+
try {
|
|
32
|
+
await applyAgentHook(offer.ctx);
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return { ok: false, detail: error instanceof Error ? error.message : "unknown error" };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return tasks;
|
|
42
|
+
}
|
|
43
|
+
export function buildCommitGuardTask(repo) {
|
|
44
|
+
return {
|
|
45
|
+
label: "commits in this repo are scanned",
|
|
46
|
+
run: async () => {
|
|
47
|
+
try {
|
|
48
|
+
applyGitHook(repo);
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { ok: false, detail: error instanceof Error ? error.message : "unknown error" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|