@primitive.ai/prim 0.1.0-alpha.64 → 0.1.0-alpha.66

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.
@@ -1,10 +1,12 @@
1
1
  import {
2
+ clearRepoSyncId,
2
3
  gitToplevel,
3
4
  githubRepositoryFullName,
4
5
  isValidRepoSyncId,
5
6
  setRepoSyncId
6
- } from "./chunk-XTNMSS4C.js";
7
+ } from "./chunk-TDRXHBEX.js";
7
8
  import {
9
+ HttpError,
8
10
  getClient,
9
11
  withFileLock
10
12
  } from "./chunk-CPX3BUOC.js";
@@ -614,6 +616,9 @@ import { basename, dirname as dirname3, isAbsolute as isAbsolute2, resolve as re
614
616
  var PRIM_POST_COMMIT_BLOCK_START = "# >>> prim post-commit hook >>>";
615
617
  var PRIM_POST_COMMIT_BLOCK_END = "# <<< prim post-commit hook <<<";
616
618
  var PRIM_CREATED_MARK = "prim-created-post-commit-hook";
619
+ var PRIM_POST_REWRITE_BLOCK_START = "# >>> prim post-rewrite hook >>>";
620
+ var PRIM_POST_REWRITE_BLOCK_END = "# <<< prim post-rewrite hook <<<";
621
+ var PRIM_POST_REWRITE_CREATED_MARK = "prim-created-post-rewrite-hook";
617
622
  var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
618
623
  var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
619
624
  var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
@@ -682,6 +687,67 @@ ${PRIM_POST_COMMIT_BLOCK_END}`;
682
687
  function postCommitHookBlock() {
683
688
  return currentPostCommitBlock();
684
689
  }
690
+ function postRewriteHookBlock() {
691
+ return `${PRIM_POST_REWRITE_BLOCK_START}
692
+ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
693
+ case "$1" in amend|rebase) prim_rewrite_source="$1" ;; *) prim_rewrite_source= ;; esac
694
+ if [ -n "$prim_rewrite_source" ]; then
695
+ prim_rewrite_pairs_file=$(mktemp "\${TMPDIR:-/tmp}/prim-post-rewrite-pairs.XXXXXXXX" 2>/dev/null) || prim_rewrite_pairs_file=
696
+ if [ -n "$prim_rewrite_pairs_file" ] && ! chmod 600 "$prim_rewrite_pairs_file" 2>/dev/null; then
697
+ rm -f "$prim_rewrite_pairs_file"
698
+ prim_rewrite_pairs_file=
699
+ fi
700
+ if [ -n "$prim_rewrite_pairs_file" ]; then
701
+ if cat > "$prim_rewrite_pairs_file"; then
702
+ exec < "$prim_rewrite_pairs_file"
703
+ prim_rewrite_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_rewrite_branch=
704
+ prim_cache_dir="\${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin"
705
+ prim_post_rewrite_ran=0
706
+ if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-rewrite" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-rewrite" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-1440}" 2>/dev/null)" ]; then
707
+ prim_node=$(cat "$prim_cache_dir/node")
708
+ prim_entry=$(cat "$prim_cache_dir/prim-post-rewrite")
709
+ if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
710
+ ( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; "$prim_node" "$prim_entry" ) </dev/null >/dev/null 2>&1 &
711
+ prim_post_rewrite_ran=1
712
+ fi
713
+ fi
714
+ if [ "$prim_post_rewrite_ran" -eq 0 ]; then
715
+ if [ -x "./node_modules/.bin/prim-post-rewrite" ]; then
716
+ ( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; ./node_modules/.bin/prim-post-rewrite ) </dev/null >/dev/null 2>&1 &
717
+ prim_post_rewrite_ran=1
718
+ elif command -v npx >/dev/null 2>&1; then
719
+ ( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; npx --yes -p @primitive.ai/prim@latest prim-post-rewrite ) </dev/null >/dev/null 2>&1 &
720
+ prim_post_rewrite_ran=1
721
+ fi
722
+ fi
723
+ if [ "$prim_post_rewrite_ran" -eq 0 ]; then
724
+ rm -f "$prim_rewrite_pairs_file"
725
+ fi
726
+ unset prim_cache_dir prim_post_rewrite_ran prim_node prim_entry prim_rewrite_branch
727
+ else
728
+ rm -f "$prim_rewrite_pairs_file"
729
+ fi
730
+ unset prim_rewrite_pairs_file
731
+ fi
732
+ fi
733
+ unset prim_rewrite_source
734
+ fi
735
+ ${PRIM_POST_REWRITE_BLOCK_END}`;
736
+ }
737
+ var POST_COMMIT_MANAGED_HOOK = {
738
+ hookName: "post-commit",
739
+ blockStart: PRIM_POST_COMMIT_BLOCK_START,
740
+ blockEnd: PRIM_POST_COMMIT_BLOCK_END,
741
+ createdMark: PRIM_CREATED_MARK,
742
+ block: postCommitHookBlock
743
+ };
744
+ var POST_REWRITE_MANAGED_HOOK = {
745
+ hookName: "post-rewrite",
746
+ blockStart: PRIM_POST_REWRITE_BLOCK_START,
747
+ blockEnd: PRIM_POST_REWRITE_BLOCK_END,
748
+ createdMark: PRIM_POST_REWRITE_CREATED_MARK,
749
+ block: postRewriteHookBlock
750
+ };
685
751
  function legacyFloatingInvocation() {
686
752
  return `if command -v prim-post-commit >/dev/null 2>&1; then
