Files
opencode-devbox/deploy/sync-to-vm.sh
T

143 lines
5.9 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
}
# ── Ensure workspace directory exists on remote ─────────────────────
REMOTE_ENV="~/opencode-devbox/.env"
WORKSPACE_PATH=$(ssh_cmd "grep -E '^\s*WORKSPACE_PATH=' $REMOTE_ENV 2>/dev/null | cut -d= -f2- | tr -d '\"'" 2>/dev/null || true)
if [[ -n "$WORKSPACE_PATH" ]]; then
info "Ensuring WORKSPACE_PATH (${WORKSPACE_PATH}) exists on ${SSH_HOST}..."
ssh_cmd "mkdir -p ${WORKSPACE_PATH}"
ok "Workspace directory ready"
else
# Default from docker-compose.yml is ~/projects or current dir
WORKSPACE_PATH=$(echo "$REMOTE_COMPOSE_CONTENT" | grep -oP 'WORKSPACE_PATH:-[^}]+' | sed 's/WORKSPACE_PATH:-//' || true)
if [[ -n "$WORKSPACE_PATH" && "$WORKSPACE_PATH" != "." ]]; then
info "Ensuring default WORKSPACE_PATH (${WORKSPACE_PATH}) exists on ${SSH_HOST}..."
ssh_cmd "mkdir -p ${WORKSPACE_PATH}"
ok "Workspace directory ready"
fi
fi
# ── 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)
# Exclude generated/cached content that gets recreated on the remote
if command -v rsync &>/dev/null; then
rsync -az --progress \
--exclude='node_modules' \
--exclude='__pycache__' \
--exclude='.venv' \
--exclude='*.pyc' \
--exclude='cli/cache' \
--exclude='sso/cache' \
-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