diff --git a/install/verify.sh b/install/verify.sh index 3737142..dfdadb5 100755 --- a/install/verify.sh +++ b/install/verify.sh @@ -75,7 +75,11 @@ main() { local local_mcp_overrides_file="$canonical_home/.local/mcp.local.jsonc" local session_start_hook_file="$canonical_home/resources/hooks/session-audit.json" local session_start_hook_script="$canonical_home/resources/scripts/report-hook-event.sh" + local session_end_hook_script="$canonical_home/resources/scripts/report-hook-event-end.sh" local port_registry_script="$canonical_home/resources/scripts/update-port-registry.mjs" + local cleanup_history_script="$canonical_home/resources/scripts/cleanup-copilot-history.sh" + local restore_history_script="$canonical_home/resources/scripts/restore-copilot-history.sh" + local purge_history_script="$canonical_home/resources/scripts/purge-copilot-history-quarantine.sh" check_path "repo root" "$repo_root" check_path "canonical home" "$canonical_home" @@ -86,8 +90,16 @@ main() { check_path "session start hook" "$session_start_hook_file" check_path "session start hook script" "$session_start_hook_script" check_readable_file "session start hook script" "$session_start_hook_script" + check_path "session end hook script" "$session_end_hook_script" + check_readable_file "session end hook script" "$session_end_hook_script" check_path "port registry updater script" "$port_registry_script" check_readable_file "port registry updater script" "$port_registry_script" + check_path "history cleanup script" "$cleanup_history_script" + check_readable_file "history cleanup script" "$cleanup_history_script" + check_path "history restore script" "$restore_history_script" + check_readable_file "history restore script" "$restore_history_script" + check_path "history purge script" "$purge_history_script" + check_readable_file "history purge script" "$purge_history_script" check_command "session start hook shell" "bash" check_command "port registry updater runtime" "node" check_path "prompts link" "$vscode_user_dir/prompts" diff --git a/resources/scripts/sync-copilot-state.sh b/resources/scripts/sync-copilot-state.sh new file mode 100755 index 0000000..d98068c --- /dev/null +++ b/resources/scripts/sync-copilot-state.sh @@ -0,0 +1,125 @@ +#!/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'