cmux/scripts/reload.sh
Lawrence Chen 84af32c56e
Add cmux omo command for oh-my-openagent integration (#2087)
* Add `cmux omo` command for OpenCode + oh-my-openagent integration

Same pattern as `cmux claude-teams`: creates a tmux shim so
oh-my-openagent's TmuxSessionManager spawns agents as native cmux
splits instead of tmux panes. Sets TMUX/TMUX_PANE env vars, prepends
shim to PATH, and execs into opencode.

Closes https://github.com/manaflow-ai/cmux/issues/2085

* Auto-install oh-my-opencode plugin when running cmux omo

Before launching opencode, cmux omo now:
- Checks if oh-my-opencode is registered in ~/.config/opencode/opencode.json
- If not, creates/updates the config with the plugin entry
- Checks if the npm package is installed in node_modules
- If not, runs bun add (or npm install) to install it
- Then proceeds with tmux shim setup and exec

* Use shadow config dir to avoid modifying user's opencode setup

Instead of writing directly to ~/.config/opencode/opencode.json,
cmux omo now creates a shadow config at ~/.cmuxterm/omo-config/ that
layers oh-my-opencode on top of the user's existing config. Symlinks
node_modules, package.json, bun.lock, and plugin config from the
original dir. Sets OPENCODE_CONFIG_DIR to the shadow directory.

Running plain `opencode` remains unaffected.

* Add Agent Integrations docs section with Claude Code Teams and oh-my-opencode pages

Adds sectioned sidebar navigation to the docs site. The new Agent
Integrations section contains separate pages for cmux claude-teams and
cmux omo, documenting usage, tmux shim mechanics, directory layout,
environment variables, and the shadow config approach. Both pages
include a nightly-only warning. Full English and Japanese translations,
nav item keys added to all 19 locales.

* Remove uppercase from sidebar section headers

* Add more spacing above and below sidebar section headers

* Enable tmux mode in oh-my-opencode config, improve docs

- cmux omo now writes tmux.enabled=true to the shadow oh-my-opencode.json
  config. Without this, oh-my-openagent's TmuxSessionManager won't spawn
  visual panes even though $TMUX is set (the config defaults to false).
- Nightly warnings now link to /nightly instead of generic text.
- Added "What you get" section to oh-my-opencode docs explaining the
  visual pane behavior (auto-layout, idle cleanup, queueing).
- Added tmux.enabled step to first-run and how-it-works sections.

* Add terminal-notifier shim to route oh-my-openagent notifications to cmux

oh-my-openagent sends macOS notifications via terminal-notifier
(args: -title <t> -message <m> [-activate <id>]). The shim in
~/.cmuxterm/omo-bin/terminal-notifier intercepts these calls and
routes them through cmux notify, so notifications appear in cmux's
sidebar panel instead of as raw macOS notifications.

* Add pane geometry to tmux-compat for oh-my-openagent grid planning

oh-my-openagent's TmuxSessionManager needs pane geometry (columns,
rows, position, window dimensions) to decide where to spawn agent
panes. Without this data, agents run headlessly.

Server side:
- pane.list v2 response now includes pixel_frame, cell_size, columns,
  rows per pane, plus container_frame at the top level
- Uses BonsplitController.layoutSnapshot() for pixel geometry and
  ghostty_surface_size() for terminal grid dimensions

CLI side:
- tmuxEnrichContextWithGeometry() computes character-cell positions
  from pixel frames and cell dimensions for tmux format variables
  (pane_width, pane_height, pane_left, pane_top, pane_active,
  window_width, window_height)
- list-panes now resolves pane targets (%uuid) via tmuxResolvePaneTarget
  instead of failing with "Workspace not found"
- display-message enriched with geometry for format strings like
  #{pane_width},#{window_width}
- tmux -V now returns "tmux 3.4" (needed by oh-my-openagent's
  tmux-path-resolver verification)

* Add socket tests for tmux-compat pane geometry

6 tests verifying the geometry enrichment works end-to-end:
- pane.list returns pixel_frame, columns, rows, cell_size, container_frame
- tmux -V returns version string
- list-panes -F renders geometry format variables as integers
- list-panes -t %<uuid> resolves pane targets
- display -p renders pane_width and window_width
- After split, two panes have different positions and halved widths

All 6 pass on macmini (cmux-macmini).

* Handle tmux -V in shim script directly (no socket needed)

