chore: back up Pi environment
This commit is contained in:
215
pi-env-backup.sh
Executable file
215
pi-env-backup.sh
Executable file
@@ -0,0 +1,215 @@
|
||||
#!/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 <archive>
|
||||
# ./pi-env-backup.sh restore <archive>
|
||||
|
||||
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 <pi-backup-*.tar.zst|*.tar.gz>
|
||||
|
||||
Восстановить на ноутбуке/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
|
||||
Reference in New Issue
Block a user