diff --git a/scripts/rename-checkout.sh b/scripts/rename-checkout.sh new file mode 100755 index 0000000..9243aed --- /dev/null +++ b/scripts/rename-checkout.sh @@ -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/`). 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 /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."