|
#!/usr/bin/env bash |
|
# Usage: dev <boot|start|stop|restart> |
|
|
|
set -euo pipefail |
|
|
|
CONFIG="$HOME/dev-workspace.yml" |
|
SESSION="$HOME/.local/dev-session" |
|
_TMP=$(mktemp) && trap 'rm -f "$_TMP"' EXIT |
|
|
|
[[ -f "$CONFIG" ]] || { echo "error: config not found: $CONFIG" >&2; exit 1; } |
|
|
|
# ── parse dev-workspace.yml → JSON (no yq / PyYAML needed) ────────────────── |
|
|
|
python3 - "$CONFIG" "$_TMP" <<'PYEOF' |
|
import sys, json, re |
|
|
|
def parse(src, dst): |
|
text = open(src).read() |
|
out = {} |
|
|
|
m = re.search(r'^workspace:\s+(.+)$', text, re.M) |
|
if m: out['workspace'] = m.group(1).strip() |
|
|
|
out['repos'] = {} |
|
rm = re.search(r'^repos:\n(.+)', text, re.M | re.S) |
|
if not rm: |
|
with open(dst, 'w') as f: json.dump(out, f) |
|
return |
|
|
|
body = rm.group(1) |
|
for repo_m in re.finditer(r'^ (\w[\w-]*):\n', body, re.M): |
|
name = repo_m.group(1) |
|
s = repo_m.end() |
|
nx = re.search(r'^ \w', body[s:], re.M) |
|
block = body[s : s + nx.start() if nx else len(body)] |
|
|
|
repo = {} |
|
for f in re.finditer(r'^ ([a-z_]+):\s+(.+)$', block, re.M): |
|
repo[f.group(1)] = f.group(2).strip() |
|
|
|
em = re.search(r'^ extra:\n((?: .+\n?)*)', block, re.M) |
|
if em: |
|
items, cur = [], {} |
|
for ln in em.group(1).splitlines(): |
|
s2 = ln.strip() |
|
if not s2: continue |
|
if s2.startswith('- '): |
|
if cur: items.append(cur) |
|
cur = {}; s2 = s2[2:] |
|
if ': ' in s2: |
|
k, v = s2.split(': ', 1) |
|
cur[k.strip()] = v.strip() |
|
if cur: items.append(cur) |
|
repo['extra'] = items |
|
|
|
out['repos'][name] = repo |
|
|
|
with open(dst, 'w') as f: |
|
json.dump(out, f) |
|
|
|
parse(sys.argv[1], sys.argv[2]) |
|
PYEOF |
|
|
|
# ── helpers ────────────────────────────────────────────────────────────────── |
|
|
|
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } |
|
step() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; } |
|
ok() { printf ' \033[1;32m✓\033[0m %s\n' "$*"; } |
|
|
|
xp() { echo "${1/#\~/$HOME}"; } # expand leading ~ |
|
|
|
# Evaluate a Python expression against the parsed config |
|
cfg() { |
|
python3 -c " |
|
import json |
|
d = json.load(open('${_TMP}')) |
|
v = ${1} |
|
print(json.dumps(v) if isinstance(v, (dict, list)) else ('' if v is None else str(v))) |
|
" |
|
} |
|
|
|
# Repo names in dependency order (no depends_on → first) |
|
ordered_repos() { |
|
python3 -c " |
|
import json |
|
d = json.load(open('${_TMP}')) |
|
r = d['repos'] |
|
first = [k for k, v in r.items() if 'depends_on' not in v] |
|
rest = [k for k, v in r.items() if 'depends_on' in v] |
|
for name in first + rest: print(name) |
|
" |
|
} |
|
|
|
# Return true if a surface ref still exists in the cmux tree |
|
surf_alive() { |
|
cmux tree --all 2>/dev/null | grep -q "$1" |
|
} |
|
|
|
# ── boot ───────────────────────────────────────────────────────────────────── |
|
# Runs bootstrap sequentially in the current terminal, intercom first. |
|
|
|
cmd_boot() { |
|
step "Bootstrapping repos (dependency order)" |
|
while IFS= read -r repo; do |
|
local path boot_cmd |
|
path=$(xp "$(cfg "d['repos']['${repo}']['path']")") |
|
boot_cmd=$(cfg "d['repos']['${repo}']['boot']") |
|
step "[$repo] $boot_cmd" |
|
(cd "$path" && bash -c "$boot_cmd") || die "[$repo] bootstrap failed" |
|
ok "$repo done" |
|
done < <(ordered_repos) |
|
step "Bootstrap complete." |
|
} |
|
|
|
# ── start ───────────────────────────────────────────────────────────────────── |
|
# Splits the current terminal into a 2×2 grid: |
|
# |
|
# ┌─────────────────────┬─────────────┐ |
|
# │ dev (this pane) │ embercom │ |
|
# │ http://intercom.test│ │ |
|
# ├─────────────────────┼─────────────┤ |
|
# │ intercom │ team-app │ |
|
# └─────────────────────┴─────────────┘ |
|
|
|
cmd_start() { |
|
command -v cmux >/dev/null 2>&1 || die "cmux not found — is the cmux app running?" |
|
[[ -z "${CMUX_WORKSPACE_ID:-}" ]] && die "Not running inside a cmux terminal" |
|
|
|
# Guard against double-start |
|
if [[ -f "$SESSION" ]]; then |
|
local first_surf |
|
first_surf=$(head -1 "$SESSION" | cut -d'|' -f2) |
|
if surf_alive "$first_surf"; then |
|
die "Dev session already running — use 'dev stop' or 'dev restart'" |
|
else |
|
rm -f "$SESSION" # stale session, clean up |
|
fi |
|
fi |
|
|
|
local ws_ref="$CMUX_WORKSPACE_ID" |
|
local info_surf="$CMUX_SURFACE_ID" |
|
|
|
# Build flat list of "name|path|cmd" (repos + extras, ordered) |
|
local -a entries=() |
|
while IFS= read -r repo; do |
|
local path start_cmd n |
|
path=$(xp "$(cfg "d['repos']['${repo}']['path']")") |
|
start_cmd=$(cfg "d['repos']['${repo}']['start']") |
|
entries+=("${repo}|${path}|${start_cmd}") |
|
|
|
n=$(cfg "len(d['repos']['${repo}'].get('extra', []))") |
|
for ((i = 0; i < n; i++)); do |
|
local label extra_cmd |
|
label=$(cfg "d['repos']['${repo}']['extra'][${i}]['label']") |
|
extra_cmd=$(cfg "d['repos']['${repo}']['extra'][${i}]['cmd']") |
|
entries+=("${label}|${path}|${extra_cmd}") |
|
done |
|
done < <(ordered_repos) |
|
|
|
# Parse entries into named slots (intercom=0, embercom=1, teammate-app=2) |
|
IFS='|' read -r _ ic_path ic_cmd <<< "${entries[0]}" |
|
IFS='|' read -r _ em_path em_cmd <<< "${entries[1]:-||}" |
|
IFS='|' read -r _ ta_path ta_cmd <<< "${entries[2]:-||}" |
|
|
|
# Split the current pane into a 2×2 grid: |
|
# right from info → embercom (top-right) |
|
# down from info → intercom (bottom-left) |
|
# down from ember → team-app (bottom-right) |
|
local em_surf ic_surf ta_surf |
|
em_surf=$(cmux new-split right --workspace "$ws_ref" --surface "$info_surf" --focus false 2>/dev/null | grep -oE 'surface:[0-9]+') || true |
|
ic_surf=$(cmux new-split down --workspace "$ws_ref" --surface "$info_surf" --focus false 2>/dev/null | grep -oE 'surface:[0-9]+') || true |
|
ta_surf=$(cmux new-split down --workspace "$ws_ref" --surface "$em_surf" --focus false 2>/dev/null | grep -oE 'surface:[0-9]+') || true |
|
|
|
# Name and launch each pane |
|
cmux rename-tab --workspace "$ws_ref" --surface "$info_surf" "dev" &>/dev/null || true |
|
for slot in "intercom|$ic_path|$ic_cmd|$ic_surf" "embercom|$em_path|$em_cmd|$em_surf" "teammate-app|$ta_path|$ta_cmd|$ta_surf"; do |
|
IFS='|' read -r name path cmd surf <<< "$slot" |
|
if [[ -n "$surf" && -n "$cmd" ]]; then |
|
cmux rename-tab --workspace "$ws_ref" --surface "$surf" "$name" &>/dev/null || true |
|
cmux send --workspace "$ws_ref" --surface "$surf" "cd $path && $cmd" &>/dev/null || true |
|
cmux send-key --workspace "$ws_ref" --surface "$surf" "Enter" &>/dev/null || true |
|
ok "[$name] → $cmd" |
|
fi |
|
done |
|
|
|
# Save session: name|surface per line for friendly stop output |
|
printf '%s\n' "intercom|$ic_surf" "embercom|$em_surf" "teammate-app|$ta_surf" > "$SESSION" |
|
|
|
# Reference card |
|
local user="${INTERCOM_USER:-$(whoami)}" |
|
printf '\n' |
|
step "Dev environment ready" |
|
printf '\n' |
|
printf ' \033[1;36m%-12s\033[0m %s\n' "URL" "http://intercom.test" |
|
printf ' \033[1;36m%-12s\033[0m %s\n' "Email" "${user}+${user}@intercom.io" |
|
printf ' \033[1;36m%-12s\033[0m %s\n' "Password" "aaaaaa" |
|
printf '\n' |
|
} |
|
|
|
# ── stop ───────────────────────────────────────────────────────────────────── |
|
|
|
cmd_stop() { |
|
command -v cmux >/dev/null 2>&1 || die "cmux not found" |
|
[[ -f "$SESSION" ]] || { ok "No dev session found."; return; } |
|
|
|
step "Stopping dev servers" |
|
local ws_ref="${CMUX_WORKSPACE_ID:-}" |
|
while IFS='|' read -r name surf; do |
|
[[ -z "$surf" ]] && continue |
|
cmux close-surface --surface "$surf" ${ws_ref:+--workspace "$ws_ref"} &>/dev/null \ |
|
&& ok "Stopped: $name" || true |
|
done < "$SESSION" |
|
|
|
# Restore info pane name |
|
local info_surf="${CMUX_SURFACE_ID:-}" |
|
[[ -n "$info_surf" && -n "$ws_ref" ]] && \ |
|
cmux rename-tab --workspace "$ws_ref" --surface "$info_surf" "terminal" &>/dev/null || true |
|
|
|
rm -f "$SESSION" |
|
} |
|
|
|
# ── restart ─────────────────────────────────────────────────────────────────── |
|
|
|
cmd_restart() { |
|
cmd_stop |
|
cmd_start |
|
} |
|
|
|
# ── main ───────────────────────────────────────────────────────────────────── |
|
|
|
case "${1:-}" in |
|
boot) cmd_boot ;; |
|
start) cmd_start ;; |
|
stop) cmd_stop ;; |
|
restart) cmd_restart ;; |
|
*) |
|
printf 'Usage: %s <boot|start|stop|restart>\n\n' "$(basename "$0")" |
|
printf ' boot Bootstrap repos in dependency order (intercom → embercom)\n' |
|
printf ' start Split current terminal into 2×2 dev grid\n' |
|
printf ' stop Close server panes and clear session\n' |
|
printf ' restart stop + start\n' |
|
exit 1 |
|
;; |
|
esac |