#!/usr/bin/env bash set -euo pipefail REPOSITORY="TraderAlice/OpenAlice" DEFAULT_BRANCH="master" OPENALICE_INSTALLER_RELEASE_VERSION="${OPENALICE_INSTALLER_RELEASE_VERSION:-0.89.1-beta}" OPENALICE_INSTALLER_UPDATE_CHANNEL="${OPENALICE_INSTALLER_UPDATE_CHANNEL:-stable}" PUBLIC_INSTALLER_URL="https://openalice.ai/install" VERSION="" REF_KIND="" REF_OPTION="" MINIMUM_NODE_VERSION="22.19.0" PI_VERSION="0.83.0" PI_RELEASE_BASE="${OPENALICE_PI_RELEASE_BASE_URL:-https://github.com/earendil-works/pi/releases/download/v${PI_VERSION}}" PI_RELEASE_BASE="${PI_RELEASE_BASE%/}" PI_PACKAGE_SHA256="41f07a3eb41227905ac436ad41d949e4589dcc34c15454d718f85f399b533cb6" PI_LOCK_SHA256="f5cb41dcfc60561ba54490b49c17beecec202900f73eb5f104b34f8b2a79a0af" PI_ASSET_SOURCE_DIR="${OPENALICE_PI_SOURCE_DIR:-}" NPM_BIN="${OPENALICE_NPM_BIN:-npm}" SOURCE_DIR="" RUNTIME_ARCHIVE="${OPENALICE_RUNTIME_ARCHIVE:-}" RUNTIME_ARCHIVE_SHA256="${OPENALICE_RUNTIME_ARCHIVE_SHA256:-}" RUNTIME_CONTENT_IDENTITY="" RUNTIME_PRODUCT_VERSION="" RUNTIME_PLATFORM="" RUNTIME_ARCH="" RUNTIME_AUTO_DOWNLOAD=0 RUNTIME_RELEASE_BASE_URL="" RUNTIME_ARCHIVE_FILE="" RUNTIME_ARCHIVE_SIZE=0 RUNTIME_ARCHIVE_SIZE_LABEL="" MODIFY_PATH=1 ASSUME_YES=0 PLAN_ONLY=0 WITH_RUNTIME_DEPS=0 INSTALL_ROOT="${OPENALICE_INSTALL_DIR:-${HOME}/.openalice}" INSTALL_CONTEXT="${OPENALICE_INSTALL_CONTEXT:-local}" case "$INSTALL_CONTEXT" in local|remote) ;; *) printf 'Error: OPENALICE_INSTALL_CONTEXT must be local or remote.\n' >&2 exit 1 ;; esac BOLD="" DIM="" CYAN="" GREEN="" YELLOW="" RED="" RESET="" STAGING="" LOCK_DIR="" LOCK_HELD=0 TTY_OPEN=0 SHELL_LAUNCHER_TEMP="" CMD_LAUNCHER_TEMP="" PI_SHELL_LAUNCHER_TEMP="" PI_CMD_LAUNCHER_TEMP="" RUNTIME_DEPS_MANAGER="" RUNTIME_DEPS_COMMAND="" RUNTIME_DEPS_BLOCKER="" RUNTIME_MISSING=() ELEVATE=() if [[ -t 1 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then BOLD=$'\033[1m' DIM=$'\033[2m' CYAN=$'\033[36m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RED=$'\033[31m' RESET=$'\033[0m' fi cleanup() { local status=$? [[ -z "$STAGING" ]] || rm -rf "$STAGING" [[ -z "$SHELL_LAUNCHER_TEMP" ]] || rm -f "$SHELL_LAUNCHER_TEMP" [[ -z "$CMD_LAUNCHER_TEMP" ]] || rm -f "$CMD_LAUNCHER_TEMP" [[ -z "$PI_SHELL_LAUNCHER_TEMP" ]] || rm -f "$PI_SHELL_LAUNCHER_TEMP" [[ -z "$PI_CMD_LAUNCHER_TEMP" ]] || rm -f "$PI_CMD_LAUNCHER_TEMP" if [[ "$LOCK_HELD" -eq 1 && -n "$LOCK_DIR" ]]; then rm -rf "$LOCK_DIR" fi if [[ "$TTY_OPEN" -eq 1 ]]; then exec 3>&- 3<&- fi return "$status" } trap cleanup EXIT trap 'exit 130' HUP INT TERM die() { printf '\n%bError:%b %s\n' "${RED}${BOLD}" "$RESET" "$*" >&2 exit 1 } step() { printf '\n%b[%s/6]%b %s\n' "$CYAN" "$1" "$RESET" "$2" } ok() { printf ' %b✓%b %s\n' "$GREEN" "$RESET" "$1" } warn() { printf ' %b!%b %s\n' "$YELLOW" "$RESET" "$1" } usage() { cat <<'EOF' OpenAlice CLI installer Usage: ./install ./install --branch dev ./install --version curl -fsSL https://openalice.ai/install | bash Options: --branch Git branch to install (default: master) --version Git tag or commit to install --install-dir OpenAlice user root (default: ~/.openalice) --source Install CLI files from a local checkout (development) --runtime-archive Install a local platform Runtime archive (development) --runtime-sha256 Require this SHA-256 for --runtime-archive --with-runtime-deps Install missing Linux source-build tools after consent --no-modify-path Do not update the shell profile --plan Show the install plan and exit without changing files -y, --yes Accept the install plan without an interactive prompt -h, --help Show this help EOF } open_tty() { if { exec 3<>/dev/tty; } 2>/dev/null; then TTY_OPEN=1 return 0 fi return 1 } prompt_yes_no() { local message="$1" local reply="" while true; do printf '\n%b%s%b [y/N] ' "$BOLD" "$message" "$RESET" >&3 if ! IFS= read -r reply <&3; then return 2 fi case "$reply" in y|Y|yes|YES|Yes) return 0 ;; ""|n|N|no|NO|No) return 1 ;; *) printf 'Please answer y or n. Press Enter to choose no.\n' >&3 ;; esac done } acquire_install_lock() { local owner="" mkdir -p "$INSTALL_ROOT" LOCK_DIR="$INSTALL_ROOT/.cli-install.lock" if mkdir "$LOCK_DIR" 2>/dev/null; then printf '%s\n' "$$" > "$LOCK_DIR/pid" LOCK_HELD=1 return fi if [[ -r "$LOCK_DIR/pid" ]]; then IFS= read -r owner < "$LOCK_DIR/pid" || owner="" fi if [[ "$owner" =~ ^[0-9]+$ ]] && kill -0 "$owner" 2>/dev/null; then die "Another OpenAlice CLI installer is running (pid $owner)." fi warn "Removing a stale CLI installer lock" rm -rf "$LOCK_DIR" mkdir "$LOCK_DIR" || die "Could not acquire the CLI installer lock at $LOCK_DIR" printf '%s\n' "$$" > "$LOCK_DIR/pid" LOCK_HELD=1 } release_install_lock() { if [[ "$LOCK_HELD" -eq 1 ]]; then rm -rf "$LOCK_DIR" LOCK_HELD=0 fi } content_id() { local root="$1" shift node - "$root" "$@" <<'NODE' const { createHash } = require('node:crypto') const { readFileSync } = require('node:fs') const { join } = require('node:path') const [, , root, ...files] = process.argv const hash = createHash('sha256') for (const file of files) { hash.update(file) hash.update('\0') hash.update(readFileSync(join(root, file))) hash.update('\0') } process.stdout.write(hash.digest('hex').slice(0, 16)) NODE } file_sha256() { node - "$1" <<'NODE' const { createHash } = require('node:crypto') const { createReadStream } = require('node:fs') const hash = createHash('sha256') const input = createReadStream(process.argv[2]) input.on('data', (chunk) => hash.update(chunk)) input.on('end', () => process.stdout.write(hash.digest('hex'))) input.on('error', (error) => { console.error(error.message) process.exitCode = 1 }) NODE } node_version_supported() { node - "$MINIMUM_NODE_VERSION" <<'NODE' const minimum = process.argv[2].split('.').map(Number) const current = process.versions.node.split('.').map(Number) for (let index = 0; index < 3; index += 1) { if (current[index] > minimum[index]) process.exit(0) if (current[index] < minimum[index]) process.exit(1) } NODE } update_path_profile() { local begin_marker="# >>> OpenAlice CLI >>>" local end_marker="# <<< OpenAlice CLI <<<" local temporary="${profile}.openalice.$$" mkdir -p "$(dirname "$profile")" || return 1 touch "$profile" || return 1 awk -v begin="$begin_marker" -v end="$end_marker" -v legacy="$path_line" ' $0 == begin { skipping = 1; next } $0 == end { skipping = 0; next } !skipping && $0 != legacy { print } ' "$profile" > "$temporary" || return 1 if [[ -s "$temporary" ]]; then printf '\n' >> "$temporary" || return 1 fi printf '%s\n%s\n%s\n' "$begin_marker" "$path_line" "$end_marker" >> "$temporary" || return 1 if [[ -L "$profile" ]]; then command cat "$temporary" > "$profile" || return 1 rm -f "$temporary" || return 1 else mv -f "$temporary" "$profile" || return 1 fi } find_openalice_checkout() { node - "$PWD" <<'NODE' const { readFileSync } = require('node:fs') const { dirname, join, resolve } = require('node:path') let current = resolve(process.argv[2]) while (true) { try { const manifest = JSON.parse(readFileSync(join(current, 'package.json'), 'utf8')) if (manifest?.name === 'open-alice' && manifest?.scripts?.['build:server']) { process.stdout.write(current) process.exit(0) } } catch { // Walking up from an arbitrary install directory normally encounters no manifest. } const parent = dirname(current) if (parent === current) process.exit(1) current = parent } NODE } probe_runtime_deps() { RUNTIME_MISSING=() command -v git >/dev/null 2>&1 || RUNTIME_MISSING+=("git") command -v python3 >/dev/null 2>&1 || RUNTIME_MISSING+=("python3") command -v make >/dev/null 2>&1 || RUNTIME_MISSING+=("make") if ! command -v c++ >/dev/null 2>&1 \ && ! command -v g++ >/dev/null 2>&1 \ && ! command -v clang++ >/dev/null 2>&1; then RUNTIME_MISSING+=("cxx") fi } runtime_missing_text() { local dependency local label local output="" for dependency in "${RUNTIME_MISSING[@]}"; do case "$dependency" in git) label="Git" ;; python3) label="Python 3" ;; make) label="make" ;; cxx) label="a C++ compiler" ;; esac [[ -z "$output" ]] || output+=", " output+="$label" done printf '%s' "$output" } resolve_runtime_deps_plan() { RUNTIME_DEPS_MANAGER="" RUNTIME_DEPS_COMMAND="" RUNTIME_DEPS_BLOCKER="" ELEVATE=() [[ ${#RUNTIME_MISSING[@]} -gt 0 ]] || return 0 if [[ "$(uname -s)" != "Linux" ]]; then RUNTIME_DEPS_BLOCKER='Automatic source Runtime prerequisite setup is currently supported on Linux. On macOS, run "xcode-select --install" in a local session, then retry.' return 1 fi if [[ "$EUID" -ne 0 ]]; then if command -v sudo >/dev/null 2>&1; then ELEVATE=(sudo) else RUNTIME_DEPS_BLOCKER="Installing source Runtime prerequisites requires root or sudo on this Linux host." return 1 fi fi local prefix="" [[ ${#ELEVATE[@]} -eq 0 ]] || prefix="sudo " if command -v apt-get >/dev/null 2>&1; then RUNTIME_DEPS_MANAGER="apt-get" RUNTIME_DEPS_COMMAND="${prefix}apt-get update && ${prefix}apt-get install -y git python3 make g++" elif command -v dnf >/dev/null 2>&1; then RUNTIME_DEPS_MANAGER="dnf" RUNTIME_DEPS_COMMAND="${prefix}dnf install -y git python3 make gcc-c++" elif command -v yum >/dev/null 2>&1; then RUNTIME_DEPS_MANAGER="yum" RUNTIME_DEPS_COMMAND="${prefix}yum install -y git python3 make gcc-c++" elif command -v apk >/dev/null 2>&1; then RUNTIME_DEPS_MANAGER="apk" RUNTIME_DEPS_COMMAND="${prefix}apk add --no-cache git python3 make g++" elif command -v pacman >/dev/null 2>&1; then RUNTIME_DEPS_MANAGER="pacman" RUNTIME_DEPS_COMMAND="${prefix}pacman -Sy --needed --noconfirm git python make gcc" else RUNTIME_DEPS_BLOCKER="No supported Linux package manager was found (apt-get, dnf, yum, apk, or pacman)." return 1 fi } install_runtime_deps() { case "$RUNTIME_DEPS_MANAGER" in apt-get) "${ELEVATE[@]}" apt-get update "${ELEVATE[@]}" apt-get install -y git python3 make g++ ;; dnf) "${ELEVATE[@]}" dnf install -y git python3 make gcc-c++ ;; yum) "${ELEVATE[@]}" yum install -y git python3 make gcc-c++ ;; apk) "${ELEVATE[@]}" apk add --no-cache git python3 make g++ ;; pacman) "${ELEVATE[@]}" pacman -Sy --needed --noconfirm git python make gcc ;; *) die "No source Runtime prerequisite install command is available." ;; esac } while [[ $# -gt 0 ]]; do case "$1" in --branch) [[ $# -ge 2 ]] || { echo "openalice install: --branch requires a value" >&2; exit 1; } [[ -z "$REF_OPTION" ]] || die "Use only one of --branch or --version." VERSION="$2" REF_KIND="branch" REF_OPTION="--branch" shift 2 ;; --version) [[ $# -ge 2 ]] || { echo "openalice install: --version requires a value" >&2; exit 1; } [[ -z "$REF_OPTION" ]] || die "Use only one of --branch or --version." VERSION="$2" REF_KIND="version" REF_OPTION="--version" shift 2 ;; --install-dir) [[ $# -ge 2 ]] || { echo "openalice install: --install-dir requires a value" >&2; exit 1; } INSTALL_ROOT="$2" shift 2 ;; --source) [[ $# -ge 2 ]] || { echo "openalice install: --source requires a value" >&2; exit 1; } SOURCE_DIR="$2" shift 2 ;; --runtime-archive) [[ $# -ge 2 ]] || { echo "openalice install: --runtime-archive requires a value" >&2; exit 1; } RUNTIME_ARCHIVE="$2" shift 2 ;; --runtime-sha256) [[ $# -ge 2 ]] || { echo "openalice install: --runtime-sha256 requires a value" >&2; exit 1; } RUNTIME_ARCHIVE_SHA256="$2" shift 2 ;; --no-modify-path) MODIFY_PATH=0 shift ;; --with-runtime-deps) WITH_RUNTIME_DEPS=1 shift ;; --plan) PLAN_ONLY=1 shift ;; -y|--yes) ASSUME_YES=1 shift ;; -h|--help) usage exit 0 ;; *) echo "openalice install: unknown option: $1" >&2 usage >&2 exit 1 ;; esac done if [[ -z "$VERSION" ]]; then if [[ -n "$OPENALICE_INSTALLER_RELEASE_VERSION" ]]; then VERSION="v$OPENALICE_INSTALLER_RELEASE_VERSION" REF_KIND="version" else VERSION="$DEFAULT_BRANCH" REF_KIND="branch" fi fi if [[ ${#VERSION} -gt 128 || "$VERSION" == *".."* || ! "$VERSION" =~ ^[A-Za-z0-9._/-]+$ ]]; then die "Unsupported version/ref: $VERSION" fi DEFAULT_INSTALLER_URL="https://raw.githubusercontent.com/$REPOSITORY/$VERSION/install" if [[ "$REF_KIND" == "branch" && "$VERSION" == "$DEFAULT_BRANCH" ]]; then DEFAULT_INSTALLER_URL="$PUBLIC_INSTALLER_URL" fi INSTALLER_URL="${OPENALICE_INSTALL_URL:-$DEFAULT_INSTALLER_URL}" if [[ -n "${OPENALICE_INSTALL_UPDATE_CHANNEL:-}" ]]; then UPDATE_CHANNEL="$OPENALICE_INSTALL_UPDATE_CHANNEL" elif [[ -n "$REF_OPTION" ]]; then if [[ "$REF_KIND" == "version" ]]; then UPDATE_CHANNEL="pinned" elif [[ "$VERSION" == "$DEFAULT_BRANCH" && "$INSTALLER_URL" == "$PUBLIC_INSTALLER_URL" ]]; then UPDATE_CHANNEL="stable" elif [[ "$VERSION" == "$DEFAULT_BRANCH" ]]; then UPDATE_CHANNEL="custom" else UPDATE_CHANNEL="development" fi elif [[ -n "$OPENALICE_INSTALLER_UPDATE_CHANNEL" ]]; then UPDATE_CHANNEL="$OPENALICE_INSTALLER_UPDATE_CHANNEL" elif [[ "$REF_KIND" == "version" ]]; then UPDATE_CHANNEL="pinned" elif [[ "$VERSION" == "$DEFAULT_BRANCH" && "$INSTALLER_URL" == "$PUBLIC_INSTALLER_URL" ]]; then UPDATE_CHANNEL="stable" elif [[ "$VERSION" == "$DEFAULT_BRANCH" ]]; then UPDATE_CHANNEL="custom" else UPDATE_CHANNEL="development" fi case "$UPDATE_CHANNEL" in stable|pinned|development|custom) ;; *) die "Unsupported OpenAlice update channel: $UPDATE_CHANNEL" ;; esac INSTALL_ROOT="${INSTALL_ROOT/#\~/$HOME}" BIN_DIR="$INSTALL_ROOT/bin" VERSIONS_DIR="$INSTALL_ROOT/cli-versions" SAFE_VERSION="${VERSION//\//_}" if [[ -n "$SOURCE_DIR" ]]; then source_argument="$SOURCE_DIR" if ! SOURCE_DIR="$(cd "$SOURCE_DIR" 2>/dev/null && pwd)"; then die "Local source checkout not found: $source_argument" fi fi if [[ -n "$RUNTIME_ARCHIVE" ]]; then runtime_archive_argument="$RUNTIME_ARCHIVE" runtime_archive_dir="$(dirname "$RUNTIME_ARCHIVE")" runtime_archive_name="$(basename "$RUNTIME_ARCHIVE")" if ! runtime_archive_dir="$(cd "$runtime_archive_dir" 2>/dev/null && pwd)"; then die "Runtime archive directory not found: $runtime_archive_argument" fi RUNTIME_ARCHIVE="$runtime_archive_dir/$runtime_archive_name" [[ -f "$RUNTIME_ARCHIVE" ]] || die "Runtime archive not found: $runtime_archive_argument" fi if [[ -n "$RUNTIME_ARCHIVE_SHA256" && -z "$RUNTIME_ARCHIVE" ]]; then die "--runtime-sha256 requires --runtime-archive." fi if [[ -n "$RUNTIME_ARCHIVE_SHA256" && ! "$RUNTIME_ARCHIVE_SHA256" =~ ^[a-fA-F0-9]{64}$ ]]; then die "--runtime-sha256 must be exactly 64 hexadecimal characters." fi runtime_release_version="$OPENALICE_INSTALLER_RELEASE_VERSION" if [[ -z "$runtime_release_version" && -n "${OPENALICE_EXPECTED_CLI_VERSION:-}" ]]; then runtime_release_version="$OPENALICE_EXPECTED_CLI_VERSION" fi if [[ -z "$RUNTIME_ARCHIVE" && -n "$runtime_release_version" ]]; then RUNTIME_AUTO_DOWNLOAD=1 runtime_release_tag="v$runtime_release_version" RUNTIME_RELEASE_BASE_URL="${OPENALICE_RUNTIME_RELEASE_BASE_URL:-https://github.com/$REPOSITORY/releases/download/$runtime_release_tag}" RUNTIME_RELEASE_BASE_URL="${RUNTIME_RELEASE_BASE_URL%/}" fi path_present=0 case ":$PATH:" in *":$BIN_DIR:"*) path_present=1 ;; esac profile="" path_line="" printf -v quoted_bin '%q' "$BIN_DIR" current_path_command="export PATH=$quoted_bin:\$PATH" if [[ "$MODIFY_PATH" -eq 0 ]]; then shell_setup="Skip (--no-modify-path)" elif [[ "$path_present" -eq 1 ]]; then shell_setup="Already available on PATH" else case "${SHELL:-}" in */zsh) if [[ "$(uname -s)" == "Darwin" ]]; then profile="$HOME/.zprofile"; else profile="$HOME/.zshrc"; fi path_line="export PATH=$quoted_bin:\$PATH" ;; */bash) if [[ "$(uname -s)" == "Darwin" ]]; then profile="$HOME/.bash_profile"; else profile="$HOME/.bashrc"; fi path_line="export PATH=$quoted_bin:\$PATH" ;; */fish) profile="$HOME/.config/fish/config.fish" path_line="fish_add_path $quoted_bin" current_path_command="fish_add_path $quoted_bin" ;; esac if [[ -n "$profile" ]]; then shell_setup="Manage OpenAlice PATH in $profile" else shell_setup="Manual PATH setup (shell profile not detected)" fi fi existing_command="$(command -v openalice 2>/dev/null || true)" install_action="Install a new command" if [[ -e "$BIN_DIR/openalice" || -e "$BIN_DIR/openalice.cmd" ]]; then install_action="Update the installed command" fi printf '\n%bOpenAlice%b\n' "${BOLD}${CYAN}" "$RESET" if [[ "$INSTALL_CONTEXT" == "remote" ]]; then printf '%bRemote Runtime CLI installer%b\n\n' "$BOLD" "$RESET" printf 'Install the matching OpenAlice command and ready-to-use Pi agent on this SSH host.\n' else printf '%bLocal Runtime CLI installer%b\n\n' "$BOLD" "$RESET" printf 'Install the OpenAlice command and its ready-to-use Pi agent, then run locally in your browser.\n' fi printf '%bPi stays inside the OpenAlice install root. System build tools are optional and listed before consent; your checkout, Electron app, credentials, and services otherwise stay untouched.%b\n' "$DIM" "$RESET" step 1 "Checking your system" if ! command -v node >/dev/null 2>&1; then die "Node.js $MINIMUM_NODE_VERSION or newer is required. Install Node.js 22 LTS, then run this installer again." fi if ! node_version_supported; then die "Node.js $MINIMUM_NODE_VERSION or newer is required for OpenAlice and managed Pi (found $(node --version))." fi ok "Node.js $(node --version)" RUNTIME_PLATFORM="$(node -p 'process.platform')" RUNTIME_ARCH="$(node -p 'process.arch')" case "$RUNTIME_PLATFORM-$RUNTIME_ARCH" in darwin-arm64|darwin-x64|linux-arm64|linux-x64) ;; *) RUNTIME_AUTO_DOWNLOAD=0 ;; esac if ! node -e ' const url = new URL(process.argv[1]); if (url.protocol !== "https:" && url.protocol !== "http:") process.exit(1); ' "$INSTALLER_URL" >/dev/null 2>&1; then die "OPENALICE_INSTALL_URL must be an HTTP(S) URL." fi if [[ "$NPM_BIN" == *[[:space:]]* ]]; then die "OPENALICE_NPM_BIN must name one executable without arguments." fi if [[ "$NPM_BIN" == */* ]]; then [[ -x "$NPM_BIN" ]] || die "npm is required to install managed Pi (not executable: $NPM_BIN)." elif ! command -v "$NPM_BIN" >/dev/null 2>&1; then die "npm is required to install managed Pi. Install the complete Node.js 22 LTS distribution, then retry." fi ok "npm is available for managed Pi" if [[ -n "$SOURCE_DIR" ]]; then [[ -f "$SOURCE_DIR/packages/cli/package.json" ]] || die "OpenAlice CLI sources are missing under $SOURCE_DIR" source_cli_version="$(node -e ' const manifest = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); if (manifest.name !== "@traderalice/openalice-cli" || typeof manifest.version !== "string") process.exit(1); process.stdout.write(manifest.version); ' "$SOURCE_DIR/packages/cli/package.json")" \ || die "The local package manifest is not an OpenAlice CLI package" if [[ -n "${OPENALICE_EXPECTED_CLI_VERSION:-}" \ && "$source_cli_version" != "$OPENALICE_EXPECTED_CLI_VERSION" ]]; then die "The downloaded CLI reported $source_cli_version instead of expected release $OPENALICE_EXPECTED_CLI_VERSION" fi ok "Local checkout is readable" install_source="Local checkout: $SOURCE_DIR" else ok "curl is available" BASE_URL="${OPENALICE_INSTALL_BASE_URL:-https://raw.githubusercontent.com/$REPOSITORY/$VERSION/packages/cli}" BASE_URL="${BASE_URL%/}" if [[ -n "${OPENALICE_INSTALL_BASE_URL:-}" ]]; then install_source="Download URL: $BASE_URL" elif [[ "$REF_KIND" == "branch" ]]; then install_source="GitHub branch: $REPOSITORY@$VERSION" else install_source="GitHub ref: $REPOSITORY@$VERSION" fi fi if [[ ( -z "$SOURCE_DIR" || -z "$PI_ASSET_SOURCE_DIR" || "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ) ]] && ! command -v curl >/dev/null 2>&1; then die "curl is required to download the OpenAlice and managed Pi files." fi if [[ "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ]]; then runtime_metadata_file="openalice-runtime-${runtime_release_version}-${RUNTIME_PLATFORM}-${RUNTIME_ARCH}.json" runtime_metadata_json="$(curl --fail --silent --show-error --location --retry 3 --connect-timeout 15 \ "$RUNTIME_RELEASE_BASE_URL/$runtime_metadata_file")" \ || die "Could not download OpenAlice Runtime release metadata" RUNTIME_ARCHIVE_FILE="$(node -e ' const value = JSON.parse(process.argv[1]); const [version, platform, arch] = process.argv.slice(2); if (value.schemaVersion !== 1 || value.productVersion !== version || value.platform !== platform || value.arch !== arch || typeof value.archive?.file !== "string" || !/^openalice-runtime-[0-9A-Za-z.+_-]+-(darwin|linux)-(arm64|x64)-[a-f0-9]{16}\.tar\.gz$/.test(value.archive.file) || !Number.isSafeInteger(value.archive.size) || value.archive.size <= 0 || !/^[a-f0-9]{64}$/.test(value.archive.sha256 ?? "")) process.exit(1); process.stdout.write(value.archive.file); ' "$runtime_metadata_json" "$runtime_release_version" "$RUNTIME_PLATFORM" "$RUNTIME_ARCH")" \ || die "OpenAlice Runtime release metadata is invalid" RUNTIME_ARCHIVE_SHA256="$(node -e ' process.stdout.write(JSON.parse(process.argv[1]).archive.sha256); ' "$runtime_metadata_json")" RUNTIME_ARCHIVE_SIZE="$(node -e ' process.stdout.write(String(JSON.parse(process.argv[1]).archive.size)); ' "$runtime_metadata_json")" RUNTIME_ARCHIVE_SIZE_LABEL="$(node -e ' const bytes = Number(process.argv[1]); const mib = bytes / 1024 / 1024; process.stdout.write(`${mib.toFixed(mib >= 100 ? 0 : 1)} MiB`); ' "$RUNTIME_ARCHIVE_SIZE")" fi if [[ -n "$PI_ASSET_SOURCE_DIR" ]]; then pi_source_argument="$PI_ASSET_SOURCE_DIR" if ! PI_ASSET_SOURCE_DIR="$(cd "$PI_ASSET_SOURCE_DIR" 2>/dev/null && pwd)"; then die "Managed Pi asset directory not found: $pi_source_argument" fi [[ -f "$PI_ASSET_SOURCE_DIR/package.json" ]] || die "Managed Pi package.json is missing under $PI_ASSET_SOURCE_DIR" [[ -f "$PI_ASSET_SOURCE_DIR/package-lock.json" ]] || die "Managed Pi package-lock.json is missing under $PI_ASSET_SOURCE_DIR" pi_install_source="Local verified assets: $PI_ASSET_SOURCE_DIR" else pi_install_source="Pinned release: $PI_RELEASE_BASE" fi probe_runtime_deps runtime_deps_ready=0 [[ ${#RUNTIME_MISSING[@]} -gt 0 ]] || runtime_deps_ready=1 resolve_runtime_deps_plan || true if [[ -n "$RUNTIME_ARCHIVE" || "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ]]; then runtime_deps_ready=1 RUNTIME_MISSING=() RUNTIME_DEPS_BLOCKER="" fi if [[ -n "$RUNTIME_ARCHIVE" || "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ]]; then ok "Prebuilt Runtime selected; source build tools are not needed" elif [[ "$runtime_deps_ready" -eq 1 ]]; then ok "Source Runtime build tools are available" else warn "Source Runtime build tools are missing: $(runtime_missing_text)" fi if [[ "$WITH_RUNTIME_DEPS" -eq 0 && "$PLAN_ONLY" -eq 0 && "$ASSUME_YES" -eq 0 \ && "$runtime_deps_ready" -eq 0 && -z "$RUNTIME_DEPS_BLOCKER" ]]; then if open_tty; then printf '\nThe CLI can prepare a source checkout later, but native Node modules may need these tools.\n' >&3 runtime_choice=0 prompt_yes_no "Also install the missing source Runtime build tools?" || runtime_choice=$? if [[ "$runtime_choice" -eq 0 ]]; then WITH_RUNTIME_DEPS=1 fi fi fi if [[ -n "$RUNTIME_ARCHIVE" || "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ]]; then runtime_setup="Not needed (prebuilt Runtime)" elif [[ "$runtime_deps_ready" -eq 1 ]]; then runtime_setup="Already ready" elif [[ "$WITH_RUNTIME_DEPS" -eq 1 && -z "$RUNTIME_DEPS_BLOCKER" ]]; then runtime_setup="Install $(runtime_missing_text)" elif [[ -n "$RUNTIME_DEPS_BLOCKER" ]]; then runtime_setup="Manual setup required" else runtime_setup="Skip (use --with-runtime-deps)" fi printf '\n%bInstall plan%b\n' "$BOLD" "$RESET" printf ' %-14s %s\n' "Action" "$install_action" printf ' %-14s %s\n' "Source" "$install_source" if [[ "$REF_KIND" == "branch" ]]; then printf ' %-14s %s\n' "Branch" "$VERSION" else printf ' %-14s %s\n' "Version" "$VERSION" fi printf ' %-14s %s\n' "Updates" "$UPDATE_CHANNEL" printf ' %-14s %s\n' "Install root" "$INSTALL_ROOT" printf ' %-14s %s\n' "Command" "$BIN_DIR/openalice" printf ' %-14s %s\n' "Managed agent" "Pi $PI_VERSION -> $BIN_DIR/pi" printf ' %-14s %s\n' "Pi source" "$pi_install_source" printf ' %-14s %s\n' "Pi install" "$NPM_BIN ci --omit=dev --ignore-scripts --no-audit --no-fund" if [[ -n "$RUNTIME_ARCHIVE" ]]; then printf ' %-14s %s\n' "Runtime" "$RUNTIME_ARCHIVE" elif [[ "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ]]; then printf ' %-14s %s\n' "Runtime" "OpenAlice $runtime_release_version ($RUNTIME_PLATFORM-$RUNTIME_ARCH, $RUNTIME_ARCHIVE_SIZE_LABEL)" fi printf ' %-14s %s\n' "Shell setup" "$shell_setup" printf ' %-14s %s\n' "Runtime tools" "$runtime_setup" if [[ "$WITH_RUNTIME_DEPS" -eq 1 && "$runtime_deps_ready" -eq 0 && -n "$RUNTIME_DEPS_COMMAND" ]]; then printf ' %-14s %s\n' "System command" "$RUNTIME_DEPS_COMMAND" elif [[ "$runtime_deps_ready" -eq 0 && -n "$RUNTIME_DEPS_BLOCKER" ]]; then printf ' %-14s %s\n' "Runtime note" "$RUNTIME_DEPS_BLOCKER" fi if [[ -n "$existing_command" && "$existing_command" != "$BIN_DIR/openalice" ]]; then printf ' %-14s %s\n' "PATH warning" "$existing_command currently resolves first" fi if [[ "$PLAN_ONLY" -eq 1 ]]; then if [[ "$WITH_RUNTIME_DEPS" -eq 1 && "$runtime_deps_ready" -eq 0 && -n "$RUNTIME_DEPS_BLOCKER" ]]; then printf '\n%bBlocked:%b %s\n' "${RED}${BOLD}" "$RESET" "$RUNTIME_DEPS_BLOCKER" >&2 exit 1 fi printf '\n%bPlan complete.%b No files were changed.\n\n' "$BOLD" "$RESET" exit 0 fi if [[ "$WITH_RUNTIME_DEPS" -eq 1 && "$runtime_deps_ready" -eq 0 && -n "$RUNTIME_DEPS_BLOCKER" ]]; then die "$RUNTIME_DEPS_BLOCKER" fi if [[ "$ASSUME_YES" -eq 0 ]]; then if ! open_tty; then printf '\n%bNo interactive terminal is available.%b\n' "${RED}${BOLD}" "$RESET" >&2 printf 'Review the install plan above, then re-run with %b--yes%b to approve it.\n' "$BOLD" "$RESET" >&2 exit 2 fi confirmation_status=0 prompt_yes_no "Continue with this install?" || confirmation_status=$? case "$confirmation_status" in 0) ;; 1) printf '\nNo changes made. You can run the installer again whenever you are ready.\n' exit 0 ;; *) printf '\nNo confirmation received. No changes made.\n' >&2 exit 2 ;; esac fi step 2 "Preparing source Runtime tools" if [[ "$WITH_RUNTIME_DEPS" -eq 1 && "$runtime_deps_ready" -eq 0 ]]; then install_runtime_deps probe_runtime_deps if [[ ${#RUNTIME_MISSING[@]} -gt 0 ]]; then die "The package manager finished, but source Runtime build tools are still missing: $(runtime_missing_text)" fi runtime_deps_ready=1 ok "Source Runtime build tools are ready" else printf ' No system packages were changed.\n' fi FILES=( "package.json" "bin/openalice.ts" "bin/openalice.mjs" "src/launch-context.ts" "src/main.ts" "src/managed-source.ts" "src/pi-tui-loader.ts" "src/supervisor-config.ts" "src/supervisor-tui.ts" "src/doctor.mjs" "src/install-layout.mjs" "src/install-source.mjs" "src/lifecycle-command.mjs" "src/lifecycle.mjs" "src/local-start.mjs" "src/logs.mjs" "src/observability-command.mjs" "src/remote.mjs" "src/runtime-deps.mjs" "src/runtime-bundle.mjs" "src/runtime-client.mjs" "src/server.mjs" "src/server-control.mjs" "src/ssh-connect.mjs" "src/uninstall.mjs" "src/update.mjs" ) step 3 "Downloading OpenAlice and managed Pi" acquire_install_lock STAGING="$(mktemp -d "${TMPDIR:-/tmp}/openalice-cli.XXXXXX")" mkdir -p "$STAGING/bin" "$STAGING/src" "$STAGING/managed/pi" if [[ -n "$SOURCE_DIR" ]]; then for file in "${FILES[@]}"; do cp "$SOURCE_DIR/packages/cli/$file" "$STAGING/$file" done else for file in "${FILES[@]}"; do curl --fail --silent --show-error --location --retry 3 --connect-timeout 15 \ "$BASE_URL/$file" --output "$STAGING/$file" done fi ok "CLI files are ready" if [[ -n "$PI_ASSET_SOURCE_DIR" ]]; then cp "$PI_ASSET_SOURCE_DIR/package.json" "$STAGING/managed/pi/package.json" cp "$PI_ASSET_SOURCE_DIR/package-lock.json" "$STAGING/managed/pi/package-lock.json" else curl --fail --silent --show-error --location --retry 3 --connect-timeout 15 \ "$PI_RELEASE_BASE/pi-coding-agent-install-package.json" \ --output "$STAGING/managed/pi/package.json" curl --fail --silent --show-error --location --retry 3 --connect-timeout 15 \ "$PI_RELEASE_BASE/pi-coding-agent-install-package-lock.json" \ --output "$STAGING/managed/pi/package-lock.json" fi [[ "$(file_sha256 "$STAGING/managed/pi/package.json")" == "$PI_PACKAGE_SHA256" ]] \ || die "Managed Pi package.json failed SHA-256 verification" [[ "$(file_sha256 "$STAGING/managed/pi/package-lock.json")" == "$PI_LOCK_SHA256" ]] \ || die "Managed Pi package-lock.json failed SHA-256 verification" ok "Pinned Pi $PI_VERSION assets passed SHA-256 verification" if [[ "$RUNTIME_AUTO_DOWNLOAD" -eq 1 ]]; then RUNTIME_ARCHIVE="$STAGING/$RUNTIME_ARCHIVE_FILE" curl --fail --silent --show-error --location --retry 3 --connect-timeout 15 \ "$RUNTIME_RELEASE_BASE_URL/$RUNTIME_ARCHIVE_FILE" \ --output "$RUNTIME_ARCHIVE" [[ "$(node -p "require('node:fs').statSync(process.argv[1]).size" "$RUNTIME_ARCHIVE")" == "$RUNTIME_ARCHIVE_SIZE" ]] \ || die "OpenAlice Runtime archive size does not match release metadata" fi if [[ -n "$RUNTIME_ARCHIVE" ]]; then runtime_archive_actual_sha256="$(file_sha256 "$RUNTIME_ARCHIVE")" runtime_archive_expected_sha256="$(printf '%s' "$RUNTIME_ARCHIVE_SHA256" | tr 'A-F' 'a-f')" if [[ -n "$RUNTIME_ARCHIVE_SHA256" \ && "$runtime_archive_actual_sha256" != "$runtime_archive_expected_sha256" ]]; then die "OpenAlice Runtime archive failed SHA-256 verification" fi mkdir -p "$STAGING/managed/runtime" if ! tar -tzf "$RUNTIME_ARCHIVE" | node -e ' let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { const entries = input.split(/\r?\n/).filter(Boolean); if (entries.length === 0) process.exit(1); for (const entry of entries) { const normalized = entry.replace(/^\.\//, ""); if (normalized !== "openalice-runtime" && normalized !== "openalice-runtime/" && !normalized.startsWith("openalice-runtime/")) process.exit(1); if (normalized.split("/").includes("..") || normalized.startsWith("/")) process.exit(1); } }); '; then die "OpenAlice Runtime archive contains an unsafe or unexpected path" fi tar -xzf "$RUNTIME_ARCHIVE" \ -C "$STAGING/managed/runtime" \ --strip-components 1 runtime_manifest_json="$(node "$STAGING/src/runtime-bundle.mjs" verify \ "$STAGING/managed/runtime" \ --platform "$RUNTIME_PLATFORM" \ --arch "$RUNTIME_ARCH")" \ || die "OpenAlice Runtime bundle verification failed" RUNTIME_CONTENT_IDENTITY="$(node -e ' const value = JSON.parse(process.argv[1]); if (!/^[a-f0-9]{16}$/.test(value.contentIdentity ?? "")) process.exit(1); process.stdout.write(value.contentIdentity); ' "$runtime_manifest_json")" \ || die "OpenAlice Runtime bundle did not report a valid content identity" RUNTIME_PRODUCT_VERSION="$(node -e ' const value = JSON.parse(process.argv[1]); if (typeof value.productVersion !== "string") process.exit(1); process.stdout.write(value.productVersion); ' "$runtime_manifest_json")" \ || die "OpenAlice Runtime bundle did not report a product version" ok "OpenAlice Runtime $RUNTIME_PLATFORM-$RUNTIME_ARCH passed verification ($RUNTIME_CONTENT_IDENTITY)" fi step 4 "Installing the managed Pi runtime" ( cd "$STAGING/managed/pi" npm_config_update_notifier=false "$NPM_BIN" ci \ --omit=dev \ --ignore-scripts \ --no-audit \ --no-fund ) PI_RELATIVE_ENTRY="managed/pi/node_modules/@earendil-works/pi-coding-agent/dist/cli.js" PI_STAGED_ENTRY="$STAGING/$PI_RELATIVE_ENTRY" [[ -f "$PI_STAGED_ENTRY" ]] || die "npm completed without the managed Pi CLI" node -e ' const { createRequire } = require("node:module"); createRequire(process.argv[1]).resolve("@earendil-works/pi-tui"); ' "$PI_STAGED_ENTRY" \ || die "npm completed without the managed Pi TUI dependency" PI_STAGED_VERSION="$(node "$PI_STAGED_ENTRY" --version)" [[ "$PI_STAGED_VERSION" == "$PI_VERSION" ]] || die "Managed Pi reported $PI_STAGED_VERSION instead of $PI_VERSION" ok "Managed Pi $PI_VERSION is ready" step 5 "Verifying the OpenAlice CLI" chmod +x "$STAGING/bin/openalice.ts" chmod +x "$STAGING/bin/openalice.mjs" node --check "$STAGING/bin/openalice.ts" node --check "$STAGING/bin/openalice.mjs" node --check "$STAGING/src/managed-source.ts" node --check "$STAGING/src/supervisor-config.ts" node --check "$STAGING/src/doctor.mjs" node --check "$STAGING/src/install-layout.mjs" node --check "$STAGING/src/install-source.mjs" node --check "$STAGING/src/lifecycle-command.mjs" node --check "$STAGING/src/lifecycle.mjs" node --check "$STAGING/src/local-start.mjs" node --check "$STAGING/src/logs.mjs" node --check "$STAGING/src/observability-command.mjs" node --check "$STAGING/src/remote.mjs" node --check "$STAGING/src/runtime-deps.mjs" node --check "$STAGING/src/runtime-bundle.mjs" node --check "$STAGING/src/runtime-client.mjs" node --check "$STAGING/src/server.mjs" node --check "$STAGING/src/server-control.mjs" node --check "$STAGING/src/ssh-connect.mjs" node --check "$STAGING/src/uninstall.mjs" node --check "$STAGING/src/update.mjs" CLI_VERSION="$(node -e ' const manifest = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); if (manifest.name !== "@traderalice/openalice-cli" || typeof manifest.version !== "string") process.exit(1); process.stdout.write(manifest.version); ' "$STAGING/package.json")" || die "The downloaded package manifest is not an OpenAlice CLI package" if [[ -n "${OPENALICE_EXPECTED_CLI_VERSION:-}" \ && "$CLI_VERSION" != "$OPENALICE_EXPECTED_CLI_VERSION" ]]; then die "The downloaded CLI reported $CLI_VERSION instead of expected release $OPENALICE_EXPECTED_CLI_VERSION" fi if [[ -n "$RUNTIME_PRODUCT_VERSION" && "$RUNTIME_PRODUCT_VERSION" != "$CLI_VERSION" ]]; then die "The Runtime bundle reports OpenAlice $RUNTIME_PRODUCT_VERSION, but the CLI reports $CLI_VERSION" fi STAGED_VERSION="$(node "$STAGING/bin/openalice.ts" --version)" [[ "$STAGED_VERSION" == "$CLI_VERSION" ]] || die "The downloaded CLI did not report its package version" node -e ' const { writeFileSync } = require("node:fs"); const [path, repository, cliVersion, kind, value, installerUrl, updateChannel] = process.argv.slice(1); writeFileSync(path, `${JSON.stringify({ schemaVersion: 2, repository, cliVersion, selector: { kind, value }, installerUrl, updateChannel, }, null, 2)}\n`, { mode: 0o644 }); ' "$STAGING/install-source.json" "$REPOSITORY" "$CLI_VERSION" "$REF_KIND" "$VERSION" "$INSTALLER_URL" "$UPDATE_CHANNEL" node "$STAGING/bin/openalice.ts" version --json | node -e ' let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { const [kind, value, installerUrl, cliVersion, updateChannel] = process.argv.slice(1); const info = JSON.parse(input); if (info.version !== cliVersion || info.installSource?.schemaVersion !== 2 || info.installSource?.cliVersion !== cliVersion || info.installSource?.selector?.kind !== kind || info.installSource?.selector?.value !== value || info.installSource?.installerUrl !== installerUrl || info.installSource?.updateChannel !== updateChannel) process.exit(1); }); ' "$REF_KIND" "$VERSION" "$INSTALLER_URL" "$CLI_VERSION" "$UPDATE_CHANNEL" \ || die "The downloaded CLI did not preserve its installation source" CONTENT_FILES=("${FILES[@]}" "install-source.json" "managed/pi/package.json" "managed/pi/package-lock.json") # Keep the CLI identity stable across operating systems. The platform-specific # Runtime has its own manifest identity and is verified independently below. CONTENT_ID="$(content_id "$STAGING" "${CONTENT_FILES[@]}")" ok "OpenAlice CLI $CLI_VERSION passed validation ($CONTENT_ID)" step 6 "Installing the commands" mkdir -p "$BIN_DIR" "$VERSIONS_DIR" DESTINATION="$VERSIONS_DIR/${SAFE_VERSION}-${CONTENT_ID}" if [[ -e "$DESTINATION" ]]; then destination_pi_version="$(node "$DESTINATION/$PI_RELATIVE_ENTRY" --version 2>/dev/null || true)" destination_pi_tui_ready=0 if node -e ' const { createRequire } = require("node:module"); createRequire(process.argv[1]).resolve("@earendil-works/pi-tui"); ' "$DESTINATION/$PI_RELATIVE_ENTRY" >/dev/null 2>&1; then destination_pi_tui_ready=1 fi if [[ -d "$DESTINATION" \ && "$(content_id "$DESTINATION" "${CONTENT_FILES[@]}" 2>/dev/null || true)" == "$CONTENT_ID" \ && "$destination_pi_version" == "$PI_VERSION" \ && "$destination_pi_tui_ready" -eq 1 \ && ( -z "$RUNTIME_CONTENT_IDENTITY" \ || "$(node "$DESTINATION/src/runtime-bundle.mjs" verify \ "$DESTINATION/managed/runtime" \ --platform "$RUNTIME_PLATFORM" \ --arch "$RUNTIME_ARCH" 2>/dev/null \ | node -e ' let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { try { process.stdout.write(JSON.parse(input).contentIdentity ?? ""); } catch {} }); ')" == "$RUNTIME_CONTENT_IDENTITY" ) ]]; then rm -rf "$STAGING" STAGING="" ok "Reusing the already-verified OpenAlice and Pi release" else damaged_destination="${DESTINATION}.damaged.$$" warn "Preserving a damaged prior release at $damaged_destination" mv "$DESTINATION" "$damaged_destination" mv "$STAGING" "$DESTINATION" STAGING="" fi else mv "$STAGING" "$DESTINATION" STAGING="" fi CLI_ENTRY="$DESTINATION/bin/openalice.ts" PI_ENTRY="$DESTINATION/$PI_RELATIVE_ENTRY" RUNTIME_ENTRY="$DESTINATION/managed/runtime" printf -v quoted_cli_entry '%q' "$CLI_ENTRY" printf -v quoted_pi_entry '%q' "$PI_ENTRY" printf -v quoted_runtime_entry '%q' "$RUNTIME_ENTRY" SHELL_LAUNCHER_TEMP="$BIN_DIR/.openalice.$$.tmp" CMD_LAUNCHER_TEMP="$BIN_DIR/.openalice.$$.cmd.tmp" PI_SHELL_LAUNCHER_TEMP="$BIN_DIR/.pi.$$.tmp" PI_CMD_LAUNCHER_TEMP="$BIN_DIR/.pi.$$.cmd.tmp" if [[ -n "$RUNTIME_CONTENT_IDENTITY" ]]; then printf '#!/usr/bin/env bash\n# OpenAlice CLI launcher with managed Pi and Runtime\nnode_binary="$(command -v node)" || { printf "OpenAlice requires Node.js.\\n" >&2; exit 1; }\nexport OPENALICE_MANAGED_PI_PATH=%s\nexport OPENALICE_MANAGED_PI_NODE_PATH="$node_binary"\nexport OPENALICE_MANAGED_RUNTIME_PATH=%s\nexport OPENALICE_MANAGED_RUNTIME_CONTENT_IDENTITY=%q\nexec "$node_binary" %s "$@"\n' \ "$quoted_pi_entry" "$quoted_runtime_entry" "$RUNTIME_CONTENT_IDENTITY" "$quoted_cli_entry" > "$SHELL_LAUNCHER_TEMP" else printf '#!/usr/bin/env bash\n# OpenAlice CLI launcher with managed Pi\nnode_binary="$(command -v node)" || { printf "OpenAlice requires Node.js.\\n" >&2; exit 1; }\nexport OPENALICE_MANAGED_PI_PATH=%s\nexport OPENALICE_MANAGED_PI_NODE_PATH="$node_binary"\nexec "$node_binary" %s "$@"\n' \ "$quoted_pi_entry" "$quoted_cli_entry" > "$SHELL_LAUNCHER_TEMP" fi chmod +x "$SHELL_LAUNCHER_TEMP" printf '#!/usr/bin/env bash\n# OpenAlice-managed Pi launcher\nnode_binary="$(command -v node)" || { printf "Pi requires Node.js.\\n" >&2; exit 1; }\nexec "$node_binary" %s "$@"\n' \ "$quoted_pi_entry" > "$PI_SHELL_LAUNCHER_TEMP" chmod +x "$PI_SHELL_LAUNCHER_TEMP" WINDOWS_CLI_ENTRY="$CLI_ENTRY" WINDOWS_PI_ENTRY="$PI_ENTRY" if command -v cygpath >/dev/null 2>&1; then WINDOWS_CLI_ENTRY="$(cygpath -w "$CLI_ENTRY")" WINDOWS_PI_ENTRY="$(cygpath -w "$PI_ENTRY")" fi WINDOWS_CLI_ENTRY="${WINDOWS_CLI_ENTRY//%/%%}" WINDOWS_PI_ENTRY="${WINDOWS_PI_ENTRY//%/%%}" printf '@echo off\r\nrem OpenAlice CLI launcher with managed Pi\r\nwhere node >nul 2>nul || (echo OpenAlice requires Node.js. 1>&2 & exit /b 1)\r\nfor %%%%I in (node.exe) do set "OPENALICE_MANAGED_PI_NODE_PATH=%%%%~$PATH:I"\r\nset "OPENALICE_MANAGED_PI_PATH=%s"\r\n"%%OPENALICE_MANAGED_PI_NODE_PATH%%" "%s" %%*\r\n' \ "$WINDOWS_PI_ENTRY" "$WINDOWS_CLI_ENTRY" > "$CMD_LAUNCHER_TEMP" printf '@echo off\r\nrem OpenAlice-managed Pi launcher\r\nnode "%s" %%*\r\n' \ "$WINDOWS_PI_ENTRY" > "$PI_CMD_LAUNCHER_TEMP" LAUNCHER_VERSION="$("$SHELL_LAUNCHER_TEMP" --version)" [[ "$LAUNCHER_VERSION" == "$CLI_VERSION" ]] || die "The staged OpenAlice launcher is not runnable" PI_LAUNCHER_VERSION="$("$PI_SHELL_LAUNCHER_TEMP" --version)" [[ "$PI_LAUNCHER_VERSION" == "$PI_VERSION" ]] || die "The staged managed Pi launcher is not runnable" # Every launcher points to an immutable, validated release before any visible # command is replaced. A failed update therefore leaves each visible launcher # pointing to a complete old or new release rather than a half-written tree. mv -f "$PI_CMD_LAUNCHER_TEMP" "$BIN_DIR/pi.cmd" PI_CMD_LAUNCHER_TEMP="" mv -f "$CMD_LAUNCHER_TEMP" "$BIN_DIR/openalice.cmd" CMD_LAUNCHER_TEMP="" mv -f "$PI_SHELL_LAUNCHER_TEMP" "$BIN_DIR/pi" PI_SHELL_LAUNCHER_TEMP="" mv -f "$SHELL_LAUNCHER_TEMP" "$BIN_DIR/openalice" SHELL_LAUNCHER_TEMP="" path_updated=0 if [[ "$MODIFY_PATH" -eq 1 && "$path_present" -eq 0 && -n "$profile" ]]; then if update_path_profile; then path_updated=1 else rm -f "${profile}.openalice.$$" warn "Could not update $profile; the command is installed and can be added to PATH manually" fi fi INSTALLED_VERSION="$("$BIN_DIR/openalice" --version)" [[ "$INSTALLED_VERSION" == "$CLI_VERSION" ]] || die "The installed OpenAlice command failed its version check" INSTALLED_PI_VERSION="$("$BIN_DIR/pi" --version)" [[ "$INSTALLED_PI_VERSION" == "$PI_VERSION" ]] || die "The installed Pi command failed its version check" ok "Installed and verified $BIN_DIR/openalice" ok "Installed and verified $BIN_DIR/pi" release_install_lock printf '\n%bOpenAlice and Pi are ready.%b\n' "${GREEN}${BOLD}" "$RESET" printf ' %-12s %s\n' "Command" "$BIN_DIR/openalice" printf ' %-12s %s\n' "CLI version" "$CLI_VERSION" printf ' %-12s %s\n' "Agent" "$BIN_DIR/pi" printf ' %-12s %s\n' "Pi version" "$PI_VERSION" if [[ -n "$RUNTIME_CONTENT_IDENTITY" ]]; then printf ' %-12s %s\n' "Runtime" "$RUNTIME_PRODUCT_VERSION ($RUNTIME_PLATFORM-$RUNTIME_ARCH)" printf ' %-12s %s\n' "Runtime ID" "$RUNTIME_CONTENT_IDENTITY" fi if [[ "$REF_KIND" == "branch" ]]; then printf ' %-12s %s\n' "Branch" "$VERSION" else printf ' %-12s %s\n' "Source ref" "$VERSION" fi if [[ "$path_updated" -eq 1 ]]; then printf ' %-12s %s\n' "Shell" "Future terminals will load the managed block in $profile" fi if [[ "$path_present" -eq 0 ]]; then printf '\nActivate OpenAlice in this terminal now (no restart required):\n %s\n' "$current_path_command" fi if [[ -n "$existing_command" && "$existing_command" != "$BIN_DIR/openalice" ]]; then printf '\n%bPATH note:%b this terminal still resolves %s first.\n' "$YELLOW" "$RESET" "$existing_command" printf 'Use %s now, or apply the PATH command above.\n' "$BIN_DIR/openalice" fi checkout="" if [[ -n "$RUNTIME_CONTENT_IDENTITY" && "$INSTALL_CONTEXT" != "remote" ]]; then printf '\n%bReady to launch%b\n' "$BOLD" "$RESET" printf ' Local UI Starts on an available loopback port\n' printf ' Supervisor Starts and manages the installed Runtime\n' if [[ "$TTY_OPEN" -eq 1 ]]; then start_status=0 prompt_yes_no "Open the OpenAlice Supervisor now?" || start_status=$? if [[ "$start_status" -eq 0 ]]; then exec 3>&- 3<&- TTY_OPEN=0 printf '\nOpening the OpenAlice Supervisor...\n\n' exec "$BIN_DIR/openalice" fi fi printf '\nStart it when you are ready from any directory:\n %s\n\n' "$BIN_DIR/openalice" elif [[ "$INSTALL_CONTEXT" == "remote" ]]; then printf '\nReturning to the approved remote setup plan.\n\n' else checkout="$(find_openalice_checkout 2>/dev/null || true)" fi if [[ -z "$RUNTIME_CONTENT_IDENTITY" && -n "$checkout" ]]; then printf '\n%bReady to launch%b\n' "$BOLD" "$RESET" printf ' Checkout %s\n' "$checkout" printf ' Local UI http://127.0.0.1:47331\n' printf ' Supervisor Starts and manages a persistent background Runtime\n' if [[ "$TTY_OPEN" -eq 1 ]]; then start_status=0 prompt_yes_no "Open the OpenAlice Supervisor now?" || start_status=$? if [[ "$start_status" -eq 0 ]]; then exec 3>&- 3<&- TTY_OPEN=0 printf '\nOpening the OpenAlice Supervisor...\n\n' cd "$checkout" exec "$BIN_DIR/openalice" fi fi printf '\nStart it when you are ready:\n cd %q\n %s\n\n' "$checkout" "$BIN_DIR/openalice" elif [[ -z "$RUNTIME_CONTENT_IDENTITY" && "$INSTALL_CONTEXT" != "remote" ]]; then printf '\n%bNext: open the OpenAlice Supervisor%b\n' "$BOLD" "$RESET" printf ' %s\n\n' "$BIN_DIR/openalice" printf 'Press %bEnter%b inside the Supervisor to prepare, start, and open OpenAlice.\n' "$BOLD" "$RESET" printf 'Use %bc%b only if you want to select an existing checkout instead.\n\n' "$BOLD" "$RESET" fi