#!/bin/sh # Voltropy CLI Installer # Usage: curl -fsSL https://api.voltropy.com/install | sh # # Environment variables: # VOLT_TOKEN (optional) Your Voltropy access token for auto-configuration # VOLT_VERSION (optional) Version to install, defaults to "latest" # VOLT_INSTALL_DIR (optional) Installation directory override # # Supported platforms: # - macOS (Darwin) ARM64 (Apple Silicon) # - Linux ARM64 and x86_64 set -e PG_VERSION=17.7 PG_BUILD=1 BINARY_PATH="" while [ $# -gt 0 ]; do case "$1" in --binary) BINARY_PATH="$2" shift 2 ;; --version) VOLT_VERSION="$2" shift 2 ;; --install-dir) VOLT_INSTALL_DIR="$2" shift 2 ;; *) shift ;; esac done # Colors (only if terminal supports it) if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color else RED='' GREEN='' YELLOW='' BLUE='' NC='' fi info() { printf "${BLUE}==>${NC} %s\n" "$1" } success() { printf "${GREEN}==>${NC} %s\n" "$1" } warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1" } error() { printf "${RED}Error:${NC} %s\n" "$1" >&2 exit 1 } # VOLT_TOKEN is optional — if provided, auto-configures the client # Detect OS OS=$(uname -s) case "$OS" in Darwin) PLATFORM="darwin" ;; Linux) PLATFORM="linux" ;; *) error "Unsupported operating system: $OS. Only macOS and Linux are supported." ;; esac # Detect architecture ARCH=$(uname -m) case "$ARCH" in arm64|aarch64) ARCH="arm64" ;; x86_64|amd64) if [ "$PLATFORM" = "darwin" ]; then error "VoltCode requires Apple Silicon (M1/M2/M3/M4). Intel Macs are not supported." fi ARCH="x64" ;; *) error "Unsupported architecture: $ARCH" ;; esac info "Detected $PLATFORM $ARCH" # Check for curl if ! command -v curl >/dev/null 2>&1; then error "curl is required but not found." fi # Check for python3 (required for gateway downloads) if [ -z "$BINARY_PATH" ]; then if ! command -v python3 >/dev/null 2>&1; then error "python3 is required but not found. Please install Python 3." fi fi # Default values VERSION="${VOLT_VERSION:-latest}" API_BASE="https://api.voltropy.com" if [ -z "$BINARY_PATH" ]; then info "Fetching download URL for version: $VERSION" # Get download URL from gateway using python3 for JSON handling # Use set +e to capture errors instead of exiting silently set +e RESPONSE=$(python3 -c " import urllib.request import json import sys data = json.dumps({ 'token': '''$VOLT_TOKEN''', 'platform': '$PLATFORM', 'arch': '$ARCH', 'version': '$VERSION' }).encode('utf-8') req = urllib.request.Request( '$API_BASE/v1/bootstrap/download-url', data=data, headers={ 'Content-Type': 'application/json', 'User-Agent': 'volt-installer/1.0' } ) try: with urllib.request.urlopen(req, timeout=30) as resp: result = json.load(resp) if 'error' in result: print('ERROR:' + result['error'], file=sys.stderr) sys.exit(1) print(json.dumps(result)) except urllib.error.HTTPError as e: body = e.read().decode('utf-8', errors='replace') try: err = json.loads(body) print('ERROR:' + err.get('error', str(e.code)), file=sys.stderr) except: print('ERROR:HTTP ' + str(e.code) + ': ' + body[:200], file=sys.stderr) sys.exit(1) except urllib.error.URLError as e: print('ERROR:Network error: ' + str(e.reason), file=sys.stderr) sys.exit(1) except Exception as e: print('ERROR:' + str(e), file=sys.stderr) sys.exit(1) " 2>&1) PYTHON_EXIT=$? set -e # Check for Python execution failure if [ $PYTHON_EXIT -ne 0 ]; then error "Failed to fetch download URL: $RESPONSE" fi # Check for errors in response if echo "$RESPONSE" | grep -q "^ERROR:"; then ERR_MSG=$(echo "$RESPONSE" | sed 's/^ERROR://') error "$ERR_MSG" fi # Parse response fields DOWNLOAD_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])" 2>/dev/null) || true EXPECTED_SHA=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha256'])" 2>/dev/null) || true ACTUAL_VERSION=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])" 2>/dev/null) || true if [ -z "$DOWNLOAD_URL" ]; then error "Failed to get download URL from server. Response: $RESPONSE" fi info "Downloading volt $ACTUAL_VERSION for $PLATFORM/$ARCH..." # Create temp directory TMPDIR=$(mktemp -d) trap "rm -rf '$TMPDIR'" EXIT # Download binary if ! curl -fsSL "$DOWNLOAD_URL" -o "$TMPDIR/volt.tar.gz"; then error "Failed to download binary" fi # Verify SHA256 info "Verifying checksum..." if [ "$PLATFORM" = "darwin" ]; then ACTUAL_SHA=$(shasum -a 256 "$TMPDIR/volt.tar.gz" | cut -d' ' -f1) else ACTUAL_SHA=$(sha256sum "$TMPDIR/volt.tar.gz" | cut -d' ' -f1) fi if [ -n "$EXPECTED_SHA" ] && [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then error "SHA256 checksum mismatch! Expected: $EXPECTED_SHA Got: $ACTUAL_SHA The download may be corrupted. Please try again." fi success "Checksum verified" # Extract info "Extracting..." tar -xzf "$TMPDIR/volt.tar.gz" -C "$TMPDIR" # Find the binary (might be in a subdirectory) VOLT_BIN=$(find "$TMPDIR" -name "volt" -type f | head -1) if [ -z "$VOLT_BIN" ]; then error "Could not find volt binary in archive" fi else if [ ! -f "$BINARY_PATH" ]; then error "Binary not found at $BINARY_PATH" fi VOLT_BIN="$BINARY_PATH" ACTUAL_VERSION="${VERSION:-local}" fi CLI_NAME="volt" # Determine install location if [ -n "$VOLT_INSTALL_DIR" ]; then INSTALL_DIR="$VOLT_INSTALL_DIR" else INSTALL_DIR="$HOME/.voltcode/bin" fi # Check if install dir is inside HOME (no sudo needed) or elsewhere (may need sudo) case "$INSTALL_DIR" in "$HOME"/*|"$HOME") # Inside home directory - create with user permissions if [ ! -d "$INSTALL_DIR" ]; then mkdir -p "$INSTALL_DIR" fi USE_SUDO="" ;; *) # Outside home directory - may need sudo if [ ! -d "$INSTALL_DIR" ]; then if [ -w "$(dirname "$INSTALL_DIR")" ]; then mkdir -p "$INSTALL_DIR" else info "Creating $INSTALL_DIR (requires sudo)..." sudo mkdir -p "$INSTALL_DIR" fi fi if [ -w "$INSTALL_DIR" ]; then USE_SUDO="" else USE_SUDO="sudo" fi ;; esac # Install binary info "Installing to $INSTALL_DIR/$CLI_NAME..." if [ -n "$BINARY_PATH" ]; then $USE_SUDO cp "$VOLT_BIN" "$INSTALL_DIR/$CLI_NAME" else $USE_SUDO mv "$VOLT_BIN" "$INSTALL_DIR/$CLI_NAME" fi $USE_SUDO chmod +x "$INSTALL_DIR/$CLI_NAME" # Remove macOS quarantine attribute (macOS only) if [ "$PLATFORM" = "darwin" ]; then $USE_SUDO xattr -d com.apple.quarantine "$INSTALL_DIR/$CLI_NAME" 2>/dev/null || true fi success "Installed $CLI_NAME $ACTUAL_VERSION" detect_openssl_variant() { # Detect OpenSSL version for Linux PostgreSQL download if command -v openssl >/dev/null 2>&1; then SSL_VERSION=$(openssl version 2>/dev/null || echo "") case "$SSL_VERSION" in *"OpenSSL 1.1"*) echo "ssl1.1" return ;; *"OpenSSL 3.5"*) echo "ssl3.5" return ;; *"OpenSSL 3."*) echo "ssl3" return ;; esac fi # Default to ssl3 for modern systems echo "ssl3" } install_postgres() { XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" PG_ROOT="$XDG_DATA_HOME/voltcode/postgres/$PG_VERSION" PG_BIN="$PG_ROOT/bin" PG_EXE="$PG_BIN/postgres" if [ -x "$PG_EXE" ]; then # Verify it actually runs if "$PG_EXE" --version >/dev/null 2>&1; then info "Embedded PostgreSQL already installed" return 0 else warn "PostgreSQL binary exists but doesn't run, reinstalling..." rm -rf "$PG_ROOT" fi fi PG_TMPDIR=$(mktemp -d) PG_EXTRACT="$PG_TMPDIR/extract" mkdir -p "$PG_EXTRACT" if [ "$PLATFORM" = "darwin" ]; then # macOS: Download from EnterpriseDB (zip format) if ! command -v unzip >/dev/null 2>&1; then error "unzip is required but not found. Please install unzip." fi PG_ARCHIVE="$PG_TMPDIR/postgres.zip" PG_URL="https://get.enterprisedb.com/postgresql/postgresql-${PG_VERSION}-${PG_BUILD}-osx-binaries.zip" info "Downloading embedded PostgreSQL $PG_VERSION (~340MB, please wait)..." if ! curl -fSL --progress-bar "$PG_URL" -o "$PG_ARCHIVE"; then error "Failed to download embedded PostgreSQL from $PG_URL" fi info "Extracting PostgreSQL..." if ! unzip -q "$PG_ARCHIVE" -d "$PG_EXTRACT"; then error "Failed to extract PostgreSQL archive" fi else # Linux: Download from Percona (tar.gz format) PG_ARCHIVE="$PG_TMPDIR/postgres.tar.gz" SSL_VARIANT=$(detect_openssl_variant) PG_MAJOR=$(echo "$PG_VERSION" | cut -d. -f1) # Map architecture for Percona downloads if [ "$ARCH" = "arm64" ]; then PG_ARCH="aarch64" else PG_ARCH="x86_64" fi PG_URL="https://downloads.percona.com/downloads/postgresql-distribution-${PG_MAJOR}/${PG_VERSION}/binary/tarball/percona-postgresql-${PG_VERSION}-${SSL_VARIANT}-linux-${PG_ARCH}.tar.gz" info "Downloading embedded PostgreSQL $PG_VERSION for Linux ($SSL_VARIANT, $PG_ARCH)..." if ! curl -fSL --progress-bar "$PG_URL" -o "$PG_ARCHIVE"; then error "Failed to download embedded PostgreSQL from $PG_URL" fi info "Extracting PostgreSQL..." if ! tar -xzf "$PG_ARCHIVE" -C "$PG_EXTRACT"; then error "Failed to extract PostgreSQL archive" fi fi PG_BIN_PATH=$(find "$PG_EXTRACT" -maxdepth 4 -type f -path "*/bin/postgres" | head -1) if [ -z "$PG_BIN_PATH" ]; then error "Failed to locate PostgreSQL binaries in archive" fi PG_ROOT_SRC=$(dirname "$(dirname "$PG_BIN_PATH")") mkdir -p "$PG_ROOT" cp -R "$PG_ROOT_SRC"/. "$PG_ROOT"/ # Verify the installed binary works # On Linux, we may need to set LD_LIBRARY_PATH for the verification if [ "$PLATFORM" = "linux" ]; then export LD_LIBRARY_PATH="$PG_ROOT/lib:${LD_LIBRARY_PATH:-}" fi if ! "$PG_EXE" --version >/dev/null 2>&1; then rm -rf "$PG_ROOT" if [ "$PLATFORM" = "darwin" ]; then error "PostgreSQL binary was installed but failed to execute. Your macOS version may not be supported." else error "PostgreSQL binary was installed but failed to execute. You may need to install OpenSSL or other dependencies." fi fi # Clean up temp directory rm -rf "$PG_TMPDIR" success "Embedded PostgreSQL installed to $PG_ROOT" } install_postgres if [ -n "$VOLT_TOKEN" ]; then # Create config directory info "Configuring..." mkdir -p "$HOME/.volt" chmod 700 "$HOME/.volt" # Write config file CONFIG_FILE="$HOME/.volt/config.json" # Backup existing config if token differs if [ -f "$CONFIG_FILE" ]; then EXISTING_TOKEN=$(python3 -c " import json try: with open('$CONFIG_FILE') as f: print(json.load(f).get('token', '')) except: print('') " 2>/dev/null || echo "") if [ -n "$EXISTING_TOKEN" ] && [ "$EXISTING_TOKEN" != "$VOLT_TOKEN" ]; then cp "$CONFIG_FILE" "$CONFIG_FILE.bak" warn "Backed up existing config to $CONFIG_FILE.bak" fi fi # Write new config python3 -c " import json config = { 'token': '''$VOLT_TOKEN''', 'api_base': '$API_BASE/v1' } with open('$CONFIG_FILE', 'w') as f: json.dump(config, f, indent=2) f.write('\n') " chmod 600 "$CONFIG_FILE" success "Configuration saved to $CONFIG_FILE" # Create VoltCode provider config (only if it doesn't exist) VOLTCODE_CONFIG_DIR="$HOME/.config/voltcode" VOLTCODE_CONFIG="$VOLTCODE_CONFIG_DIR/voltcode.json" mkdir -p "$VOLTCODE_CONFIG_DIR" if [ -f "$VOLTCODE_CONFIG" ]; then info "VoltCode config already exists at $VOLTCODE_CONFIG, skipping" else python3 -c " import json config = { '\$schema': 'https://opencode.ai/config.json', 'provider': { 'voltropy': { 'npm': '@ai-sdk/openai-compatible', 'name': 'Voltropy', 'options': { 'baseURL': '$API_BASE/v1', 'apiKey': '''$VOLT_TOKEN''' }, 'models': { 'glm-4.7': { 'name': 'Volt 0.1', 'limit': { 'context': 128000, 'output': 8192 } } } } }, 'model': 'voltropy/glm-4.7' } with open('$VOLTCODE_CONFIG', 'w') as f: json.dump(config, f, indent=2) f.write('\n') " chmod 600 "$VOLTCODE_CONFIG" success "VoltCode config saved to $VOLTCODE_CONFIG" fi fi # Add to PATH if needed add_to_path() { SHELL_CONFIG="" # Detect zsh: running in zsh, .zshrc exists, or user's login shell is zsh # (curl | sh runs in sh, so $ZSH_VERSION won't be set even for zsh users) if [ -n "$ZSH_VERSION" ] || [ -f "$HOME/.zshrc" ] || [ "${SHELL##*/}" = "zsh" ]; then SHELL_CONFIG="$HOME/.zshrc" elif [ -f "$HOME/.bashrc" ]; then SHELL_CONFIG="$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then SHELL_CONFIG="$HOME/.bash_profile" fi if [ -n "$SHELL_CONFIG" ]; then # Create the file if it doesn't exist (e.g. fresh Mac with no .zshrc) [ -f "$SHELL_CONFIG" ] || touch "$SHELL_CONFIG" PATH_LINE="export PATH=\"\$HOME/.voltcode/bin:\$PATH\"" if ! grep -q "\.voltcode/bin" "$SHELL_CONFIG" 2>/dev/null; then echo "" >> "$SHELL_CONFIG" echo "# Added by VoltCode installer" >> "$SHELL_CONFIG" echo "$PATH_LINE" >> "$SHELL_CONFIG" info "Added $INSTALL_DIR to PATH in $SHELL_CONFIG" fi fi } if [ "$INSTALL_DIR" = "$HOME/.voltcode/bin" ]; then add_to_path fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" success "VoltCode $ACTUAL_VERSION installed successfully!" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Get started:" echo "" if [ "$INSTALL_DIR" = "$HOME/.voltcode/bin" ]; then if [ "$PLATFORM" = "darwin" ]; then echo " Run: ${BLUE}source ~/.zshrc${NC} (or restart your terminal)" else echo " Run: ${BLUE}source ~/.bashrc${NC} (or restart your terminal)" fi echo " Then: ${BLUE}${CLI_NAME}${NC}" else echo " ${BLUE}${CLI_NAME}${NC}" fi echo ""