#!/usr/bin/env bash # mountOS installer, https://mountos.sh/install # Usage: curl -fsSL https://mountos.sh/install | bash [-- OPTIONS] # --pkg NAME package to install (default: the full client bundle, mountos-macos on # macOS / mountos-windows on Windows; mountos-cli elsewhere. The bundles # carry the mountos CLI plus that platform's mount backend) # --version X.Y.Z install specific version (default: latest) # --major N install latest of major series N # --platform NAME override platform: linux, darwin, windows # --arch NAME override arch: amd64, arm64 # --prefix PATH install prefix (default: /usr/local/bin) # --list list available versions for --pkg and exit # --auto-install-mcp register the MCP connector with local AI assistants after install set -euo pipefail DIST_BASE="https://mountos.sh/install" PKG="" VERSION="" MAJOR="" PREFIX="/usr/local/bin" PLATFORM_OVERRIDE="" ARCH_OVERRIDE="" LIST=0 MCP_INSTALL=0 # Out-of-band trust root: verifies the sha256 checksum was signed by mountOS's # release key, not just fetched from the same bucket the artifact lives in (a # compromised bucket/CDN could otherwise swap the binary and its checksum together). # Public key, not a secret; matches internal/identity.LicenseVerifierKey's format. RELEASE_SIGNING_PUBKEY="MCowBQYDK2VwAyEAeMcv01w22DLxoreGVjrnkWbyl0/VnOLuSzm5QkoX354=" _usage() { echo "Usage: bash mountos.sh [--pkg NAME] [--version X.Y.Z] [--major N]" >&2 echo " [--platform NAME] [--arch NAME] [--prefix PATH] [--list]" >&2 echo " [--auto-install-mcp]" >&2 exit 1 } _safe_arg() { local val="$1" name="$2" [[ "$val" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "Invalid value for ${name}: '${val}'" >&2; exit 1 } echo "$val" } while [[ $# -gt 0 ]]; do case "$1" in --pkg) [[ $# -ge 2 ]] || _usage; PKG=$(_safe_arg "$2" "--pkg"); shift 2 ;; --version) [[ $# -ge 2 ]] || _usage; VERSION=$(_safe_arg "$2" "--version"); shift 2 ;; --major) [[ $# -ge 2 ]] || _usage [[ "$2" =~ ^[0-9]+$ ]] || { echo "--major must be a number" >&2; exit 1; } MAJOR="$2"; shift 2 ;; --platform) [[ $# -ge 2 ]] || _usage; PLATFORM_OVERRIDE=$(_safe_arg "$2" "--platform"); shift 2 ;; --arch) [[ $# -ge 2 ]] || _usage; ARCH_OVERRIDE=$(_safe_arg "$2" "--arch"); shift 2 ;; --prefix) [[ $# -ge 2 ]] || _usage; PREFIX="$2"; shift 2 ;; --list) LIST=1; shift ;; --auto-install-mcp) MCP_INSTALL=1; shift ;; --help|-h) _usage ;; *) echo "Unknown option: $1" >&2; _usage ;; esac done # Detect platform and arch, apply overrides (needed before the default --pkg # choice below, and before --list, so both see the real target platform). _os=$(uname -s | tr '[:upper:]' '[:lower:]') _arch_raw=$(uname -m) case "$_arch_raw" in x86_64) _arch="amd64" ;; aarch64|arm64) _arch="arm64" ;; *) _arch="$_arch_raw" ;; esac [[ -n "$PLATFORM_OVERRIDE" ]] && _os="$PLATFORM_OVERRIDE" [[ -n "$ARCH_OVERRIDE" ]] && _arch="$ARCH_OVERRIDE" case "$_os" in darwin|linux|windows) PLATFORM_ARCH="${_os}-${_arch}" ;; *) echo "Unsupported platform: $_os" >&2; exit 1 ;; esac # Default package is the full client bundle where one exists (macOS: signed .pkg # with FSKit/Desktop/CLI; Windows: signed .exe with the mountosio driver + CLI). # Linux has no bundled GUI suite, so it defaults to the bare CLI archive. if [[ -z "$PKG" ]]; then case "$_os" in darwin) PKG="mountos-macos" ;; windows) PKG="mountos-windows" ;; *) PKG="mountos-cli-${_os}" ;; esac fi # mountos-macos ships one universal .pkg (FSKit/Desktop build universal, and the # CLI inside the bundle is lipo'd into a universal binary too), unlike every other # package here, which is per-arch. [[ "$PKG" == "mountos-macos" ]] && PLATFORM_ARCH="darwin-universal" # The CLI is published per platform (it versions per platform, and one shared package's # latest.json cannot carry three different versions). Accept the old bare name so existing # instructions and scripts keep working. if [[ "$PKG" == "mountos-cli" ]]; then PKG="mountos-cli-${_os}" fi # --list: fetch versions.json for PKG, print, and exit if [[ "$LIST" == 1 ]]; then VERSIONS_URL="${DIST_BASE}/dist/${PKG}/versions.json" echo "Fetching versions for ${PKG}..." _raw=$(curl -fsSL --connect-timeout 10 --max-time 30 "$VERSIONS_URL") || { echo "Failed to fetch versions for ${PKG}" >&2; exit 1 } _latest="" _versions=() if command -v jq &>/dev/null; then _latest=$(jq -r '.latest // empty' <<< "$_raw") while IFS= read -r v; do [[ -n "$v" ]] && _versions+=("$v") done < <(jq -r ' [.versions[] | split(".") | map(tonumber)] | sort | reverse | .[] | join(".") ' <<< "$_raw" 2>/dev/null || jq -r '.versions[] // empty' <<< "$_raw") else _latest=$(printf '%s' "$_raw" \ | grep -o '"latest"[[:space:]]*:[[:space:]]*"[^"]*"' \ | sed 's/.*"latest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1) while IFS= read -r v; do [[ -n "$v" ]] && _versions+=("$v") done < <(printf '%s' "$_raw" \ | grep -o '"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[^"]*"' \ | tr -d '"') fi echo "Available versions for ${PKG}:" for v in "${_versions[@]}"; do if [[ -n "$MAJOR" && "${v%%.*}" != "$MAJOR" ]]; then continue fi if [[ "$v" == "$_latest" ]]; then printf ' %s (latest)\n' "$v" else printf ' %s\n' "$v" fi done exit 0 fi # Detect sha256 tool if command -v sha256sum &>/dev/null; then _sha256() { sha256sum "$1" | awk '{print $1}'; } elif command -v shasum &>/dev/null; then _sha256() { shasum -a 256 "$1" | awk '{print $1}'; } else echo "Neither sha256sum nor shasum found, cannot verify download integrity" >&2; exit 1 fi # Full client bundles are opaque platform installers (.pkg/.exe), handed directly # to the OS installer below rather than extracted; every other package is a plain # tar.gz/zip of binaries. $1 = pkg name. _ext_for_pkg() { case "$1" in mountos-macos) echo "pkg" ;; mountos-windows) echo "exe" ;; *) [[ "$_os" == "windows" ]] && echo "zip" || echo "tar.gz" ;; esac } ARTIFACT_URL="" EXPECTED_SHA256="" EXT="" if [[ -n "$VERSION" ]]; then EXT="$(_ext_for_pkg "$PKG")" ARTIFACT_URL="${DIST_BASE}/dist/${PKG}/${VERSION}/${PLATFORM_ARCH}.${EXT}" SHA256_URL="${DIST_BASE}/dist/${PKG}/${VERSION}/${PLATFORM_ARCH}.sha256" else if [[ -n "$MAJOR" ]]; then MANIFEST_URL="${DIST_BASE}/dist/${PKG}/v${MAJOR}/latest.json" else MANIFEST_URL="${DIST_BASE}/dist/${PKG}/latest.json" fi echo "Fetching manifest: ${MANIFEST_URL}" MANIFEST=$(curl -fsSL --connect-timeout 10 --max-time 30 "$MANIFEST_URL") || { echo "Failed to fetch manifest for ${PKG}" >&2; exit 1 } if command -v jq &>/dev/null; then VERSION=$(jq -r '.version // empty' <<< "$MANIFEST") ARTIFACT_URL=$(jq -r --arg k "$PLATFORM_ARCH" '.artifacts[$k].url // empty' <<< "$MANIFEST") EXPECTED_SHA256=$(jq -r --arg k "$PLATFORM_ARCH" '.artifacts[$k].sha256 // empty' <<< "$MANIFEST") else VERSION=$(echo "$MANIFEST" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' \ | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') _block=$(echo "$MANIFEST" | grep -F -A10 "\"${PLATFORM_ARCH}\"") ARTIFACT_URL=$(echo "$_block" | grep '"url"' | head -1 \ | sed 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') EXPECTED_SHA256=$(echo "$_block" | grep '"sha256"' | head -1 \ | sed 's/.*"sha256"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') fi SHA256_URL="" [[ -z "$VERSION" ]] && { echo "Could not parse version from manifest" >&2; exit 1; } [[ -z "$ARTIFACT_URL" ]] && { echo "No artifact for ${PLATFORM_ARCH} in ${PKG} ${VERSION}" >&2; exit 1 } _path="${ARTIFACT_URL%%\?*}" _basename="${_path##*/}" EXT="${_basename#*.}" fi echo "Installing ${PKG} ${VERSION} (${PLATFORM_ARCH})" if [[ -d "$PREFIX" && ! -w "$PREFIX" ]]; then echo "Prefix ${PREFIX} is not writable. Try: sudo bash $0 --prefix $PREFIX" >&2 echo "Or use a user-local prefix: bash $0 --prefix \$HOME/.local/bin" >&2 exit 1 fi TMPDIR_PKG=$(mktemp -d) trap 'rm -rf "${TMPDIR_PKG:?}"' EXIT OUTFILE="${TMPDIR_PKG}/artifact.${EXT}" echo "Downloading ${ARTIFACT_URL}" curl -fSL --progress-bar --connect-timeout 30 --max-time 600 "$ARTIFACT_URL" -o "$OUTFILE" # Verifies $1 (a sha256 hex digest) was signed by RELEASE_SIGNING_PUBKEY, fetching # the detached signature from the same dist///.sha256.sig # path upload.sh writes it to. Warns and continues if openssl/the sig itself is # unavailable (best-effort hardening layer, not required), but a signature that IS # present and does NOT verify is treated as tampering and hard-fails the install. _verify_release_signature() { local digest="$1" command -v openssl &>/dev/null || { echo "Warning: openssl not found, skipping release signature check" >&2; return 0 } local sig_url="${DIST_BASE}/dist/${PKG}/${VERSION}/${PLATFORM_ARCH}.sha256.sig" local sig_b64 sig_b64=$(curl -fsSL --connect-timeout 10 --max-time 30 "$sig_url" 2>/dev/null) || { echo "Warning: no release signature available, skipping" >&2; return 0 } local pubfile datafile sigfile pubfile=$(mktemp); datafile=$(mktemp); sigfile=$(mktemp) { echo "-----BEGIN PUBLIC KEY-----" echo "$RELEASE_SIGNING_PUBKEY" echo "-----END PUBLIC KEY-----" } >| "$pubfile" printf '%s' "$digest" >| "$datafile" printf '%s' "$sig_b64" | openssl base64 -d -A -out "$sigfile" 2>/dev/null if openssl pkeyutl -verify -pubin -inkey "$pubfile" -rawin -in "$datafile" -sigfile "$sigfile" &>/dev/null; then echo "Release signature OK" rm -f "$pubfile" "$datafile" "$sigfile" else rm -f "$pubfile" "$datafile" "$sigfile" echo "Release signature verification FAILED, refusing to install (possible tampering)" >&2 exit 1 fi } if [[ -n "$EXPECTED_SHA256" ]]; then _got=$(_sha256 "$OUTFILE") if [[ "$_got" != "$EXPECTED_SHA256" ]]; then echo "sha256 mismatch: expected ${EXPECTED_SHA256}, got ${_got}" >&2; exit 1 fi echo "sha256 OK" _verify_release_signature "$_got" elif [[ -n "${SHA256_URL:-}" ]]; then _expected=$(curl -fsSL --connect-timeout 10 --max-time 30 "$SHA256_URL" | awk '{print $1}') _got=$(_sha256 "$OUTFILE") if [[ "$_got" != "$_expected" ]]; then echo "sha256 mismatch: expected ${_expected}, got ${_got}" >&2; exit 1 fi echo "sha256 OK" _verify_release_signature "$_got" else echo "Warning: no sha256 available, skipping integrity check" >&2 fi # Full client bundles (.pkg/.exe) are whole platform installers, not archives to # unpack: hand them to the OS installer instead of tar/unzip. Copy out of # TMPDIR_PKG first since it's deleted on EXIT the moment this script returns, but # `open` on macOS launches Installer.app asynchronously and returns immediately, # well before the user finishes the GUI flow. if [[ "$EXT" == "pkg" || "$EXT" == "exe" ]]; then _dest_dir="${HOME}/Downloads" [[ -d "$_dest_dir" && -w "$_dest_dir" ]] || _dest_dir="$PWD" _dest="${_dest_dir}/mountOS-${VERSION}-${PLATFORM_ARCH}.${EXT}" cp "$OUTFILE" "$_dest" echo "Saved installer to ${_dest}" if [[ "$EXT" == "pkg" && "$(uname -s)" == "Darwin" ]]; then echo "Opening the mountOS installer..." open "$_dest" echo "Finish the installation in the window that just opened (component selection, license, admin password)." elif [[ "$EXT" == "exe" && "$_os" == "windows" ]]; then echo "Launching the mountOS installer..." chmod +x "$_dest" 2>/dev/null || true "$_dest" else echo "This installer targets ${PLATFORM_ARCH} and can't run on this host; run it manually on that platform." fi exit 0 fi EXTRACT_DIR="${TMPDIR_PKG}/pkg" mkdir -p "$EXTRACT_DIR" case "$EXT" in tar.gz|gz|tgz) tar -xzf "$OUTFILE" -C "$EXTRACT_DIR" ;; zip) command -v unzip &>/dev/null || { echo "unzip not found, cannot extract .zip" >&2; exit 1; } unzip -q "$OUTFILE" -d "$EXTRACT_DIR" ;; *) echo "Unknown archive type: .${EXT}" >&2; exit 1 ;; esac if [[ -f "${EXTRACT_DIR}/install.sh" ]]; then bash "${EXTRACT_DIR}/install.sh" --prefix "$PREFIX" else mkdir -p "$PREFIX" installed=0 while IFS= read -r -d '' bin; do install -m755 "$bin" "$PREFIX/" installed=$(( installed + 1 )) done < <(find "$EXTRACT_DIR" -maxdepth 2 -type f -perm -100 -print0) (( installed > 0 )) || { echo "No executable files found in archive" >&2; exit 1; } echo "Installed ${installed} file(s) to ${PREFIX}" fi echo "mountOS ${PKG} ${VERSION} installed successfully." if [[ -x "${PREFIX}/mountos" ]]; then if [[ "$MCP_INSTALL" == 1 ]]; then echo echo "Registering MCP connector with local AI assistants..." "${PREFIX}/mountos" mcp install || \ echo " (mcp install reported an issue; re-run later: ${PREFIX}/mountos mcp install)" else echo echo "AI assistant access (optional): register the read-only MCP connector with" echo "Claude Desktop, Claude Code, Codex and Gemini by running:" echo " ${PREFIX}/mountos mcp install" fi fi