#!/bin/bash ############################################################################### # migration-pack.sh # # Creates an encrypted migration archive of all non-git ClaudeTools data. # Works in Git Bash on Windows AND native Linux bash. # # Usage: ./migration-pack.sh [source_dir] # source_dir Path to ClaudeTools repo (default: script's parent directory) # # Output: claudetools-migration-YYYYMMDD.tar.gpg in current working directory ############################################################################### set -euo pipefail # --------------------------------------------------------------------------- # Globals # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="${1:-"$(cd "$SCRIPT_DIR/.." && pwd)"}" DATE_STAMP="$(date +%Y%m%d)" ARCHIVE_NAME="claudetools-migration-${DATE_STAMP}.tar.gpg" STAGING_DIR="" MANIFEST_FILE="MIGRATION_MANIFEST.txt" WARN_COUNT=0 COPY_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 "$STAGING_DIR" && -d "$STAGING_DIR" ]]; then log_info "Cleaning up staging directory..." rm -rf "$STAGING_DIR" fi } trap cleanup EXIT check_tool() { if ! command -v "$1" &>/dev/null; then log_error "Required tool not found: $1" exit 1 fi } # Copy a single file into the staging area, preserving relative path. # Warns and continues if the source does not exist. stage_file() { local rel_path="$1" local src="${SOURCE_DIR}/${rel_path}" local dst="${STAGING_DIR}/${rel_path}" if [[ ! -e "$src" ]]; then log_warn "File not found, skipping: ${rel_path}" return fi mkdir -p "$(dirname "$dst")" cp -a "$src" "$dst" COPY_COUNT=$((COPY_COUNT + 1)) log_ok "Staged: ${rel_path}" } # Copy an entire directory into the staging area. stage_dir() { local rel_path="$1" local src="${SOURCE_DIR}/${rel_path}" local dst="${STAGING_DIR}/${rel_path}" if [[ ! -d "$src" ]]; then log_warn "Directory not found, skipping: ${rel_path}" return fi mkdir -p "$(dirname "$dst")" cp -a "$src" "$dst" COPY_COUNT=$((COPY_COUNT + 1)) log_ok "Staged directory: ${rel_path}" } # Detect the Claude AI context directory based on platform conventions. # On Windows (Git Bash), the repo path D:\ClaudeTools becomes D--ClaudeTools. # On Linux/macOS, /home/user/ClaudeTools becomes -home-user-ClaudeTools. detect_claude_context_dir() { local claude_projects_base="${HOME}/.claude/projects" if [[ ! -d "$claude_projects_base" ]]; then echo "" return fi # Try Windows-style mapping first: D:\ClaudeTools -> D--ClaudeTools # Convert SOURCE_DIR from /d/path or D:/path to D--path local win_name="" if [[ "$SOURCE_DIR" =~ ^/([a-zA-Z])/(.*) ]]; then # Git Bash path like /d/ClaudeTools local drive="${BASH_REMATCH[1]^^}" local rest="${BASH_REMATCH[2]}" win_name="${drive}--${rest//\//-}" elif [[ "$SOURCE_DIR" =~ ^([a-zA-Z]):(.*) ]]; then # Windows path like D:\ClaudeTools or D:/ClaudeTools local drive="${BASH_REMATCH[1]^^}" local rest="${BASH_REMATCH[2]}" rest="${rest//\\/-}" rest="${rest//\//-}" rest="${rest#-}" win_name="${drive}--${rest}" fi if [[ -n "$win_name" && -d "${claude_projects_base}/${win_name}" ]]; then echo "${claude_projects_base}/${win_name}" return fi # Try Linux-style mapping: absolute path with slashes replaced by dashes local linux_name="${SOURCE_DIR//\//-}" linux_name="${linux_name#-}" if [[ -d "${claude_projects_base}/${linux_name}" ]]; then echo "${claude_projects_base}/${linux_name}" return fi echo "" } # Write a manifest of everything in the staging directory. write_manifest() { local manifest="${STAGING_DIR}/${MANIFEST_FILE}" { echo "============================================================" echo " ClaudeTools Migration Manifest" echo " Created: $(date '+%Y-%m-%d %H:%M:%S')" echo " Source: ${SOURCE_DIR}" echo " Host: $(hostname)" echo "============================================================" echo "" echo "Contents:" echo "------------------------------------------------------------" # Use find to list all files with sizes. # On Git Bash, stat flags differ from GNU coreutils; use portable approach. find "$STAGING_DIR" -type f ! -name "$MANIFEST_FILE" -print0 | while IFS= read -r -d '' file; do local rel="${file#"${STAGING_DIR}/"}" local size size="$(wc -c < "$file" 2>/dev/null || echo "?")" printf " %-60s %s bytes\n" "$rel" "$size" done | sort echo "------------------------------------------------------------" echo "" # Directory count and file count local dir_count file_count total_size dir_count="$(find "$STAGING_DIR" -mindepth 1 -type d | wc -l)" file_count="$(find "$STAGING_DIR" -type f ! -name "$MANIFEST_FILE" | wc -l)" total_size="$(find "$STAGING_DIR" -type f ! -name "$MANIFEST_FILE" -print0 | xargs -0 wc -c 2>/dev/null | tail -n1 | awk '{print $1}')" echo "Directories: ${dir_count}" echo "Files: ${file_count}" echo "Total size: ${total_size:-0} bytes" } > "$manifest" log_ok "Manifest written: ${MANIFEST_FILE}" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { echo "============================================================" echo " ClaudeTools Migration Packer" echo " $(date '+%Y-%m-%d %H:%M:%S')" echo "============================================================" echo "" # Validate source directory if [[ ! -d "$SOURCE_DIR" ]]; then log_error "Source directory does not exist: ${SOURCE_DIR}" exit 1 fi log_info "Source directory: ${SOURCE_DIR}" # Check required tools check_tool tar check_tool gpg log_ok "Required tools available (tar, gpg)" # Create staging directory STAGING_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t 'migration')" log_info "Staging directory: ${STAGING_DIR}" echo "" # ------------------------------------------------------------------ # Stage individual files # ------------------------------------------------------------------ log_info "--- Staging individual files ---" stage_file "credentials.md" stage_file ".env" stage_file ".mcp.json" stage_file "dataforth-notifications-creds.txt" stage_file ".claude/settings.local.json" stage_file "projects/solverbot/.env" stage_file "session-logs/2026-02-25-session.md" echo "" # ------------------------------------------------------------------ # Stage directories # ------------------------------------------------------------------ log_info "--- Staging directories ---" stage_dir "imported-conversations" stage_dir "backups" stage_dir "clients/gurushow" echo "" # ------------------------------------------------------------------ # Stage Claude AI context # ------------------------------------------------------------------ log_info "--- Staging Claude AI context ---" local claude_ctx claude_ctx="$(detect_claude_context_dir)" if [[ -n "$claude_ctx" && -d "$claude_ctx" ]]; then local ctx_dst="${STAGING_DIR}/claude-context" mkdir -p "$ctx_dst" cp -a "$claude_ctx"/. "$ctx_dst/" COPY_COUNT=$((COPY_COUNT + 1)) log_ok "Staged Claude context from: ${claude_ctx}" else log_warn "Claude AI context directory not found. Looked under \$HOME/.claude/projects/" log_warn "You may need to manually copy this after migration." fi echo "" # ------------------------------------------------------------------ # Write manifest # ------------------------------------------------------------------ log_info "--- Writing manifest ---" write_manifest echo "" # ------------------------------------------------------------------ # Create encrypted archive # ------------------------------------------------------------------ log_info "--- Creating encrypted archive ---" log_info "You will be prompted for a passphrase to encrypt the archive." echo "" # Create tar from staging contents, then encrypt with GPG symmetric. # Use --batch only if GPG_PASSPHRASE env var is set (for automation). local tar_tmp="${STAGING_DIR}.tar" tar -cf "$tar_tmp" -C "$STAGING_DIR" . if [[ -n "${GPG_PASSPHRASE:-}" ]]; then echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ --symmetric --cipher-algo AES256 \ --output "$ARCHIVE_NAME" "$tar_tmp" else gpg --symmetric --cipher-algo AES256 \ --output "$ARCHIVE_NAME" "$tar_tmp" fi rm -f "$tar_tmp" if [[ ! -f "$ARCHIVE_NAME" ]]; then log_error "Archive creation failed." exit 1 fi local archive_size archive_size="$(wc -c < "$ARCHIVE_NAME")" echo "" echo "============================================================" echo " Migration Pack Complete" echo "============================================================" echo "" echo " Archive: $(pwd)/${ARCHIVE_NAME}" echo " Size: ${archive_size} bytes" echo " Items: ${COPY_COUNT} files/directories staged" echo " Warnings: ${WARN_COUNT}" echo " Encrypted: AES-256 (GPG symmetric)" echo "" echo " To restore, run:" echo " ./migration-restore.sh ${ARCHIVE_NAME}" echo "" log_ok "Done." } main "$@"