CB-624: add scripts/rename-checkout.sh to rename the checkout safely
CI / contract (pull_request) Successful in 46s
CI / build (pull_request) Successful in 1m32s

Renames ~/LTMS/claude-bridge -> ~/LTMS/fleetd as one auditable command,
modeled on redeploy-bridged.sh. --check reports every surface holding the
old absolute path (checkout files, launchd plist, Claude Code project
state, worktree .git pointers, running daemon). Apply mode stops the
daemon first (launchctl-aware), moves the checkout and the Claude Code
project-state slug dir derived from both paths, repairs worktree gitdir
pointers, rewrites bridged.yaml and the installed plist if they exist,
restarts, and verifies /healthz plus a fresh 'bridged listening' line
anchored to a pre-stop marker.
This commit is contained in:
Dai Ha
2026-08-23 05:55:04 +02:00
parent c135583402
commit d43f670285
+374
View File
@@ -0,0 +1,374 @@
#!/usr/bin/env bash
#
# Rename this checkout from ~/LTMS/claude-bridge to ~/LTMS/fleetd.
#
# The project was renamed upstream (fleet/fleetd on git.ltms.dev), but the checkout keeps its old
# name. Renaming it by hand misses surfaces, because the absolute path is written down in more
# places than the directory itself:
#
# 1. The launchd agent (~/Library/LaunchAgents/dev.ltms.bridged.plist) hard-codes absolute paths
# to the wrapper script and the jar. Move the checkout without fixing it and the daemon never
# starts again after a reboot.
# 2. Every git worktree carries a `.git` FILE with an ABSOLUTE pointer into the main checkout
# (`gitdir: /…/claude-bridge/.git/worktrees/<id>`). Rename the main checkout and every
# worktree breaks at once ("not a git repository").
# 3. Claude Code keys saved sessions and memory by a slug derived from the working directory,
# with `/` and `.` replaced by `-`. Move the checkout and it computes a NEW slug, finds
# nothing, and silently starts empty — every saved session is still on disk, just unreachable.
# 4. bridged/bridged.yaml is gitignored, so it survives only in the live checkout and holds
# `fleet.leaders.*.cwd` and `configDir` — both absolute, both old.
# 5. A running daemon holds the old paths open. Moving underneath it corrupts its state, so the
# daemon stops FIRST, and the script waits for the process to actually exit rather than
# assuming (same trap as redeploy-bridged.sh).
# 6. An old daemon that never died looks identical from the outside, so verification polls
# /healthz AND demands a FRESH "bridged listening" log line dated after the restart, anchored
# to a line-count marker taken before anything moved.
#
# Usage:
# scripts/rename-checkout.sh --check # report what holds the old path; changes nothing
# scripts/rename-checkout.sh # stop daemon, move everything, restart, verify
# scripts/rename-checkout.sh --yes # skip the confirmation prompt
#
# Run this FROM INSIDE OR OUTSIDE the checkout — it operates on fixed absolute paths, never on
# $PWD, because after the rename $PWD itself is gone.
#
# Exits non-zero on any failure. A failed pre-flight never touches the daemon.
set -euo pipefail
OLD_PATH="/Users/dai.ha/LTMS/claude-bridge"
NEW_PATH="/Users/dai.ha/LTMS/fleetd"
BRIDGED_SUB="bridged" # daemon lives at <checkout>/bridged/
JAR_NAME="bridged.jar"
OUT_NAME="bridged.out"
CC_INSTANCE="ltms"
CC_PROJECTS="$HOME/.ccs/instances/$CC_INSTANCE/projects"
LAUNCHD_LABEL='dev.ltms.bridged'
LAUNCHD_PLIST="$HOME/Library/LaunchAgents/$LAUNCHD_LABEL.plist"
HEALTH='http://127.0.0.1:8765/healthz'
PATTERN="target/$JAR_NAME"
STOP_WAIT=30 # seconds to wait for a clean daemon exit before reporting failure
HEALTH_WAIT=60 # seconds to wait for /healthz to answer after start
ASSUME_YES=0; CHECK_ONLY=0
for arg in "$@"; do
case "$arg" in
--check) CHECK_ONLY=1 ;;
--yes|-y) ASSUME_YES=1 ;;
-h|--help) sed -n '3,40p' "${BASH_SOURCE[0]}"; exit 0 ;;
*) echo "unknown option: $arg (try --help)" >&2; exit 2 ;;
esac
done
say() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
ok() { printf ' ok %s\n' "$*"; }
warn() { printf ' WARN %s\n' "$*"; }
die() { printf '\n FAIL %s\n\n' "$*" >&2; exit 1; }
running_pid() { pgrep -f "$PATTERN" || true; }
launchd_installed() { [ -f "$LAUNCHD_PLIST" ]; }
launchd_loaded() { launchctl list "$LAUNCHD_LABEL" >/dev/null 2>&1; }
# Claude Code derives its per-project directory name from the working directory by replacing
# "/" and "." with "-". Derived here, never hardcoded, so the script stays correct if either
# endpoint path ever changes again.
cc_slug() { printf '%s' "$1" | tr '/.' '-'; }
OLD_OUT="$OLD_PATH/$BRIDGED_SUB/$OUT_NAME"
NEW_OUT="$NEW_PATH/$BRIDGED_SUB/$OUT_NAME"
# Files under a directory that contain the literal old path, with a hit count each. Read-only.
# Skips .git internals, build output and node_modules: they are generated, huge, and repaired by
# rebuilding rather than rewriting.
scan_old_path_in() {
local root="$1"
[ -e "$root" ] || return 0
grep -rIlF --exclude-dir=.git --exclude-dir=target --exclude-dir=node_modules \
"$OLD_PATH" "$root" 2>/dev/null || true
}
report_hits() {
local label="$1" root="$2" f n total=0
while IFS= read -r f; do
n="$(grep -cF "$OLD_PATH" "$f" 2>/dev/null || true)"
printf ' %5d %s\n' "$n" "${f#"$root"/}"
total=$((total + 1))
done < <(scan_old_path_in "$root")
if [ "$total" -eq 0 ]; then
ok "$label: no files reference the old path"
else
warn "$label: $total file(s) reference the old path:"
fi
}
# ---------------------------------------------------------------------- --check
if [ "$CHECK_ONLY" = 1 ]; then
say "--check (read-only): who holds $OLD_PATH"
say "1. files containing the old absolute path"
if [ -d "$OLD_PATH" ]; then
report_hits "checkout ($OLD_PATH)" "$OLD_PATH"
else
warn "checkout $OLD_PATH does not exist — skipping its scan"
fi
if launchd_installed; then
report_hits "installed launchd plist ($LAUNCHD_PLIST)" "$LAUNCHD_PLIST"
else
warn "installed launchd plist not found at $LAUNCHD_PLIST"
fi
report_hits "Claude Code instance data ($CC_PROJECTS)" "$CC_PROJECTS"
say "2. launchd agent"
if launchd_installed; then
ok "installed: $LAUNCHD_PLIST"
ARGS="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments' "$LAUNCHD_PLIST" 2>/dev/null | grep -F '/' || true)"
if printf '%s' "$ARGS" | grep -qF "$OLD_PATH"; then
warn "plist ProgramArguments point at the OLD path:"
printf '%s\n' "$ARGS" | sed 's/^/ /'
elif [ -n "$ARGS" ]; then
ok "plist ProgramArguments do not reference the old path"
else
warn "could not read ProgramArguments from the plist"
fi
else
warn "launchd agent NOT installed (daemon would be started by hand only)"
fi
if launchd_loaded; then
ok "loaded: launchd supervises '$LAUNCHD_LABEL'"
else
warn "launchd agent not loaded"
fi
say "3. Claude Code project-state directory"
OLD_SLUG="$(cc_slug "$OLD_PATH")"; NEW_SLUG="$(cc_slug "$NEW_PATH")"
if [ -d "$CC_PROJECTS/$OLD_SLUG" ]; then
ok "old slug EXISTS: $CC_PROJECTS/$OLD_SLUG ($(find "$CC_PROJECTS/$OLD_SLUG" -type f | wc -l | tr -d ' ') files)"
else
warn "old slug missing: $CC_PROJECTS/$OLD_SLUG"
fi
if [ -e "$CC_PROJECTS/$NEW_SLUG" ]; then
warn "new slug ALREADY EXISTS: $CC_PROJECTS/$NEW_SLUG — moving over it needs a decision"
else
ok "new slug absent (clean): $CC_PROJECTS/$NEW_SLUG"
fi
say "4. git worktrees"
if [ -d "$OLD_PATH" ]; then
git -C "$OLD_PATH" worktree list || warn "'git worktree list' failed"
BROKEN=0
while IFS= read -r wt; do
[ "$wt" = "$OLD_PATH" ] && continue # the main checkout moves WITH the repo
GITFILE="$wt/.git"
if [ -f "$GITFILE" ] && grep -qF "$OLD_PATH" "$GITFILE" 2>/dev/null; then
warn "worktree $wt — .git pointer references the old path (breaks on rename)"
BROKEN=$((BROKEN + 1))
fi
done < <(git -C "$OLD_PATH" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
if [ "$BROKEN" -eq 0 ]; then
ok "no external worktree .git pointer references the old path"
fi
else
warn "checkout $OLD_PATH missing — cannot enumerate worktrees"
fi
say "5. daemon process"
PID="$(running_pid)"
if [ -n "$PID" ]; then
ok "daemon running, pid $PID"
CMD="$(ps -p "$PID" -o command= 2>/dev/null || true)"
if printf '%s' "$CMD" | grep -qF "$OLD_PATH"; then
warn "it runs the OLD path: $CMD"
else
ok "command: $CMD"
fi
else
warn "no daemon running"
fi
say "--check done: nothing changed"
exit 0
fi
# ------------------------------------------------------------------- apply mode
say "pre-flight"
[ -d "$OLD_PATH" ] || die "old checkout $OLD_PATH does not exist — nothing to rename"
[ -e "$NEW_PATH" ] && die "new path $NEW_PATH already exists — refusing to overwrite"
if [ -n "$(git -C "$OLD_PATH" status --porcelain 2>/dev/null)" ]; then
git -C "$OLD_PATH" status --short | sed 's/^/ /'
die "working tree at $OLD_PATH is dirty — commit or stash first, then rerun"
fi
ok "working tree clean, target path free"
if [ "$ASSUME_YES" = 0 ]; then
echo
echo " This MOVES the checkout this system runs from and RESTARTS the daemon."
echo " If you are connected through that daemon, your connection dies mid-turn."
echo
read -r -p " Type yes to continue: " reply
[ "$reply" = "yes" ] || die "aborted — nothing changed"
fi
SUPERVISED=0
launchd_loaded && SUPERVISED=1
# ------------------------------------------------------- stop the daemon FIRST
say "stop"
OLD_PID="$(running_pid)"
RESTART_MARK="$(wc -l < "$OLD_OUT" 2>/dev/null || echo 0)" # demand a FRESH listening line later
if [ -n "$OLD_PID" ]; then
if [ "$SUPERVISED" = 1 ]; then
echo " supervision is ON: 'launchctl unload' (not kill) so launchd's KeepAlive cannot"
echo " restart the OLD jar out from under this script."
launchctl unload -w "$LAUNCHD_PLIST" \
|| die "launchctl unload failed — investigate before retrying; NOTHING has been moved"
else
kill "$OLD_PID"
fi
for _ in $(seq "$STOP_WAIT"); do
[ -z "$(running_pid)" ] && break
sleep 1
done
[ -z "$(running_pid)" ] || die "pid $OLD_PID still alive after ${STOP_WAIT}s — NOT escalating to kill -9 and NOT moving anything. Wait or stop it by hand."
ok "pid $OLD_PID exited"
elif [ "$SUPERVISED" = 1 ]; then
launchctl unload -w "$LAUNCHD_PLIST" 2>/dev/null || true
ok "launchd agent unloaded (daemon was not running)"
else
ok "daemon was not running"
fi
# ------------------------------------------------------------------- move things
say "move"
mv "$OLD_PATH" "$NEW_PATH" || die "mv $OLD_PATH -> $NEW_PATH failed — the checkout is now at its old name, nothing else was touched"
ok "checkout moved: $OLD_PATH -> $NEW_PATH"
OLD_SLUG="$(cc_slug "$OLD_PATH")"; NEW_SLUG="$(cc_slug "$NEW_PATH")"
if [ -d "$CC_PROJECTS/$OLD_SLUG" ]; then
if [ -e "$CC_PROJECTS/$NEW_SLUG" ]; then
warn "NOT moving Claude Code state: $CC_PROJECTS/$NEW_SLUG already exists — merge by hand"
else
mv "$CC_PROJECTS/$OLD_SLUG" "$CC_PROJECTS/$NEW_SLUG" \
|| warn "moving Claude Code project state failed — sessions remain under the old slug"
ok "Claude Code project state moved: $OLD_SLUG -> $NEW_SLUG"
fi
else
warn "no Claude Code project state at $CC_PROJECTS/$OLD_SLUG — nothing to move"
fi
# ------------------------------------------------------- repair the worktrees
say "repair worktrees"
FIXED=0
for GITDIR_FILE in "$NEW_PATH"/.git/worktrees/*/gitdir; do
[ -f "$GITDIR_FILE" ] || continue
WT_GIT="$(cat "$GITDIR_FILE")"
if [ -f "$WT_GIT" ] && grep -qF "$OLD_PATH" "$WT_GIT"; then
sed -i '' "s|$OLD_PATH|$NEW_PATH|g" "$WT_GIT" \
|| warn "could not rewrite $WT_GIT — that worktree stays broken until fixed by hand"
FIXED=$((FIXED + 1))
fi
done
if git -C "$NEW_PATH" worktree list >/dev/null 2>&1; then
ok "main repo answers after the rename"
else
die "git cannot read $NEW_PATH after the rename — inspect .git by hand"
fi
if [ "$FIXED" -gt 0 ]; then
ok "rewrote $FIXED worktree .git pointer(s) to the new path"
else
ok "no worktree pointer referenced the old path"
fi
# ------------------------------------- rewrite gitignored config, if it survived
YAML="$NEW_PATH/$BRIDGED_SUB/bridged.yaml"
if [ -f "$YAML" ]; then
if grep -qF "$OLD_PATH" "$YAML"; then
cp "$YAML" "$YAML.pre-rename.bak"
sed -i '' "s|$OLD_PATH|$NEW_PATH|g" "$YAML"
ok "rewrote $YAML (backup: $YAML.pre-rename.bak)"
else
ok "$YAML does not reference the old path"
fi
else
warn "no $YAML present — skipped (this branch could not be exercised from a worktree)"
fi
# --------------------------------------------------------- fix + reload launchd
say "launchd"
if launchd_installed; then
if grep -qF "$OLD_PATH" "$LAUNCHD_PLIST"; then
cp "$LAUNCHD_PLIST" "$LAUNCHD_PLIST.pre-rename.bak"
sed -i '' "s|$OLD_PATH|$NEW_PATH|g" "$LAUNCHD_PLIST"
plutil -lint "$LAUNCHD_PLIST" >/dev/null \
|| die "rewritten plist fails plutil -lint — restore $LAUNCHD_PLIST.pre-rename.bak and fix by hand"
ok "rewrote $LAUNCHD_PLIST (backup beside it)"
else
ok "installed plist does not reference the old path"
fi
else
warn "launchd agent not installed — starting unsupervised below"
fi
# ------------------------------------------------------------- start + verify
say "start"
if [ "$SUPERVISED" = 1 ]; then
launchctl load -w "$LAUNCHD_PLIST" || die "launchctl load failed — recover with: launchctl load -w \"$LAUNCHD_PLIST\""
else
JAR="$NEW_PATH/$BRIDGED_SUB/target/$JAR_NAME"
[ -f "$JAR" ] || die "no jar at $JAR — build in the new location, then rerun"
( cd "$NEW_PATH/$BRIDGED_SUB" && zsh -lc "nohup java -jar '$JAR' >> $OUT_NAME 2>&1 &" )
fi
for _ in $(seq 10); do
NEW_PID="$(running_pid)"; [ -n "$NEW_PID" ] && break; sleep 1
done
[ -n "${NEW_PID:-}" ] || die "no daemon process appeared. Last lines of $NEW_OUT:
$(tail -20 "$NEW_OUT" 2>/dev/null)"
[ "$NEW_PID" != "${OLD_PID:-}" ] || die "pid unchanged ($NEW_PID) — the old daemon never died"
ok "started, pid $NEW_PID"
say "verify"
HEALTH_OK=0; CODE=""
for _ in $(seq "$HEALTH_WAIT"); do
CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "$HEALTH" 2>/dev/null || echo 000)"
case "$CODE" in
200) HEALTH_OK=1; ok "/healthz 200"; break ;;
# 503 still means the daemon is UP — it means herdr is unreachable.
503) HEALTH_OK=1; warn "/healthz answers 503 degraded — daemon up, herdr unreachable"; break ;;
esac
sleep 1
done
[ "$HEALTH_OK" = 1 ] || die "/healthz never answered within ${HEALTH_WAIT}s (last code: ${CODE:-000})"
if tail -n "+$((RESTART_MARK + 1))" "$NEW_OUT" 2>/dev/null | grep -q 'bridged listening'; then
ok "$(tail -n "+$((RESTART_MARK + 1))" "$NEW_OUT" | grep 'bridged listening' | tail -1)"
else
warn "no fresh 'bridged listening' line after the restart — check $NEW_OUT yourself"
fi
# ------------------------------------------------------------------- summary
say "changed"
echo " checkout: $OLD_PATH -> $NEW_PATH"
echo " claude code state: $CC_PROJECTS/$OLD_SLUG -> $CC_PROJECTS/$NEW_SLUG (when present)"
echo " worktree .git ptr: rewritten where they named the old path"
[ -f "$YAML" ] && echo " bridged.yaml: rewritten (backup kept)"
launchd_installed && echo " launchd plist: rewritten (backup kept)"
say "still referencing the old path (left alone on purpose)"
LEFT=$(scan_old_path_in "$NEW_PATH")
if [ -n "$LEFT" ]; then
printf '%s\n' "$LEFT" | sed 's/^/ /'
echo " Review these by hand: tracked docs and IDE state are yours to update,"
echo " session history must not be rewritten wholesale."
else
ok "(none inside the checkout)"
fi
scan_old_path_in "$CC_PROJECTS" | head -5 | sed 's/^/ /' || true
echo " (session transcripts quote the old cwd — history, left untouched)"
echo
echo " Next: reconnect leads/members to the renamed checkout and re-check with --check."