#!/usr/bin/env sh set -eu ORIGIN="${RADSUITE_RELEASE_ORIGIN:-https://get.radsuite.com}" CHANNEL="${RADSUITE_RELEASE_CHANNEL:-stable}" PREFIX="${RADSUITE_INSTALL_PREFIX:-$HOME/.rad}" MANIFEST_URL="" DRY_RUN="0" VERIFY_DOWNLOAD="0" WITH_DESKTOP="0" MODIFY_PATH="${RADSUITE_INSTALL_MODIFY_PATH:-1}" SILENT="0" DOCKER_CHANNEL_RUNTIME_IMAGE="" while [ "$#" -gt 0 ]; do case "$1" in --origin) ORIGIN="$2" shift 2 ;; --origin=*) ORIGIN="${1#*=}" shift ;; --channel) CHANNEL="$2" shift 2 ;; --channel=*) CHANNEL="${1#*=}" shift ;; --prefix) PREFIX="$2" shift 2 ;; --prefix=*) PREFIX="${1#*=}" shift ;; --manifest-url|--local-manifest) MANIFEST_URL="$2" shift 2 ;; --manifest-url=*|--local-manifest=*) MANIFEST_URL="${1#*=}" shift ;; --dry-run) DRY_RUN="1" shift ;; --verify-download) VERIFY_DOWNLOAD="1" shift ;; --with-desktop) WITH_DESKTOP="1" shift ;; --no-modify-path) MODIFY_PATH="0" shift ;; --silent) SILENT="1" shift ;; --docker-channel-runtime-image) DOCKER_CHANNEL_RUNTIME_IMAGE="$2" shift 2 ;; --docker-channel-runtime-image=*) DOCKER_CHANNEL_RUNTIME_IMAGE="${1#*=}" shift ;; -h|--help) cat <. By default, the installer appends a managed PATH block to every writable profile for the detected shell. Use --with-desktop to install the optional radsuite-desktop package when present. Use --no-modify-path to skip shell profile edits. Use --silent for non-interactive output; it still installs the managed PATH entry unless --no-modify-path is set. Use --dry-run to fetch metadata and print planned paths without writing files. Use --dry-run --verify-download to also download artifacts and verify checksums. Use --docker-channel-runtime-image to persist the Docker image used by local channel runtime plans. EOF exit 0 ;; *) echo "Unknown option: $1" >&2 exit 1 ;; esac done if [ "$SILENT" = "1" ]; then exec >/dev/null fi ORIGIN="$(printf '%s' "$ORIGIN" | sed 's:/*$::')" if ! command -v curl >/dev/null 2>&1; then echo "Error: curl is required." >&2 exit 1 fi if [ -n "$DOCKER_CHANNEL_RUNTIME_IMAGE" ] && ! printf '%s' "$DOCKER_CHANNEL_RUNTIME_IMAGE" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,255}$'; then echo "Error: Docker channel runtime image is invalid." >&2 exit 1 fi if [ -z "$MANIFEST_URL" ]; then LATEST_URL="$ORIGIN/releases/$CHANNEL/latest.json" LATEST_FILE="$(mktemp)" echo "Fetching RADSuite release pointer: $LATEST_URL" curl -fsSL "$LATEST_URL" -o "$LATEST_FILE" MANIFEST_URL="$(sed -n 's/.*"manifestUrl"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$LATEST_FILE" | head -n 1)" rm -f "$LATEST_FILE" if [ -z "$MANIFEST_URL" ]; then echo "Error: latest.json does not contain manifestUrl." >&2 exit 1 fi fi if ! command -v tar >/dev/null 2>&1; then echo "Error: tar is required." >&2 exit 1 fi case "$(uname -s 2>/dev/null || printf unknown)" in Linux) OS="linux" ;; Darwin) OS="darwin" ;; MINGW*|MSYS*|CYGWIN*) OS="win32" ;; *) OS="unknown" ;; esac case "$(uname -m 2>/dev/null || printf unknown)" in x86_64|amd64) ARCH="x64" ;; arm64|aarch64) ARCH="arm64" ;; *) ARCH="unknown" ;; esac PLATFORM="$OS-$ARCH" if ! command -v sha256sum >/dev/null 2>&1; then if command -v shasum >/dev/null 2>&1; then sha256sum() { shasum -a 256 "$@"; } else echo "Error: sha256sum or shasum is required." >&2 exit 1 fi fi TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT INT TERM MANIFEST_FILE="$TMP_DIR/manifest.json" echo "Fetching RADSuite release manifest: $MANIFEST_URL" case "$MANIFEST_URL" in file://*) MANIFEST_PATH="$(printf '%s' "$MANIFEST_URL" | sed 's|^file://||')" cp "$MANIFEST_PATH" "$MANIFEST_FILE" ;; *) curl -fsSL "$MANIFEST_URL" -o "$MANIFEST_FILE" ;; esac VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$MANIFEST_FILE" | head -n 1)" BASE_URL="$(sed -n 's/.*"baseUrl"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$MANIFEST_FILE" | head -n 1)" if [ -z "$VERSION" ] || [ -z "$BASE_URL" ]; then echo "Error: manifest does not contain version/baseUrl." >&2 exit 1 fi ALL_ARTIFACTS_FILE="$TMP_DIR/all-artifacts.tsv" ARTIFACTS_FILE="$TMP_DIR/artifacts.tsv" awk ' /"kind"[[:space:]]*:/ { kind=$0 gsub(/.*"kind"[[:space:]]*:[[:space:]]*"/, "", kind) gsub(/".*/, "", kind) } /"platform"[[:space:]]*:/ { platform=$0 gsub(/.*"platform"[[:space:]]*:[[:space:]]*"/, "", platform) gsub(/".*/, "", platform) } /"path"[[:space:]]*:/ { artifact_path=$0 gsub(/.*"path"[[:space:]]*:[[:space:]]*"/, "", artifact_path) gsub(/".*/, "", artifact_path) } /"sha256"[[:space:]]*:/ { sha=$0 gsub(/.*"sha256"[[:space:]]*:[[:space:]]*"/, "", sha) gsub(/".*/, "", sha) if (kind != "" && platform != "" && artifact_path != "") { print kind "\t" platform "\t" artifact_path "\t" sha kind="" platform="" artifact_path="" sha="" } } ' "$MANIFEST_FILE" > "$ALL_ARTIFACTS_FILE" awk -F '\t' -v platform="$PLATFORM" -v os="$OS" -v with_desktop="$WITH_DESKTOP" ' ($2 == "any" || $2 == platform || ($1 == "zvault-runtime" && os == "linux" && $2 == "win32-x64")) && ($1 != "desktop-app" || with_desktop == "1") && $1 != "znfs-runtime" { print $1 "\t" $3 "\t" $4 } ' "$ALL_ARTIFACTS_FILE" > "$ARTIFACTS_FILE" # Apple Silicon Colima runs a Linux ARM64 Docker daemon, so its ZNFS payload # is selected independently of the Darwin CLI platform when the experimental # ZNFS transport is explicitly enabled. if [ "$OS" = "darwin" ] && [ "${RADSUITE_ZNFS:-0}" = "1" ]; then awk -F '\t' '$1 == "znfs-runtime" && $2 == "linux-arm64" { print $1 "\t" $3 "\t" $4 }' "$ALL_ARTIFACTS_FILE" >> "$ARTIFACTS_FILE" fi if ! grep -q '^runtime ' "$ARTIFACTS_FILE"; then echo "Error: manifest does not contain a runtime artifact." >&2 exit 1 fi if ! grep -q '^cli-runtime ' "$ARTIFACTS_FILE"; then echo "Error: manifest does not contain a radsuite CLI binary for platform $PLATFORM." >&2 exit 1 fi if ! grep -q '^agent-runtime ' "$ARTIFACTS_FILE"; then echo "Error: manifest does not contain a Gemini agent runtime for platform $PLATFORM." >&2 exit 1 fi if [ "$OS" = "darwin" ] && [ "${RADSUITE_ZNFS:-0}" = "1" ] && ! grep -q '^znfs-runtime ' "$ARTIFACTS_FILE"; then echo "Error: manifest does not contain the required Linux ARM64 ZNFS runtime for Docker channels on macOS." >&2 exit 1 fi BASE_URL="$(printf '%s' "$BASE_URL" | sed 's:/*$::')" INSTALL_ROOT="$PREFIX/packages" VERSION_ROOT="$INSTALL_ROOT/releases/$VERSION" BIN_DIR="$PREFIX/bin" if [ "$DRY_RUN" = "1" ] && [ "$VERIFY_DOWNLOAD" != "1" ]; then cat <&2 exit 1 fi fi done < "$ARTIFACTS_FILE" if [ "$DRY_RUN" = "1" ]; then cat <&2; exit 1 ;; esac case "$artifact_path" in *windows-x64*.zip|*win32-x64*.zip) zvault_platform="windows-x64"; zvault_helper="zvault.exe" ;; *darwin-x64*.tar.gz) zvault_platform="darwin-x64"; zvault_helper="zvault" ;; *darwin-arm64*.tar.gz) zvault_platform="darwin-arm64"; zvault_helper="zvault" ;; *) echo "Error: unsupported zvault development artifact: $artifact_path" >&2; exit 1 ;; esac zvault_destination="$VERSION_ROOT/cli-runtime/vendor/zvault/$zvault_platform" rm -rf "$zvault_destination" mkdir -p "$zvault_destination" case "$archive_file" in *.zip) unzip -q "$archive_file" -d "$zvault_destination" ;; *.tar.gz) tar -xzf "$archive_file" -C "$zvault_destination" ;; esac [ -f "$zvault_destination/$zvault_helper" ] || { echo 'Error: zvault archive layout is invalid.' >&2; exit 1; } [ "$(find "$zvault_destination" -mindepth 1 -maxdepth 1 -type f | wc -l | tr -d ' ')" = "1" ] || { echo 'Error: zvault archive must contain exactly one helper.' >&2; exit 1; } find "$zvault_destination" -mindepth 1 -maxdepth 1 -type l -exec false {} + || { echo 'Error: zvault archive must not contain symlinks.' >&2; exit 1; } if [ "$zvault_helper" != "zvault.exe" ] || [ "$OS" = "linux" ]; then chmod 0755 "$zvault_destination/$zvault_helper" fi ;; desktop-app) mkdir -p "$VERSION_ROOT/desktop-app" tar -xzf "$archive_file" -C "$VERSION_ROOT/desktop-app" ;; znfs-runtime) znfs_stage="$INSTALL_ROOT/znfs/$sha.tmp.$$" znfs_root="$INSTALL_ROOT/znfs/$sha" mkdir -p "$INSTALL_ROOT/znfs" find "$INSTALL_ROOT/znfs" -mindepth 1 -maxdepth 1 -type d ! -name "$sha" -exec rm -rf {} + rm -rf "$znfs_stage" "$znfs_root" mkdir -p "$znfs_stage/runtime" tar -xzf "$archive_file" -C "$znfs_stage/runtime" [ -f "$znfs_stage/runtime/znfs-server" ] && [ -f "$znfs_stage/runtime/manifest.json" ] || { echo "Error: ZNFS artifact layout is invalid." >&2; exit 1; } chmod 0700 "$znfs_stage" "$znfs_stage/runtime" chmod 0755 "$znfs_stage/runtime/znfs-server" mv "$znfs_stage" "$znfs_root" ;; *) mkdir -p "$VERSION_ROOT/artifacts" cp "$archive_file" "$VERSION_ROOT/artifacts/$(basename "$archive_file")" ;; esac done < "$ARTIFACTS_FILE" if [ -f "$VERSION_ROOT/runtime/provider-catalog/provider-model-catalog.json" ]; then catalog_cache_dir="$PREFIX/cache" catalog_cache_file="$catalog_cache_dir/provider-model-catalog-$CHANNEL.json" mkdir -p "$catalog_cache_dir" chmod 0700 "$catalog_cache_dir" catalog_stage_file="$catalog_cache_file.tmp.$$" python3 - "$VERSION_ROOT/runtime/provider-catalog/provider-model-catalog.json" "$catalog_stage_file" "$CHANNEL" <<'PY' import json import sys catalog_path, output_path, channel = sys.argv[1:] with open(catalog_path, encoding='utf-8') as source: catalog = json.load(source) with open(output_path, 'w', encoding='utf-8') as output: json.dump({'schemaVersion': 1, 'channel': channel, 'catalog': catalog}, output, separators=(',', ':')) output.write('\n') PY chmod 0600 "$catalog_stage_file" mv "$catalog_stage_file" "$catalog_cache_file" fi if [ ! -x "$VERSION_ROOT/cli-runtime/bin/radsuite" ] && [ ! -x "$VERSION_ROOT/cli-runtime/bin/radsuite.exe" ]; then echo "Error: staged radsuite CLI runtime is missing an executable." >&2 exit 1 fi for asset in radsuite-cli.js channel-daemon.js; do [ -f "$VERSION_ROOT/cli-runtime/bin/$asset" ] || { echo "Error: staged radsuite CLI runtime is missing $asset." >&2 exit 1 } done BACKUP_ROOT="$VERSION_ROOT.previous" if [ -e "$FINAL_VERSION_ROOT" ] || [ -L "$FINAL_VERSION_ROOT" ]; then mv "$FINAL_VERSION_ROOT" "$BACKUP_ROOT" fi if ! mv "$VERSION_ROOT" "$FINAL_VERSION_ROOT"; then if [ -e "$BACKUP_ROOT" ]; then mv "$BACKUP_ROOT" "$FINAL_VERSION_ROOT" fi exit 1 fi trap cleanup EXIT INT TERM VERSION_ROOT="$FINAL_VERSION_ROOT" rm -rf "$BACKUP_ROOT" mkdir -p "$BIN_DIR" ln -sfn "$VERSION_ROOT" "$INSTALL_ROOT/current" if [ -x "$INSTALL_ROOT/current/cli-runtime/bin/radsuite" ]; then RADSUITE_CLI_EXECUTABLE="$INSTALL_ROOT/current/cli-runtime/bin/radsuite" elif [ -x "$INSTALL_ROOT/current/cli-runtime/bin/radsuite.exe" ]; then RADSUITE_CLI_EXECUTABLE="$INSTALL_ROOT/current/cli-runtime/bin/radsuite.exe" else echo "Error: installed radsuite CLI runtime is missing an executable." >&2 exit 1 fi cat > "$BIN_DIR/radsuite" < "$BIN_DIR/radsuite-uninstall" <&2; exit 1 ;; esac done if [ -x "\$PREFIX/bin/radsuite" ]; then "\$PREFIX/bin/radsuite" stop local >/dev/null 2>&1 || true elif command -v radsuite >/dev/null 2>&1; then radsuite stop local >/dev/null 2>&1 || true fi rm -f "\$PREFIX/bin/radsuite" "\$PREFIX/bin/radsuite-uninstall" if [ "\$PURGE" = "1" ]; then rm -rf "\$PREFIX" elif [ "\$KEEP_CACHE" = "1" ]; then rm -rf "\$PREFIX/packages/current" else rm -rf "\$PREFIX/packages" fi echo "RADSuite uninstalled." EOF chmod +x "$BIN_DIR/radsuite-uninstall" cat > "$INSTALL_ROOT/current-release.json" </dev/null || true)" profile_files="" case "$shell_name" in zsh) profile_files="$HOME/.zshenv $HOME/.zprofile $HOME/.zshrc" ;; bash) profile_files="$HOME/.bash_profile $HOME/.bashrc" ;; *) profile_files="$HOME/.profile" ;; esac updated="" for profile_file in $profile_files; do if [ ! -e "$profile_file" ]; then touch "$profile_file" 2>/dev/null || true; fi if [ -w "$profile_file" ]; then if ! grep -q 'RADSuite CLI PATH' "$profile_file" 2>/dev/null; then cat >> "$profile_file" <&2 exit 1 }