-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathupdate.sh
More file actions
executable file
·513 lines (473 loc) · 26.6 KB
/
Copy pathupdate.sh
File metadata and controls
executable file
·513 lines (473 loc) · 26.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
#!/bin/bash
# set -x
set -e
source "$HOME/.claude/lib/lint.sh"
source "$HOME/.claude/lib/pnpm.sh"
# OS detection drives the package-manager branches below. Git Bash / MSYS2 on Windows reports
# OSTYPE=msys (uname → MINGW64_NT / MSYS_NT); macOS is darwin*. macOS installs system tools via
# Homebrew; Windows has no brew, so there we install what the cross-platform managers (cargo, pnpm,
# npm) can and verify-and-instruct (winget) for the rest. Anything else (Linux) degrades to
# verify-and-warn, matching the pre-existing non-macOS fallbacks.
case "$OSTYPE" in
darwin*) CLAUDE_OS=macos ;;
msys*|cygwin*|win*) CLAUDE_OS=windows ;;
*) CLAUDE_OS=other ;;
esac
# Detect the OpenAI Codex orphan-history clobber (the curated-sync disable further down is the cure).
# If this repo's HEAD shares NO commit ancestry with origin/main, main has been reset onto an unrelated
# snapshot — Codex's refs/codex/curated-sync — and is silently detached: a `git pull` here would try to
# reconcile unrelated histories and a push could clobber origin. Warn loudly with the recovery command
# but DON'T auto-reset (that would nuke any legitimate local commits). merge-base against the existing
# origin/main ref needs no fetch — an orphan has no common ancestor even with a stale origin/main.
claude_repo="$HOME/.claude"
if git -C "$claude_repo" rev-parse --git-dir >/dev/null 2>&1 \
&& git -C "$claude_repo" rev-parse --verify -q origin/main >/dev/null \
&& ! git -C "$claude_repo" merge-base HEAD origin/main >/dev/null 2>&1; then
echo ""
echo " ⚠️ ~/.claude HEAD shares no history with origin/main — it looks reset onto an unrelated"
echo " snapshot (likely OpenAI Codex's refs/codex/curated-sync). Do NOT git pull/push here."
echo " Recover, then re-run this script:"
echo " git -C ~/.claude fetch origin"
echo " git -C ~/.claude reset --hard origin/main"
echo ""
fi
# Non-keeper machines must stay fast-forwardable clones of origin/main: local commits there can
# never be pushed (main is branch-protected; contributions go through /keeper's proposal-PR flow),
# so they only accumulate conflicts against future pulls. Warn-only, like the Codex check above —
# /keeper contributor mode is the recovery surface that carries the commits onto a proposal branch
# and resets main back to a pure clone with the user's consent.
if git -C "$claude_repo" rev-parse --git-dir >/dev/null 2>&1 \
&& git -C "$claude_repo" rev-parse --verify -q origin/main >/dev/null \
&& [ "$(git -C "$claude_repo" config --get reflect.keeper 2>/dev/null)" != "true" ] \
&& git -C "$claude_repo" merge-base HEAD origin/main >/dev/null 2>&1 \
&& [ -n "$(git -C "$claude_repo" rev-list origin/main..HEAD 2>/dev/null | head -1)" ]; then
echo ""
echo " ⚠️ ~/.claude main has local commits that cannot be pushed from this machine (it is not"
echo " the keeper's). Run /keeper — it proposes what has global value as a PR and restores"
echo " this clone to a clean copy of origin/main."
echo ""
fi
echo "Updating Claude Code..."
# Non-fatal: a failed self-update (e.g. an npm-managed install on Windows where `claude update` is a
# no-op or errors) must not abort the whole bootstrap.
claude update || echo " ⚠️ 'claude update' failed or is unsupported here; continuing."
echo "Updating plugin marketplaces..."
claude plugin marketplace update
claude plugin marketplace update claude-plugins-official
echo "Installing lsp servers..."
claude plugin install typescript-lsp
# Ensure pnpm's global bin directory is on PATH — `pnpm add -g` refuses to run when it isn't. pnpm 11 keeps it at
# $PNPM_HOME/bin, pnpm 10 at $PNPM_HOME itself (measured), and the standalone installer's rc snippet adds only the
# latter, so add both. Augment PATH unconditionally (the parent shell always exports PNPM_HOME, so a `-z "$PNPM_HOME"`
# guard would skip this every time).
if [ -z "$PNPM_HOME" ]; then
case "$CLAUDE_OS" in
macos) export PNPM_HOME="$HOME/Library/pnpm" ;;
# pnpm on Windows keeps globals under %LOCALAPPDATA%\pnpm; convert to a POSIX path for Git Bash.
# Require BOTH a non-empty LOCALAPPDATA and cygpath: `cygpath -u ""` exits 0 with empty output, and
# a failed `$(cygpath …)` substitution does not trip set -e — either would yield a bogus "/pnpm".
windows)
if [ -n "$LOCALAPPDATA" ] && command -v cygpath >/dev/null 2>&1; then
export PNPM_HOME="$(cygpath -u "$LOCALAPPDATA")/pnpm"
else
export PNPM_HOME="$HOME/.local/share/pnpm"
fi ;;
*) export PNPM_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/pnpm" ;;
esac
fi
for pnpm_bin_dir in "$PNPM_HOME/bin" "$PNPM_HOME"; do
case ":$PATH:" in
*":$pnpm_bin_dir:"*) ;;
*) export PATH="$pnpm_bin_dir:$PATH" ;;
esac
done
# A Corepack shim asks before downloading a version it has not cached; nothing is at the keyboard in a /update run.
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# Install a Homebrew formula if missing, upgrade it if present. Idempotent: present-and-current is a no-op.
# Branch on `brew list` because `brew install` never upgrades and `brew upgrade` errors on a not-installed formula.
# The asymmetry under `set -e` is deliberate: an upgrade failure is swallowed (`|| true` — the working older version
# stays, so keep going), but an install failure aborts the bootstrap — a core tool that is entirely absent is not
# something to continue past.
ensure_brew() {
local formula
for formula in "$@"; do
if brew list "$formula" >/dev/null 2>&1; then
brew upgrade "$formula" >/dev/null 2>&1 || true
else
brew install "$formula"
fi
done
}
# Core CLI tools the skills assume on PATH: gh (start/finish/pr-update/dependency-updater/reap-worktrees) and jq
# (preflight-gated with `exit 1` in several scripts). Prefer Homebrew (the linear-cli bootstrap below also needs it),
# and run this before the slow pnpm/cargo work so a missing Homebrew surfaces early, not 90 lines into the cargo build.
# macOS without Homebrew is a hard stop (brew is how the whole script installs system tools). Non-macOS degrades like
# the rest of the script: just verify gh/jq are present (they come from the distro package manager, not brew) and warn.
echo ""
echo "Ensuring core CLI tools (gh, jq)..."
if command -v brew >/dev/null 2>&1; then
ensure_brew gh jq
elif [ "$CLAUDE_OS" = macos ]; then
echo " ❌ Homebrew is required but not found. Install it from https://brew.sh and re-run." >&2
exit 1
elif [ "$CLAUDE_OS" = windows ]; then
# winget is a Store app-execution-alias that Git Bash usually can't invoke directly, so on Windows
# verify presence and print the exact PowerShell command to run rather than shelling out to winget.
for tool in gh jq; do
if ! command -v "$tool" >/dev/null 2>&1; then
case "$tool" in
gh) echo " ⚠️ gh not found — in PowerShell run: winget install -e --id GitHub.cli" ;;
jq) echo " ⚠️ jq not found — in PowerShell run: winget install -e --id jqlang.jq" ;;
esac
fi
done
else
for tool in gh jq; do
command -v "$tool" >/dev/null 2>&1 \
|| echo " ⚠️ $tool not found on PATH — install it via your package manager; skills that use it will fail until you do."
done
fi
# Windows: two interpreter shims in $HOME/bin, which Git for Windows' /etc/profile.d/env.sh puts first on PATH.
#
# jq — the winget build ends every output line with CRLF (measured on jq 1.8.2: `jq -r '.[]'` over `["TT-26"]` emits
# `TT-26\r\n`). Bash's `$(…)` strips the trailing CR along with the newline, which is why most scripts work, but `read`
# keeps it: a `while read id … done < <(jq -r …)` loop walks `TT-26\r`, so epic-graph.sh reported every child of an epic
# as not found and scratch-path-guard.sh let a multi-line `cp a /tmp/b` through. `jq -b` is jq's own switch for exactly
# this (the manual: Windows users under MSYS2 or Cygwin should pass it), and the shim adds it to every call so no script
# has to know which build it got.
#
# python3 — python.org's Windows installer ships python.exe and py.exe with no python3 alias. The four .py entry points'
# polyglot shebang already falls back to `python`, but the shell scripts that invoke python3 by name (finish-read-verdict.sh
# and auto-prep-pool.sh on production paths) do not, so resolve a real interpreter once and shim the name.
if [ "$CLAUDE_OS" = windows ]; then
mkdir -p "$HOME/bin"
write_shim() { # write_shim <path> <body> — rewrite only when the content changed, so re-runs stay quiet
if [ ! -f "$1" ] || [ "$(cat "$1")" != "$2" ]; then
printf '%s\n' "$2" > "$1" && chmod +x "$1" && echo " ✓ wrote $1"
fi
}
real_jq=$(which -a jq 2>/dev/null | grep -vx "$HOME/bin/jq" | head -1 || true)
if [ -n "$real_jq" ]; then
write_shim "$HOME/bin/jq" "$(printf '#!/bin/sh\nexec "%s" -b "$@"' "$real_jq")"
if printf '["x"]' | "$HOME/bin/jq" -r '.[]' | od -c | grep -q '\\r'; then
echo " ⚠️ $HOME/bin/jq still emits CRLF — scripts that read jq output line by line (epic-graph.sh, the hooks) will misparse it." >&2
fi
fi
if ! python3 -c 'import sys' >/dev/null 2>&1; then
real_py=$( { python -c 'import sys; print(sys.executable)' || py -3 -c 'import sys; print(sys.executable)'; } 2>/dev/null | tr -d '\r' || true)
if [ -n "$real_py" ]; then
write_shim "$HOME/bin/python3" "$(printf '#!/bin/sh\nexec "%s" "$@"' "$(cygpath -u "$real_py")")"
else
echo " ⚠️ no Python interpreter found (python3 / python / py) — in PowerShell run: winget install -e --id Python.Python.3.13" >&2
fi
fi
case ":$PATH:" in
*":$HOME/bin:"*) ;;
*) echo " ⚠️ $HOME/bin is not on PATH, so the shims above are inert — open a new Git Bash (its /etc/profile adds it) or add it yourself." >&2 ;;
esac
fi
# gh must be authenticated for the PR/start/finish flows (gh api user, gh pr create). Mirror the linear-cli auth step
# below: check status, and only launch the interactive `gh auth login` when there is a TTY — in a non-interactive run
# (CI, piped, ssh one-shot, a /full macro) it would hang on a prompt with no stdin, so warn and continue instead.
# Scope the status check to github.com: bare `gh auth status` exits non-zero if ANY configured host is logged out,
# which would force a needless re-login on machines that once added a now-expired enterprise host.
if gh auth status --hostname github.com >/dev/null 2>&1; then
echo " ✓ gh already authenticated"
elif [ -t 0 ] && [ -t 1 ]; then
echo ""
echo " gh is not authenticated — launching gh auth login..."
if ! gh auth login; then
echo ""
echo " ⚠️ gh authentication did not complete. Run it yourself before using PR/start/finish skills:"
echo " gh auth login"
echo " gh auth status # confirm"
fi
else
echo ""
echo " ⚠️ gh is not authenticated and no TTY is available for interactive login. Authenticate before using PR/start/finish skills:"
echo " gh auth login"
echo " gh auth status # confirm"
fi
# Both repos pin the same pnpm in package.json; lib/pnpm.sh explains why the machine's own copy still has to match it.
converge_pnpm "$claude_repo/package.json"
echo ""
echo "Installing skills helper..."
pnpm add -g skills
echo "Installing npm-check-updates (ncu) — required global for the dependency-updater skill..."
pnpm add -g npm-check-updates
AI_AGENT_LIST=(codex github-copilot claude-code)
AI_AGENTS=$(printf -- '-a %s ' "${AI_AGENT_LIST[@]}")
echo ""
echo "Installing skills for: ${AI_AGENT_LIST[*]}"
echo ""
echo "Updating vercel agent-browser..."
# pnpm 11 enables strictDepBuilds by default, so a global install of a package with a
# build script (agent-browser's postinstall fetches its native binary) prompts for
# approval in an interactive shell and hangs the script. --allow-build approves it
# non-interactively. It is per-invocation (not persisted to global config), so it must
# stay on this line.
pnpm add -g agent-browser --allow-build=agent-browser
# agent-browser ships prebuilt native binaries per platform but none for win32-arm64 (Windows-on-ARM
# dev VMs). Its postinstall still downloads the win32-x64 binary, and Windows-on-ARM runs x64 exes
# under built-in emulation — so alias x64 to the per-arch name the wrapper wants and retry. The
# wrapper prints the exact path it looked for ("Expected: …"), so parse that rather than guessing
# pnpm's store layout (pnpm root -g does not point at the real linked package dir). Never abort the
# bootstrap under set -e for this optional tool — everything after it (linear-cli included) matters more.
agent-browser install || {
ab_skip_msg=" ⚠️ agent-browser has no prebuilt binary for this platform — skipping; browser-automation skills won't work here."
ab_expected=$(agent-browser --version 2>&1 | sed -n 's/^Expected: //p' | tr -d '\r')
if [ "$CLAUDE_OS" = windows ] && [ -n "$ab_expected" ]; then
ab_expected=$(cygpath -u "$ab_expected")
ab_x64="$(dirname "$ab_expected")/agent-browser-win32-x64.exe"
if [ -f "$ab_x64" ]; then
echo " No upstream binary for this arch — aliasing win32-x64 to $(basename "$ab_expected") (runs under x64 emulation)..."
cp "$ab_x64" "$ab_expected"
agent-browser install \
|| echo " ⚠️ agent-browser still failing under x64 emulation — skipping; browser-automation skills won't work here."
else
echo "$ab_skip_msg"
fi
else
echo "$ab_skip_msg"
fi
}
pnpm dlx skills add vercel-labs/agent-browser \
-g \
--skill agent-browser \
--skill skill-creator \
$AI_AGENTS \
-y
echo "Updating vercel agent-skills..."
pnpm dlx skills add vercel-labs/agent-skills \
-g \
--skill vercel-composition-patterns \
--skill vercel-react-best-practices \
$AI_AGENTS \
-y
# OpenAI Codex's ChatGPT VS Code extension ("codex app-server") ships a remote plugin sync
# (~/.codex/.tmp/app-server-remote-plugin-sync-v1) that `git reset --hard`s THIS ~/.claude repo onto its
# own curated snapshot (refs/codex/curated-sync) whenever the app-server launches — silently detaching
# main from origin onto an orphan commit (the classic "N to pull, 1 to push" against unrelated history).
# We install codex skills into this shared dir just above, so every teammate running the extension is
# exposed. Turn the curated sync off in ~/.codex/config.toml so Codex stops managing this directory.
# A running app-server still holds the old config in memory, so the final "restart vscode" note is what
# makes it take effect. Idempotent: only touch an existing file, and only rewrite when the flag changes.
echo ""
echo "Disabling OpenAI Codex curated plugin sync (it hard-resets this repo)..."
codex_cfg="$HOME/.codex/config.toml"
if [ ! -f "$codex_cfg" ]; then
echo " no ~/.codex/config.toml — Codex not configured here; nothing to do."
else
codex_tmp=$(mktemp)
# Scope the flag flip to the [plugins."github@openai-curated"] table (sed can't do section-scoped
# edits); append the table if it's absent. Rerunning over the output is a fixed point (verified).
awk '
BEGIN { section=""; done=0; saw=0 }
/^[[:space:]]*\[/ {
if (section=="target" && !done) { print "enabled = false"; done=1 }
section = ($0 ~ /^[[:space:]]*\[plugins\."github@openai-curated"\][[:space:]]*$/) ? "target" : "other"
if (section=="target") saw=1
print; next
}
section=="target" && /^[[:space:]]*enabled[[:space:]]*=/ { if (!done) { print "enabled = false"; done=1 } next }
{ print }
END {
if (section=="target" && !done) print "enabled = false"
if (!saw) { print ""; print "[plugins.\"github@openai-curated\"]"; print "enabled = false" }
}
' "$codex_cfg" > "$codex_tmp"
if cmp -s "$codex_tmp" "$codex_cfg"; then
echo " ✓ already disabled."
else
cp -p "$codex_cfg" "$codex_cfg.bak"
cat "$codex_tmp" > "$codex_cfg" # truncate-in-place preserves the file's 0600 perms and inode
echo " ✓ disabled — backup at ~/.codex/config.toml.bak (restart vscode/Codex for it to take effect)."
fi
rm -f "$codex_tmp"
fi
# Remove the old joa23/Light Linear `linear-cli` Homebrew formula if present. It installs its own
# `linear-cli` binary that shadows the Finesssee cargo binary on PATH (whichever brew/cargo dir comes
# first wins), so a stale brew copy silently breaks the Linear skills. Idempotent: only act when the
# formula is actually installed / the tap actually present, so re-runs are no-ops.
# Homebrew-only cleanup — the shadowing formula only ever existed on macOS, so guard the whole block
# (a bare `brew` call on Windows would spew "command not found").
if [ "$CLAUDE_OS" = macos ] && command -v brew >/dev/null 2>&1; then
if brew list linear-cli >/dev/null 2>&1; then
echo "Removing old Homebrew linear-cli (joa23/Light Linear)..."
brew uninstall linear-cli
fi
if brew tap | grep -q '^joa23/linear-cli$'; then
echo "Untapping joa23/linear-cli..."
brew untap joa23/linear-cli
fi
fi
echo "Installing linear-cli (Finesssee — https://github.com/Finesssee/linear-cli)..."
# Rust CLI with a raw-GraphQL `api` escape hatch. We use it (not joa23/Light Linear)
# because Light Linear cannot read description-anchored comments or unassign issues,
# and has no API passthrough to work around either. This installer assumes macOS +
# Homebrew and bootstraps the whole chain (Rust → cargo-binstall → linear-cli) so a
# fresh machine ends up with a working `linear-cli` on PATH.
# `cargo install` places binaries in ~/.cargo/bin regardless of how Rust was installed
# (brew or rustup), so that must be on PATH for this run and for future shells.
export PATH="$HOME/.cargo/bin:$PATH"
# Windows: install the prebuilt x86_64 release binary via gh and skip the cargo chain — a source
# compile needs a C toolchain cargo can't assume (ring wants clang on ARM64 hosts), and the x86_64
# build runs natively on the all-x64 production machines and under Windows' built-in x64 emulation
# on ARM64 dev VMs. Falls through to the cargo chain below only if the download fails.
linear_cli_installed=false
if [ "$CLAUDE_OS" = windows ]; then
echo " Downloading prebuilt linear-cli release (x86_64-pc-windows-msvc)..."
linear_tmp=$(mktemp -d)
if gh release download --repo Finesssee/linear-cli --pattern '*x86_64-pc-windows-msvc.zip' --dir "$linear_tmp" --clobber \
&& unzip -o -q "$linear_tmp"/*.zip -d "$linear_tmp" \
&& mkdir -p "$HOME/.cargo/bin" \
&& cp "$(find "$linear_tmp" -name linear-cli.exe | head -1)" "$HOME/.cargo/bin/linear-cli.exe"; then
linear_cli_installed=true
else
echo " ⚠️ Prebuilt download failed — falling back to a cargo source build."
fi
rm -rf "$linear_tmp"
fi
if [ "$linear_cli_installed" = false ]; then
# 1. Rust toolchain (provides cargo). macOS installs it via Homebrew; on Windows/other we don't
# auto-run winget from Git Bash (see note above), so instruct via rustup and stop.
if ! command -v cargo >/dev/null 2>&1; then
if [ "$CLAUDE_OS" = macos ]; then
echo " Rust toolchain not found — installing via Homebrew (brew install rust)..."
brew install rust
else
echo " ❌ cargo not found. Install Rust via rustup — https://rustup.rs" >&2
[ "$CLAUDE_OS" = windows ] && echo " on Windows (PowerShell): winget install -e --id Rustlang.Rustup" >&2
echo " then re-run this script." >&2
exit 1
fi
fi
if ! command -v cargo >/dev/null 2>&1; then
echo " ❌ cargo still not found after install. Install Rust manually (https://rustup.rs) and re-run." >&2
exit 1
fi
# 2. cargo-binstall — pulls a prebuilt linear-cli binary (via QuickInstall) instead of
# a slow source compile. Homebrew-only: on non-mac, compiling binstall from source just to avoid
# compiling linear-cli is a net loss, so step 3's `cargo install` fallback handles those platforms.
if ! command -v cargo-binstall >/dev/null 2>&1 && [ "$CLAUDE_OS" = macos ]; then
echo " Installing cargo-binstall..."
brew install cargo-binstall 2>/dev/null || cargo install cargo-binstall
fi
# 3. linear-cli — binstall (fast, prebuilt) with a source-compile fallback.
if command -v cargo-binstall >/dev/null 2>&1; then
cargo binstall -y linear-cli || cargo install linear-cli
else
cargo install linear-cli
fi
fi
if ! command -v linear-cli >/dev/null 2>&1; then
echo " ❌ linear-cli install failed (not found on PATH after install)." >&2
exit 1
fi
echo " ✓ $(linear-cli --version 2>/dev/null | head -1)"
# 4. Persist ~/.cargo/bin on PATH for future shells (so skills that call linear-cli
# directly resolve it). Idempotent — skip if the rc already references cargo.
if [ -f "$HOME/.zshrc" ] \
&& ! grep -q 'cargo/bin' "$HOME/.zshrc" 2>/dev/null \
&& ! grep -q 'cargo/env' "$HOME/.zshrc" 2>/dev/null; then
printf '\n# rust/cargo — put ~/.cargo/bin on PATH (linear-cli, cargo-installed binaries)\nexport PATH="$HOME/.cargo/bin:$PATH"\n' >> "$HOME/.zshrc"
echo " ✓ added ~/.cargo/bin to ~/.zshrc PATH"
fi
# 5. Authentication. Linear skills are part of this repo (skills/) and need no
# separate install, but linear-cli must be authenticated to be useful. Check
# status; if not logged in and a TTY is available, run the interactive browser OAuth
# now; otherwise warn (a non-interactive run would hang on the prompt). LINEAR_API_KEY
# in the env also satisfies it.
if linear-cli auth status >/dev/null 2>&1; then
echo " ✓ linear-cli already authenticated"
elif [ -n "${LINEAR_API_KEY:-}" ]; then
echo " ✓ linear-cli will use LINEAR_API_KEY from the environment"
elif [ -t 0 ] && [ -t 1 ]; then
echo ""
echo " linear-cli is not authenticated — launching browser OAuth..."
if ! linear-cli auth oauth; then
echo ""
echo " ⚠️ Authentication did not complete. Run it yourself before using Linear skills:"
echo " linear-cli auth oauth # or: export LINEAR_API_KEY=<key>"
echo " linear-cli auth status # confirm"
fi
else
echo ""
echo " ⚠️ linear-cli is not authenticated and no TTY is available for interactive OAuth. Authenticate before using Linear skills:"
echo " linear-cli auth oauth # or: export LINEAR_API_KEY=<key>"
echo " linear-cli auth status # confirm"
fi
# Render a launchd plist template (__HOME__ → $HOME, since launchd needs absolute paths and won't expand
# $HOME) into ~/Library/LaunchAgents and (re)load it idempotently. Args: <label> <success-message tail>.
install_launchd_agent() {
local label="$1" cadence="$2"
local template="$HOME/.claude/launchd/$label.plist"
local dest="$HOME/Library/LaunchAgents/$label.plist"
[ -f "$template" ] || { echo " skipped — template missing: $template"; return 0; }
mkdir -p "$HOME/Library/LaunchAgents"
local rendered
rendered=$(sed "s|__HOME__|$HOME|g" "$template")
if [ "$rendered" != "$(cat "$dest" 2>/dev/null)" ]; then
printf '%s\n' "$rendered" > "$dest"
launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true
if launchctl bootstrap "gui/$(id -u)" "$dest" 2>/dev/null; then
echo " installed/updated — $cadence"
else
echo " WARNING: wrote $dest but launchctl bootstrap failed; load it manually:"
echo " launchctl bootstrap gui/\$(id -u) $dest"
fi
elif launchctl print "gui/$(id -u)/$label" >/dev/null 2>&1; then
echo " already current."
else
launchctl bootstrap "gui/$(id -u)" "$dest" 2>/dev/null \
&& echo " loaded — $cadence" \
|| echo " WARNING: could not load; run: launchctl bootstrap gui/\$(id -u) $dest"
fi
}
echo ""
echo "Installing launchd agents..."
# All four are local launchd mechanisms; skip on non-macOS. The drainer lands deferred /finish merges;
# the worktree reaper reclaims completed/abandoned /start wt worktrees (the PR-merged-later and
# Canceled-in-Linear cases finish-merge.sh's own cleanup can't reach); the stall watcher alerts on a
# /loop /auto session frozen mid-iteration by an API quota cutoff, which no Stop hook can see (a turn
# killed by an API error fires none) and no wakeup can recover (ScheduleWakeup is turn-ending, so an
# iteration in flight has none pending); the tmp reaper ages out the scratch every skill leaves under
# <project>/tmp/ while keeping the named handoff files other skills still read.
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "Installing merge-queue drainer (launchd)..."
install_launchd_agent "com.alienfast.merge-queue-drain" "drains the merge queue every 15 min."
echo "Installing worktree reaper (launchd)..."
install_launchd_agent "com.alienfast.worktree-reap" "reaps completed/abandoned worktrees hourly."
echo "Installing /auto stall watcher (launchd)..."
install_launchd_agent "com.alienfast.auto-stall-watch" "checks for stalled /auto sessions every 10 min."
echo "Installing tmp reaper (launchd)..."
install_launchd_agent "com.alienfast.tmp-reap" "ages out tmp/ scratch daily."
else
echo " skipped (macOS/launchd-only; this is $OSTYPE)."
fi
# One sweep now, on every platform: the launchd agent never fires on Windows or Linux, and even on macOS
# its first daily tick is up to a day away. Still in the project directory here (the cd below comes
# later), so the no-arg form covers the registered repos, ~/.claude, and the repo this run started in.
echo ""
echo "Reaping aged tmp/ scratch..."
bash "$claude_repo/scripts/reap-tmp.sh" reap || echo " WARNING: tmp reap failed (non-fatal); run ~/.claude/scripts/reap-tmp.sh list to inspect."
# Reconcile ~/.claude's own devDependencies (markdownlint-cli2) against the committed lockfile before the lint step below
# relies on them — a fresh clone has no node_modules, and a git pull can bump the lockfile out from under a stale install.
# --frozen-lockfile keeps it deterministic. Run FROM the repo, never through `-C`: this script also runs from a project
# directory (/update) and from a home directory (sync-main.sh's one-liner, /update in a non-project session), and Corepack
# picks the pnpm version from the cwd — `-C` is invisible to it, and pnpm does not switch versions under Corepack. From a
# home directory `pnpm -C ~/.claude install` therefore ran Corepack's default pnpm against a node_modules the pinned
# 11.21.0 had built and refused, without a TTY, to replace it (ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY — the /update
# failure on a contributor machine, 2026-09-18; a default newer than the pin fails a step earlier with
# ERR_PNPM_BAD_PM_VERSION). It never showed on the keeper's machine, whose Corepack default equals the pin. The cd also
# puts lint_and_fix's headless Claude where markdownlint's relative paths resolve; nothing after this needs the old cwd.
echo ""
echo "Installing ~/.claude devDependencies..."
cd "$claude_repo"
pnpm install --frozen-lockfile
lint_and_fix "pnpm check-markdown"
echo ""
echo ""
echo "Installed skills for: ${AI_AGENT_LIST[*]}"
echo ""
echo "Done! You must restart Claude (or vscode) for the changes to take effect."