@primitive.ai/prim 0.1.0-alpha.92 → 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.
- package/README.md +84 -10
- package/SKILL.md +5 -2
- package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
- package/dist/{chunk-26P57LAR.js → chunk-AXQQMX2U.js} +203 -9
- package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
- package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
- package/dist/chunk-FXF4JWK2.js +162 -0
- package/dist/{chunk-PCVGBE5B.js → chunk-G2VMA2QO.js} +2 -1
- package/dist/{chunk-NAHHFSLH.js → chunk-ICM2XCXS.js} +2 -2
- package/dist/{chunk-5HRR4AOR.js → chunk-IMK4JKTA.js} +1 -1
- package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
- package/dist/{chunk-ADUTW66W.js → chunk-O7YAW7G6.js} +6 -6
- package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
- package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
- package/dist/chunk-OYHYCK4K.js +13 -0
- package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
- package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
- package/dist/{chunk-LGGMJYY4.js → chunk-WUXFLE7X.js} +720 -369
- package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
- package/dist/daemon/server.js +372 -123
- package/dist/hook-runtime/daemon/server.js +372 -123
- package/dist/hook-runtime/hooks/post-commit.js +192 -58
- package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
- package/dist/hook-runtime/hooks/post-tool-use.js +150 -67
- package/dist/hook-runtime/hooks/pre-tool-use.js +87 -15
- package/dist/hook-runtime/hooks/prim-hook.js +197 -107
- package/dist/hook-runtime/hooks/session-end.js +53 -5
- package/dist/hook-runtime/hooks/session-start.js +880 -280
- package/dist/hook-runtime/index.js +4008 -2438
- package/dist/hook-runtime/statusline-main.js +23 -1
- package/dist/hooks/post-commit.js +15 -8
- package/dist/hooks/post-rewrite.js +11 -8
- package/dist/hooks/post-tool-use.js +13 -15
- package/dist/hooks/pre-commit.js +4 -4
- package/dist/hooks/pre-tool-use.js +10 -12
- package/dist/hooks/prim-hook.js +17 -14
- package/dist/hooks/session-end.js +4 -4
- package/dist/hooks/session-start.js +35 -45
- package/dist/index.js +1401 -620
- package/dist/statusline-main.js +23 -1
- package/package.json +1 -1
- package/dist/chunk-BTNNEH2W.js +0 -79
|
@@ -1,26 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
unattendedEnv
|
|
3
|
+
} from "./chunk-OYHYCK4K.js";
|
|
1
4
|
import {
|
|
2
5
|
isValidRepoSyncId,
|
|
3
6
|
setRepoSyncId,
|
|
4
7
|
setRepositoryBindingState
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DSWT6BCC.js";
|
|
6
9
|
import {
|
|
7
10
|
HttpError,
|
|
8
11
|
getClient
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-G2VMA2QO.js";
|
|
10
13
|
import {
|
|
14
|
+
GIT_HOOK_CONTRACT_VERSION,
|
|
11
15
|
binFile,
|
|
16
|
+
blockMarkers,
|
|
17
|
+
canonicalHookBlock,
|
|
12
18
|
compareSemver,
|
|
13
19
|
cursorConfigDirectory,
|
|
20
|
+
hookBlockContractVersion,
|
|
21
|
+
inspectGitHookEntrypoint,
|
|
22
|
+
managedHookBlock,
|
|
23
|
+
pinnedHookCommand,
|
|
14
24
|
pinnedNpxCommand
|
|
15
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AXQQMX2U.js";
|
|
16
26
|
import {
|
|
17
27
|
gitToplevel,
|
|
18
28
|
githubRepositoryFullName
|
|
19
29
|
} from "./chunk-H4UNYFUC.js";
|
|
20
30
|
import {
|
|
21
31
|
atomicWriteFile,
|
|
32
|
+
primConfigDirectory,
|
|
22
33
|
withFileLock
|
|
23
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-2GP5UFV7.js";
|
|
24
35
|
|
|
25
36
|
// src/lib/bin-cache.ts
|
|
26
37
|
import { chmodSync, mkdirSync } from "fs";
|
|
@@ -557,8 +568,8 @@ function applyBlock(existing, block, eol) {
|
|
|
557
568
|
return existing.slice(0, b) + block + existing.slice(e + SKILL_END.length);
|
|
558
569
|
}
|
|
559
570
|
if (existing.length === 0) return `${block}${eol}`;
|
|
560
|
-
const
|
|
561
|
-
return `${existing}${
|
|
571
|
+
const sep3 = existing.endsWith(eol) ? "" : eol;
|
|
572
|
+
return `${existing}${sep3}${block}${eol}`;
|
|
562
573
|
}
|
|
563
574
|
function removeBlock(existing) {
|
|
564
575
|
const b = existing.indexOf(SKILL_BEGIN);
|
|
@@ -698,60 +709,28 @@ function registerSkillCommands(program) {
|
|
|
698
709
|
});
|
|
699
710
|
}
|
|
700
711
|
|
|
701
|
-
// src/lib/
|
|
702
|
-
import {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
var PRIM_POST_REWRITE_BLOCK_END = "# <<< prim post-rewrite hook <<<";
|
|
724
|
-
var PRIM_POST_REWRITE_CREATED_MARK = "prim-created-post-rewrite-hook";
|
|
725
|
-
var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
|
|
726
|
-
var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
|
|
727
|
-
var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
|
|
728
|
-
var MAX_HOOK_BYTES = 1048576;
|
|
729
|
-
var GIT_TIMEOUT_MS = 1e3;
|
|
730
|
-
var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
|
|
731
|
-
"#!/bin/sh",
|
|
732
|
-
"#!/usr/bin/env sh",
|
|
733
|
-
"#!/bin/bash",
|
|
734
|
-
"#!/usr/bin/env bash",
|
|
735
|
-
"#!/bin/zsh",
|
|
736
|
-
"#!/usr/bin/env zsh"
|
|
737
|
-
]);
|
|
738
|
-
var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
|
|
739
|
-
'#!/usr/bin/env sh\n. "${0%/*}/h"',
|
|
740
|
-
'#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
|
|
741
|
-
]);
|
|
742
|
-
var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
|
|
743
|
-
var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
|
|
744
|
-
"d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
|
|
745
|
-
"b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
|
|
746
|
-
"9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
|
|
747
|
-
"3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
|
|
748
|
-
"548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
|
|
749
|
-
"c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
|
|
750
|
-
"ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
|
|
751
|
-
"70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
|
|
752
|
-
]);
|
|
753
|
-
function currentPostCommitBlock() {
|
|
754
|
-
return `${PRIM_POST_COMMIT_BLOCK_START}
|
|
712
|
+
// src/lib/git-hook-legacy.ts
|
|
713
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
714
|
+
var PRIM_MANAGED_MARK = "prim-managed-hook";
|
|
715
|
+
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
716
|
+
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
717
|
+
var POST_REWRITE = { hookName: "post-rewrite", binName: "prim-post-rewrite" };
|
|
718
|
+
function blockMarkers2(spec) {
|
|
719
|
+
return {
|
|
720
|
+
start: `# >>> prim ${spec.hookName} hook >>>`,
|
|
721
|
+
end: `# <<< prim ${spec.hookName} hook <<<`
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function hookShim(binName) {
|
|
725
|
+
return `{ ${pinnedHookCommand(binName)}; } || true`;
|
|
726
|
+
}
|
|
727
|
+
function gatedShim(binName) {
|
|
728
|
+
return `if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
729
|
+
${hookShim(binName)}
|
|
730
|
+
fi`;
|
|
731
|
+
}
|
|
732
|
+
function legacyPostCommitBlock() {
|
|
733
|
+
return `# >>> prim post-commit hook >>>
|
|
755
734
|
if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
756
735
|
prim_commit_sha=$(git rev-parse --verify HEAD 2>/dev/null) || prim_commit_sha=
|
|
757
736
|
case "$prim_commit_sha" in *[!0-9a-f]*|"") prim_commit_sha= ;; esac
|
|
@@ -780,13 +759,10 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
780
759
|
fi
|
|
781
760
|
unset prim_commit_sha
|
|
782
761
|
fi
|
|
783
|
-
|
|
762
|
+
# <<< prim post-commit hook <<<`;
|
|
784
763
|
}
|
|
785
|
-
function
|
|
786
|
-
return
|
|
787
|
-
}
|
|
788
|
-
function postRewriteHookBlock() {
|
|
789
|
-
return `${PRIM_POST_REWRITE_BLOCK_START}
|
|
764
|
+
function legacyPostRewriteBlock() {
|
|
765
|
+
return `# >>> prim post-rewrite hook >>>
|
|
790
766
|
if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
791
767
|
case "$1" in amend|rebase) prim_rewrite_source="$1" ;; *) prim_rewrite_source= ;; esac
|
|
792
768
|
if [ -n "$prim_rewrite_source" ]; then
|
|
@@ -825,22 +801,227 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
825
801
|
fi
|
|
826
802
|
unset prim_rewrite_source
|
|
827
803
|
fi
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
hookName: "
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
804
|
+
# <<< prim post-rewrite hook <<<`;
|
|
805
|
+
}
|
|
806
|
+
function legacyGlobalHookScript(spec) {
|
|
807
|
+
const chainExit = spec.hookName === PRE_COMMIT.hookName ? "|| exit $?" : "|| true";
|
|
808
|
+
const managedBlock = spec.hookName === POST_COMMIT.hookName ? legacyPostCommitBlock() : spec.hookName === POST_REWRITE.hookName ? legacyPostRewriteBlock() : void 0;
|
|
809
|
+
const invocation = managedBlock ?? gatedShim(spec.binName);
|
|
810
|
+
const managedRepoGuard = managedBlock ? ` && ! grep -Fq '${blockMarkers2(spec).start.slice(0, -3)}'">>>" "$repo_hook" 2>/dev/null` : "";
|
|
811
|
+
const beforeComments = managedBlock ? `${invocation}
|
|
812
|
+
` : "";
|
|
813
|
+
const afterComments = managedBlock ? "" : `${invocation}
|
|
814
|
+
`;
|
|
815
|
+
return `#!/bin/sh
|
|
816
|
+
${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
817
|
+
# Install/uninstall: prim hooks install|uninstall --scope user
|
|
818
|
+
# Runs prim only where activated \u2014 'prim enable' (this repo) or
|
|
819
|
+
# 'git config --global prim.active true' (every repo). Chains to the repo's own
|
|
820
|
+
# hook regardless, so inactive repos are unaffected.
|
|
821
|
+
${afterComments}common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
822
|
+
repo_hook="$common_dir/hooks/${spec.hookName}"
|
|
823
|
+
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null${managedRepoGuard}; then
|
|
824
|
+
"$repo_hook" "$@" ${chainExit}
|
|
825
|
+
fi
|
|
826
|
+
exit 0
|
|
827
|
+
`;
|
|
828
|
+
}
|
|
829
|
+
var PRE_COMMIT_GATE = 'if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then';
|
|
830
|
+
function preCommitLadder(fallback, failSoft) {
|
|
831
|
+
const soft = failSoft ? " || true" : "";
|
|
832
|
+
return `if command -v prim-pre-commit >/dev/null 2>&1; then
|
|
833
|
+
prim-pre-commit${soft}
|
|
834
|
+
elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
|
|
835
|
+
./node_modules/.bin/prim-pre-commit${soft}
|
|
836
|
+
else
|
|
837
|
+
${fallback} 2>/dev/null || true
|
|
838
|
+
fi`;
|
|
839
|
+
}
|
|
840
|
+
var LADDER_ALPHA_2 = preCommitLadder("npx --yes @primitive.ai/prim pre-commit-hook", false);
|
|
841
|
+
var LADDER_ALPHA_9 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", false);
|
|
842
|
+
var LADDER_ALPHA_35 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", true);
|
|
843
|
+
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`;
|
|
844
|
+
var PINNED_PRE_COMMIT_RE = new RegExp(
|
|
845
|
+
String.raw`^\{ (?:${PINNED_NPX_PRE_COMMIT}|if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else ${PINNED_NPX_PRE_COMMIT}; fi); \} \|\| true$`,
|
|
846
|
+
"u"
|
|
847
|
+
);
|
|
848
|
+
var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
|
|
849
|
+
function decodePinnedPath(token) {
|
|
850
|
+
if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
|
|
851
|
+
const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
|
|
852
|
+
if (!isAbsolute2(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
853
|
+
return void 0;
|
|
854
|
+
}
|
|
855
|
+
return value;
|
|
856
|
+
}
|
|
857
|
+
function isPinnedPreCommitInvocation(line) {
|
|
858
|
+
const match = PINNED_PRE_COMMIT_RE.exec(line);
|
|
859
|
+
if (!match) return false;
|
|
860
|
+
if (match.groups?.node === void 0) return true;
|
|
861
|
+
return Boolean(
|
|
862
|
+
decodePinnedPath(match.groups.node) && decodePinnedPath(match.groups.entry)?.endsWith("/dist/hooks/pre-commit.js")
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
function isLegacyPreCommitBody(body) {
|
|
866
|
+
if (body === LADDER_ALPHA_2 || body === LADDER_ALPHA_9 || body === LADDER_ALPHA_35) return true;
|
|
867
|
+
if (isPinnedPreCommitInvocation(body)) return true;
|
|
868
|
+
const prefix = `${PRE_COMMIT_GATE}
|
|
869
|
+
`;
|
|
870
|
+
if (!body.startsWith(prefix) || !body.endsWith("\nfi")) return false;
|
|
871
|
+
const inner = body.slice(prefix.length, -"\nfi".length);
|
|
872
|
+
return inner === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(inner);
|
|
873
|
+
}
|
|
874
|
+
function isLegacyOwnedPreCommitScript(content) {
|
|
875
|
+
const original = (ladder) => `#!/bin/sh
|
|
876
|
+
# prim pre-commit hook \u2014 auto-syncs affected specs on commit
|
|
877
|
+
# Installed by: prim hooks install
|
|
878
|
+
|
|
879
|
+
# Find the nearest node_modules/.bin with prim, or use npx
|
|
880
|
+
${ladder}
|
|
881
|
+
`;
|
|
882
|
+
if (content === original(LADDER_ALPHA_2) || content === original(LADDER_ALPHA_9)) return true;
|
|
883
|
+
if (content === `#!/bin/sh
|
|
884
|
+
# prim pre-commit hook \u2014 installed by: prim hooks install
|
|
885
|
+
|
|
886
|
+
${LADDER_ALPHA_9}
|
|
887
|
+
`) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
const prefix = `#!/bin/sh
|
|
891
|
+
# prim pre-commit hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
|
|
892
|
+
|
|
893
|
+
`;
|
|
894
|
+
if (!content.startsWith(prefix) || !content.endsWith("\n")) return false;
|
|
895
|
+
const body = content.slice(prefix.length, -1);
|
|
896
|
+
return body === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(body);
|
|
897
|
+
}
|
|
898
|
+
var PINNED_PACKAGE_VERSION_RE = /@primitive\.ai\/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/gu;
|
|
899
|
+
function normalizeOwnedGlobalHook(content, hookName) {
|
|
900
|
+
if (hookName === PRE_COMMIT.hookName) {
|
|
901
|
+
const lines = content.split("\n");
|
|
902
|
+
const invocationIndexes = lines.flatMap(
|
|
903
|
+
(line, index) => line.startsWith("{ ") && line.endsWith("; } || true") ? [index] : []
|
|
904
|
+
);
|
|
905
|
+
if (invocationIndexes.length !== 1) return null;
|
|
906
|
+
const invocationIndex = invocationIndexes[0];
|
|
907
|
+
if (!isPinnedPreCommitInvocation(lines[invocationIndex])) return null;
|
|
908
|
+
lines[invocationIndex] = "{ <recognized Prim pre-commit invocation>; } || true";
|
|
909
|
+
return lines.join("\n");
|
|
910
|
+
}
|
|
911
|
+
return content.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
|
|
912
|
+
}
|
|
913
|
+
function isLegacyOwnedGlobalHook(content, hookName) {
|
|
914
|
+
const spec = [PRE_COMMIT, POST_COMMIT, POST_REWRITE].find((s) => s.hookName === hookName);
|
|
915
|
+
if (!spec) return false;
|
|
916
|
+
const expected = legacyGlobalHookScript(spec);
|
|
917
|
+
if (content === expected) return true;
|
|
918
|
+
if (spec === PRE_COMMIT && content === expected.replace(gatedShim(spec.binName), `${PRE_COMMIT_GATE}
|
|
919
|
+
${LADDER_ALPHA_35}
|
|
920
|
+
fi`)) {
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
const normalized = normalizeOwnedGlobalHook(content, hookName);
|
|
924
|
+
return normalized !== null && normalized === normalizeOwnedGlobalHook(expected, hookName);
|
|
925
|
+
}
|
|
926
|
+
function legacyInlineHookBlock(hookName) {
|
|
927
|
+
return hookName === "post-commit" ? legacyPostCommitBlock() : legacyPostRewriteBlock();
|
|
928
|
+
}
|
|
929
|
+
function isRecognizedLegacyBlock(hookName, block) {
|
|
930
|
+
if (hookName === "pre-commit") {
|
|
931
|
+
const { start, end } = blockMarkers2(PRE_COMMIT);
|
|
932
|
+
const prefix = `${start}
|
|
933
|
+
`;
|
|
934
|
+
const suffix = `
|
|
935
|
+
${end}`;
|
|
936
|
+
if (!block.startsWith(prefix) || !block.endsWith(suffix)) return false;
|
|
937
|
+
return isLegacyPreCommitBody(block.slice(prefix.length, -suffix.length));
|
|
938
|
+
}
|
|
939
|
+
const expected = legacyInlineHookBlock(hookName);
|
|
940
|
+
const normalize = (text) => text.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
|
|
941
|
+
return normalize(block) === normalize(expected);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/lib/git-hooks.ts
|
|
945
|
+
import { execFileSync } from "child_process";
|
|
946
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
947
|
+
import {
|
|
948
|
+
constants as constants3,
|
|
949
|
+
closeSync as closeSync3,
|
|
950
|
+
existsSync as existsSync3,
|
|
951
|
+
fchmodSync,
|
|
952
|
+
fsyncSync,
|
|
953
|
+
lstatSync as lstatSync3,
|
|
954
|
+
mkdirSync as mkdirSync3,
|
|
955
|
+
openSync as openSync3,
|
|
956
|
+
readFileSync as readFileSync3,
|
|
957
|
+
realpathSync as realpathSync2,
|
|
958
|
+
renameSync,
|
|
959
|
+
unlinkSync as unlinkSync2,
|
|
960
|
+
writeFileSync
|
|
961
|
+
} from "fs";
|
|
962
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
963
|
+
var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
|
|
964
|
+
var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
|
|
965
|
+
var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
|
|
966
|
+
var MAX_HOOK_BYTES = 1048576;
|
|
967
|
+
var GIT_TIMEOUT_MS = 1e3;
|
|
968
|
+
var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
|
|
969
|
+
"#!/bin/sh",
|
|
970
|
+
"#!/usr/bin/env sh",
|
|
971
|
+
"#!/bin/bash",
|
|
972
|
+
"#!/usr/bin/env bash",
|
|
973
|
+
"#!/bin/zsh",
|
|
974
|
+
"#!/usr/bin/env zsh"
|
|
975
|
+
]);
|
|
976
|
+
var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
|
|
977
|
+
'#!/usr/bin/env sh\n. "${0%/*}/h"',
|
|
978
|
+
'#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
|
|
979
|
+
]);
|
|
980
|
+
var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
|
|
981
|
+
var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
|
|
982
|
+
"d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
|
|
983
|
+
"b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
|
|
984
|
+
"9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
|
|
985
|
+
"3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
|
|
986
|
+
"548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
|
|
987
|
+
"c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
|
|
988
|
+
"ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
|
|
989
|
+
"70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
|
|
990
|
+
]);
|
|
991
|
+
function managedHookSpecFor(hookName) {
|
|
992
|
+
const { start, end } = blockMarkers(hookName);
|
|
993
|
+
return {
|
|
994
|
+
hookName,
|
|
995
|
+
blockStart: start,
|
|
996
|
+
blockEnd: end,
|
|
997
|
+
createdMark: `prim-created-${hookName}-hook`
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
var MANAGED_HOOK_SPECS = {
|
|
1001
|
+
"pre-commit": managedHookSpecFor("pre-commit"),
|
|
1002
|
+
"post-commit": managedHookSpecFor("post-commit"),
|
|
1003
|
+
"post-rewrite": managedHookSpecFor("post-rewrite")
|
|
843
1004
|
};
|
|
1005
|
+
function managedHookSpec(hookName) {
|
|
1006
|
+
return MANAGED_HOOK_SPECS[hookName];
|
|
1007
|
+
}
|
|
1008
|
+
function gitHooksMode(options = {}) {
|
|
1009
|
+
try {
|
|
1010
|
+
const value = execFileSync(
|
|
1011
|
+
"git",
|
|
1012
|
+
["config", ...options.global ? ["--global"] : [], "--get", "prim.gitHooks"],
|
|
1013
|
+
{
|
|
1014
|
+
cwd: options.cwd,
|
|
1015
|
+
encoding: "utf8",
|
|
1016
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1017
|
+
timeout: GIT_TIMEOUT_MS
|
|
1018
|
+
}
|
|
1019
|
+
).trim();
|
|
1020
|
+
return value === "manual" ? "manual" : "auto";
|
|
1021
|
+
} catch {
|
|
1022
|
+
return "auto";
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
844
1025
|
function legacyFloatingInvocation() {
|
|
845
1026
|
return `if command -v prim-post-commit >/dev/null 2>&1; then
|
|
846
1027
|
prim-post-commit || true
|
|
@@ -857,15 +1038,6 @@ fi
|
|
|
857
1038
|
`);
|
|
858
1039
|
}
|
|
859
1040
|
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;
|
|
860
|
-
var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
|
|
861
|
-
function decodePinnedPath(token) {
|
|
862
|
-
if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
|
|
863
|
-
const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
|
|
864
|
-
if (!isAbsolute2(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
865
|
-
return void 0;
|
|
866
|
-
}
|
|
867
|
-
return value;
|
|
868
|
-
}
|
|
869
1041
|
function isPinnedInvocation(value) {
|
|
870
1042
|
const match = PINNED_INVOCATION_RE.exec(value);
|
|
871
1043
|
const node = decodePinnedPath(match?.groups?.node);
|
|
@@ -898,48 +1070,149 @@ function isKnownLegacyGlobalGate(value) {
|
|
|
898
1070
|
}
|
|
899
1071
|
function createdScaffold(spec) {
|
|
900
1072
|
return Buffer.from(`#!/bin/sh
|
|
901
|
-
${spec.
|
|
1073
|
+
${managedHookBlock(spec.hookName)}
|
|
902
1074
|
# ${spec.createdMark}
|
|
903
1075
|
`);
|
|
904
1076
|
}
|
|
1077
|
+
function oldCreatedPrefixes(spec) {
|
|
1078
|
+
return [
|
|
1079
|
+
Buffer.from(`#!/bin/sh
|
|
1080
|
+
# ${spec.createdMark}
|
|
1081
|
+
|
|
1082
|
+
`),
|
|
1083
|
+
Buffer.from(`#!/bin/sh
|
|
1084
|
+
# ${LEGACY_PRIM_CREATED_MARK}
|
|
1085
|
+
|
|
1086
|
+
`)
|
|
1087
|
+
];
|
|
1088
|
+
}
|
|
1089
|
+
function isOwnedStandalonePreCommit(content) {
|
|
1090
|
+
return content === "#!/bin/sh\nprim-pre-commit\n" || isLegacyOwnedPreCommitScript(content);
|
|
1091
|
+
}
|
|
1092
|
+
function realpathLoose(path) {
|
|
1093
|
+
const missing = [];
|
|
1094
|
+
let current = resolve3(path);
|
|
1095
|
+
for (; ; ) {
|
|
1096
|
+
try {
|
|
1097
|
+
return join4(realpathSync2.native(current), ...missing);
|
|
1098
|
+
} catch {
|
|
1099
|
+
const parent = dirname3(current);
|
|
1100
|
+
if (parent === current) return resolve3(path);
|
|
1101
|
+
missing.unshift(basename(current));
|
|
1102
|
+
current = parent;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function isWithin(path, directory) {
|
|
1107
|
+
const rel = relative2(directory, path);
|
|
1108
|
+
return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep2}`);
|
|
1109
|
+
}
|
|
1110
|
+
function primGitHooksDirectory() {
|
|
1111
|
+
try {
|
|
1112
|
+
return join4(primConfigDirectory(), "git-hooks");
|
|
1113
|
+
} catch {
|
|
1114
|
+
return void 0;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function otherWorktreeRoots(gitRoot) {
|
|
1118
|
+
try {
|
|
1119
|
+
const self = realpathLoose(gitRoot);
|
|
1120
|
+
return execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1121
|
+
cwd: gitRoot,
|
|
1122
|
+
encoding: "utf8",
|
|
1123
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1124
|
+
timeout: GIT_TIMEOUT_MS
|
|
1125
|
+
}).split("\n").flatMap((line) => line.startsWith("worktree ") ? [realpathLoose(line.slice(9))] : []).filter((root) => root !== self);
|
|
1126
|
+
} catch {
|
|
1127
|
+
return [];
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function hookLocation(hookPath, dirs) {
|
|
1131
|
+
const path = realpathLoose(hookPath);
|
|
1132
|
+
if (isWithin(path, realpathLoose(dirs.gitDir)) || isWithin(path, realpathLoose(dirs.commonDir))) {
|
|
1133
|
+
return "repository";
|
|
1134
|
+
}
|
|
1135
|
+
const primDir = primGitHooksDirectory();
|
|
1136
|
+
if (primDir !== void 0 && isWithin(path, realpathLoose(primDir))) return "prim";
|
|
1137
|
+
const root = realpathLoose(dirs.gitRoot);
|
|
1138
|
+
if (!isWithin(path, root)) return "external";
|
|
1139
|
+
const nested = otherWorktreeRoots(dirs.gitRoot).filter((other) => isWithin(other, root));
|
|
1140
|
+
return nested.some((other) => isWithin(path, other)) ? "external" : "worktree";
|
|
1141
|
+
}
|
|
1142
|
+
function hooksPathScope(cwd) {
|
|
1143
|
+
try {
|
|
1144
|
+
const scope = execFileSync("git", ["config", "--show-scope", "--get", "core.hooksPath"], {
|
|
1145
|
+
cwd,
|
|
1146
|
+
encoding: "utf8",
|
|
1147
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1148
|
+
timeout: GIT_TIMEOUT_MS
|
|
1149
|
+
}).split(" ")[0];
|
|
1150
|
+
return scope === "local" || scope === "worktree" || scope === "global" || scope === "system" || scope === "command" ? scope : void 0;
|
|
1151
|
+
} catch {
|
|
1152
|
+
return void 0;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function externalHookRemedy(hookName, cwd) {
|
|
1156
|
+
const snippet = `place \`prim hooks snippet ${hookName}\` there yourself`;
|
|
1157
|
+
switch (hooksPathScope(cwd)) {
|
|
1158
|
+
case "global":
|
|
1159
|
+
return `your global core.hooksPath runs every repository's hooks: ${snippet}, or have prim add it with \`prim hooks install --scope user --global-hooks-path\` (it changes every repository's hooks: an agent must ask the user first)`;
|
|
1160
|
+
case "system":
|
|
1161
|
+
return `the system core.hooksPath is shared by every user on this machine: ${snippet}, or ask its owner`;
|
|
1162
|
+
case "local":
|
|
1163
|
+
case "worktree":
|
|
1164
|
+
return `this repository's core.hooksPath points outside it: ${snippet}, or point core.hooksPath inside the repository`;
|
|
1165
|
+
default:
|
|
1166
|
+
return snippet;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
905
1169
|
function safeGitPath(root, value) {
|
|
906
1170
|
if (value.length === 0 || value !== value.trim() || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
907
1171
|
throw new Error("Git returned an unsafe hooks path");
|
|
908
1172
|
}
|
|
909
|
-
return
|
|
1173
|
+
return isAbsolute3(value) ? resolve3(value) : resolve3(root, value);
|
|
910
1174
|
}
|
|
911
|
-
function
|
|
1175
|
+
function resolveEffectiveGitHook(hookName, cwd = process.cwd()) {
|
|
912
1176
|
const gitRoot = gitToplevel(cwd);
|
|
913
1177
|
if (!gitRoot) throw new Error("not a git repository");
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1178
|
+
const lines = execFileSync(
|
|
1179
|
+
"git",
|
|
1180
|
+
["rev-parse", "--git-path", "hooks", "--absolute-git-dir", "--git-common-dir"],
|
|
1181
|
+
{
|
|
1182
|
+
cwd: gitRoot,
|
|
1183
|
+
encoding: "utf8",
|
|
1184
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1185
|
+
timeout: GIT_TIMEOUT_MS
|
|
1186
|
+
}
|
|
1187
|
+
).replace(/\r?\n$/u, "").split(/\r?\n/u);
|
|
1188
|
+
if (lines.length !== 3) throw new Error("Git returned unexpected repository paths");
|
|
1189
|
+
const [hooksValue, gitDirValue, commonDirValue] = lines;
|
|
1190
|
+
const hooksDir = safeGitPath(gitRoot, hooksValue);
|
|
1191
|
+
const dirs = {
|
|
1192
|
+
gitRoot,
|
|
1193
|
+
gitDir: safeGitPath(gitRoot, gitDirValue),
|
|
1194
|
+
commonDir: safeGitPath(gitRoot, commonDirValue)
|
|
1195
|
+
};
|
|
921
1196
|
if (basename(hooksDir) === "_" && basename(dirname3(hooksDir)) === ".husky") {
|
|
1197
|
+
const hookPath2 = resolve3(dirname3(hooksDir), hookName);
|
|
922
1198
|
return {
|
|
923
1199
|
gitRoot,
|
|
924
1200
|
hooksDir,
|
|
925
|
-
hookPath:
|
|
1201
|
+
hookPath: hookPath2,
|
|
926
1202
|
kind: "husky_v9",
|
|
927
|
-
dispatcherPath: resolve3(hooksDir,
|
|
1203
|
+
dispatcherPath: resolve3(hooksDir, hookName),
|
|
1204
|
+
location: hookLocation(hookPath2, dirs)
|
|
928
1205
|
};
|
|
929
1206
|
}
|
|
1207
|
+
const hookPath = resolve3(hooksDir, hookName);
|
|
930
1208
|
return {
|
|
931
1209
|
gitRoot,
|
|
932
1210
|
hooksDir,
|
|
933
|
-
hookPath
|
|
934
|
-
kind: "direct"
|
|
1211
|
+
hookPath,
|
|
1212
|
+
kind: "direct",
|
|
1213
|
+
location: hookLocation(hookPath, dirs)
|
|
935
1214
|
};
|
|
936
1215
|
}
|
|
937
|
-
function resolveEffectivePostCommitHook(cwd = process.cwd()) {
|
|
938
|
-
return resolveEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
|
|
939
|
-
}
|
|
940
|
-
function resolveEffectivePostRewriteHook(cwd = process.cwd()) {
|
|
941
|
-
return resolveEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
|
|
942
|
-
}
|
|
943
1216
|
function assertSafeFile(path, spec) {
|
|
944
1217
|
if (!existsSync3(path)) return void 0;
|
|
945
1218
|
const stat = lstatSync3(path);
|
|
@@ -1021,16 +1294,107 @@ function blockRange(content, spec) {
|
|
|
1021
1294
|
}
|
|
1022
1295
|
const start = starts[0];
|
|
1023
1296
|
const end = ends[0] + endMarker.length;
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1297
|
+
const text = content.subarray(start, end).toString("utf8");
|
|
1298
|
+
if (canonicalHookBlock(text) === canonicalHookBlock(managedHookBlock(spec.hookName))) {
|
|
1299
|
+
return { kind: "current", start, end };
|
|
1300
|
+
}
|
|
1301
|
+
const version = hookBlockContractVersion(text);
|
|
1302
|
+
if (version !== void 0 && version > GIT_HOOK_CONTRACT_VERSION) {
|
|
1303
|
+
return { kind: "newer", start, end };
|
|
1304
|
+
}
|
|
1305
|
+
return { kind: "stale", start, end };
|
|
1306
|
+
}
|
|
1307
|
+
var HUSKY_V8_SOURCE_RE = /^\.[ \t]+"\$\(dirname(?:[ \t]+--)?[ \t]+"\$0"\)\/_\/husky\.sh"[ \t]*$/mu;
|
|
1308
|
+
function blockInsertionPoint(content, allowShebangless, spec, huskyV8) {
|
|
1309
|
+
const shebangEnd = shellInsertionPoint(content, allowShebangless, spec);
|
|
1310
|
+
if (!huskyV8) return shebangEnd;
|
|
1311
|
+
const rest = content.subarray(shebangEnd).toString("utf8");
|
|
1312
|
+
const match = HUSKY_V8_SOURCE_RE.exec(rest);
|
|
1313
|
+
if (!match) return shebangEnd;
|
|
1314
|
+
const lineEnd = match.index + match[0].length;
|
|
1315
|
+
const afterLine = rest[lineEnd] === "\n" ? lineEnd + 1 : lineEnd;
|
|
1316
|
+
return shebangEnd + Buffer.byteLength(rest.slice(0, afterLine), "utf8");
|
|
1317
|
+
}
|
|
1318
|
+
var HUSKY_SPEC_MAJOR_RE = /^\s*[~^]?=?\s*v?([0-9]+)(?:\.(?:[0-9]+|[xX*])){0,2}(?:-[0-9A-Za-z.-]+)?\s*$/u;
|
|
1319
|
+
function declaredHuskyMajor(dir) {
|
|
1320
|
+
for (let current = resolve3(dir); ; ) {
|
|
1321
|
+
const manifest = join4(current, "package.json");
|
|
1322
|
+
if (existsSync3(manifest)) {
|
|
1323
|
+
try {
|
|
1324
|
+
if (lstatSync3(manifest).size > MAX_HOOK_BYTES) return void 0;
|
|
1325
|
+
const parsed = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
1326
|
+
const range = parsed.devDependencies?.husky ?? parsed.dependencies?.husky;
|
|
1327
|
+
const major = typeof range === "string" ? HUSKY_SPEC_MAJOR_RE.exec(range)?.[1] : void 0;
|
|
1328
|
+
return major === void 0 ? void 0 : Number(major);
|
|
1329
|
+
} catch {
|
|
1330
|
+
return void 0;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const parent = dirname3(current);
|
|
1334
|
+
if (parent === current || existsSync3(join4(current, ".git"))) return void 0;
|
|
1335
|
+
current = parent;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function isHuskyV8Hook(hookPath) {
|
|
1339
|
+
const runtime = join4(dirname3(hookPath), "_", "husky.sh");
|
|
1340
|
+
try {
|
|
1341
|
+
const stat = lstatSync3(runtime);
|
|
1342
|
+
return stat.isFile() && stat.size <= MAX_HOOK_BYTES && readFileSync3(runtime, "utf8").includes("husky_skip_init");
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (errorCode(error) !== "ENOENT") return false;
|
|
1345
|
+
}
|
|
1346
|
+
const major = declaredHuskyMajor(dirname3(dirname3(hookPath)));
|
|
1347
|
+
return major !== void 0 && major <= 8;
|
|
1348
|
+
}
|
|
1349
|
+
var TERMINAL_LINE_RE = /^(?:exit|return)(?:\s|;|$)/u;
|
|
1350
|
+
var REDIRECT_OPERATOR_RE = /^[0-9]*(?:<<?|>>?|<&|>&|<>|>\|)$/u;
|
|
1351
|
+
var REDIRECT_WORD_RE = /^[0-9]*(?:<|>)/u;
|
|
1352
|
+
function isReplacingExec(line) {
|
|
1353
|
+
if (!/^exec(?:\s|$)/u.test(line)) return false;
|
|
1354
|
+
const words = line.slice(4).trim().split(/\s+/u).filter(Boolean);
|
|
1355
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
1356
|
+
const word = words[index];
|
|
1357
|
+
if (word.startsWith("#")) break;
|
|
1358
|
+
if (REDIRECT_OPERATOR_RE.test(word)) {
|
|
1359
|
+
index += 1;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (!REDIRECT_WORD_RE.test(word)) return true;
|
|
1363
|
+
}
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
function terminatesBefore(content, from, offset) {
|
|
1367
|
+
const lines = content.subarray(from, offset).toString("utf8").split("\n");
|
|
1368
|
+
return lines.some((line) => TERMINAL_LINE_RE.test(line) || isReplacingExec(line));
|
|
1369
|
+
}
|
|
1370
|
+
function mayTerminateBefore(content, from, offset) {
|
|
1371
|
+
return content.subarray(from, offset).toString("utf8").split(/\n|;|&&|\|\||\||[{()]|\b(?:then|do|else)\b/u).some((part) => {
|
|
1372
|
+
const command = part.trim();
|
|
1373
|
+
return TERMINAL_LINE_RE.test(command) || isReplacingExec(command);
|
|
1374
|
+
});
|
|
1026
1375
|
}
|
|
1027
|
-
function
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1376
|
+
function withoutRange(content, start, end) {
|
|
1377
|
+
const suffix = content.subarray(end);
|
|
1378
|
+
return Buffer.concat([
|
|
1379
|
+
content.subarray(0, start),
|
|
1380
|
+
suffix.at(0) === 10 ? suffix.subarray(1) : suffix
|
|
1381
|
+
]);
|
|
1382
|
+
}
|
|
1383
|
+
function insertBlock(content, at, spec) {
|
|
1384
|
+
return Buffer.concat([
|
|
1385
|
+
content.subarray(0, at),
|
|
1386
|
+
Buffer.from(managedHookBlock(spec.hookName)),
|
|
1387
|
+
Buffer.from("\n"),
|
|
1388
|
+
content.subarray(at)
|
|
1389
|
+
]);
|
|
1390
|
+
}
|
|
1391
|
+
function migratedLegacyContent(existing, allowShebangless, spec, huskyV8) {
|
|
1392
|
+
const text = existing.toString("utf8");
|
|
1393
|
+
if (spec.hookName === "pre-commit" && isOwnedStandalonePreCommit(text)) {
|
|
1030
1394
|
return createdScaffold(spec);
|
|
1031
1395
|
}
|
|
1032
|
-
|
|
1033
|
-
if (
|
|
1396
|
+
if (spec.hookName !== "post-commit") return void 0;
|
|
1397
|
+
if (text.startsWith(`#!/bin/sh
|
|
1034
1398
|
${LEGACY_GLOBAL_OWNED_HEADER}
|
|
1035
1399
|
`)) {
|
|
1036
1400
|
const gateAt = existing.indexOf(Buffer.from('if [ "$(git config --get prim.active'));
|
|
@@ -1045,15 +1409,13 @@ ${LEGACY_GLOBAL_OWNED_HEADER}
|
|
|
1045
1409
|
existing.subarray(0, gateAt),
|
|
1046
1410
|
existing.subarray(chainAt)
|
|
1047
1411
|
]);
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
withoutLegacyGate
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
withoutLegacyGate.subarray(shebangEnd2)
|
|
1054
|
-
]);
|
|
1412
|
+
return insertBlock(
|
|
1413
|
+
withoutLegacyGate,
|
|
1414
|
+
blockInsertionPoint(withoutLegacyGate, allowShebangless, spec, huskyV8),
|
|
1415
|
+
spec
|
|
1416
|
+
);
|
|
1055
1417
|
}
|
|
1056
|
-
if (
|
|
1418
|
+
if (text.startsWith(`#!/bin/sh
|
|
1057
1419
|
${LEGACY_PRIM_OWNED_HEADER}
|
|
1058
1420
|
`)) {
|
|
1059
1421
|
const prefixLength = legacyProjectPrefixLength(existing);
|
|
@@ -1062,42 +1424,59 @@ ${LEGACY_PRIM_OWNED_HEADER}
|
|
|
1062
1424
|
}
|
|
1063
1425
|
return Buffer.concat([createdScaffold(spec), existing.subarray(prefixLength)]);
|
|
1064
1426
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
];
|
|
1076
|
-
const oldCreated = oldCreatedPrefixes.some(
|
|
1427
|
+
return void 0;
|
|
1428
|
+
}
|
|
1429
|
+
function mergedContent(existing, allowShebangless, spec, relocate, huskyV8) {
|
|
1430
|
+
if (!existing) return { content: createdScaffold(spec), migrates: false };
|
|
1431
|
+
const range = blockRange(existing, spec);
|
|
1432
|
+
if (range.kind === "absent") {
|
|
1433
|
+
const migrated = migratedLegacyContent(existing, allowShebangless, spec, huskyV8);
|
|
1434
|
+
if (migrated) return { content: migrated, migrates: true };
|
|
1435
|
+
} else if (range.kind !== "newer") {
|
|
1436
|
+
const oldCreated = oldCreatedPrefixes(spec).some(
|
|
1077
1437
|
(prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
|
|
1078
1438
|
);
|
|
1079
1439
|
if (oldCreated) {
|
|
1080
1440
|
const suffix = existing.subarray(range.end);
|
|
1081
1441
|
const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
|
|
1082
|
-
return
|
|
1442
|
+
return {
|
|
1443
|
+
content: Buffer.concat([createdScaffold(spec), tail]),
|
|
1444
|
+
migrates: range.kind === "stale"
|
|
1445
|
+
};
|
|
1083
1446
|
}
|
|
1084
1447
|
}
|
|
1085
1448
|
const shebangEnd = shellInsertionPoint(existing, allowShebangless, spec);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
existing
|
|
1092
|
-
|
|
1093
|
-
|
|
1449
|
+
const insertion = blockInsertionPoint(existing, allowShebangless, spec, huskyV8);
|
|
1450
|
+
switch (range.kind) {
|
|
1451
|
+
case "newer":
|
|
1452
|
+
return { content: existing, migrates: false };
|
|
1453
|
+
case "absent":
|
|
1454
|
+
return { content: insertBlock(existing, insertion, spec), migrates: false };
|
|
1455
|
+
case "current":
|
|
1456
|
+
case "stale": {
|
|
1457
|
+
const misplaced = terminatesBefore(existing, shebangEnd, range.start) || range.start < insertion;
|
|
1458
|
+
if (relocate && misplaced) {
|
|
1459
|
+
const without = withoutRange(existing, range.start, range.end);
|
|
1460
|
+
return {
|
|
1461
|
+
content: insertBlock(
|
|
1462
|
+
without,
|
|
1463
|
+
blockInsertionPoint(without, allowShebangless, spec, huskyV8),
|
|
1464
|
+
spec
|
|
1465
|
+
),
|
|
1466
|
+
migrates: range.kind === "stale"
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
if (range.kind === "current") return { content: existing, migrates: false };
|
|
1470
|
+
return {
|
|
1471
|
+
content: Buffer.concat([
|
|
1472
|
+
existing.subarray(0, range.start),
|
|
1473
|
+
Buffer.from(managedHookBlock(spec.hookName)),
|
|
1474
|
+
existing.subarray(range.end)
|
|
1475
|
+
]),
|
|
1476
|
+
migrates: true
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1094
1479
|
}
|
|
1095
|
-
return Buffer.concat([
|
|
1096
|
-
withoutBlock.subarray(0, shebangEnd),
|
|
1097
|
-
block,
|
|
1098
|
-
Buffer.from("\n"),
|
|
1099
|
-
withoutBlock.subarray(shebangEnd)
|
|
1100
|
-
]);
|
|
1101
1480
|
}
|
|
1102
1481
|
function errorCode(error) {
|
|
1103
1482
|
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
@@ -1157,137 +1536,143 @@ function unlinkHookUnchanged(path, expected, stat, spec) {
|
|
|
1157
1536
|
assertTargetUnchanged(path, expected, spec, stat);
|
|
1158
1537
|
unlinkSync2(path);
|
|
1159
1538
|
}
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1539
|
+
function workingLegacyBlock(content, target, spec) {
|
|
1540
|
+
try {
|
|
1541
|
+
const range = blockRange(content, spec);
|
|
1542
|
+
if (range.kind !== "stale") return false;
|
|
1543
|
+
const block = content.subarray(range.start, range.end).toString("utf8");
|
|
1544
|
+
const from = shellInsertionPoint(content, target.kind === "husky_v9", spec);
|
|
1545
|
+
return isRecognizedLegacyBlock(spec.hookName, block) && !mayTerminateBefore(content, from, range.start);
|
|
1546
|
+
} catch {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1167
1549
|
}
|
|
1168
|
-
|
|
1550
|
+
var AMBIENT_LOCATIONS = ["repository", "prim"];
|
|
1551
|
+
var EXPLICIT_LOCATIONS = ["repository", "prim", "worktree"];
|
|
1552
|
+
function ensureTarget(target, spec, policy) {
|
|
1553
|
+
const result = { hookName: spec.hookName, path: target.hookPath, kind: target.kind };
|
|
1554
|
+
const skip = (outcome) => ({
|
|
1555
|
+
...result,
|
|
1556
|
+
changed: false,
|
|
1557
|
+
outcome
|
|
1558
|
+
});
|
|
1169
1559
|
assertUsableHuskyDispatcher(target, spec);
|
|
1170
1560
|
const stat = assertSafeFile(target.hookPath, spec);
|
|
1171
1561
|
if (stat && target.kind === "direct" && (stat.mode & 64) === 0) {
|
|
1172
1562
|
throw new Error(`existing ${spec.hookName} hook is not executable: ${target.hookPath}`);
|
|
1173
1563
|
}
|
|
1174
1564
|
const existing = stat ? readHookFile(target.hookPath) : void 0;
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1565
|
+
if (policy.repairOnly && (!existing || blockRange(existing, spec).kind === "absent")) {
|
|
1566
|
+
return skip("skipped");
|
|
1567
|
+
}
|
|
1568
|
+
const huskyV8 = isHuskyV8Hook(target.hookPath);
|
|
1569
|
+
const next = mergedContent(existing, target.kind === "husky_v9", spec, policy.relocate, huskyV8);
|
|
1570
|
+
if (existing && next.content.equals(existing)) return skip("unchanged");
|
|
1571
|
+
if (!policy.writable(target.location)) {
|
|
1572
|
+
if (target.location === "worktree") return skip("deferred");
|
|
1573
|
+
return skip(existing && workingLegacyBlock(existing, target, spec) ? "kept" : "external");
|
|
1574
|
+
}
|
|
1575
|
+
if (next.migrates && inspectGitHookEntrypoint() !== "ready") return skip("runtime_missing");
|
|
1576
|
+
rewriteHookAtomically(target.hookPath, next.content, existing, spec, stat);
|
|
1577
|
+
return { ...result, changed: true, outcome: existing ? "updated" : "created" };
|
|
1578
|
+
}
|
|
1579
|
+
function ensureEffectiveGitHook(hookName, cwd = process.cwd(), options = {}) {
|
|
1580
|
+
const target = resolveEffectiveGitHook(hookName, cwd);
|
|
1581
|
+
if (gitHooksMode({ cwd: target.gitRoot }) === "manual") {
|
|
1582
|
+
return {
|
|
1583
|
+
hookName,
|
|
1584
|
+
path: target.hookPath,
|
|
1585
|
+
kind: target.kind,
|
|
1586
|
+
changed: false,
|
|
1587
|
+
outcome: "manual"
|
|
1588
|
+
};
|
|
1186
1589
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
hooksDir: dirname3(absolute),
|
|
1195
|
-
hookPath: absolute,
|
|
1196
|
-
kind: "direct"
|
|
1197
|
-
},
|
|
1198
|
-
POST_COMMIT_MANAGED_HOOK,
|
|
1199
|
-
initialContent
|
|
1200
|
-
);
|
|
1201
|
-
return { ...result, kind: "direct" };
|
|
1590
|
+
const explicit = (options.context ?? "explicit") === "explicit";
|
|
1591
|
+
const allowed = explicit ? EXPLICIT_LOCATIONS : AMBIENT_LOCATIONS;
|
|
1592
|
+
return ensureTarget(target, managedHookSpec(hookName), {
|
|
1593
|
+
writable: (location) => allowed.includes(location),
|
|
1594
|
+
relocate: explicit,
|
|
1595
|
+
repairOnly: options.repairOnly === true
|
|
1596
|
+
});
|
|
1202
1597
|
}
|
|
1203
|
-
function
|
|
1598
|
+
function explicitTarget(hookPath, options) {
|
|
1204
1599
|
const absolute = resolve3(hookPath);
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
return { ...result, kind: "direct" };
|
|
1600
|
+
return {
|
|
1601
|
+
gitRoot: dirname3(dirname3(absolute)),
|
|
1602
|
+
hooksDir: dirname3(absolute),
|
|
1603
|
+
hookPath: absolute,
|
|
1604
|
+
// A `.husky/<hook>` file is run by Husky's dispatcher via `sh`, so it needs
|
|
1605
|
+
// neither a shebang nor the executable bit.
|
|
1606
|
+
kind: options.husky ? "husky_v9" : "direct",
|
|
1607
|
+
// The caller chose this exact file (a --target, or a consented global dir).
|
|
1608
|
+
location: "external"
|
|
1609
|
+
};
|
|
1216
1610
|
}
|
|
1217
|
-
function
|
|
1218
|
-
|
|
1611
|
+
function hasCurrentHookBlock(hookName, hookPath) {
|
|
1612
|
+
try {
|
|
1613
|
+
const kind = blockRange(readHookFile(hookPath), managedHookSpec(hookName)).kind;
|
|
1614
|
+
return kind === "current" || kind === "newer";
|
|
1615
|
+
} catch {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1219
1618
|
}
|
|
1220
|
-
function
|
|
1221
|
-
return
|
|
1619
|
+
function ensureGitHookAtPath(hookName, hookPath, options = {}) {
|
|
1620
|
+
return ensureTarget(explicitTarget(hookPath, options), managedHookSpec(hookName), {
|
|
1621
|
+
writable: () => true,
|
|
1622
|
+
relocate: true,
|
|
1623
|
+
repairOnly: false
|
|
1624
|
+
});
|
|
1222
1625
|
}
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1626
|
+
function inspectEffectiveGitHook(hookName, cwd = process.cwd()) {
|
|
1627
|
+
const spec = managedHookSpec(hookName);
|
|
1628
|
+
const target = resolveEffectiveGitHook(hookName, cwd);
|
|
1629
|
+
const base = {
|
|
1630
|
+
...target,
|
|
1631
|
+
hookName,
|
|
1632
|
+
mode: gitHooksMode({ cwd: target.gitRoot }),
|
|
1633
|
+
entrypoint: inspectGitHookEntrypoint()
|
|
1634
|
+
};
|
|
1635
|
+
const uncovered = (reason2, executable2 = false) => ({ ...base, covered: false, executable: executable2, current: false, reason: reason2 });
|
|
1225
1636
|
try {
|
|
1226
1637
|
assertUsableHuskyDispatcher(target, spec);
|
|
1227
1638
|
} catch (error) {
|
|
1228
1639
|
const missing = error instanceof Error && (error.message.includes("missing or not executable") || error.message.includes("unsafe"));
|
|
1229
|
-
return
|
|
1230
|
-
...target,
|
|
1231
|
-
covered: false,
|
|
1232
|
-
executable: false,
|
|
1233
|
-
current: false,
|
|
1234
|
-
reason: missing ? "husky_dispatcher_missing" : "husky_dispatcher_invalid"
|
|
1235
|
-
};
|
|
1640
|
+
return uncovered(missing ? "husky_dispatcher_missing" : "husky_dispatcher_invalid");
|
|
1236
1641
|
}
|
|
1237
1642
|
let stat;
|
|
1238
1643
|
try {
|
|
1239
1644
|
stat = assertSafeFile(target.hookPath, spec);
|
|
1240
1645
|
} catch {
|
|
1241
|
-
return
|
|
1242
|
-
...target,
|
|
1243
|
-
covered: false,
|
|
1244
|
-
executable: false,
|
|
1245
|
-
current: false,
|
|
1246
|
-
reason: "unsafe_target"
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
if (!stat) {
|
|
1250
|
-
return {
|
|
1251
|
-
...target,
|
|
1252
|
-
covered: false,
|
|
1253
|
-
executable: false,
|
|
1254
|
-
current: false,
|
|
1255
|
-
reason: "missing"
|
|
1256
|
-
};
|
|
1646
|
+
return uncovered("unsafe_target");
|
|
1257
1647
|
}
|
|
1648
|
+
if (!stat) return uncovered("missing");
|
|
1258
1649
|
const executable = target.kind === "husky_v9" || (stat.mode & 64) !== 0;
|
|
1650
|
+
let range;
|
|
1651
|
+
let content;
|
|
1652
|
+
let insertion;
|
|
1259
1653
|
try {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const positioned = range.kind !== "absent" && range.start === shebangEnd;
|
|
1264
|
-
const current = range.kind === "current" && positioned;
|
|
1265
|
-
return {
|
|
1266
|
-
...target,
|
|
1267
|
-
covered: current && executable,
|
|
1268
|
-
executable,
|
|
1269
|
-
current,
|
|
1270
|
-
reason: range.kind === "current" && !positioned ? "unreachable_block" : !current ? "stale_block" : executable ? void 0 : "not_executable"
|
|
1271
|
-
};
|
|
1654
|
+
content = readHookFile(target.hookPath);
|
|
1655
|
+
insertion = shellInsertionPoint(content, target.kind === "husky_v9", spec);
|
|
1656
|
+
range = blockRange(content, spec);
|
|
1272
1657
|
} catch (error) {
|
|
1273
1658
|
const message = error instanceof Error ? error.message : "";
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
executable,
|
|
1279
|
-
current: false,
|
|
1280
|
-
reason
|
|
1281
|
-
};
|
|
1659
|
+
return uncovered(
|
|
1660
|
+
message.includes("binary") ? "binary" : message.includes("unsupported") ? "unsupported_interpreter" : "malformed_markers",
|
|
1661
|
+
executable
|
|
1662
|
+
);
|
|
1282
1663
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1664
|
+
const huskyV8 = isHuskyV8Hook(target.hookPath);
|
|
1665
|
+
const preferred = blockInsertionPoint(content, target.kind === "husky_v9", spec, huskyV8);
|
|
1666
|
+
const current = range.kind === "current" || range.kind === "newer";
|
|
1667
|
+
const legacy = range.kind === "stale" && workingLegacyBlock(content, target, spec);
|
|
1668
|
+
const reason = range.kind === "absent" ? "missing_block" : range.kind === "stale" && !legacy ? "stale_block" : terminatesBefore(content, insertion, range.start) ? "unreachable_block" : !executable ? "not_executable" : legacy ? "legacy_block" : base.entrypoint !== "ready" ? "entrypoint_missing" : range.start < preferred ? "misplaced_block" : void 0;
|
|
1669
|
+
return {
|
|
1670
|
+
...base,
|
|
1671
|
+
covered: reason === void 0,
|
|
1672
|
+
executable,
|
|
1673
|
+
current,
|
|
1674
|
+
...reason ? { reason } : {}
|
|
1675
|
+
};
|
|
1291
1676
|
}
|
|
1292
1677
|
function projectHooksPathIsConfigured(gitRoot) {
|
|
1293
1678
|
for (const scope of ["--worktree", "--local"]) {
|
|
@@ -1304,45 +1689,53 @@ function projectHooksPathIsConfigured(gitRoot) {
|
|
|
1304
1689
|
}
|
|
1305
1690
|
return false;
|
|
1306
1691
|
}
|
|
1307
|
-
function
|
|
1308
|
-
const
|
|
1309
|
-
if (
|
|
1310
|
-
|
|
1311
|
-
return uninstallEffectivePostCommitHook(gitRoot);
|
|
1692
|
+
function uninstallProjectGitHook(hookName, cwd = process.cwd()) {
|
|
1693
|
+
const target = projectGitHookTarget(hookName, cwd);
|
|
1694
|
+
if (target.location === "external") {
|
|
1695
|
+
return { path: target.hookPath, changed: false, removedFile: false, skipped: "external" };
|
|
1312
1696
|
}
|
|
1313
|
-
|
|
1314
|
-
cwd: gitRoot,
|
|
1315
|
-
encoding: "utf8",
|
|
1316
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1317
|
-
timeout: GIT_TIMEOUT_MS
|
|
1318
|
-
}).replace(/\r?\n$/u, "");
|
|
1319
|
-
return uninstallPostCommitHookAtPath(
|
|
1320
|
-
resolve3(safeGitPath(gitRoot, commonDir), "hooks", "post-commit")
|
|
1321
|
-
);
|
|
1697
|
+
return uninstallTarget(target, managedHookSpec(hookName));
|
|
1322
1698
|
}
|
|
1323
|
-
function
|
|
1699
|
+
function projectGitHookTarget(hookName, cwd = process.cwd()) {
|
|
1324
1700
|
const gitRoot = gitToplevel(cwd);
|
|
1325
1701
|
if (!gitRoot) throw new Error("not a git repository");
|
|
1326
|
-
if (projectHooksPathIsConfigured(gitRoot))
|
|
1327
|
-
|
|
1328
|
-
|
|
1702
|
+
if (projectHooksPathIsConfigured(gitRoot)) return resolveEffectiveGitHook(hookName, gitRoot);
|
|
1703
|
+
return {
|
|
1704
|
+
...explicitTarget(resolve3(projectHooksDir(gitRoot), hookName), {}),
|
|
1705
|
+
gitRoot,
|
|
1706
|
+
location: "repository"
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function projectHooksDir(gitRoot) {
|
|
1329
1710
|
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
1330
1711
|
cwd: gitRoot,
|
|
1331
1712
|
encoding: "utf8",
|
|
1332
1713
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1333
1714
|
timeout: GIT_TIMEOUT_MS
|
|
1334
1715
|
}).replace(/\r?\n$/u, "");
|
|
1335
|
-
return
|
|
1336
|
-
|
|
1337
|
-
|
|
1716
|
+
return resolve3(safeGitPath(gitRoot, commonDir), "hooks");
|
|
1717
|
+
}
|
|
1718
|
+
function uninstallGitHookAtPath(hookName, hookPath, options = {}) {
|
|
1719
|
+
return uninstallTarget(explicitTarget(hookPath, options), managedHookSpec(hookName));
|
|
1338
1720
|
}
|
|
1339
1721
|
function uninstallTarget(target, spec) {
|
|
1722
|
+
const unchanged = { path: target.hookPath, changed: false, removedFile: false };
|
|
1340
1723
|
const stat = assertSafeFile(target.hookPath, spec);
|
|
1341
|
-
if (!stat) return
|
|
1724
|
+
if (!stat) return unchanged;
|
|
1342
1725
|
const existing = readHookFile(target.hookPath);
|
|
1726
|
+
const remove = () => {
|
|
1727
|
+
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1728
|
+
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1729
|
+
};
|
|
1730
|
+
const rewrite = (next) => {
|
|
1731
|
+
rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
|
|
1732
|
+
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1733
|
+
};
|
|
1343
1734
|
const range = blockRange(existing, spec);
|
|
1344
1735
|
if (range.kind === "absent") {
|
|
1345
|
-
|
|
1736
|
+
const text = existing.toString("utf8");
|
|
1737
|
+
if (spec.hookName === "pre-commit" && isOwnedStandalonePreCommit(text)) return remove();
|
|
1738
|
+
if (spec.hookName === "post-commit" && text.startsWith(`#!/bin/sh
|
|
1346
1739
|
${LEGACY_PRIM_OWNED_HEADER}
|
|
1347
1740
|
`)) {
|
|
1348
1741
|
const prefixLength = legacyProjectPrefixLength(existing);
|
|
@@ -1350,20 +1743,9 @@ ${LEGACY_PRIM_OWNED_HEADER}
|
|
|
1350
1743
|
throw new Error("unrecognized legacy Prim post-commit invocation");
|
|
1351
1744
|
}
|
|
1352
1745
|
const tail = existing.subarray(prefixLength);
|
|
1353
|
-
|
|
1354
|
-
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1355
|
-
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1356
|
-
}
|
|
1357
|
-
rewriteHookAtomically(
|
|
1358
|
-
target.hookPath,
|
|
1359
|
-
Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
|
|
1360
|
-
existing,
|
|
1361
|
-
spec,
|
|
1362
|
-
stat
|
|
1363
|
-
);
|
|
1364
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1746
|
+
return tail.length === 0 ? remove() : rewrite(Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]));
|
|
1365
1747
|
}
|
|
1366
|
-
return
|
|
1748
|
+
return unchanged;
|
|
1367
1749
|
}
|
|
1368
1750
|
const shebangEnd = shellInsertionPoint(existing, target.kind === "husky_v9", spec);
|
|
1369
1751
|
const suffix = existing.subarray(range.end);
|
|
@@ -1372,77 +1754,16 @@ ${LEGACY_PRIM_OWNED_HEADER}
|
|
|
1372
1754
|
`);
|
|
1373
1755
|
if (range.start === shebangEnd && suffix.subarray(0, createdSuffix.length).equals(createdSuffix)) {
|
|
1374
1756
|
const tail = suffix.subarray(createdSuffix.length);
|
|
1375
|
-
|
|
1376
|
-
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1377
|
-
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1378
|
-
}
|
|
1379
|
-
rewriteHookAtomically(
|
|
1380
|
-
target.hookPath,
|
|
1381
|
-
Buffer.concat([existing.subarray(0, shebangEnd), tail]),
|
|
1382
|
-
existing,
|
|
1383
|
-
spec,
|
|
1384
|
-
stat
|
|
1385
|
-
);
|
|
1386
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1757
|
+
return tail.length === 0 ? remove() : rewrite(Buffer.concat([existing.subarray(0, shebangEnd), tail]));
|
|
1387
1758
|
}
|
|
1388
|
-
const
|
|
1389
|
-
Buffer.from(`#!/bin/sh
|
|
1390
|
-
# ${spec.createdMark}
|
|
1391
|
-
|
|
1392
|
-
`),
|
|
1393
|
-
...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
|
|
1394
|
-
# ${LEGACY_PRIM_CREATED_MARK}
|
|
1395
|
-
|
|
1396
|
-
`)] : []
|
|
1397
|
-
];
|
|
1398
|
-
const createdPrefix = createdPrefixes.find(
|
|
1759
|
+
const createdPrefix = oldCreatedPrefixes(spec).find(
|
|
1399
1760
|
(prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
|
|
1400
1761
|
);
|
|
1401
1762
|
if (createdPrefix) {
|
|
1402
1763
|
const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
|
|
1403
|
-
|
|
1404
|
-
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1405
|
-
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1406
|
-
}
|
|
1407
|
-
rewriteHookAtomically(
|
|
1408
|
-
target.hookPath,
|
|
1409
|
-
Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
|
|
1410
|
-
existing,
|
|
1411
|
-
spec,
|
|
1412
|
-
stat
|
|
1413
|
-
);
|
|
1414
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1764
|
+
return tail.length === 0 ? remove() : rewrite(Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]));
|
|
1415
1765
|
}
|
|
1416
|
-
|
|
1417
|
-
existing.subarray(0, range.start),
|
|
1418
|
-
range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
|
|
1419
|
-
]);
|
|
1420
|
-
rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
|
|
1421
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1422
|
-
}
|
|
1423
|
-
function uninstallPostCommitHookAtPath(hookPath) {
|
|
1424
|
-
const absolute = resolve3(hookPath);
|
|
1425
|
-
return uninstallTarget(
|
|
1426
|
-
{
|
|
1427
|
-
gitRoot: dirname3(dirname3(absolute)),
|
|
1428
|
-
hooksDir: dirname3(absolute),
|
|
1429
|
-
hookPath: absolute,
|
|
1430
|
-
kind: "direct"
|
|
1431
|
-
},
|
|
1432
|
-
POST_COMMIT_MANAGED_HOOK
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
function uninstallPostRewriteHookAtPath(hookPath) {
|
|
1436
|
-
const absolute = resolve3(hookPath);
|
|
1437
|
-
return uninstallTarget(
|
|
1438
|
-
{
|
|
1439
|
-
gitRoot: dirname3(dirname3(absolute)),
|
|
1440
|
-
hooksDir: dirname3(absolute),
|
|
1441
|
-
hookPath: absolute,
|
|
1442
|
-
kind: "direct"
|
|
1443
|
-
},
|
|
1444
|
-
POST_REWRITE_MANAGED_HOOK
|
|
1445
|
-
);
|
|
1766
|
+
return rewrite(withoutRange(existing, range.start, range.end));
|
|
1446
1767
|
}
|
|
1447
1768
|
|
|
1448
1769
|
// src/lib/repository-binding.ts
|
|
@@ -1504,20 +1825,49 @@ async function bindRepository(root, options) {
|
|
|
1504
1825
|
return bindRepositoryWithClient(root, getClient(), options);
|
|
1505
1826
|
}
|
|
1506
1827
|
|
|
1828
|
+
// src/daemon/self-heal.ts
|
|
1829
|
+
import { spawn } from "child_process";
|
|
1830
|
+
function kickDaemonEnsure(options = {}) {
|
|
1831
|
+
const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
|
|
1832
|
+
if (!primEntry) {
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
try {
|
|
1836
|
+
const args = [primEntry, "daemon", "ensure"];
|
|
1837
|
+
if (options.latestBootstrap ?? (options.platform ?? process.platform) === "darwin") {
|
|
1838
|
+
args.push("--latest-bootstrap");
|
|
1839
|
+
}
|
|
1840
|
+
const child = (options.spawnProcess ?? spawn)(
|
|
1841
|
+
options.nodeEntry ?? process.execPath,
|
|
1842
|
+
args,
|
|
1843
|
+
// Unattended: neither SessionStart's repair nor a drift heal is a person
|
|
1844
|
+
// running prim, so the child (and its descendants) never heal in turn.
|
|
1845
|
+
{ detached: true, stdio: "ignore", env: unattendedEnv() }
|
|
1846
|
+
);
|
|
1847
|
+
child.once("error", () => {
|
|
1848
|
+
});
|
|
1849
|
+
child.unref();
|
|
1850
|
+
return true;
|
|
1851
|
+
} catch {
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1507
1856
|
export {
|
|
1508
1857
|
warmBinCache,
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1858
|
+
isLegacyOwnedGlobalHook,
|
|
1859
|
+
gitHooksMode,
|
|
1860
|
+
isOwnedStandalonePreCommit,
|
|
1861
|
+
externalHookRemedy,
|
|
1862
|
+
resolveEffectiveGitHook,
|
|
1863
|
+
ensureEffectiveGitHook,
|
|
1864
|
+
hasCurrentHookBlock,
|
|
1865
|
+
ensureGitHookAtPath,
|
|
1866
|
+
inspectEffectiveGitHook,
|
|
1867
|
+
uninstallProjectGitHook,
|
|
1868
|
+
projectGitHookTarget,
|
|
1869
|
+
projectHooksDir,
|
|
1870
|
+
uninstallGitHookAtPath,
|
|
1521
1871
|
resolveRepositoryBinding,
|
|
1522
1872
|
bindRepositoryWithClient,
|
|
1523
1873
|
bindRepository,
|
|
@@ -1525,5 +1875,6 @@ export {
|
|
|
1525
1875
|
refreshClaudePlugins,
|
|
1526
1876
|
hasUsableCodexGuidance,
|
|
1527
1877
|
hasUsableCursorSkill,
|
|
1528
|
-
registerSkillCommands
|
|
1878
|
+
registerSkillCommands,
|
|
1879
|
+
kickDaemonEnsure
|
|
1529
1880
|
};
|