* 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>
544 lines
18 KiB
Bash
Executable file
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
|