Skip to content

Commit b760259

Browse files
committed
chore(conductor): add conductor.json + .conductor/setup.sh
Adds the same Conductor wiring that customer gitops repos use, in the canonical template. Every NEW customer gitops onboarded going forward inherits this setup automatically — existing customer repos (gitops-mudflap, gitops-roofr, etc.) need a one-shot copy. setup.sh: - resolves CONDUCTOR_ROOT_PATH / WORKSPACE_PATH (with git-worktree fallback for manual invocation) - hardens PATH for non-interactive shells (homebrew, pnpm, asdf shims) - SYMLINKS .env* secrets from root so key rotation propagates - copies .vapi-state*.json (workspaces should diverge state) - lockfile-aware install (pnpm/npm ci/npm install) with rsync --link-dest hardlink shortcut from root's node_modules - non-fatal build verification (catches TS errors at workspace creation without blocking workspaces on pre-existing type noise) conductor.json: - dispatch table at repo root, committed to git so teammates inherit the same scripts. Note: personal scripts in Conductor → Settings → Repository scripts override conductor.json — clear them once for the JSON to take effect.
1 parent 4c4f607 commit b760259

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

.conductor/setup.sh

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

conductor.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"scripts": {
3+
"setup": "bash .conductor/setup.sh"
4+
},
5+
"runScriptMode": "concurrent"
6+
}

0 commit comments

Comments
 (0)