Skip to content

Instantly share code, notes, and snippets.

@brunokiafuka
Last active May 12, 2026 22:05
Show Gist options
  • Select an option

  • Save brunokiafuka/771455b43e12bccc7bbe7d521fcfb8d5 to your computer and use it in GitHub Desktop.

Select an option

Save brunokiafuka/771455b43e12bccc7bbe7d521fcfb8d5 to your computer and use it in GitHub Desktop.
Local dev environment bootstrap for Intercom — splits cmux terminal into 2×2 grid (dev info, embercom, intercom, teammate-app)

dev — local dev environment for cmux

Splits your current cmux terminal into a 2×2 grid and starts all services in one command.

┌─────────────────────┬─────────────┐
│  dev (info pane)    │   embercom  │
│  http://intercom.test│            │
├─────────────────────┼─────────────┤
│  intercom           │  team-app   │
└─────────────────────┴─────────────┘

Requirements

  • cmux as your terminal
  • python3 (macOS built-in)
  • Repos cloned at the paths defined in dev-workspace.yml

Setup

1. Place the config

cp dev-workspace.yml ~/dev-workspace.yml

Edit ~/dev-workspace.yml to match your repo paths and commands.

2. Install the script

curl -fsSL https://gist.githubusercontent.com/brunokiafuka/771455b43e12bccc7bbe7d521fcfb8d5/raw/dev \
  -o ~/.local/bin/dev
chmod +x ~/.local/bin/dev

Make sure ~/.local/bin is on your PATH (add to ~/.zshrc if needed):

export PATH="$HOME/.local/bin:$PATH"

3. Bootstrap repos (first time only)

dev boot

Runs each repo's boot command in dependency order.

Usage

dev start    — split current terminal into 2×2 grid and start all servers
dev stop     — stop servers and close panes
dev restart  — stop + start
dev boot     — bootstrap repos (script/bootstrap) in dependency order

How it works

  • dev-workspace.yml defines repos, their paths, boot/start commands, and optional extras
  • dev start must be run from inside a cmux terminal — it uses $CMUX_WORKSPACE_ID and $CMUX_SURFACE_ID to split the current pane in place
  • Session state (surface refs) is saved to ~/.local/dev-session so dev stop knows what to close
  • Running dev start while a session is active prints an error — use dev restart instead
#!/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
workspace: active-dev
repos:
intercom:
path: ~/src/intercom
boot: script/bootstrap
start: script/server
embercom:
path: ~/src/embercom
depends_on: intercom
boot: script/bootstrap
start: script/server
extra:
- label: teammate-app
cmd: script/server teammate-app
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment