#!/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 # # 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 " echo " Example: $0 devbox-affection" exit 1 fi SSH_HOST="$1" REMOTE_COMPOSE="~/opencode-devbox/docker-compose.yml" # ── 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" ) # ── Verify SSH connectivity ───────────────────────────────────────── info "Checking SSH connectivity to ${SSH_HOST}..." if ! ssh -o ConnectTimeout=5 "$SSH_HOST" 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 "$SSH_HOST" "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 "$SSH_HOST" "mkdir -p ${remote_path}" # Sync with rsync (fall back to scp if rsync unavailable) if command -v rsync &>/dev/null; then rsync -az --info=progress2 "${local_path}/" "${SSH_HOST}:${remote_path}/" else scp -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