@goodfoot/agent-hooks 1.0.10 → 1.0.12

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/CHANGELOG.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # @goodfoot/agent-hooks npm package changelog
2
2
 
3
- ## 1.0.10
4
- - Fixed scaffolded hook bundles to work when the install path is a symlink or otherwise differs from the project root, so hooks resolve correctly across repository layouts
5
- - Improved stack traces from hooks by making source map resolution reliable in relocated or symlinked installs
6
- - Applied the portability fixes consistently across the Claude Code, Codex, and Antigravity entry points
3
+ ## 1.0.12
4
+
5
+ Waits for stdout and stderr writes to complete before exiting, preventing truncated hook responses and error diagnostics when output is piped. Asynchronous stdout write failures now follow the existing unexpected-error policy without retrying the payload.
6
+
7
+ ## 1.0.11
8
+
9
+ Moves generated-file tracking out of `hooks.json` into a sibling `hooks.meta.json`, because Claude Code rejects a hooks manifest carrying any key besides `hooks`. The manifest is now a pure function of its inputs, so a rebuild that changes nothing writes identical bytes and leaves the host's hook-trust hash intact. A missing or unreadable sidecar is treated as "nothing tracked as generated", which preserves the entries already in the manifest rather than pruning them.
7
10
 
8
11
  ## 1.0.10
9
12
 
@@ -50,7 +50,7 @@ function packageJson(projectName, outputPath) {
50
50
  lint: "biome check .",
51
51
  typecheck: "tsc --noEmit",
52
52
  },
53
- dependencies: { [PACKAGE_NAME]: "^1.0.10" },
53
+ dependencies: { [PACKAGE_NAME]: "^1.0.12" },
54
54
  devDependencies: {
55
55
  "@biomejs/biome": "2.4.9",
56
56
  "@types/node": "^24.0.0",
@@ -55,7 +55,7 @@ function generatePackageJson(projectName, outputPath) {
55
55
  typecheck: "tsc --noEmit",
56
56
  },
57
57
  dependencies: {
58
- [PACKAGE_NAME]: "^1.0.10",
58
+ [PACKAGE_NAME]: "^1.0.12",
59
59
  },
60
60
  devDependencies: {
61
61
  "@biomejs/biome": "2.4.9",
package/dist/cli.js CHANGED
@@ -26,7 +26,9 @@ import { scaffoldProject } from "./scaffold.js";
26
26
  // ============================================================================
27
27
  // Constants
28
28
  // ============================================================================
29
- const VERSION = "1.0.10";
29
+ const VERSION = "1.0.12";
30
+ /** Filename of the tracking sidecar written beside hooks.json. */
31
+ const HOOKS_META_FILENAME = "hooks.meta.json";
30
32
  const DEFAULT_ESBUILD_LOADERS = {
31
33
  ".md": "text",
32
34
  };
@@ -584,8 +586,12 @@ function symlinkVisiblePath(realPath, resolveDir) {
584
586
  pkgRel = path.relative(dir, realPath);
585
587
  }
586
588
  }
587
- catch {
588
- // Not a readable package.json — the realpath is not under a package.
589
+ catch (error) {
590
+ // An unreadable or malformed manifest is the same case as no manifest
591
+ // at all: identity stays undefined and the caller falls back below.
592
+ log("debug", `Could not read package manifest: ${pkgJsonPath}`, {
593
+ error: error instanceof Error ? error.message : String(error),
594
+ });
589
595
  }
590
596
  break;
591
597
  }
@@ -1049,13 +1055,7 @@ function generateHooksJson(compiledHooks, buildDir, contextInfo, executable = "n
1049
1055
  }
1050
1056
  hooks[eventName] = entries;
1051
1057
  }
1052
- return {
1053
- hooks,
1054
- __generated: {
1055
- files: compiledHooks.map((h) => h.outputFilename),
1056
- timestamp: new Date().toISOString(),
1057
- },
1058
- };
1058
+ return { hooks };
1059
1059
  }