687
753
  prim-post-commit || true
@@ -737,10 +803,10 @@ function isKnownLegacyGlobalGate(value) {
737
803
  const suffix = Buffer.from("\nfi\n");
738
804
  return value.subarray(0, prefix.length).equals(prefix) && value.subarray(-suffix.length).equals(suffix) && isPinnedInvocation(value.subarray(prefix.length, -suffix.length).toString("utf8"));
739
805
  }
740
- function createdScaffold() {
806
+ function createdScaffold(spec) {
741
807
  return Buffer.from(`#!/bin/sh
742
- ${currentPostCommitBlock()}
743
- # ${PRIM_CREATED_MARK}
808
+ ${spec.block()}
809
+ # ${spec.createdMark}
744
810
  `);
745
811
  }
746
812
  function safeGitPath(root, value) {
@@ -749,7 +815,7 @@ function safeGitPath(root, value) {
749
815
  }
750
816
  return isAbsolute2(value) ? resolve3(value) : resolve3(root, value);
751
817
  }
752
- function resolveEffectivePostCommitHook(cwd = process.cwd()) {
818
+ function resolveEffectiveManagedHook(spec, cwd = process.cwd()) {
753
819
  const gitRoot = gitToplevel(cwd);
754
820
  if (!gitRoot) throw new Error("not a git repository");
755
821
  const value = execFileSync("git", ["rev-parse", "--git-path", "hooks"], {
@@ -763,42 +829,49 @@ function resolveEffectivePostCommitHook(cwd = process.cwd()) {
763
829
  return {
764
830
  gitRoot,
765
831
  hooksDir,
766
- hookPath: resolve3(dirname3(hooksDir), "post-commit"),
832
+ hookPath: resolve3(dirname3(hooksDir), spec.hookName),
767
833
  kind: "husky_v9",
768
- dispatcherPath: resolve3(hooksDir, "post-commit")
834
+ dispatcherPath: resolve3(hooksDir, spec.hookName)
769
835
  };
770
836
  }
771
837
  return {
772
838
  gitRoot,
773
839
  hooksDir,
774
- hookPath: resolve3(hooksDir, "post-commit"),
840
+ hookPath: resolve3(hooksDir, spec.hookName),
775
841
  kind: "direct"
776
842
  };
777
843
  }
778
- function assertSafeFile(path) {
844
+ function resolveEffectivePostCommitHook(cwd = process.cwd()) {
845
+ return resolveEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
846
+ }
847
+ function resolveEffectivePostRewriteHook(cwd = process.cwd()) {
848
+ return resolveEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
849
+ }
850
+ function assertSafeFile(path, spec) {
779
851
  if (!existsSync3(path)) return void 0;
780
852
  const stat = lstatSync3(path);
781
853
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_HOOK_BYTES) {
782
- throw new Error(`unsafe post-commit hook target: ${path}`);
854
+ throw new Error(`unsafe ${spec.hookName} hook target: ${path}`);
783
855
  }
784
856
  return stat;
785
857
  }
786
- function assertUsableHuskyDispatcher(target) {
858
+ function assertUsableHuskyDispatcher(target, spec) {
787
859
  if (!target.dispatcherPath) return;
788
- const stat = assertSafeFile(target.dispatcherPath);
860
+ const stat = assertSafeFile(target.dispatcherPath, spec);
789
861
  if (!stat || (stat.mode & 64) === 0) {
790
862
  throw new Error(
791
- `Husky post-commit dispatcher is missing or not executable: ${target.dispatcherPath}`
863
+ `Husky ${spec.hookName} dispatcher is missing or not executable: ${target.dispatcherPath}`
792
864
  );
793
865
  }
794
- const dispatcher = decodeHookText(readHookFile(target.dispatcherPath));
795
- if (dispatcher === HUSKY_DIRECT_DISPATCHER) return;
866
+ const dispatcher = decodeHookText(readHookFile(target.dispatcherPath), spec);
867
+ const directDispatcher = HUSKY_DIRECT_DISPATCHER.replaceAll("post-commit", spec.hookName);
868
+ if (dispatcher === directDispatcher) return;
796
869
  const normalizedDispatcher = dispatcher.endsWith("\n") ? dispatcher.slice(0, -1) : dispatcher;
797
870
  if (!HUSKY_V9_DISPATCHERS.has(normalizedDispatcher)) {
798
- throw new Error(`unrecognized Husky post-commit dispatcher: ${target.dispatcherPath}`);
871
+ throw new Error(`unrecognized Husky ${spec.hookName} dispatcher: ${target.dispatcherPath}`);
799
872
  }
800
873
  const huskyRuntime = resolve3(dirname3(target.dispatcherPath), "h");
801
- const runtimeStat = assertSafeFile(huskyRuntime);
874
+ const runtimeStat = assertSafeFile(huskyRuntime, spec);
802
875
  const runtime = runtimeStat ? readHookFile(huskyRuntime) : void 0;
803
876
  const runtimeHash = runtime ? createHash2("sha256").update(runtime).digest("hex") : void 0;
804
877
  if (!runtimeHash || !HUSKY_V9_RUNTIME_HASHES.has(runtimeHash)) {
@@ -809,28 +882,28 @@ function readHookFile(path) {
809
882
  const value = readFileSync3(path);
810
883
  return Buffer.isBuffer(value) ? value : Buffer.from(value);
811
884
  }
812
- function decodeHookText(content) {
813
- if (content.includes(0)) throw new Error("binary post-commit hook");
885
+ function decodeHookText(content, spec) {
886
+ if (content.includes(0)) throw new Error(`binary ${spec.hookName} hook`);
814
887
  try {
815
888
  return new TextDecoder("utf-8", { fatal: true }).decode(content);
816
889
  } catch {
817
- throw new Error("binary post-commit hook");
890
+ throw new Error(`binary ${spec.hookName} hook`);
818
891
  }
819
892
  }
820
- function supportedShebangEnd(content) {
821
- const text = decodeHookText(content);
893
+ function supportedShebangEnd(content, spec) {
894
+ const text = decodeHookText(content, spec);
822
895
  const newline = text.indexOf("\n");
823
896
  const shebang = newline === -1 ? text : text.slice(0, newline);
824
897
  if (!SUPPORTED_SHEBANGS.has(shebang) || newline === -1) {
825
- throw new Error("unsupported post-commit hook interpreter");
898
+ throw new Error(`unsupported ${spec.hookName} hook interpreter`);
826
899
  }
827
900
  return Buffer.byteLength(text.slice(0, newline + 1), "utf8");
828
901
  }
829
- function shellInsertionPoint(content, allowShebangless) {
830
- const text = decodeHookText(content);
831
- if (text.startsWith("#!")) return supportedShebangEnd(content);
902
+ function shellInsertionPoint(content, allowShebangless, spec) {
903
+ const text = decodeHookText(content, spec);
904
+ if (text.startsWith("#!")) return supportedShebangEnd(content, spec);
832
905
  if (allowShebangless) return 0;
833
- throw new Error("unsupported post-commit hook interpreter");
906
+ throw new Error(`unsupported ${spec.hookName} hook interpreter`);
834
907
  }
835
908
  function allIndexes(haystack, needle) {
836
909
  const indexes = [];
@@ -843,28 +916,28 @@ function allIndexes(haystack, needle) {
843
916
  }
844
917
  return indexes;
845
918
  }
846
- function blockRange(content) {
847
- decodeHookText(content);
848
- const startMarker = Buffer.from(PRIM_POST_COMMIT_BLOCK_START);
849
- const endMarker = Buffer.from(PRIM_POST_COMMIT_BLOCK_END);
919
+ function blockRange(content, spec) {
920
+ decodeHookText(content, spec);
921
+ const startMarker = Buffer.from(spec.blockStart);
922
+ const endMarker = Buffer.from(spec.blockEnd);
850
923
  const starts = allIndexes(content, startMarker);
851
924
  const ends = allIndexes(content, endMarker);
852
925
  if (starts.length === 0 && ends.length === 0) return { kind: "absent" };
853
926
  if (starts.length !== 1 || ends.length !== 1 || ends[0] < starts[0]) {
854
- throw new Error("malformed Prim post-commit markers");
927
+ throw new Error(`malformed Prim ${spec.hookName} markers`);
855
928
  }
856
929
  const start = starts[0];
857
930
  const end = ends[0] + endMarker.length;
858
- const current = Buffer.from(currentPostCommitBlock());
931
+ const current = Buffer.from(spec.block());
859
932
  return content.subarray(start, end).equals(current) ? { kind: "current", start, end } : { kind: "stale", start, end };
860
933
  }
861
- function mergedContent(existing, allowShebangless) {
862
- const block = Buffer.from(currentPostCommitBlock());
934
+ function mergedContent(existing, allowShebangless, spec) {
935
+ const block = Buffer.from(spec.block());
863
936
  if (!existing) {
864
- return createdScaffold();
937
+ return createdScaffold(spec);
865
938
  }
866
- const range = blockRange(existing);
867
- if (range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
939
+ const range = blockRange(existing, spec);
940
+ if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
868
941
  ${LEGACY_GLOBAL_OWNED_HEADER}
869
942
  `)) {
870
943
  const gateAt = existing.indexOf(Buffer.from('if [ "$(git config --get prim.active'));
@@ -879,7 +952,7 @@ ${LEGACY_GLOBAL_OWNED_HEADER}
879
952
  existing.subarray(0, gateAt),
880
953
  existing.subarray(chainAt)
881
954
  ]);
882
- const shebangEnd2 = shellInsertionPoint(withoutLegacyGate, allowShebangless);
955
+ const shebangEnd2 = shellInsertionPoint(withoutLegacyGate, allowShebangless, spec);
883
956
  return Buffer.concat([
884
957
  withoutLegacyGate.subarray(0, shebangEnd2),
885
958
  block,
@@ -887,25 +960,25 @@ ${LEGACY_GLOBAL_OWNED_HEADER}
887
960
  withoutLegacyGate.subarray(shebangEnd2)
888
961
  ]);
889
962
  }
890
- if (range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
963
+ if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
891
964
  ${LEGACY_PRIM_OWNED_HEADER}
892
965
  `)) {
893
966
  const prefixLength = legacyProjectPrefixLength(existing);
894
967
  if (prefixLength === void 0) {
895
968
  throw new Error("unrecognized legacy Prim post-commit invocation");
896
969
  }
897
- return Buffer.concat([createdScaffold(), existing.subarray(prefixLength)]);
970
+ return Buffer.concat([createdScaffold(spec), existing.subarray(prefixLength)]);
898
971
  }
899
972
  if (range.kind !== "absent") {
900
973
  const oldCreatedPrefixes = [
901
974
  Buffer.from(`#!/bin/sh
902
- # ${PRIM_CREATED_MARK}
975
+ # ${spec.createdMark}
903
976
 
904
977
  `),
905
- Buffer.from(`#!/bin/sh
978
+ ...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
906
979
  # ${LEGACY_PRIM_CREATED_MARK}
907
980
 
908
- `)
981
+ `)] : []
909
982
  ];
910
983
  const oldCreated = oldCreatedPrefixes.some(
911
984
  (prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
@@ -913,10 +986,10 @@ ${LEGACY_PRIM_OWNED_HEADER}
913
986
  if (oldCreated) {
914
987
  const suffix = existing.subarray(range.end);
915
988
  const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
916
- return Buffer.concat([createdScaffold(), tail]);
989
+ return Buffer.concat([createdScaffold(spec), tail]);
917
990
  }
918
991
  }
919
- const shebangEnd = shellInsertionPoint(existing, allowShebangless);
992
+ const shebangEnd = shellInsertionPoint(existing, allowShebangless, spec);
920
993
  if (range.kind === "current" && range.start === shebangEnd) return existing;
921
994
  let withoutBlock = existing;
922
995
  if (range.kind !== "absent") {
@@ -936,22 +1009,22 @@ ${LEGACY_PRIM_OWNED_HEADER}
936
1009
  function errorCode(error) {
937
1010
  return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
938
1011
  }
939
- function assertTargetUnchanged(path, expected, stat) {
1012
+ function assertTargetUnchanged(path, expected, spec, stat) {
940
1013
  let current;
941
1014
  try {
942
1015
  current = lstatSync3(path);
943
1016
  } catch (error) {
944
1017
  if (expected === void 0 && errorCode(error) === "ENOENT") return;
945
- throw new Error(`post-commit hook changed concurrently: ${path}`);
1018
+ throw new Error(`${spec.hookName} hook changed concurrently: ${path}`);
946
1019
  }
947
1020
  if (expected === void 0) {
948
- throw new Error(`post-commit hook appeared concurrently: ${path}`);
1021
+ throw new Error(`${spec.hookName} hook appeared concurrently: ${path}`);
949
1022
  }
950
1023
  if (!current.isFile() || current.isSymbolicLink() || current.size > MAX_HOOK_BYTES || current.dev !== stat?.dev || current.ino !== stat?.ino || (current.mode & 4095) !== ((stat?.mode ?? 0) & 4095) || !readHookFile(path).equals(expected)) {
951
- throw new Error(`post-commit hook changed concurrently: ${path}`);
1024
+ throw new Error(`${spec.hookName} hook changed concurrently: ${path}`);
952
1025
  }
953
1026
  }
954
- function rewriteHookAtomically(path, next, expected, stat) {
1027
+ function rewriteHookAtomically(path, next, expected, spec, stat) {
955
1028
  const parent = dirname3(path);
956
1029
  mkdirSync2(parent, { recursive: true });
957
1030
  const temporary = resolve3(
@@ -970,7 +1043,7 @@ function rewriteHookAtomically(path, next, expected, stat) {
970
1043
  fsyncSync2(fd);
971
1044
  closeSync3(fd);
972
1045
  fd = void 0;
973
- assertTargetUnchanged(path, expected, stat);
1046
+ assertTargetUnchanged(path, expected, spec, stat);
974
1047
  renameSync2(temporary, path);
975
1048
  } finally {
976
1049
  if (fd !== void 0) {
@@ -987,32 +1060,36 @@ function rewriteHookAtomically(path, next, expected, stat) {
987
1060
  }
988
1061
  }
989
1062
  }
990
- function unlinkHookUnchanged(path, expected, stat) {
991
- assertTargetUnchanged(path, expected, stat);
1063
+ function unlinkHookUnchanged(path, expected, stat, spec) {
1064
+ assertTargetUnchanged(path, expected, spec, stat);
992
1065
  unlinkSync(path);
993
1066
  }
994
1067
  function ensureEffectivePostCommitHook(cwd = process.cwd()) {
995
1068
  const target = resolveEffectivePostCommitHook(cwd);
996
- return ensureTarget(target);
1069
+ return ensureTarget(target, POST_COMMIT_MANAGED_HOOK);
1070
+ }
1071
+ function ensureEffectivePostRewriteHook(cwd = process.cwd()) {
1072
+ const target = resolveEffectivePostRewriteHook(cwd);
1073
+ return ensureTarget(target, POST_REWRITE_MANAGED_HOOK);
997
1074
  }
998
- function ensureTarget(target, initialContent) {
999
- assertUsableHuskyDispatcher(target);
1000
- const stat = assertSafeFile(target.hookPath);
1075
+ function ensureTarget(target, spec, initialContent) {
1076
+ assertUsableHuskyDispatcher(target, spec);
1077
+ const stat = assertSafeFile(target.hookPath, spec);
1001
1078
  if (stat && target.kind === "direct" && (stat.mode & 64) === 0) {
1002
- throw new Error(`existing post-commit hook is not executable: ${target.hookPath}`);
1079
+ throw new Error(`existing ${spec.hookName} hook is not executable: ${target.hookPath}`);
1003
1080
  }
1004
1081
  const existing = stat ? readHookFile(target.hookPath) : void 0;
1005
- const next = !existing && initialContent !== void 0 ? Buffer.from(initialContent) : mergedContent(existing, target.kind === "husky_v9");
1082
+ const next = !existing && initialContent !== void 0 ? Buffer.from(initialContent) : mergedContent(existing, target.kind === "husky_v9", spec);
1006
1083
  if (!existing && initialContent !== void 0) {
1007
- const shebangEnd = shellInsertionPoint(next, target.kind === "husky_v9");
1008
- const range = blockRange(next);
1084
+ const shebangEnd = shellInsertionPoint(next, target.kind === "husky_v9", spec);
1085
+ const range = blockRange(next, spec);
1009
1086
  if (range.kind !== "current" || range.start !== shebangEnd) {
1010
- throw new Error("invalid initial Prim post-commit scaffold");
1087
+ throw new Error(`invalid initial Prim ${spec.hookName} scaffold`);
1011
1088
  }
1012
1089
  }
1013
1090
  const contentChanged = !existing || !next.equals(existing);
1014
1091
  if (contentChanged) {
1015
- rewriteHookAtomically(target.hookPath, next, existing, stat);
1092
+ rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
1016
1093
  }
1017
1094
  return { path: target.hookPath, changed: contentChanged || !stat, kind: target.kind };
1018
1095
  }
@@ -1025,14 +1102,35 @@ function ensurePostCommitHookAtPath(hookPath, initialContent) {
1025
1102
  hookPath: absolute,
1026
1103
  kind: "direct"
1027
1104
  },
1105
+ POST_COMMIT_MANAGED_HOOK,
1106
+ initialContent
1107
+ );
1108
+ return { ...result, kind: "direct" };
1109
+ }
1110
+ function ensurePostRewriteHookAtPath(hookPath, initialContent) {
1111
+ const absolute = resolve3(hookPath);
1112
+ const result = ensureTarget(
1113
+ {
1114
+ gitRoot: dirname3(dirname3(absolute)),
1115
+ hooksDir: dirname3(absolute),
1116
+ hookPath: absolute,
1117
+ kind: "direct"
1118
+ },
1119
+ POST_REWRITE_MANAGED_HOOK,
1028
1120
  initialContent
1029
1121
  );
1030
1122
  return { ...result, kind: "direct" };
1031
1123
  }
1032
1124
  function inspectEffectivePostCommitHook(cwd = process.cwd()) {
1033
- const target = resolveEffectivePostCommitHook(cwd);
1125
+ return inspectEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
1126
+ }
1127
+ function inspectEffectivePostRewriteHook(cwd = process.cwd()) {
1128
+ return inspectEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
1129
+ }
1130
+ function inspectEffectiveManagedHook(spec, cwd) {
1131
+ const target = resolveEffectiveManagedHook(spec, cwd);
1034
1132
  try {
1035
- assertUsableHuskyDispatcher(target);
1133
+ assertUsableHuskyDispatcher(target, spec);
1036
1134
  } catch (error) {
1037
1135
  const missing = error instanceof Error && (error.message.includes("missing or not executable") || error.message.includes("unsafe"));
1038
1136
  return {
@@ -1045,7 +1143,7 @@ function inspectEffectivePostCommitHook(cwd = process.cwd()) {
1045
1143
  }
1046
1144
  let stat;
1047
1145
  try {
1048
- stat = assertSafeFile(target.hookPath);
1146
+ stat = assertSafeFile(target.hookPath, spec);
1049
1147
  } catch {
1050
1148
  return {
1051
1149
  ...target,
@@ -1067,8 +1165,8 @@ function inspectEffectivePostCommitHook(cwd = process.cwd()) {
1067
1165
  const executable = target.kind === "husky_v9" || (stat.mode & 64) !== 0;
1068
1166
  try {
1069
1167
  const content = readHookFile(target.hookPath);
1070
- const shebangEnd = shellInsertionPoint(content, target.kind === "husky_v9");
1071
- const range = blockRange(content);
1168
+ const shebangEnd = shellInsertionPoint(content, target.kind === "husky_v9", spec);
1169
+ const range = blockRange(content, spec);
1072
1170
  const positioned = range.kind !== "absent" && range.start === shebangEnd;
1073
1171
  const current = range.kind === "current" && positioned;
1074
1172
  return {
@@ -1092,7 +1190,11 @@ function inspectEffectivePostCommitHook(cwd = process.cwd()) {
1092
1190
  }
1093
1191
  function uninstallEffectivePostCommitHook(cwd = process.cwd()) {
1094
1192
  const target = resolveEffectivePostCommitHook(cwd);
1095
- return uninstallTarget(target);
1193
+ return uninstallTarget(target, POST_COMMIT_MANAGED_HOOK);
1194
+ }
1195
+ function uninstallEffectivePostRewriteHook(cwd = process.cwd()) {
1196
+ const target = resolveEffectivePostRewriteHook(cwd);
1197
+ return uninstallTarget(target, POST_REWRITE_MANAGED_HOOK);
1096
1198
  }
1097
1199
  function projectHooksPathIsConfigured(gitRoot) {
1098
1200
  for (const scope of ["--worktree", "--local"]) {
@@ -1125,13 +1227,29 @@ function uninstallProjectPostCommitHook(cwd = process.cwd()) {
1125
1227
  resolve3(safeGitPath(gitRoot, commonDir), "hooks", "post-commit")
1126
1228
  );
1127
1229
  }
1128
- function uninstallTarget(target) {
1129
- const stat = assertSafeFile(target.hookPath);
1230
+ function uninstallProjectPostRewriteHook(cwd = process.cwd()) {
1231
+ const gitRoot = gitToplevel(cwd);
1232
+ if (!gitRoot) throw new Error("not a git repository");
1233
+ if (projectHooksPathIsConfigured(gitRoot)) {
1234
+ return uninstallEffectivePostRewriteHook(gitRoot);
1235
+ }
1236
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
1237
+ cwd: gitRoot,
1238
+ encoding: "utf8",
1239
+ stdio: ["ignore", "pipe", "ignore"],
1240
+ timeout: GIT_TIMEOUT_MS
1241
+ }).replace(/\r?\n$/u, "");
1242
+ return uninstallPostRewriteHookAtPath(
1243
+ resolve3(safeGitPath(gitRoot, commonDir), "hooks", POST_REWRITE_MANAGED_HOOK.hookName)
1244
+ );
1245
+ }
1246
+ function uninstallTarget(target, spec) {
1247
+ const stat = assertSafeFile(target.hookPath, spec);
1130
1248
  if (!stat) return { path: target.hookPath, changed: false, removedFile: false };
1131
1249
  const existing = readHookFile(target.hookPath);
1132
- const range = blockRange(existing);
1250
+ const range = blockRange(existing, spec);
1133
1251
  if (range.kind === "absent") {
1134
- if (existing.toString("utf8").startsWith(`#!/bin/sh
1252
+ if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && existing.toString("utf8").startsWith(`#!/bin/sh
1135
1253
  ${LEGACY_PRIM_OWNED_HEADER}
1136
1254
  `)) {
1137
1255
  const prefixLength = legacyProjectPrefixLength(existing);
@@ -1140,47 +1258,49 @@ ${LEGACY_PRIM_OWNED_HEADER}
1140
1258
  }
1141
1259
  const tail = existing.subarray(prefixLength);
1142
1260
  if (tail.length === 0) {
1143
- unlinkHookUnchanged(target.hookPath, existing, stat);
1261
+ unlinkHookUnchanged(target.hookPath, existing, stat, spec);
1144
1262
  return { path: target.hookPath, changed: true, removedFile: true };
1145
1263
  }
1146
1264
  rewriteHookAtomically(
1147
1265
  target.hookPath,
1148
1266
  Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
1149
1267
  existing,
1268
+ spec,
1150
1269
  stat
1151
1270
  );
1152
1271
  return { path: target.hookPath, changed: true, removedFile: false };
1153
1272
  }
1154
1273
  return { path: target.hookPath, changed: false, removedFile: false };
1155
1274
  }
1156
- const shebangEnd = shellInsertionPoint(existing, target.kind === "husky_v9");
1275
+ const shebangEnd = shellInsertionPoint(existing, target.kind === "husky_v9", spec);
1157
1276
  const suffix = existing.subarray(range.end);
1158
1277
  const createdSuffix = Buffer.from(`
1159
- # ${PRIM_CREATED_MARK}
1278
+ # ${spec.createdMark}
1160
1279
  `);
1161
1280
  if (range.start === shebangEnd && suffix.subarray(0, createdSuffix.length).equals(createdSuffix)) {
1162
1281
  const tail = suffix.subarray(createdSuffix.length);
1163
1282
  if (tail.length === 0) {
1164
- unlinkHookUnchanged(target.hookPath, existing, stat);
1283
+ unlinkHookUnchanged(target.hookPath, existing, stat, spec);
1165
1284
  return { path: target.hookPath, changed: true, removedFile: true };
1166
1285
  }
1167
1286
  rewriteHookAtomically(
1168
1287
  target.hookPath,
1169
1288
  Buffer.concat([existing.subarray(0, shebangEnd), tail]),
1170
1289
  existing,
1290
+ spec,
1171
1291
  stat
1172
1292
  );
1173
1293
  return { path: target.hookPath, changed: true, removedFile: false };
1174
1294
  }
1175
1295
  const createdPrefixes = [
1176
1296
  Buffer.from(`#!/bin/sh
1177
- # ${PRIM_CREATED_MARK}
1297
+ # ${spec.createdMark}
1178
1298
 
1179
1299
  `),
1180
- Buffer.from(`#!/bin/sh
1300
+ ...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
1181
1301
  # ${LEGACY_PRIM_CREATED_MARK}
1182
1302
 
1183
- `)
1303
+ `)] : []
1184
1304
  ];
1185
1305
  const createdPrefix = createdPrefixes.find(
1186
1306
  (prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
@@ -1188,13 +1308,14 @@ ${LEGACY_PRIM_OWNED_HEADER}
1188
1308
  if (createdPrefix) {
1189
1309
  const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
1190
1310
  if (tail.length === 0) {
1191
- unlinkHookUnchanged(target.hookPath, existing, stat);
1311
+ unlinkHookUnchanged(target.hookPath, existing, stat, spec);
1192
1312
  return { path: target.hookPath, changed: true, removedFile: true };
1193
1313
  }
1194
1314
  rewriteHookAtomically(
1195
1315
  target.hookPath,
1196
1316
  Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
1197
1317
  existing,
1318
+ spec,
1198
1319
  stat
1199
1320
  );
1200
1321
  return { path: target.hookPath, changed: true, removedFile: false };
@@ -1203,17 +1324,32 @@ ${LEGACY_PRIM_OWNED_HEADER}
1203
1324
  existing.subarray(0, range.start),
1204
1325
  range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
1205
1326
  ]);
1206
- rewriteHookAtomically(target.hookPath, next, existing, stat);
1327
+ rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
1207
1328
  return { path: target.hookPath, changed: true, removedFile: false };
1208
1329
  }
1209
1330
  function uninstallPostCommitHookAtPath(hookPath) {
1210
1331
  const absolute = resolve3(hookPath);
1211
- return uninstallTarget({
1212
- gitRoot: dirname3(dirname3(absolute)),
1213
- hooksDir: dirname3(absolute),
1214
- hookPath: absolute,
1215
- kind: "direct"
1216
- });
1332
+ return uninstallTarget(
1333
+ {
1334
+ gitRoot: dirname3(dirname3(absolute)),
1335
+ hooksDir: dirname3(absolute),
1336
+ hookPath: absolute,
1337
+ kind: "direct"
1338
+ },
1339
+ POST_COMMIT_MANAGED_HOOK
1340
+ );
1341
+ }
1342
+ function uninstallPostRewriteHookAtPath(hookPath) {
1343
+ const absolute = resolve3(hookPath);
1344
+ return uninstallTarget(
1345
+ {
1346
+ gitRoot: dirname3(dirname3(absolute)),
1347
+ hooksDir: dirname3(absolute),
1348
+ hookPath: absolute,
1349
+ kind: "direct"
1350
+ },
1351
+ POST_REWRITE_MANAGED_HOOK
1352
+ );
1217
1353
  }
1218
1354
 
1219
1355
  // src/lib/repository-binding.ts
@@ -1222,29 +1358,47 @@ async function resolveRepositoryBinding(root, options) {
1222
1358
  if (!repositoryFullName) {
1223
1359
  throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
1224
1360
  }
1225
- const response = await getClient().post(
1226
- "/api/cli/repositories/bind",
1227
- { repositoryFullName },
1228
- options
1229
- );
1361
+ let response;
1362
+ try {
1363
+ response = await getClient().post(
1364
+ "/api/cli/repositories/bind",
1365
+ { repositoryFullName },
1366
+ options
1367
+ );
1368
+ } catch (error) {
1369
+ if (error instanceof HttpError && error.status === 404) {
1370
+ return { status: "pending", repositoryFullName };
1371
+ }
1372
+ throw error;
1373
+ }
1230
1374
  if (!isValidRepoSyncId(response.repoSyncId)) {
1231
1375
  throw new Error("server returned no repository binding");
1232
1376
  }
1233
- return { repoSyncId: response.repoSyncId, repositoryFullName };
1377
+ return { status: "connected", repoSyncId: response.repoSyncId, repositoryFullName };
1234
1378
  }
1235
1379
  async function bindRepository(root, options) {
1236
1380
  const binding = await resolveRepositoryBinding(root, options);
1237
- setRepoSyncId(root, binding.repoSyncId);
1381
+ if (binding.status === "connected") {
1382
+ setRepoSyncId(root, binding.repoSyncId);
1383
+ } else {
1384
+ clearRepoSyncId(root);
1385
+ }
1238
1386
  return binding;
1239
1387
  }
1240
1388
 
1241
1389
  export {
1242
1390
  postCommitHookBlock,
1391
+ postRewriteHookBlock,
1243
1392
  ensureEffectivePostCommitHook,
1393
+ ensureEffectivePostRewriteHook,
1244
1394
  ensurePostCommitHookAtPath,
1395
+ ensurePostRewriteHookAtPath,
1245
1396
  inspectEffectivePostCommitHook,
1397
+ inspectEffectivePostRewriteHook,
1246
1398
  uninstallProjectPostCommitHook,
1399
+ uninstallProjectPostRewriteHook,
1247
1400
  uninstallPostCommitHookAtPath,
1401
+ uninstallPostRewriteHookAtPath,
1248
1402
  resolveRepositoryBinding,
1249
1403
  bindRepository,
1250
1404
  printJson,