@primitive.ai/prim 0.1.0-alpha.93 → 0.1.0-alpha.94

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.
@@ -242,6 +242,13 @@ function pinnedNpxCommand(bin, args = [], options = {}) {
242
242
  }
243
243
  var STABLE_HOOK_ARGUMENTS_RE = /^[-A-Za-z0-9_ ]*$/u;
244
244
  var STABLE_HOOK_BIN_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
245
+ var PRIM_CONFIG_SHELL_RESOLVER = [
246
+ 'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
247
+ 'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
248
+ 'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
249
+ 'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
250
+ "fi; fi; "
251
+ ].join("");
245
252
  function stableHookCommand(bin, args = "") {
246
253
  if (!STABLE_HOOK_BIN_RE.test(bin) || !STABLE_HOOK_ARGUMENTS_RE.test(args)) {
247
254
  throw new Error("invalid stable hook command");
@@ -251,11 +258,7 @@ function stableHookCommand(bin, args = "") {
251
258
  // it as inert data during the rolling window so an older uninstall or
252
259
  // reinstall can remove the stable entry without invoking npm or PATH.
253
260
  `prim_legacy_reader='-p ${PKG_NAME}@stable prim-shim.sh ${bin} '; `,
254
- 'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
255
- 'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
256
- 'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
257
- 'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
258
- "fi; fi; ",
261
+ PRIM_CONFIG_SHELL_RESOLVER,
259
262
  `case "$prim_config" in /) prim_launcher=/${STABLE_HOOK_LAUNCHER_NAME} ;; *) prim_launcher="$prim_config/${STABLE_HOOK_LAUNCHER_NAME}" ;; esac; `,
260
263
  'exec "$prim_launcher" "$@"'
261
264
  ].join("");
@@ -427,14 +430,14 @@ function throwIfAborted(signal) {
427
430
  }
428
431
  async function defaultSleep(ms, signal) {
429
432
  if (!signal) {
430
- await new Promise((resolve9) => setTimeout(resolve9, ms));
433
+ await new Promise((resolve10) => setTimeout(resolve10, ms));
431
434
  return;
432
435
  }
433
436
  throwIfAborted(signal);
434
- await new Promise((resolve9, reject) => {
437
+ await new Promise((resolve10, reject) => {
435
438
  const timer = setTimeout(() => {
436
439
  signal.removeEventListener("abort", onAbort);
437
- resolve9();
440
+ resolve10();
438
441
  }, ms);
439
442
  const onAbort = () => {
440
443
  clearTimeout(timer);
@@ -476,6 +479,144 @@ async function withFileLock(lockDir, operation, options = {}) {
476
479
  }
477
480
  }
478
481
 
482
+ // src/lib/git-hook-contract.ts
483
+ var MANAGED_GIT_HOOK_NAMES = ["pre-commit", "post-commit", "post-rewrite"];
484
+ var GIT_HOOK_ENTRYPOINT_NAME = "prim-git-hook-v1";
485
+ var GIT_HOOK_CONTRACT_VERSION = 1;
486
+ var CONTRACT_LINE_PREFIX = "# prim git hook v";
487
+ var CONTRACT_LINE_RE = /^# prim git hook v([0-9]+)\b/mu;
488
+ function blockMarkers(hook) {
489
+ return { start: `# >>> prim ${hook} hook >>>`, end: `# <<< prim ${hook} hook <<<` };
490
+ }
491
+ function entrypointCommand(hook) {
492
+ const script = [
493
+ PRIM_CONFIG_SHELL_RESOLVER,
494
+ `case "$prim_config" in /) prim_entry=/${GIT_HOOK_ENTRYPOINT_NAME} ;; `,
495
+ `*) prim_entry="$prim_config/${GIT_HOOK_ENTRYPOINT_NAME}" ;; esac; `,
496
+ '[ -x "$prim_entry" ] || exit 0; exec "$prim_entry" "$@"'
497
+ ].join("");
498
+ return `/bin/sh -c ${shellQuote(script)} ${GIT_HOOK_ENTRYPOINT_NAME} ${hook} "$@"`;
499
+ }
500
+ function managedHookBlock(hook) {
501
+ const { start, end } = blockMarkers(hook);
502
+ const header = `${CONTRACT_LINE_PREFIX}${GIT_HOOK_CONTRACT_VERSION}: a no-op unless prim is installed and enabled here (see \`prim hooks snippet ${hook}\`).`;
503
+ if (hook !== "post-rewrite") {
504
+ return `${start}
505
+ ${header}
506
+ # shellcheck disable=SC2016
507
+ ${entrypointCommand(hook)} || :
508
+ ${end}`;
509
+ }
510
+ return `${start}
511
+ ${header}
512
+ prim_rewrite_stdin=$(mktemp 2>/dev/null) || prim_rewrite_stdin=
513
+ if test -n "\${prim_rewrite_stdin}" && cat >"\${prim_rewrite_stdin}"; then
514
+ # shellcheck disable=SC2016
515
+ ${entrypointCommand(hook)} <"\${prim_rewrite_stdin}" || :
516
+ exec <"\${prim_rewrite_stdin}"
517
+ fi
518
+ if test -n "\${prim_rewrite_stdin}"; then rm -f "\${prim_rewrite_stdin}"; fi
519
+ unset prim_rewrite_stdin
520
+ ${end}`;
521
+ }
522
+ var OPEN_WORDS = /* @__PURE__ */ new Set(["then", "do", "else", "{", "(", "&&", "||", "|"]);
523
+ function canonicalHookBlock(text) {
524
+ const tokens = [];
525
+ let word = "";
526
+ let quote;
527
+ const separate = () => {
528
+ const last = tokens.at(-1);
529
+ if (last !== void 0 && last !== ";" && !OPEN_WORDS.has(last)) tokens.push(";");
530
+ };
531
+ const flush = () => {
532
+ if (word === "") return;
533
+ const last = tokens.at(-1);
534
+ if (last !== void 0 && /^[0-9]*(?:<|>|>>|<&|>&)$/u.test(last))
535
+ tokens[tokens.length - 1] += word;
536
+ else tokens.push(word);
537
+ word = "";
538
+ };
539
+ for (let index = 0; index < text.length; index += 1) {
540
+ const char = text[index];
541
+ if (quote) {
542
+ word += char;
543
+ if (char === quote) quote = void 0;
544
+ continue;
545
+ }
546
+ if (char === "'" || char === '"') {
547
+ quote = char;
548
+ word += char;
549
+ } else if (char === "#" && word === "") {
550
+ const end = text.indexOf("\n", index);
551
+ const comment = text.slice(index, end === -1 ? text.length : end);
552
+ tokens.push(comment.replace(/\s+/gu, " ").trim());
553
+ index = (end === -1 ? text.length : end) - 1;
554
+ } else if (char === "\n" || char === ";") {
555
+ flush();
556
+ if (char === ";" && text[index + 1] === ";") {
557
+ tokens.push(";;");
558
+ index += 1;
559
+ } else {
560
+ separate();
561
+ }
562
+ } else if (/\s/u.test(char)) {
563
+ flush();
564
+ } else {
565
+ word += char;
566
+ }
567
+ }
568
+ flush();
569
+ while (tokens.at(-1) === ";") tokens.pop();
570
+ return tokens.join("\0");
571
+ }
572
+ function hookBlockContractVersion(block) {
573
+ const match = CONTRACT_LINE_RE.exec(block);
574
+ return match?.[1] ? Number(match[1]) : void 0;
575
+ }
576
+ var GIT_HOOK_ENTRYPOINT_CONTENT = `#!/bin/sh
577
+ # prim git hook entrypoint v1 \u2014 managed by prim; do not edit.
578
+ # Run by the "# >>> prim <hook> hook >>>" block in a Git hook file. Acts only
579
+ # where prim is active (git config prim.active true) and never fails the hook.
580
+ case "$0" in */*) prim_dir=\${0%/*} ;; *) exit 0 ;; esac
581
+ prim_launcher="$prim_dir/${STABLE_HOOK_LAUNCHER_NAME}"
582
+ [ -x "$prim_launcher" ] || exit 0
583
+ [ "$(git config --get prim.active 2>/dev/null)" = "true" ] || exit 0
584
+ prim_hook=\${1-}
585
+ if [ "$#" -gt 0 ]; then shift; fi
586
+ case "$prim_hook" in
587
+ pre-commit)
588
+ "$prim_launcher" prim-pre-commit "$@" || :
589
+ ;;
590
+ post-commit)
591
+ prim_sha=$(git rev-parse --verify HEAD 2>/dev/null) || exit 0
592
+ case "$prim_sha" in *[!0-9a-f]* | "") exit 0 ;; esac
593
+ case "\${#prim_sha}" in 40 | 64) ;; *) exit 0 ;; esac
594
+ prim_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_branch=
595
+ prim_observed=$(mktemp "\${TMPDIR:-/tmp}/prim-post-commit-observed.XXXXXXXX" 2>/dev/null) || prim_observed=
596
+ (
597
+ trap '' HUP
598
+ trap 'if [ -n "$prim_observed" ]; then rm -f "$prim_observed"; fi' 0
599
+ PRIM_COMMIT_SHA=$prim_sha PRIM_COMMIT_BRANCH=$prim_branch PRIM_COMMIT_OBSERVED_FILE=$prim_observed "$prim_launcher" prim-post-commit
600
+ ) </dev/null >/dev/null 2>&1 &
601
+ ;;
602
+ post-rewrite)
603
+ case "\${1-}" in amend | rebase) prim_source=$1 ;; *) exit 0 ;; esac
604
+ prim_pairs=$(mktemp "\${TMPDIR:-/tmp}/prim-post-rewrite-pairs.XXXXXXXX" 2>/dev/null) || exit 0
605
+ if ! chmod 600 "$prim_pairs" 2>/dev/null || ! cat >"$prim_pairs"; then
606
+ rm -f "$prim_pairs"
607
+ exit 0
608
+ fi
609
+ prim_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_branch=
610
+ (
611
+ trap '' HUP
612
+ trap 'rm -f "$prim_pairs"' 0
613
+ PRIM_REWRITE_SOURCE=$prim_source PRIM_REWRITE_BRANCH=$prim_branch PRIM_REWRITE_PAIRS_FILE=$prim_pairs "$prim_launcher" prim-post-rewrite
614
+ ) </dev/null >/dev/null 2>&1 &
615
+ ;;
616
+ esac
617
+ exit 0
618
+ `;
619
+
479
620
  // src/lib/paths.ts
480
621
  import { homedir as homedir2 } from "os";
481
622
  import { isAbsolute as isAbsolute2, join as join4, normalize } from "path";
@@ -529,6 +670,24 @@ var SOURCE_ENTRIES = Object.fromEntries(
529
670
  target.replace(/^dist\//u, "")
530
671
  ])
531
672
  );
673
+ function hookRuntimePaths(options = {}) {
674
+ const env = options.env ?? process.env;
675
+ const configDir = primConfigDirectory({
676
+ ...options,
677
+ env,
678
+ homeDir: options.homeDir ?? env.HOME ?? ""
679
+ });
680
+ const runtimeDir = join5(configDir, "hook-runtime");
681
+ return {
682
+ configDir,
683
+ launcher: join5(configDir, STABLE_HOOK_LAUNCHER_NAME),
684
+ gitHookEntrypoint: join5(configDir, GIT_HOOK_ENTRYPOINT_NAME),
685
+ runtimeDir,
686
+ releasesDir: join5(runtimeDir, "releases"),
687
+ current: join5(runtimeDir, "current"),
688
+ selectionLock: join5(runtimeDir, "selection.lock")
689
+ };
690
+ }
532
691
  var STABLE_HOOK_LAUNCHER_CONTENT = `#!/bin/sh
533
692
  set -eu
534
693
  case "$0" in */*) prim_launcher_dir=\${0%/*} ;; *) exit 78 ;; esac
@@ -548,6 +707,40 @@ case "$prim_node" in /*) ;; *) exit 69 ;; esac
548
707
  [ -x "$prim_node" ] && [ -f "$prim_entry" ] || exit 69
549
708
  exec "$prim_node" "$prim_entry" "$@"
550
709
  `;
710
+ function gitHookEntrypointCurrent(paths) {
711
+ try {
712
+ return readFileSync3(paths.gitHookEntrypoint, "utf8") === GIT_HOOK_ENTRYPOINT_CONTENT;
713
+ } catch {
714
+ return false;
715
+ }
716
+ }
717
+ function inspectGitHookEntrypoint(options = {}) {
718
+ let paths;
719
+ try {
720
+ paths = hookRuntimePaths(options);
721
+ } catch {
722
+ return "invalid";
723
+ }
724
+ try {
725
+ const entrypoint = lstatIfPresent(paths.gitHookEntrypoint);
726
+ if (!entrypoint) return "missing";
727
+ if (!entrypoint.isFile() || (Number(entrypoint.mode) & 64) === 0 || !gitHookEntrypointCurrent(paths) || readFileSync3(paths.launcher, "utf8") !== STABLE_HOOK_LAUNCHER_CONTENT || !lstatIfPresent(paths.current)?.isFile()) {
728
+ return "invalid";
729
+ }
730
+ return "ready";
731
+ } catch {
732
+ return "invalid";
733
+ }
734
+ }
735
+ function lstatIfPresent(path) {
736
+ try {
737
+ return lstatSync(path);
738
+ } catch (error) {
739
+ const code = error.code;
740
+ if (code === "ENOENT" || code === "ENOTDIR") return void 0;
741
+ throw error;
742
+ }
743
+ }
551
744
 
552
745
  // src/commands/claude-install.ts
553
746
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
@@ -881,13 +1074,13 @@ function buildHookOutput(options) {
881
1074
  return output;
882
1075
  }
883
1076
  function writeHookOutput(output, stream = process.stdout) {
884
- return new Promise((resolve9) => {
1077
+ return new Promise((resolve10) => {
885
1078
  let settled = false;
886
1079
  const finish = (ok, retainErrorListener = false) => {
887
1080
  if (settled) return;
888
1081
  settled = true;
889
1082
  if (!retainErrorListener) stream.off("error", onError);
890
- resolve9(ok);
1083
+ resolve10(ok);
891
1084
  };
892
1085
  const onError = () => {
893
1086
  if (settled) {
@@ -922,7 +1115,7 @@ async function handoffHookOutput(output, acknowledge, stream = process.stdout) {
922
1115
  // src/hooks/hook-stdin.ts
923
1116
  var DEFAULT_MAX_BYTES = 1024 * 1024;
924
1117
  function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
925
- return new Promise((resolve9, reject) => {
1118
+ return new Promise((resolve10, reject) => {
926
1119
  const chunks = [];
927
1120
  let bytes = 0;
928
1121
  let settled = false;
@@ -934,7 +1127,7 @@ function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
934
1127
  process.stdin.removeAllListeners("end");
935
1128
  process.stdin.removeAllListeners("error");
936
1129
  if (error) reject(error);
937
- else resolve9(Buffer.concat(chunks, bytes).toString("utf-8"));
1130
+ else resolve10(Buffer.concat(chunks, bytes).toString("utf-8"));
938
1131
  };
939
1132
  const timer = setTimeout(() => finish(new Error("stdin read timeout")), timeoutMs);
940
1133
  process.stdin.on("data", (chunk) => {
@@ -5191,10 +5384,10 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5191
5384
  let offset = bm.offset;
5192
5385
  let commentEnd = null;
5193
5386
  for (const collItem of bm.items) {
5194
- const { start, key, sep: sep3, value } = collItem;
5387
+ const { start, key, sep: sep4, value } = collItem;
5195
5388
  const keyProps = resolveProps(start, {
5196
5389
  indicator: "explicit-key-ind",
5197
- next: key ?? sep3?.[0],
5390
+ next: key ?? sep4?.[0],
5198
5391
  offset,
5199
5392
  onError,
5200
5393
  parentIndent: bm.indent,
@@ -5208,7 +5401,7 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5208
5401
  else if ("indent" in key && key.indent !== bm.indent)
5209
5402
  onError(offset, "BAD_INDENT", startColMsg);
5210
5403
  }
5211
- if (!keyProps.anchor && !keyProps.tag && !sep3) {
5404
+ if (!keyProps.anchor && !keyProps.tag && !sep4) {
5212
5405
  commentEnd = keyProps.end;
5213
5406
  if (keyProps.comment) {
5214
5407
  if (map2.comment)
@@ -5232,7 +5425,7 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5232
5425
  ctx.atKey = false;
5233
5426
  if (mapIncludes(ctx, map2.items, keyNode))
5234
5427
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
5235
- const valueProps = resolveProps(sep3 ?? [], {
5428
+ const valueProps = resolveProps(sep4 ?? [], {
5236
5429
  indicator: "map-value-ind",
5237
5430
  next: value,
5238
5431
  offset: keyNode.range[2],
@@ -5248,7 +5441,7 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5248
5441
  if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
5249
5442
  onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
5250
5443
  }
5251
- const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : composeEmptyNode2(ctx, offset, sep3, null, valueProps, onError);
5444
+ const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : composeEmptyNode2(ctx, offset, sep4, null, valueProps, onError);
5252
5445
  if (ctx.schema.compat)
5253
5446
  flowIndentCheck(bm.indent, value, onError);
5254
5447
  offset = valueNode.range[2];
@@ -5324,7 +5517,7 @@ function resolveEnd(end, offset, reqSpace, onError) {
5324
5517
  let comment = "";
5325
5518
  if (end) {
5326
5519
  let hasSpace = false;
5327
- let sep3 = "";
5520
+ let sep4 = "";
5328
5521
  for (const token of end) {
5329
5522
  const { source, type } = token;
5330
5523
  switch (type) {
@@ -5338,13 +5531,13 @@ function resolveEnd(end, offset, reqSpace, onError) {
5338
5531
  if (!comment)
5339
5532
  comment = cb;
5340
5533
  else
5341
- comment += sep3 + cb;
5342
- sep3 = "";
5534
+ comment += sep4 + cb;
5535
+ sep4 = "";
5343
5536
  break;
5344
5537
  }
5345
5538
  case "newline":
5346
5539
  if (comment)
5347
- sep3 += source;
5540
+ sep4 += source;
5348
5541
  hasSpace = true;
5349
5542
  break;
5350
5543
  default:
@@ -5373,18 +5566,18 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5373
5566
  let offset = fc.offset + fc.start.source.length;
5374
5567
  for (let i = 0; i < fc.items.length; ++i) {
5375
5568
  const collItem = fc.items[i];
5376
- const { start, key, sep: sep3, value } = collItem;
5569
+ const { start, key, sep: sep4, value } = collItem;
5377
5570
  const props = resolveProps(start, {
5378
5571
  flow: fcName,
5379
5572
  indicator: "explicit-key-ind",
5380
- next: key ?? sep3?.[0],
5573
+ next: key ?? sep4?.[0],
5381
5574
  offset,
5382
5575
  onError,
5383
5576
  parentIndent: fc.indent,
5384
5577
  startOnNewline: false
5385
5578
  });
5386
5579
  if (!props.found) {
5387
- if (!props.anchor && !props.tag && !sep3 && !value) {
5580
+ if (!props.anchor && !props.tag && !sep4 && !value) {
5388
5581
  if (i === 0 && props.comma)
5389
5582
  onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
5390
5583
  else if (i < fc.items.length - 1)
@@ -5438,8 +5631,8 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5438
5631
  }
5439
5632
  }
5440
5633
  }
5441
- if (!isMap2 && !sep3 && !props.found) {
5442
- const valueNode = value ? composeNode2(ctx, value, props, onError) : composeEmptyNode2(ctx, props.end, sep3, null, props, onError);
5634
+ if (!isMap2 && !sep4 && !props.found) {
5635
+ const valueNode = value ? composeNode2(ctx, value, props, onError) : composeEmptyNode2(ctx, props.end, sep4, null, props, onError);
5443
5636
  coll.items.push(valueNode);
5444
5637
  offset = valueNode.range[2];
5445
5638
  if (isBlock(value))
@@ -5451,7 +5644,7 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5451
5644
  if (isBlock(key))
5452
5645
  onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
5453
5646
  ctx.atKey = false;
5454
- const valueProps = resolveProps(sep3 ?? [], {
5647
+ const valueProps = resolveProps(sep4 ?? [], {
5455
5648
  flow: fcName,
5456
5649
  indicator: "map-value-ind",
5457
5650
  next: value,
@@ -5462,8 +5655,8 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5462
5655
  });
5463
5656
  if (valueProps.found) {
5464
5657
  if (!isMap2 && !props.found && ctx.options.strict) {
5465
- if (sep3)
5466
- for (const st of sep3) {
5658
+ if (sep4)
5659
+ for (const st of sep4) {
5467
5660
  if (st === valueProps.found)
5468
5661
  break;
5469
5662
  if (st.type === "newline") {
@@ -5480,7 +5673,7 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5480
5673
  else
5481
5674
  onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
5482
5675
  }
5483
- const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode2(ctx, valueProps.end, sep3, null, valueProps, onError) : null;
5676
+ const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode2(ctx, valueProps.end, sep4, null, valueProps, onError) : null;
5484
5677
  if (valueNode) {
5485
5678
  if (isBlock(value))
5486
5679
  onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
@@ -5640,7 +5833,7 @@ function resolveBlockScalar(ctx, scalar, onError) {
5640
5833
  chompStart = i + 1;
5641
5834
  }
5642
5835
  let value = "";
5643
- let sep3 = "";
5836
+ let sep4 = "";
5644
5837
  let prevMoreIndented = false;
5645
5838
  for (let i = 0; i < contentStart; ++i)
5646
5839
  value += lines[i][0].slice(trimIndent) + "\n";
@@ -5657,24 +5850,24 @@ function resolveBlockScalar(ctx, scalar, onError) {
5657
5850
  indent = "";
5658
5851
  }
5659
5852
  if (type === Scalar.BLOCK_LITERAL) {
5660
- value += sep3 + indent.slice(trimIndent) + content;
5661
- sep3 = "\n";
5853
+ value += sep4 + indent.slice(trimIndent) + content;
5854
+ sep4 = "\n";
5662
5855
  } else if (indent.length > trimIndent || content[0] === " ") {
5663
- if (sep3 === " ")
5664
- sep3 = "\n";
5665
- else if (!prevMoreIndented && sep3 === "\n")
5666
- sep3 = "\n\n";
5667
- value += sep3 + indent.slice(trimIndent) + content;
5668
- sep3 = "\n";
5856
+ if (sep4 === " ")
5857
+ sep4 = "\n";
5858
+ else if (!prevMoreIndented && sep4 === "\n")
5859
+ sep4 = "\n\n";
5860
+ value += sep4 + indent.slice(trimIndent) + content;
5861
+ sep4 = "\n";
5669
5862
  prevMoreIndented = true;
5670
5863
  } else if (content === "") {
5671
- if (sep3 === "\n")
5864
+ if (sep4 === "\n")
5672
5865
  value += "\n";
5673
5866
  else
5674
- sep3 = "\n";
5867
+ sep4 = "\n";
5675
5868
  } else {
5676
- value += sep3 + content;
5677
- sep3 = " ";
5869
+ value += sep4 + content;
5870
+ sep4 = " ";
5678
5871
  prevMoreIndented = false;
5679
5872
  }
5680
5873
  }
@@ -5848,25 +6041,25 @@ function foldLines(source) {
5848
6041
  if (!match)
5849
6042
  return source;
5850
6043
  let res = match[1];
5851
- let sep3 = " ";
6044
+ let sep4 = " ";
5852
6045
  let pos = first.lastIndex;
5853
6046
  line.lastIndex = pos;
5854
6047
  while (match = line.exec(source)) {
5855
6048
  if (match[1] === "") {
5856
- if (sep3 === "\n")
5857
- res += sep3;
6049
+ if (sep4 === "\n")
6050
+ res += sep4;
5858
6051
  else
5859
- sep3 = "\n";
6052
+ sep4 = "\n";
5860
6053
  } else {
5861
- res += sep3 + match[1];
5862
- sep3 = " ";
6054
+ res += sep4 + match[1];
6055
+ sep4 = " ";
5863
6056
  }
5864
6057
  pos = line.lastIndex;
5865
6058
  }
5866
6059
  const last = /[ \t]*(.*)/sy;
5867
6060
  last.lastIndex = pos;
5868
6061
  match = last.exec(source);
5869
- return res + sep3 + (match?.[1] ?? "");
6062
+ return res + sep4 + (match?.[1] ?? "");
5870
6063
  }
5871
6064
  function doubleQuotedValue(source, onError) {
5872
6065
  let res = "";
@@ -7483,18 +7676,18 @@ var Parser = class {
7483
7676
  if (this.type === "map-value-ind") {
7484
7677
  const prev = getPrevProps(this.peek(2));
7485
7678
  const start = getFirstKeyStartProps(prev);
7486
- let sep3;
7679
+ let sep4;
7487
7680
  if (scalar.end) {
7488
- sep3 = scalar.end;
7489
- sep3.push(this.sourceToken);
7681
+ sep4 = scalar.end;
7682
+ sep4.push(this.sourceToken);
7490
7683
  delete scalar.end;
7491
7684
  } else
7492
- sep3 = [this.sourceToken];
7685
+ sep4 = [this.sourceToken];
7493
7686
  const map2 = {
7494
7687
  type: "block-map",
7495
7688
  offset: scalar.offset,
7496
7689
  indent: scalar.indent,
7497
- items: [{ start, key: scalar, sep: sep3 }]
7690
+ items: [{ start, key: scalar, sep: sep4 }]
7498
7691
  };
7499
7692
  this.onKeyLine = true;
7500
7693
  this.stack[this.stack.length - 1] = map2;
@@ -7647,15 +7840,15 @@ var Parser = class {
7647
7840
  } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
7648
7841
  const start2 = getFirstKeyStartProps(it.start);
7649
7842
  const key = it.key;
7650
- const sep3 = it.sep;
7651
- sep3.push(this.sourceToken);
7843
+ const sep4 = it.sep;
7844
+ sep4.push(this.sourceToken);
7652
7845
  delete it.key;
7653
7846
  delete it.sep;
7654
7847
  this.stack.push({
7655
7848
  type: "block-map",
7656
7849
  offset: this.offset,
7657
7850
  indent: this.indent,
7658
- items: [{ start: start2, key, sep: sep3 }]
7851
+ items: [{ start: start2, key, sep: sep4 }]
7659
7852
  });
7660
7853
  } else if (start.length > 0) {
7661
7854
  it.sep = it.sep.concat(start, this.sourceToken);
@@ -7849,13 +8042,13 @@ var Parser = class {
7849
8042
  const prev = getPrevProps(parent);
7850
8043
  const start = getFirstKeyStartProps(prev);
7851
8044
  fixFlowSeqItems(fc);
7852
- const sep3 = fc.end.splice(1, fc.end.length);
7853
- sep3.push(this.sourceToken);
8045
+ const sep4 = fc.end.splice(1, fc.end.length);
8046
+ sep4.push(this.sourceToken);
7854
8047
  const map2 = {
7855
8048
  type: "block-map",
7856
8049
  offset: fc.offset,
7857
8050
  indent: fc.indent,
7858
- items: [{ start, key: fc, sep: sep3 }]
8051
+ items: [{ start, key: fc, sep: sep4 }]
7859
8052
  };
7860
8053
  this.onKeyLine = true;
7861
8054
  this.stack[this.stack.length - 1] = map2;
@@ -8761,7 +8954,7 @@ function daemonRequest(method, params = {}, opts = {}) {
8761
8954
  ...caller ? { caller } : {}
8762
8955
  };
8763
8956
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
8764
- return new Promise((resolve9) => {
8957
+ return new Promise((resolve10) => {
8765
8958
  let settled = false;
8766
8959
  let socket;
8767
8960
  let buffer = Buffer.alloc(0);
@@ -8776,7 +8969,7 @@ function daemonRequest(method, params = {}, opts = {}) {
8776
8969
  } catch {
8777
8970
  }
8778
8971
  }
8779
- resolve9(value);
8972
+ resolve10(value);
8780
8973
  };
8781
8974
  const timer = setTimeout(() => settle(null), timeoutMs);
8782
8975
  timer.unref();
@@ -27089,8 +27282,59 @@ async function fetchAndCacheCollectScope(cwd, dependencies = defaultDependencies
27089
27282
  return dependencies.writeCached(cwd, response);
27090
27283
  }
27091
27284
 
27092
- // src/lib/post-commit-hook.ts
27285
+ // src/lib/commit-heartbeat.ts
27093
27286
  import { execFileSync as execFileSync3 } from "child_process";
27287
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "fs";
27288
+ import { dirname as dirname10, join as join15, resolve as resolve6 } from "path";
27289
+ var WIRED_PATH = "prim/git-hooks-wired";
27290
+ var GIT_TIMEOUT_MS3 = 1e3;
27291
+ var MAX_STAMP_BYTES = 1024;
27292
+ function commonPath(cwd, relative4) {
27293
+ const value = execFileSync3("git", ["rev-parse", "--git-common-dir"], {
27294
+ cwd,
27295
+ encoding: "utf8",
27296
+ stdio: ["ignore", "pipe", "ignore"],
27297
+ timeout: GIT_TIMEOUT_MS3
27298
+ }).replace(/\r?\n$/u, "");
27299
+ if (value === "" || /[\0\r\n]/u.test(value)) throw new Error("Git returned an unsafe path");
27300
+ return join15(resolve6(cwd, value), relative4);
27301
+ }
27302
+ function ensurePrivateParent(path) {
27303
+ mkdirSync7(dirname10(dirname10(path)), { recursive: true, mode: 448 });
27304
+ mkdirSync7(dirname10(path), { recursive: true, mode: 448 });
27305
+ }
27306
+ function writeStamp(path, at) {
27307
+ ensurePrivateParent(path);
27308
+ atomicWriteFile(path, `${JSON.stringify({ at })}
27309
+ `, { mode: 384 });
27310
+ }
27311
+ function readStampAt(path) {
27312
+ try {
27313
+ const raw = readFileSync11(path, "utf8");
27314
+ if (raw.length > MAX_STAMP_BYTES) return void 0;
27315
+ const parsed = JSON.parse(raw);
27316
+ return typeof parsed.at === "number" && Number.isSafeInteger(parsed.at) && parsed.at > 0 ? parsed.at : void 0;
27317
+ } catch {
27318
+ return void 0;
27319
+ }
27320
+ }
27321
+ function recordHooksWired(cwd, options = {}) {
27322
+ try {
27323
+ const path = commonPath(cwd, WIRED_PATH);
27324
+ if (options.onlyIfAbsent && readStampAt(path) !== void 0) return;
27325
+ writeStamp(path, options.now ?? Date.now());
27326
+ } catch {
27327
+ }
27328
+ }
27329
+ function clearHooksWired(cwd) {
27330
+ try {
27331
+ unlinkSync2(commonPath(cwd, WIRED_PATH));
27332
+ } catch {
27333
+ }
27334
+ }
27335
+
27336
+ // src/lib/git-hooks.ts
27337
+ import { execFileSync as execFileSync4 } from "child_process";
27094
27338
  import { createHash as createHash8, randomUUID as randomUUID2 } from "crypto";
27095
27339
  import {
27096
27340
  constants as constants5,
@@ -27099,50 +27343,28 @@ import {
27099
27343
  fchmodSync as fchmodSync2,
27100
27344
  fsyncSync as fsyncSync2,
27101
27345
  lstatSync as lstatSync5,
27102
- mkdirSync as mkdirSync7,
27346
+ mkdirSync as mkdirSync8,
27103
27347
  openSync as openSync4,
27104
- readFileSync as readFileSync11,
27348
+ readFileSync as readFileSync12,
27349
+ realpathSync as realpathSync4,
27105
27350
  renameSync as renameSync3,
27106
- unlinkSync as unlinkSync2,
27351
+ unlinkSync as unlinkSync3,
27107
27352
  writeFileSync as writeFileSync4
27108
27353
  } from "fs";
27109
- import { basename as basename2, dirname as dirname10, isAbsolute as isAbsolute7, resolve as resolve6 } from "path";
27110
- var PRIM_POST_COMMIT_BLOCK_START = "# >>> prim post-commit hook >>>";
27111
- var PRIM_POST_COMMIT_BLOCK_END = "# <<< prim post-commit hook <<<";
27112
- var PRIM_CREATED_MARK = "prim-created-post-commit-hook";
27113
- var PRIM_POST_REWRITE_BLOCK_START = "# >>> prim post-rewrite hook >>>";
27114
- var PRIM_POST_REWRITE_BLOCK_END = "# <<< prim post-rewrite hook <<<";
27115
- var PRIM_POST_REWRITE_CREATED_MARK = "prim-created-post-rewrite-hook";
27116
- var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
27117
- var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
27118
- var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
27119
- var MAX_HOOK_BYTES = 1048576;
27120
- var GIT_TIMEOUT_MS3 = 1e3;
27121
- var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
27122
- "#!/bin/sh",
27123
- "#!/usr/bin/env sh",
27124
- "#!/bin/bash",
27125
- "#!/usr/bin/env bash",
27126
- "#!/bin/zsh",
27127
- "#!/usr/bin/env zsh"
27128
- ]);
27129
- var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
27130
- '#!/usr/bin/env sh\n. "${0%/*}/h"',
27131
- '#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
27132
- ]);
27133
- var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
27134
- var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
27135
- "d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
27136
- "b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
27137
- "9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
27138
- "3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
27139
- "548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
27140
- "c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
27141
- "ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
27142
- "70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
27143
- ]);
27144
- function currentPostCommitBlock() {
27145
- return `${PRIM_POST_COMMIT_BLOCK_START}
27354
+ import { basename as basename2, dirname as dirname11, isAbsolute as isAbsolute8, join as join16, relative as relative3, resolve as resolve7, sep as sep3 } from "path";
27355
+
27356
+ // src/lib/git-hook-legacy.ts
27357
+ import { isAbsolute as isAbsolute7 } from "path";
27358
+ var PRIM_MANAGED_MARK = "prim-managed-hook";
27359
+ var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
27360
+ function blockMarkers2(spec) {
27361
+ return {
27362
+ start: `# >>> prim ${spec.hookName} hook >>>`,
27363
+ end: `# <<< prim ${spec.hookName} hook <<<`
27364
+ };
27365
+ }
27366
+ function legacyPostCommitBlock() {
27367
+ return `# >>> prim post-commit hook >>>
27146
27368
  if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27147
27369
  prim_commit_sha=$(git rev-parse --verify HEAD 2>/dev/null) || prim_commit_sha=
27148
27370
  case "$prim_commit_sha" in *[!0-9a-f]*|"") prim_commit_sha= ;; esac
@@ -27171,13 +27393,10 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27171
27393
  fi
27172
27394
  unset prim_commit_sha
27173
27395
  fi
27174
- ${PRIM_POST_COMMIT_BLOCK_END}`;
27396
+ # <<< prim post-commit hook <<<`;
27175
27397
  }
27176
- function postCommitHookBlock() {
27177
- return currentPostCommitBlock();
27178
- }
27179
- function postRewriteHookBlock() {
27180
- return `${PRIM_POST_REWRITE_BLOCK_START}
27398
+ function legacyPostRewriteBlock() {
27399
+ return `# >>> prim post-rewrite hook >>>
27181
27400
  if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27182
27401
  case "$1" in amend|rebase) prim_rewrite_source="$1" ;; *) prim_rewrite_source= ;; esac
27183
27402
  if [ -n "$prim_rewrite_source" ]; then
@@ -27216,22 +27435,159 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27216
27435
  fi
27217
27436
  unset prim_rewrite_source
27218
27437
  fi
27219
- ${PRIM_POST_REWRITE_BLOCK_END}`;
27220
- }
27221
- var POST_COMMIT_MANAGED_HOOK = {
27222
- hookName: "post-commit",
27223
- blockStart: PRIM_POST_COMMIT_BLOCK_START,
27224
- blockEnd: PRIM_POST_COMMIT_BLOCK_END,
27225
- createdMark: PRIM_CREATED_MARK,
27226
- block: postCommitHookBlock
27227
- };
27228
- var POST_REWRITE_MANAGED_HOOK = {
27229
- hookName: "post-rewrite",
27230
- blockStart: PRIM_POST_REWRITE_BLOCK_START,
27231
- blockEnd: PRIM_POST_REWRITE_BLOCK_END,
27232
- createdMark: PRIM_POST_REWRITE_CREATED_MARK,
27233
- block: postRewriteHookBlock
27438
+ # <<< prim post-rewrite hook <<<`;
27439
+ }
27440
+ var PRE_COMMIT_GATE = 'if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then';
27441
+ function preCommitLadder(fallback, failSoft) {
27442
+ const soft = failSoft ? " || true" : "";
27443
+ return `if command -v prim-pre-commit >/dev/null 2>&1; then
27444
+ prim-pre-commit${soft}
27445
+ elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
27446
+ ./node_modules/.bin/prim-pre-commit${soft}
27447
+ else
27448
+ ${fallback} 2>/dev/null || true
27449
+ fi`;
27450
+ }
27451
+ var LADDER_ALPHA_2 = preCommitLadder("npx --yes @primitive.ai/prim pre-commit-hook", false);
27452
+ var LADDER_ALPHA_9 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", false);
27453
+ var LADDER_ALPHA_35 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", true);
27454
+ var PINNED_NPX_PRE_COMMIT = String.raw`npx --yes(?: --ignore-scripts)? -p @primitive\.ai/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)? prim-pre-commit`;
27455
+ var PINNED_PRE_COMMIT_RE = new RegExp(
27456
+ String.raw`^\{ (?:${PINNED_NPX_PRE_COMMIT}|if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else ${PINNED_NPX_PRE_COMMIT}; fi); \} \|\| true$`,
27457
+ "u"
27458
+ );
27459
+ var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
27460
+ function decodePinnedPath(token) {
27461
+ if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
27462
+ const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
27463
+ if (!isAbsolute7(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
27464
+ return void 0;
27465
+ }
27466
+ return value;
27467
+ }
27468
+ function isPinnedPreCommitInvocation(line) {
27469
+ const match = PINNED_PRE_COMMIT_RE.exec(line);
27470
+ if (!match) return false;
27471
+ if (match.groups?.node === void 0) return true;
27472
+ return Boolean(
27473
+ decodePinnedPath(match.groups.node) && decodePinnedPath(match.groups.entry)?.endsWith("/dist/hooks/pre-commit.js")
27474
+ );
27475
+ }
27476
+ function isLegacyPreCommitBody(body) {
27477
+ if (body === LADDER_ALPHA_2 || body === LADDER_ALPHA_9 || body === LADDER_ALPHA_35) return true;
27478
+ if (isPinnedPreCommitInvocation(body)) return true;
27479
+ const prefix = `${PRE_COMMIT_GATE}
27480
+ `;
27481
+ if (!body.startsWith(prefix) || !body.endsWith("\nfi")) return false;
27482
+ const inner = body.slice(prefix.length, -"\nfi".length);
27483
+ return inner === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(inner);
27484
+ }
27485
+ function isLegacyOwnedPreCommitScript(content) {
27486
+ const original = (ladder) => `#!/bin/sh
27487
+ # prim pre-commit hook \u2014 auto-syncs affected specs on commit
27488
+ # Installed by: prim hooks install
27489
+
27490
+ # Find the nearest node_modules/.bin with prim, or use npx
27491
+ ${ladder}
27492
+ `;
27493
+ if (content === original(LADDER_ALPHA_2) || content === original(LADDER_ALPHA_9)) return true;
27494
+ if (content === `#!/bin/sh
27495
+ # prim pre-commit hook \u2014 installed by: prim hooks install
27496
+
27497
+ ${LADDER_ALPHA_9}
27498
+ `) {
27499
+ return true;
27500
+ }
27501
+ const prefix = `#!/bin/sh
27502
+ # prim pre-commit hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
27503
+
27504
+ `;
27505
+ if (!content.startsWith(prefix) || !content.endsWith("\n")) return false;
27506
+ const body = content.slice(prefix.length, -1);
27507
+ return body === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(body);
27508
+ }
27509
+ var PINNED_PACKAGE_VERSION_RE = /@primitive\.ai\/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/gu;
27510
+ function legacyInlineHookBlock(hookName) {
27511
+ return hookName === "post-commit" ? legacyPostCommitBlock() : legacyPostRewriteBlock();
27512
+ }
27513
+ function isRecognizedLegacyBlock(hookName, block) {
27514
+ if (hookName === "pre-commit") {
27515
+ const { start, end } = blockMarkers2(PRE_COMMIT);
27516
+ const prefix = `${start}
27517
+ `;
27518
+ const suffix = `
27519
+ ${end}`;
27520
+ if (!block.startsWith(prefix) || !block.endsWith(suffix)) return false;
27521
+ return isLegacyPreCommitBody(block.slice(prefix.length, -suffix.length));
27522
+ }
27523
+ const expected = legacyInlineHookBlock(hookName);
27524
+ const normalize2 = (text) => text.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
27525
+ return normalize2(block) === normalize2(expected);
27526
+ }
27527
+
27528
+ // src/lib/git-hooks.ts
27529
+ var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
27530
+ var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
27531
+ var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
27532
+ var MAX_HOOK_BYTES = 1048576;
27533
+ var GIT_TIMEOUT_MS4 = 1e3;
27534
+ var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
27535
+ "#!/bin/sh",
27536
+ "#!/usr/bin/env sh",
27537
+ "#!/bin/bash",
27538
+ "#!/usr/bin/env bash",
27539
+ "#!/bin/zsh",
27540
+ "#!/usr/bin/env zsh"
27541
+ ]);
27542
+ var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
27543
+ '#!/usr/bin/env sh\n. "${0%/*}/h"',
27544
+ '#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
27545
+ ]);
27546
+ var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
27547
+ var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
27548
+ "d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
27549
+ "b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
27550
+ "9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
27551
+ "3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
27552
+ "548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
27553
+ "c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
27554
+ "ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
27555
+ "70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
27556
+ ]);
27557
+ function managedHookSpecFor(hookName) {
27558
+ const { start, end } = blockMarkers(hookName);
27559
+ return {
27560
+ hookName,
27561
+ blockStart: start,
27562
+ blockEnd: end,
27563
+ createdMark: `prim-created-${hookName}-hook`
27564
+ };
27565
+ }
27566
+ var MANAGED_HOOK_SPECS = {
27567
+ "pre-commit": managedHookSpecFor("pre-commit"),
27568
+ "post-commit": managedHookSpecFor("post-commit"),
27569
+ "post-rewrite": managedHookSpecFor("post-rewrite")
27234
27570
  };
27571
+ function managedHookSpec(hookName) {
27572
+ return MANAGED_HOOK_SPECS[hookName];
27573
+ }
27574
+ function gitHooksMode(options = {}) {
27575
+ try {
27576
+ const value = execFileSync4(
27577
+ "git",
27578
+ ["config", ...options.global ? ["--global"] : [], "--get", "prim.gitHooks"],
27579
+ {
27580
+ cwd: options.cwd,
27581
+ encoding: "utf8",
27582
+ stdio: ["ignore", "pipe", "ignore"],
27583
+ timeout: GIT_TIMEOUT_MS4
27584
+ }
27585
+ ).trim();
27586
+ return value === "manual" ? "manual" : "auto";
27587
+ } catch {
27588
+ return "auto";
27589
+ }
27590
+ }
27235
27591
  function legacyFloatingInvocation() {
27236
27592
  return `if command -v prim-post-commit >/dev/null 2>&1; then
27237
27593
  prim-post-commit || true
@@ -27248,15 +27604,6 @@ fi
27248
27604
  `);
27249
27605
  }
27250
27606
  var PINNED_INVOCATION_RE = /^\{ if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else npx --yes -p @primitive\.ai\/prim@(?<version>[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?) prim-post-commit; fi; \} \|\| true$/u;
27251
- var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
27252
- function decodePinnedPath(token) {
27253
- if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
27254
- const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
27255
- if (!isAbsolute7(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
27256
- return void 0;
27257
- }
27258
- return value;
27259
- }
27260
27607
  function isPinnedInvocation(value) {
27261
27608
  const match = PINNED_INVOCATION_RE.exec(value);
27262
27609
  const node = decodePinnedPath(match?.groups?.node);
@@ -27289,48 +27636,122 @@ function isKnownLegacyGlobalGate(value) {
27289
27636
  }
27290
27637
  function createdScaffold(spec) {
27291
27638
  return Buffer.from(`#!/bin/sh
27292
- ${spec.block()}
27639
+ ${managedHookBlock(spec.hookName)}
27293
27640
  # ${spec.createdMark}
27294
27641
  `);
27295
27642
  }
27643
+ function oldCreatedPrefixes(spec) {
27644
+ return [
27645
+ Buffer.from(`#!/bin/sh
27646
+ # ${spec.createdMark}
27647
+
27648
+ `),
27649
+ Buffer.from(`#!/bin/sh
27650
+ # ${LEGACY_PRIM_CREATED_MARK}
27651
+
27652
+ `)
27653
+ ];
27654
+ }
27655
+ function isOwnedStandalonePreCommit(content) {
27656
+ return content === "#!/bin/sh\nprim-pre-commit\n" || isLegacyOwnedPreCommitScript(content);
27657
+ }
27658
+ function realpathLoose(path) {
27659
+ const missing = [];
27660
+ let current = resolve7(path);
27661
+ for (; ; ) {
27662
+ try {
27663
+ return join16(realpathSync4.native(current), ...missing);
27664
+ } catch {
27665
+ const parent = dirname11(current);
27666
+ if (parent === current) return resolve7(path);
27667
+ missing.unshift(basename2(current));
27668
+ current = parent;
27669
+ }
27670
+ }
27671
+ }
27672
+ function isWithin(path, directory) {
27673
+ const rel = relative3(directory, path);
27674
+ return rel === "" || !isAbsolute8(rel) && rel !== ".." && !rel.startsWith(`..${sep3}`);
27675
+ }
27676
+ function primGitHooksDirectory() {
27677
+ try {
27678
+ return join16(primConfigDirectory(), "git-hooks");
27679
+ } catch {
27680
+ return void 0;
27681
+ }
27682
+ }
27683
+ function otherWorktreeRoots(gitRoot) {
27684
+ try {
27685
+ const self = realpathLoose(gitRoot);
27686
+ return execFileSync4("git", ["worktree", "list", "--porcelain"], {
27687
+ cwd: gitRoot,
27688
+ encoding: "utf8",
27689
+ stdio: ["ignore", "pipe", "ignore"],
27690
+ timeout: GIT_TIMEOUT_MS4
27691
+ }).split("\n").flatMap((line) => line.startsWith("worktree ") ? [realpathLoose(line.slice(9))] : []).filter((root) => root !== self);
27692
+ } catch {
27693
+ return [];
27694
+ }
27695
+ }
27696
+ function hookLocation(hookPath, dirs) {
27697
+ const path = realpathLoose(hookPath);
27698
+ if (isWithin(path, realpathLoose(dirs.gitDir)) || isWithin(path, realpathLoose(dirs.commonDir))) {
27699
+ return "repository";
27700
+ }
27701
+ const primDir = primGitHooksDirectory();
27702
+ if (primDir !== void 0 && isWithin(path, realpathLoose(primDir))) return "prim";
27703
+ const root = realpathLoose(dirs.gitRoot);
27704
+ if (!isWithin(path, root)) return "external";
27705
+ const nested = otherWorktreeRoots(dirs.gitRoot).filter((other) => isWithin(other, root));
27706
+ return nested.some((other) => isWithin(path, other)) ? "external" : "worktree";
27707
+ }
27296
27708
  function safeGitPath(root, value) {
27297
27709
  if (value.length === 0 || value !== value.trim() || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
27298
27710
  throw new Error("Git returned an unsafe hooks path");
27299
27711
  }
27300
- return isAbsolute7(value) ? resolve6(value) : resolve6(root, value);
27712
+ return isAbsolute8(value) ? resolve7(value) : resolve7(root, value);
27301
27713
  }
27302
- function resolveEffectiveManagedHook(spec, cwd = process.cwd()) {
27714
+ function resolveEffectiveGitHook(hookName, cwd = process.cwd()) {
27303
27715
  const gitRoot = gitToplevel(cwd);
27304
27716
  if (!gitRoot) throw new Error("not a git repository");
27305
- const value = execFileSync3("git", ["rev-parse", "--git-path", "hooks"], {
27306
- cwd: gitRoot,
27307
- encoding: "utf8",
27308
- stdio: ["ignore", "pipe", "ignore"],
27309
- timeout: GIT_TIMEOUT_MS3
27310
- }).replace(/\r?\n$/u, "");
27311
- const hooksDir = safeGitPath(gitRoot, value);
27312
- if (basename2(hooksDir) === "_" && basename2(dirname10(hooksDir)) === ".husky") {
27717
+ const lines = execFileSync4(
27718
+ "git",
27719
+ ["rev-parse", "--git-path", "hooks", "--absolute-git-dir", "--git-common-dir"],
27720
+ {
27721
+ cwd: gitRoot,
27722
+ encoding: "utf8",
27723
+ stdio: ["ignore", "pipe", "ignore"],
27724
+ timeout: GIT_TIMEOUT_MS4
27725
+ }
27726
+ ).replace(/\r?\n$/u, "").split(/\r?\n/u);
27727
+ if (lines.length !== 3) throw new Error("Git returned unexpected repository paths");
27728
+ const [hooksValue, gitDirValue, commonDirValue] = lines;
27729
+ const hooksDir = safeGitPath(gitRoot, hooksValue);
27730
+ const dirs = {
27731
+ gitRoot,
27732
+ gitDir: safeGitPath(gitRoot, gitDirValue),
27733
+ commonDir: safeGitPath(gitRoot, commonDirValue)
27734
+ };
27735
+ if (basename2(hooksDir) === "_" && basename2(dirname11(hooksDir)) === ".husky") {
27736
+ const hookPath2 = resolve7(dirname11(hooksDir), hookName);
27313
27737
  return {
27314
27738
  gitRoot,
27315
27739
  hooksDir,
27316
- hookPath: resolve6(dirname10(hooksDir), spec.hookName),
27740
+ hookPath: hookPath2,
27317
27741
  kind: "husky_v9",
27318
- dispatcherPath: resolve6(hooksDir, spec.hookName)
27742
+ dispatcherPath: resolve7(hooksDir, hookName),
27743
+ location: hookLocation(hookPath2, dirs)
27319
27744
  };
27320
27745
  }
27746
+ const hookPath = resolve7(hooksDir, hookName);
27321
27747
  return {
27322
27748
  gitRoot,
27323
27749
  hooksDir,
27324
- hookPath: resolve6(hooksDir, spec.hookName),
27325
- kind: "direct"
27750
+ hookPath,
27751
+ kind: "direct",
27752
+ location: hookLocation(hookPath, dirs)
27326
27753
  };
27327
27754
  }
27328
- function resolveEffectivePostCommitHook(cwd = process.cwd()) {
27329
- return resolveEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
27330
- }
27331
- function resolveEffectivePostRewriteHook(cwd = process.cwd()) {
27332
- return resolveEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
27333
- }
27334
27755
  function assertSafeFile(path, spec) {
27335
27756
  if (!existsSync10(path)) return void 0;
27336
27757
  const stat = lstatSync5(path);
@@ -27354,7 +27775,7 @@ function assertUsableHuskyDispatcher(target, spec) {
27354
27775
  if (!HUSKY_V9_DISPATCHERS.has(normalizedDispatcher)) {
27355
27776
  throw new Error(`unrecognized Husky ${spec.hookName} dispatcher: ${target.dispatcherPath}`);
27356
27777
  }
27357
- const huskyRuntime = resolve6(dirname10(target.dispatcherPath), "h");
27778
+ const huskyRuntime = resolve7(dirname11(target.dispatcherPath), "h");
27358
27779
  const runtimeStat = assertSafeFile(huskyRuntime, spec);
27359
27780
  const runtime = runtimeStat ? readHookFile(huskyRuntime) : void 0;
27360
27781
  const runtimeHash = runtime ? createHash8("sha256").update(runtime).digest("hex") : void 0;
@@ -27363,7 +27784,7 @@ function assertUsableHuskyDispatcher(target, spec) {
27363
27784
  }
27364
27785
  }
27365
27786
  function readHookFile(path) {
27366
- const value = readFileSync11(path);
27787
+ const value = readFileSync12(path);
27367
27788
  return Buffer.isBuffer(value) ? value : Buffer.from(value);
27368
27789
  }
27369
27790
  function decodeHookText(content, spec) {
@@ -27412,16 +27833,107 @@ function blockRange(content, spec) {
27412
27833
  }
27413
27834
  const start = starts[0];
27414
27835
  const end = ends[0] + endMarker.length;
27415
- const current = Buffer.from(spec.block());
27416
- return content.subarray(start, end).equals(current) ? { kind: "current", start, end } : { kind: "stale", start, end };
27836
+ const text = content.subarray(start, end).toString("utf8");
27837
+ if (canonicalHookBlock(text) === canonicalHookBlock(managedHookBlock(spec.hookName))) {
27838
+ return { kind: "current", start, end };
27839
+ }
27840
+ const version = hookBlockContractVersion(text);
27841
+ if (version !== void 0 && version > GIT_HOOK_CONTRACT_VERSION) {
27842
+ return { kind: "newer", start, end };
27843
+ }
27844
+ return { kind: "stale", start, end };
27845
+ }
27846
+ var HUSKY_V8_SOURCE_RE = /^\.[ \t]+"\$\(dirname(?:[ \t]+--)?[ \t]+"\$0"\)\/_\/husky\.sh"[ \t]*$/mu;
27847
+ function blockInsertionPoint(content, allowShebangless, spec, huskyV8) {
27848
+ const shebangEnd = shellInsertionPoint(content, allowShebangless, spec);
27849
+ if (!huskyV8) return shebangEnd;
27850
+ const rest = content.subarray(shebangEnd).toString("utf8");
27851
+ const match = HUSKY_V8_SOURCE_RE.exec(rest);
27852
+ if (!match) return shebangEnd;
27853
+ const lineEnd = match.index + match[0].length;
27854
+ const afterLine = rest[lineEnd] === "\n" ? lineEnd + 1 : lineEnd;
27855
+ return shebangEnd + Buffer.byteLength(rest.slice(0, afterLine), "utf8");
27856
+ }
27857
+ var HUSKY_SPEC_MAJOR_RE = /^\s*[~^]?=?\s*v?([0-9]+)(?:\.(?:[0-9]+|[xX*])){0,2}(?:-[0-9A-Za-z.-]+)?\s*$/u;
27858
+ function declaredHuskyMajor(dir) {
27859
+ for (let current = resolve7(dir); ; ) {
27860
+ const manifest = join16(current, "package.json");
27861
+ if (existsSync10(manifest)) {
27862
+ try {
27863
+ if (lstatSync5(manifest).size > MAX_HOOK_BYTES) return void 0;
27864
+ const parsed = JSON.parse(readFileSync12(manifest, "utf8"));
27865
+ const range = parsed.devDependencies?.husky ?? parsed.dependencies?.husky;
27866
+ const major = typeof range === "string" ? HUSKY_SPEC_MAJOR_RE.exec(range)?.[1] : void 0;
27867
+ return major === void 0 ? void 0 : Number(major);
27868
+ } catch {
27869
+ return void 0;
27870
+ }
27871
+ }
27872
+ const parent = dirname11(current);
27873
+ if (parent === current || existsSync10(join16(current, ".git"))) return void 0;
27874
+ current = parent;
27875
+ }
27417
27876
  }
27418
- function mergedContent(existing, allowShebangless, spec) {
27419
- const block = Buffer.from(spec.block());
27420
- if (!existing) {
27877
+ function isHuskyV8Hook(hookPath) {
27878
+ const runtime = join16(dirname11(hookPath), "_", "husky.sh");
27879
+ try {
27880
+ const stat = lstatSync5(runtime);
27881
+ return stat.isFile() && stat.size <= MAX_HOOK_BYTES && readFileSync12(runtime, "utf8").includes("husky_skip_init");
27882
+ } catch (error) {
27883
+ if (errorCode(error) !== "ENOENT") return false;
27884
+ }
27885
+ const major = declaredHuskyMajor(dirname11(dirname11(hookPath)));
27886
+ return major !== void 0 && major <= 8;
27887
+ }
27888
+ var TERMINAL_LINE_RE = /^(?:exit|return)(?:\s|;|$)/u;
27889
+ var REDIRECT_OPERATOR_RE = /^[0-9]*(?:<<?|>>?|<&|>&|<>|>\|)$/u;
27890
+ var REDIRECT_WORD_RE = /^[0-9]*(?:<|>)/u;
27891
+ function isReplacingExec(line) {
27892
+ if (!/^exec(?:\s|$)/u.test(line)) return false;
27893
+ const words = line.slice(4).trim().split(/\s+/u).filter(Boolean);
27894
+ for (let index = 0; index < words.length; index += 1) {
27895
+ const word = words[index];
27896
+ if (word.startsWith("#")) break;
27897
+ if (REDIRECT_OPERATOR_RE.test(word)) {
27898
+ index += 1;
27899
+ continue;
27900
+ }
27901
+ if (!REDIRECT_WORD_RE.test(word)) return true;
27902
+ }
27903
+ return false;
27904
+ }
27905
+ function terminatesBefore(content, from, offset) {
27906
+ const lines = content.subarray(from, offset).toString("utf8").split("\n");
27907
+ return lines.some((line) => TERMINAL_LINE_RE.test(line) || isReplacingExec(line));
27908
+ }
27909
+ function mayTerminateBefore(content, from, offset) {
27910
+ return content.subarray(from, offset).toString("utf8").split(/\n|;|&&|\|\||\||[{()]|\b(?:then|do|else)\b/u).some((part) => {
27911
+ const command = part.trim();
27912
+ return TERMINAL_LINE_RE.test(command) || isReplacingExec(command);
27913
+ });
27914
+ }
27915
+ function withoutRange(content, start, end) {
27916
+ const suffix = content.subarray(end);
27917
+ return Buffer.concat([
27918
+ content.subarray(0, start),
27919
+ suffix.at(0) === 10 ? suffix.subarray(1) : suffix
27920
+ ]);
27921
+ }
27922
+ function insertBlock(content, at, spec) {
27923
+ return Buffer.concat([
27924
+ content.subarray(0, at),
27925
+ Buffer.from(managedHookBlock(spec.hookName)),
27926
+ Buffer.from("\n"),
27927
+ content.subarray(at)
27928
+ ]);
27929
+ }
27930
+ function migratedLegacyContent(existing, allowShebangless, spec, huskyV8) {
27931
+ const text = existing.toString("utf8");
27932
+ if (spec.hookName === "pre-commit" && isOwnedStandalonePreCommit(text)) {
27421
27933
  return createdScaffold(spec);
27422
27934
  }
27423
- const range = blockRange(existing, spec);
27424
- if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
27935
+ if (spec.hookName !== "post-commit") return void 0;
27936
+ if (text.startsWith(`#!/bin/sh
27425
27937
  ${LEGACY_GLOBAL_OWNED_HEADER}
27426
27938
  `)) {
27427
27939
  const gateAt = existing.indexOf(Buffer.from('if [ "$(git config --get prim.active'));
@@ -27436,15 +27948,13 @@ ${LEGACY_GLOBAL_OWNED_HEADER}
27436
27948
  existing.subarray(0, gateAt),
27437
27949
  existing.subarray(chainAt)
27438
27950
  ]);
27439
- const shebangEnd2 = shellInsertionPoint(withoutLegacyGate, allowShebangless, spec);
27440
- return Buffer.concat([
27441
- withoutLegacyGate.subarray(0, shebangEnd2),
27442
- block,
27443
- Buffer.from("\n"),
27444
- withoutLegacyGate.subarray(shebangEnd2)
27445
- ]);
27951
+ return insertBlock(
27952
+ withoutLegacyGate,
27953
+ blockInsertionPoint(withoutLegacyGate, allowShebangless, spec, huskyV8),
27954
+ spec
27955
+ );
27446
27956
  }
27447
- if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
27957
+ if (text.startsWith(`#!/bin/sh
27448
27958
  ${LEGACY_PRIM_OWNED_HEADER}
27449
27959
  `)) {
27450
27960
  const prefixLength = legacyProjectPrefixLength(existing);
@@ -27453,42 +27963,59 @@ ${LEGACY_PRIM_OWNED_HEADER}
27453
27963
  }
27454
27964
  return Buffer.concat([createdScaffold(spec), existing.subarray(prefixLength)]);
27455
27965
  }
27456
- if (range.kind !== "absent") {
27457
- const oldCreatedPrefixes = [
27458
- Buffer.from(`#!/bin/sh
27459
- # ${spec.createdMark}
27460
-
27461
- `),
27462
- ...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
27463
- # ${LEGACY_PRIM_CREATED_MARK}
27464
-
27465
- `)] : []
27466
- ];
27467
- const oldCreated = oldCreatedPrefixes.some(
27966
+ return void 0;
27967
+ }
27968
+ function mergedContent(existing, allowShebangless, spec, relocate, huskyV8) {
27969
+ if (!existing) return { content: createdScaffold(spec), migrates: false };
27970
+ const range = blockRange(existing, spec);
27971
+ if (range.kind === "absent") {
27972
+ const migrated = migratedLegacyContent(existing, allowShebangless, spec, huskyV8);
27973
+ if (migrated) return { content: migrated, migrates: true };
27974
+ } else if (range.kind !== "newer") {
27975
+ const oldCreated = oldCreatedPrefixes(spec).some(
27468
27976
  (prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
27469
27977
  );
27470
27978
  if (oldCreated) {
27471
27979
  const suffix = existing.subarray(range.end);
27472
27980
  const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
27473
- return Buffer.concat([createdScaffold(spec), tail]);
27981
+ return {
27982
+ content: Buffer.concat([createdScaffold(spec), tail]),
27983
+ migrates: range.kind === "stale"
27984
+ };
27474
27985
  }
27475
27986
  }
27476
27987
  const shebangEnd = shellInsertionPoint(existing, allowShebangless, spec);
27477
- if (range.kind === "current" && range.start === shebangEnd) return existing;
27478
- let withoutBlock = existing;
27479
- if (range.kind !== "absent") {
27480
- const suffix = existing.subarray(range.end);
27481
- withoutBlock = Buffer.concat([
27482
- existing.subarray(0, range.start),
27483
- range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
27484
- ]);
27988
+ const insertion = blockInsertionPoint(existing, allowShebangless, spec, huskyV8);
27989
+ switch (range.kind) {
27990
+ case "newer":
27991
+ return { content: existing, migrates: false };
27992
+ case "absent":
27993
+ return { content: insertBlock(existing, insertion, spec), migrates: false };
27994
+ case "current":
27995
+ case "stale": {
27996
+ const misplaced = terminatesBefore(existing, shebangEnd, range.start) || range.start < insertion;
27997
+ if (relocate && misplaced) {
27998
+ const without = withoutRange(existing, range.start, range.end);
27999
+ return {
28000
+ content: insertBlock(
28001
+ without,
28002
+ blockInsertionPoint(without, allowShebangless, spec, huskyV8),
28003
+ spec
28004
+ ),
28005
+ migrates: range.kind === "stale"
28006
+ };
28007
+ }
28008
+ if (range.kind === "current") return { content: existing, migrates: false };
28009
+ return {
28010
+ content: Buffer.concat([
28011
+ existing.subarray(0, range.start),
28012
+ Buffer.from(managedHookBlock(spec.hookName)),
28013
+ existing.subarray(range.end)
28014
+ ]),
28015
+ migrates: true
28016
+ };
28017
+ }
27485
28018
  }
27486
- return Buffer.concat([
27487
- withoutBlock.subarray(0, shebangEnd),
27488
- block,
27489
- Buffer.from("\n"),
27490
- withoutBlock.subarray(shebangEnd)
27491
- ]);
27492
28019
  }
27493
28020
  function errorCode(error) {
27494
28021
  return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
@@ -27509,9 +28036,9 @@ function assertTargetUnchanged(path, expected, spec, stat) {
27509
28036
  }
27510
28037
  }
27511
28038
  function rewriteHookAtomically(path, next, expected, spec, stat) {
27512
- const parent = dirname10(path);
27513
- mkdirSync7(parent, { recursive: true });
27514
- const temporary = resolve6(
28039
+ const parent = dirname11(path);
28040
+ mkdirSync8(parent, { recursive: true });
28041
+ const temporary = resolve7(
27515
28042
  parent,
27516
28043
  `.${basename2(path)}.prim-${String(process.pid)}-${randomUUID2()}.tmp`
27517
28044
  );
@@ -27537,41 +28064,71 @@ function rewriteHookAtomically(path, next, expected, spec, stat) {
27537
28064
  }
27538
28065
  }
27539
28066
  try {
27540
- unlinkSync2(temporary);
28067
+ unlinkSync3(temporary);
27541
28068
  } catch (error) {
27542
28069
  if (errorCode(error) !== "ENOENT") {
27543
28070
  }
27544
28071
  }
27545
28072
  }
27546
28073
  }
27547
- function ensureEffectivePostCommitHook(cwd = process.cwd()) {
27548
- const target = resolveEffectivePostCommitHook(cwd);
27549
- return ensureTarget(target, POST_COMMIT_MANAGED_HOOK);
27550
- }
27551
- function ensureEffectivePostRewriteHook(cwd = process.cwd()) {
27552
- const target = resolveEffectivePostRewriteHook(cwd);
27553
- return ensureTarget(target, POST_REWRITE_MANAGED_HOOK);
28074
+ function workingLegacyBlock(content, target, spec) {
28075
+ try {
28076
+ const range = blockRange(content, spec);
28077
+ if (range.kind !== "stale") return false;
28078
+ const block = content.subarray(range.start, range.end).toString("utf8");
28079
+ const from = shellInsertionPoint(content, target.kind === "husky_v9", spec);
28080
+ return isRecognizedLegacyBlock(spec.hookName, block) && !mayTerminateBefore(content, from, range.start);
28081
+ } catch {
28082
+ return false;
28083
+ }
27554
28084
  }
27555
- function ensureTarget(target, spec, initialContent) {
28085
+ var AMBIENT_LOCATIONS = ["repository", "prim"];
28086
+ var EXPLICIT_LOCATIONS = ["repository", "prim", "worktree"];
28087
+ function ensureTarget(target, spec, policy) {
28088
+ const result = { hookName: spec.hookName, path: target.hookPath, kind: target.kind };
28089
+ const skip = (outcome) => ({
28090
+ ...result,
28091
+ changed: false,
28092
+ outcome
28093
+ });
27556
28094
  assertUsableHuskyDispatcher(target, spec);
27557
28095
  const stat = assertSafeFile(target.hookPath, spec);
27558
28096
  if (stat && target.kind === "direct" && (stat.mode & 64) === 0) {
27559
28097
  throw new Error(`existing ${spec.hookName} hook is not executable: ${target.hookPath}`);
27560
28098
  }
27561
28099
  const existing = stat ? readHookFile(target.hookPath) : void 0;
27562
- const next = !existing && initialContent !== void 0 ? Buffer.from(initialContent) : mergedContent(existing, target.kind === "husky_v9", spec);
27563
- if (!existing && initialContent !== void 0) {
27564
- const shebangEnd = shellInsertionPoint(next, target.kind === "husky_v9", spec);
27565
- const range = blockRange(next, spec);
27566
- if (range.kind !== "current" || range.start !== shebangEnd) {
27567
- throw new Error(`invalid initial Prim ${spec.hookName} scaffold`);
27568
- }
27569
- }
27570
- const contentChanged = !existing || !next.equals(existing);
27571
- if (contentChanged) {
27572
- rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
28100
+ if (policy.repairOnly && (!existing || blockRange(existing, spec).kind === "absent")) {
28101
+ return skip("skipped");
28102
+ }
28103
+ const huskyV8 = isHuskyV8Hook(target.hookPath);
28104
+ const next = mergedContent(existing, target.kind === "husky_v9", spec, policy.relocate, huskyV8);
28105
+ if (existing && next.content.equals(existing)) return skip("unchanged");
28106
+ if (!policy.writable(target.location)) {
28107
+ if (target.location === "worktree") return skip("deferred");
28108
+ return skip(existing && workingLegacyBlock(existing, target, spec) ? "kept" : "external");
28109
+ }
28110
+ if (next.migrates && inspectGitHookEntrypoint() !== "ready") return skip("runtime_missing");
28111
+ rewriteHookAtomically(target.hookPath, next.content, existing, spec, stat);
28112
+ return { ...result, changed: true, outcome: existing ? "updated" : "created" };
28113
+ }
28114
+ function ensureEffectiveGitHook(hookName, cwd = process.cwd(), options = {}) {
28115
+ const target = resolveEffectiveGitHook(hookName, cwd);
28116
+ if (gitHooksMode({ cwd: target.gitRoot }) === "manual") {
28117
+ return {
28118
+ hookName,
28119
+ path: target.hookPath,
28120
+ kind: target.kind,
28121
+ changed: false,
28122
+ outcome: "manual"
28123
+ };
27573
28124
  }
27574
- return { path: target.hookPath, changed: contentChanged || !stat, kind: target.kind };
28125
+ const explicit = (options.context ?? "explicit") === "explicit";
28126
+ const allowed = explicit ? EXPLICIT_LOCATIONS : AMBIENT_LOCATIONS;
28127
+ return ensureTarget(target, managedHookSpec(hookName), {
28128
+ writable: (location) => allowed.includes(location),
28129
+ relocate: explicit,
28130
+ repairOnly: options.repairOnly === true
28131
+ });
27575
28132
  }
27576
28133
 
27577
28134
  // src/lib/repository-binding.ts
@@ -27631,7 +28188,7 @@ async function bindRepository(root, options) {
27631
28188
  }
27632
28189
 
27633
28190
  // src/lib/workspace-id.ts
27634
- import { execFileSync as execFileSync4 } from "child_process";
28191
+ import { execFileSync as execFileSync5 } from "child_process";
27635
28192
  import { randomUUID as randomUUID3 } from "crypto";
27636
28193
  import {
27637
28194
  constants as constants6,
@@ -27641,15 +28198,15 @@ import {
27641
28198
  fsyncSync as fsyncSync3,
27642
28199
  linkSync,
27643
28200
  lstatSync as lstatSync6,
27644
- mkdirSync as mkdirSync8,
28201
+ mkdirSync as mkdirSync9,
27645
28202
  openSync as openSync5,
27646
- readFileSync as readFileSync12,
28203
+ readFileSync as readFileSync13,
27647
28204
  statSync as statSync3,
27648
- unlinkSync as unlinkSync3,
28205
+ unlinkSync as unlinkSync4,
27649
28206
  writeFileSync as writeFileSync5
27650
28207
  } from "fs";
27651
- import { dirname as dirname11, isAbsolute as isAbsolute8, join as join15, resolve as resolve7 } from "path";
27652
- var WORKSPACE_ID_RELATIVE_PATH = join15("prim", "workspace-id");
28208
+ import { dirname as dirname12, isAbsolute as isAbsolute9, join as join17, resolve as resolve8 } from "path";
28209
+ var WORKSPACE_ID_RELATIVE_PATH = join17("prim", "workspace-id");
27653
28210
  var MAX_ANCESTORS = 64;
27654
28211
  var MAX_GIT_FILE_BYTES = 4096;
27655
28212
  var MAX_ID_FILE_BYTES = 128;
@@ -27677,7 +28234,7 @@ function resolveWorkspaceIdPath(cwd) {
27677
28234
  if (fromFilesystem) return { status: "resolved", path: fromFilesystem };
27678
28235
  }
27679
28236
  try {
27680
- const output = execFileSync4(
28237
+ const output = execFileSync5(
27681
28238
  "git",
27682
28239
  ["rev-parse", "--is-inside-work-tree", "--git-path", WORKSPACE_ID_RELATIVE_PATH],
27683
28240
  {
@@ -27695,7 +28252,7 @@ function resolveWorkspaceIdPath(cwd) {
27695
28252
  }
27696
28253
  return {
27697
28254
  status: "resolved",
27698
- path: isAbsolute8(gitPath) ? resolve7(gitPath) : resolve7(cwd, gitPath)
28255
+ path: isAbsolute9(gitPath) ? resolve8(gitPath) : resolve8(cwd, gitPath)
27699
28256
  };
27700
28257
  } catch (error) {
27701
28258
  const code = errorCode2(error);
@@ -27706,21 +28263,21 @@ function resolveWorkspaceIdPath(cwd) {
27706
28263
  }
27707
28264
  }
27708
28265
  function resolveFromFilesystem(cwd) {
27709
- let current = resolve7(cwd);
28266
+ let current = resolve8(cwd);
27710
28267
  for (let depth = 0; depth < MAX_ANCESTORS; depth += 1) {
27711
- const dotGit = join15(current, ".git");
28268
+ const dotGit = join17(current, ".git");
27712
28269
  try {
27713
28270
  const stat = lstatSync6(dotGit);
27714
28271
  if (stat.isDirectory() && isGitDirectory(dotGit)) {
27715
- return join15(dotGit, WORKSPACE_ID_RELATIVE_PATH);
28272
+ return join17(dotGit, WORKSPACE_ID_RELATIVE_PATH);
27716
28273
  }
27717
28274
  if (stat.isFile() && stat.size <= MAX_GIT_FILE_BYTES) {
27718
- const contents = readFileSync12(dotGit, "utf8");
28275
+ const contents = readFileSync13(dotGit, "utf8");
27719
28276
  const match = /^gitdir: (.+?)(?:\r?\n)?$/.exec(contents);
27720
28277
  if (match?.[1]) {
27721
- const gitDirectory = isAbsolute8(match[1]) ? resolve7(match[1]) : resolve7(current, match[1]);
28278
+ const gitDirectory = isAbsolute9(match[1]) ? resolve8(match[1]) : resolve8(current, match[1]);
27722
28279
  if (isGitDirectory(gitDirectory)) {
27723
- return join15(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
28280
+ return join17(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
27724
28281
  }
27725
28282
  }
27726
28283
  }
@@ -27731,7 +28288,7 @@ function resolveFromFilesystem(cwd) {
27731
28288
  return null;
27732
28289
  }
27733
28290
  }
27734
- const parent = dirname11(current);
28291
+ const parent = dirname12(current);
27735
28292
  if (parent === current) break;
27736
28293
  current = parent;
27737
28294
  }
@@ -27739,7 +28296,7 @@ function resolveFromFilesystem(cwd) {
27739
28296
  }
27740
28297
  function isGitDirectory(path) {
27741
28298
  try {
27742
- return statSync3(join15(path, "HEAD")).isFile();
28299
+ return statSync3(join17(path, "HEAD")).isFile();
27743
28300
  } catch {
27744
28301
  return false;
27745
28302
  }
@@ -27758,7 +28315,7 @@ function inspectPath(path) {
27758
28315
  return { status: "corrupt", path, reason: "invalid_content" };
27759
28316
  }
27760
28317
  try {
27761
- const contents = readFileSync12(path, "utf8");
28318
+ const contents = readFileSync13(path, "utf8");
27762
28319
  const workspaceId = contents.endsWith("\n") ? contents.slice(0, -1) : contents;
27763
28320
  if ((contents === workspaceId || contents === `${workspaceId}
27764
28321
  `) && isCanonicalWorkspaceId(workspaceId)) {
@@ -27775,13 +28332,13 @@ function inspectPath(path) {
27775
28332
  }
27776
28333
  }
27777
28334
  function createWorkspaceId(path) {
27778
- const parent = dirname11(path);
28335
+ const parent = dirname12(path);
27779
28336
  const workspaceId = randomUUID3();
27780
- const temporaryPath = join15(parent, `.workspace-id-${process.pid}-${randomUUID3()}.tmp`);
28337
+ const temporaryPath = join17(parent, `.workspace-id-${process.pid}-${randomUUID3()}.tmp`);
27781
28338
  let temporaryCreated = false;
27782
28339
  let publishing = false;
27783
28340
  try {
27784
- mkdirSync8(parent, { recursive: true, mode: 448 });
28341
+ mkdirSync9(parent, { recursive: true, mode: 448 });
27785
28342
  chmodSync4(parent, 448);
27786
28343
  const fd = openSync5(
27787
28344
  temporaryPath,
@@ -27811,7 +28368,7 @@ function createWorkspaceId(path) {
27811
28368
  } finally {
27812
28369
  if (temporaryCreated) {
27813
28370
  try {
27814
- unlinkSync3(temporaryPath);
28371
+ unlinkSync4(temporaryPath);
27815
28372
  } catch {
27816
28373
  }
27817
28374
  }
@@ -27841,16 +28398,16 @@ import { createHash as createHash9, randomBytes as randomBytes4 } from "crypto";
27841
28398
  import {
27842
28399
  chmodSync as chmodSync5,
27843
28400
  existsSync as existsSync11,
27844
- mkdirSync as mkdirSync9,
27845
- readFileSync as readFileSync13,
27846
- readdirSync as readdirSync4,
28401
+ mkdirSync as mkdirSync10,
28402
+ readFileSync as readFileSync14,
28403
+ readdirSync as readdirSync5,
27847
28404
  renameSync as renameSync4,
27848
28405
  rmSync as rmSync6,
27849
28406
  statSync as statSync4,
27850
- unlinkSync as unlinkSync4,
28407
+ unlinkSync as unlinkSync5,
27851
28408
  writeFileSync as writeFileSync6
27852
28409
  } from "fs";
27853
- import { join as join16 } from "path";
28410
+ import { join as join18 } from "path";
27854
28411
 
27855
28412
  // src/daemon/decision-digest-cache.ts
27856
28413
  import { randomBytes as randomBytes3 } from "crypto";
@@ -28003,10 +28560,10 @@ var STATE_VERSION = 1;
28003
28560
  var DRAFT_STATE_VERSION = 2;
28004
28561
  var NO_CURSOR = -1;
28005
28562
  function stateRoot(namespace) {
28006
- return join16(primConfigDirectory(), namespace, "decision-digests");
28563
+ return join18(primConfigDirectory(), namespace, "decision-digests");
28007
28564
  }
28008
28565
  function draftStateRoot(namespace) {
28009
- return join16(primConfigDirectory(), namespace, "decision-draft-deliveries");
28566
+ return join18(primConfigDirectory(), namespace, "decision-draft-deliveries");
28010
28567
  }
28011
28568
  function workspaceFor(cwd) {
28012
28569
  try {
@@ -28018,12 +28575,12 @@ function workspaceFor(cwd) {
28018
28575
  function statePath(args) {
28019
28576
  const identity = `${args.siteUrl}\0${workspaceFor(args.cwd)}\0${args.sessionId}`;
28020
28577
  const key = createHash9("sha256").update(identity).digest("hex");
28021
- return join16(stateRoot(args.namespace), `${key}.json`);
28578
+ return join18(stateRoot(args.namespace), `${key}.json`);
28022
28579
  }
28023
28580
  function draftStatePath(args) {
28024
28581
  const identity = `${args.siteUrl}\0${workspaceFor(args.cwd)}\0${args.sessionId}\0${args.principalId}\0${args.organizationId}\0${args.credentialFingerprint}`;
28025
28582
  const key = createHash9("sha256").update(identity).digest("hex");
28026
- return join16(draftStateRoot(args.namespace), `${key}.json`);
28583
+ return join18(draftStateRoot(args.namespace), `${key}.json`);
28027
28584
  }
28028
28585
  function parseState(value) {
28029
28586
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -28064,7 +28621,7 @@ function parseDraftState(value) {
28064
28621
  function readState(path) {
28065
28622
  if (!existsSync11(path)) return void 0;
28066
28623
  try {
28067
- return parseState(JSON.parse(readFileSync13(path, "utf8")));
28624
+ return parseState(JSON.parse(readFileSync14(path, "utf8")));
28068
28625
  } catch {
28069
28626
  return void 0;
28070
28627
  }
@@ -28072,15 +28629,15 @@ function readState(path) {
28072
28629
  function readDraftState(path) {
28073
28630
  if (!existsSync11(path)) return void 0;
28074
28631
  try {
28075
- return parseDraftState(JSON.parse(readFileSync13(path, "utf8")));
28632
+ return parseDraftState(JSON.parse(readFileSync14(path, "utf8")));
28076
28633
  } catch {
28077
28634
  return void 0;
28078
28635
  }
28079
28636
  }
28080
28637
  function writeStateFile(path, directory, state) {
28081
- mkdirSync9(directory, { recursive: true, mode: 448 });
28638
+ mkdirSync10(directory, { recursive: true, mode: 448 });
28082
28639
  chmodSync5(directory, 448);
28083
- const temporary = join16(
28640
+ const temporary = join18(
28084
28641
  directory,
28085
28642
  `.${path.slice(directory.length + 1)}.tmp-${process.pid}-${randomBytes4(8).toString("hex")}`
28086
28643
  );
@@ -28095,7 +28652,7 @@ function writeStateFile(path, directory, state) {
28095
28652
  renameSync4(temporary, path);
28096
28653
  } finally {
28097
28654
  try {
28098
- unlinkSync4(temporary);
28655
+ unlinkSync5(temporary);
28099
28656
  } catch {
28100
28657
  }
28101
28658
  }
@@ -28104,14 +28661,14 @@ function cleanupStateFiles(directory, now) {
28104
28661
  if (!existsSync11(directory)) return;
28105
28662
  let names;
28106
28663
  try {
28107
- names = readdirSync4(directory);
28664
+ names = readdirSync5(directory);
28108
28665
  } catch {
28109
28666
  return;
28110
28667
  }
28111
28668
  const files = [];
28112
28669
  const residue = [];
28113
28670
  for (const name of names) {
28114
- const path = join16(directory, name);
28671
+ const path = join18(directory, name);
28115
28672
  try {
28116
28673
  const mtimeMs = statSync4(path).mtimeMs;
28117
28674
  if (name.endsWith(".json")) {
@@ -28126,7 +28683,7 @@ function cleanupStateFiles(directory, now) {
28126
28683
  const excess = files.filter((file) => !stale.some((candidate) => candidate.path === file.path)).sort((a, b) => a.mtimeMs - b.mtimeMs).slice(0, Math.max(0, files.length - CODEX_DIGEST_STATE_MAX_FILES));
28127
28684
  for (const file of [...stale, ...excess]) {
28128
28685
  try {
28129
- unlinkSync4(file.path);
28686
+ unlinkSync5(file.path);
28130
28687
  } catch {
28131
28688
  }
28132
28689
  }
@@ -28417,8 +28974,8 @@ async function prepareCodexContext(options) {
28417
28974
 
28418
28975
  // src/hooks/normalize.ts
28419
28976
  import { createHash as createHash10 } from "crypto";
28420
- import { realpathSync as realpathSync4 } from "fs";
28421
- import { isAbsolute as isAbsolute9, resolve as resolve8 } from "path";
28977
+ import { realpathSync as realpathSync5 } from "fs";
28978
+ import { isAbsolute as isAbsolute10, resolve as resolve9 } from "path";
28422
28979
  var HERMES_EVENT_MAP = {
28423
28980
  on_session_start: "SessionStart",
28424
28981
  on_session_end: "SessionEnd",
@@ -28466,16 +29023,16 @@ function cursorToolUseId(value) {
28466
29023
  function workspaceCandidates(value) {
28467
29024
  if (!Array.isArray(value)) return [];
28468
29025
  const roots = value.filter((item) => typeof item === "string" && item.length > 0);
28469
- if (roots.some((root) => !isAbsolute9(root))) {
29026
+ if (roots.some((root) => !isAbsolute10(root))) {
28470
29027
  throw new TypeError("Cursor workspace roots must be absolute");
28471
29028
  }
28472
29029
  return roots;
28473
29030
  }
28474
29031
  function pathIdentity(path) {
28475
29032
  try {
28476
- return realpathSync4.native(path);
29033
+ return realpathSync5.native(path);
28477
29034
  } catch {
28478
- return resolve8(path);
29035
+ return resolve9(path);
28479
29036
  }
28480
29037
  }
28481
29038
  function resolveCursorCwd(parsed) {
@@ -28490,17 +29047,17 @@ function resolveCursorCwd(parsed) {
28490
29047
  }
28491
29048
  const expectedRepo = [...repositoryRoots][0];
28492
29049
  const rawCwd = typeof parsed.cwd === "string" && parsed.cwd.length > 0 ? parsed.cwd : void 0;
28493
- if (rawCwd && !isAbsolute9(rawCwd)) {
29050
+ if (rawCwd && !isAbsolute10(rawCwd)) {
28494
29051
  throw new TypeError("Cursor hook cwd must be absolute");
28495
29052
  }
28496
29053
  const envelopeCwd = rawCwd ? pathIdentity(rawCwd) : void 0;
28497
29054
  const input = typeof parsed.tool_input === "object" && parsed.tool_input !== null && !Array.isArray(parsed.tool_input) ? parsed.tool_input : void 0;
28498
29055
  const rawToolCwd = parsed.tool_name === "Shell" && typeof input?.working_directory === "string" ? input.working_directory : void 0;
28499
29056
  const base = envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
28500
- if (rawToolCwd && !isAbsolute9(rawToolCwd) && !base) {
29057
+ if (rawToolCwd && !isAbsolute10(rawToolCwd) && !base) {
28501
29058
  throw new TypeError("relative Cursor tool cwd has no workspace base");
28502
29059
  }
28503
- const toolCwd = rawToolCwd ? pathIdentity(isAbsolute9(rawToolCwd) ? rawToolCwd : resolve8(base, rawToolCwd)) : void 0;
29060
+ const toolCwd = rawToolCwd ? pathIdentity(isAbsolute10(rawToolCwd) ? rawToolCwd : resolve9(base, rawToolCwd)) : void 0;
28504
29061
  const selected = toolCwd ?? envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
28505
29062
  if (!selected) {
28506
29063
  throw new TypeError("Cursor hook envelope has no unambiguous workspace");
@@ -28594,7 +29151,11 @@ var CODEX_PRIM_REMINDER = primReminder(CODEX_PRIM_ACTION, CODEX_PRIM_ACTION);
28594
29151
  async function activeProjectRoot(cwd) {
28595
29152
  try {
28596
29153
  const root = gitToplevel(cwd);
28597
- if (!root || !isRepoActiveForCapture(cwd)) return null;
29154
+ if (!root) return null;
29155
+ if (!isRepoActiveForCapture(cwd)) {
29156
+ if (repoActiveFlag(root) === "false") clearHooksWired(root);
29157
+ return null;
29158
+ }
28598
29159
  let shellGateActive = repoActiveFlag(root) === "true";
28599
29160
  if (!shellGateActive) {
28600
29161
  try {
@@ -28604,13 +29165,18 @@ async function activeProjectRoot(cwd) {
28604
29165
  }
28605
29166
  }
28606
29167
  if (shellGateActive) {
28607
- try {
28608
- ensureEffectivePostCommitHook(root);
28609
- } catch {
28610
- }
28611
- try {
28612
- ensureEffectivePostRewriteHook(root);
28613
- } catch {
29168
+ for (const hookName of MANAGED_GIT_HOOK_NAMES) {
29169
+ try {
29170
+ const result = ensureEffectiveGitHook(hookName, root, {
29171
+ context: "ambient",
29172
+ repairOnly: hookName === "pre-commit"
29173
+ });
29174
+ if (hookName === "post-commit" && // Manual mode: the user's own wiring is expected to reach prim too.
29175
+ ["created", "updated", "unchanged", "manual"].includes(result.outcome)) {
29176
+ recordHooksWired(root, { onlyIfAbsent: true });
29177
+ }
29178
+ } catch {
29179
+ }
28614
29180
  }
28615
29181
  }
28616
29182
  try {