@westbayberry/dg 1.3.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -201
- package/NOTICE +1 -4
- package/README.md +293 -0
- package/dist/api/analyze.js +210 -0
- package/dist/audit/deep.js +180 -0
- package/dist/audit/detectors.js +247 -0
- package/dist/audit/events.js +41 -0
- package/dist/audit/rules.js +426 -0
- package/dist/audit-ui/AuditApp.js +39 -0
- package/dist/audit-ui/components/AuditHeader.js +24 -0
- package/dist/audit-ui/components/AuditResultsView.js +307 -0
- package/dist/audit-ui/components/DeepStatusRow.js +11 -0
- package/dist/audit-ui/export.js +85 -0
- package/dist/audit-ui/format.js +34 -0
- package/dist/audit-ui/launch.js +34 -0
- package/dist/auth/device-login.js +271 -0
- package/dist/auth/env-token.js +6 -0
- package/dist/auth/login-app.js +156 -0
- package/dist/auth/store.js +147 -0
- package/dist/bin/dg.js +71 -0
- package/dist/commands/audit.js +357 -0
- package/dist/commands/completion.js +116 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/doctor.js +39 -0
- package/dist/commands/explain.js +100 -0
- package/dist/commands/guard-commit.js +158 -0
- package/dist/commands/help.js +74 -0
- package/dist/commands/licenses.js +435 -0
- package/dist/commands/login.js +81 -0
- package/dist/commands/logout.js +37 -0
- package/dist/commands/router.js +98 -0
- package/dist/commands/scan.js +18 -0
- package/dist/commands/service.js +475 -0
- package/dist/commands/setup.js +302 -0
- package/dist/commands/status.js +115 -0
- package/dist/commands/suggest.js +35 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/unavailable.js +11 -0
- package/dist/commands/uninstall.js +111 -0
- package/dist/commands/update.js +210 -0
- package/dist/commands/verify.js +151 -0
- package/dist/commands/version.js +22 -0
- package/dist/commands/wrap.js +55 -0
- package/dist/config/settings.js +302 -0
- package/dist/install-ui/LiveInstall.js +24 -0
- package/dist/install-ui/block-render.js +83 -0
- package/dist/install-ui/live-install-app.js +48 -0
- package/dist/install-ui/prompt.js +24 -0
- package/dist/launcher/classify.js +116 -0
- package/dist/launcher/env.js +53 -0
- package/dist/launcher/live-install.js +50 -0
- package/dist/launcher/output-redaction.js +77 -0
- package/dist/launcher/preflight-prompt.js +139 -0
- package/dist/launcher/resolve-real-binary.js +73 -0
- package/dist/launcher/run.js +417 -0
- package/dist/policy/evaluate.js +128 -0
- package/dist/presentation/mode.js +52 -0
- package/dist/presentation/theme.js +29 -0
- package/dist/proxy/buffer-budget.js +64 -0
- package/dist/proxy/ca.js +126 -0
- package/dist/proxy/classify-host.js +26 -0
- package/dist/proxy/enforcement.js +102 -0
- package/dist/proxy/metadata-map.js +336 -0
- package/dist/proxy/server.js +909 -0
- package/dist/proxy/upstream-proxy.js +102 -0
- package/dist/proxy/worker.js +39 -0
- package/dist/publish-set/collect.js +51 -0
- package/dist/publish-set/no-exec-shell.js +19 -0
- package/dist/publish-set/npm.js +109 -0
- package/dist/publish-set/pack.js +36 -0
- package/dist/publish-set/pypi.js +59 -0
- package/dist/runtime/cli.js +17 -0
- package/dist/runtime/first-run.js +60 -0
- package/dist/runtime/node-version.js +58 -0
- package/dist/runtime/nudges.js +105 -0
- package/dist/scan/analyze-worker.js +21 -0
- package/dist/scan/collect.js +153 -0
- package/dist/scan/command.js +159 -0
- package/dist/scan/discovery.js +209 -0
- package/dist/scan/render.js +240 -0
- package/dist/scan/scanner-report.js +82 -0
- package/dist/scan/staged.js +173 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan-ui/LegacyApp.js +156 -0
- package/dist/scan-ui/alt-screen.js +84 -0
- package/dist/scan-ui/api-aliases.js +1 -0
- package/dist/scan-ui/components/ErrorView.js +23 -0
- package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
- package/dist/scan-ui/components/ProgressBar.js +89 -0
- package/dist/scan-ui/components/ProjectSelector.js +62 -0
- package/dist/scan-ui/components/ScoreHeader.js +20 -0
- package/dist/scan-ui/components/SetupBanner.js +13 -0
- package/dist/scan-ui/components/Spinner.js +4 -0
- package/dist/scan-ui/format-helpers.js +40 -0
- package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
- package/dist/scan-ui/hooks/useScan.js +113 -0
- package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
- package/dist/scan-ui/launch.js +27 -0
- package/dist/scan-ui/logo.js +91 -0
- package/dist/scan-ui/shims.js +30 -0
- package/dist/security/sanitize.js +28 -0
- package/dist/service/state.js +837 -0
- package/dist/service/trust-store.js +234 -0
- package/dist/service/worker.js +88 -0
- package/dist/setup/git-hook.js +244 -0
- package/dist/setup/optional-support.js +58 -0
- package/dist/setup/plan.js +899 -0
- package/dist/state/cleanup-registry.js +60 -0
- package/dist/state/index.js +5 -0
- package/dist/state/locks.js +161 -0
- package/dist/state/paths.js +24 -0
- package/dist/state/sessions.js +170 -0
- package/dist/state/store.js +50 -0
- package/dist/telemetry/events.js +40 -0
- package/dist/util/git.js +20 -0
- package/dist/util/tty-prompt.js +43 -0
- package/dist/verify/local.js +400 -0
- package/dist/verify/package-check.js +240 -0
- package/dist/verify/preflight.js +698 -0
- package/dist/verify/render.js +184 -0
- package/dist/verify/types.js +1 -0
- package/package.json +33 -50
- package/dist/index.mjs +0 -54141
- package/dist/postinstall.mjs +0 -731
- package/dist/python-hook/dg_pip_hook.pth +0 -1
- package/dist/python-hook/dg_pip_hook.py +0 -130
|
@@ -0,0 +1,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
|
+
}
|