#!/bin/sh set -eu : "${HOME:?HOME is not set}" DEFAULT_RECLAW_BASE_URL="https://reclaw.io" DEFAULT_RECLAW_RELEASE_URL="https://reclaw.io/api/cli-release" DEFAULT_RECLAW_INSTALL_SCRIPT_URL="https://reclaw.io/install.sh" DEFAULT_RECLAW_TARBALL_URL="https://reclaw.io/reclaw-cli.tar.gz" DEFAULT_RECLAW_TARBALL_CHECKSUM="f23e43068d341e23418a0127a0fe8d6fdbf0f99f8653172971235b83c7f62943" DEFAULT_RECLAW_SKILL_URL="https://reclaw.io/skills/reclaw_backup_operator" DEFAULT_RECLAW_SKILL_INSTALL_PATH="$HOME/.openclaw/workspace/skills/reclaw_backup_operator/SKILL.md" DEFAULT_RECLAW_SKILL_CHECKSUM="f3b1a02572369d739b0edd3dfd0669d2392b91ab7f7f71883a6c3418b557f801" RECLAW_BASE_URL=${RECLAW_BASE_URL:-$DEFAULT_RECLAW_BASE_URL} RECLAW_RELEASE_URL=${RECLAW_RELEASE_URL:-$DEFAULT_RECLAW_RELEASE_URL} RECLAW_INSTALL_SCRIPT_URL=${RECLAW_INSTALL_SCRIPT_URL:-$DEFAULT_RECLAW_INSTALL_SCRIPT_URL} RECLAW_TARBALL_URL=${RECLAW_TARBALL_URL:-$DEFAULT_RECLAW_TARBALL_URL} RECLAW_TARBALL_CHECKSUM=${RECLAW_TARBALL_CHECKSUM:-$DEFAULT_RECLAW_TARBALL_CHECKSUM} RECLAW_INSTALL_ROOT=${RECLAW_INSTALL_ROOT:-${HOME}/.local/share/reclaw} RECLAW_BIN_DIR=${RECLAW_BIN_DIR:-${HOME}/.local/bin} RECLAW_MIN_NODE_MAJOR=${RECLAW_MIN_NODE_MAJOR:-20} RECLAW_SKIP_SKILL_INSTALL=${RECLAW_SKIP_SKILL_INSTALL:-0} RECLAW_SKILL_URL=${RECLAW_SKILL_URL:-$DEFAULT_RECLAW_SKILL_URL} RECLAW_SKILL_INSTALL_PATH=${RECLAW_SKILL_INSTALL_PATH:-$DEFAULT_RECLAW_SKILL_INSTALL_PATH} RECLAW_SKILL_CHECKSUM=${RECLAW_SKILL_CHECKSUM:-$DEFAULT_RECLAW_SKILL_CHECKSUM} case "$RECLAW_SKILL_INSTALL_PATH" in "~/"*) RECLAW_SKILL_INSTALL_PATH="${HOME}/${RECLAW_SKILL_INSTALL_PATH#~/}" ;; "\$HOME/"*) RECLAW_SKILL_INSTALL_PATH="${HOME}/${RECLAW_SKILL_INSTALL_PATH#\$HOME/}" ;; esac log() { printf '%s\n' "$*" } fail() { printf 'error: %s\n' "$*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" } verify_sha256() { node - "$1" "$2" <<'NODE' const crypto = require("node:crypto"); const fs = require("node:fs"); const [path, expected] = process.argv.slice(2); if (!expected) { console.error("error: missing expected checksum"); process.exit(1); } const actual = crypto.createHash("sha256").update(fs.readFileSync(path)).digest("hex"); if (actual !== expected) { console.error(`error: checksum mismatch for ${path}`); console.error(`expected: ${expected}`); console.error(`actual: ${actual}`); process.exit(1); } NODE } cleanup() { if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then rm -rf "$WORK_DIR" fi } trap cleanup EXIT INT HUP TERM need_cmd curl need_cmd tar need_cmd node NODE_BIN=$(command -v node) NODE_BIN_DIR=$(dirname "$NODE_BIN") NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]") case "$NODE_MAJOR" in ''|*[!0-9]*) fail "unable to determine the installed Node.js version" ;; esac if [ "$NODE_MAJOR" -lt "$RECLAW_MIN_NODE_MAJOR" ]; then fail "Node.js ${RECLAW_MIN_NODE_MAJOR}+ is required (found $(node -v))" fi WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/reclaw-install.XXXXXX") ARCHIVE_PATH=$WORK_DIR/reclaw-cli.tar.gz SKILL_TMP_PATH=$WORK_DIR/reclaw-backup-operator.SKILL.md INSTALL_DIR=$RECLAW_INSTALL_ROOT/libexec STAGING_DIR=$RECLAW_INSTALL_ROOT/libexec.new.$$ LAUNCHER_PATH=$RECLAW_BIN_DIR/reclaw LAUNCHER_TMP=$WORK_DIR/reclaw MANAGED_INSTALL_METADATA_PATH=$STAGING_DIR/managed-install.json mkdir -p "$RECLAW_INSTALL_ROOT" "$RECLAW_BIN_DIR" rm -rf "$STAGING_DIR" mkdir -p "$STAGING_DIR" log "Downloading Reclaw CLI bundle..." curl --fail --location --silent --show-error "$RECLAW_TARBALL_URL" -o "$ARCHIVE_PATH" verify_sha256 "$ARCHIVE_PATH" "$RECLAW_TARBALL_CHECKSUM" log "Extracting CLI bundle..." tar -xzf "$ARCHIVE_PATH" -C "$STAGING_DIR" [ -f "$STAGING_DIR/package.json" ] || fail "downloaded bundle is missing package.json" [ -f "$STAGING_DIR/dist/index.js" ] || fail "downloaded bundle is missing dist/index.js" chmod +x "$STAGING_DIR/dist/index.js" if [ "$RECLAW_SKIP_SKILL_INSTALL" != "1" ]; then log "Downloading Reclaw OpenClaw skill..." curl --fail --location --silent --show-error "$RECLAW_SKILL_URL" -o "$SKILL_TMP_PATH" verify_sha256 "$SKILL_TMP_PATH" "$RECLAW_SKILL_CHECKSUM" fi RECLAW_MANAGED_SKILL_CHECKSUM="" if [ "$RECLAW_SKIP_SKILL_INSTALL" != "1" ]; then RECLAW_MANAGED_SKILL_CHECKSUM=$RECLAW_SKILL_CHECKSUM fi export MANAGED_INSTALL_METADATA_PATH RECLAW_INSTALL_ROOT RECLAW_BIN_DIR LAUNCHER_PATH export RECLAW_BASE_URL RECLAW_RELEASE_URL RECLAW_INSTALL_SCRIPT_URL RECLAW_TARBALL_CHECKSUM RECLAW_SKILL_URL export RECLAW_SKILL_INSTALL_PATH RECLAW_MANAGED_SKILL_CHECKSUM node <<'NODE' const fs = require("node:fs"); const metadata = { version: 1, installRoot: process.env.RECLAW_INSTALL_ROOT, binDir: process.env.RECLAW_BIN_DIR, launcherPath: process.env.LAUNCHER_PATH, baseUrl: process.env.RECLAW_BASE_URL, releaseUrl: process.env.RECLAW_RELEASE_URL, installScriptUrl: process.env.RECLAW_INSTALL_SCRIPT_URL, tarballChecksum: process.env.RECLAW_TARBALL_CHECKSUM, skillUrl: process.env.RECLAW_SKILL_URL, skillInstallPath: process.env.RECLAW_SKILL_INSTALL_PATH, skillChecksum: process.env.RECLAW_MANAGED_SKILL_CHECKSUM || null, }; fs.writeFileSync(process.env.MANAGED_INSTALL_METADATA_PATH, `${JSON.stringify(metadata, null, 2)}\n`); NODE rm -rf "$INSTALL_DIR" mv "$STAGING_DIR" "$INSTALL_DIR" cat >"$LAUNCHER_TMP" <