@westbayberry/dg 1.3.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +1 -201
  2. package/NOTICE +1 -4
  3. package/README.md +293 -0
  4. package/dist/api/analyze.js +210 -0
  5. package/dist/audit/deep.js +180 -0
  6. package/dist/audit/detectors.js +247 -0
  7. package/dist/audit/events.js +41 -0
  8. package/dist/audit/rules.js +426 -0
  9. package/dist/audit-ui/AuditApp.js +39 -0
  10. package/dist/audit-ui/components/AuditHeader.js +24 -0
  11. package/dist/audit-ui/components/AuditResultsView.js +307 -0
  12. package/dist/audit-ui/components/DeepStatusRow.js +11 -0
  13. package/dist/audit-ui/export.js +85 -0
  14. package/dist/audit-ui/format.js +34 -0
  15. package/dist/audit-ui/launch.js +34 -0
  16. package/dist/auth/device-login.js +271 -0
  17. package/dist/auth/env-token.js +6 -0
  18. package/dist/auth/login-app.js +156 -0
  19. package/dist/auth/store.js +147 -0
  20. package/dist/bin/dg.js +71 -0
  21. package/dist/commands/audit.js +357 -0
  22. package/dist/commands/completion.js +116 -0
  23. package/dist/commands/config.js +99 -0
  24. package/dist/commands/doctor.js +39 -0
  25. package/dist/commands/explain.js +100 -0
  26. package/dist/commands/guard-commit.js +158 -0
  27. package/dist/commands/help.js +74 -0
  28. package/dist/commands/licenses.js +435 -0
  29. package/dist/commands/login.js +81 -0
  30. package/dist/commands/logout.js +37 -0
  31. package/dist/commands/router.js +98 -0
  32. package/dist/commands/scan.js +18 -0
  33. package/dist/commands/service.js +475 -0
  34. package/dist/commands/setup.js +302 -0
  35. package/dist/commands/status.js +115 -0
  36. package/dist/commands/suggest.js +35 -0
  37. package/dist/commands/types.js +4 -0
  38. package/dist/commands/unavailable.js +11 -0
  39. package/dist/commands/uninstall.js +111 -0
  40. package/dist/commands/update.js +210 -0
  41. package/dist/commands/verify.js +151 -0
  42. package/dist/commands/version.js +22 -0
  43. package/dist/commands/wrap.js +55 -0
  44. package/dist/config/settings.js +302 -0
  45. package/dist/install-ui/LiveInstall.js +24 -0
  46. package/dist/install-ui/block-render.js +83 -0
  47. package/dist/install-ui/live-install-app.js +48 -0
  48. package/dist/install-ui/prompt.js +24 -0
  49. package/dist/launcher/classify.js +116 -0
  50. package/dist/launcher/env.js +53 -0
  51. package/dist/launcher/live-install.js +50 -0
  52. package/dist/launcher/output-redaction.js +77 -0
  53. package/dist/launcher/preflight-prompt.js +139 -0
  54. package/dist/launcher/resolve-real-binary.js +73 -0
  55. package/dist/launcher/run.js +417 -0
  56. package/dist/policy/evaluate.js +128 -0
  57. package/dist/presentation/mode.js +52 -0
  58. package/dist/presentation/theme.js +29 -0
  59. package/dist/proxy/buffer-budget.js +64 -0
  60. package/dist/proxy/ca.js +126 -0
  61. package/dist/proxy/classify-host.js +26 -0
  62. package/dist/proxy/enforcement.js +102 -0
  63. package/dist/proxy/metadata-map.js +336 -0
  64. package/dist/proxy/server.js +909 -0
  65. package/dist/proxy/upstream-proxy.js +102 -0
  66. package/dist/proxy/worker.js +39 -0
  67. package/dist/publish-set/collect.js +51 -0
  68. package/dist/publish-set/no-exec-shell.js +19 -0
  69. package/dist/publish-set/npm.js +109 -0
  70. package/dist/publish-set/pack.js +36 -0
  71. package/dist/publish-set/pypi.js +59 -0
  72. package/dist/runtime/cli.js +17 -0
  73. package/dist/runtime/first-run.js +60 -0
  74. package/dist/runtime/node-version.js +58 -0
  75. package/dist/runtime/nudges.js +105 -0
  76. package/dist/scan/analyze-worker.js +21 -0
  77. package/dist/scan/collect.js +153 -0
  78. package/dist/scan/command.js +159 -0
  79. package/dist/scan/discovery.js +209 -0
  80. package/dist/scan/render.js +240 -0
  81. package/dist/scan/scanner-report.js +82 -0
  82. package/dist/scan/staged.js +173 -0
  83. package/dist/scan/types.js +1 -0
  84. package/dist/scan-ui/LegacyApp.js +156 -0
  85. package/dist/scan-ui/alt-screen.js +84 -0
  86. package/dist/scan-ui/api-aliases.js +1 -0
  87. package/dist/scan-ui/components/ErrorView.js +23 -0
  88. package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
  89. package/dist/scan-ui/components/ProgressBar.js +89 -0
  90. package/dist/scan-ui/components/ProjectSelector.js +62 -0
  91. package/dist/scan-ui/components/ScoreHeader.js +20 -0
  92. package/dist/scan-ui/components/SetupBanner.js +13 -0
  93. package/dist/scan-ui/components/Spinner.js +4 -0
  94. package/dist/scan-ui/format-helpers.js +40 -0
  95. package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
  96. package/dist/scan-ui/hooks/useScan.js +113 -0
  97. package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
  98. package/dist/scan-ui/launch.js +27 -0
  99. package/dist/scan-ui/logo.js +91 -0
  100. package/dist/scan-ui/shims.js +30 -0
  101. package/dist/security/sanitize.js +28 -0
  102. package/dist/service/state.js +837 -0
  103. package/dist/service/trust-store.js +234 -0
  104. package/dist/service/worker.js +88 -0
  105. package/dist/setup/git-hook.js +244 -0
  106. package/dist/setup/optional-support.js +58 -0
  107. package/dist/setup/plan.js +899 -0
  108. package/dist/state/cleanup-registry.js +60 -0
  109. package/dist/state/index.js +5 -0
  110. package/dist/state/locks.js +161 -0
  111. package/dist/state/paths.js +24 -0
  112. package/dist/state/sessions.js +170 -0
  113. package/dist/state/store.js +50 -0
  114. package/dist/telemetry/events.js +40 -0
  115. package/dist/util/git.js +20 -0
  116. package/dist/util/tty-prompt.js +43 -0
  117. package/dist/verify/local.js +400 -0
  118. package/dist/verify/package-check.js +240 -0
  119. package/dist/verify/preflight.js +698 -0
  120. package/dist/verify/render.js +184 -0
  121. package/dist/verify/types.js +1 -0
  122. package/package.json +33 -50
  123. package/dist/index.mjs +0 -54141
  124. package/dist/postinstall.mjs +0 -731
  125. package/dist/python-hook/dg_pip_hook.pth +0 -1
  126. package/dist/python-hook/dg_pip_hook.py +0 -130
