@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +202 -0
  3. package/README.md +320 -0
  4. package/dist/build-info.json +1 -0
  5. package/dist/chunk-4H56Y7O6.js +589 -0
  6. package/dist/chunk-A3WLZX2F.js +120 -0
  7. package/dist/chunk-A6NNKRJU.js +650 -0
  8. package/dist/chunk-D77LS3UI.js +42 -0
  9. package/dist/chunk-DEDV5V4U.js +87324 -0
  10. package/dist/chunk-ECSYRPCC.js +334 -0
  11. package/dist/chunk-IWIUYLDR.js +472 -0
  12. package/dist/chunk-JJP7YLMN.js +25 -0
  13. package/dist/chunk-KFYJV2ZG.js +7978 -0
  14. package/dist/chunk-OZ7C3XUE.js +34 -0
  15. package/dist/chunk-RCQXYLMO.js +96 -0
  16. package/dist/chunk-TFUIDMOB.js +34 -0
  17. package/dist/chunk-VNVZDWO3.js +10 -0
  18. package/dist/chunk-VU34RWDU.js +339 -0
  19. package/dist/chunk-WQ6VGRGZ.js +150 -0
  20. package/dist/chunk-XN2XZFTK.js +165 -0
  21. package/dist/chunk-YMTXR7PJ.js +200 -0
  22. package/dist/chunk-YRYMNBGQ.js +54 -0
  23. package/dist/chunk-Z5GEFMRW.js +45 -0
  24. package/dist/chunk-ZJSNLVCE.js +58 -0
  25. package/dist/cli-3JYGL2OB.js +5311 -0
  26. package/dist/config-POF7DEQW.js +7 -0
  27. package/dist/context-materialize-J6CRQ7O7.js +589 -0
  28. package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
  29. package/dist/emit-cmd-TWSYYEZI.js +208 -0
  30. package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
  31. package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
  32. package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
  33. package/dist/hook-fast-CBW4ZETX.js +8 -0
  34. package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
  35. package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
  36. package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +25 -0
  39. package/dist/init-PTBITAUO.js +103 -0
  40. package/dist/login-cmd-IRX6LZT7.js +62 -0
  41. package/dist/logs-TRNPQM42.js +63 -0
  42. package/dist/loop-NFSOBUUJ.js +646 -0
  43. package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
  44. package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
  45. package/dist/paths-D2VGWWFI.js +6 -0
  46. package/dist/pidfile-PTW76F56.js +8 -0
  47. package/dist/status-WGOJTXZF.js +94 -0
  48. package/dist/statusline-fast-Z5U5CEC6.js +8 -0
  49. package/dist/sync-cmd-C5UGI4SF.js +15 -0
  50. package/dist/watch-V3K4PESQ.js +70 -0
  51. package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
  52. package/dist/work-cmd-CW26EZZO.js +16 -0
  53. package/dist/work-launch-YNNCKIH3.js +6 -0
  54. package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
  55. 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 };