From 3c7df3f888abcbd1ef1b8366848caf24f47446e9 Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Sun, 19 Apr 2026 19:25:18 +0200 Subject: [PATCH] Add sync-to-vm.sh to copy local config directories to remote VM --- deploy/README.md | 11 +++++ deploy/sync-to-vm.sh | 103 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100755 deploy/sync-to-vm.sh diff --git a/deploy/README.md b/deploy/README.md index 50847e3..32cdc5e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -7,6 +7,7 @@ Scripts for setting up a fresh Linux VM to host opencode-devbox. - **`cloud-init.yml`** — cloud-init user-data template for automated VM provisioning on OpenStack, Proxmox, or any cloud with cloud-init support - **`setup-host.sh`** — interactive post-install script for VMs that weren't provisioned with cloud-init - **`setup-openstack-secgroup.sh`** — creates an OpenStack security group with the right rules (SSH, mosh, ICMP) +- **`sync-to-vm.sh`** — syncs local config directories (`~/.aws`, `~/.config/opencode`, etc.) to a remote VM based on which bind mounts are active in its `docker-compose.yml` ## Supported distributions @@ -186,3 +187,13 @@ docker compose exec -u developer devbox opencode ``` > **AWS Bedrock users:** Uncomment the `~/.aws` volume mount in `docker-compose.yml` before starting. You'll also need to copy your `~/.aws/config` from a machine where SSO is already configured, then authenticate inside the container with `aws sso login`. + +### Syncing local config to the VM + +After editing `docker-compose.yml` on the VM to uncomment the bind mounts you need, run `sync-to-vm.sh` from your local machine to copy the corresponding directories: + +```bash +./deploy/sync-to-vm.sh devbox-affection +``` + +The script reads `docker-compose.yml` on the remote VM, detects which bind mounts are active, and syncs only those directories from your local machine. It also creates the remote directories if they don't exist. diff --git a/deploy/sync-to-vm.sh b/deploy/sync-to-vm.sh new file mode 100755 index 0000000..0234363 --- /dev/null +++ b/deploy/sync-to-vm.sh @@ -0,0 +1,103 @@ +#!/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