#!/bin/sh # Mode installer -- download and install Mode (server + TUI) from a release tarball. # # Usage: # curl -fsSL https://raw.githubusercontent.com/gotmode/mode/main/install.sh | sh # curl -fsSL https://raw.githubusercontent.com/gotmode/mode/main/install.sh | sh -s -- --version 0.2.0 # curl -fsSL https://gotmode.sh | sh # # Custom hosting: # curl -fsSL https://raw.githubusercontent.com/gotmode/mode/main/install.sh | sh -s -- --base-url https://dl.example.com/releases # curl -fsSL https://gotmode.sh | sh -s -- --channel beta # # Environment variables: # MODE_VERSION -- version to install (default: latest stable from manifest) # MODE_INSTALL -- install directory (default: ~/.config/mode) # MODE_BASE_URL -- base URL for release downloads and version manifest # MODE_CHANNEL -- release channel: stable, beta, alpha, rc (default: stable) set -eu REPO="gotmode/mode" INSTALL_DIR="${MODE_INSTALL:-$HOME/.config/mode}" VERSION="${MODE_VERSION:-}" BASE_URL="${MODE_BASE_URL:-https://dl.gotmode.sh}" CHANNEL="${MODE_CHANNEL:-stable}" # ------------------------------------------------------------------- # Helpers # ------------------------------------------------------------------- info() { printf "\033[1;34m==>\033[0m %s\n" "$1"; } warn() { printf "\033[1;33mWARN:\033[0m %s\n" "$1" >&2; } error() { printf "\033[1;31mERROR:\033[0m %s\n" "$1" >&2; exit 1; } need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then error "Required command '$1' not found. Please install it and retry." fi } # ------------------------------------------------------------------- # Detect OS and architecture # ------------------------------------------------------------------- detect_platform() { OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m)" case "$OS" in linux) OS="linux" ;; darwin) OS="darwin" ;; *) error "Unsupported operating system: $OS" ;; esac case "$ARCH" in x86_64) ARCH="amd64" ;; amd64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; arm64) ARCH="arm64" ;; *) error "Unsupported architecture: $ARCH" ;; esac PLATFORM="${OS}-${ARCH}" } # ------------------------------------------------------------------- # Parse arguments # ------------------------------------------------------------------- parse_args() { while [ $# -gt 0 ]; do case "$1" in --version) VERSION="$2"; shift 2 ;; --version=*) VERSION="${1#--version=}"; shift ;; --base-url) BASE_URL="$2"; shift 2 ;; --base-url=*) BASE_URL="${1#--base-url=}"; shift ;; --channel) CHANNEL="$2"; shift 2 ;; --channel=*) CHANNEL="${1#--channel=}"; shift ;; *) shift ;; esac done } # ------------------------------------------------------------------- # Resolve version # ------------------------------------------------------------------- resolve_version() { if [ -n "$VERSION" ]; then # Explicit version provided -- use it directly. return fi case "$CHANNEL" in stable|alpha|beta|rc) ;; *) error "Unknown channel: ${CHANNEL}. Use stable, alpha, beta, or rc." ;; esac info "Detecting latest ${CHANNEL} version..." # Fetch the versions manifest from the download host. # The manifest is a JSON file mapping channel names to version strings: # { "stable": "0.1.0", "alpha": "0.1.0-alpha.1", "beta": null, "rc": null } MANIFEST_URL="${BASE_URL%/}/versions.json" MANIFEST="$(curl -fsSL "$MANIFEST_URL" 2>/dev/null)" || true if [ -n "$MANIFEST" ]; then # Extract the version for the requested channel from the JSON. # Handles both "channel": "version" and "channel": null. VERSION="$(printf '%s' "$MANIFEST" \ | sed -n "s/.*\"${CHANNEL}\": *\"\([^\"]*\)\".*/\1/p")" || true fi if [ -z "$VERSION" ]; then error "Could not determine latest ${CHANNEL} version from ${MANIFEST_URL}.\n Set MODE_VERSION or use --version to specify a version explicitly." fi } # ------------------------------------------------------------------- # Build download URL # ------------------------------------------------------------------- build_download_url() { TARBALL="mode-v${VERSION}-${PLATFORM}.tar.gz" # Detect whether BASE_URL looks like a GitHub Releases URL or a # generic file host. GitHub uses /download/v/; a plain # file server just serves / directly. case "$BASE_URL" in *github.com/*/releases*) DOWNLOAD_URL="${BASE_URL}/download/v${VERSION}/${TARBALL}" ;; *) # Generic host -- expect tarballs served at /. # Strip trailing slash for consistency. DOWNLOAD_URL="${BASE_URL%/}/${TARBALL}" ;; esac } # ------------------------------------------------------------------- # Download and extract # ------------------------------------------------------------------- download_and_install() { need_cmd curl need_cmd tar TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT info "Downloading Mode v${VERSION} for ${PLATFORM}..." info " ${DOWNLOAD_URL}" HTTP_CODE="$(curl -fSL -w '%{http_code}' -o "${TMP_DIR}/${TARBALL}" "$DOWNLOAD_URL" 2>/dev/null)" || true if [ ! -f "${TMP_DIR}/${TARBALL}" ] || [ "$HTTP_CODE" = "404" ]; then error "Release not found: v${VERSION} for ${PLATFORM}.\n Check available releases at ${BASE_URL}" fi info "Extracting to ${INSTALL_DIR}..." mkdir -p "$INSTALL_DIR" tar -xzf "${TMP_DIR}/${TARBALL}" -C "$TMP_DIR" # The tarball extracts to mode-v-/. EXTRACTED="${TMP_DIR}/mode-v${VERSION}-${PLATFORM}" if [ ! -d "$EXTRACTED" ]; then error "Unexpected tarball structure -- missing ${EXTRACTED}" fi # Copy into install directory, overwriting previous install. rm -rf "${INSTALL_DIR:?}/bin" "${INSTALL_DIR:?}/server" cp -R "${EXTRACTED}/bin" "$INSTALL_DIR/bin" cp -R "${EXTRACTED}/server" "$INSTALL_DIR/server" chmod +x "$INSTALL_DIR/bin/mode" chmod +x "$INSTALL_DIR/server/bin/mode" # Create the sandbox directory (inert CWD for the daemon process). # If the LLM falls through to File.cwd!(), operations land here # instead of in the install tree or a user project. mkdir -p "$INSTALL_DIR/.sandbox" } # ------------------------------------------------------------------- # Shell PATH integration # ------------------------------------------------------------------- configure_path() { BIN_PATH="$INSTALL_DIR/bin" # Make `mode` available for the verify step within this script. export PATH="${BIN_PATH}:${PATH}" # Check if already configured in the shell rc file. SHELL_NAME="$(basename "$SHELL")" case "$SHELL_NAME" in zsh) RC_FILE="$HOME/.zshrc" ;; bash) RC_FILE="$HOME/.bashrc" ;; fish) RC_FILE="$HOME/.config/fish/config.fish" ;; *) RC_FILE="" ;; esac PATH_LINE="export PATH=\"${BIN_PATH}:\$PATH\"" NEEDS_RELOAD=false if [ -n "$RC_FILE" ]; then if [ "$SHELL_NAME" = "fish" ]; then PATH_LINE="set -gx PATH ${BIN_PATH} \$PATH" fi # Don't add if already present. if ! grep -qF "$BIN_PATH" "$RC_FILE" 2>/dev/null; then printf '\n# Mode\n%s\n' "$PATH_LINE" >> "$RC_FILE" info "Added ${BIN_PATH} to ${RC_FILE}" NEEDS_RELOAD=true fi else warn "Could not detect shell rc file. Add this to your shell profile:" warn " ${PATH_LINE}" NEEDS_RELOAD=true fi } # ------------------------------------------------------------------- # Verify installation # ------------------------------------------------------------------- verify() { info "Installed Mode v${VERSION} to ${INSTALL_DIR}" echo "" echo " Binary: ${INSTALL_DIR}/bin/mode" echo "" echo " Usage:" echo " mode Launch TUI (auto-starts runtime)" echo " mode stop Stop the runtime daemon" echo " mode status Show daemon status" echo " mode version Print version info" echo "" if [ "$NEEDS_RELOAD" = true ] && [ -n "$RC_FILE" ]; then printf "\033[1;33m%s\033[0m\n" " To start using mode, reload your shell:" echo "" if [ "$SHELL_NAME" = "fish" ]; then echo " source ${RC_FILE}" else echo " source ${RC_FILE} # reload PATH in current session" fi echo "" fi } # ------------------------------------------------------------------- # Main # ------------------------------------------------------------------- main() { parse_args "$@" detect_platform resolve_version build_download_url download_and_install configure_path verify } main "$@"