CB-624: add scripts/rename-checkout.sh to rename the checkout safely
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:
Executable
+374
@@ -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."
|
||||
Reference in New Issue
Block a user