oh-my-openagent's tmux-path-resolver runs tmux -V to verify the binary
works. The __tmux-compat handler requires a socket connection, which
may not be established at verification time. Handle -V in the bash
shim directly to avoid the socket dependency.

* Lower default tmux pane min widths for cmux omo

oh-my-openagent defaults: main_pane_min_width=120, agent_pane_min_width=40,
requiring 161+ columns. Most terminal windows are narrower, causing
decideSpawnActions to return canSpawn=false and defer agents forever.

cmux omo now sets: main_pane_min_width=60, agent_pane_min_width=30,
main_pane_size=50, requiring only 91 columns. Also moved tmux -V
handling into the bash shim to avoid needing a socket connection for
the version check.

* Resolve merge conflicts with main (main-vertical layout, focus param)

- Keep upstream main-vertical layout anchoring from #2119
- Keep upstream focus param (v2Bool) instead of no_focus
- Combine with our -d flag handling: -d sets focus=false
- Include customCommands nav item from main

* Implement select-layout equalize and resize-pane absolute width

When oh-my-openagent spawns agent panes, it calls select-layout
main-vertical after each split to redistribute panes evenly, then
resize-pane -x <columns> to set the main pane width. Both were
previously no-ops, causing cascading uneven splits.

Server side:
- Add workspace.equalize_splits v2 API that calls the existing
  TabManager.equalizeSplits (sets all dividers to 0.5)

CLI side:
- select-layout now calls workspace.equalize_splits before tracking
  main-vertical state
- resize-pane -x <columns> without directional flags now computes
  the pixel delta from current to desired width and resizes accordingly

* Fix equalize to use proportional divider positions

The previous equalize set all dividers to 0.5, which in a right-
recursive binary tree (from successive splits) gives 50/25/12.5/6.25%
instead of equal sizes.

New algorithm counts leaf panes on each side of each split and sets
the divider to N_left / (N_left + N_right). For 5 panes in a chain:
1/5, 1/4, 1/3, 1/2, giving each pane exactly 20%.

* Fix select-layout main-vertical to only equalize vertical splits

The proportional equalize was treating the top-level horizontal split
(main vs agent column) the same as vertical splits, setting the main
pane to 1/6 of the window with 5 agents.

For main-vertical layout, only equalize vertical splits (the agent
column), leaving the horizontal main/agent divider untouched. The
subsequent resize-pane -x handles the main pane width.

workspace.equalize_splits now accepts an optional orientation filter
("vertical" or "horizontal") to scope which splits get equalized.

* Re-equalize agent column after kill-pane

* Address PR review comments

- Fix cmux omo --help: remove omo from the help-bypass guard so
  --help shows usage text instead of trying to launch opencode
- Don't overwrite unreadable opencode.json: fail with an error
  instead of silently resetting to empty config
- Drain installer pipes concurrently before waitUntilExit to
  prevent deadlock from full pipe buffers during bun/npm install

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
2026-03-26 16:07:59 -07:00

