Files
bw-copilot-resources/resources/scripts/sync-copilot-state.sh
T
fragginwagon b43e4c825e Add multi-machine state sync script and extend verify.sh coverage
D - resources/scripts/sync-copilot-state.sh: rsync-based push/pull for
  publish-log.tsv and project-ports-registry.json; dry-run by default;
  reads remote config from env or ~/.copilot-resources-state/sync.conf;
  excludes quarantine dir and machine-local log files

E - install/verify.sh: add checks for session end hook script and all three
  history maintenance scripts (cleanup, restore, purge)
2026-06-30 17:35:38 -04:00

126 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Sync persistent Copilot state files between machines using rsync over SSH.
# Only publish-log.tsv and project-ports-registry.json are synced; machine-
# local files (hook-events.log, session-events.tsv) and the quarantine
# directory are always excluded.
#
# Usage:
# sync-copilot-state.sh --mode push|pull --remote user@host [options]
#
# Configuration:
# Set COPILOT_STATE_SYNC_REMOTE in environment or in
# ~/.copilot-resources-state/sync.conf to avoid passing --remote every time.
set -euo pipefail
state_dir="${COPILOT_RESOURCES_STATE_DIR:-$HOME/.copilot-resources-state}"
sync_conf="$state_dir/sync.conf"
dry_run="true"
mode=""
remote=""
ssh_port="22"
function usage() {
cat >&2 <<EOF
Usage: sync-copilot-state.sh --mode <push|pull> [options]
Required:
--mode <push|pull> push: send local state to remote.
pull: fetch remote state to local.
Optional:
--remote <user@host> Remote SSH target. Overrides COPILOT_STATE_SYNC_REMOTE
and sync.conf.
--remote-path <path> Remote state directory. Default: ~/.copilot-resources-state
--ssh-port <port> SSH port. Default: 22
--execute Apply the sync. Default is dry-run.
--help Show this help text.
Configuration file:
$sync_conf
Supported keys (shell variable syntax):
COPILOT_STATE_SYNC_REMOTE=user@host
COPILOT_STATE_SYNC_REMOTE_PATH=/custom/path
COPILOT_STATE_SYNC_SSH_PORT=22
EOF
}
# Load optional config file before parsing args so args take precedence.
if [[ -f "$sync_conf" ]]; then
# shellcheck source=/dev/null
source "$sync_conf"
fi
remote="${COPILOT_STATE_SYNC_REMOTE:-}"
remote_path="${COPILOT_STATE_SYNC_REMOTE_PATH:-.copilot-resources-state}"
ssh_port="${COPILOT_STATE_SYNC_SSH_PORT:-22}"
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
mode="$2"; shift 2 ;;
--remote)
remote="$2"; shift 2 ;;
--remote-path)
remote_path="$2"; shift 2 ;;
--ssh-port)
ssh_port="$2"; shift 2 ;;
--execute)
dry_run="false"; shift ;;
--help)
usage; exit 0 ;;
*)
printf 'Unknown argument: %s\n' "$1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$mode" ]]; then
printf '--mode push|pull is required.\n' >&2; usage; exit 1
fi
if [[ "$mode" != "push" && "$mode" != "pull" ]]; then
printf '--mode must be push or pull.\n' >&2; exit 1
fi
if [[ -z "$remote" ]]; then
printf 'No remote configured. Pass --remote user@host or set COPILOT_STATE_SYNC_REMOTE.\n' >&2
exit 1
fi
mkdir -p -- "$state_dir"
# Files to sync: only durable shared state; exclude machine-local and ephemeral.
sync_files=(
"publish-log.tsv"
"project-ports-registry.json"
)
rsync_args=(-avz --checksum -e "ssh -p ${ssh_port}")
if [[ "$dry_run" == "true" ]]; then
rsync_args+=(--dry-run)
printf 'Dry-run mode — no changes will be made. Pass --execute to apply.\n\n'
fi
if [[ "$mode" == "push" ]]; then
printf 'Pushing state to %s:%s\n\n' "$remote" "$remote_path"
for file in "${sync_files[@]}"; do
local_file="$state_dir/$file"
if [[ ! -f "$local_file" ]]; then
printf 'SKIP (not found): %s\n' "$local_file"
continue
fi
printf 'SYNC: %s -> %s:%s/%s\n' "$local_file" "$remote" "$remote_path" "$file"
rsync "${rsync_args[@]}" "$local_file" "${remote}:${remote_path}/${file}"
done
else
printf 'Pulling state from %s:%s\n\n' "$remote" "$remote_path"
for file in "${sync_files[@]}"; do
printf 'SYNC: %s:%s/%s -> %s/%s\n' "$remote" "$remote_path" "$file" "$state_dir" "$file"
rsync "${rsync_args[@]}" "${remote}:${remote_path}/${file}" "$state_dir/${file}"
done
fi
printf '\nDone.\n'