#!/bin/bash ############################################################################### # migration-restore.sh # # Restores a ClaudeTools environment from an encrypted migration archive. # Works in Git Bash on Windows AND native Linux bash. # # Usage: ./migration-restore.sh [target_dir] # archive.tar.gpg Path to the encrypted migration archive # target_dir Where to clone/restore (default: $HOME/ClaudeTools) ############################################################################### set -euo pipefail # --------------------------------------------------------------------------- # Globals # --------------------------------------------------------------------------- ARCHIVE_PATH="${1:-}" TARGET_DIR="${2:-"${HOME}/ClaudeTools"}" GITEA_REPO="ssh://git@172.16.3.20:2222/azcomputerguru/claudetools.git" TEMP_EXTRACT="" WARN_COUNT=0 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log_info() { echo "[INFO] $*"; } log_ok() { echo "[OK] $*"; } log_warn() { echo "[WARNING] $*"; WARN_COUNT=$((WARN_COUNT + 1)); } log_error() { echo "[ERROR] $*"; } cleanup() { if [[ -n "$TEMP_EXTRACT" && -d "$TEMP_EXTRACT" ]]; then log_info "Cleaning up temporary extraction directory..." rm -rf "$TEMP_EXTRACT" fi } trap cleanup EXIT check_tool() { local tool="$1" if ! command -v "$tool" &>/dev/null; then log_error "Required tool not found: ${tool}" log_error "Please install ${tool} before running this script." exit 1 fi log_ok "Found: ${tool}" } # Derive the Claude projects directory name from the target path. # On Windows (Git Bash): /d/ClaudeTools -> D--ClaudeTools # On Linux: /home/user/ClaudeTools -> -home-user-ClaudeTools derive_claude_project_name() { local abs_target abs_target="$(cd "$TARGET_DIR" && pwd)" # Check if we are on Windows (Git Bash) by looking at path format if [[ "$abs_target" =~ ^/([a-zA-Z])/(.*) ]]; then # Git Bash path: /d/ClaudeTools -> D--ClaudeTools local drive="${BASH_REMATCH[1]^^}" local rest="${BASH_REMATCH[2]}" echo "${drive}--${rest//\//-}" elif [[ "$abs_target" =~ ^([a-zA-Z]):(.*) ]]; then # Raw Windows path: D:\ClaudeTools -> D--ClaudeTools local drive="${BASH_REMATCH[1]^^}" local rest="${BASH_REMATCH[2]}" rest="${rest//\\/-}" rest="${rest//\//-}" rest="${rest#-}" echo "${drive}--${rest}" else # Linux/macOS: /home/user/ClaudeTools -> -home-user-ClaudeTools local name="${abs_target//\//-}" name="${name#-}" echo "${name}" fi } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { echo "============================================================" echo " ClaudeTools Migration Restore" echo " $(date '+%Y-%m-%d %H:%M:%S')" echo "============================================================" echo "" # ------------------------------------------------------------------ # Validate arguments # ------------------------------------------------------------------ if [[ -z "$ARCHIVE_PATH" ]]; then log_error "Usage: $0 [target_dir]" log_error " archive.tar.gpg Encrypted migration archive" log_error " target_dir Restore location (default: \$HOME/ClaudeTools)" exit 1 fi if [[ ! -f "$ARCHIVE_PATH" ]]; then log_error "Archive not found: ${ARCHIVE_PATH}" exit 1 fi log_info "Archive: ${ARCHIVE_PATH}" log_info "Target dir: ${TARGET_DIR}" echo "" # ------------------------------------------------------------------ # Check required tools # ------------------------------------------------------------------ log_info "--- Checking required tools ---" check_tool git check_tool gpg check_tool tar echo "" # ------------------------------------------------------------------ # Decrypt archive # ------------------------------------------------------------------ log_info "--- Decrypting archive ---" log_info "You will be prompted for the passphrase." echo "" TEMP_EXTRACT="$(mktemp -d 2>/dev/null || mktemp -d -t 'migration-restore')" local tar_tmp="${TEMP_EXTRACT}/archive.tar" if [[ -n "${GPG_PASSPHRASE:-}" ]]; then echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ --decrypt --output "$tar_tmp" "$ARCHIVE_PATH" else gpg --decrypt --output "$tar_tmp" "$ARCHIVE_PATH" fi if [[ ! -f "$tar_tmp" ]]; then log_error "Decryption failed. Check your passphrase and try again." exit 1 fi log_ok "Archive decrypted." echo "" # ------------------------------------------------------------------ # Extract archive to temp location # ------------------------------------------------------------------ log_info "--- Extracting archive ---" local extract_dir="${TEMP_EXTRACT}/contents" mkdir -p "$extract_dir" tar -xf "$tar_tmp" -C "$extract_dir" rm -f "$tar_tmp" log_ok "Archive extracted." # Show manifest if present if [[ -f "${extract_dir}/MIGRATION_MANIFEST.txt" ]]; then echo "" log_info "--- Migration Manifest ---" cat "${extract_dir}/MIGRATION_MANIFEST.txt" echo "" fi # ------------------------------------------------------------------ # Clone repository # ------------------------------------------------------------------ log_info "--- Cloning repository ---" if [[ -d "$TARGET_DIR/.git" ]]; then log_warn "Target directory already contains a git repo: ${TARGET_DIR}" log_info "Skipping clone; will overlay migration files into existing repo." elif [[ -d "$TARGET_DIR" ]]; then # Directory exists but is not a git repo log_warn "Target directory exists but is not a git repo: ${TARGET_DIR}" log_info "Attempting clone into existing directory..." git clone "$GITEA_REPO" "${TARGET_DIR}.tmp" # Move .git and tracked files into existing directory mv "${TARGET_DIR}.tmp/.git" "${TARGET_DIR}/" # Checkout working tree into existing directory (cd "$TARGET_DIR" && git checkout -- .) rm -rf "${TARGET_DIR}.tmp" log_ok "Cloned repository into existing directory." else git clone "$GITEA_REPO" "$TARGET_DIR" log_ok "Cloned repository to: ${TARGET_DIR}" fi echo "" # ------------------------------------------------------------------ # Overlay non-git files from migration archive # ------------------------------------------------------------------ log_info "--- Restoring non-git files ---" # Copy everything except claude-context (handled separately) and manifest local item for item in "$extract_dir"/*; do local basename basename="$(basename "$item")" # Skip claude-context dir and manifest if [[ "$basename" == "claude-context" || "$basename" == "MIGRATION_MANIFEST.txt" ]]; then continue fi if [[ -d "$item" ]]; then cp -a "$item" "$TARGET_DIR/" log_ok "Restored directory: ${basename}" elif [[ -f "$item" ]]; then cp -a "$item" "$TARGET_DIR/" log_ok "Restored file: ${basename}" fi done # Handle dotfiles (hidden files/dirs from archive root) for item in "$extract_dir"/.*; do local basename basename="$(basename "$item")" [[ "$basename" == "." || "$basename" == ".." ]] && continue if [[ -d "$item" ]]; then # Merge directory contents (e.g., .claude/) cp -a "$item"/. "$TARGET_DIR/${basename}/" 2>/dev/null || cp -a "$item" "$TARGET_DIR/" log_ok "Restored directory: ${basename}" elif [[ -f "$item" ]]; then cp -a "$item" "$TARGET_DIR/" log_ok "Restored file: ${basename}" fi done echo "" # ------------------------------------------------------------------ # Restore Claude AI context # ------------------------------------------------------------------ log_info "--- Restoring Claude AI context ---" local claude_ctx_src="${extract_dir}/claude-context" if [[ -d "$claude_ctx_src" ]]; then local project_name project_name="$(derive_claude_project_name)" local claude_ctx_dst="${HOME}/.claude/projects/${project_name}" mkdir -p "$claude_ctx_dst" cp -a "$claude_ctx_src"/. "$claude_ctx_dst/" log_ok "Restored Claude context to: ${claude_ctx_dst}" else log_warn "No claude-context directory found in archive. Skipping." fi echo "" # ------------------------------------------------------------------ # Initialize submodules # ------------------------------------------------------------------ log_info "--- Initializing git submodules ---" (cd "$TARGET_DIR" && git submodule update --init --recursive) && \ log_ok "Submodules initialized." || \ log_warn "Submodule initialization had issues. You may need to run it manually." echo "" # ------------------------------------------------------------------ # Summary and post-restore checklist # ------------------------------------------------------------------ echo "============================================================" echo " Restore Complete" echo "============================================================" echo "" echo " Target: ${TARGET_DIR}" echo " Warnings: ${WARN_COUNT}" echo "" echo "============================================================" echo " Post-Restore Checklist" echo "============================================================" echo "" echo " [ ] Verify credentials.md contains correct, unredacted values" echo " File: ${TARGET_DIR}/credentials.md" echo "" echo " [ ] Set up Python virtual environment for MCP servers" echo " cd ${TARGET_DIR} && python -m venv .venv" echo " source .venv/bin/activate (or .venv\\Scripts\\activate on Windows)" echo " pip install -r requirements.txt" echo "" echo " [ ] Configure Claude Code CLI" echo " Run: claude (first launch will prompt for authentication)" echo " Verify .claude/ context was restored correctly" echo "" echo " [ ] Test Gitea SSH access" echo " Run: ssh -p 2222 git@172.16.3.20" echo " If it fails, copy your SSH keys and update ~/.ssh/config" echo "" echo " [ ] Rebuild grepai index" echo " The semantic search index is machine-specific." echo " Run grepai indexing from within Claude Code." echo "" echo " [ ] Verify .env and .mcp.json values are correct for new machine" echo "" echo " [ ] Test database connectivity" echo " Ensure 172.16.3.30:3306 is reachable from this machine" echo "" log_ok "Done." } main "$@"