544 lines
18 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
APP_NAME="cmux DEV"
BUNDLE_ID="com.cmuxterm.app.debug"
BASE_APP_NAME="cmux DEV"
DERIVED_DATA=""
NAME_SET=0
BUNDLE_SET=0
DERIVED_SET=0
TAG=""
LAUNCH=0
CMUX_DEBUG_LOG=""
CLI_PATH=""
LAST_SOCKET_PATH_DIR="$HOME/Library/Application Support/cmux"
LAST_SOCKET_PATH_FILE="${LAST_SOCKET_PATH_DIR}/last-socket-path"
write_dev_cli_shim() {
local target="$1"
local fallback_bin="$2"
mkdir -p "$(dirname "$target")"
cat > "$target" <<EOF
#!/usr/bin/env bash
# cmux dev shim (managed by scripts/reload.sh)
set -euo pipefail
CLI_PATH_FILE="/tmp/cmux-last-cli-path"
CLI_PATH_OWNER="\$(stat -f '%u' "\$CLI_PATH_FILE" 2>/dev/null || stat -c '%u' "\$CLI_PATH_FILE" 2>/dev/null || echo -1)"
if [[ -r "\$CLI_PATH_FILE" ]] && [[ ! -L "\$CLI_PATH_FILE" ]] && [[ "\$CLI_PATH_OWNER" == "\$(id -u)" ]]; then
CLI_PATH="\$(cat "\$CLI_PATH_FILE")"
if [[ -x "\$CLI_PATH" ]]; then
exec "\$CLI_PATH" "\$@"
fi
fi
if [[ -x "$fallback_bin" ]]; then
exec "$fallback_bin" "\$@"
fi
echo "error: no reload-selected dev cmux CLI found. Run ./scripts/reload.sh --tag <name> first." >&2
exit 1
EOF
chmod +x "$target"
}
select_cmux_shim_target() {
local app_cli_dir="/Applications/cmux.app/Contents/Resources/bin"
local marker="cmux dev shim (managed by scripts/reload.sh)"
local target=""
local path_entry=""
local candidate=""
IFS=':' read -r -a path_entries <<< "${PATH:-}"
for path_entry in "${path_entries[@]}"; do
[[ -z "$path_entry" ]] && continue
if [[ "$path_entry" == "~/"* ]]; then
path_entry="$HOME/${path_entry#~/}"
fi
if [[ "$path_entry" == "$app_cli_dir" ]]; then
break
fi
[[ -d "$path_entry" && -w "$path_entry" ]] || continue
candidate="$path_entry/cmux"
if [[ ! -e "$candidate" ]]; then
target="$candidate"
break
fi
if [[ -f "$candidate" ]] && grep -q "$marker" "$candidate" 2>/dev/null; then
target="$candidate"
break
fi
done
if [[ -n "$target" ]]; then
echo "$target"
return 0
fi
# Fallback for PATH layouts where app CLI isn't listed or no earlier entries were writable.
for path_entry in /opt/homebrew/bin /usr/local/bin "$HOME/.local/bin" "$HOME/bin"; do
[[ -d "$path_entry" && -w "$path_entry" ]] || continue
candidate="$path_entry/cmux"
if [[ ! -e "$candidate" ]]; then
echo "$candidate"
return 0
fi
if [[ -f "$candidate" ]] && grep -q "$marker" "$candidate" 2>/dev/null; then
echo "$candidate"
return 0
fi
done
return 1
}
write_last_socket_path() {
local socket_path="$1"
mkdir -p "$LAST_SOCKET_PATH_DIR"
echo "$socket_path" > "$LAST_SOCKET_PATH_FILE" || true
echo "$socket_path" > /tmp/cmux-last-socket-path || true
}
usage() {
cat <<'EOF'
Usage: ./scripts/reload.sh --tag <name> [options]
Options:
--tag <name> Required. Short tag for parallel builds (e.g., feature-xyz-lol).
Sets app name, bundle id, and derived data path unless overridden.
--launch Launch the app after building. Without this flag, the script
builds and prints the app path but does not open it.
--name <app name> Override app display/bundle name.
--bundle-id <id> Override bundle identifier.
--derived-data <path> Override derived data path.
-h, --help Show this help.
EOF
}
sanitize_bundle() {
local raw="$1"
local cleaned
cleaned="$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/./g; s/^\\.+//; s/\\.+$//; s/\\.+/./g')"
if [[ -z "$cleaned" ]]; then
cleaned="agent"
fi
echo "$cleaned"
}
sanitize_path() {
local raw="$1"
local cleaned
cleaned="$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
if [[ -z "$cleaned" ]]; then
cleaned="agent"
fi
echo "$cleaned"
}
tagged_derived_data_path() {
local slug="$1"
echo "$HOME/Library/Developer/Xcode/DerivedData/cmux-${slug}"
}
print_tag_cleanup_reminder() {
local current_slug="$1"
local path=""
local tag=""
local seen=" "
local -a stale_tags=()
while IFS= read -r -d '' path; do
if [[ "$path" == /tmp/cmux-* ]]; then
tag="${path#/tmp/cmux-}"
elif [[ "$path" == "$HOME/Library/Developer/Xcode/DerivedData/cmux-"* ]]; then
tag="${path#$HOME/Library/Developer/Xcode/DerivedData/cmux-}"
else
continue
fi
if [[ "$tag" == "$current_slug" ]]; then
continue
fi
# Only surface stale debug tag builds.
if [[ ! -d "$path/Build/Products/Debug" ]]; then
continue
fi
if [[ "$seen" == *" $tag "* ]]; then
continue
fi
seen="${seen}${tag} "
stale_tags+=("$tag")
done < <(
find /tmp -maxdepth 1 -name 'cmux-*' -print0 2>/dev/null
find "$HOME/Library/Developer/Xcode/DerivedData" -maxdepth 1 -type d -name 'cmux-*' -print0 2>/dev/null
)
echo
echo "Tag cleanup status:"
echo " current tag: ${current_slug} (keep this running until you verify)"
if [[ "${#stale_tags[@]}" -eq 0 ]]; then
echo " stale tags: none"
echo " stale cleanup: not needed"
else
echo " stale tags:"
for tag in "${stale_tags[@]}"; do
echo " - ${tag}"
done
echo "Cleanup stale tags only:"
for tag in "${stale_tags[@]}"; do
echo " pkill -f \"cmux DEV ${tag}.app/Contents/MacOS/cmux DEV\""
echo " rm -rf \"$(tagged_derived_data_path "$tag")\" \"/tmp/cmux-${tag}\" \"/tmp/cmux-debug-${tag}.sock\""
echo " rm -f \"/tmp/cmux-debug-${tag}.log\""
echo " rm -f \"$HOME/Library/Application Support/cmux/cmuxd-dev-${tag}.sock\""
done
fi
echo "After you verify current tag, cleanup command:"
echo " pkill -f \"cmux DEV ${current_slug}.app/Contents/MacOS/cmux DEV\""
echo " rm -rf \"$(tagged_derived_data_path "$current_slug")\" \"/tmp/cmux-${current_slug}\" \"/tmp/cmux-debug-${current_slug}.sock\""
echo " rm -f \"/tmp/cmux-debug-${current_slug}.log\""
echo " rm -f \"$HOME/Library/Application Support/cmux/cmuxd-dev-${current_slug}.sock\""
}
while [[ $# -gt 0 ]]; do
case "$1" in
--tag)
TAG="${2:-}"
if [[ -z "$TAG" ]]; then
echo "error: --tag requires a value" >&2
exit 1
fi
shift 2
;;
--name)
APP_NAME="${2:-}"
if [[ -z "$APP_NAME" ]]; then
echo "error: --name requires a value" >&2
exit 1
fi
NAME_SET=1
shift 2
;;
--bundle-id)
BUNDLE_ID="${2:-}"
if [[ -z "$BUNDLE_ID" ]]; then
echo "error: --bundle-id requires a value" >&2
exit 1
fi
BUNDLE_SET=1
shift 2
;;
--launch)
LAUNCH=1
shift
;;
--derived-data)
DERIVED_DATA="${2:-}"
if [[ -z "$DERIVED_DATA" ]]; then
echo "error: --derived-data requires a value" >&2
exit 1
fi
DERIVED_SET=1
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown option $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$TAG" ]]; then
echo "error: --tag is required (example: ./scripts/reload.sh --tag fix-sidebar-theme)" >&2
usage
exit 1
fi
if [[ -n "$TAG" ]]; then
TAG_ID="$(sanitize_bundle "$TAG")"
TAG_SLUG="$(sanitize_path "$TAG")"
if [[ "$NAME_SET" -eq 0 ]]; then
APP_NAME="cmux DEV ${TAG}"
fi
if [[ "$BUNDLE_SET" -eq 0 ]]; then
BUNDLE_ID="com.cmuxterm.app.debug.${TAG_ID}"
fi
if [[ "$DERIVED_SET" -eq 0 ]]; then
DERIVED_DATA="$(tagged_derived_data_path "$TAG_SLUG")"
fi
fi
XCODEBUILD_ARGS=(
-project GhosttyTabs.xcodeproj
-scheme cmux
-configuration Debug
-destination 'platform=macOS'
)
if [[ -n "$DERIVED_DATA" ]]; then
XCODEBUILD_ARGS+=(-derivedDataPath "$DERIVED_DATA")
fi
if [[ -z "$TAG" ]]; then
XCODEBUILD_ARGS+=(
INFOPLIST_KEY_CFBundleName="$APP_NAME"
INFOPLIST_KEY_CFBundleDisplayName="$APP_NAME"
PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID"
)
fi
# Forward CMUX_SKIP_ZIG_BUILD to xcodebuild run script phases (e.g. macOS
# Tahoe where zig 0.15.2 can't link the ghostty CLI helper).
if [[ "${CMUX_SKIP_ZIG_BUILD:-}" == "1" ]]; then
XCODEBUILD_ARGS+=(CMUX_SKIP_ZIG_BUILD=1)
fi
XCODEBUILD_ARGS+=(build)
XCODE_LOG="/tmp/cmux-xcodebuild-${TAG_SLUG}.log"
set +e
xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | tee "$XCODE_LOG" | grep -E '(warning:|error:|fatal:|BUILD FAILED|BUILD SUCCEEDED|\*\* BUILD)'
XCODE_PIPESTATUS=("${PIPESTATUS[@]}")
set -e
XCODE_EXIT="${XCODE_PIPESTATUS[0]}"
echo "Full build log: $XCODE_LOG"
if [[ "$XCODE_EXIT" -ne 0 ]]; then
echo "error: xcodebuild failed with exit code $XCODE_EXIT" >&2
exit "$XCODE_EXIT"
fi
sleep 0.2
FALLBACK_APP_NAME="$BASE_APP_NAME"
SEARCH_APP_NAME="$APP_NAME"
if [[ -n "$TAG" ]]; then
SEARCH_APP_NAME="$BASE_APP_NAME"
fi
if [[ -n "$DERIVED_DATA" ]]; then
APP_PATH="${DERIVED_DATA}/Build/Products/Debug/${SEARCH_APP_NAME}.app"
if [[ ! -d "${APP_PATH}" && "$SEARCH_APP_NAME" != "$FALLBACK_APP_NAME" ]]; then
APP_PATH="${DERIVED_DATA}/Build/Products/Debug/${FALLBACK_APP_NAME}.app"
fi
else
APP_BINARY="$(
find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/${SEARCH_APP_NAME}.app/Contents/MacOS/${SEARCH_APP_NAME}" -print0 \
| xargs -0 /usr/bin/stat -f "%m %N" 2>/dev/null \
| sort -nr \
| head -n 1 \
| cut -d' ' -f2-
)"
if [[ -n "${APP_BINARY}" ]]; then
APP_PATH="$(dirname "$(dirname "$(dirname "$APP_BINARY")")")"
fi
if [[ -z "${APP_PATH}" && "$SEARCH_APP_NAME" != "$FALLBACK_APP_NAME" ]]; then
APP_BINARY="$(
find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/${FALLBACK_APP_NAME}.app/Contents/MacOS/${FALLBACK_APP_NAME}" -print0 \
| xargs -0 /usr/bin/stat -f "%m %N" 2>/dev/null \
| sort -nr \
| head -n 1 \
| cut -d' ' -f2-
)"
if [[ -n "${APP_BINARY}" ]]; then
APP_PATH="$(dirname "$(dirname "$(dirname "$APP_BINARY")")")"
fi
fi
fi
if [[ -z "${APP_PATH}" || ! -d "${APP_PATH}" ]]; then
echo "${APP_NAME}.app not found in DerivedData" >&2
exit 1
fi
if [[ -n "${TAG_SLUG:-}" ]]; then
TMP_COMPAT_DERIVED_LINK="/tmp/cmux-${TAG_SLUG}"
if [[ "$DERIVED_DATA" != "$TMP_COMPAT_DERIVED_LINK" ]]; then
ABS_DERIVED_DATA="$(cd "$DERIVED_DATA" && pwd)"
rm -rf "$TMP_COMPAT_DERIVED_LINK"
ln -s "$ABS_DERIVED_DATA" "$TMP_COMPAT_DERIVED_LINK"
fi
fi
if [[ -n "$TAG" && "$APP_NAME" != "$SEARCH_APP_NAME" ]]; then
TAG_APP_PATH="$(dirname "$APP_PATH")/${APP_NAME}.app"
rm -rf "$TAG_APP_PATH"
cp -R "$APP_PATH" "$TAG_APP_PATH"
INFO_PLIST="$TAG_APP_PATH/Contents/Info.plist"
if [[ -f "$INFO_PLIST" ]]; then
/usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleName string $APP_NAME" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $APP_NAME" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $BUNDLE_ID" "$INFO_PLIST"
if [[ -n "${TAG_SLUG:-}" ]]; then
APP_SUPPORT_DIR="$HOME/Library/Application Support/cmux"
CMUXD_SOCKET="${APP_SUPPORT_DIR}/cmuxd-dev-${TAG_SLUG}.sock"
CMUX_SOCKET="/tmp/cmux-debug-${TAG_SLUG}.sock"
CMUX_DEBUG_LOG="/tmp/cmux-debug-${TAG_SLUG}.log"
write_last_socket_path "$CMUX_SOCKET"
echo "$CMUX_DEBUG_LOG" > /tmp/cmux-last-debug-log-path || true
/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUXD_UNIX_PATH \"${CMUXD_SOCKET}\"" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUXD_UNIX_PATH string \"${CMUXD_SOCKET}\"" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_SOCKET_PATH \"${CMUX_SOCKET}\"" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_SOCKET_PATH string \"${CMUX_SOCKET}\"" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_DEBUG_LOG \"${CMUX_DEBUG_LOG}\"" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_DEBUG_LOG string \"${CMUX_DEBUG_LOG}\"" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_SOCKET_ENABLE 1" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_SOCKET_ENABLE string 1" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_SOCKET_MODE allowAll" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_SOCKET_MODE string allowAll" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD 1" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD string 1" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUXTERM_REPO_ROOT \"${PWD}\"" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUXTERM_REPO_ROOT string \"${PWD}\"" "$INFO_PLIST"
if [[ -S "$CMUXD_SOCKET" ]]; then
for PID in $(lsof -t "$CMUXD_SOCKET" 2>/dev/null); do
kill "$PID" 2>/dev/null || true
done
rm -f "$CMUXD_SOCKET"
fi
if [[ -S "$CMUX_SOCKET" ]]; then
rm -f "$CMUX_SOCKET"
fi
fi
/usr/bin/codesign --force --sign - --timestamp=none --generate-entitlement-der "$TAG_APP_PATH" >/dev/null 2>&1 || true
fi
APP_PATH="$TAG_APP_PATH"
fi
CLI_PATH="$(dirname "$APP_PATH")/cmux"
if [[ -x "$CLI_PATH" ]]; then
(umask 077; printf '%s\n' "$CLI_PATH" > /tmp/cmux-last-cli-path) || true
ln -sfn "$CLI_PATH" /tmp/cmux-cli || true
# Stable shim that always follows the last reload-selected dev CLI.
DEV_CLI_SHIM="$HOME/.local/bin/cmux-dev"
write_dev_cli_shim "$DEV_CLI_SHIM" "/Applications/cmux.app/Contents/Resources/bin/cmux"
CMUX_SHIM_TARGET="$(select_cmux_shim_target || true)"
if [[ -n "${CMUX_SHIM_TARGET:-}" ]]; then
write_dev_cli_shim "$CMUX_SHIM_TARGET" "/Applications/cmux.app/Contents/Resources/bin/cmux"
fi
fi
# Build cmuxd and ghostty helper binaries (needed for both launch and no-launch).
CMUXD_SRC="$PWD/cmuxd/zig-out/bin/cmuxd"
GHOSTTY_HELPER_SRC="$PWD/ghostty/zig-out/bin/ghostty"
if [[ -d "$PWD/cmuxd" ]]; then
(cd "$PWD/cmuxd" && zig build -Doptimize=ReleaseFast)
fi
if [[ -d "$PWD/ghostty" ]]; then
(cd "$PWD/ghostty" && zig build cli-helper -Dapp-runtime=none -Demit-macos-app=false -Demit-xcframework=false -Doptimize=ReleaseFast)
fi
if [[ -x "$CMUXD_SRC" ]]; then
BIN_DIR="$APP_PATH/Contents/Resources/bin"
mkdir -p "$BIN_DIR"
cp "$CMUXD_SRC" "$BIN_DIR/cmuxd"
chmod +x "$BIN_DIR/cmuxd"
fi
if [[ -x "$GHOSTTY_HELPER_SRC" ]]; then
BIN_DIR="$APP_PATH/Contents/Resources/bin"
mkdir -p "$BIN_DIR"
cp "$GHOSTTY_HELPER_SRC" "$BIN_DIR/ghostty"
chmod +x "$BIN_DIR/ghostty"
fi
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
if [[ -x "$CLI_PATH" ]]; then
echo "$CLI_PATH" > /tmp/cmux-last-cli-path || true
fi
if [[ "$LAUNCH" -eq 1 ]]; then
# Ensure any running instance is fully terminated, regardless of DerivedData path.
/usr/bin/osascript -e "tell application id \"${BUNDLE_ID}\" to quit" >/dev/null 2>&1 || true
sleep 0.3
if [[ -z "$TAG" ]]; then
# Non-tag mode: kill any running instance (across any DerivedData path) to avoid socket conflicts.
pkill -f "/${BASE_APP_NAME}.app/Contents/MacOS/${BASE_APP_NAME}" || true
else
# Tag mode: only kill the tagged instance; allow side-by-side with the main app.
pkill -f "${APP_NAME}.app/Contents/MacOS/${BASE_APP_NAME}" || true
fi
sleep 0.3
# Avoid inheriting cmux/ghostty environment variables from the terminal that
# runs this script (often inside another cmux instance), which can cause
# socket and resource-path conflicts.
OPEN_CLEAN_ENV=(
env
-u CMUX_SOCKET_PATH
-u CMUX_WORKSPACE_ID
-u CMUX_SURFACE_ID
-u CMUX_TAB_ID
-u CMUX_PANEL_ID
-u CMUXD_UNIX_PATH
-u CMUX_TAG
-u CMUX_DEBUG_LOG
-u CMUX_BUNDLE_ID
-u CMUX_SHELL_INTEGRATION
-u GHOSTTY_BIN_DIR
-u GHOSTTY_RESOURCES_DIR
-u GHOSTTY_SHELL_FEATURES
# Dev shells (including CI/Codex) often force-disable paging by exporting these.
# Don't leak that into cmux, otherwise `git diff` won't page even with PAGER=less.
-u GIT_PAGER
-u GH_PAGER
-u TERMINFO
-u XDG_DATA_DIRS
)
if [[ -n "${TAG_SLUG:-}" && -n "${CMUX_SOCKET:-}" ]]; then
# Ensure tag-specific socket paths win even if the caller has CMUX_* overrides.
"${OPEN_CLEAN_ENV[@]}" CMUX_TAG="$TAG_SLUG" CMUX_SOCKET_ENABLE=1 CMUX_SOCKET_MODE=allowAll CMUX_SOCKET_PATH="$CMUX_SOCKET" CMUXD_UNIX_PATH="$CMUXD_SOCKET" CMUX_DEBUG_LOG="$CMUX_DEBUG_LOG" CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1 CMUXTERM_REPO_ROOT="$PWD" open -g "$APP_PATH"
elif [[ -n "${TAG_SLUG:-}" ]]; then
"${OPEN_CLEAN_ENV[@]}" CMUX_TAG="$TAG_SLUG" CMUX_SOCKET_ENABLE=1 CMUX_SOCKET_MODE=allowAll CMUX_DEBUG_LOG="$CMUX_DEBUG_LOG" CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1 CMUXTERM_REPO_ROOT="$PWD" open -g "$APP_PATH"
else
echo "/tmp/cmux-debug.sock" > /tmp/cmux-last-socket-path || true
echo "/tmp/cmux-debug.log" > /tmp/cmux-last-debug-log-path || true
"${OPEN_CLEAN_ENV[@]}" open -g "$APP_PATH"
fi
# Safety: ensure only one instance is running.
sleep 0.2
PIDS=($(pgrep -f "${APP_PATH}/Contents/MacOS/" || true))
if [[ "${#PIDS[@]}" -gt 1 ]]; then
NEWEST_PID=""
NEWEST_AGE=999999
for PID in "${PIDS[@]}"; do
AGE="$(ps -o etimes= -p "$PID" | tr -d ' ')"
if [[ -n "$AGE" && "$AGE" -lt "$NEWEST_AGE" ]]; then
NEWEST_AGE="$AGE"
NEWEST_PID="$PID"
fi
done
for PID in "${PIDS[@]}"; do
if [[ "$PID" != "$NEWEST_PID" ]]; then
kill "$PID" 2>/dev/null || true
fi
done
fi
fi
echo
echo "App path:"
echo " $APP_PATH"
if [[ -n "${TAG_SLUG:-}" ]]; then
print_tag_cleanup_reminder "$TAG_SLUG"
fi
if [[ -x "${CLI_PATH:-}" ]]; then
echo
echo "CLI path:"
echo " $CLI_PATH"
echo "CLI helpers:"
echo " /tmp/cmux-cli ..."
echo " $HOME/.local/bin/cmux-dev ..."
if [[ -n "${CMUX_SHIM_TARGET:-}" ]]; then
echo " $CMUX_SHIM_TARGET ..."
fi
echo "If your shell still resolves the old cmux, run: rehash"
fi
if [[ "$LAUNCH" -eq 0 ]]; then
echo
echo "Build complete. Pass --launch to open the app, or cmd-click the path above."
fi