#!/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 < [options] Required: --mode push: send local state to remote. pull: fetch remote state to local. Optional: --remote Remote SSH target. Overrides COPILOT_STATE_SYNC_REMOTE and sync.conf. --remote-path Remote state directory. Default: ~/.copilot-resources-state --ssh-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'