@westbayberry/dg 2.0.7 → 2.0.10
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 +17 -12
- package/dist/api/analyze.js +134 -34
- package/dist/audit-ui/export.js +3 -4
- package/dist/auth/device-login.js +13 -9
- package/dist/auth/store.js +43 -26
- package/dist/bin/dg.js +5 -0
- package/dist/commands/audit.js +14 -4
- package/dist/commands/config.js +3 -5
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/explain.js +138 -6
- package/dist/commands/licenses.js +37 -24
- package/dist/commands/login.js +12 -3
- package/dist/commands/logout.js +15 -4
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +76 -24
- package/dist/commands/status.js +38 -4
- package/dist/commands/types.js +1 -0
- package/dist/config/settings.js +102 -22
- package/dist/install-ui/prompt.js +5 -2
- package/dist/launcher/install-preflight.js +158 -0
- package/dist/launcher/live-install.js +11 -2
- package/dist/launcher/output-redaction.js +5 -3
- package/dist/launcher/pip-report.js +18 -2
- package/dist/launcher/preflight-prompt.js +31 -12
- package/dist/launcher/run.js +87 -8
- package/dist/proxy/ca.js +69 -29
- package/dist/proxy/enforcement.js +41 -3
- package/dist/proxy/worker.js +21 -9
- package/dist/runtime/first-run.js +33 -2
- package/dist/runtime/nudges.js +9 -2
- package/dist/scan/analyze-worker.js +18 -8
- package/dist/scan/collect.js +35 -28
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +9 -3
- package/dist/scan/render.js +22 -6
- package/dist/scan/scanner-report.js +89 -12
- package/dist/scan/staged.js +69 -7
- package/dist/scan-ui/LegacyApp.js +10 -48
- package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
- package/dist/scan-ui/components/ProjectSelector.js +3 -3
- package/dist/scan-ui/components/ScoreHeader.js +8 -4
- package/dist/scan-ui/hooks/useScan.js +74 -27
- package/dist/scan-ui/launch.js +18 -4
- package/dist/service/state.js +15 -4
- package/dist/service/trust-store.js +23 -2
- package/dist/setup/git-hook.js +28 -17
- package/dist/setup/plan.js +302 -18
- package/dist/state/cleanup-registry.js +65 -8
- package/dist/state/locks.js +95 -9
- package/dist/state/sessions.js +66 -2
- package/dist/verify/package-check.js +22 -3
- package/dist/verify/preflight.js +328 -170
- package/package.json +1 -1
|
@@ -3,6 +3,9 @@ import { Text, Box } from "ink";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { useTerminalSize } from "../hooks/useTerminalSize.js";
|
|
5
5
|
import { renderLogo } from "../logo.js";
|
|
6
|
+
export const COMPACT_ROWS = 24;
|
|
7
|
+
// Below ~75 cols the side-by-side logo column garbles the header layout.
|
|
8
|
+
export const LOGO_MIN_COLS = 75;
|
|
6
9
|
function scoreColor(score, action) {
|
|
7
10
|
const colorFn = action === "block" ? chalk.red.bold :
|
|
8
11
|
action === "warn" ? chalk.yellow.bold :
|
|
@@ -12,9 +15,10 @@ function scoreColor(score, action) {
|
|
|
12
15
|
}
|
|
13
16
|
export const ScoreHeader = ({ score, action, total, flagged, clean, userStatus, scanUsage, usageNearLimit, }) => {
|
|
14
17
|
const logo = renderLogo(action);
|
|
15
|
-
const { cols } = useTerminalSize();
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
const { cols, rows } = useTerminalSize();
|
|
19
|
+
const compact = rows < COMPACT_ROWS;
|
|
20
|
+
const showLogo = cols >= LOGO_MIN_COLS && !compact;
|
|
21
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: action === "block" ? "red" : action === "warn" ? "yellow" : action === "analysis_incomplete" ? "cyan" : "green", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Text, { bold: true, children: ["Dependency Guardian ", userStatus ?? ""] }), !compact && _jsx(Text, { children: " " }), _jsxs(Text, { children: [chalk.dim("Score"), " ", scoreColor(score, action)] }), total !== undefined && (_jsxs(_Fragment, { children: [!compact && _jsx(Text, { children: " " }), _jsxs(Text, { children: [chalk.dim(`${total} package${total !== 1 ? "s" : ""} scanned`), flagged !== undefined && flagged > 0 ? (_jsxs(_Fragment, { children: [" ", chalk.yellow(`${flagged} flagged`), " ", chalk.green(`${clean ?? 0} clean`)] })) : (_jsxs(_Fragment, { children: [" ", chalk.green("all clean")] }))] }), scanUsage && (usageNearLimit
|
|
22
|
+
? _jsxs(Text, { color: "yellow", children: [scanUsage, " \u2191 Pro: westbayberry.com/pricing"] })
|
|
19
23
|
: _jsx(Text, { dimColor: true, children: scanUsage }))] }))] }), showLogo && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: logo.map((line, i) => _jsx(Text, { children: line }, i)) }))] }) }));
|
|
20
24
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { useReducer, useEffect, useRef, useCallback, useState } from "react";
|
|
2
3
|
import { analyzePackages, AnalyzeError, mergeAnalyzeResponses } from "../../api/analyze.js";
|
|
3
4
|
import { collectScanPackages, discoverScanProjectsAsync } from "../../scan/collect.js";
|
|
@@ -37,7 +38,12 @@ function reducer(_state, action) {
|
|
|
37
38
|
export function useScan(config) {
|
|
38
39
|
const [state, dispatch] = useReducer(reducer, { phase: "discovering" });
|
|
39
40
|
const started = useRef(false);
|
|
41
|
+
const abortRef = useRef(null);
|
|
42
|
+
if (abortRef.current === null) {
|
|
43
|
+
abortRef.current = new AbortController();
|
|
44
|
+
}
|
|
40
45
|
const [multiProjects, setMultiProjects] = useState(null);
|
|
46
|
+
useEffect(() => () => abortRef.current?.abort(), []);
|
|
41
47
|
useEffect(() => {
|
|
42
48
|
if (started.current) {
|
|
43
49
|
return;
|
|
@@ -56,11 +62,11 @@ export function useScan(config) {
|
|
|
56
62
|
dispatch({ type: "PROJECTS_FOUND", projects });
|
|
57
63
|
return;
|
|
58
64
|
}
|
|
59
|
-
await scanProjects(projects, dispatch);
|
|
65
|
+
await scanProjects(projects, dispatch, abortRef.current?.signal);
|
|
60
66
|
})();
|
|
61
67
|
}, [config]);
|
|
62
68
|
const scanSelectedProjects = useCallback((projects) => {
|
|
63
|
-
void scanProjects(projects, dispatch);
|
|
69
|
+
void scanProjects(projects, dispatch, abortRef.current?.signal);
|
|
64
70
|
}, []);
|
|
65
71
|
const restartSelection = useCallback(() => {
|
|
66
72
|
if (multiProjects) {
|
|
@@ -73,43 +79,84 @@ export function useScan(config) {
|
|
|
73
79
|
restartSelection: multiProjects ? restartSelection : null
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
|
-
async function scanProjects(projects, dispatch) {
|
|
82
|
+
async function scanProjects(projects, dispatch, signal) {
|
|
83
|
+
const startMs = Date.now();
|
|
84
|
+
let skipped = 0;
|
|
85
|
+
let total = 0;
|
|
77
86
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
87
|
+
const collected = collectScanPackages(projects);
|
|
88
|
+
skipped = collected.skipped;
|
|
89
|
+
const entries = [...collected.byEcosystem.entries()];
|
|
90
|
+
total = entries.reduce((sum, [, list]) => sum + list.length, 0);
|
|
80
91
|
if (total === 0) {
|
|
81
92
|
dispatch({ type: "DISCOVERY_EMPTY", message: "No packages to scan." });
|
|
82
93
|
return;
|
|
83
94
|
}
|
|
84
95
|
dispatch({ type: "DISCOVERY_COMPLETE", total });
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
96
|
+
const scanId = randomUUID();
|
|
97
|
+
const progressByEcosystem = new Map(entries.map(([ecosystem]) => [ecosystem, { done: 0, batchIndex: 0, batchCount: 1 }]));
|
|
98
|
+
const reportProgress = () => {
|
|
99
|
+
let done = 0;
|
|
100
|
+
let batchIndex = 0;
|
|
101
|
+
let batchCount = 0;
|
|
102
|
+
for (const progress of progressByEcosystem.values()) {
|
|
103
|
+
done += progress.done;
|
|
104
|
+
batchIndex += progress.batchIndex;
|
|
105
|
+
batchCount += progress.batchCount;
|
|
106
|
+
}
|
|
107
|
+
dispatch({ type: "SCAN_PROGRESS", done, total, batchIndex, batchCount });
|
|
108
|
+
};
|
|
109
|
+
const outcomes = await Promise.all(entries.map(async ([ecosystem, packages]) => {
|
|
110
|
+
try {
|
|
111
|
+
const response = await analyzePackages(packages, {
|
|
112
|
+
ecosystem,
|
|
113
|
+
scanId,
|
|
114
|
+
...(signal ? { signal } : {}),
|
|
115
|
+
onProgress: (progress) => {
|
|
116
|
+
progressByEcosystem.set(ecosystem, {
|
|
117
|
+
done: progress.done,
|
|
118
|
+
batchIndex: progress.batchIndex,
|
|
119
|
+
batchCount: progress.batchCount
|
|
120
|
+
});
|
|
121
|
+
reportProgress();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return { response };
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return { error };
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
if (signal?.aborted) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const responses = outcomes.flatMap((outcome) => ("response" in outcome ? [outcome.response] : []));
|
|
134
|
+
const firstFailure = outcomes.find((outcome) => "error" in outcome);
|
|
135
|
+
if (responses.length > 0) {
|
|
136
|
+
const merged = mergeAnalyzeResponses(responses);
|
|
137
|
+
dispatch({
|
|
138
|
+
type: "SCAN_COMPLETE",
|
|
139
|
+
result: firstFailure && merged.action === "pass" ? { ...merged, action: "analysis_incomplete" } : merged,
|
|
140
|
+
durationMs: Date.now() - startMs,
|
|
141
|
+
skippedCount: skipped,
|
|
142
|
+
discoveredTotal: total
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (firstFailure) {
|
|
147
|
+
throw firstFailure.error;
|
|
97
148
|
}
|
|
98
|
-
dispatch({
|
|
99
|
-
type: "SCAN_COMPLETE",
|
|
100
|
-
result: mergeAnalyzeResponses(responses),
|
|
101
|
-
durationMs: Date.now() - startMs,
|
|
102
|
-
skippedCount: skipped,
|
|
103
|
-
discoveredTotal: total
|
|
104
|
-
});
|
|
105
149
|
}
|
|
106
150
|
catch (error) {
|
|
107
|
-
if (
|
|
151
|
+
if (signal?.aborted) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (error instanceof AnalyzeError && error.code === "quota_exceeded") {
|
|
108
155
|
const body = (error.body ?? {});
|
|
109
156
|
dispatch({
|
|
110
157
|
type: "FREE_CAP_REACHED",
|
|
111
|
-
scansUsed:
|
|
112
|
-
maxScans:
|
|
158
|
+
scansUsed: error.scansUsed ?? 0,
|
|
159
|
+
maxScans: error.scansLimit ?? 0,
|
|
113
160
|
capReason: body.reason === "prefix_cap" ? "prefix_cap" : "monthly_limit",
|
|
114
161
|
...(typeof body.resetsAt === "string" ? { resetsAt: body.resetsAt } : {})
|
|
115
162
|
});
|
package/dist/scan-ui/launch.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { loadUserConfig } from "../config/settings.js";
|
|
2
2
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
3
|
+
import { pendingUpdate } from "../runtime/nudges.js";
|
|
4
|
+
import { enterTui, leaveTui } from "./alt-screen.js";
|
|
3
5
|
export function shouldLaunchScanTui(options) {
|
|
4
6
|
if (options.format !== "text" || options.outputPath) {
|
|
5
7
|
return false;
|
|
@@ -7,6 +9,9 @@ export function shouldLaunchScanTui(options) {
|
|
|
7
9
|
if (options.targetPath !== ".") {
|
|
8
10
|
return false;
|
|
9
11
|
}
|
|
12
|
+
if (process.env.TERM === "dumb") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
10
15
|
return resolvePresentation().mode === "rich";
|
|
11
16
|
}
|
|
12
17
|
export async function launchScanTui(initialView = "results") {
|
|
@@ -19,9 +24,18 @@ export async function launchScanTui(initialView = "results") {
|
|
|
19
24
|
import("react"),
|
|
20
25
|
import("./LegacyApp.js")
|
|
21
26
|
]);
|
|
22
|
-
const
|
|
23
|
-
const mode = policyMode === "off" ? "off" : policyMode === "warn" ? "warn" : "block";
|
|
27
|
+
const mode = loadUserConfig().policy.mode;
|
|
24
28
|
const config = { mode };
|
|
25
|
-
const
|
|
26
|
-
|
|
29
|
+
const update = pendingUpdate();
|
|
30
|
+
const updateAvailable = update
|
|
31
|
+
? `Update available: ${update.current} → ${update.latest} · run dg update`
|
|
32
|
+
: undefined;
|
|
33
|
+
enterTui();
|
|
34
|
+
try {
|
|
35
|
+
const instance = render(react.default.createElement(app.App, { config, initialView, updateAvailable }), { exitOnCtrlC: true });
|
|
36
|
+
await instance.waitUntilExit();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
leaveTui();
|
|
40
|
+
}
|
|
27
41
|
}
|
package/dist/service/state.js
CHANGED
|
@@ -4,8 +4,8 @@ import { dirname, join } 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
|
-
import { applyTrustInstall, applyTrustUninstall, readCertificateFingerprints, readServiceTrustRecord, renderTrustStorePlanLines, resolveTrustInstallPlan, TrustStoreError, writeServiceTrustRecord } from "./trust-store.js";
|
|
8
|
-
import { acquireLockSync, cleanupSessionSync, createSessionSync, resolveDgPaths, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
7
|
+
import { applyTrustInstall, applyTrustUninstall, readCertificateFingerprints, readServiceTrustRecord, renderTrustStorePlanLines, resolveTrustInstallPlan, TrustStoreError, TrustToolMissingError, writeServiceTrustRecord } from "./trust-store.js";
|
|
8
|
+
import { acquireLockSync, cleanupSessionSync, createSessionSync, preserveCorruptCleanupRegistrySync, resolveDgPaths, CLEANUP_REGISTRY_LOCK } from "../state/index.js";
|
|
9
9
|
export const SERVICE_SENTINEL = "dg-service-mode-v1";
|
|
10
10
|
export const TRUST_SENTINEL = "dg-service-trust-v1";
|
|
11
11
|
export const SERVICE_LOCK = "service-control";
|
|
@@ -68,7 +68,7 @@ export function buildTrustInstallPlan(env) {
|
|
|
68
68
|
writes: [
|
|
69
69
|
...renderTrustStorePlanLines(trustPlan).map((line) => ({
|
|
70
70
|
action: line,
|
|
71
|
-
path:
|
|
71
|
+
path: ""
|
|
72
72
|
})),
|
|
73
73
|
{
|
|
74
74
|
action: "write dg-owned managed trust record after trust-store mutation succeeds",
|
|
@@ -135,7 +135,7 @@ export function renderServicePlan(title, plan) {
|
|
|
135
135
|
title,
|
|
136
136
|
"",
|
|
137
137
|
"No service or trust-store state is changed until this plan is confirmed.",
|
|
138
|
-
...plan.writes.map((write) => `- ${write.action}: ${write.path}`)
|
|
138
|
+
...plan.writes.map((write) => (write.path ? `- ${write.action}: ${write.path}` : `- ${write.action}`))
|
|
139
139
|
];
|
|
140
140
|
return `${lines.join("\n")}\n`;
|
|
141
141
|
}
|
|
@@ -435,6 +435,13 @@ export class ServiceTrustStoreError extends Error {
|
|
|
435
435
|
super(message);
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
|
+
export class ServiceTrustToolMissingError extends ServiceTrustStoreError {
|
|
439
|
+
tool;
|
|
440
|
+
constructor(tool) {
|
|
441
|
+
super(`native trust tool '${tool}' is not available on this system`);
|
|
442
|
+
this.tool = tool;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
438
445
|
export class ServiceProxyError extends Error {
|
|
439
446
|
constructor(message) {
|
|
440
447
|
super(message);
|
|
@@ -446,6 +453,9 @@ function trustStoreOperation(operation) {
|
|
|
446
453
|
return operation();
|
|
447
454
|
}
|
|
448
455
|
catch (error) {
|
|
456
|
+
if (error instanceof TrustToolMissingError) {
|
|
457
|
+
throw new ServiceTrustToolMissingError(error.tool);
|
|
458
|
+
}
|
|
449
459
|
if (error instanceof TrustStoreError) {
|
|
450
460
|
throw new ServiceTrustStoreError(error.message);
|
|
451
461
|
}
|
|
@@ -782,6 +792,7 @@ function readRegistry(paths) {
|
|
|
782
792
|
return parsed;
|
|
783
793
|
}
|
|
784
794
|
catch {
|
|
795
|
+
preserveCorruptCleanupRegistrySync(paths);
|
|
785
796
|
return {
|
|
786
797
|
version: 1,
|
|
787
798
|
entries: []
|
|
@@ -8,6 +8,13 @@ export class TrustStoreError extends Error {
|
|
|
8
8
|
super(message);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
export class TrustToolMissingError extends TrustStoreError {
|
|
12
|
+
tool;
|
|
13
|
+
constructor(tool) {
|
|
14
|
+
super(`native trust tool '${tool}' is not available on this system`);
|
|
15
|
+
this.tool = tool;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
11
18
|
export function resolveTrustInstallPlan(caPath, env = process.env, platform = process.platform) {
|
|
12
19
|
const cert = readCertificateInfo(caPath);
|
|
13
20
|
const backend = env.DG_SERVICE_TRUST_STORE_BACKEND ?? "native";
|
|
@@ -104,7 +111,15 @@ export function applyTrustInstall(plan, installedAt, sentinel) {
|
|
|
104
111
|
mode: 0o755
|
|
105
112
|
});
|
|
106
113
|
copyFileSync(plan.caPath, plan.target);
|
|
107
|
-
|
|
114
|
+
try {
|
|
115
|
+
runNativeCommand(["update-ca-certificates"], "Linux trust-store refresh failed");
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
rmSync(plan.target, {
|
|
119
|
+
force: true
|
|
120
|
+
});
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
108
123
|
}
|
|
109
124
|
return {
|
|
110
125
|
version: 1,
|
|
@@ -227,8 +242,14 @@ function runNativeCommand(command, failureMessage) {
|
|
|
227
242
|
const result = spawnSync(program, args, {
|
|
228
243
|
encoding: "utf8"
|
|
229
244
|
});
|
|
245
|
+
if (result.error) {
|
|
246
|
+
if (result.error.code === "ENOENT") {
|
|
247
|
+
throw new TrustToolMissingError(program);
|
|
248
|
+
}
|
|
249
|
+
throw new TrustStoreError(`${failureMessage}: ${result.error.message}`);
|
|
250
|
+
}
|
|
230
251
|
if (result.status !== 0) {
|
|
231
|
-
const detail = result.stderr
|
|
252
|
+
const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? "unknown"}`;
|
|
232
253
|
throw new TrustStoreError(`${failureMessage}: ${detail}`);
|
|
233
254
|
}
|
|
234
255
|
}
|
package/dist/setup/git-hook.js
CHANGED
|
@@ -4,7 +4,7 @@ import { 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
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";
|
|
7
|
+
import { GUARD_HOOK_SENTINEL, SETUP_UNINSTALL_LOCK, SETUP_UNINSTALL_LOCK_STALE_MS, chainedHookOriginal, guardHookScript, mergeRegistry, readRegistry, reverseGitHookEntry, writeRegistry } from "./plan.js";
|
|
8
8
|
export { GUARD_HOOK_SENTINEL } from "./plan.js";
|
|
9
9
|
export const GUARD_SELFTEST_ENV = "DG_GUARD_COMMIT_SELFTEST";
|
|
10
10
|
function dgEntrypoint() {
|
|
@@ -48,14 +48,17 @@ export function resolveGitRepo(options = {}) {
|
|
|
48
48
|
env
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
function
|
|
51
|
+
function hookText(path) {
|
|
52
52
|
try {
|
|
53
|
-
return readFileSync(path, "utf8")
|
|
53
|
+
return readFileSync(path, "utf8");
|
|
54
54
|
}
|
|
55
55
|
catch {
|
|
56
56
|
return "";
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
function secondLine(path) {
|
|
60
|
+
return hookText(path).split("\n", 2)[1] ?? "";
|
|
61
|
+
}
|
|
59
62
|
function isManaged(path) {
|
|
60
63
|
return existsSync(path) && secondLine(path).includes(GUARD_HOOK_SENTINEL);
|
|
61
64
|
}
|
|
@@ -77,29 +80,25 @@ export function planGitHook(context) {
|
|
|
77
80
|
const state = gitHookState(context);
|
|
78
81
|
return { context, state, willChain: state === "foreign" };
|
|
79
82
|
}
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
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`;
|
|
83
|
+
function registryOriginal(context) {
|
|
84
|
+
const entry = readRegistry(context.paths).registry.entries.find((candidate) => candidate.kind === "git-hook" && candidate.owner === "dg" && candidate.path === context.hookTarget);
|
|
85
|
+
return entry?.original ?? null;
|
|
91
86
|
}
|
|
92
87
|
export function applyGitHook(context, now = new Date()) {
|
|
93
88
|
const lock = acquireLockSync(context.paths, SETUP_UNINSTALL_LOCK, { staleMs: SETUP_UNINSTALL_LOCK_STALE_MS });
|
|
94
89
|
let chainedOriginal = null;
|
|
95
90
|
try {
|
|
96
91
|
mkdirSync(context.hooksDir, { recursive: true });
|
|
97
|
-
|
|
92
|
+
const state = gitHookState(context);
|
|
93
|
+
if (state === "foreign") {
|
|
98
94
|
const backup = join(context.hooksDir, `pre-commit.dg-chained-${randomBytes(4).toString("hex")}`);
|
|
99
95
|
renameSync(context.hookTarget, backup);
|
|
100
96
|
chainedOriginal = backup;
|
|
101
97
|
}
|
|
102
|
-
|
|
98
|
+
else if (state === "managed") {
|
|
99
|
+
chainedOriginal = chainedHookOriginal(hookText(context.hookTarget)) ?? registryOriginal(context);
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(context.hookTarget, guardHookScript(context.dgPath, chainedOriginal), { encoding: "utf8", mode: 0o755 });
|
|
103
102
|
chmodSync(context.hookTarget, 0o755);
|
|
104
103
|
const entry = {
|
|
105
104
|
kind: "git-hook",
|
|
@@ -191,6 +190,18 @@ function runSelfTest(context) {
|
|
|
191
190
|
detail: `self-test expected exit 2, got ${result.status === null ? "no exit" : result.status}`
|
|
192
191
|
};
|
|
193
192
|
}
|
|
193
|
+
function unregisteredHookEntry(context) {
|
|
194
|
+
const original = chainedHookOriginal(hookText(context.hookTarget));
|
|
195
|
+
return {
|
|
196
|
+
kind: "git-hook",
|
|
197
|
+
path: context.hookTarget,
|
|
198
|
+
mode: "mode1",
|
|
199
|
+
sentinel: GUARD_HOOK_SENTINEL,
|
|
200
|
+
installedAt: "",
|
|
201
|
+
owner: "dg",
|
|
202
|
+
...(original ? { original } : {})
|
|
203
|
+
};
|
|
204
|
+
}
|
|
194
205
|
function isUnderRoot(path, root) {
|
|
195
206
|
const a = resolve(path);
|
|
196
207
|
const b = resolve(root);
|
|
@@ -221,7 +232,7 @@ export function removeGitHookForRepo(context) {
|
|
|
221
232
|
const targets = mine.length > 0
|
|
222
233
|
? mine
|
|
223
234
|
: isManaged(context.hookTarget)
|
|
224
|
-
? [
|
|
235
|
+
? [unregisteredHookEntry(context)]
|
|
225
236
|
: [];
|
|
226
237
|
for (const entry of targets) {
|
|
227
238
|
reverseGitHookEntry(entry, removed, missing, warnings);
|