diff --git a/openclaw/install.sh b/openclaw/install.sh index 0ce903a7..6cdf9dc0 100755 --- a/openclaw/install.sh +++ b/openclaw/install.sh @@ -9,7 +9,7 @@ set -euo pipefail # # Or with options: # curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY # # Direct execution: -# bash install.sh [--non-interactive] [--provider=claude|gemini|openrouter] [--api-key=KEY] +# bash install.sh [--non-interactive] [--upgrade] [--provider=claude|gemini|openrouter] [--api-key=KEY] ############################################################################### # Constants @@ -25,6 +25,7 @@ readonly INSTALLER_VERSION="1.0.0" NON_INTERACTIVE="" CLI_PROVIDER="" CLI_API_KEY="" +UPGRADE_MODE="" while [[ $# -gt 0 ]]; do case "$1" in @@ -32,6 +33,10 @@ while [[ $# -gt 0 ]]; do NON_INTERACTIVE="true" shift ;; + --upgrade) + UPGRADE_MODE="true" + shift + ;; --provider=*) CLI_PROVIDER="${1#--provider=}" shift @@ -124,6 +129,141 @@ read_tty() { read "$@" <&"$TTY_FD" } +############################################################################### +# Global cleanup trap — removes temp directories on unexpected exit +############################################################################### + +CLEANUP_DIRS=() + +register_cleanup_dir() { + CLEANUP_DIRS+=("$1") +} + +cleanup_on_exit() { + local exit_code=$? + for dir in "${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}"; do + if [[ -d "$dir" ]]; then + rm -rf "$dir" + fi + done + if [[ $exit_code -ne 0 ]]; then + echo "" >&2 + error "Installation failed (exit code: ${exit_code})" + error "Any temporary files have been cleaned up." + error "Fix the issue above and re-run the installer." + fi +} + +trap cleanup_on_exit EXIT + +############################################################################### +# Prerequisite checks +############################################################################### + +check_git() { + if command -v git &>/dev/null; then + return 0 + fi + + error "git is not installed" + echo "" >&2 + case "${PLATFORM:-}" in + macos) + error "Install git on macOS with:" + error " xcode-select --install" + error " # or: brew install git" + ;; + linux) + error "Install git on Linux with:" + error " sudo apt install git # Debian/Ubuntu" + error " sudo dnf install git # Fedora/RHEL" + error " sudo pacman -S git # Arch" + ;; + *) + error "Please install git and re-run this installer." + ;; + esac + exit 1 +} + +############################################################################### +# Port conflict detection — check if port 37777 is already in use +############################################################################### + +check_port_37777() { + local port_in_use="" + + # Try lsof first (macOS/Linux) + if command -v lsof &>/dev/null; then + if lsof -i :37777 -sTCP:LISTEN &>/dev/null; then + port_in_use="true" + fi + # Fallback to ss (Linux) + elif command -v ss &>/dev/null; then + if ss -tlnp 2>/dev/null | grep -q ':37777 '; then + port_in_use="true" + fi + # Fallback to curl probe + elif command -v curl &>/dev/null; then + local response + response="$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:37777/api/health" 2>/dev/null)" || true + if [[ "$response" == "200" ]]; then + port_in_use="true" + fi + fi + + if [[ "$port_in_use" == "true" ]]; then + return 0 # port IS in use + fi + return 1 # port is free +} + +############################################################################### +# Upgrade detection — check if claude-mem is already installed +############################################################################### + +is_claude_mem_installed() { + # Check if the plugin directory exists with the worker script + if find_claude_mem_install_dir 2>/dev/null; then + return 0 + fi + return 1 +} + +############################################################################### +# JSON manipulation helper — jq with python3/node fallback +# Usage: ensure_jq_or_fallback [jq_args...] +# For simple read operations, returns the result on stdout. +# For write operations, updates the file in-place. +############################################################################### + +ensure_jq_or_fallback() { + local json_file="$1" + shift + local jq_filter="$1" + shift + # remaining args are passed as jq --arg pairs + + if command -v jq &>/dev/null; then + local tmp_file + tmp_file="$(mktemp)" + jq "$@" "$jq_filter" "$json_file" > "$tmp_file" && mv "$tmp_file" "$json_file" + return $? + fi + + if command -v python3 &>/dev/null; then + # For complex jq filters, fall back to node instead + # Python is used only for simple operations + : + fi + + # Fallback to node (always available — it's a dependency) + # This is a passthrough; callers that need node-specific logic + # should use node -e directly. This function is for jq compatibility. + warn "jq not found — using node for JSON manipulation" + return 1 +} + ############################################################################### # Banner ############################################################################### @@ -399,22 +539,17 @@ check_openclaw() { CLAUDE_MEM_REPO="https://github.com/thedotmack/claude-mem.git" install_plugin() { + # Check for git before attempting clone + check_git + local build_dir build_dir="$(mktemp -d)" - - # Ensure cleanup on exit from this function - cleanup_build_dir() { - if [[ -d "$build_dir" ]]; then - rm -rf "$build_dir" - fi - } - trap cleanup_build_dir EXIT + register_cleanup_dir "$build_dir" info "Cloning claude-mem repository..." if ! git clone --depth 1 "$CLAUDE_MEM_REPO" "$build_dir/claude-mem" 2>&1; then error "Failed to clone claude-mem repository" error "Check your internet connection and try again." - cleanup_build_dir exit 1 fi @@ -425,7 +560,6 @@ install_plugin() { if ! (cd "$plugin_src" && NODE_ENV=development npm install --ignore-scripts 2>&1 && npx tsc 2>&1); then error "Failed to build the claude-mem OpenClaw plugin" error "Make sure Node.js and npm are installed." - cleanup_build_dir exit 1 fi @@ -454,7 +588,6 @@ install_plugin() { if ! node "$OPENCLAW_PATH" plugins install "$installable_dir" 2>&1; then error "Failed to install claude-mem plugin" error "Try manually: node ${OPENCLAW_PATH} plugins install " - cleanup_build_dir exit 1 fi @@ -463,12 +596,9 @@ install_plugin() { if ! node "$OPENCLAW_PATH" plugins enable claude-mem 2>&1; then error "Failed to enable claude-mem plugin" error "Try manually: node ${OPENCLAW_PATH} plugins enable claude-mem" - cleanup_build_dir exit 1 fi - cleanup_build_dir - trap - EXIT success "claude-mem plugin installed and enabled" } @@ -1228,10 +1358,16 @@ main() { info "${COLOR_BOLD}[2/8]${COLOR_RESET} Locating OpenClaw gateway..." check_openclaw - # --- Step 3: Plugin installation --- + # --- Step 3: Plugin installation (skip if upgrading and already installed) --- echo "" info "${COLOR_BOLD}[3/8]${COLOR_RESET} Installing claude-mem plugin..." - install_plugin + + if [[ "$UPGRADE_MODE" == "true" ]] && is_claude_mem_installed; then + success "claude-mem already installed at ${CLAUDE_MEM_INSTALL_DIR}" + info "Upgrade mode: skipping clone/build/register, updating settings only" + else + install_plugin + fi # --- Step 4: Memory slot configuration --- echo "" @@ -1251,11 +1387,24 @@ main() { # --- Step 7: Start worker and verify --- echo "" info "${COLOR_BOLD}[7/8]${COLOR_RESET} Starting worker service..." - if start_worker; then - verify_health || true + + if check_port_37777; then + warn "Port 37777 is already in use (worker may already be running)" + info "Checking if the existing service is healthy..." + if verify_health; then + success "Existing worker is healthy — skipping startup" + else + warn "Port 37777 is occupied but not responding to health checks" + warn "Another process may be using this port. Stop it and re-run the installer," + warn "or change CLAUDE_MEM_WORKER_PORT in ~/.claude-mem/settings.json" + fi else - warn "Worker startup failed — you can start it manually later" - warn " cd ~/.claude/plugins/marketplaces/thedotmack && bun plugin/scripts/worker-service.cjs" + if start_worker; then + verify_health || true + else + warn "Worker startup failed — you can start it manually later" + warn " cd ~/.claude/plugins/marketplaces/thedotmack && bun plugin/scripts/worker-service.cjs" + fi fi # --- Step 8: Observation feed setup (optional) --- diff --git a/openclaw/test-install.sh b/openclaw/test-install.sh index b1aeb0bf..1627f79f 100755 --- a/openclaw/test-install.sh +++ b/openclaw/test-install.sh @@ -1593,6 +1593,375 @@ test_write_settings_via_provider_flag() { test_write_settings_via_provider_flag +############################################################################### +# Test: --upgrade flag parsing +############################################################################### + +echo "" +echo "=== --upgrade flag parsing ===" + +test_upgrade_flag() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--upgrade" + source "$tmp" + rm -f "$tmp" + echo "UPGRADE=$UPGRADE_MODE" + ' 2>/dev/null)" || true + + assert_contains "$result" "UPGRADE=true" "--upgrade sets UPGRADE_MODE=true" +} + +test_upgrade_flag + +test_upgrade_flag_with_provider() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--upgrade" "--provider=gemini" "--api-key=upgrade-key" + source "$tmp" + rm -f "$tmp" + echo "UPGRADE=$UPGRADE_MODE" + echo "PROVIDER=$CLI_PROVIDER" + echo "KEY=$CLI_API_KEY" + ' 2>/dev/null)" || true + + assert_contains "$result" "UPGRADE=true" "--upgrade + --provider: upgrade flag parsed" + assert_contains "$result" "PROVIDER=gemini" "--upgrade + --provider: provider flag parsed" + assert_contains "$result" "KEY=upgrade-key" "--upgrade + --api-key: API key parsed" +} + +test_upgrade_flag_with_provider + +test_upgrade_not_set_by_default() { + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + echo "UPGRADE=${UPGRADE_MODE:-}" + ' 2>/dev/null)" || true + + assert_eq "UPGRADE=" "$result" "UPGRADE_MODE is empty by default" +} + +test_upgrade_not_set_by_default + +############################################################################### +# Test: is_claude_mem_installed() — upgrade detection +############################################################################### + +echo "" +echo "=== is_claude_mem_installed() ===" + +test_is_claude_mem_installed_found() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + # Create the expected directory structure + mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts" + touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs" + + if is_claude_mem_installed; then + test_pass "is_claude_mem_installed returns true when plugin exists" + else + test_fail "is_claude_mem_installed should return true when plugin exists" + fi + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_is_claude_mem_installed_found + +test_is_claude_mem_installed_not_found() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + CLAUDE_MEM_INSTALL_DIR="" + + if is_claude_mem_installed; then + test_fail "is_claude_mem_installed should return false when plugin not found" + else + test_pass "is_claude_mem_installed returns false when plugin not found" + fi + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_is_claude_mem_installed_not_found + +############################################################################### +# Test: check_git() — git availability check +############################################################################### + +echo "" +echo "=== check_git() ===" + +test_check_git_available() { + # git should be available in test environment + if command -v git &>/dev/null; then + local output + output="$(check_git 2>&1)" || true + test_pass "check_git succeeds when git is installed" + else + test_pass "check_git test skipped (git not available)" + fi +} + +test_check_git_available + +test_check_git_not_available() { + # Test that check_git fails gracefully when git is not in PATH + local exit_code=0 + PLATFORM="macos" + bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + check_git + ' >/dev/null 2>&1 || exit_code=$? + + if [[ "$exit_code" -ne 0 ]]; then + test_pass "check_git exits with error when git is missing" + else + test_fail "check_git should exit with error when git is missing" + fi +} + +test_check_git_not_available + +test_check_git_macos_message() { + local output + output="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + PLATFORM="macos" + check_git + ' 2>&1)" || true + + assert_contains "$output" "xcode-select" "check_git suggests xcode-select on macOS" +} + +test_check_git_macos_message + +test_check_git_linux_message() { + local output + output="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + PATH="/nonexistent" + PLATFORM="linux" + check_git + ' 2>&1)" || true + + assert_contains "$output" "apt install git" "check_git suggests apt on Linux" +} + +test_check_git_linux_message + +############################################################################### +# Test: check_port_37777() — port conflict detection +############################################################################### + +echo "" +echo "=== check_port_37777() ===" + +test_check_port_function_exists() { + if declare -f check_port_37777 &>/dev/null; then + test_pass "Function check_port_37777() is defined" + else + test_fail "Function check_port_37777() should be defined" + fi +} + +test_check_port_function_exists + +############################################################################### +# Test: cleanup_on_exit() — global cleanup trap +############################################################################### + +echo "" +echo "=== cleanup_on_exit() ===" + +test_cleanup_trap_functions_exist() { + if declare -f register_cleanup_dir &>/dev/null; then + test_pass "Function register_cleanup_dir() is defined" + else + test_fail "Function register_cleanup_dir() should be defined" + fi + + if declare -f cleanup_on_exit &>/dev/null; then + test_pass "Function cleanup_on_exit() is defined" + else + test_fail "Function cleanup_on_exit() should be defined" + fi +} + +test_cleanup_trap_functions_exist + +test_register_cleanup_dir() { + local test_dir + test_dir="$(mktemp -d)" + + # Save existing cleanup dirs + local saved_dirs=("${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}") + CLEANUP_DIRS=() + + register_cleanup_dir "$test_dir" + + if [[ "${#CLEANUP_DIRS[@]}" -eq 1 ]] && [[ "${CLEANUP_DIRS[0]}" == "$test_dir" ]]; then + test_pass "register_cleanup_dir adds directory to CLEANUP_DIRS" + else + test_fail "register_cleanup_dir should add directory to CLEANUP_DIRS" + fi + + # Restore + CLEANUP_DIRS=("${saved_dirs[@]+"${saved_dirs[@]}"}") + rm -rf "$test_dir" +} + +test_register_cleanup_dir + +############################################################################### +# Test: ensure_jq_or_fallback() — JSON utility function +############################################################################### + +echo "" +echo "=== ensure_jq_or_fallback() ===" + +test_ensure_jq_or_fallback_exists() { + if declare -f ensure_jq_or_fallback &>/dev/null; then + test_pass "Function ensure_jq_or_fallback() is defined" + else + test_fail "Function ensure_jq_or_fallback() should be defined" + fi +} + +test_ensure_jq_or_fallback_exists + +test_ensure_jq_with_jq_available() { + if ! command -v jq &>/dev/null; then + test_pass "ensure_jq jq-path: skipped (jq not installed)" + return 0 + fi + + local tmp_json + tmp_json="$(mktemp)" + echo '{"name": "test", "value": 1}' > "$tmp_json" + + if ensure_jq_or_fallback "$tmp_json" '.name = "updated"'; then + local result + result="$(node -e "const j = JSON.parse(require('fs').readFileSync('${tmp_json}','utf8')); console.log(j.name);")" + assert_eq "updated" "$result" "ensure_jq_or_fallback updates JSON via jq" + else + test_fail "ensure_jq_or_fallback should succeed with jq available" + fi + + rm -f "$tmp_json" +} + +test_ensure_jq_with_jq_available + +############################################################################### +# Test: main() references new functions +############################################################################### + +echo "" +echo "=== main() references new functions ===" + +test_main_calls_check_port() { + if grep -q 'check_port_37777' "$INSTALL_SCRIPT"; then + test_pass "main() calls check_port_37777" + else + test_fail "main() should call check_port_37777" + fi +} + +test_main_calls_check_port + +test_main_calls_is_claude_mem_installed() { + if grep -q 'is_claude_mem_installed' "$INSTALL_SCRIPT"; then + test_pass "main() calls is_claude_mem_installed for upgrade detection" + else + test_fail "main() should call is_claude_mem_installed" + fi +} + +test_main_calls_is_claude_mem_installed + +test_main_references_upgrade_mode() { + if grep -q 'UPGRADE_MODE' "$INSTALL_SCRIPT"; then + test_pass "main() references UPGRADE_MODE" + else + test_fail "main() should reference UPGRADE_MODE" + fi +} + +test_main_references_upgrade_mode + +test_install_plugin_calls_check_git() { + if grep -q 'check_git' "$INSTALL_SCRIPT"; then + test_pass "install_plugin() calls check_git" + else + test_fail "install_plugin() should call check_git" + fi +} + +test_install_plugin_calls_check_git + +test_install_plugin_uses_register_cleanup() { + if grep -q 'register_cleanup_dir' "$INSTALL_SCRIPT"; then + test_pass "install_plugin() uses register_cleanup_dir" + else + test_fail "install_plugin() should use register_cleanup_dir" + fi +} + +test_install_plugin_uses_register_cleanup + +test_usage_comment_includes_upgrade() { + if grep -q '\-\-upgrade' "$INSTALL_SCRIPT"; then + test_pass "Usage comment documents --upgrade flag" + else + test_fail "Usage comment should document --upgrade flag" + fi +} + +test_usage_comment_includes_upgrade + ############################################################################### # Summary ###############################################################################