123 lines
4.8 KiB
Bash
Executable File
123 lines
4.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# sync-to-vm.sh — Copy local config to an opencode-devbox VM
|
|
#
|
|
# Reads docker-compose.yml on the remote VM to detect which bind mounts
|
|
# are active, then syncs the corresponding directories from this machine.
|
|
#
|
|
# Usage:
|
|
# ./sync-to-vm.sh <ssh-host>
|
|
#
|
|
# Examples:
|
|
# ./sync-to-vm.sh devbox-affection
|
|
# ./sync-to-vm.sh devbox@129.192.68.184
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Colors ──────────────────────────────────────────────────────────
|
|
BOLD="\033[1m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m"
|
|
info() { echo -e "${BOLD}==>${RESET} $*"; }
|
|
ok() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}${BOLD}!${RESET} $*"; }
|
|
err() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
|
|
# ── Args ────────────────────────────────────────────────────────────
|
|
if [[ $# -lt 1 ]]; then
|
|
err "Usage: $0 <ssh-host>"
|
|
echo " Example: $0 devbox-affection"
|
|
exit 1
|
|
fi
|
|
|
|
SSH_HOST="$1"
|
|
REMOTE_COMPOSE="~/opencode-devbox/docker-compose.yml"
|
|
|
|
# ── SSH multiplexing (reuse one connection for all operations) ──────
|
|
CTRL_SOCKET=$(mktemp -u /tmp/sync-to-vm-XXXXXX)
|
|
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${CTRL_SOCKET} -o ControlPersist=120 -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
|
|
|
|
cleanup() {
|
|
ssh ${SSH_OPTS} -O exit "$SSH_HOST" 2>/dev/null || true
|
|
rm -f "$CTRL_SOCKET"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
ssh_cmd() {
|
|
ssh ${SSH_OPTS} "$SSH_HOST" "$@"
|
|
}
|
|
|
|
# ── Bind mount patterns to detect ──────────────────────────────────
|
|
# Maps: grep pattern → local source → remote destination
|
|
declare -a MOUNT_PATTERNS=(
|
|
"~/.aws:/home/developer/.aws|$HOME/.aws|~/.aws"
|
|
"~/.config/opencode:/home/developer/.config/opencode|$HOME/.config/opencode|~/.config/opencode"
|
|
"~/.config/nvim:/home/developer/.config/nvim|$HOME/.config/nvim|~/.config/nvim"
|
|
"~/.agents/skills:/home/developer/.agents/skills|$HOME/.agents/skills|~/.agents/skills"
|
|
)
|
|
|
|
# ── Establish persistent SSH connection ─────────────────────────────
|
|
info "Connecting to ${SSH_HOST}..."
|
|
if ! ssh_cmd true 2>/dev/null; then
|
|
err "Cannot connect to ${SSH_HOST}"
|
|
exit 1
|
|
fi
|
|
ok "Connected to ${SSH_HOST}"
|
|
|
|
# ── Fetch remote docker-compose.yml ─────────────────────────────────
|
|
info "Reading docker-compose.yml from ${SSH_HOST}..."
|
|
REMOTE_COMPOSE_CONTENT=$(ssh_cmd "cat $REMOTE_COMPOSE 2>/dev/null") || {
|
|
err "Could not read ${REMOTE_COMPOSE} on ${SSH_HOST}"
|
|
err "Has the VM been set up? Run the post-setup steps first."
|
|
exit 1
|
|
}
|
|
|
|
# ── Detect active bind mounts ──────────────────────────────────────
|
|
SYNCED=0
|
|
|
|
for entry in "${MOUNT_PATTERNS[@]}"; do
|
|
IFS='|' read -r pattern local_path remote_path <<< "$entry"
|
|
|
|
# Check if the mount is uncommented (active) in docker-compose.yml
|
|
# Match lines that start with optional whitespace and a dash, NOT preceded by #
|
|
if echo "$REMOTE_COMPOSE_CONTENT" | grep -qE "^\s*-\s+['\"]?${pattern}" 2>/dev/null; then
|
|
# Mount is active — check if local source exists
|
|
if [[ ! -d "$local_path" ]]; then
|
|
warn "Mount active for ${pattern} but ${local_path} does not exist locally — skipping"
|
|
continue
|
|
fi
|
|
|
|
# Check if directory has content
|
|
if [[ -z "$(ls -A "$local_path" 2>/dev/null)" ]]; then
|
|
warn "${local_path} is empty — skipping"
|
|
continue
|
|
fi
|
|
|
|
info "Syncing ${local_path} → ${SSH_HOST}:${remote_path}"
|
|
|
|
# Ensure remote directory exists
|
|
ssh_cmd "mkdir -p ${remote_path}"
|
|
|
|
# Sync with rsync (fall back to scp if rsync unavailable)
|
|
if command -v rsync &>/dev/null; then
|
|
rsync -az --progress \
|
|
--exclude='node_modules' \
|
|
--exclude='__pycache__' \
|
|
--exclude='.venv' \
|
|
--exclude='*.pyc' \
|
|
-e "ssh ${SSH_OPTS}" "${local_path}/" "${SSH_HOST}:${remote_path}/"
|
|
else
|
|
scp -o "ControlPath=${CTRL_SOCKET}" -r "${local_path}/." "${SSH_HOST}:${remote_path}/"
|
|
fi
|
|
|
|
ok "Synced ${local_path}"
|
|
SYNCED=$((SYNCED + 1))
|
|
fi
|
|
done
|
|
|
|
# ── Summary ─────────────────────────────────────────────────────────
|
|
echo ""
|
|
if [[ $SYNCED -eq 0 ]]; then
|
|
warn "No active bind mounts detected in remote docker-compose.yml"
|
|
warn "Uncomment the mounts you need in ${REMOTE_COMPOSE} on the VM, then re-run this script"
|
|
else
|
|
ok "Synced ${SYNCED} director$([ $SYNCED -eq 1 ] && echo 'y' || echo 'ies') to ${SSH_HOST}"
|
|
fi
|