@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.
Files changed (53) hide show
  1. package/README.md +17 -12
  2. package/dist/api/analyze.js +134 -34
  3. package/dist/audit-ui/export.js +3 -4
  4. package/dist/auth/device-login.js +13 -9
  5. package/dist/auth/store.js +43 -26
  6. package/dist/bin/dg.js +5 -0
  7. package/dist/commands/audit.js +14 -4
  8. package/dist/commands/config.js +3 -5
  9. package/dist/commands/doctor.js +3 -3
  10. package/dist/commands/explain.js +138 -6
  11. package/dist/commands/licenses.js +37 -24
  12. package/dist/commands/login.js +12 -3
  13. package/dist/commands/logout.js +15 -4
  14. package/dist/commands/scan.js +1 -1
  15. package/dist/commands/service.js +76 -24
  16. package/dist/commands/status.js +38 -4
  17. package/dist/commands/types.js +1 -0
  18. package/dist/config/settings.js +102 -22
  19. package/dist/install-ui/prompt.js +5 -2
  20. package/dist/launcher/install-preflight.js +158 -0
  21. package/dist/launcher/live-install.js +11 -2
  22. package/dist/launcher/output-redaction.js +5 -3
  23. package/dist/launcher/pip-report.js +18 -2
  24. package/dist/launcher/preflight-prompt.js +31 -12
  25. package/dist/launcher/run.js +87 -8
  26. package/dist/proxy/ca.js +69 -29
  27. package/dist/proxy/enforcement.js +41 -3
  28. package/dist/proxy/worker.js +21 -9
  29. package/dist/runtime/first-run.js +33 -2
  30. package/dist/runtime/nudges.js +9 -2
  31. package/dist/scan/analyze-worker.js +18 -8
  32. package/dist/scan/collect.js +35 -28
  33. package/dist/scan/command.js +80 -40
  34. package/dist/scan/discovery.js +9 -3
  35. package/dist/scan/render.js +22 -6
  36. package/dist/scan/scanner-report.js +89 -12
  37. package/dist/scan/staged.js +69 -7
  38. package/dist/scan-ui/LegacyApp.js +10 -48
  39. package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
  40. package/dist/scan-ui/components/ProjectSelector.js +3 -3
  41. package/dist/scan-ui/components/ScoreHeader.js +8 -4
  42. package/dist/scan-ui/hooks/useScan.js +74 -27
  43. package/dist/scan-ui/launch.js +18 -4
  44. package/dist/service/state.js +15 -4
  45. package/dist/service/trust-store.js +23 -2
  46. package/dist/setup/git-hook.js +28 -17
  47. package/dist/setup/plan.js +302 -18
  48. package/dist/state/cleanup-registry.js +65 -8
  49. package/dist/state/locks.js +95 -9
  50. package/dist/state/sessions.js +66 -2
  51. package/dist/verify/package-check.js +22 -3
  52. package/dist/verify/preflight.js +328 -170
  53. 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 showLogo = cols >= 60;
17
- 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 ?? ""] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [chalk.dim("Score"), " ", scoreColor(score, action)] }), total !== undefined && (_jsxs(_Fragment, { children: [_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
18
- ? _jsxs(Text, { color: "yellow", children: [scanUsage, " \u2191 dg upgrade for Pro"] })
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 { byEcosystem, skipped } = collectScanPackages(projects);
79
- const total = [...byEcosystem.values()].reduce((sum, list) => sum + list.length, 0);
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 startMs = Date.now();
86
- const responses = [];
87
- let completed = 0;
88
- for (const [ecosystem, packages] of byEcosystem) {
89
- const base = completed;
90
- responses.push(await analyzePackages(packages, {
91
- ecosystem,
92
- onProgress: (progress) => {
93
- dispatch({ type: "SCAN_PROGRESS", done: base + progress.done, total, batchIndex: progress.batchIndex, batchCount: progress.batchCount });
94
- }
95
- }));
96
- completed = base + packages.length;
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 (error instanceof AnalyzeError && (error.statusCode === 402 || error.statusCode === 429)) {
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: body.scansUsed ?? 0,
112
- maxScans: body.maxScans ?? 0,
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
  });
@@ -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 policyMode = loadUserConfig().policy.mode;
23
- const mode = policyMode === "off" ? "off" : policyMode === "warn" ? "warn" : "block";
27
+ const mode = loadUserConfig().policy.mode;
24
28
  const config = { mode };
25
- const instance = render(react.default.createElement(app.App, { config, initialView }), { exitOnCtrlC: true });
26
- await instance.waitUntilExit();
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
  }
@@ -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: trustPlan?.target ?? paths.trustRecordPath
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
- runNativeCommand(["update-ca-certificates"], "Linux trust-store refresh failed");
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.trim() || result.stdout.trim() || `exit ${result.status ?? "unknown"}`;
252
+ const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? "unknown"}`;
232
253
  throw new TrustStoreError(`${failureMessage}: ${detail}`);
233
254
  }
234
255
  }
@@ -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 secondLine(path) {
51
+ function hookText(path) {
52
52
  try {
53
- return readFileSync(path, "utf8").split("\n", 2)[1] ?? "";
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 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`;
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
- if (gitHookState(context) === "foreign") {
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
- writeFileSync(context.hookTarget, hookScript(context.dgPath, chainedOriginal), { encoding: "utf8", mode: 0o755 });
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
- ? [{ kind: "git-hook", path: context.hookTarget, mode: "mode1", sentinel: GUARD_HOOK_SENTINEL, installedAt: "", owner: "dg" }]
235
+ ? [unregisteredHookEntry(context)]
225
236
  : [];
226
237
  for (const entry of targets) {
227
238
  reverseGitHookEntry(entry, removed, missing, warnings);