@outerlayer/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +202 -0
- package/README.md +320 -0
- package/dist/build-info.json +1 -0
- package/dist/chunk-4H56Y7O6.js +589 -0
- package/dist/chunk-A3WLZX2F.js +120 -0
- package/dist/chunk-A6NNKRJU.js +650 -0
- package/dist/chunk-D77LS3UI.js +42 -0
- package/dist/chunk-DEDV5V4U.js +87324 -0
- package/dist/chunk-ECSYRPCC.js +334 -0
- package/dist/chunk-IWIUYLDR.js +472 -0
- package/dist/chunk-JJP7YLMN.js +25 -0
- package/dist/chunk-KFYJV2ZG.js +7978 -0
- package/dist/chunk-OZ7C3XUE.js +34 -0
- package/dist/chunk-RCQXYLMO.js +96 -0
- package/dist/chunk-TFUIDMOB.js +34 -0
- package/dist/chunk-VNVZDWO3.js +10 -0
- package/dist/chunk-VU34RWDU.js +339 -0
- package/dist/chunk-WQ6VGRGZ.js +150 -0
- package/dist/chunk-XN2XZFTK.js +165 -0
- package/dist/chunk-YMTXR7PJ.js +200 -0
- package/dist/chunk-YRYMNBGQ.js +54 -0
- package/dist/chunk-Z5GEFMRW.js +45 -0
- package/dist/chunk-ZJSNLVCE.js +58 -0
- package/dist/cli-3JYGL2OB.js +5311 -0
- package/dist/config-POF7DEQW.js +7 -0
- package/dist/context-materialize-J6CRQ7O7.js +589 -0
- package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
- package/dist/emit-cmd-TWSYYEZI.js +208 -0
- package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
- package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
- package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
- package/dist/hook-fast-CBW4ZETX.js +8 -0
- package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
- package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
- package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/init-PTBITAUO.js +103 -0
- package/dist/login-cmd-IRX6LZT7.js +62 -0
- package/dist/logs-TRNPQM42.js +63 -0
- package/dist/loop-NFSOBUUJ.js +646 -0
- package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
- package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
- package/dist/paths-D2VGWWFI.js +6 -0
- package/dist/pidfile-PTW76F56.js +8 -0
- package/dist/status-WGOJTXZF.js +94 -0
- package/dist/statusline-fast-Z5U5CEC6.js +8 -0
- package/dist/sync-cmd-C5UGI4SF.js +15 -0
- package/dist/watch-V3K4PESQ.js +70 -0
- package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
- package/dist/work-cmd-CW26EZZO.js +16 -0
- package/dist/work-launch-YNNCKIH3.js +6 -0
- package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
- package/package.json +58 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
4
|
+
import { existsSync, readFileSync, unlinkSync, readdirSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
|
|
5
|
+
import { join, isAbsolute, dirname, basename, resolve } from 'path';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
9
|
+
var CONTEXT_EXCLUDE_MARKER = "outerlayer context materialization";
|
|
10
|
+
var VERSION = 1;
|
|
11
|
+
var CONTEXT_EXCLUDE_BEGIN_PREFIX = `# BEGIN ${CONTEXT_EXCLUDE_MARKER}`;
|
|
12
|
+
var BEGIN_PREFIX = CONTEXT_EXCLUDE_BEGIN_PREFIX;
|
|
13
|
+
var BEGIN = `${BEGIN_PREFIX} v${VERSION}`;
|
|
14
|
+
var END = `# END ${CONTEXT_EXCLUDE_MARKER}`;
|
|
15
|
+
var ENTRIES = ["/AGENTS.md", "/.claude/", "/.outerlayer/"];
|
|
16
|
+
var BLOCK_LINES = [BEGIN, ...ENTRIES, END];
|
|
17
|
+
function excludeFilePath(gitCommonDir) {
|
|
18
|
+
return join(gitCommonDir, "info", "exclude");
|
|
19
|
+
}
|
|
20
|
+
function readLines(path) {
|
|
21
|
+
if (!existsSync(path)) return [];
|
|
22
|
+
const content = readFileSync(path, "utf8");
|
|
23
|
+
if (content === "") return [];
|
|
24
|
+
const lines = content.split("\n");
|
|
25
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
function writeLines(path, lines) {
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeFileSync(path, lines.length ? lines.join("\n") + "\n" : "");
|
|
31
|
+
}
|
|
32
|
+
function findWellFormedBlock(lines) {
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < lines.length) {
|
|
35
|
+
if (!lines[i].startsWith(BEGIN_PREFIX)) {
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const beginIdx = i;
|
|
40
|
+
let endIdx = -1;
|
|
41
|
+
let j = beginIdx + 1;
|
|
42
|
+
for (; j < lines.length; j++) {
|
|
43
|
+
if (lines[j] === END) {
|
|
44
|
+
endIdx = j;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (lines[j].startsWith(BEGIN_PREFIX)) break;
|
|
48
|
+
}
|
|
49
|
+
if (endIdx !== -1) return { beginIdx, endIdx };
|
|
50
|
+
i = j;
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
function ensureContextExcludes(gitCommonDir) {
|
|
55
|
+
const path = excludeFilePath(gitCommonDir);
|
|
56
|
+
const lines = readLines(path);
|
|
57
|
+
const block = findWellFormedBlock(lines);
|
|
58
|
+
if (block !== void 0) {
|
|
59
|
+
const { beginIdx, endIdx } = block;
|
|
60
|
+
const current = lines.slice(beginIdx, endIdx + 1);
|
|
61
|
+
if (current.length === BLOCK_LINES.length && current.every((l, i) => l === BLOCK_LINES[i])) {
|
|
62
|
+
return { changed: false };
|
|
63
|
+
}
|
|
64
|
+
const next = [...lines.slice(0, beginIdx), ...BLOCK_LINES, ...lines.slice(endIdx + 1)];
|
|
65
|
+
writeLines(path, next);
|
|
66
|
+
return { changed: true };
|
|
67
|
+
}
|
|
68
|
+
writeLines(path, [...lines, ...BLOCK_LINES]);
|
|
69
|
+
return { changed: true };
|
|
70
|
+
}
|
|
71
|
+
function contextExcludesPresent(gitCommonDir) {
|
|
72
|
+
const lines = readLines(excludeFilePath(gitCommonDir));
|
|
73
|
+
const beginIdx = lines.findIndex((l) => l === BEGIN);
|
|
74
|
+
if (beginIdx === -1) return false;
|
|
75
|
+
return BLOCK_LINES.every((l, i) => lines[beginIdx + i] === l);
|
|
76
|
+
}
|
|
77
|
+
var POST_REWRITE_MARKER = "# outerlayer post-rewrite journal";
|
|
78
|
+
var POST_REWRITE_VERSION = `${POST_REWRITE_MARKER} v1`;
|
|
79
|
+
var POST_REWRITE_HOOK_SCRIPT = `#!/bin/sh
|
|
80
|
+
${POST_REWRITE_VERSION}
|
|
81
|
+
# Appends exact old->new commit pairs (amend/rebase, incl. squashes) to the
|
|
82
|
+
# OuterLayer spool so rewritten history stays creditable to the recorded
|
|
83
|
+
# sessions that produced it. Always exits 0: journaling must never break a
|
|
84
|
+
# rebase.
|
|
85
|
+
# On a machine without OuterLayer (this hook can be committed into a repo
|
|
86
|
+
# and run for every contributor) there is nothing to journal and no reason
|
|
87
|
+
# to create ~/.outerlayer: bow out before touching the filesystem.
|
|
88
|
+
[ -n "$OUTERLAYER_SPOOL_DIR" ] || [ -d "$HOME/.outerlayer" ] || exit 0
|
|
89
|
+
SPOOL_DIR="\${OUTERLAYER_SPOOL_DIR:-$HOME/.outerlayer/spool}"
|
|
90
|
+
mkdir -p "$SPOOL_DIR" 2>/dev/null || exit 0
|
|
91
|
+
FILE="$SPOOL_DIR/rewrites.jsonl"
|
|
92
|
+
if [ -f "$FILE" ] && [ "$(wc -c < "$FILE" 2>/dev/null || echo 0)" -gt 5242880 ]; then
|
|
93
|
+
mv -f "$FILE" "$FILE.1" 2>/dev/null || true
|
|
94
|
+
fi
|
|
95
|
+
REPO="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
|
|
96
|
+
if [ -z "$REPO" ]; then
|
|
97
|
+
REPO="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
|
|
98
|
+
case "$REPO" in /*) ;; *) REPO="$PWD/$REPO" ;; esac
|
|
99
|
+
fi
|
|
100
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
101
|
+
awk -v repo="$REPO" -v ts="$TS" -v src="$1" '
|
|
102
|
+
NF >= 2 && $1 ~ /^[0-9a-f]+$/ && $2 ~ /^[0-9a-f]+$/ {
|
|
103
|
+
pairs = pairs (n ? "," : "") "[\\"" $1 "\\",\\"" $2 "\\"]"; n++
|
|
104
|
+
}
|
|
105
|
+
END {
|
|
106
|
+
if (n) {
|
|
107
|
+
gsub(/\\\\/, "\\\\\\\\", repo); gsub(/"/, "\\\\\\"", repo)
|
|
108
|
+
printf "{\\"t\\":\\"%s\\",\\"repo\\":\\"%s\\",\\"source\\":\\"%s\\",\\"pairs\\":[%s]}\\n", ts, repo, src, pairs
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
' >> "$FILE" 2>/dev/null || true
|
|
112
|
+
exit 0
|
|
113
|
+
`;
|
|
114
|
+
var POST_COMMIT_HOOK_SCRIPT = `#!/bin/sh
|
|
115
|
+
${POST_REWRITE_VERSION}
|
|
116
|
+
# Appends every commit's SHA to the OuterLayer spool the moment it exists,
|
|
117
|
+
# so a session whose transcript lost git's porcelain line (piped or muted
|
|
118
|
+
# output) can still be credited at sync time. Always exits 0.
|
|
119
|
+
# On a machine without OuterLayer (this hook can be committed into a repo
|
|
120
|
+
# and run for every contributor) there is nothing to journal and no reason
|
|
121
|
+
# to create ~/.outerlayer: bow out before touching the filesystem.
|
|
122
|
+
[ -n "$OUTERLAYER_SPOOL_DIR" ] || [ -d "$HOME/.outerlayer" ] || exit 0
|
|
123
|
+
SPOOL_DIR="\${OUTERLAYER_SPOOL_DIR:-$HOME/.outerlayer/spool}"
|
|
124
|
+
mkdir -p "$SPOOL_DIR" 2>/dev/null || exit 0
|
|
125
|
+
FILE="$SPOOL_DIR/commits.jsonl"
|
|
126
|
+
if [ -f "$FILE" ] && [ "$(wc -c < "$FILE" 2>/dev/null || echo 0)" -gt 5242880 ]; then
|
|
127
|
+
mv -f "$FILE" "$FILE.1" 2>/dev/null || true
|
|
128
|
+
fi
|
|
129
|
+
REPO="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
|
|
130
|
+
if [ -z "$REPO" ]; then
|
|
131
|
+
REPO="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
|
|
132
|
+
case "$REPO" in /*) ;; *) REPO="$PWD/$REPO" ;; esac
|
|
133
|
+
fi
|
|
134
|
+
SHA="$(git rev-parse HEAD 2>/dev/null)" || exit 0
|
|
135
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
136
|
+
case "$SHA" in
|
|
137
|
+
*[!0-9a-f]*) exit 0 ;;
|
|
138
|
+
esac
|
|
139
|
+
REPO_ESCAPED=$(printf '%s' "$REPO" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g')
|
|
140
|
+
printf '{"t":"%s","repo":"%s","sha":"%s"}
|
|
141
|
+
' "$TS" "$REPO_ESCAPED" "$SHA" >> "$FILE" 2>/dev/null || true
|
|
142
|
+
exit 0
|
|
143
|
+
`;
|
|
144
|
+
var PREPARE_COMMIT_MSG_HOOK_SCRIPT = `#!/bin/sh
|
|
145
|
+
${POST_REWRITE_VERSION}
|
|
146
|
+
# Appends "OuterLayer-Session: <id>" to commits made during a recorded agent
|
|
147
|
+
# session, so the commit itself carries the session identity through squash
|
|
148
|
+
# and cross-machine rebases (the server-side trailer tier parses it). Stamps
|
|
149
|
+
# NOTHING outside a session: the agent env marker is required, and the id
|
|
150
|
+
# comes from the shell's own session variable \u2014 exact, because the commit
|
|
151
|
+
# process is a child of the session that issued it \u2014 with the spool journal
|
|
152
|
+
# as the fallback for harnesses that don't export one. Always exits 0.
|
|
153
|
+
# The journal-fallback selection rules mirror detectActiveSession in
|
|
154
|
+
# packages/cli/src/session-detect.ts \u2014 keep them in step.
|
|
155
|
+
[ -n "$OUTERLAYER_NO_TRAILER" ] && exit 0
|
|
156
|
+
[ -n "$CLAUDECODE" ] || exit 0
|
|
157
|
+
MSG_FILE="$1"
|
|
158
|
+
[ -f "$MSG_FILE" ] || exit 0
|
|
159
|
+
grep -qi '^outerlayer-session:' "$MSG_FILE" 2>/dev/null && exit 0
|
|
160
|
+
SID="$CLAUDE_CODE_SESSION_ID"
|
|
161
|
+
case "$SID" in
|
|
162
|
+
*[!0-9a-fA-F-]*) SID="" ;;
|
|
163
|
+
esac
|
|
164
|
+
if [ -n "$SID" ]; then
|
|
165
|
+
printf '\\nOuterLayer-Session: %s\\n' "$SID" >> "$MSG_FILE" 2>/dev/null || true
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
SPOOL_DIR="\${OUTERLAYER_SPOOL_DIR:-$HOME/.outerlayer/spool}"
|
|
169
|
+
EVENTS="$SPOOL_DIR/events.jsonl"
|
|
170
|
+
[ -f "$EVENTS" ] || exit 0
|
|
171
|
+
TOP="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
|
|
172
|
+
[ -n "$TOP" ] || exit 0
|
|
173
|
+
TOP_REAL="$(cd "$TOP" 2>/dev/null && pwd -P)" || TOP_REAL="$TOP"
|
|
174
|
+
CUTOFF="$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo 1970-01-01T00:00:00Z)"
|
|
175
|
+
SID="$(tail -c 65536 "$EVENTS" 2>/dev/null | awk -v top="$TOP" -v topr="$TOP_REAL" -v cutoff="$CUTOFF" '
|
|
176
|
+
function field(line, key, re, v) {
|
|
177
|
+
re = "\\"" key "\\":\\"[^\\"]*\\""
|
|
178
|
+
if (match(line, re) == 0) return ""
|
|
179
|
+
v = substr(line, RSTART, RLENGTH)
|
|
180
|
+
sub("^\\"" key "\\":\\"", "", v); sub("\\"$", "", v)
|
|
181
|
+
return v
|
|
182
|
+
}
|
|
183
|
+
# macOS spells one directory two ways (/var is a symlink to /private/var);
|
|
184
|
+
# git reports the resolved form while the session recorded the launch
|
|
185
|
+
# spelling, so both sides normalize before comparing.
|
|
186
|
+
function norm(p) { sub(/^\\/private\\//, "/", p); return p }
|
|
187
|
+
function cwdMatches(c, cn, tn, rn) {
|
|
188
|
+
cn = norm(c); tn = norm(top); rn = norm(topr)
|
|
189
|
+
return cn == tn || cn == rn || index(cn, tn "/") == 1 || index(cn, rn "/") == 1
|
|
190
|
+
}
|
|
191
|
+
/^\\{/ {
|
|
192
|
+
sid = field($0, "sessionId"); if (sid == "") next
|
|
193
|
+
ev = field($0, "event")
|
|
194
|
+
if (ev == "SessionEnd") { ended[sid] = 1; next }
|
|
195
|
+
t = field($0, "t"); c = field($0, "cwd")
|
|
196
|
+
if (t >= cutoff && cwdMatches(c)) { lastAt[sid] = NR }
|
|
197
|
+
}
|
|
198
|
+
END {
|
|
199
|
+
best = ""; bestNr = 0
|
|
200
|
+
for (sid in lastAt) if (!ended[sid] && lastAt[sid] > bestNr) { best = sid; bestNr = lastAt[sid] }
|
|
201
|
+
if (best != "" && best ~ /^[0-9a-fA-F-]+$/) print best
|
|
202
|
+
}
|
|
203
|
+
')"
|
|
204
|
+
[ -n "$SID" ] || exit 0
|
|
205
|
+
printf '\\nOuterLayer-Session: %s\\n' "$SID" >> "$MSG_FILE" 2>/dev/null || true
|
|
206
|
+
exit 0
|
|
207
|
+
`;
|
|
208
|
+
var PRE_COMMIT_GUARD_MARKER = "# outerlayer materialized-context commit guard";
|
|
209
|
+
var PRE_COMMIT_GUARD_VERSION = `${PRE_COMMIT_GUARD_MARKER} v1`;
|
|
210
|
+
var PRE_COMMIT_GUARD_BODY_SIGNATURE = "outerlayer: refusing to commit materialized control-plane context";
|
|
211
|
+
var PRE_COMMIT_GUARD_SCRIPT = `#!/bin/sh
|
|
212
|
+
${PRE_COMMIT_GUARD_VERSION}
|
|
213
|
+
unset GIT_LITERAL_PATHSPECS GIT_GLOB_PATHSPECS GIT_NOGLOB_PATHSPECS GIT_ICASE_PATHSPECS
|
|
214
|
+
PATH="$PATH:/usr/bin:/bin"
|
|
215
|
+
export PATH
|
|
216
|
+
COMMON_DIR="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
|
|
217
|
+
if [ -z "$COMMON_DIR" ]; then
|
|
218
|
+
COMMON_DIR="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
|
|
219
|
+
case "$COMMON_DIR" in /*) ;; *) COMMON_DIR="$PWD/$COMMON_DIR" ;; esac
|
|
220
|
+
fi
|
|
221
|
+
grep -qF "${CONTEXT_EXCLUDE_BEGIN_PREFIX}" "$COMMON_DIR/info/exclude" 2>/dev/null || exit 0
|
|
222
|
+
|
|
223
|
+
if git rev-parse --verify -q HEAD >/dev/null 2>&1; then
|
|
224
|
+
BASE=HEAD
|
|
225
|
+
REMEDY="git restore --staged <path>"
|
|
226
|
+
else
|
|
227
|
+
BASE="$(git hash-object -t tree --stdin < /dev/null)"
|
|
228
|
+
REMEDY="git rm --cached <path>"
|
|
229
|
+
fi
|
|
230
|
+
[ -n "$BASE" ] || exit 0
|
|
231
|
+
|
|
232
|
+
BLOCKED="$(git diff --cached --name-only --diff-filter=A --no-renames --ignore-submodules=none "$BASE" -- ':(icase)AGENTS.md' ':(icase).claude' ':(icase).outerlayer' 2>/dev/null)"
|
|
233
|
+
[ -n "$BLOCKED" ] || exit 0
|
|
234
|
+
|
|
235
|
+
echo "${PRE_COMMIT_GUARD_BODY_SIGNATURE}:" >&2
|
|
236
|
+
printf '%s\\n' "$BLOCKED" | while IFS= read -r path; do
|
|
237
|
+
[ -n "$path" ] && echo " $path" >&2
|
|
238
|
+
done
|
|
239
|
+
echo "These paths come from a private control plane and must never enter this" >&2
|
|
240
|
+
echo "repository's history. Unstage them to continue:" >&2
|
|
241
|
+
echo " $REMEDY" >&2
|
|
242
|
+
exit 1
|
|
243
|
+
`;
|
|
244
|
+
function managedHookScripts() {
|
|
245
|
+
return [
|
|
246
|
+
["post-rewrite", printableHookScript(POST_REWRITE_HOOK_SCRIPT)],
|
|
247
|
+
["post-commit", printableHookScript(POST_COMMIT_HOOK_SCRIPT)],
|
|
248
|
+
["prepare-commit-msg", printableHookScript(PREPARE_COMMIT_MSG_HOOK_SCRIPT)],
|
|
249
|
+
["pre-commit", printableHookScript(PRE_COMMIT_GUARD_SCRIPT)]
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
function printableHookScript(script) {
|
|
253
|
+
const lines = script.split("\n");
|
|
254
|
+
return [lines[0], ...lines.slice(2)].join("\n");
|
|
255
|
+
}
|
|
256
|
+
function git(cwd, args) {
|
|
257
|
+
try {
|
|
258
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
259
|
+
encoding: "utf8",
|
|
260
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
261
|
+
timeout: 2e3
|
|
262
|
+
}).trim();
|
|
263
|
+
} catch {
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function hooksDir(cwd) {
|
|
268
|
+
const dir = git(cwd, ["rev-parse", "--git-path", "hooks"]);
|
|
269
|
+
if (dir === void 0) return void 0;
|
|
270
|
+
return isAbsolute(dir) ? dir : resolve(cwd, dir);
|
|
271
|
+
}
|
|
272
|
+
function commonHooksDir(cwd) {
|
|
273
|
+
const abs = git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
274
|
+
if (abs !== void 0) return join(abs, "hooks");
|
|
275
|
+
const rel = git(cwd, ["rev-parse", "--git-common-dir"]);
|
|
276
|
+
if (rel === void 0) return void 0;
|
|
277
|
+
return join(isAbsolute(rel) ? rel : join(cwd, rel), "hooks");
|
|
278
|
+
}
|
|
279
|
+
function installHookFile(dir, name, script, marker) {
|
|
280
|
+
const path = join(dir, name);
|
|
281
|
+
let existing;
|
|
282
|
+
try {
|
|
283
|
+
existing = readFileSync(path, "utf8");
|
|
284
|
+
} catch {
|
|
285
|
+
existing = void 0;
|
|
286
|
+
}
|
|
287
|
+
if (existing !== void 0) {
|
|
288
|
+
if (!isOwnRecognitionLine(existing, marker)) return "skipped-foreign";
|
|
289
|
+
if (existing === script) {
|
|
290
|
+
try {
|
|
291
|
+
chmodSync(path, 493);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
return "unchanged";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
mkdirSync(dir, { recursive: true });
|
|
298
|
+
writeFileSync(path, script);
|
|
299
|
+
chmodSync(path, 493);
|
|
300
|
+
return existing === void 0 ? "installed" : "updated";
|
|
301
|
+
}
|
|
302
|
+
function installPostRewriteHook(cwd, opts) {
|
|
303
|
+
const stampTrailers = opts?.stampTrailers ?? true;
|
|
304
|
+
const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
305
|
+
if (inRepo !== "true") return { status: "not-a-repo" };
|
|
306
|
+
if ((git(cwd, ["config", "core.hooksPath"]) ?? "") !== "") {
|
|
307
|
+
return { status: "skipped-custom-hooks-path" };
|
|
308
|
+
}
|
|
309
|
+
const dir = hooksDir(cwd);
|
|
310
|
+
if (dir === void 0) return { status: "not-a-repo" };
|
|
311
|
+
const pairOutcomes = [
|
|
312
|
+
installHookFile(dir, "post-rewrite", POST_REWRITE_HOOK_SCRIPT, POST_REWRITE_MARKER),
|
|
313
|
+
installHookFile(dir, "post-commit", POST_COMMIT_HOOK_SCRIPT, POST_REWRITE_MARKER)
|
|
314
|
+
];
|
|
315
|
+
const outcomes = [...pairOutcomes];
|
|
316
|
+
if (stampTrailers) {
|
|
317
|
+
outcomes.push(installHookFile(dir, "prepare-commit-msg", PREPARE_COMMIT_MSG_HOOK_SCRIPT, POST_REWRITE_MARKER));
|
|
318
|
+
} else {
|
|
319
|
+
removeOurHookFile(join(dir, "prepare-commit-msg"), POST_REWRITE_MARKER);
|
|
320
|
+
}
|
|
321
|
+
const path = join(dir, "post-rewrite");
|
|
322
|
+
if (pairOutcomes.every((o) => o === "skipped-foreign")) return { status: "skipped-foreign", path };
|
|
323
|
+
if (outcomes.every((o) => o === "unchanged")) return { status: "unchanged", path };
|
|
324
|
+
if (outcomes.includes("updated")) return { status: "updated", path };
|
|
325
|
+
if (outcomes.includes("installed")) return { status: "installed", path };
|
|
326
|
+
return { status: "skipped-foreign", path };
|
|
327
|
+
}
|
|
328
|
+
function removeOurHookFile(path, marker) {
|
|
329
|
+
if (!existsSync(path)) return false;
|
|
330
|
+
let content;
|
|
331
|
+
try {
|
|
332
|
+
content = readFileSync(path, "utf8");
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
if (!isOwnRecognitionLine(content, marker)) return false;
|
|
337
|
+
unlinkSync(path);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
function isOwnRecognitionLine(content, marker) {
|
|
341
|
+
const secondLine = content.split("\n", 2)[1];
|
|
342
|
+
return secondLine !== void 0 && secondLine.startsWith(marker);
|
|
343
|
+
}
|
|
344
|
+
function removePostRewriteHook(cwd) {
|
|
345
|
+
const dir = hooksDir(cwd);
|
|
346
|
+
if (dir === void 0) return { removed: false };
|
|
347
|
+
let removed = false;
|
|
348
|
+
for (const name of ["post-rewrite", "post-commit", "prepare-commit-msg"]) {
|
|
349
|
+
if (removeOurHookFile(join(dir, name), POST_REWRITE_MARKER)) removed = true;
|
|
350
|
+
}
|
|
351
|
+
return { removed, path: join(dir, "post-rewrite") };
|
|
352
|
+
}
|
|
353
|
+
function postRewriteHookPresence(cwd) {
|
|
354
|
+
const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
355
|
+
if (inRepo !== "true") return "not-a-repo";
|
|
356
|
+
if ((git(cwd, ["config", "core.hooksPath"]) ?? "") !== "") return "custom-hooks-path";
|
|
357
|
+
const dir = hooksDir(cwd);
|
|
358
|
+
if (dir === void 0) return "not-a-repo";
|
|
359
|
+
const states = ["post-rewrite", "post-commit"].map((name) => {
|
|
360
|
+
let content;
|
|
361
|
+
try {
|
|
362
|
+
content = readFileSync(join(dir, name), "utf8");
|
|
363
|
+
} catch {
|
|
364
|
+
return "absent";
|
|
365
|
+
}
|
|
366
|
+
return content.includes(POST_REWRITE_MARKER) ? "ours" : "foreign";
|
|
367
|
+
});
|
|
368
|
+
if (states.every((st) => st === "ours")) return "ours";
|
|
369
|
+
if (states.includes("foreign")) return "foreign";
|
|
370
|
+
return "absent";
|
|
371
|
+
}
|
|
372
|
+
function installPreCommitGuardHook(cwd) {
|
|
373
|
+
const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
374
|
+
if (inRepo !== "true") return { status: "not-a-repo", adopted: false };
|
|
375
|
+
const hooksPath = git(cwd, ["config", "core.hooksPath"]) ?? "";
|
|
376
|
+
if (hooksPath !== "") {
|
|
377
|
+
const expanded = expandHooksPathTilde(hooksPath);
|
|
378
|
+
const effectiveDir = isAbsolute(expanded) ? expanded : join(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd, expanded);
|
|
379
|
+
const path2 = join(effectiveDir, "pre-commit");
|
|
380
|
+
return { status: "skipped-custom-hooks-path", path: path2, adopted: guardAdoptedAtEffectiveHook(path2) };
|
|
381
|
+
}
|
|
382
|
+
const dir = hooksDir(cwd);
|
|
383
|
+
if (dir === void 0) return { status: "not-a-repo", adopted: false };
|
|
384
|
+
const path = join(dir, "pre-commit");
|
|
385
|
+
const status = installHookFile(dir, "pre-commit", PRE_COMMIT_GUARD_SCRIPT, PRE_COMMIT_GUARD_MARKER);
|
|
386
|
+
return { status, path, adopted: status === "skipped-foreign" ? guardBodyPresent(path) : true };
|
|
387
|
+
}
|
|
388
|
+
function removePreCommitGuardHook(cwd) {
|
|
389
|
+
const dir = commonHooksDir(cwd);
|
|
390
|
+
if (dir === void 0) return { removed: false };
|
|
391
|
+
const path = join(dir, "pre-commit");
|
|
392
|
+
return { removed: removeOurHookFile(path, PRE_COMMIT_GUARD_MARKER), path };
|
|
393
|
+
}
|
|
394
|
+
var GUARD_BODY_LINE_PREFIXES = [`echo "${PRE_COMMIT_GUARD_BODY_SIGNATURE}`, `echo '${PRE_COMMIT_GUARD_BODY_SIGNATURE}`];
|
|
395
|
+
function guardBodyPresent(path) {
|
|
396
|
+
let content;
|
|
397
|
+
try {
|
|
398
|
+
content = readFileSync(path, "utf8");
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return content.split("\n").some((line) => {
|
|
403
|
+
const trimmed = line.trimStart();
|
|
404
|
+
return GUARD_BODY_LINE_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function guardAdoptedAtEffectiveHook(effectiveHookPath) {
|
|
408
|
+
if (guardBodyPresent(effectiveHookPath)) return true;
|
|
409
|
+
const shimDir = dirname(effectiveHookPath);
|
|
410
|
+
if (basename(shimDir) !== "_") return false;
|
|
411
|
+
return guardBodyPresent(join(dirname(shimDir), basename(effectiveHookPath)));
|
|
412
|
+
}
|
|
413
|
+
function expandHooksPathTilde(hooksPath) {
|
|
414
|
+
if (hooksPath === "~") return homeFromEnv();
|
|
415
|
+
if (hooksPath.startsWith("~/")) return join(homeFromEnv(), hooksPath.slice(2));
|
|
416
|
+
return hooksPath;
|
|
417
|
+
}
|
|
418
|
+
function ensurePreCommitGuardHook(repoRoot, gitCommonDir) {
|
|
419
|
+
const hooksPath = git(repoRoot, ["config", "core.hooksPath"]) ?? "";
|
|
420
|
+
if (hooksPath !== "") {
|
|
421
|
+
const expanded = expandHooksPathTilde(hooksPath);
|
|
422
|
+
const effectivePath = isAbsolute(expanded) ? join(expanded, "pre-commit") : join(repoRoot, expanded, "pre-commit");
|
|
423
|
+
return { status: "skipped-custom-hooks-path", adopted: guardAdoptedAtEffectiveHook(effectivePath) };
|
|
424
|
+
}
|
|
425
|
+
const path = join(gitCommonDir, "hooks", "pre-commit");
|
|
426
|
+
const status = installHookFile(join(gitCommonDir, "hooks"), "pre-commit", PRE_COMMIT_GUARD_SCRIPT, PRE_COMMIT_GUARD_MARKER);
|
|
427
|
+
return { status, adopted: status === "skipped-foreign" ? guardBodyPresent(path) : true };
|
|
428
|
+
}
|
|
429
|
+
function deadHooksState(cwd) {
|
|
430
|
+
const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
431
|
+
if (inRepo !== "true") return { dead: false };
|
|
432
|
+
const hooksPath = git(cwd, ["config", "core.hooksPath"]) ?? "";
|
|
433
|
+
if (hooksPath === "") return { dead: false };
|
|
434
|
+
const top = git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
435
|
+
if (top === void 0) return { dead: false };
|
|
436
|
+
const expanded = expandHooksPathTilde(hooksPath);
|
|
437
|
+
const hooksDir2 = isAbsolute(expanded) ? expanded : join(top, expanded);
|
|
438
|
+
let entries;
|
|
439
|
+
try {
|
|
440
|
+
entries = readdirSync(hooksDir2);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
const code = err.code;
|
|
443
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { dead: true, hooksDir: hooksDir2, repoRoot: top };
|
|
444
|
+
return { dead: false, hooksDir: hooksDir2, repoRoot: top, unreadableError: err };
|
|
445
|
+
}
|
|
446
|
+
const dead = entries.filter((e) => e !== ".gitignore").length === 0;
|
|
447
|
+
return { dead, hooksDir: hooksDir2, repoRoot: top };
|
|
448
|
+
}
|
|
449
|
+
var ALLOWED_PACKAGE_MANAGERS = ["yarn", "npm", "pnpm", "bun"];
|
|
450
|
+
function preparePackageManagerCommand(repoRoot) {
|
|
451
|
+
let declared = "";
|
|
452
|
+
try {
|
|
453
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
454
|
+
if (typeof pkg.packageManager === "string") declared = pkg.packageManager.split("@")[0] ?? "";
|
|
455
|
+
} catch {
|
|
456
|
+
declared = "";
|
|
457
|
+
}
|
|
458
|
+
if (!ALLOWED_PACKAGE_MANAGERS.includes(declared)) declared = "";
|
|
459
|
+
if (declared === "yarn") return "yarn prepare";
|
|
460
|
+
return `${declared || "npm"} run prepare`;
|
|
461
|
+
}
|
|
462
|
+
function huskyPrepareScript(repoRoot) {
|
|
463
|
+
try {
|
|
464
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
465
|
+
const script = pkg.scripts?.prepare;
|
|
466
|
+
if (typeof script === "string" && /husky/i.test(script)) return script;
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export { contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPostRewriteHook, installPreCommitGuardHook, managedHookScripts, postRewriteHookPresence, preparePackageManagerCommand, removePostRewriteHook, removePreCommitGuardHook };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
createRequire(import.meta.url);
|
|
6
|
+
var RUNNER_DIR_MODE = 448;
|
|
7
|
+
var RUNNER_FILE_MODE = 384;
|
|
8
|
+
function runnerDir(configPath) {
|
|
9
|
+
return join(dirname(configPath), "runner");
|
|
10
|
+
}
|
|
11
|
+
function pidFilePath(configPath) {
|
|
12
|
+
return join(runnerDir(configPath), "runner.pid");
|
|
13
|
+
}
|
|
14
|
+
function jobsDir(configPath) {
|
|
15
|
+
return join(runnerDir(configPath), "jobs");
|
|
16
|
+
}
|
|
17
|
+
function jobDir(configPath, queue, item, claimId, attempt) {
|
|
18
|
+
const suffix = attempt === void 0 ? "" : `-${attempt}`;
|
|
19
|
+
return join(jobsDir(configPath), `${queue}-${item}-${claimId}${suffix}`);
|
|
20
|
+
}
|
|
21
|
+
function attemptStamp(at) {
|
|
22
|
+
return at.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { RUNNER_DIR_MODE, RUNNER_FILE_MODE, attemptStamp, jobDir, jobsDir, pidFilePath, runnerDir };
|