fix(migrate-v2): bash 3.2 compatibility + reset coverage

migrate-v2.sh
  Replace `declare -A STEP_RESULTS` with two parallel indexed arrays
  (STEP_NAMES + STEP_STATUSES) plus a `record_step` helper. macOS ships
  bash 3.2 which has no associative arrays — `declare -A` errored out
  silently and every `STEP_RESULTS["1a-env"]=...` triggered a fatal
  bash arithmetic error (interpreting "1a" as a number). Visible
  symptom: `steps: {}` in handoff.json. Latent symptom: phase 2c's
  install loop sometimes bailed mid-iteration before invoking the
  channel install script, leaving channel code uninstalled while
  reporting `overall_status: success`.

migrate-v2-reset.sh
  Cover the gaps that left install side-effects in place between
  iterations:
    - Remove untracked adapter files in src/channels/ (mirror the
      pattern already used for container/skills/).
    - Restore tracked setup helpers that channel installs overwrite
      (setup/whatsapp-auth.ts, setup/pair-telegram.ts, setup/index.ts)
      and remove untracked ones they create (setup/groups.ts).
    - Restore package.json + pnpm-lock.yaml (channel installs add
      deps like @whiskeysockets/baileys).
  Setup/migrate-v2/* is intentionally not touched — that's where user
  WIP lives.

Verified end-to-end: reset → migrate → all 9 steps reported in
handoff.json with status "success", phase 2c install actually runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gavriel Cohen
2026-05-02 14:50:21 +03:00
parent aec7ddd099
commit 416c283dcb
2 changed files with 73 additions and 31 deletions

View File

@@ -6,17 +6,27 @@
# bash migrate-v2-reset.sh && bash migrate-v2.sh # bash migrate-v2-reset.sh && bash migrate-v2.sh
# #
# What it removes: # What it removes:
# - data/ (v2 DBs, session state) # - data/ (v2 DBs, session state)
# - logs/ (migration + setup logs) # - logs/ (migration + setup logs)
# - .env (merged env keys) # - .env (merged env keys)
# - groups/*/ (non-git group folders copied from v1) # - groups/*/ (non-git group folders copied from v1)
# - container/skills/*/ (untracked skill dirs copied from v1)
# - src/channels/*.ts (untracked adapters copied from channels branch)
# - setup/groups.ts (untracked, copied by channel install scripts)
# #
# What it restores: # What it restores from git:
# - groups/global/CLAUDE.md and groups/main/CLAUDE.md from git # - groups/ (CLAUDE.md files etc.)
# - container/skills/ (tracked container skills)
# - src/channels/ (tracked bridge / registry code)
# - setup/whatsapp-auth.ts (channel installs may overwrite)
# - setup/pair-telegram.ts (channel installs may overwrite)
# - setup/index.ts (channel installs append entries)
# - package.json + pnpm-lock.yaml (channel installs add deps)
# #
# What it does NOT touch: # What it does NOT touch:
# - node_modules/ (expensive to reinstall, keep it) # - node_modules/ (expensive to reinstall, kept on purpose)
# - The v1 install (read-only, never modified) # - setup/migrate-v2/* (the migration scripts themselves, plus user WIP)
# - The v1 install (read-only, never modified)
set -euo pipefail set -euo pipefail
@@ -63,7 +73,26 @@ printf '%s Restored %s\n' "$(green '✓')" "container/skills/ from git"
# Restore channel code (src/channels/) to git state # Restore channel code (src/channels/) to git state
git checkout -- src/channels/ 2>/dev/null || true git checkout -- src/channels/ 2>/dev/null || true
# Remove any untracked channel adapters copied in by install-*.sh
for f in src/channels/*.ts; do
[ -f "$f" ] || continue
if ! git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
rm -f "$f"
fi
done
printf '%s Restored %s\n' "$(green '✓')" "src/channels/ from git" printf '%s Restored %s\n' "$(green '✓')" "src/channels/ from git"
# Restore tracked setup helpers that channel installs overwrite, and
# remove the untracked ones they create. Don't blanket-clean setup/
# because user WIP (setup/migrate-v2/*) lives there too.
git checkout -- setup/whatsapp-auth.ts setup/pair-telegram.ts setup/index.ts 2>/dev/null || true
rm -f setup/groups.ts
printf '%s Restored %s\n' "$(green '✓')" "setup/ install helpers"
# Restore package.json + lockfile (channel installs add deps like
# @whiskeysockets/baileys). node_modules/ is intentionally kept.
git checkout -- package.json pnpm-lock.yaml 2>/dev/null || true
printf '%s Restored %s\n' "$(green '✓')" "package.json + pnpm-lock.yaml"
echo echo
printf '%s\n\n' "$(dim 'Clean. Run: bash migrate-v2.sh')" printf '%s\n\n' "$(dim 'Clean. Run: bash migrate-v2.sh')"

View File

@@ -29,14 +29,25 @@ SERVICE_SWITCHED=false
SELECTED_CHANNELS=() SELECTED_CHANNELS=()
ABORTED_AT="" ABORTED_AT=""
# Per-step status tracking. Parallel indexed arrays so this works on
# bash 3.2 (macOS default) which has no associative arrays.
STEP_NAMES=()
STEP_STATUSES=()
record_step() {
STEP_NAMES+=("$1")
STEP_STATUSES+=("$2")
}
# Write handoff.json on any exit so the skill can always read it # Write handoff.json on any exit so the skill can always read it
write_handoff() { write_handoff() {
local handoff_dir="$LOGS_DIR/setup-migration" local handoff_dir="$LOGS_DIR/setup-migration"
mkdir -p "$handoff_dir" mkdir -p "$handoff_dir"
local has_failures=false local has_failures=false
for step_name in "${!STEP_RESULTS[@]}"; do local i
[ "${STEP_RESULTS[$step_name]}" = "failed" ] && has_failures=true for ((i=0; i<${#STEP_NAMES[@]}; i++)); do
[ "${STEP_STATUSES[$i]}" = "failed" ] && has_failures=true
done done
local overall="success" local overall="success"
@@ -44,8 +55,10 @@ write_handoff() {
[ -n "$ABORTED_AT" ] && overall="failed" [ -n "$ABORTED_AT" ] && overall="failed"
local steps_json="{" local steps_json="{"
for step_name in "${!STEP_RESULTS[@]}"; do for ((i=0; i<${#STEP_NAMES[@]}; i++)); do
steps_json="${steps_json}\"${step_name}\": {\"status\": \"${STEP_RESULTS[$step_name]}\", \"log\": \"logs/migrate-steps/${step_name}.log\"}," local n="${STEP_NAMES[$i]}"
local s="${STEP_STATUSES[$i]}"
steps_json="${steps_json}\"${n}\": {\"status\": \"${s}\", \"log\": \"logs/migrate-steps/${n}.log\"},"
done done
steps_json="${steps_json%,}}" steps_json="${steps_json%,}}"
@@ -245,8 +258,8 @@ export NANOCLAW_V2_PATH="$PROJECT_ROOT"
# ─── run_step helper ───────────────────────────────────────────────────── # ─── run_step helper ─────────────────────────────────────────────────────
# Runs a TypeScript migration step, captures output, reports success/failure. # Runs a TypeScript migration step, captures output, reports success/failure.
# Track step outcomes for handoff.json # Step outcomes are tracked via record_step() into STEP_NAMES/STEP_STATUSES
declare -A STEP_RESULTS # (defined above, near write_handoff).
run_step() { run_step() {
local name=$1 label=$2 script=$3 local name=$1 label=$2 script=$3
@@ -258,7 +271,7 @@ run_step() {
result=$(grep '^OK:' "$raw" | head -1 || true) result=$(grep '^OK:' "$raw" | head -1 || true)
step_ok "$label $(dim "$result")" step_ok "$label $(dim "$result")"
log "$name: $result" log "$name: $result"
STEP_RESULTS[$name]="success" record_step "$name" "success"
# Surface partial errors (rows skipped due to parse/lookup failures) # Surface partial errors (rows skipped due to parse/lookup failures)
# even when the step exited successfully — they're easy to miss in the # even when the step exited successfully — they're easy to miss in the
# raw log and have caused silent migrations before. # raw log and have caused silent migrations before.
@@ -276,7 +289,7 @@ run_step() {
reason=$(grep '^SKIPPED:' "$raw" | head -1 | sed 's/^SKIPPED://') reason=$(grep '^SKIPPED:' "$raw" | head -1 | sed 's/^SKIPPED://')
step_skip "$label $(dim "($reason)")" step_skip "$label $(dim "($reason)")"
log "$name: skipped ($reason)" log "$name: skipped ($reason)"
STEP_RESULTS[$name]="skipped" record_step "$name" "skipped"
else else
step_fail "$label" step_fail "$label"
echo echo
@@ -285,7 +298,7 @@ run_step() {
done done
echo echo
log "$name: FAILED (see $raw)" log "$name: FAILED (see $raw)"
STEP_RESULTS[$name]="failed" record_step "$name" "failed"
fi fi
} }
@@ -359,10 +372,10 @@ else
STATUS_LINE=$(grep '^STATUS:' "$STEP_LOG" | head -1 | sed 's/^STATUS: *//') STATUS_LINE=$(grep '^STATUS:' "$STEP_LOG" | head -1 | sed 's/^STATUS: *//')
if [ "$STATUS_LINE" = "already-installed" ]; then if [ "$STATUS_LINE" = "already-installed" ]; then
step_skip "Install $ch $(dim "(already installed)")" step_skip "Install $ch $(dim "(already installed)")"
STEP_RESULTS[$STEP_NAME]="skipped" record_step "$STEP_NAME" "skipped"
else else
step_ok "Install $ch" step_ok "Install $ch"
STEP_RESULTS[$STEP_NAME]="success" record_step "$STEP_NAME" "success"
fi fi
log "install-$ch: $STATUS_LINE" log "install-$ch: $STATUS_LINE"
else else
@@ -371,12 +384,12 @@ else
echo " $(dim "$line")" echo " $(dim "$line")"
done done
log "install-$ch: FAILED (see $STEP_LOG)" log "install-$ch: FAILED (see $STEP_LOG)"
STEP_RESULTS[$STEP_NAME]="failed" record_step "$STEP_NAME" "failed"
fi fi
else else
step_skip "Install $ch $(dim "(no install script)")" step_skip "Install $ch $(dim "(no install script)")"
log "install-$ch: no install script" log "install-$ch: no install script"
STEP_RESULTS[$STEP_NAME]="failed" record_step "$STEP_NAME" "failed"
fi fi
done done
fi fi
@@ -401,11 +414,11 @@ else
if bash setup/install-docker.sh > "$DOCKER_LOG" 2>&1; then if bash setup/install-docker.sh > "$DOCKER_LOG" 2>&1; then
hash -r 2>/dev/null || true hash -r 2>/dev/null || true
step_ok "Docker installed" step_ok "Docker installed"
STEP_RESULTS["3a-docker"]="success" record_step "3a-docker" "success"
log "Docker: installed" log "Docker: installed"
else else
step_fail "Docker install failed $(dim "(see $DOCKER_LOG)")" step_fail "Docker install failed $(dim "(see $DOCKER_LOG)")"
STEP_RESULTS["3a-docker"]="failed" record_step "3a-docker" "failed"
log "Docker: FAILED" log "Docker: FAILED"
fi fi
fi fi
@@ -426,16 +439,16 @@ elif command -v docker >/dev/null 2>&1; then
if pnpm exec tsx setup/index.ts --step onecli > "$ONECLI_LOG" 2>"$ONECLI_ERR"; then if pnpm exec tsx setup/index.ts --step onecli > "$ONECLI_LOG" 2>"$ONECLI_ERR"; then
step_ok "OneCLI ready" step_ok "OneCLI ready"
ONECLI_OK=true ONECLI_OK=true
STEP_RESULTS["3b-onecli"]="success" record_step "3b-onecli" "success"
log "OneCLI: installed/configured" log "OneCLI: installed/configured"
else else
step_fail "OneCLI setup failed $(dim "(see $ONECLI_LOG)")" step_fail "OneCLI setup failed $(dim "(see $ONECLI_LOG)")"
STEP_RESULTS["3b-onecli"]="failed" record_step "3b-onecli" "failed"
log "OneCLI: FAILED" log "OneCLI: FAILED"
fi fi
else else
step_fail "OneCLI needs Docker $(dim "(install Docker first)")" step_fail "OneCLI needs Docker $(dim "(install Docker first)")"
STEP_RESULTS["3b-onecli"]="failed" record_step "3b-onecli" "failed"
log "OneCLI: skipped (no Docker)" log "OneCLI: skipped (no Docker)"
fi fi
@@ -449,11 +462,11 @@ elif [ "$ONECLI_OK" = "true" ]; then
AUTH_ERR="$STEPS_DIR/3c-auth.err" AUTH_ERR="$STEPS_DIR/3c-auth.err"
if pnpm exec tsx setup/index.ts --step auth > "$AUTH_LOG" 2>"$AUTH_ERR"; then if pnpm exec tsx setup/index.ts --step auth > "$AUTH_LOG" 2>"$AUTH_ERR"; then
step_ok "Anthropic credential registered" step_ok "Anthropic credential registered"
STEP_RESULTS["3c-auth"]="success" record_step "3c-auth" "success"
log "Anthropic credential: registered via auth step" log "Anthropic credential: registered via auth step"
else else
step_fail "Auth setup failed $(dim "(see $AUTH_LOG)")" step_fail "Auth setup failed $(dim "(see $AUTH_LOG)")"
STEP_RESULTS["3c-auth"]="failed" record_step "3c-auth" "failed"
log "Anthropic credential: FAILED" log "Anthropic credential: FAILED"
fi fi
else else
@@ -494,11 +507,11 @@ if command -v docker >/dev/null 2>&1; then
BUILD_LOG="$STEPS_DIR/3e-container-build.log" BUILD_LOG="$STEPS_DIR/3e-container-build.log"
if bash container/build.sh > "$BUILD_LOG" 2>&1; then if bash container/build.sh > "$BUILD_LOG" 2>&1; then
step_ok "Container image built" step_ok "Container image built"
STEP_RESULTS["3e-build"]="success" record_step "3e-build" "success"
log "Container build: success" log "Container build: success"
else else
step_fail "Container build failed" step_fail "Container build failed"
STEP_RESULTS["3e-build"]="failed" record_step "3e-build" "failed"
tail -10 "$BUILD_LOG" 2>/dev/null | while IFS= read -r line; do tail -10 "$BUILD_LOG" 2>/dev/null | while IFS= read -r line; do
echo " $(dim "$line")" echo " $(dim "$line")"
done done
@@ -506,7 +519,7 @@ if command -v docker >/dev/null 2>&1; then
fi fi
else else
step_fail "Docker not available — cannot build container" step_fail "Docker not available — cannot build container"
STEP_RESULTS["3e-build"]="failed" record_step "3e-build" "failed"
log "Container build: skipped (no Docker)" log "Container build: skipped (no Docker)"
fi fi