@@ -0,0 +1,234 @@
1
+ import { X509Certificate } from "node:crypto";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ export class TrustStoreError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ }
10
+ }
11
+ export function resolveTrustInstallPlan(caPath, env = process.env, platform = process.platform) {
12
+ const cert = readCertificateInfo(caPath);
13
+ const backend = env.DG_SERVICE_TRUST_STORE_BACKEND ?? "native";
14
+ if (backend === "file") {
15
+ const root = env.DG_SERVICE_TRUST_STORE_DIR;
16
+ if (!root) {
17
+ return unsupportedPlan(caPath, cert, "DG_SERVICE_TRUST_STORE_BACKEND=file requires DG_SERVICE_TRUST_STORE_DIR.");
18
+ }
19
+ const target = join(root, `dependency-guardian-${cert.fingerprintSha256.slice(0, 16)}.pem`);
20
+ return {
21
+ provider: "file",
22
+ supported: true,
23
+ adminRequired: false,
24
+ native: false,
25
+ caPath,
26
+ target,
27
+ fingerprintSha256: cert.fingerprintSha256,
28
+ fingerprintSha1: cert.fingerprintSha1,
29
+ installCommand: ["copy", caPath, target],
30
+ uninstallCommand: ["remove", target],
31
+ reason: undefined
32
+ };
33
+ }
34
+ if (backend !== "native") {
35
+ return unsupportedPlan(caPath, cert, `Unsupported DG_SERVICE_TRUST_STORE_BACKEND '${backend}'.`);
36
+ }
37
+ if (platform === "darwin") {
38
+ const keychain = env.DG_SERVICE_TRUST_KEYCHAIN ?? join(env.HOME ?? homedir(), "Library", "Keychains", "login.keychain-db");
39
+ return {
40
+ provider: "darwin-user-keychain",
41
+ supported: true,
42
+ adminRequired: false,
43
+ native: true,
44
+ caPath,
45
+ target: keychain,
46
+ fingerprintSha256: cert.fingerprintSha256,
47
+ fingerprintSha1: cert.fingerprintSha1,
48
+ installCommand: ["security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", keychain, caPath],
49
+ uninstallCommand: ["security", "delete-certificate", "-Z", cert.fingerprintSha1, keychain],
50
+ reason: undefined
51
+ };
52
+ }
53
+ if (platform === "linux") {
54
+ const target = join("/usr/local/share/ca-certificates", `dependency-guardian-${cert.fingerprintSha256.slice(0, 16)}.crt`);
55
+ const adminRequired = typeof process.getuid === "function" && process.getuid() !== 0;
56
+ return {
57
+ provider: adminRequired ? "unsupported" : "linux-system-ca",
58
+ supported: !adminRequired,
59
+ adminRequired,
60
+ native: true,
61
+ caPath,
62
+ target,
63
+ fingerprintSha256: cert.fingerprintSha256,
64
+ fingerprintSha1: cert.fingerprintSha1,
65
+ installCommand: ["install", "-m", "0644", caPath, target, "&&", "update-ca-certificates"],
66
+ uninstallCommand: ["rm", "-f", target, "&&", "update-ca-certificates"],
67
+ reason: adminRequired ? "Linux system trust-store installation requires admin/root privileges." : undefined
68
+ };
69
+ }
70
+ return unsupportedPlan(caPath, cert, `Native service trust-store mutation is not supported on ${platform} in this build.`);
71
+ }
72
+ export function renderTrustStorePlanLines(plan) {
73
+ if (!plan) {
74
+ return ["active service CA certificate: unavailable; run dg service start before trust install"];
75
+ }
76
+ const lines = [
77
+ `active service CA certificate: ${plan.caPath}`,
78
+ `certificate SHA-256 fingerprint: ${plan.fingerprintSha256}`,
79
+ `trust provider: ${plan.provider}`,
80
+ `trust target: ${plan.target}`,
81
+ `requires admin/root: ${plan.adminRequired ? "yes" : "no"}`,
82
+ `install action: ${plan.installCommand.join(" ")}`,
83
+ `uninstall action: ${plan.uninstallCommand.join(" ")}`
84
+ ];
85
+ return plan.reason ? [...lines, `support note: ${plan.reason}`] : lines;
86
+ }
87
+ export function applyTrustInstall(plan, installedAt, sentinel) {
88
+ if (!plan.supported || plan.provider === "unsupported") {
89
+ throw new TrustStoreError(plan.reason ?? "Service trust-store installation is unsupported on this platform.");
90
+ }
91
+ if (plan.provider === "file") {
92
+ mkdirSync(dirname(plan.target), {
93
+ recursive: true,
94
+ mode: 0o700
95
+ });
96
+ copyFileSync(plan.caPath, plan.target);
97
+ }
98
+ else if (plan.provider === "darwin-user-keychain") {
99
+ runNativeCommand(plan.installCommand, "macOS user keychain trust installation failed");
100
+ }
101
+ else {
102
+ mkdirSync(dirname(plan.target), {
103
+ recursive: true,
104
+ mode: 0o755
105
+ });
106
+ copyFileSync(plan.caPath, plan.target);
107
+ runNativeCommand(["update-ca-certificates"], "Linux trust-store refresh failed");
108
+ }
109
+ return {
110
+ version: 1,
111
+ sentinel,
112
+ installedAt: installedAt.toISOString(),
113
+ scope: plan.native ? "os-user-trust-store" : "ci-file-trust-store",
114
+ provider: plan.provider,
115
+ native: plan.native,
116
+ adminRequired: plan.adminRequired,
117
+ caPath: plan.caPath,
118
+ target: plan.target,
119
+ fingerprintSha256: plan.fingerprintSha256,
120
+ fingerprintSha1: plan.fingerprintSha1
121
+ };
122
+ }
123
+ export function applyTrustUninstall(record) {
124
+ if (record.provider === "file") {
125
+ rmSync(record.target, {
126
+ force: true
127
+ });
128
+ return;
129
+ }
130
+ if (record.provider === "darwin-user-keychain") {
131
+ runNativeCommand(["security", "delete-certificate", "-Z", record.fingerprintSha1, record.target], "macOS user keychain trust removal failed");
132
+ return;
133
+ }
134
+ rmSync(record.target, {
135
+ force: true
136
+ });
137
+ runNativeCommand(["update-ca-certificates"], "Linux trust-store refresh failed");
138
+ }
139
+ export function readServiceTrustRecord(path, sentinel) {
140
+ try {
141
+ if (!existsSync(path)) {
142
+ return undefined;
143
+ }
144
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
145
+ if (parsed.version !== 1 ||
146
+ parsed.sentinel !== sentinel ||
147
+ !isTrustStoreProvider(parsed.provider) ||
148
+ typeof parsed.installedAt !== "string" ||
149
+ typeof parsed.native !== "boolean" ||
150
+ typeof parsed.adminRequired !== "boolean" ||
151
+ typeof parsed.caPath !== "string" ||
152
+ typeof parsed.target !== "string" ||
153
+ typeof parsed.fingerprintSha256 !== "string" ||
154
+ typeof parsed.fingerprintSha1 !== "string") {
155
+ return undefined;
156
+ }
157
+ return {
158
+ version: 1,
159
+ sentinel,
160
+ installedAt: parsed.installedAt,
161
+ scope: parsed.provider === "file" ? "ci-file-trust-store" : "os-user-trust-store",
162
+ provider: parsed.provider,
163
+ native: parsed.native,
164
+ adminRequired: parsed.adminRequired,
165
+ caPath: parsed.caPath,
166
+ target: parsed.target,
167
+ fingerprintSha256: parsed.fingerprintSha256,
168
+ fingerprintSha1: parsed.fingerprintSha1
169
+ };
170
+ }
171
+ catch {
172
+ return undefined;
173
+ }
174
+ }
175
+ export function readCertificateFingerprints(path) {
176
+ return readCertificateInfo(path);
177
+ }
178
+ export function writeServiceTrustRecord(path, record) {
179
+ mkdirSync(dirname(path), {
180
+ recursive: true,
181
+ mode: 0o700
182
+ });
183
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, {
184
+ encoding: "utf8",
185
+ mode: 0o600
186
+ });
187
+ }
188
+ function readCertificateInfo(path) {
189
+ try {
190
+ const certificate = new X509Certificate(readFileSync(path));
191
+ return {
192
+ fingerprintSha256: normalizeFingerprint(certificate.fingerprint256),
193
+ fingerprintSha1: normalizeFingerprint(certificate.fingerprint)
194
+ };
195
+ }
196
+ catch (error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ throw new TrustStoreError(`Cannot read active service CA certificate at ${path}: ${message}`);
199
+ }
200
+ }
201
+ function unsupportedPlan(caPath, cert, reason) {
202
+ return {
203
+ provider: "unsupported",
204
+ supported: false,
205
+ adminRequired: false,
206
+ native: true,
207
+ caPath,
208
+ target: "unsupported",
209
+ fingerprintSha256: cert.fingerprintSha256,
210
+ fingerprintSha1: cert.fingerprintSha1,
211
+ installCommand: [],
212
+ uninstallCommand: [],
213
+ reason
214
+ };
215
+ }
216
+ function normalizeFingerprint(value) {
217
+ return value.replaceAll(":", "").toLowerCase();
218
+ }
219
+ function isTrustStoreProvider(value) {
220
+ return value === "darwin-user-keychain" || value === "linux-system-ca" || value === "file";
221
+ }
222
+ function runNativeCommand(command, failureMessage) {
223
+ const [program, ...args] = command;
224
+ if (!program) {
225
+ throw new TrustStoreError(failureMessage);
226
+ }
227
+ const result = spawnSync(program, args, {
228
+ encoding: "utf8"
229
+ });
230
+ if (result.status !== 0) {
231
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status ?? "unknown"}`;
232
+ throw new TrustStoreError(`${failureMessage}: ${detail}`);
233
+ }
234
+ }
@@ -0,0 +1,88 @@
1
+ import { createServer } from "node:http";
2
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { startProductionHttpProxy } from "../proxy/server.js";
4
+ const sessionPath = process.argv[2];
5
+ const apiBaseUrl = process.argv[3];
6
+ const runtimePath = process.argv[4];
7
+ const classificationJson = process.env.DG_SERVICE_CLASSIFICATION;
8
+ if (!sessionPath || !apiBaseUrl || !runtimePath || !classificationJson) {
9
+ process.stderr.write("dg service worker missing startup arguments\n");
10
+ process.exit(1);
11
+ }
12
+ const requiredSessionPath = sessionPath;
13
+ const requiredApiBaseUrl = apiBaseUrl;
14
+ const requiredRuntimePath = runtimePath;
15
+ const requiredClassificationJson = classificationJson;
16
+ const session = JSON.parse(readFileSync(requiredSessionPath, "utf8"));
17
+ const classification = JSON.parse(requiredClassificationJson);
18
+ let proxy = null;
19
+ let healthServer = null;
20
+ let closed = false;
21
+ async function close() {
22
+ if (closed) {
23
+ return;
24
+ }
25
+ closed = true;
26
+ rmSync(requiredRuntimePath, {
27
+ force: true
28
+ });
29
+ await Promise.all([proxy?.close(), closeHealthServer(healthServer)]);
30
+ }
31
+ process.stdin.on("end", () => {
32
+ close().finally(() => process.exit(0));
33
+ });
34
+ process.on("SIGTERM", () => {
35
+ close().finally(() => process.exit(0));
36
+ });
37
+ process.on("SIGINT", () => {
38
+ close().finally(() => process.exit(0));
39
+ });
40
+ proxy = await startProductionHttpProxy({
41
+ session,
42
+ apiBaseUrl: requiredApiBaseUrl,
43
+ classification,
44
+ env: process.env
45
+ });
46
+ healthServer = await startHealthServer(proxy.port);
47
+ const healthAddress = healthServer.address();
48
+ if (typeof healthAddress !== "object" || healthAddress === null) {
49
+ throw new Error("service health endpoint did not bind a TCP port");
50
+ }
51
+ writeFileSync(requiredRuntimePath, `${JSON.stringify({
52
+ pid: process.pid,
53
+ proxyUrl: `http://127.0.0.1:${proxy.port}`,
54
+ healthUrl: `http://127.0.0.1:${healthAddress.port}/health`,
55
+ sessionDir: session.dir,
56
+ caPath: session.files.ca,
57
+ startedAt: new Date().toISOString()
58
+ }, null, 2)}\n`, {
59
+ encoding: "utf8",
60
+ mode: 0o600
61
+ });
62
+ function startHealthServer(proxyPort) {
63
+ const server = createServer((_request, response) => {
64
+ response.writeHead(200, {
65
+ "Content-Type": "application/json"
66
+ });
67
+ response.end(`${JSON.stringify({
68
+ ok: true,
69
+ pid: process.pid,
70
+ proxyPort
71
+ })}\n`);
72
+ });
73
+ return new Promise((resolve, reject) => {
74
+ server.once("error", reject);
75
+ server.listen(0, "127.0.0.1", () => {
76
+ server.off("error", reject);
77
+ resolve(server);
78
+ });
79
+ });
80
+ }
81
+ function closeHealthServer(server) {
82
+ if (!server) {
83
+ return Promise.resolve();
84
+ }
85
+ return new Promise((resolve) => {
86
+ server.close(() => resolve());
87
+ });
88
+ }
@@ -0,0 +1,244 @@
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";
4
+ import { randomBytes } from "node:crypto";
5
+ import { acquireLockSync, CLEANUP_REGISTRY_LOCK, resolveDgPaths } from "../state/index.js";
6
+ import { gitTrimmed } from "../util/git.js";
7
+ import { GUARD_HOOK_SENTINEL, SETUP_UNINSTALL_LOCK, SETUP_UNINSTALL_LOCK_STALE_MS, mergeRegistry, readRegistry, reverseGitHookEntry, writeRegistry } from "./plan.js";
8
+ export { GUARD_HOOK_SENTINEL } from "./plan.js";
9
+ export const GUARD_SELFTEST_ENV = "DG_GUARD_COMMIT_SELFTEST";
10
+ function dgEntrypoint() {
11
+ const argv1 = process.argv[1];
12
+ return argv1 ? resolve(argv1) : "dg";
13
+ }
14
+ function resolveHooksDir(cwd, env, root) {
15
+ const configured = gitTrimmed(["config", "--get", "core.hooksPath"], { cwd, env });
16
+ if (configured) {
17
+ return { dir: isAbsolute(configured) ? configured : resolve(root, configured), configured: true };
18
+ }
19
+ const gitPath = gitTrimmed(["rev-parse", "--git-path", "hooks"], { cwd, env });
20
+ if (!gitPath) {
21
+ return null;
22
+ }
23
+ return { dir: isAbsolute(gitPath) ? gitPath : resolve(root, gitPath), configured: false };
24
+ }
25
+ export function resolveGitRepo(options = {}) {
26
+ const env = options.env ?? process.env;
27
+ const cwd = options.cwd ?? process.cwd();
28
+ const inside = gitTrimmed(["rev-parse", "--is-inside-work-tree"], { cwd, env });
29
+ if (inside !== "true") {
30
+ return { error: "not a git repository — run dg guard-commit inside a repo" };
31
+ }
32
+ const root = gitTrimmed(["rev-parse", "--show-toplevel"], { cwd, env });
33
+ if (!root) {
34
+ return { error: "could not resolve the repository root" };
35
+ }
36
+ const hooks = resolveHooksDir(cwd, env, root);
37
+ if (!hooks) {
38
+ return { error: "could not resolve the git hooks directory" };
39
+ }
40
+ return {
41
+ cwd,
42
+ root,
43
+ hooksDir: hooks.dir,
44
+ hookTarget: join(hooks.dir, "pre-commit"),
45
+ hooksPathConfigured: hooks.configured,
46
+ dgPath: options.dgPath ?? dgEntrypoint(),
47
+ paths: resolveDgPaths(env),
48
+ env
49
+ };
50
+ }
51
+ function secondLine(path) {
52
+ try {
53
+ return readFileSync(path, "utf8").split("\n", 2)[1] ?? "";
54
+ }
55
+ catch {
56
+ return "";
57
+ }
58
+ }
59
+ function isManaged(path) {
60
+ return existsSync(path) && secondLine(path).includes(GUARD_HOOK_SENTINEL);
61
+ }
62
+ function isExecutable(path) {
63
+ try {
64
+ return (statSync(path).mode & 0o111) !== 0;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ export function gitHookState(context) {
71
+ if (!existsSync(context.hookTarget)) {
72
+ return "fresh";
73
+ }
74
+ return isManaged(context.hookTarget) ? "managed" : "foreign";
75
+ }
76
+ export function planGitHook(context) {
77
+ const state = gitHookState(context);
78
+ return { context, state, willChain: state === "foreign" };
79
+ }
80
+ function hookScript(dgPath, chainedOriginal) {
81
+ const lines = [
82
+ "#!/bin/sh",
83
+ `# ${GUARD_HOOK_SENTINEL}`,
84
+ `"${dgPath}" scan --staged --hook || exit $?`
85
+ ];
86
+ if (chainedOriginal) {
87
+ lines.push(`[ -x "${chainedOriginal}" ] && exec "${chainedOriginal}" "$@"`);
88
+ }
89
+ lines.push("exit 0");
90
+ return `${lines.join("\n")}\n`;
91
+ }
92
+ export function applyGitHook(context, now = new Date()) {
93
+ const lock = acquireLockSync(context.paths, SETUP_UNINSTALL_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
94
+ let chainedOriginal = null;
95
+ try {
96
+ mkdirSync(context.hooksDir, { recursive: true });
97
+ if (gitHookState(context) === "foreign") {
98
+ const backup = join(context.hooksDir, `pre-commit.dg-chained-${randomBytes(4).toString("hex")}`);
99
+ renameSync(context.hookTarget, backup);
100
+ chainedOriginal = backup;
101
+ }
102
+ writeFileSync(context.hookTarget, hookScript(context.dgPath, chainedOriginal), { encoding: "utf8", mode: 0o755 });
103
+ chmodSync(context.hookTarget, 0o755);
104
+ const entry = {
105
+ kind: "git-hook",
106
+ path: context.hookTarget,
107
+ mode: "mode1",
108
+ sentinel: GUARD_HOOK_SENTINEL,
109
+ installedAt: now.toISOString(),
110
+ owner: "dg",
111
+ ...(chainedOriginal ? { original: chainedOriginal } : {})
112
+ };
113
+ const registryLock = acquireLockSync(context.paths, CLEANUP_REGISTRY_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
114
+ try {
115
+ const registry = mergeRegistry(readRegistry(context.paths).registry, [entry]);
116
+ writeRegistry(context.paths, registry);
117
+ }
118
+ finally {
119
+ registryLock.release();
120
+ }
121
+ }
122
+ finally {
123
+ lock.release();
124
+ }
125
+ const checks = verifyGitHook(context);
126
+ return {
127
+ hookTarget: context.hookTarget,
128
+ chainedOriginal,
129
+ checks,
130
+ active: checks.every((check) => check.ok)
131
+ };
132
+ }
133
+ export function verifyGitHook(context) {
134
+ const checks = [];
135
+ const live = resolveHooksDir(context.cwd, context.env, context.root);
136
+ const liveMatches = live !== null && resolve(live.dir) === resolve(context.hooksDir);
137
+ checks.push({
138
+ name: "git-uses-this-dir",
139
+ ok: liveMatches,
140
+ detail: liveMatches
141
+ ? `git runs hooks from ${context.hooksDir}`
142
+ : `git runs hooks from ${live?.dir ?? "an unresolved dir"}, not ${context.hooksDir}`
143
+ });
144
+ const present = existsSync(context.hookTarget);
145
+ checks.push({
146
+ name: "hook-present",
147
+ ok: present,
148
+ detail: present ? `hook written at ${context.hookTarget}` : `no hook at ${context.hookTarget}`
149
+ });
150
+ const sentinel = isManaged(context.hookTarget);
151
+ checks.push({
152
+ name: "dg-owned",
153
+ ok: sentinel,
154
+ detail: sentinel ? "dg sentinel present" : "hook is not dg-owned"
155
+ });
156
+ const exec = isExecutable(context.hookTarget);
157
+ checks.push({
158
+ name: "executable",
159
+ ok: exec,
160
+ detail: exec ? "hook is executable" : "hook is not executable"
161
+ });
162
+ const dgOk = context.dgPath === "dg" || isExecutable(context.dgPath) || existsSync(context.dgPath);
163
+ checks.push({
164
+ name: "dg-runnable",
165
+ ok: dgOk,
166
+ detail: dgOk ? `dg resolves to ${context.dgPath}` : `dg path ${context.dgPath} is not runnable`
167
+ });
168
+ const fires = runSelfTest(context);
169
+ checks.push({
170
+ name: "fires-on-block",
171
+ ok: fires.ok,
172
+ detail: fires.detail
173
+ });
174
+ return checks;
175
+ }
176
+ function runSelfTest(context) {
177
+ if (!existsSync(context.hookTarget) || !isManaged(context.hookTarget)) {
178
+ return { ok: false, detail: "no dg hook to exercise" };
179
+ }
180
+ const result = spawnSync("sh", [context.hookTarget], {
181
+ cwd: context.root,
182
+ env: { ...context.env, [GUARD_SELFTEST_ENV]: "1" },
183
+ encoding: "utf8",
184
+ stdio: "ignore"
185
+ });
186
+ if (result.status === 2) {
187
+ return { ok: true, detail: "synthetic block aborts the commit (exit 2)" };
188
+ }
189
+ return {
190
+ ok: false,
191
+ detail: `self-test expected exit 2, got ${result.status === null ? "no exit" : result.status}`
192
+ };
193
+ }
194
+ function isUnderRoot(path, root) {
195
+ const a = resolve(path);
196
+ const b = resolve(root);
197
+ return a === b || a.startsWith(b + sep);
198
+ }
199
+ export function gitHookStatusState(options = {}) {
200
+ const context = resolveGitRepo(options);
201
+ if ("error" in context) {
202
+ return "not-a-repo";
203
+ }
204
+ if (isManaged(context.hookTarget) && isExecutable(context.hookTarget)) {
205
+ return "active";
206
+ }
207
+ const registry = readRegistry(context.paths).registry;
208
+ const installedHere = registry.entries.some((entry) => entry.kind === "git-hook" && entry.owner === "dg" && isUnderRoot(entry.path, context.root));
209
+ return installedHere ? "dead" : "off";
210
+ }
211
+ export function removeGitHookForRepo(context) {
212
+ const lock = acquireLockSync(context.paths, SETUP_UNINSTALL_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
213
+ const removed = [];
214
+ const missing = [];
215
+ const warnings = [];
216
+ try {
217
+ const registryLock = acquireLockSync(context.paths, CLEANUP_REGISTRY_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
218
+ try {
219
+ const registry = readRegistry(context.paths).registry;
220
+ const mine = registry.entries.filter((entry) => entry.kind === "git-hook" && entry.owner === "dg" && isUnderRoot(entry.path, context.root));
221
+ const targets = mine.length > 0
222
+ ? mine
223
+ : isManaged(context.hookTarget)
224
+ ? [{ kind: "git-hook", path: context.hookTarget, mode: "mode1", sentinel: GUARD_HOOK_SENTINEL, installedAt: "", owner: "dg" }]
225
+ : [];
226
+ for (const entry of targets) {
227
+ reverseGitHookEntry(entry, removed, missing, warnings);
228
+ }
229
+ if (mine.length > 0) {
230
+ writeRegistry(context.paths, {
231
+ version: 1,
232
+ entries: registry.entries.filter((entry) => !mine.includes(entry))
233
+ });
234
+ }
235
+ return { removed, missing, warnings, found: targets.length };
236
+ }
237
+ finally {
238
+ registryLock.release();
239
+ }
240
+ }
241
+ finally {
242
+ lock.release();
243
+ }
244
+ }
@@ -0,0 +1,58 @@
1
+ export const OPTIONAL_SUPPORT_GATES = Object.freeze([
2
+ {
3
+ id: "windows",
4
+ label: "Windows support",
5
+ kind: "platform",
6
+ status: "unclaimed",
7
+ message: "Windows support is gated in this release; use dg prefix mode from a supported POSIX shell or run 'dg --help' for supported commands"
8
+ },
9
+ {
10
+ id: "python-hook",
11
+ label: "Python .pth hook",
12
+ kind: "hook",
13
+ status: "unclaimed",
14
+ message: "Python .pth hook support is gated in this release; use 'dg pip ...', 'dg pipx ...', 'dg uv ...', or 'dg uvx ...' prefix mode instead"
15
+ },
16
+ {
17
+ id: "bun",
18
+ label: "Bun and bunx",
19
+ kind: "package-manager",
20
+ status: "unclaimed",
21
+ standaloneCommand: true,
22
+ message: "Bun support is gated in this release; use 'dg npm ...', 'dg pnpm ...', or 'dg yarn ...' for supported JavaScript installs"
23
+ },
24
+ {
25
+ id: "yarn-berry",
26
+ label: "Yarn Berry",
27
+ kind: "package-manager",
28
+ status: "unclaimed",
29
+ standaloneCommand: false,
30
+ message: "Yarn Berry support is gated in this release; use Yarn classic through 'dg yarn ...' or another supported prefix manager"
31
+ },
32
+ {
33
+ id: "conda",
34
+ label: "Conda",
35
+ kind: "package-manager",
36
+ status: "unclaimed",
37
+ standaloneCommand: true,
38
+ message: "Conda support is gated in this release; use 'dg pip ...' or 'dg uv ...' for supported Python package installs"
39
+ },
40
+ {
41
+ id: "mamba",
42
+ label: "Mamba",
43
+ kind: "package-manager",
44
+ status: "unclaimed",
45
+ standaloneCommand: true,
46
+ message: "Mamba support is gated in this release; use 'dg pip ...' or 'dg uv ...' for supported Python package installs"
47
+ }
48
+ ]);
49
+ export function optionalSupportGate(id) {
50
+ const gate = OPTIONAL_SUPPORT_GATES.find((candidate) => candidate.id === id);
51
+ if (!gate) {
52
+ throw new Error(`unknown optional support gate: ${id}`);
53
+ }
54
+ return gate;
55
+ }
56
+ export function optionalPackageManagerNames() {
57
+ return OPTIONAL_SUPPORT_GATES.filter((gate) => gate.kind === "package-manager" && gate.standaloneCommand === true).map((gate) => gate.id);
58
+ }