#!/usr/bin/env bash # migrate.sh — bring an existing ECHO vault up to the plugin's current schema. # # Reads the marker's schema_version and applies each intervening migration in order. # Migrations are idempotent and additive; every destructive step (DELETE) is gated # behind --apply AND prints what it will do first. Default mode is a DRY-RUN plan. # # Usage: # migrate.sh # print the migration plan (no changes) # migrate.sh --apply # perform the migration (moves/deletes included) # # Env: ECHO_BASE, ECHO_KEY (via echo.sh). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ECHO="$SCRIPT_DIR/echo.sh" CURRENT_SCHEMA=2 APPLY=0 [ "${1:-}" = "--apply" ] && APPLY=1 [ -x "$ECHO" ] || chmod +x "$ECHO" 2>/dev/null || true say() { echo "migrate: $*"; } do_or_show() { # do_or_show "" cmd args... local desc="$1"; shift if [ "$APPLY" = "1" ]; then say "APPLY $desc"; "$@"; else say "PLAN $desc"; fi } # ---- Read current schema ----------------------------------------------------- if ! marker="$("$ECHO" get _agent/echo-vault.md 2>/dev/null)"; then say "marker missing — vault not bootstrapped. Run bootstrap.sh, not migrate.sh." exit 3 fi FROM="$(printf '%s' "$marker" | sed -n 's/^schema_version:[[:space:]]*//p' | head -1)" FROM="${FROM:-0}" say "vault schema_version=$FROM, plugin schema=$CURRENT_SCHEMA $([ "$APPLY" = 1 ] && echo '(APPLY)' || echo '(dry-run)')" if [ "$FROM" -ge "$CURRENT_SCHEMA" ] 2>/dev/null; then say "up to date — nothing to do." exit 0 fi ls_files() { "$ECHO" ls "$1" 2>/dev/null | python3 -c 'import sys,json;print("\n".join(json.load(sys.stdin).get("files",[])))' 2>/dev/null || true; } move() { # move SRC DST preserving content (PUT dst <- get src, then delete src) local src="$1" dst="$2" "$ECHO" get "$src" 2>/dev/null | "$ECHO" put "$dst" - >/dev/null "$ECHO" delete "$src" >/dev/null } # ---- 0 -> 1 : control docs moved into the plugin ----------------------------- mig_0_1() { say "[0->1] retire in-vault control docs (CLAUDE/BOOTSTRAP/STRUCTURE/index.md)" for f in CLAUDE.md BOOTSTRAP.md STRUCTURE.md index.md; do if ECHO_VERIFY=0 "$ECHO" get "$f" >/dev/null 2>&1; then do_or_show "delete vault/$f (back it up outside the vault first)" "$ECHO" delete "$f" fi done say "[0->1] reminder: scrub dangling [[CLAUDE]]/[[BOOTSTRAP]]/[[STRUCTURE]]/[[index]] links from ## Related sections (manual/agent step)." } # ---- 1 -> 2 : reviews/ folded into journal/ + _agent/health/ ----------------- mig_1_2() { say "[1->2] fold reviews/ into journal/ and _agent/health/" for f in $(ls_files reviews/weekly); do [ "${f%.md}" != "$f" ] || continue dst="journal/weekly/$(printf '%s' "$f" | sed 's/-review\.md$/.md/')" do_or_show "move reviews/weekly/$f -> $dst" move "reviews/weekly/$f" "$dst" done for f in $(ls_files reviews/monthly); do [ "${f%.md}" != "$f" ] || continue case "$f" in *vault-health.md) dst="_agent/health/$f" ;; *) dst="journal/monthly/$f" ;; esac do_or_show "move reviews/monthly/$f -> $dst" move "reviews/monthly/$f" "$dst" done for period in quarterly annual; do for f in $(ls_files "reviews/$period"); do [ "${f%.md}" != "$f" ] || continue do_or_show "move reviews/$period/$f -> journal/$period/$f" move "reviews/$period/$f" "journal/$period/$f" done done say "[1->2] reminder: update inbound [[reviews/...]] wikilinks in ## Related sections (manual/agent step)." } [ "$FROM" -lt 1 ] && mig_0_1 [ "$FROM" -lt 2 ] && mig_1_2 # ---- Stamp the marker -------------------------------------------------------- do_or_show "set _agent/echo-vault.md schema_version -> $CURRENT_SCHEMA" \ "$ECHO" fm _agent/echo-vault.md schema_version "$CURRENT_SCHEMA" if [ "$APPLY" = "1" ]; then say "migration complete -> schema $CURRENT_SCHEMA. Run vault-lint.sh to confirm invariants." else say "dry-run only. Re-run with --apply to perform the moves/deletes above." fi