1060
1060
  /**
1061
1061
  * Reads an existing hooks.json file if it exists.
@@ -1078,14 +1078,66 @@ function readExistingHooksJson(outputPath) {
1078
1078
  }
1079
1079
  }
1080
1080
  /**
1081
- * Removes previously generated hook files from disk.
1082
- * Only removes files that were tracked in __generated.files.
1083
- * @param existingHooksJson - The existing hooks.json content
1081
+ * Absolute path of the tracking sidecar that sits beside hooks.json.
1082
+ * @param outputPath - Path to hooks.json
1083
+ * @returns Path to hooks.meta.json
1084
+ */
1085
+ function hooksMetaPath(outputPath) {
1086
+ return path.join(path.dirname(outputPath), HOOKS_META_FILENAME);
1087
+ }
1088
+ /**
1089
+ * Reads the tracking sidecar beside hooks.json if it exists.
1090
+ *
1091
+ * A missing or unreadable sidecar is not fatal: it only means no generated
1092
+ * filenames are known, so every existing entry is treated as externally
1093
+ * authored and preserved.
1094
+ * @param outputPath - Path to the hooks.json the sidecar describes
1095
+ * @returns Parsed HooksMeta or undefined if absent or unusable
1096
+ */
1097
+ function readHooksMeta(outputPath) {
1098
+ const metaPath = hooksMetaPath(outputPath);
1099
+ if (!fs.existsSync(metaPath)) {
1100
+ return undefined;
1101
+ }
1102
+ try {
1103
+ const parsed = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1104
+ const rawFiles = parsed.files;
1105
+ if (!Array.isArray(rawFiles)) {
1106
+ log("warn", "hooks.meta.json has no files array, treating generated hooks as untracked", { path: metaPath });
1107
+ return undefined;
1108
+ }
1109
+ const files = rawFiles.filter((entry) => typeof entry === "string");
1110
+ if (files.length !== rawFiles.length) {
1111
+ log("warn", "hooks.meta.json lists non-string filenames, treating generated hooks as untracked", {
1112
+ path: metaPath,
1113
+ });
1114
+ return undefined;
1115
+ }
1116
+ return { files };
1117
+ }
1118
+ catch (error) {
1119
+ log("warn", "Failed to parse hooks.meta.json, treating generated hooks as untracked", {
1120
+ path: metaPath,
1121
+ error: error instanceof Error ? error.message : String(error),
1122
+ });
1123
+ return undefined;
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Removes bundles a previous build generated that this build did not. Only
1128
+ * filenames tracked in the sidecar are ever removed, and callers must run this
1129
+ * after the new hooks.json is on disk so no reachable state loses a file it
1130
+ * references.
1131
+ * @param previousMeta - Tracking sidecar from the previous build, if any
1084
1132
  * @param outputDir - Directory containing the generated files
1133
+ * @param keepFilenames - Filenames written by the current build
1085
1134
  */
1086
- function removeOldGeneratedFiles(existingHooksJson, outputDir) {
1087
- const filesToRemove = existingHooksJson.__generated?.files ?? [];
1135
+ function removeStaleGeneratedFiles(previousMeta, outputDir, keepFilenames) {
1136
+ const filesToRemove = previousMeta?.files ?? [];
1088
1137
  for (const filename of filesToRemove) {
1138
+ if (keepFilenames.has(filename)) {
1139
+ continue;
1140
+ }
1089
1141
  const filePath = path.join(outputDir, filename);
1090
1142
  if (fs.existsSync(filePath)) {
1091
1143
  try {
@@ -1101,13 +1153,15 @@ function removeOldGeneratedFiles(existingHooksJson, outputDir) {
1101
1153
  }
1102
1154
  }
1103
1155
  /**
1104
- * Extracts hooks from an existing hooks.json that were NOT generated by this package.
1105
- * Identifies generated hooks by checking if their command path matches the generated file pattern.
1156
+ * Extracts hooks from an existing hooks.json that were NOT generated by this
1157
+ * package. Generated hooks are identified by the filenames tracked in the
1158
+ * sidecar, so entries authored anywhere else survive a rebuild untouched.
1106
1159
  * @param existingHooksJson - The existing hooks.json content
1160
+ * @param meta - Tracking sidecar from the previous build, if any
1107
1161
  * @returns Object containing preserved hooks (keyed by event type)
1108
1162
  */
1109
- function extractPreservedHooks(existingHooksJson) {
1110
- const generatedFiles = new Set(existingHooksJson.__generated?.files ?? []);
1163
+ function extractPreservedHooks(existingHooksJson, meta) {
1164
+ const generatedFiles = new Set(meta?.files ?? []);
1111
1165
  const preserved = {};
1112
1166
  for (const [eventType, entries] of Object.entries(existingHooksJson.hooks)) {
1113
1167
  if (!entries)
@@ -1155,28 +1209,24 @@ function mergeHooksJson(newHooksJson, preservedHooks) {
1155
1209
  // Combine preserved and generated entries
1156
1210
  mergedHooks[eventType] = [...preserved, ...generated];
1157
1211
  }
1158
- return {
1159
- hooks: mergedHooks,
1160
- __generated: newHooksJson.__generated,
1161
- };
1212
+ return { hooks: mergedHooks };
1162
1213
  }
1163
1214
  /**
1164
- * Writes hooks.json to the specified path atomically.
1165
- * Uses write-to-temp-then-rename pattern for atomicity.
1166
- * @param hooksJson - The hooks.json content
1167
- * @param outputPath - Path to write hooks.json
1215
+ * Writes a JSON document atomically, using write-to-temp-then-rename.
1216
+ * @param value - Value to serialize
1217
+ * @param filePath - Destination path
1168
1218
  */
1169
- function writeHooksJson(hooksJson, outputPath) {
1170
- const dir = path.dirname(outputPath);
1219
+ function writeJsonAtomic(value, filePath) {
1220
+ const dir = path.dirname(filePath);
1171
1221
  if (!fs.existsSync(dir)) {
1172
1222
  fs.mkdirSync(dir, { recursive: true });
1173
1223
  }
1174
1224
  // Write to a temporary file first, then rename for atomicity
1175
- const tempPath = `${outputPath}.tmp.${process.pid}`;
1176
- const content = `${JSON.stringify(hooksJson, null, 2)}\n`;
1225
+ const tempPath = `${filePath}.tmp.${process.pid}`;
1226
+ const content = `${JSON.stringify(value, null, 2)}\n`;
1177
1227
  try {
1178
1228
  fs.writeFileSync(tempPath, content, "utf-8");
1179
- fs.renameSync(tempPath, outputPath);
1229
+ fs.renameSync(tempPath, filePath);
1180
1230
  }
1181
1231
  catch (error) {
1182
1232
  // Clean up temp file if rename failed
@@ -1191,6 +1241,22 @@ function writeHooksJson(hooksJson, outputPath) {
1191
1241
  throw error;
1192
1242
  }
1193
1243
  }
1244
+ /**
1245
+ * Writes hooks.json to the specified path atomically.
1246
+ * @param hooksJson - The hooks.json content
1247
+ * @param outputPath - Path to write hooks.json
1248
+ */
1249
+ function writeHooksJson(hooksJson, outputPath) {
1250
+ writeJsonAtomic(hooksJson, outputPath);
1251
+ }
1252
+ /**
1253
+ * Writes the tracking sidecar beside hooks.json atomically.
1254
+ * @param meta - Tracking metadata for this build
1255
+ * @param outputPath - Path to the hooks.json the sidecar describes
1256
+ */
1257
+ function writeHooksMeta(meta, outputPath) {
1258
+ writeJsonAtomic(meta, hooksMetaPath(outputPath));
1259
+ }
1194
1260
  // ============================================================================
1195
1261
  // Main Entry Point
1196
1262
  // ============================================================================
@@ -1376,15 +1442,14 @@ async function main() {
1376
1442
  process.stderr.write(`Error: No hook files found matching pattern: ${args.input}\n`);
1377
1443
  process.exit(1);
1378
1444
  }
1379
- // Read existing hooks.json to preserve non-generated hooks
1445
+ // Read existing hooks.json and its tracking sidecar to preserve non-generated hooks
1380
1446
  const existingHooksJson = readExistingHooksJson(outputPath);
1447
+ const existingMeta = readHooksMeta(outputPath);
1381
1448
  let preservedHooks = {};
1382
1449
  if (existingHooksJson !== undefined) {
1383
1450
  log("info", "Found existing hooks.json, will preserve non-generated hooks");
1384
1451
  // Extract hooks that were NOT generated by this package
1385
- preservedHooks = extractPreservedHooks(existingHooksJson);
1386
- // Remove old generated files from disk
1387
- removeOldGeneratedFiles(existingHooksJson, buildDir);
1452
+ preservedHooks = extractPreservedHooks(existingHooksJson, existingMeta);
1388
1453
  const preservedCount = Object.values(preservedHooks).reduce((sum, entries) => sum + (entries?.reduce((s, e) => s + e.hooks.length, 0) ?? 0), 0);
1389
1454
  log("info", `Preserved ${preservedCount} hooks from other sources`);
1390
1455
  }
@@ -1408,19 +1473,16 @@ async function main() {
1408
1473
  // Generate hooks.json for newly compiled hooks
1409
1474
  const executable = args.executable !== undefined && args.executable !== "" ? args.executable : "node";
1410
1475
  const newHooksJson = generateHooksJson(compiledHooks, buildDir, hookContext, executable);
1411
- // Preserve timestamp if generated files haven't changed
1412
- if (existingHooksJson !== undefined) {
1413
- const existingFiles = [...(existingHooksJson.__generated?.files ?? [])].sort();
1414
- const newFiles = [...newHooksJson.__generated.files].sort();
1415
- const filesUnchanged = existingFiles.length === newFiles.length && existingFiles.every((f, i) => f === newFiles[i]);
1416
- if (filesUnchanged && existingHooksJson.__generated?.timestamp) {
1417
- newHooksJson.__generated.timestamp = existingHooksJson.__generated.timestamp;
1418
- log("info", "Files unchanged, preserving existing timestamp");
1419
- }
1420
- }
1421
1476
  // Merge with preserved hooks
1422
1477
  const finalHooksJson = mergeHooksJson(newHooksJson, preservedHooks);
1423
1478
  writeHooksJson(finalHooksJson, outputPath);
1479
+ // Drop the bundles the previous build produced that this one did not, then
1480
+ // record what this build wrote. Stale files go only after the hooks.json
1481
+ // that references the current set is on disk, so every state on disk
1482
+ // between the two writes stays runnable.
1483
+ const writtenFilenames = new Set(compiledHooks.map((hook) => hook.outputFilename));
1484
+ removeStaleGeneratedFiles(existingMeta, buildDir, writtenFilenames);
1485
+ writeHooksMeta({ files: [...writtenFilenames].sort() }, outputPath);
1424
1486
  log("info", "Compilation complete", {
1425
1487
  hooksCompiled: compiledHooks.length,
1426
1488
  outputPath,
@@ -1471,4 +1533,4 @@ if (isDirectExecution) {
1471
1533
  // Shared build primitives reused by the per-agent CLI branches (single
1472
1534
  // source for driver logic — the duplication check forbids re-deriving these
1473
1535
  // inside agents/*).
1474
- export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, mergeHooksJson, moduleWorkingDir, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, removeOldGeneratedFiles, symlinkVisiblePath, validateArgs, };
1536
+ export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, hooksMetaPath, mergeHooksJson, moduleWorkingDir, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, readHooksMeta, removeStaleGeneratedFiles, symlinkVisiblePath, validateArgs, };
@@ -111,18 +111,29 @@ function classify(error, phase, policy, onUnexpectedError) {
111
111
  }
112
112
  return { kind: "handlerError", error, phase };
113
113
  }
114
+ // A successful write return value only describes buffering; its callback confirms delivery.
115
+ function writeStream(stream, content) {
116
+ return new Promise((resolve, reject) => {
117
+ stream.write(content, (error) => {
118
+ if (error)
119
+ reject(error);
120
+ else
121
+ resolve();
122
+ });
123
+ });
124
+ }
114
125
  /**
115
126
  * Writes an unexpected (non-handler, runtime-owned) error's stacktrace to
116
127
  * stderr. Used for cleanup/finalize failures under the default `"error"`
117
128
  * policy, which exit with code 1 — these are not the handler's own
118
129
  * intentional signal.
119
130
  */
120
- function writeUnexpectedErrorStderr(error) {
131
+ async function writeUnexpectedErrorStderr(error) {
121
132
  if (error instanceof Error) {
122
- process.stderr.write(`${error.stack ?? error.message}\n`);
133
+ await writeStream(process.stderr, `${error.stack ?? error.message}\n`);
123
134
  }
124
135
  else {
125
- process.stderr.write(`${String(error)}\n`);
136
+ await writeStream(process.stderr, `${String(error)}\n`);
126
137
  }
127
138
  }
128
139
  /**
@@ -198,7 +209,7 @@ export async function drive(transport, hookFn) {
198
209
  cleanupQuietly();
199
210
  process.exit(FALLBACK_EXIT_SUCCESS);
200
211
  }
201
- writeUnexpectedErrorStderr(error);
212
+ await writeUnexpectedErrorStderr(error);
202
213
  cleanupQuietly();
203
214
  process.exit(FALLBACK_EXIT_ERROR);
204
215
  }
@@ -206,15 +217,15 @@ export async function drive(transport, hookFn) {
206
217
  cleanup(policy, onUnexpectedError);
207
218
  }
208
219
  catch (error) {
209
- writeUnexpectedErrorStderr(error);
220
+ await writeUnexpectedErrorStderr(error);
210
221
  process.exit(FALLBACK_EXIT_ERROR);
211
222
  }
212
223
  if (finalized.stderr !== undefined) {
213
- process.stderr.write(finalized.stderr);
224
+ await writeStream(process.stderr, finalized.stderr);
214
225
  }
215
226
  if (finalized.stdout !== undefined) {
216
227
  try {
217
- process.stdout.write(finalized.stdout);
228
+ await writeStream(process.stdout, finalized.stdout);
218
229
  }
219
230
  catch (error) {
220
231
  if (policy === "continue") {
@@ -222,7 +233,7 @@ export async function drive(transport, hookFn) {
222
233
  cleanupQuietly();
223
234
  process.exit(FALLBACK_EXIT_SUCCESS);
224
235
  }
225
- writeUnexpectedErrorStderr(error);
236
+ await writeUnexpectedErrorStderr(error);
226
237
  cleanupQuietly();
227
238
  process.exit(FALLBACK_EXIT_ERROR);
228
239
  }
package/dist/scaffold.js CHANGED
@@ -131,7 +131,7 @@ function generatePackageJson(projectName, outputPath) {
131
131
  typecheck: "tsc --noEmit",
132
132
  },
133
133
  dependencies: {
134
- "@goodfoot/agent-hooks": "^1.0.10",
134
+ "@goodfoot/agent-hooks": "^1.0.12",
135
135
  },
136
136
  devDependencies: {
137
137
  "@biomejs/biome": "2.4.16",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfoot/agent-hooks",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Unified agent hooks library: shared core runtime with per-agent entry points for Claude Code, Codex, and Antigravity",
5
5
  "homepage": "https://github.com/goodfoot-io/marketplace/tree/main/packages/agent-hooks",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "scripts": {
23
23
  "typecheck": "tsc --noEmit",
24
24
  "build": "tsc --build tsconfig.build.json",
25
- "lint": "biome check --write --unsafe --max-diagnostics 500 ./src ./tests ./e2e ./scripts && node ./scripts/check-stale-imports.mjs",
25
+ "lint": "biome check --write --unsafe --max-diagnostics 500 ./src ./tests ./e2e ./scripts ./vitest.config.ts && node ./scripts/check-stale-imports.mjs",
26
26
  "test": "node ./scripts/check-stale-imports.mjs && node ./scripts/check-agent-duplication.mjs && vitest run",
27
27
  "release": "bash ../../scripts/release-package.sh agent-hooks",
28
28
  "release:dry-run": "bash ../../scripts/release-package.sh agent-hooks --dry-run"
@@ -56,7 +56,7 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "esbuild": "^0.24.0",
59
+ "esbuild": "^0.28.2",
60
60
  "glob": "^11.0.0",
61
61
  "typescript": "^5.9.3"
62
62
  },
@@ -70,10 +70,15 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@anthropic-ai/claude-agent-sdk": "^0.3.162",
73
+ "@anthropic-ai/sdk": "^0.133.0",
73
74
  "@biomejs/biome": "2.4.16",
75
+ "@goodfoot/vitest-cache": "workspace:*",
76
+ "@modelcontextprotocol/sdk": "^1.29.0",
74
77
  "@opencode-ai/plugin": "1.18.23",
75
- "@types/node": "^24",
78
+ "@types/node": "^26.6.5",
76
79
  "tsx": "^4.20.6",
77
- "vitest": "4.0.13"
80
+ "vite": "8.3.4",
81
+ "vitest": "patch:vitest@npm%3A5.0.4#~/.yarn/patches/vitest-npm-5.0.4-2ab4bceca5.patch",
82
+ "zod": "^4.6.7"
78
83
  }
79
84
  }
package/types/cli.d.ts CHANGED
@@ -106,17 +106,22 @@ interface MatcherEntry {
106
106
  * The complete hooks.json structure expected by Claude Code.
107
107
  *
108
108
  * Format: { hooks: { EventType: [ { matcher?, hooks: [...] } ] } }
109
+ *
110
+ * `hooks` is the only key that may appear here: the host validates hooks.json
111
+ * against its own schema and rejects files carrying unknown keys, so build
112
+ * tracking lives in the sidecar instead (see HooksMeta).
109
113
  */
110
114
  interface HooksJson {
111
115
  /** Object keyed by event type (PreToolUse, SessionStart, etc.). */
112
116
  hooks: Partial<Record<HookEventName, MatcherEntry[]>>;
113
- /** Generated file tracking metadata. */
114
- __generated: {
115
- /** Array of generated filenames. */
116
- files: string[];
117
- /** ISO timestamp of generation. */
118
- timestamp: string;
119
- };
117
+ }
118
+ /**
119
+ * Build tracking metadata for a generated hooks.json, written to a sidecar
120
+ * file beside it (hooks.meta.json) rather than inside hooks.json itself.
121
+ */
122
+ interface HooksMeta {
123
+ /** Bundle filenames this package generated, relative to the bin/ directory. */
124
+ files: string[];
120
125
  }
121
126
  /**
122
127
  * Parses command-line arguments.
@@ -301,19 +306,40 @@ declare function generateHooksJson(compiledHooks: CompiledHook[], buildDir: stri
301
306
  */
302
307
  declare function readExistingHooksJson(outputPath: string): HooksJson | undefined;
303
308
  /**
304
- * Removes previously generated hook files from disk.
305
- * Only removes files that were tracked in __generated.files.
306
- * @param existingHooksJson - The existing hooks.json content
309
+ * Absolute path of the tracking sidecar that sits beside hooks.json.
310
+ * @param outputPath - Path to hooks.json
311
+ * @returns Path to hooks.meta.json
312
+ */
313
+ declare function hooksMetaPath(outputPath: string): string;
314
+ /**
315
+ * Reads the tracking sidecar beside hooks.json if it exists.
316
+ *
317
+ * A missing or unreadable sidecar is not fatal: it only means no generated
318
+ * filenames are known, so every existing entry is treated as externally
319
+ * authored and preserved.
320
+ * @param outputPath - Path to the hooks.json the sidecar describes
321
+ * @returns Parsed HooksMeta or undefined if absent or unusable
322
+ */
323
+ declare function readHooksMeta(outputPath: string): HooksMeta | undefined;
324
+ /**
325
+ * Removes bundles a previous build generated that this build did not. Only
326
+ * filenames tracked in the sidecar are ever removed, and callers must run this
327
+ * after the new hooks.json is on disk so no reachable state loses a file it
328
+ * references.
329
+ * @param previousMeta - Tracking sidecar from the previous build, if any
307
330
  * @param outputDir - Directory containing the generated files
331
+ * @param keepFilenames - Filenames written by the current build
308
332
  */
309
- declare function removeOldGeneratedFiles(existingHooksJson: HooksJson, outputDir: string): void;
333
+ declare function removeStaleGeneratedFiles(previousMeta: HooksMeta | undefined, outputDir: string, keepFilenames: Set<string>): void;
310
334
  /**
311
- * Extracts hooks from an existing hooks.json that were NOT generated by this package.
312
- * Identifies generated hooks by checking if their command path matches the generated file pattern.
335
+ * Extracts hooks from an existing hooks.json that were NOT generated by this
336
+ * package. Generated hooks are identified by the filenames tracked in the
337
+ * sidecar, so entries authored anywhere else survive a rebuild untouched.
313
338
  * @param existingHooksJson - The existing hooks.json content
339
+ * @param meta - Tracking sidecar from the previous build, if any
314
340
  * @returns Object containing preserved hooks (keyed by event type)
315
341
  */
316
- declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Record<HookEventName, MatcherEntry[]>>;
342
+ declare function extractPreservedHooks(existingHooksJson: HooksJson, meta: HooksMeta | undefined): Partial<Record<HookEventName, MatcherEntry[]>>;
317
343
  /**
318
344
  * Merges preserved hooks with newly generated hooks.
319
345
  * Preserved hooks are added first, then new hooks are appended.
@@ -323,4 +349,4 @@ declare function extractPreservedHooks(existingHooksJson: HooksJson): Partial<Re
323
349
  */
324
350
  declare function mergeHooksJson(newHooksJson: HooksJson, preservedHooks: Partial<Record<HookEventName, MatcherEntry[]>>): HooksJson;
325
351
  export type { CliArgs, CompiledHook, HookConfig, HookLoaderMap, HookMetadata, HooksJson, MatcherEntry };
326
- export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, mergeHooksJson, moduleWorkingDir, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, removeOldGeneratedFiles, symlinkVisiblePath, validateArgs, };
352
+ export { analyzeHookFile, buildLoaderMap, buildLoaderMap as buildEsbuildLoaderMap, compileHook, detectHookContext, discoverHookFiles, discoverHookFiles as discoverHookSourceFiles, extractPreservedHooks, generateCommandPath, generateContentHash, generateContentHash as sha256Prefix8, generateHooksJson, groupHooksByEventAndMatcher, HOOK_FACTORY_TO_EVENT, hooksMetaPath, mergeHooksJson, moduleWorkingDir, parseArgs, parseLoaderFlag, parseLoaderFlag as parseEsbuildLoaderFlag, pruneStaleHashedBundles, readExistingHooksJson, readHooksMeta, removeStaleGeneratedFiles, symlinkVisiblePath, validateArgs, };