3669bec8ff
Replace 'rsync -az' with 'rsync -rlptDz' (archive minus owner/group preservation). Running as a non-root user on the VM, rsync can't preserve UID anyway, but it was successfully preserving GID whenever the numeric GID happened to exist on the target. That caused synced dirs (~/.aws, ~/.config/opencode, ~/.config/nvim, ~/.agents/skills, ~/.ssh) to end up with group 1001 on the VM, which was confusing and, for group-writable mode, potentially insecure. With -o and -g dropped, received files get the receiving user's UID:GID (devbox:devbox), which is what you want.
147 lines
6.1 KiB
Bash
Executable File
147 lines
6.1 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.
|
|
# Use -rlptD (archive minus -o -g) so ownership on the remote is set
|
|
# by the receiving user (devbox). Preserving host UID/GID with -a
|
|
# tagged files with the pusher's numeric GID, which leaked through
|
|
# whenever the VM happened to have a matching group (see #group-1001).
|
|
if command -v rsync &>/dev/null; then
|
|
rsync -rlptDz --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
|