@westbayberry/dg 2.0.11 → 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 +6 -4
- 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 +26 -10
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +169 -0
- 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 +8 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +3 -2
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/status.js +5 -2
- 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 +154 -65
- package/dist/decisions/apply.js +128 -0
- package/dist/decisions/remember-prompt.js +92 -0
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +21 -4
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/install-ui/prompt.js +14 -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 +167 -17
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +43 -3
- package/dist/launcher/run.js +100 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +117 -0
- package/dist/policy/evaluate.js +5 -21
- 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/provenance.js +23 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +446 -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 +57 -17
- package/dist/proxy/metadata-map.js +66 -4
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +473 -45
- 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 +51 -17
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +63 -8
- package/dist/scan/scanner-report.js +42 -9
- package/dist/scan/staged.js +71 -11
- package/dist/scan-ui/LegacyApp.js +12 -16
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
- 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 +46 -4
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +14 -4
- package/dist/scripts/detect.js +158 -0
- package/dist/scripts/gate.js +170 -0
- 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 +98 -29
- 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/json-file.js +24 -0
- package/dist/util/report-writer.js +57 -0
- package/dist/util/tty-prompt.js +13 -6
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +86 -18
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +14 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -116
- package/dist/commands/explain.js +0 -232
- package/dist/commands/unavailable.js +0 -11
- package/dist/telemetry/events.js +0 -40
package/dist/service/state.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { loadUserConfig } from "../config/settings.js";
|
|
7
7
|
import { applyTrustInstall, applyTrustUninstall, readCertificateFingerprints, readServiceTrustRecord, renderTrustStorePlanLines, resolveTrustInstallPlan, TrustStoreError, TrustToolMissingError, writeServiceTrustRecord } from "./trust-store.js";
|
|
8
|
+
import { clearTrustRefreshError, readTrustRefreshError } from "./trust-refresh.js";
|
|
8
9
|
import { acquireLockSync, cleanupSessionSync, createSessionSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
9
10
|
export const SERVICE_SENTINEL = "dg-service-mode-v1";
|
|
10
11
|
export const TRUST_SENTINEL = "dg-service-trust-v1";
|
|
@@ -292,6 +293,7 @@ export function installServiceTrust(env, now = new Date()) {
|
|
|
292
293
|
const plan = trustStoreOperation(() => resolveTrustInstallPlan(runtime.caPath, env ?? process.env));
|
|
293
294
|
const existing = readServiceTrustRecord(paths.trustRecordPath, TRUST_SENTINEL);
|
|
294
295
|
if (existing && existing.fingerprintSha256 === plan.fingerprintSha256 && existing.provider === plan.provider && existing.target === plan.target) {
|
|
296
|
+
clearTrustRefreshError(paths.serviceDir);
|
|
295
297
|
const state = {
|
|
296
298
|
...current,
|
|
297
299
|
trustInstalled: true,
|
|
@@ -311,6 +313,7 @@ export function installServiceTrust(env, now = new Date()) {
|
|
|
311
313
|
trustStoreOperation(() => applyTrustUninstall(existing));
|
|
312
314
|
}
|
|
313
315
|
const record = trustStoreOperation(() => applyTrustInstall(plan, now, TRUST_SENTINEL));
|
|
316
|
+
clearTrustRefreshError(paths.serviceDir);
|
|
314
317
|
const state = {
|
|
315
318
|
...current,
|
|
316
319
|
trustInstalled: true,
|
|
@@ -339,6 +342,7 @@ export function uninstallServiceTrust(env, now = new Date()) {
|
|
|
339
342
|
if (record) {
|
|
340
343
|
trustStoreOperation(() => applyTrustUninstall(record));
|
|
341
344
|
}
|
|
345
|
+
clearTrustRefreshError(paths.serviceDir);
|
|
342
346
|
const removedTrust = removeFile(paths.trustRecordPath);
|
|
343
347
|
if (!current.configured && !removedTrust) {
|
|
344
348
|
return {
|
|
@@ -518,31 +522,42 @@ function readState(paths) {
|
|
|
518
522
|
function applyRuntimeDiagnostics(paths, state) {
|
|
519
523
|
const runtime = readRuntime(paths) ?? state.proxy;
|
|
520
524
|
if (!runtime) {
|
|
521
|
-
return state;
|
|
525
|
+
return withTrustRefreshError(paths, state);
|
|
522
526
|
}
|
|
523
527
|
const drift = detectTrustDrift(state.trust, runtime);
|
|
524
528
|
if (!state.running) {
|
|
525
|
-
return {
|
|
529
|
+
return withTrustRefreshError(paths, {
|
|
526
530
|
...state,
|
|
527
531
|
proxy: runtime,
|
|
528
532
|
trustDrift: drift,
|
|
529
533
|
lastError: "stale service runtime state: runtime file exists while service is stopped"
|
|
530
|
-
};
|
|
534
|
+
});
|
|
531
535
|
}
|
|
532
536
|
const health = runtimeHealth(runtime);
|
|
533
537
|
if (!health.healthy) {
|
|
534
|
-
return {
|
|
538
|
+
return withTrustRefreshError(paths, {
|
|
535
539
|
...state,
|
|
536
540
|
running: false,
|
|
537
541
|
proxy: runtime,
|
|
538
542
|
trustDrift: drift,
|
|
539
543
|
lastError: `stale service runtime state: ${health.reason}`
|
|
540
|
-
};
|
|
544
|
+
});
|
|
541
545
|
}
|
|
542
|
-
return {
|
|
546
|
+
return withTrustRefreshError(paths, {
|
|
543
547
|
...state,
|
|
544
548
|
proxy: runtime,
|
|
545
549
|
trustDrift: drift
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function withTrustRefreshError(paths, state) {
|
|
553
|
+
const refreshError = readTrustRefreshError(paths.serviceDir);
|
|
554
|
+
if (!refreshError) {
|
|
555
|
+
return state;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
...state,
|
|
559
|
+
lastError: state.lastError
|
|
560
|
+
?? `service CA trust refresh failed at ${refreshError.at}: ${refreshError.message}. Run 'dg service trust install --yes' to repair OS trust for the rotated CA.`
|
|
546
561
|
};
|
|
547
562
|
}
|
|
548
563
|
function runtimeHealth(runtime) {
|
|
@@ -711,7 +726,7 @@ function cleanupRuntime(paths, runtime) {
|
|
|
711
726
|
if (runtime?.pid && processIsAlive(runtime.pid)) {
|
|
712
727
|
killProcess(runtime.pid);
|
|
713
728
|
}
|
|
714
|
-
if (runtime?.sessionDir &&
|
|
729
|
+
if (runtime?.sessionDir && isContainedIn(paths.paths.sessionsDir, runtime.sessionDir)) {
|
|
715
730
|
rmSync(runtime.sessionDir, {
|
|
716
731
|
force: true,
|
|
717
732
|
recursive: true
|
|
@@ -719,6 +734,10 @@ function cleanupRuntime(paths, runtime) {
|
|
|
719
734
|
}
|
|
720
735
|
removeFile(paths.runtimePath);
|
|
721
736
|
}
|
|
737
|
+
function isContainedIn(parent, child) {
|
|
738
|
+
const related = relative(parent, child);
|
|
739
|
+
return related.length > 0 && !related.startsWith("..") && !isAbsolute(related);
|
|
740
|
+
}
|
|
722
741
|
function processIsAlive(pid) {
|
|
723
742
|
if (pid <= 0) {
|
|
724
743
|
return false;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { applyTrustInstall, applyTrustUninstall, readServiceTrustRecord, resolveTrustInstallPlan, writeServiceTrustRecord } from "./trust-store.js";
|
|
4
|
+
export const TRUST_REFRESH_ERROR_FILENAME = "trust-refresh-error.json";
|
|
5
|
+
export function trustRefreshErrorPath(serviceDir) {
|
|
6
|
+
return join(serviceDir, TRUST_REFRESH_ERROR_FILENAME);
|
|
7
|
+
}
|
|
8
|
+
export function readTrustRefreshError(serviceDir) {
|
|
9
|
+
try {
|
|
10
|
+
const path = trustRefreshErrorPath(serviceDir);
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
if (typeof parsed.at !== "string" || typeof parsed.message !== "string") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
at: parsed.at,
|
|
20
|
+
message: parsed.message
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function clearTrustRefreshError(serviceDir) {
|
|
28
|
+
rmSync(trustRefreshErrorPath(serviceDir), {
|
|
29
|
+
force: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function refreshServiceTrustAfterCaRotation(options) {
|
|
33
|
+
const record = readServiceTrustRecord(options.trustRecordPath, options.sentinel);
|
|
34
|
+
if (!record) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const plan = resolveTrustInstallPlan(options.caPath, options.env);
|
|
39
|
+
applyTrustUninstall(record);
|
|
40
|
+
const next = applyTrustInstall(plan, options.now ?? new Date(), options.sentinel);
|
|
41
|
+
sweepStaleDgTrustFiles(next.provider, next.target);
|
|
42
|
+
writeServiceTrustRecord(options.trustRecordPath, next);
|
|
43
|
+
clearTrustRefreshError(options.serviceDir);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
writeTrustRefreshError(options.serviceDir, error, options.now ?? new Date());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function sweepStaleDgTrustFiles(provider, targetPath) {
|
|
50
|
+
if (provider === "darwin-user-keychain") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const dir = dirname(targetPath);
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!/^dependency-guardian-[0-9a-f]{16}\.(pem|crt)$/.test(entry)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const path = join(dir, entry);
|
|
66
|
+
if (path === targetPath) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
rmSync(path, {
|
|
70
|
+
force: true
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeTrustRefreshError(serviceDir, error, now) {
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(serviceDir, {
|
|
77
|
+
recursive: true,
|
|
78
|
+
mode: 0o700
|
|
79
|
+
});
|
|
80
|
+
const record = {
|
|
81
|
+
at: now.toISOString(),
|
|
82
|
+
message: error instanceof Error ? error.message : String(error)
|
|
83
|
+
};
|
|
84
|
+
writeFileSync(trustRefreshErrorPath(serviceDir), `${JSON.stringify(record, null, 2)}\n`, {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
mode: 0o600
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/service/worker.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { startProductionHttpProxy } from "../proxy/server.js";
|
|
4
|
+
import { resolveServicePaths, TRUST_SENTINEL } from "./state.js";
|
|
5
|
+
import { refreshServiceTrustAfterCaRotation } from "./trust-refresh.js";
|
|
4
6
|
const sessionPath = process.argv[2];
|
|
5
7
|
const apiBaseUrl = process.argv[3];
|
|
6
8
|
const runtimePath = process.argv[4];
|
|
@@ -37,11 +39,31 @@ process.on("SIGTERM", () => {
|
|
|
37
39
|
process.on("SIGINT", () => {
|
|
38
40
|
close().finally(() => process.exit(0));
|
|
39
41
|
});
|
|
42
|
+
function exitOnFatal(error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
try {
|
|
45
|
+
process.stderr.write(`dg service worker: unexpected error — ${message}\n`);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// stderr is gone; nothing left to report to.
|
|
49
|
+
}
|
|
50
|
+
close().finally(() => process.exit(1));
|
|
51
|
+
}
|
|
52
|
+
process.on("uncaughtException", exitOnFatal);
|
|
53
|
+
process.on("unhandledRejection", exitOnFatal);
|
|
54
|
+
const servicePaths = resolveServicePaths(process.env);
|
|
40
55
|
proxy = await startProductionHttpProxy({
|
|
41
56
|
session,
|
|
42
57
|
apiBaseUrl: requiredApiBaseUrl,
|
|
43
58
|
classification,
|
|
44
|
-
env: process.env
|
|
59
|
+
env: process.env,
|
|
60
|
+
onCaRotate: () => refreshServiceTrustAfterCaRotation({
|
|
61
|
+
serviceDir: servicePaths.serviceDir,
|
|
62
|
+
trustRecordPath: servicePaths.trustRecordPath,
|
|
63
|
+
sentinel: TRUST_SENTINEL,
|
|
64
|
+
caPath: session.files.ca,
|
|
65
|
+
env: process.env
|
|
66
|
+
})
|
|
45
67
|
});
|
|
46
68
|
healthServer = await startHealthServer(proxy.port);
|
|
47
69
|
const healthAddress = healthServer.address();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { isCiEnv } from "../presentation/mode.js";
|
|
4
|
+
export function activationOffer(options = {}) {
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
if (env.DG_ACTIVATED_SHELL) {
|
|
7
|
+
return "none";
|
|
8
|
+
}
|
|
9
|
+
const stdin = options.stdin ?? process.stdin;
|
|
10
|
+
const stdout = options.stdout ?? process.stdout;
|
|
11
|
+
if (!stdin.isTTY || !stdout.isTTY || isCiEnv(env)) {
|
|
12
|
+
return "hint";
|
|
13
|
+
}
|
|
14
|
+
return "prompt";
|
|
15
|
+
}
|
|
16
|
+
// dg cannot mutate the parent shell's environment, so activation nests a protected interactive child shell.
|
|
17
|
+
export function activateShell(options = {}) {
|
|
18
|
+
const env = options.env ?? process.env;
|
|
19
|
+
const spawner = options.spawner ?? defaultShellSpawner;
|
|
20
|
+
const binary = env.SHELL || "/bin/sh";
|
|
21
|
+
const name = basename(binary);
|
|
22
|
+
return spawner({
|
|
23
|
+
binary,
|
|
24
|
+
args: name === "bash" || name === "zsh" ? ["-i"] : [],
|
|
25
|
+
env: { ...env, DG_ACTIVATED_SHELL: "1" }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const defaultShellSpawner = (request) => spawnSync(request.binary, request.args, { stdio: "inherit", env: request.env }).status ?? 0;
|
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,13 +1,17 @@
|
|
|
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";
|
|
8
11
|
import { compareVersions, readLatestVersion } from "../commands/update.js";
|
|
9
12
|
import { AuthError, authStatus, displayTier, readAuthState } from "../auth/store.js";
|
|
10
13
|
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
14
|
+
import { describeCooldownSettings } from "../policy/cooldown.js";
|
|
11
15
|
import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
|
|
12
16
|
import { packageManagerNames } from "../launcher/classify.js";
|
|
13
17
|
import { readServiceState } from "../service/state.js";
|
|
@@ -27,6 +31,7 @@ const LEGACY_RC_CANDIDATES = [".zshrc", ".bashrc", ".bash_profile", ".profile",
|
|
|
27
31
|
const LEGACY_PYTHON_HOOK_PY = "dg_pip_hook.py";
|
|
28
32
|
const LEGACY_PYTHON_HOOK_PTH = "dg_pip_hook.pth";
|
|
29
33
|
const LEGACY_PYTHON_HOOK_MARKER = "Dependency Guardian pip-install interceptor";
|
|
34
|
+
const LEGACY_PYTHON_HOOK_PTH_MARKER = "import dg_pip_hook";
|
|
30
35
|
export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
|
|
31
36
|
export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
32
37
|
export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
|
|
@@ -42,7 +47,7 @@ const DOCTOR_GROUP_BY_NAME = {
|
|
|
42
47
|
"cleanup-registry-stale-entries": "setup",
|
|
43
48
|
config: "setup",
|
|
44
49
|
policy: "setup",
|
|
45
|
-
|
|
50
|
+
"script-gate": "setup",
|
|
46
51
|
shims: "setup",
|
|
47
52
|
"shell-rc": "setup",
|
|
48
53
|
"python-hook-drift": "setup",
|
|
@@ -62,7 +67,6 @@ const DOCTOR_FIX_BY_NAME = {
|
|
|
62
67
|
"cleanup-registry-stale-entries": "re-run dg setup to refresh",
|
|
63
68
|
config: "fix or remove ~/.dg, then re-run dg setup",
|
|
64
69
|
policy: "fix ~/.dg config",
|
|
65
|
-
telemetry: "fix ~/.dg config",
|
|
66
70
|
shims: "dg setup",
|
|
67
71
|
"shell-rc": "dg setup",
|
|
68
72
|
"python-hook-drift": "dg uninstall, or re-run dg setup, to remove the stale pip hook",
|
|
@@ -154,7 +158,7 @@ export function applySetupPlan(plan, now = new Date()) {
|
|
|
154
158
|
recursive: true,
|
|
155
159
|
mode: 0o700
|
|
156
160
|
});
|
|
157
|
-
|
|
161
|
+
writeRcFileAtomic(plan.rcPath, withRcBlock(readText(plan.rcPath), plan));
|
|
158
162
|
entries.push(cleanupEntry("rc", plan.rcPath, "mode1", now, RC_SENTINEL));
|
|
159
163
|
sweepLegacyPythonHooks(plan.paths.homeDir, [], []);
|
|
160
164
|
const registry = withRegistryLock(plan.paths, () => {
|
|
@@ -211,6 +215,9 @@ export function uninstallSetup(options) {
|
|
|
211
215
|
else if (entry.kind === "git-hook") {
|
|
212
216
|
reverseGitHookEntry(entry, removed, missing, warnings);
|
|
213
217
|
}
|
|
218
|
+
else if (entry.kind === "agent-hook") {
|
|
219
|
+
reverseAgentHookEntry(entry, removed, missing, warnings);
|
|
220
|
+
}
|
|
214
221
|
}
|
|
215
222
|
sweepLegacyRcBlocks(paths.homeDir, removed, warnings);
|
|
216
223
|
sweepLegacyPythonHooks(paths.homeDir, removed, warnings);
|
|
@@ -275,7 +282,7 @@ export function doctorReport(options = {}) {
|
|
|
275
282
|
checks.push(configCheck(paths, env));
|
|
276
283
|
checks.push(authCheck(env));
|
|
277
284
|
checks.push(policyCheck(env));
|
|
278
|
-
checks.push(
|
|
285
|
+
checks.push(scriptGateCheck(env));
|
|
279
286
|
const missingShims = SHIM_COMMANDS.filter((command) => !validShim(join(shimDir, command), command));
|
|
280
287
|
checks.push({
|
|
281
288
|
name: "shims",
|
|
@@ -565,6 +572,9 @@ function shellRcPath(homeDir, shell) {
|
|
|
565
572
|
}
|
|
566
573
|
return join(homeDir, ".zshrc");
|
|
567
574
|
}
|
|
575
|
+
export function tildifyPath(path, home = homedir()) {
|
|
576
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
577
|
+
}
|
|
568
578
|
export function activationCommand(shell, rcDisplay) {
|
|
569
579
|
if (shell === "fish") {
|
|
570
580
|
return `source ${rcDisplay}`;
|
|
@@ -644,6 +654,26 @@ function fishRcBlock(shimDir) {
|
|
|
644
654
|
function stripRcBlock(existing) {
|
|
645
655
|
return stripRcBlockDetailed(existing).content;
|
|
646
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
|
+
}
|
|
647
677
|
// An unterminated begin marker must never strip to EOF: only lines verifiably
|
|
648
678
|
// written by dg are removed, so user content below a stale marker survives.
|
|
649
679
|
function stripRcBlockDetailed(existing) {
|
|
@@ -693,7 +723,7 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
|
|
|
693
723
|
continue;
|
|
694
724
|
}
|
|
695
725
|
try {
|
|
696
|
-
|
|
726
|
+
writeRcFileAtomic(rcPath, next);
|
|
697
727
|
removed.push(`${rcPath} (legacy dg block)`);
|
|
698
728
|
}
|
|
699
729
|
catch (error) {
|
|
@@ -701,19 +731,23 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
|
|
|
701
731
|
}
|
|
702
732
|
}
|
|
703
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
|
+
}
|
|
704
738
|
export function legacyPythonHookSites(homeDir) {
|
|
705
|
-
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));
|
|
706
740
|
}
|
|
707
741
|
export function sweepLegacyPythonHooks(homeDir, removed, warnings) {
|
|
708
742
|
for (const dir of candidateSitePackagesDirs(homeDir)) {
|
|
709
743
|
const pyPath = join(dir, LEGACY_PYTHON_HOOK_PY);
|
|
710
744
|
const pthPath = join(dir, LEGACY_PYTHON_HOOK_PTH);
|
|
711
745
|
const pyIsHook = readText(pyPath).includes(LEGACY_PYTHON_HOOK_MARKER);
|
|
712
|
-
const
|
|
713
|
-
if (!pyIsHook && !
|
|
746
|
+
const pthIsHook = isLegacyPthHook(pthPath);
|
|
747
|
+
if (!pyIsHook && !pthIsHook) {
|
|
714
748
|
continue;
|
|
715
749
|
}
|
|
716
|
-
if (
|
|
750
|
+
if (pthIsHook) {
|
|
717
751
|
removePythonHookFile(pthPath, removed, warnings);
|
|
718
752
|
}
|
|
719
753
|
if (pyIsHook) {
|
|
@@ -870,7 +904,7 @@ function dgPathCheck(env) {
|
|
|
870
904
|
};
|
|
871
905
|
}
|
|
872
906
|
function currentDgBinaryPath(env) {
|
|
873
|
-
const explicit = env.DG_TEST_CURRENT_DG_PATH;
|
|
907
|
+
const explicit = env.NODE_ENV === "test" ? env.DG_TEST_CURRENT_DG_PATH : undefined;
|
|
874
908
|
if (explicit) {
|
|
875
909
|
return explicit;
|
|
876
910
|
}
|
|
@@ -1090,7 +1124,7 @@ function policyCheck(env) {
|
|
|
1090
1124
|
return {
|
|
1091
1125
|
name: "policy",
|
|
1092
1126
|
status: "pass",
|
|
1093
|
-
message: `Local policy mode ${config.policy.mode}; project allowlists trusted: ${config.policy.trustProjectAllowlists}`
|
|
1127
|
+
message: `Local policy mode ${config.policy.mode}; project allowlists trusted: ${config.policy.trustProjectAllowlists}; release cooldown ${describeCooldownSettings(config, env)}`
|
|
1094
1128
|
};
|
|
1095
1129
|
}
|
|
1096
1130
|
catch (error) {
|
|
@@ -1101,20 +1135,35 @@ function policyCheck(env) {
|
|
|
1101
1135
|
};
|
|
1102
1136
|
}
|
|
1103
1137
|
}
|
|
1104
|
-
function
|
|
1138
|
+
function scriptGateCheck(env) {
|
|
1105
1139
|
try {
|
|
1106
|
-
const
|
|
1140
|
+
const mode = loadUserConfig(env).scriptGate.mode;
|
|
1141
|
+
if (mode === "off") {
|
|
1142
|
+
return {
|
|
1143
|
+
name: "script-gate",
|
|
1144
|
+
status: "pass",
|
|
1145
|
+
message: "Install-script gate is off; protected installs do not report script-running packages"
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
if (mode === "enforce") {
|
|
1149
|
+
return {
|
|
1150
|
+
name: "script-gate",
|
|
1151
|
+
status: "warn",
|
|
1152
|
+
message: "scriptGate.mode is enforce, but this dg release observes only — installs report script-running packages without blocking",
|
|
1153
|
+
fix: "enforcement ships in an upcoming release; dg config set scriptGate.mode observe clears this warning"
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1107
1156
|
return {
|
|
1108
|
-
name: "
|
|
1157
|
+
name: "script-gate",
|
|
1109
1158
|
status: "pass",
|
|
1110
|
-
message:
|
|
1159
|
+
message: "Install-script gate observes: protected npm/yarn installs report packages that ran install scripts (pnpm blocks them natively)"
|
|
1111
1160
|
};
|
|
1112
1161
|
}
|
|
1113
1162
|
catch (error) {
|
|
1114
1163
|
return {
|
|
1115
|
-
name: "
|
|
1164
|
+
name: "script-gate",
|
|
1116
1165
|
status: "fail",
|
|
1117
|
-
message: error instanceof ConfigError ? error.message : "Unable to read
|
|
1166
|
+
message: error instanceof ConfigError ? error.message : "Unable to read script gate config"
|
|
1118
1167
|
};
|
|
1119
1168
|
}
|
|
1120
1169
|
}
|
|
@@ -1123,12 +1172,19 @@ export function writeRegistry(paths, registry) {
|
|
|
1123
1172
|
recursive: true,
|
|
1124
1173
|
mode: 0o700
|
|
1125
1174
|
});
|
|
1126
|
-
const tempPath = `${paths.cleanupRegistryPath}.${
|
|
1175
|
+
const tempPath = `${paths.cleanupRegistryPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1127
1176
|
writeFileSync(tempPath, `${JSON.stringify(registry, null, 2)}\n`, {
|
|
1128
1177
|
encoding: "utf8",
|
|
1129
|
-
mode: 0o600
|
|
1178
|
+
mode: 0o600,
|
|
1179
|
+
flag: "wx"
|
|
1130
1180
|
});
|
|
1131
|
-
|
|
1181
|
+
try {
|
|
1182
|
+
renameSync(tempPath, paths.cleanupRegistryPath);
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
rmSync(tempPath, { force: true });
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1132
1188
|
}
|
|
1133
1189
|
function withRegistryLock(paths, action) {
|
|
1134
1190
|
const lock = acquireLockSync(paths, CLEANUP_REGISTRY_LOCK, {
|
|
@@ -1169,7 +1225,13 @@ function removeRcBlock(entry, removed, missing, warnings) {
|
|
|
1169
1225
|
return;
|
|
1170
1226
|
}
|
|
1171
1227
|
const stripped = stripRcBlockDetailed(existing);
|
|
1172
|
-
|
|
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
|
+
}
|
|
1173
1235
|
if (stripped.repairedLines.length > 0) {
|
|
1174
1236
|
warnings.push(`repaired an unterminated dg block in ${entry.path}: removed only dg-written lines (${stripped.repairedLines.join(", ")})`);
|
|
1175
1237
|
}
|
|
@@ -1179,28 +1241,35 @@ function removeRcBlock(entry, removed, missing, warnings) {
|
|
|
1179
1241
|
// a one-line notice and lets the commit (and any chained hook) proceed instead
|
|
1180
1242
|
// of blocking every commit with exit 127.
|
|
1181
1243
|
export function guardHookScript(dgPath, chainedOriginal) {
|
|
1244
|
+
const dg = escapeDoubleQuotedSh(dgPath);
|
|
1182
1245
|
const lines = [
|
|
1183
1246
|
"#!/bin/sh",
|
|
1184
1247
|
`# ${GUARD_HOOK_SENTINEL}`,
|
|
1185
|
-
`if command -v "${
|
|
1186
|
-
` "${
|
|
1248
|
+
`if command -v "${dg}" >/dev/null 2>&1; then`,
|
|
1249
|
+
` "${dg}" scan --staged --hook || exit $?`,
|
|
1187
1250
|
"else",
|
|
1188
|
-
` 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`,
|
|
1189
1252
|
"fi"
|
|
1190
1253
|
];
|
|
1191
1254
|
if (chainedOriginal) {
|
|
1192
|
-
|
|
1255
|
+
const chained = escapeDoubleQuotedSh(chainedOriginal);
|
|
1256
|
+
lines.push(`[ -x "${chained}" ] && exec "${chained}" "$@"`);
|
|
1193
1257
|
}
|
|
1194
1258
|
lines.push("exit 0");
|
|
1195
1259
|
return `${lines.join("\n")}\n`;
|
|
1196
1260
|
}
|
|
1261
|
+
function unescapeDoubleQuotedSh(value) {
|
|
1262
|
+
return value.replace(/\\([\\"$`])/g, "$1");
|
|
1263
|
+
}
|
|
1197
1264
|
export function chainedHookOriginal(content) {
|
|
1198
|
-
|
|
1265
|
+
const matched = content.match(/^\[ -x "(.+)" \] && exec "\1" "\$@"$/m)?.[1];
|
|
1266
|
+
return matched ? unescapeDoubleQuotedSh(matched) : null;
|
|
1199
1267
|
}
|
|
1200
1268
|
export function guardHookDgPath(content) {
|
|
1201
|
-
|
|
1269
|
+
const matched = content.match(/^if command -v "(.+)" >\/dev\/null 2>&1; then$/m)?.[1] ??
|
|
1202
1270
|
content.match(/^"(.+)" scan --staged --hook/m)?.[1] ??
|
|
1203
|
-
null
|
|
1271
|
+
null;
|
|
1272
|
+
return matched ? unescapeDoubleQuotedSh(matched) : null;
|
|
1204
1273
|
}
|
|
1205
1274
|
function commitGuardCheck(registry, env) {
|
|
1206
1275
|
const hooks = registry.entries.filter((entry) => entry.owner === "dg" && entry.kind === "git-hook");
|