|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Conductor setup script for gitops-mudflap. |
| 3 | +# |
| 4 | +# Wire-up: this script is dispatched from `conductor.json` at the repo root: |
| 5 | +# {"scripts": {"setup": "bash .conductor/setup.sh"}} |
| 6 | +# Note: personal scripts in Conductor → Settings → Repository scripts override |
| 7 | +# conductor.json. Clear them once for the JSON to take effect. |
| 8 | +# |
| 9 | +# Manual run (existing worktree that predates this script): |
| 10 | +# cd <worktree> |
| 11 | +# bash "$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')/.conductor/setup.sh" |
| 12 | +# |
| 13 | +# Conductor env (non-interactive shell — .zshrc is NOT sourced): |
| 14 | +# CONDUCTOR_ROOT_PATH source checkout (the user's primary tree) |
| 15 | +# CONDUCTOR_WORKSPACE_PATH the new worktree (cwd of this script) |
| 16 | +# CONDUCTOR_WORKSPACE_NAME workspace slug |
| 17 | +# CONDUCTOR_PORT base port (10 allocated: +0 through +9) |
| 18 | + |
| 19 | +set -euo pipefail |
| 20 | + |
| 21 | +log() { printf "\n==> %s\n" "$*"; } |
| 22 | +warn() { printf "WARN: %s\n" "$*" >&2; } |
| 23 | + |
| 24 | +# --- 1. Resolve ROOT and WORK ----------------------------------------------- |
| 25 | +WORK="${CONDUCTOR_WORKSPACE_PATH:-$PWD}" |
| 26 | +ROOT="${CONDUCTOR_ROOT_PATH:-}" |
| 27 | + |
| 28 | +if [[ -z "$ROOT" ]]; then |
| 29 | + if command -v git >/dev/null 2>&1 && git -C "$WORK" rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
| 30 | + ROOT="$(git -C "$WORK" worktree list --porcelain | awk '/^worktree /{print $2; exit}')" |
| 31 | + fi |
| 32 | +fi |
| 33 | + |
| 34 | +if [[ -z "$ROOT" || ! -d "$ROOT" ]]; then |
| 35 | + warn "could not resolve source checkout (CONDUCTOR_ROOT_PATH unset and not in a git worktree); aborting" |
| 36 | + exit 1 |
| 37 | +fi |
| 38 | + |
| 39 | +if [[ "$ROOT" == "$WORK" ]]; then |
| 40 | + log "root and workspace are identical; nothing to do" |
| 41 | + exit 0 |
| 42 | +fi |
| 43 | + |
| 44 | +cd "$WORK" |
| 45 | +REPO_NAME="$(basename "$WORK")" |
| 46 | + |
| 47 | +if [[ "$(basename "$ROOT")" != gitops* ]]; then |
| 48 | + warn "Source repo '$(basename "$ROOT")' does not start with 'gitops'; continuing anyway." |
| 49 | +fi |
| 50 | + |
| 51 | +# --- 2. PATH hardening (non-interactive shell) ------------------------------ |
| 52 | +# Conductor scripts run without .zshrc, so PATH-mutating tools (asdf, pnpm, |
| 53 | +# deno, mise) need explicit re-export. Mirror the user's interactive PATH. |
| 54 | +export PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/Library/pnpm:${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$HOME/.local/bin:$HOME/.deno/bin:$PATH" |
| 55 | +command -v mise >/dev/null 2>&1 && eval "$(mise activate bash 2>/dev/null || true)" |
| 56 | + |
| 57 | +log "Setting up Conductor workspace for $REPO_NAME" |
| 58 | +log " root=$ROOT" |
| 59 | +log " work=$WORK" |
| 60 | + |
| 61 | +# --- Helpers ---------------------------------------------------------------- |
| 62 | +# Symlink secrets so a key rotation in root immediately propagates to every |
| 63 | +# worktree. Idempotent — skip if target exists or is already a symlink. |
| 64 | +link_secret_if_present() { |
| 65 | + local rel="$1" |
| 66 | + local src="$ROOT/$rel" |
| 67 | + local dst="$WORK/$rel" |
| 68 | + [[ -f "$src" ]] || return 0 |
| 69 | + [[ -e "$dst" || -L "$dst" ]] && return 0 |
| 70 | + mkdir -p "$(dirname "$dst")" |
| 71 | + ln -s "$src" "$dst" |
| 72 | + echo " · linked $rel → $src" |
| 73 | +} |
| 74 | + |
| 75 | +# Copy state files (workspaces should diverge state without mutating root). |
| 76 | +# chmod 600 to keep secrets-in-state out of group-readable mode. |
| 77 | +copy_if_present() { |
| 78 | + local rel="$1" |
| 79 | + local src="$ROOT/$rel" |
| 80 | + local dst="$WORK/$rel" |
| 81 | + [[ -f "$src" ]] || return 0 |
| 82 | + [[ -e "$dst" || -L "$dst" ]] && return 0 |
| 83 | + mkdir -p "$(dirname "$dst")" |
| 84 | + cp "$src" "$dst" |
| 85 | + chmod 600 "$dst" 2>/dev/null || true |
| 86 | + echo " · copied $rel" |
| 87 | +} |
| 88 | + |
| 89 | +# --- 3. Symlink env / secret files ------------------------------------------ |
| 90 | +log "Linking org environment files from root checkout" |
| 91 | +shopt -s nullglob |
| 92 | +for env_file in "$ROOT"/.env "$ROOT"/.env.*; do |
| 93 | + [[ -e "$env_file" ]] || continue |
| 94 | + env_name="$(basename "$env_file")" |
| 95 | + [[ "$env_name" == ".env.example" ]] && continue |
| 96 | + link_secret_if_present "$env_name" |
| 97 | +done |
| 98 | +shopt -u nullglob |
| 99 | + |
| 100 | +# --- 4. Copy state files ---------------------------------------------------- |
| 101 | +# .vapi-state.<org>.json is tracked in git (worktrees inherit it automatically), |
| 102 | +# but copy any untracked variants too. `npm run pull` rewrites these locally — |
| 103 | +# copying lets a worktree experiment without touching root. |
| 104 | +log "Copying Vapi GitOps state files from root checkout" |
| 105 | +shopt -s nullglob |
| 106 | +for state_file in "$ROOT"/.vapi-state*.json; do |
| 107 | + [[ -e "$state_file" ]] || continue |
| 108 | + copy_if_present "$(basename "$state_file")" |
| 109 | +done |
| 110 | +shopt -u nullglob |
| 111 | + |
| 112 | +# --- 5. Optional local ignore / notes files --------------------------------- |
| 113 | +log "Copying optional local files" |
| 114 | +for f in \ |
| 115 | + .vapi-ignore \ |
| 116 | + .vapi-ignore.local \ |
| 117 | + resources/.vapi-ignore \ |
| 118 | + 'requested improvements.md' |
| 119 | +do |
| 120 | + copy_if_present "$f" |
| 121 | +done |
| 122 | + |
| 123 | +# --- 6. Dependencies (lockfile-aware) --------------------------------------- |
| 124 | +install_dependencies() { |
| 125 | + if [[ ! -f package.json ]]; then |
| 126 | + warn "No package.json found; skipping dependency install." |
| 127 | + return 0 |
| 128 | + fi |
| 129 | + |
| 130 | + if [[ -f pnpm-lock.yaml ]]; then |
| 131 | + if ! command -v pnpm >/dev/null 2>&1; then |
| 132 | + if command -v corepack >/dev/null 2>&1; then |
| 133 | + corepack enable |
| 134 | + corepack prepare pnpm@latest --activate |
| 135 | + else |
| 136 | + npm install -g pnpm |
| 137 | + fi |
| 138 | + fi |
| 139 | + pnpm install --frozen-lockfile |
| 140 | + return 0 |
| 141 | + fi |
| 142 | + |
| 143 | + if [[ -f package-lock.json ]]; then |
| 144 | + npm ci --no-audit --no-fund |
| 145 | + return 0 |
| 146 | + fi |
| 147 | + |
| 148 | + npm install --no-audit --no-fund |
| 149 | +} |
| 150 | + |
| 151 | +# Hardlink first if root has node_modules and workspace doesn't — saves the |
| 152 | +# install entirely on a fast SSD; rsync falls back to a real copy for any |
| 153 | +# file that differs (e.g. native binaries that vary by arch). |
| 154 | +if [[ ! -d "$WORK/node_modules" || -z "$(ls -A "$WORK/node_modules" 2>/dev/null)" ]]; then |
| 155 | + if [[ -d "$ROOT/node_modules" && -n "$(ls -A "$ROOT/node_modules" 2>/dev/null)" ]]; then |
| 156 | + log "Hardlinking node_modules from root (instant)" |
| 157 | + mkdir -p "$WORK/node_modules" |
| 158 | + rsync -a --link-dest="$ROOT/node_modules/" "$ROOT/node_modules/" "$WORK/node_modules/" |
| 159 | + else |
| 160 | + log "Installing dependencies" |
| 161 | + install_dependencies |
| 162 | + fi |
| 163 | +else |
| 164 | + log "node_modules already populated, skipping install" |
| 165 | +fi |
| 166 | + |
| 167 | +# --- 7. Build verification (non-fatal) -------------------------------------- |
| 168 | +# Surfaces TypeScript errors at workspace creation rather than at the first |
| 169 | +# `npm run push`. Runs `tsc --noEmit` via the repo's "build" script. |
| 170 | +# Non-fatal: most operational commands (pull/push/call) use tsx and do not |
| 171 | +# require a clean `tsc --noEmit`, so a TS error shouldn't gate the workspace. |
| 172 | +log "Verifying TypeScript build when available" |
| 173 | +if [[ -f package.json ]] && node -e "const s=require('./package.json').scripts||{};process.exit(s.build?0:1)" 2>/dev/null; then |
| 174 | + if npm run build; then |
| 175 | + echo " · build verification passed" |
| 176 | + else |
| 177 | + warn "build verification FAILED — workspace is still usable for npm run pull/push/call," |
| 178 | + warn "but \`npm run build\` has TypeScript errors. Investigate before merging." |
| 179 | + fi |
| 180 | +else |
| 181 | + echo "No build script found; skipping." |
| 182 | +fi |
| 183 | + |
| 184 | +# --- 8. Done ---------------------------------------------------------------- |
| 185 | +log "Setup complete" |
| 186 | +cat <<'INFO' |
| 187 | +Common commands in this workspace: |
| 188 | + npm run pull # sync state from Vapi (writes .vapi-state.<org>.json) |
| 189 | + npm run push # push local resources to Vapi |
| 190 | + npm run call # interactive test call against an assistant or squad |
| 191 | + npm test # repo tests |
| 192 | +INFO |
0 commit comments