#!/usr/bin/env bash set -Eeuo pipefail # Portable backup/restore for Pi Coding Agent. # Usage: # ./pi-env-backup.sh backup [destination-dir] # ./pi-env-backup.sh list # ./pi-env-backup.sh restore PROGRAM=${0##*/} SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) log() { printf '[pi-backup] %s\n' "$*"; } fail() { printf '[pi-backup] ERROR: %s\n' "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || fail "Не найдено: $1"; } usage() { cat <<'EOF' Portable Pi Coding Agent backup Создать бэкап: ./pi-env-backup.sh backup [папка-для-архива] Посмотреть содержимое: ./pi-env-backup.sh list Восстановить на ноутбуке/WSL: ./pi-env-backup.sh restore <архив> Переменные: PI_BACKUP_INCLUDE_SESSIONS=1 включить историю Pi-сессий (по умолчанию 0) PI_BACKUP_INCLUDE_CONTEXT=1 включить context-mode index/cache (по умолчанию 0) PI_BACKUP_INCLUDE_AYU=1 включить ayu/checkpoints (по умолчанию 0; обычно не нужно) PI_BACKUP_ALLOW_RUNNING=1 не предупреждать о запущенном Pi EOF } running_processes() { pgrep -af '(^|/)(pi|pi-coding-agent)( |$)' 2>/dev/null || true } make_manifest() { local file=$1 { echo "created_at=$(date -Is)" echo "source_user=$USER" echo "source_home=$HOME" echo "hostname=$(hostname)" echo "uname=$(uname -srmo)" echo "pi_version=$(pi --version 2>/dev/null || echo unavailable)" echo "node_version=$(node --version 2>/dev/null || echo unavailable)" echo "npm_version=$(npm --version 2>/dev/null || echo unavailable)" echo "shell=${SHELL:-unknown}" echo echo '[installed-pi-packages]' pi list 2>/dev/null || true echo echo '[external-tools]' for tool in pi node npm npx bun pnpm python3 pipx git gh jj rg fd docker zstd; do if command -v "$tool" >/dev/null 2>&1; then printf '%s: ' "$tool" case "$tool" in pi) pi --version 2>&1 || true ;; node|bun|pnpm|python3|git|gh|jj|rg|fd|docker|zstd) "$tool" --version 2>&1 || true ;; npm|npx|pipx) "$tool" --version 2>&1 || true ;; esac fi done echo echo '[paths]' find "$HOME/.pi/agent" "$HOME/.agents" -maxdepth 2 -type d 2>/dev/null | sort || true } > "$file" } backup() { local dest=${1:-$PWD} mkdir -p "$dest" need tar need pi if [[ "${PI_BACKUP_ALLOW_RUNNING:-0}" != 1 ]] && [[ -n "$(running_processes)" ]]; then fail "Pi запущен. Закрой Pi и повтори. Для принудительного режима: PI_BACKUP_ALLOW_RUNNING=1" fi local stamp archive ext compressor tmp root stamp=$(date +%Y%m%d-%H%M%S) tmp=$(mktemp -d) trap 'rm -rf "$tmp"' RETURN root="$tmp/pi-backup" mkdir -p "$root/meta" make_manifest "$root/meta/MANIFEST.txt" cp "$SELF_DIR/pi-env-backup.sh" "$root/meta/pi-env-backup.sh" chmod 755 "$root/meta/pi-env-backup.sh" cat > "$root/meta/RESTORE.md" <<'EOF' # Pi Coding Agent backup Этот архив содержит пользовательское состояние Pi Coding Agent: - `~/.pi/agent/settings.json` — настройки, модели и список packages; - `~/.pi/agent/auth.json` — авторизация/API keys; - `~/.pi/agent/mcp.json` и `mcp-oauth/` — MCP; - `~/.pi/agent/extensions/`, `skills/`, `npm/`, `git/` — плагины и пакеты; - `~/.pi/agent/AGENTS.md`, `keybindings.json`, `trust.json`; - `~/.pi/agent/sessions/` — история Pi; - `~/.pi/agent/pi-hermes-memory/`, `projects-memory/` — память; - `~/.pi/context-mode/` — локальный индекс context-mode; - `~/.agents/` — внешние пользовательские skills. ## Восстановление Внутри WSL установите Node.js/npm, затем выполните из распакованного архива: ```bash bash meta/pi-env-backup.sh restore pi-backup.tar.zst ``` Или передайте архив и этот скрипт на ноутбук: ```bash bash pi-env-backup.sh restore pi-backup.tar.zst ``` Скрипт НЕ переносит бинарник Pi и внешние системные программы. После восстановления он переустанавливает Pi через npm и проверяет состояние. Авторизация из `auth.json` переносится, но некоторые OAuth-провайдеры могут попросить повторный `/login`. EOF # Store paths relative to $HOME. Never archive unrelated parts of the home directory. local -a paths=(.pi/agent .agents) [[ -d "$HOME/.pi/local-docs" ]] && paths+=(.pi/local-docs) [[ "${PI_BACKUP_INCLUDE_CONTEXT:-0}" == 1 && -d "$HOME/.pi/context-mode" ]] && paths+=(.pi/context-mode) if [[ "${PI_BACKUP_INCLUDE_SESSIONS:-0}" != 1 ]]; then # handled after copying by tar exclusions log 'История сессий будет исключена (PI_BACKUP_INCLUDE_SESSIONS=0)' fi if command -v zstd >/dev/null 2>&1; then archive="$dest/pi-backup-$stamp.tar.zst" compressor=(zstd -T0 -19 -q) else archive="$dest/pi-backup-$stamp.tar.gz" compressor=(gzip -9) log 'zstd не найден; используется gzip. Для меньшего архива установи zstd.' fi log "Архивирую: ${paths[*]}" # Build one tar with relative paths only, then compress it. No absolute paths leak. # По умолчанию не переносим историю, context-mode cache и ayu checkpoints. # Они не нужны для работы плагинов, но могут занимать десятки гигабайт. tar_args=(--exclude=.pi/agent/.locks --exclude=.pi/agent/*.lock) [[ "${PI_BACKUP_INCLUDE_SESSIONS:-0}" != 1 ]] && tar_args+=(--exclude=.pi/agent/sessions) [[ "${PI_BACKUP_INCLUDE_CONTEXT:-0}" != 1 ]] && tar_args+=(--exclude=.pi/context-mode) [[ "${PI_BACKUP_INCLUDE_AYU:-0}" != 1 ]] && tar_args+=(--exclude=.pi/agent/ayu) tar -C "$root" -cf "$tmp/all.tar" meta tar "${tar_args[@]}" -C "$HOME" -rf "$tmp/all.tar" "${paths[@]}" 2>/dev/null || true if [[ "$archive" == *.zst ]]; then zstd -T0 -19 -q "$tmp/all.tar" -o "$archive" else gzip -9 -c "$tmp/all.tar" > "$archive" fi chmod 600 "$archive" sha256sum "$archive" | tee "$archive.sha256" log "Готово: $archive" log "Размер: $(du -h "$archive" | awk '{print $1}')" log 'Архив содержит секреты. Передавай его только по защищённому каналу.' } list_archive() { local archive=$1 [[ -f "$archive" ]] || fail "Архив не найден: $archive" if [[ "$archive" == *.zst ]]; then need zstd; zstd -dc "$archive" | tar -tvf - else tar -tzvf "$archive" fi } restore() { local archive=$1 [[ -f "$archive" ]] || fail "Архив не найден: $archive" need tar if [[ "$archive" == *.zst ]]; then need zstd; fi if [[ -e "$HOME/.pi/agent/settings.json" ]]; then log 'Обнаружен существующий ~/.pi/agent. Будет создан backup перед заменой.' mv "$HOME/.pi/agent" "$HOME/.pi/agent.before-restore-$(date +%Y%m%d-%H%M%S)" fi mkdir -p "$HOME" log 'Распаковываю состояние Pi в домашний каталог...' if [[ "$archive" == *.zst ]]; then zstd -dc "$archive" | tar -xpf - -C "$HOME" else tar -xpf "$archive" -C "$HOME" fi chmod 700 "$HOME/.pi" "$HOME/.pi/agent" "$HOME/.agents" 2>/dev/null || true chmod 600 "$HOME/.pi/agent/auth.json" "$HOME/.pi/agent/mcp.json" 2>/dev/null || true if ! command -v pi >/dev/null 2>&1; then need npm log 'Pi не найден; устанавливаю текущую версию через npm...' npm install -g --ignore-scripts @earendil-works/pi-coding-agent fi log "Pi: $(pi --version 2>/dev/null || echo 'не удалось определить версию')" log 'Проверка настроек и установленных packages:' pi list 2>&1 || true log 'Восстановление завершено. Если OAuth не подхватился, запусти Pi и выполни /login.' } case "${1:-}" in backup) backup "${2:-$PWD}" ;; list) [[ $# -ge 2 ]] || { usage; exit 2; }; list_archive "$2" ;; restore) [[ $# -ge 2 ]] || { usage; exit 2; }; restore "$2" ;; *) usage; exit 2 ;; esac