export PMUX_VERSION="v0.0.2" export PMUX_DOWNLOAD_BASE="https://web-2659-f72b00d8-zgu0kxx1.onporter.run/dl/v0.0.2" #!/usr/bin/env bash # pmux installer — modeled on opencode's install script: # - zero registration, zero telemetry, no network beyond the release host # - platform detect → prebuilt binary from GitHub Releases → PATH setup # One pmux addition: ensure tmux >= 3.2 (pmux wraps real tmux). # # Usage: curl -fsSL | bash # Overrides: PMUX_VERSION=v0.1.0 install a specific version # PMUX_INSTALL_DIR=~/bin install location # PMUX_REPO=porter-dev/pmux GitHub repo # PMUX_DOWNLOAD_BASE= fetch artifacts from a custom host # PMUX_NO_MODIFY_PATH=1 don't touch shell rc files set -euo pipefail REPO="${PMUX_REPO:-porter-dev/pmux}" INSTALL_DIR="${PMUX_INSTALL_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}" say() { printf '\033[1;32mpmux:\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mpmux:\033[0m %s\n' "$*"; } fail() { printf '\033[1;31mpmux:\033[0m %s\n' "$*" >&2; exit 1; } # --- platform --------------------------------------------------------------- case "$(uname -s)" in Darwin) OS=darwin ;; Linux) OS=linux ;; *) fail "unsupported OS $(uname -s) — on Windows, run inside WSL" ;; esac case "$(uname -m)" in x86_64|amd64) ARCH=amd64 ;; arm64|aarch64) ARCH=arm64 ;; *) fail "unsupported architecture $(uname -m)" ;; esac # pmux is a CGO-free static Go binary: no musl/glibc split needed. # --- resolve version -------------------------------------------------------- TAG="${PMUX_VERSION:-}" if [ -z "$TAG" ]; then TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | head -1 | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') || fail "could not resolve the latest release of ${REPO}" [ -n "$TAG" ] || fail "could not resolve the latest release of ${REPO}" fi VERSION="${TAG#v}" FILE="pmux_${VERSION}_${OS}_${ARCH}.tar.gz" BASE="${PMUX_DOWNLOAD_BASE:-https://github.com/${REPO}/releases/download/${TAG}}" # --- download + verify ------------------------------------------------------ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT say "downloading pmux ${TAG} (${OS}/${ARCH})" curl -fsSL -o "${TMP}/${FILE}" "${BASE}/${FILE}" || fail "download failed: ${BASE}/${FILE}" if curl -fsSL -o "${TMP}/checksums.txt" "${BASE}/checksums.txt" 2>/dev/null; then EXPECTED=$(grep " ${FILE}\$" "${TMP}/checksums.txt" | awk '{print $1}') if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "${TMP}/${FILE}" | awk '{print $1}') else ACTUAL=$(shasum -a 256 "${TMP}/${FILE}" | awk '{print $1}') fi [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$ACTUAL" ] || fail "checksum mismatch for ${FILE}" else warn "no checksums.txt published for ${TAG}; skipping verification" fi tar -xzf "${TMP}/${FILE}" -C "$TMP" mkdir -p "$INSTALL_DIR" install -m 0755 "${TMP}/pmux" "${INSTALL_DIR}/pmux" say "installed $("${INSTALL_DIR}/pmux" version) to ${INSTALL_DIR}/pmux" # --- PATH ------------------------------------------------------------------- case ":$PATH:" in *":${INSTALL_DIR}:"*) ;; *) if [ -n "${PMUX_NO_MODIFY_PATH:-}" ]; then warn "${INSTALL_DIR} is not on your PATH (left untouched per PMUX_NO_MODIFY_PATH)" else LINE="export PATH=\"${INSTALL_DIR}:\$PATH\"" ADDED="" for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do [ -f "$RC" ] || continue grep -qsF "$LINE" "$RC" && { ADDED=yes; continue; } printf '\n# added by pmux installer\n%s\n' "$LINE" >>"$RC" say "added ${INSTALL_DIR} to PATH in ${RC}" ADDED=yes done if command -v fish >/dev/null 2>&1; then FISHCONF="$HOME/.config/fish/config.fish" if [ -f "$FISHCONF" ] && ! grep -qs "pmux installer" "$FISHCONF"; then printf '\n# added by pmux installer\nfish_add_path %s\n' "$INSTALL_DIR" >>"$FISHCONF" ADDED=yes fi fi [ -n "$ADDED" ] || warn "add ${INSTALL_DIR} to your PATH to use pmux" say "restart your shell (or 'export PATH=\"${INSTALL_DIR}:\$PATH\"') to pick up pmux" fi ;; esac # --- tmux (pmux wraps the real thing) --------------------------------------- tmux_ok() { command -v tmux >/dev/null 2>&1 || return 1 V=$(tmux -V 2>/dev/null | sed -E 's/^tmux (next-)?([0-9]+)\.([0-9]+).*/\2 \3/') set -- $V [ "${1:-0}" -gt 3 ] || { [ "${1:-0}" -eq 3 ] && [ "${2:-0}" -ge 2 ]; } } if tmux_ok; then say "tmux $(tmux -V | cut -d' ' -f2) found" elif [ "$OS" = darwin ] && command -v brew >/dev/null 2>&1; then say "installing tmux via Homebrew (pmux requires tmux >= 3.2)" brew install tmux || warn "brew install tmux failed — install it manually, then rerun pmux" else warn "pmux requires tmux >= 3.2 and it isn't installed (or is too old)." if command -v apt-get >/dev/null 2>&1; then warn " install it with: sudo apt-get install -y tmux" elif command -v dnf >/dev/null 2>&1; then warn " install it with: sudo dnf install -y tmux" elif command -v pacman >/dev/null 2>&1; then warn " install it with: sudo pacman -S tmux" elif command -v apk >/dev/null 2>&1; then warn " install it with: sudo apk add tmux" else warn " install it with your package manager" fi fi # --- M0 dogfood setup (DELETE THIS BLOCK when the gateway ships in M2) ------ # Pin the internal sandbox cluster so teammates need zero manual config. # The IDs are non-secret identifiers, useless without porter auth + membership. if [ ! -f "$HOME/.pmux/config.json" ]; then mkdir -p "$HOME/.pmux" cat >"$HOME/.pmux/config.json" <<'EOF' { "image": "ubuntu:24.04", "ttl": "2h", "shell": "/bin/bash", "default_session": "main", "porter_project": 533, "porter_cluster": 2540 } EOF say "wrote ~/.pmux/config.json pinned to the internal sandbox cluster" fi if ! command -v porter >/dev/null 2>&1; then warn "this is an internal dogfood build: it drives sandboxes through the porter CLI." warn " 1) install it: curl -fsSL https://install.porter.run | bash" warn " 2) porter auth login (ask for access to the pmux Porter project)" warn " 3) pmux doctor" fi echo say "done. quickstart:" echo " pmux # attach (creates session 'main')" echo " C-b c # new window = new cloud sandbox (~5s)" echo " C-b d # detach — sandboxes keep running" echo " pmux ls # see everything"