|
| 1 | +#!/usr/bin/env sh |
| 2 | +# CtrlNode Bridge — Linux/macOS installer |
| 3 | +# Usage: |
| 4 | +# curl -fsSL https://github.com/ctrlnode-ai/ctrlnode/releases/latest/download/install.sh | sh |
| 5 | +# |
| 6 | +# With custom install directory: |
| 7 | +# curl -fsSL https://github.com/ctrlnode-ai/ctrlnode/releases/latest/download/install.sh | sh -s -- --dir ~/.local/bin |
| 8 | + |
| 9 | +set -e |
| 10 | + |
| 11 | +REPO="ctrlnode-ai/ctrlnode" |
| 12 | +BINARY_NAME="ctrlnode" |
| 13 | +# Default to ~/.local/bin when not root (no sudo needed). |
| 14 | +# Pass --dir /usr/local/bin to install system-wide. |
| 15 | +if [ "$(id -u)" -eq 0 ]; then |
| 16 | + DEFAULT_DIR="/usr/local/bin" |
| 17 | +else |
| 18 | + DEFAULT_DIR="$HOME/.local/bin" |
| 19 | +fi |
| 20 | +INSTALL_DIR="$DEFAULT_DIR" |
| 21 | + |
| 22 | +# --- parse args --- |
| 23 | +while [ $# -gt 0 ]; do |
| 24 | + case "$1" in |
| 25 | + --dir) INSTALL_DIR="$2"; shift 2 ;; |
| 26 | + --dir=*) INSTALL_DIR="${1#*=}"; shift ;; |
| 27 | + *) shift ;; |
| 28 | + esac |
| 29 | +done |
| 30 | + |
| 31 | +DEFAULT_WORKSPACE="$HOME" |
| 32 | +WORKSPACE_ROOT="" |
| 33 | + |
| 34 | +echo "" |
| 35 | +echo "CtrlNode Bridge Installer" |
| 36 | +echo "--------------------------" |
| 37 | +echo "" |
| 38 | + |
| 39 | +# --- workspace directory --- |
| 40 | +echo "Where is your workspace?" |
| 41 | +echo " This is the root folder where ctrlnode will read and write files." |
| 42 | +echo " For security, the bridge cannot access anything outside this folder." |
| 43 | +echo " If you are a developer, point this to your source code root (e.g. ~/code)." |
| 44 | +if [ -t 1 ] && [ -e /dev/tty ]; then |
| 45 | + printf "Workspace [%s]: " "$DEFAULT_WORKSPACE" > /dev/tty |
| 46 | + read -r ws_answer < /dev/tty |
| 47 | + WORKSPACE_ROOT="${ws_answer:-$DEFAULT_WORKSPACE}" |
| 48 | +else |
| 49 | + WORKSPACE_ROOT="$DEFAULT_WORKSPACE" |
| 50 | +fi |
| 51 | +echo " Workspace: $WORKSPACE_ROOT" |
| 52 | +echo " Tip: to change it later, set BASE_PATH and restart ctrlnode." |
| 53 | +echo "" |
| 54 | + |
| 55 | +# Persist workspace |
| 56 | +SHELL_RC="" |
| 57 | +if [ -f "$HOME/.bashrc" ]; then SHELL_RC="$HOME/.bashrc" |
| 58 | +elif [ -f "$HOME/.zshrc" ]; then SHELL_RC="$HOME/.zshrc" |
| 59 | +elif [ -f "$HOME/.profile" ]; then SHELL_RC="$HOME/.profile" |
| 60 | +fi |
| 61 | + |
| 62 | +if [ -n "$SHELL_RC" ]; then |
| 63 | + grep -v 'BASE_PATH' "$SHELL_RC" > "${SHELL_RC}.tmp" && mv "${SHELL_RC}.tmp" "$SHELL_RC" |
| 64 | + echo "export BASE_PATH=\"$WORKSPACE_ROOT\"" >> "$SHELL_RC" |
| 65 | +fi |
| 66 | +export BASE_PATH="$WORKSPACE_ROOT" |
| 67 | + |
| 68 | +# --- detect OS and arch --- |
| 69 | +OS="$(uname -s)" |
| 70 | +ARCH="$(uname -m)" |
| 71 | + |
| 72 | +case "$OS" in |
| 73 | + Linux) |
| 74 | + case "$ARCH" in |
| 75 | + x86_64) |
| 76 | + if grep -q avx2 /proc/cpuinfo 2>/dev/null; then |
| 77 | + ASSET="ctrlnode-linux-x64" |
| 78 | + else |
| 79 | + ASSET="ctrlnode-linux-x64-baseline" |
| 80 | + fi |
| 81 | + ;; |
| 82 | + aarch64|arm64) |
| 83 | + echo "ERROR: Linux ARM64 binary not yet available." >&2 |
| 84 | + exit 1 |
| 85 | + ;; |
| 86 | + *) |
| 87 | + echo "ERROR: Unsupported architecture: $ARCH" >&2 |
| 88 | + exit 1 |
| 89 | + ;; |
| 90 | + esac |
| 91 | + ;; |
| 92 | + Darwin) |
| 93 | + case "$ARCH" in |
| 94 | + arm64) ASSET="ctrlnode-darwin-arm64" ;; |
| 95 | + x86_64) |
| 96 | + echo "ERROR: macOS Intel binary not yet available." >&2 |
| 97 | + exit 1 |
| 98 | + ;; |
| 99 | + *) |
| 100 | + echo "ERROR: Unsupported architecture: $ARCH" >&2 |
| 101 | + exit 1 |
| 102 | + ;; |
| 103 | + esac |
| 104 | + ;; |
| 105 | + *) |
| 106 | + echo "ERROR: Unsupported OS: $OS. On Windows use install.ps1 instead." >&2 |
| 107 | + exit 1 |
| 108 | + ;; |
| 109 | +esac |
| 110 | + |
| 111 | +# --- fetch latest release from GitHub --- |
| 112 | +echo "Fetching latest release..." |
| 113 | +if command -v curl >/dev/null 2>&1; then |
| 114 | + TAG="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" |
| 115 | +elif command -v wget >/dev/null 2>&1; then |
| 116 | + TAG="$(wget -qO- "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" |
| 117 | +else |
| 118 | + echo "ERROR: curl or wget required." >&2 |
| 119 | + exit 1 |
| 120 | +fi |
| 121 | + |
| 122 | +if [ -z "$TAG" ]; then |
| 123 | + echo "ERROR: Could not determine latest release tag." >&2 |
| 124 | + exit 1 |
| 125 | +fi |
| 126 | + |
| 127 | +echo " Release: $TAG" |
| 128 | +echo " Asset: $ASSET" |
| 129 | +echo "" |
| 130 | +echo "Downloading..." |
| 131 | + |
| 132 | +DOWNLOAD_URL="https://github.com/$REPO/releases/download/$TAG/$ASSET" |
| 133 | +TMP_FILE="$(mktemp)" |
| 134 | + |
| 135 | +if command -v curl >/dev/null 2>&1; then |
| 136 | + curl -fsSL "$DOWNLOAD_URL" -o "$TMP_FILE" |
| 137 | +else |
| 138 | + wget -qO "$TMP_FILE" "$DOWNLOAD_URL" |
| 139 | +fi |
| 140 | + |
| 141 | +# --- install --- |
| 142 | +DEST="${INSTALL_DIR}/${BINARY_NAME}" |
| 143 | + |
| 144 | +# Stop any running instance before replacing the binary |
| 145 | +if [ -f "$DEST" ]; then |
| 146 | + if command -v pkill >/dev/null 2>&1; then |
| 147 | + pkill -x "$BINARY_NAME" 2>/dev/null && sleep 0.5 || true |
| 148 | + fi |
| 149 | +fi |
| 150 | + |
| 151 | +mkdir -p "$INSTALL_DIR" |
| 152 | +if [ -w "$INSTALL_DIR" ]; then |
| 153 | + cp "$TMP_FILE" "$DEST" |
| 154 | + chmod +x "$DEST" |
| 155 | +else |
| 156 | + echo "Requires sudo to install to $INSTALL_DIR..." |
| 157 | + sudo cp "$TMP_FILE" "$DEST" |
| 158 | + sudo chmod +x "$DEST" |
| 159 | +fi |
| 160 | + |
| 161 | +# macOS: remove quarantine flag |
| 162 | +if [ "$OS" = "Darwin" ]; then |
| 163 | + xattr -d com.apple.quarantine "$DEST" 2>/dev/null || true |
| 164 | +fi |
| 165 | + |
| 166 | +echo "" |
| 167 | +echo "OK Installed: $DEST" |
| 168 | +echo " Version: $TAG" |
| 169 | +echo "" |
| 170 | + |
| 171 | +# Ensure ~/.local/bin is in PATH when using user-local install |
| 172 | +PATH_UPDATED=0 |
| 173 | +if [ "$INSTALL_DIR" = "$HOME/.local/bin" ]; then |
| 174 | + if [ -n "$SHELL_RC" ] && ! grep -q '\.local/bin' "$SHELL_RC" 2>/dev/null; then |
| 175 | + echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_RC" |
| 176 | + PATH_UPDATED=1 |
| 177 | + fi |
| 178 | + export PATH="$HOME/.local/bin:$PATH" |
| 179 | + hash -r 2>/dev/null || true |
| 180 | +fi |
| 181 | + |
| 182 | +# If run as root via sudo, fix ownership of ~/.ctrlnode so the real user can write to it |
| 183 | +if [ "$(id -u)" -eq 0 ] && [ -n "$SUDO_USER" ]; then |
| 184 | + REAL_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6 2>/dev/null || echo "")" |
| 185 | + if [ -n "$REAL_HOME" ] && [ -d "$REAL_HOME/.ctrlnode" ]; then |
| 186 | + chown -R "$SUDO_USER:$SUDO_USER" "$REAL_HOME/.ctrlnode" 2>/dev/null || true |
| 187 | + echo " Fixed ownership of $REAL_HOME/.ctrlnode -> $SUDO_USER" |
| 188 | + fi |
| 189 | +fi |
| 190 | +if [ "$PATH_UPDATED" -eq 1 ]; then |
| 191 | + echo "┌─────────────────────────────────────────────────────┐" |
| 192 | + echo "│ IMPORTANT: reload your shell before running ctrlnode │" |
| 193 | + echo "│ │" |
| 194 | + echo "│ source $SHELL_RC" |
| 195 | + echo "│ │" |
| 196 | + echo "│ Then start the Bridge: ctrlnode │" |
| 197 | + echo "└─────────────────────────────────────────────────────┘" |
| 198 | +else |
| 199 | + echo "Next: start the Bridge:" |
| 200 | + echo " ctrlnode" |
| 201 | +fi |
| 202 | +echo "" |
| 203 | +echo "Workspace: $WORKSPACE_ROOT" |
| 204 | +echo "When you run the Bridge for the first time, it will prompt for your pairing token or read it from a .env file." |
| 205 | +echo "Full setup (token + API keys): ctrlnode --setup" |
| 206 | +echo "Get your token at: https://app.ctrlnode.ai (Settings -> Bridge)" |
| 207 | +echo "" |
| 208 | + |
| 209 | +# --- optional: run the bridge now --- |
| 210 | +if [ -t 1 ] && [ -e /dev/tty ]; then |
| 211 | + printf "Do you want to run ctrlnode now? (Y/n): " > /dev/tty |
| 212 | + read -r run_now < /dev/tty |
| 213 | + case "$run_now" in |
| 214 | + n|N|no|No) |
| 215 | + echo "You can start it later with: ctrlnode" |
| 216 | + ;; |
| 217 | + *) |
| 218 | + echo "Starting ctrlnode..." |
| 219 | + # Redirect stdin from /dev/tty so ctrlnode's readline reads from the |
| 220 | + # terminal, not the curl pipe. Without this, readline captures the |
| 221 | + # prompt text itself as the answer and saves a corrupted PAIRING_TOKEN. |
| 222 | + BASE_PATH="$WORKSPACE_ROOT" "$DEST" < /dev/tty |
| 223 | + ;; |
| 224 | + esac |
| 225 | +fi |
0 commit comments