#!/usr/bin/env bash # # Markus — AI Digital Workforce Platform # One-line installer: curl -fsSL https://markus.global/install.sh | bash # # If Node.js 22+ is present → lightweight npm install (~5 MB) # If Node.js is missing → downloads standalone binary with bundled runtime (~45 MB) # # Post-install: PATH registration, desktop shortcut, auto-start on login. # set -euo pipefail VERSION="latest" NPM_PACKAGE="@markus-global/cli" GITHUB_REPO="markus-global/markus" INSTALL_DIR="$HOME/.markus/app" # ─── Colors ────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' DIM='\033[2m' NC='\033[0m' BOLD='\033[1m' info() { printf "${BLUE} [info]${NC} %s\n" "$*"; } ok() { printf "${GREEN} [ok]${NC} %s\n" "$*"; } warn() { printf "${YELLOW} [warn]${NC} %s\n" "$*"; } error() { printf "${RED} [error]${NC} %s\n" "$*"; } # ─── Spinner ───────────────────────────────────────────────────────────────── SPINNER_PID="" spinner_start() { local msg="$1" local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local i=0 elapsed=0 ( while true; do local f="${frames[$((i % ${#frames[@]}))]}" if [ "$elapsed" -ge 5 ]; then printf "\r ${CYAN}%s${NC} %s ${DIM}(%ds)${NC} " "$f" "$msg" "$elapsed" else printf "\r ${CYAN}%s${NC} %s " "$f" "$msg" fi sleep 0.5 i=$((i + 1)) elapsed=$((i / 2)) done ) & SPINNER_PID=$! } spinner_stop() { if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then kill "$SPINNER_PID" 2>/dev/null wait "$SPINNER_PID" 2>/dev/null || true printf "\r\033[K" fi SPINNER_PID="" } trap 'spinner_stop' EXIT # ─── Banner ────────────────────────────────────────────────────────────────── banner() { printf "\n" printf "${CYAN}${BOLD}" printf " ┌─────────────────────────────────────┐\n" printf " │ Markus Installer │\n" printf " │ AI Digital Workforce Platform │\n" printf " └─────────────────────────────────────┘\n" printf "${NC}\n" } # ─── OS / Arch detection ──────────────────────────────────────────────────── detect_os() { case "$(uname -s)" in Darwin) echo "darwin" ;; Linux) echo "linux" ;; MINGW*|MSYS*|CYGWIN*) echo "win" ;; *) echo "unknown" ;; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "x64" ;; arm64|aarch64) echo "arm64" ;; *) echo "unknown" ;; esac } # ─── Dependency checks ────────────────────────────────────────────────────── check_node() { if ! command -v node &>/dev/null; then return 1 fi local ver ver="$(node -v | sed 's/^v//' | cut -d. -f1)" if [ "$ver" -lt 22 ] 2>/dev/null; then return 2 fi return 0 } check_npm() { command -v npm &>/dev/null } # ─── Resolve latest version tag from GitHub ────────────────────────────────── resolve_latest_version() { local api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest" local tag tag="$(curl -fsSL "$api_url" 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\([^"]*\)".*/\1/')" echo "$tag" } # ─── npm install path ─────────────────────────────────────────────────────── npm_install_global() { local pkg="$1" local logfile logfile="$(mktemp /tmp/markus-install-XXXXXX)" local install_cmd="npm install -g --no-audit --no-fund --ignore-optional --loglevel=error $pkg" spinner_start "Installing ${pkg} via npm..." if $install_cmd >"$logfile" 2>&1; then spinner_stop ok "Installed @markus-global/cli via npm" rm -f "$logfile" return 0 fi spinner_stop error "npm installation failed. Output:" printf "\n" cat "$logfile" printf "\n" info "If you see permission errors, fix npm prefix instead of using sudo:" printf " ${BOLD}mkdir -p ~/.npm-global${NC}\n" printf " ${BOLD}npm config set prefix ~/.npm-global${NC}\n" printf " ${BOLD}export PATH=~/.npm-global/bin:\$PATH${NC} (add to ~/.zshrc or ~/.bashrc)\n" printf "\n" rm -f "$logfile" return 1 } # ─── Binary install path ──────────────────────────────────────────────────── binary_install() { local os="$1" arch="$2" info "Node.js not found — downloading standalone binary (includes runtime)..." local ver ver="$(resolve_latest_version)" if [ -z "$ver" ]; then error "Could not determine latest release version from GitHub." error "Check your network connection and try again." return 1 fi info "Latest version: v${ver}" local archive_name="markus-v${ver}-${os}-${arch}" local ext="tar.gz" local url="https://github.com/${GITHUB_REPO}/releases/download/v${ver}/${archive_name}.${ext}" local tmpdir tmpdir="$(mktemp -d /tmp/markus-download-XXXXXX)" spinner_start "Downloading ${archive_name}.${ext}..." if ! curl -fSL --progress-bar -o "$tmpdir/${archive_name}.${ext}" "$url" 2>/dev/null; then spinner_stop error "Download failed: $url" error "Binary for ${os}-${arch} may not be available yet for v${ver}." rm -rf "$tmpdir" return 1 fi spinner_stop ok "Downloaded ${archive_name}.${ext}" # Extract info "Extracting to ${INSTALL_DIR}..." rm -rf "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" tar -xzf "$tmpdir/${archive_name}.${ext}" -C "$INSTALL_DIR" --strip-components=1 chmod +x "$INSTALL_DIR/markus" "$INSTALL_DIR/bin/node" 2>/dev/null || true ok "Extracted to ${INSTALL_DIR}" rm -rf "$tmpdir" return 0 } # ─── Post-install: PATH registration ──────────────────────────────────────── setup_path() { local install_mode="$1" if [ "$install_mode" = "binary" ]; then local target_dir="$INSTALL_DIR" local shell_name rc_file line shell_name="$(basename "${SHELL:-/bin/bash}")" case "$shell_name" in zsh) rc_file="$HOME/.zshrc" ;; bash) rc_file="$HOME/.bashrc" ;; fish) rc_file="$HOME/.config/fish/config.fish" ;; *) rc_file="$HOME/.profile" ;; esac if [ "$shell_name" = "fish" ]; then line="set -gx PATH $target_dir \$PATH" else line="export PATH=\"$target_dir:\$PATH\"" fi if [ -f "$rc_file" ] && grep -qF "$target_dir" "$rc_file" 2>/dev/null; then ok "PATH already configured in $rc_file" else printf "\n# Markus — AI Digital Workforce Platform\n%s\n" "$line" >> "$rc_file" ok "Added to PATH via $rc_file" fi export PATH="$target_dir:$PATH" fi } # ─── Post-install: Desktop shortcut ───────────────────────────────────────── create_desktop_shortcut() { local os="$1" markus_cmd="$2" if [ "$os" = "darwin" ]; then local shortcut="$HOME/Desktop/Markus.command" cat > "$shortcut" << SCRIPT #!/bin/bash # Markus — AI Digital Workforce Platform # If the server is already running, just open the browser PORT=8056 if curl -s --max-time 2 -o /dev/null "http://localhost:\$PORT/api/health" 2>/dev/null; then open "http://localhost:\$PORT" exit 0 fi ${markus_cmd} start SCRIPT chmod +x "$shortcut" ok "Desktop shortcut created: Markus.command" elif [ "$os" = "linux" ]; then local desktop_dir="${XDG_DESKTOP_DIR:-$HOME/Desktop}" if [ -d "$desktop_dir" ]; then local shortcut="$desktop_dir/markus.desktop" local wrapper="$HOME/.markus/markus-launch.sh" mkdir -p "$HOME/.markus" cat > "$wrapper" << WRAPPERSCRIPT #!/bin/bash PORT=8056 if curl -s --max-time 2 -o /dev/null "http://localhost:\$PORT/api/health" 2>/dev/null; then xdg-open "http://localhost:\$PORT" 2>/dev/null || true exit 0 fi ${markus_cmd} start WRAPPERSCRIPT chmod +x "$wrapper" cat > "$shortcut" << EOF [Desktop Entry] Type=Application Name=Markus Comment=AI Digital Workforce Platform Exec=${wrapper} Terminal=true Categories=Development; StartupNotify=true EOF chmod +x "$shortcut" ok "Desktop shortcut created: markus.desktop" else warn "Desktop directory not found — skipping shortcut" fi fi } # ─── Post-install: Auto-start on login ────────────────────────────────────── setup_autostart() { local os="$1" markus_cmd="$2" if [ "$os" = "darwin" ]; then local plist_dir="$HOME/Library/LaunchAgents" local plist="$plist_dir/global.markus.plist" mkdir -p "$plist_dir" mkdir -p "$HOME/.markus/logs" cat > "$plist" << EOF Label global.markus ProgramArguments ${markus_cmd} start RunAtLoad KeepAlive StandardOutPath ${HOME}/.markus/logs/stdout.log StandardErrorPath ${HOME}/.markus/logs/stderr.log EOF launchctl unload "$plist" 2>/dev/null || true launchctl load "$plist" 2>/dev/null || true ok "Auto-start on login: enabled (launchd)" elif [ "$os" = "linux" ]; then local autostart_dir="$HOME/.config/autostart" mkdir -p "$autostart_dir" cat > "$autostart_dir/markus.desktop" << EOF [Desktop Entry] Type=Application Name=Markus Exec=${markus_cmd} start Terminal=false X-GNOME-Autostart-enabled=true EOF ok "Auto-start on login: enabled (XDG autostart)" fi } # ─── Main ──────────────────────────────────────────────────────────────────── main() { banner local os arch os="$(detect_os)" arch="$(detect_arch)" info "Detected: $os / $arch" if [ "$os" = "unknown" ]; then error "Unsupported operating system: $(uname -s)" error "Markus supports macOS, Linux, and Windows (PowerShell installer)." exit 1 fi if [ "$arch" = "unknown" ]; then error "Unsupported architecture: $(uname -m)" exit 1 fi # ── Choose install path: npm (lightweight) or binary (standalone) ────── local install_mode="npm" local markus_cmd="markus" info "Checking Node.js..." if check_node; then ok "Node.js $(node -v)" if check_npm; then ok "npm $(npm -v)" printf "\n" if ! npm_install_global "${NPM_PACKAGE}@${VERSION}"; then exit 1 fi else error "npm not found — falling back to binary install" install_mode="binary" fi else if command -v node &>/dev/null; then warn "Node.js $(node -v) is too old (22+ required) — using standalone binary" else info "Node.js not found — using standalone binary (includes runtime)" fi install_mode="binary" fi if [ "$install_mode" = "binary" ]; then printf "\n" if ! binary_install "$os" "$arch"; then exit 1 fi markus_cmd="$INSTALL_DIR/markus" fi printf "\n" # ── PATH registration ───────────────────────────────────────────────── setup_path "$install_mode" # ── Verify installation ─────────────────────────────────────────────── if [ "$install_mode" = "npm" ]; then if ! command -v markus &>/dev/null; then warn "markus command not found in PATH." warn "You may need to add npm global bin to your PATH:" printf " export PATH=\"\$(npm prefix -g)/bin:\$PATH\"\n\n" markus_cmd="npx @markus-global/cli" else ok "markus $(markus --version 2>/dev/null || echo 'installed')" fi else ok "markus installed at $INSTALL_DIR" fi # ── Desktop shortcut ────────────────────────────────────────────────── create_desktop_shortcut "$os" "$markus_cmd" # ── Auto-start on login ─────────────────────────────────────────────── printf "\n" local enable_autostart="y" if [ -t 0 ]; then printf " ${BOLD}Enable auto-start on login? [Y/n]${NC} " read -r enable_autostart < /dev/tty 2>/dev/null || enable_autostart="y" enable_autostart="${enable_autostart:-y}" fi case "$enable_autostart" in [nN]*) info "Auto-start skipped" ;; *) setup_autostart "$os" "$markus_cmd" ;; esac # ── Run init wizard ─────────────────────────────────────────────────── printf "\n" info "Running setup wizard..." printf "\n" $markus_cmd init 2>/dev/null || true # ── Success banner ──────────────────────────────────────────────────── printf "\n" printf "${GREEN}${BOLD}" printf " ┌─────────────────────────────────────┐\n" printf " │ Installation Complete! │\n" printf " └─────────────────────────────────────┘\n" printf "${NC}\n" printf " Quick start:\n\n" if [ "$install_mode" = "binary" ]; then printf " ${BOLD}markus start${NC} Launch the platform\n" printf " ${BOLD}markus agent list${NC} List your agents\n" printf " ${BOLD}markus --help${NC} Show all commands\n" printf "\n" printf " ${DIM}(restart your terminal for PATH changes to take effect)${NC}\n" elif command -v markus &>/dev/null; then printf " ${BOLD}markus start${NC} Launch the platform\n" printf " ${BOLD}markus agent list${NC} List your agents\n" printf " ${BOLD}markus --help${NC} Show all commands\n" else printf " ${BOLD}npx @markus-global/cli start${NC} Launch the platform\n" printf " ${BOLD}npx @markus-global/cli agent list${NC} List your agents\n" printf " ${BOLD}npx @markus-global/cli --help${NC} Show all commands\n" fi printf "\n" printf " ${DIM}Upgrade:${NC} curl -fsSL https://markus.global/install.sh | bash\n" printf " ${DIM}Uninstall:${NC} markus uninstall\n" printf "\n" printf " Documentation: https://github.com/markus-global/markus\n" printf "\n" } main "$@"