diff --git a/.gitattributes b/.gitattributes index bf90a46..faad646 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,7 @@ -*.ps1 -text diff -*.sh -text diff -*.cmd -text diff +# Shell scripts MUST be LF: they run on Linux/macOS/WSL and are served raw via +# `curl | bash`. CRLF here makes bash choke on the trailing \r. +*.sh text eol=lf + +# Windows scripts keep CRLF. +*.ps1 text eol=crlf diff +*.cmd text eol=crlf diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..32ce0d0 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + shell: + name: Shell scripts parse + shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Regression guard: install.sh once shipped unparseable (broken line + # continuations). `bash -n` catches that class before it merges. + - name: bash -n (syntax check) + run: | + bash -n install.sh + bash -n cli/clawde.sh + + - name: shellcheck + run: | + sudo apt-get update && sudo apt-get install -y shellcheck + shellcheck --severity=error install.sh cli/clawde.sh diff --git a/cli/clawde.sh b/cli/clawde.sh index 963f5bc..c0e30e7 100644 --- a/cli/clawde.sh +++ b/cli/clawde.sh @@ -1,473 +1,484 @@ -#!/usr/bin/env bash -# clawde.sh - unified CLI for managing OpenCode + CCProxy (Linux/WSL) -# -# Commands: -# start - start CCProxy + OpenCode -# stop - stop both services -# status - health check both services -# config - view or edit configuration -# auth - re-authenticate Claude -# update - update to latest versions -# logs - tail logs from either service - -set -euo pipefail - -# --- Paths --- -CONFIG_DIR="${HOME}/.config/clawde" -DATA_DIR="${HOME}/.local/share/clawde" -BIN_DIR="${HOME}/.local/bin" -LOG_DIR="${DATA_DIR}/logs" -PID_DIR="${DATA_DIR}/pids" -CONFIG_FILE="${CONFIG_DIR}/clawde.toml" - -# --- Ensure dirs exist --- -mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$BIN_DIR" "$LOG_DIR" "$PID_DIR" - -# --- Helpers --- - -read_config() { - if [[ ! -f "$CONFIG_FILE" ]]; then - echo "Error: config not found at $CONFIG_FILE" - echo "Run the installer first: curl -fsSL https://raw.githubusercontent.com/ClintonSarkar/clawde/main/install.sh | bash" - exit 1 - fi - local section="" - while IFS= read -r line; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [[ -z "$line" || "$line" == \#* ]] && continue - if [[ "$line" =~ ^\[(.+)\]$ ]]; then - section="${BASH_REMATCH[1]}" - elif [[ "$line" =~ ^([^#]+?)\s*=\s*(.*)$ ]] && [[ -n "$section" ]]; then - local key="${BASH_REMATCH[1]// /}" - local val="${BASH_REMATCH[2]//\"/}" - val="${val//\'/}" - echo "CLAWDE_CFG_${section}_${key}=\"${val}\"" - fi - done < "$CONFIG_FILE" -} - -get_pid() { - local name="$1" - local pid_file="${PID_DIR}/${name}.pid" - if [[ -f "$pid_file" ]]; then - cat "$pid_file" 2>/dev/null || true - fi -} - -write_pid() { - local name="$1" pid="$2" - echo "$pid" > "${PID_DIR}/${name}.pid" -} - -remove_pid() { - local name="$1" - local pid_file="${PID_DIR}/${name}.pid" - [[ -f "$pid_file" ]] && rm -f "$pid_file" -} - -is_running() { - local pid="$1" - [[ -z "$pid" ]] && return 1 - kill -0 "$pid" 2>/dev/null -} - -find_binary() { - local name="$1" - local local_path="${BIN_DIR}/${name}" - if [[ -x "$local_path" ]]; then - echo "$local_path" - return 0 - fi - if command -v "$name" >/dev/null 2>&1; then - command -v "$name" - return 0 - fi - echo "Error: $name not found - run 'clawde update' or reinstall" >&2 - exit 1 -} - -get_proxy_port() { - local port="8080" - if [[ -n "${CLAWDE_CFG_proxy_port:-}" ]]; then - port="$CLAWDE_CFG_proxy_port" - fi - echo "$port" -} - -get_proxy_host() { - local host="127.0.0.1" - if [[ -n "${CLAWDE_CFG_proxy_host:-}" ]]; then - host="$CLAWDE_CFG_proxy_host" - fi - echo "$host" -} - -# --- Commands --- - -cmd_start() { - local extra_args=("$@") - eval "$(read_config)" - local port host - port="$(get_proxy_port)" - host="$(get_proxy_host)" - - local proxy_pid - proxy_pid="$(get_pid proxy)" - if is_running "$proxy_pid"; then - echo "[OK] CCProxy already running (PID $proxy_pid)" - else - echo "[INFO] Starting CCProxy..." - local ccproxy_bin - ccproxy_bin="$(find_binary ccproxy)" - local log_file="${LOG_DIR}/ccproxy.log" - nohup "$ccproxy_bin" serve --port "$port" >> "$log_file" 2>&1 & - local new_pid=$! - write_pid proxy "$new_pid" - - local healthy=false - for i in $(seq 1 30); do - sleep 0.5 - if curl -sf "http://${host}:${port}/health" >/dev/null 2>&1; then - healthy=true - break - fi - done - if $healthy; then - echo "[OK] CCProxy started (PID $new_pid) - healthy" - else - echo "[WARN] CCProxy started (PID $new_pid) - health check failed, may still be starting" - fi - fi - - local opencode_pid - opencode_pid="$(get_pid opencode)" - if is_running "$opencode_pid"; then - echo "[OK] OpenCode already running (PID $opencode_pid)" - else - echo "[INFO] Starting OpenCode..." - local opencode_bin - opencode_bin="$(find_binary opencode)" - export OPENCODE_PROVIDER_CLAWDE_BASE_URL="http://${host}:${port}/v1" - export OPENCODE_PROVIDER_CLAWDE_API_KEY="***" - "$opencode_bin" "${extra_args[@]}" & - local oc_pid=$! - write_pid opencode "$oc_pid" - echo "[OK] OpenCode started (PID $oc_pid)" - fi -} - -cmd_stop() { - for name in opencode proxy; do - local pid - pid="$(get_pid $name)" - if is_running "$pid"; then - kill "$pid" 2>/dev/null || true - sleep 2 - if is_running "$pid"; then - kill -9 "$pid" 2>/dev/null || true - fi - echo "[OK] $name stopped (was PID $pid)" - else - echo "[OK] $name not running" - fi - remove_pid "$name" - done -} - -cmd_status() { - eval "$(read_config)" - local port host - port="$(get_proxy_port)" - host="$(get_proxy_host)" - - local proxy_pid - proxy_pid="$(get_pid proxy)" - if is_running "$proxy_pid"; then - if curl -sf "http://${host}:${port}/health" >/dev/null 2>&1; then - echo "[OK] CCProxy running (PID $proxy_pid) - healthy" - else - echo "[WARN] CCProxy running (PID $proxy_pid) - not responding" - fi - else - echo "[FAIL] CCProxy not running" - fi - - local opencode_pid - opencode_pid="$(get_pid opencode)" - if is_running "$opencode_pid"; then - echo "[OK] OpenCode running (PID $opencode_pid)" - else - echo "[FAIL] OpenCode not running" - fi -} - -cmd_config() { - local edit=false - [[ "${1:-}" == "--edit" || "${1:-}" == "-e" ]] && edit=true - if $edit; then - local editor="${EDITOR:-vi}" - "$editor" "$CONFIG_FILE" - else - echo "Config file: $CONFIG_FILE" - echo "" - cat "$CONFIG_FILE" - fi -} - -cmd_auth() { - echo "[INFO] Starting Claude OAuth flow..." - echo " A browser window will open for you to log in." - echo "" - local ccproxy_bin - ccproxy_bin="$(find_binary ccproxy)" - - # Initialize CCProxy config if missing (otherwise auth provider can't be found) - local ccproxy_config_dir="${HOME}/.config/ccproxy" - local ccproxy_config_file="${ccproxy_config_dir}/ccproxy.config.settings" - if [[ ! -f "$ccproxy_config_file" ]]; then - echo " [INFO] Initializing CCProxy config (first-time setup)..." - "$ccproxy_bin" config init --output-dir "$ccproxy_config_dir" 2>/dev/null || true - fi - - if "$ccproxy_bin" auth login claude 2>&1 | grep -v -E '\[warning|cmd_id|config_file_missing|plugins_directories_missing|auth_provider_not_found' | grep -v '^\[2m'; then - echo "" - echo "[OK] Authentication complete" - else - echo "" - echo "[ERROR] Authentication failed" - exit 1 - fi -} - -cmd_update() { - echo "[INFO] Updating all components..." - echo "" - local any_errors=false - - # --- OpenCode --- - local opencode_bin - opencode_bin="$(find_binary opencode 2>/dev/null || true)" - if [[ -x "$opencode_bin" ]]; then - local ver new_ver - ver="$("$opencode_bin" --version 2>/dev/null | head -1)" - ver="${ver//[[:space:]]/ }"; ver="${ver#"${ver%%[![:space:]]*}"}"; ver="${ver%"${ver##*[![:space:]]}"}" - # Suppress upgrade noise: capture all output, only show tail on failure - local upgrade_output upgrade_rc - upgrade_output="$("$opencode_bin" upgrade 2>&1)" - upgrade_rc=$? - new_ver="$("$opencode_bin" --version 2>/dev/null | head -1)" - new_ver="${new_ver//[[:space:]]/ }"; new_ver="${new_ver#"${new_ver%%[![:space:]]*}"}"; new_ver="${new_ver%"${new_ver##*[![:space:]]}"}" - if [[ "$upgrade_rc" -eq 0 ]]; then - if [[ "$ver" != "$new_ver" ]]; then - echo " [OK] OpenCode $ver -> $new_ver" - else - echo " [OK] OpenCode $ver (already latest)" - fi - else - echo " [ERROR] OpenCode upgrade failed" - # Show last few non-empty lines for debugging - while IFS= read -r line; do - [[ -n "$line" ]] && echo " $line" - done < <(printf '%s\n' "$upgrade_output" | grep -v '^$' | tail -n 3) - any_errors=true - fi - else - echo " [ERROR] opencode not found" - any_errors=true - fi - - # --- CCProxy --- - local ccproxy_bin - ccproxy_bin="$(find_binary ccproxy 2>/dev/null || true)" - if [[ -n "$ccproxy_bin" && -x "$ccproxy_bin" ]]; then - # Suppress stderr (config_file_missing warning is harmless) - local ver - ver="$("$ccproxy_bin" --version 2>/dev/null | head -1)" - # Extract version from output like "ccproxy 0.2.10" - local current_ver - current_ver="$(echo "$ver" | sed -nE 's/.*ccproxy[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" - [[ -z "$current_ver" ]] && current_ver="$(echo "$ver" | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" - [[ -z "$current_ver" ]] && current_ver="unknown" - local release_json latest_tag - release_json="$(curl -fsSL --connect-timeout 10 --max-time 15 "https://api.github.com/repos/ClintonSarkar/ccproxy-api/releases/latest" 2>/dev/null)" || { - echo " [ERROR] Could not check CCProxy updates" - any_errors=true - } - if [[ -n "$release_json" ]]; then - latest_tag="$(echo "$release_json" | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" - if [[ "v${current_ver}" != "$latest_tag" ]]; then - echo " [INFO] CCProxy v${current_ver} -> $latest_tag" - local arch asset_name - arch="$(uname -m)" - case "$(uname -s):${arch}" in - Linux:x86_64) asset_name="ccproxy-${latest_tag}-x86_64-unknown-linux-gnu.tar.gz" ;; - Linux:aarch64) asset_name="ccproxy-${latest_tag}-x86_64-unknown-linux-gnu.tar.gz" ;; - Darwin:x86_64) asset_name="ccproxy-${latest_tag}-x86_64-apple-darwin.tar.gz" ;; - Darwin:arm64|Darwin:aarch64) asset_name="ccproxy-${latest_tag}-aarch64-apple-darwin.tar.gz" ;; - *) echo " [ERROR] Unsupported platform"; any_errors=true ;; - esac - local download_url - download_url="$(echo "$release_json" | grep -o "\"browser_download_url\": *\"[^\"]*${asset_name}[^\"]*\"" | sed -E "s/.*\"([^\"]+)\".*/\1/" | head -1)" - if [[ -n "$download_url" ]]; then - local tmp_archive="/tmp/ccproxy-update.tar.gz" - local bin_dir - bin_dir="$(dirname "$ccproxy_bin")" - # Stop running ccproxy before overwriting - local proxy_pid - proxy_pid="$(get_pid proxy 2>/dev/null || true)" - if [[ -n "$proxy_pid" ]] && is_running "$proxy_pid"; then - echo " [INFO] Stopping CCProxy (PID $proxy_pid) for update..." - kill "$proxy_pid" 2>/dev/null || true - remove_pid proxy - sleep 0.5 - fi - if curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$tmp_archive" 2>/dev/null; then - # Extract to temp dir first, then move (avoids in-place overwrite issues) - local extract_dir="/tmp/ccproxy-extract" - rm -rf "$extract_dir" - mkdir -p "$extract_dir" - if tar -xzf "$tmp_archive" -C "$extract_dir" 2>/dev/null; then - local extracted_exe="$extract_dir/ccproxy" - if [[ ! -x "$extracted_exe" ]]; then - local found - found="$(find "$extract_dir" -name ccproxy -type f -executable 2>/dev/null | head -1)" - [[ -n "$found" ]] && extracted_exe="$found" - fi - if [[ -x "$extracted_exe" ]]; then - mv "$extracted_exe" "$ccproxy_bin" - chmod +x "$ccproxy_bin" - echo " [OK] CCProxy updated to $latest_tag" - else - echo " [ERROR] ccproxy binary not found after extraction" - any_errors=true - fi - rm -rf "$extract_dir" "$tmp_archive" - else - echo " [ERROR] CCProxy extraction failed" - rm -rf "$extract_dir" "$tmp_archive" - any_errors=true - fi - else - echo " [ERROR] CCProxy download failed" - any_errors=true - fi - else - echo " [WARN] No binary found for $latest_tag" - fi - else - echo " [OK] CCProxy v${current_ver} (already latest)" - fi - fi - else - echo " [ERROR] ccproxy not found - run installer" - any_errors=true - fi - - # --- Self-update --- - self_update - - echo "" - if $any_errors; then - echo "[WARN] Update completed with errors" - else - echo "[OK] Update complete" - fi -} - - -self_update() { - local self_url="https://raw.githubusercontent.com/ClintonSarkar/clawde/main/cli/clawde.sh" - local this_script - this_script="$(realpath "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo "$0")" - if [[ ! -f "$this_script" ]]; then - echo " [WARN] Could not determine script path" - return - fi - - local tmp_file="/tmp/clawde-update.sh" - if curl -fsSL --connect-timeout 10 --max-time 30 "$self_url" -o "$tmp_file" 2>/dev/null; then - local remote_hash local_hash - remote_hash="$(sha256sum "$tmp_file" 2>/dev/null | awk '{print $1}')" - local_hash="$(sha256sum "$this_script" 2>/dev/null | awk '{print $1}')" - - if [[ "$remote_hash" == "$local_hash" ]]; then - echo " [OK] clawde CLI (already latest)" - rm -f "$tmp_file" - else - cp "$this_script" "${this_script}.bak" - cp "$tmp_file" "$this_script" - rm -f "$tmp_file" - chmod +x "$this_script" - echo " [OK] clawde CLI updated (backup: ${this_script}.bak)" - fi - else - echo " [WARN] clawde CLI not updated" - fi -} - -cmd_logs() { - local service="${1:-proxy}" - local follow=false - local line_count=50 - - shift || true - while [[ $# -gt 0 ]]; do - case "$1" in - -f|--follow) follow=true; shift ;; - -n|--lines) line_count="$2"; shift 2 ;; - *) service="$1"; shift ;; - esac - done - - local log_file="${LOG_DIR}/${service}.log" - if [[ ! -f "$log_file" ]]; then - echo "No logs found for $service at $log_file" - exit 1 - fi - - if $follow; then - tail -f "$log_file" - else - tail -n "$line_count" "$log_file" - fi -} - -# --- Main --- - -command="${1:-}" -shift || true - -if [[ -z "$command" || "$command" == "--help" || "$command" == "-h" ]]; then - echo "clawde - Claude Work to OpenCode bridge" - echo "" - echo "Usage: clawde [options]" - echo "" - echo "Commands:" - echo " start Start CCProxy + OpenCode" - echo " stop Stop both services" - echo " status Check health of both services" - echo " config View or edit configuration" - echo " auth Re-authenticate Claude" - echo " update Update to latest versions" - echo " logs Tail logs (proxy | opencode)" - echo "" - echo "Options:" - echo " --help, -h Show this help" - exit 0 -fi - -case "$command" in - start) cmd_start "$@" ;; - stop) cmd_stop ;; - status) cmd_status ;; - config) cmd_config "$@" ;; - auth) cmd_auth ;; - update) cmd_update ;; - logs) cmd_logs "$@" ;; - *) - echo "Unknown command: $command" - echo "Run 'clawde --help' for usage" - exit 1 - ;; -esac +#!/usr/bin/env bash +# clawde.sh - unified CLI for managing OpenCode + CCProxy (Linux/WSL) +# +# Commands: +# start - start CCProxy + OpenCode +# stop - stop both services +# status - health check both services +# config - view or edit configuration +# auth - re-authenticate Claude +# update - update to latest versions +# logs - tail logs from either service + +set -euo pipefail + +# --- Paths --- +CONFIG_DIR="${HOME}/.config/clawde" +DATA_DIR="${HOME}/.local/share/clawde" +BIN_DIR="${HOME}/.local/bin" +LOG_DIR="${DATA_DIR}/logs" +PID_DIR="${DATA_DIR}/pids" +CONFIG_FILE="${CONFIG_DIR}/clawde.toml" + +# --- Ensure dirs exist --- +mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$BIN_DIR" "$LOG_DIR" "$PID_DIR" + +# --- Helpers --- + +read_config() { + if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: config not found at $CONFIG_FILE" + echo "Run the installer first: curl -fsSL https://raw.githubusercontent.com/ClintonSarkar/clawde/main/install.sh | bash" + exit 1 + fi + local section="" + while IFS= read -r line; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" || "$line" == \#* ]] && continue + if [[ "$line" =~ ^\[(.+)\]$ ]]; then + section="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^([^=#]+)=(.*)$ ]] && [[ -n "$section" ]]; then + # POSIX ERE only (no \s / lazy quantifiers): works on macOS/BSD bash too. + # The line was whitespace-trimmed above; trim any around the split parts. + local key="${BASH_REMATCH[1]//[[:space:]]/}" + local val="${BASH_REMATCH[2]}" + val="${val#"${val%%[![:space:]]*}"}" + val="${val%"${val##*[![:space:]]}"}" + val="${val//\"/}" + val="${val//\'/}" + echo "CLAWDE_CFG_${section}_${key}=\"${val}\"" + fi + done < "$CONFIG_FILE" +} + +get_pid() { + local name="$1" + local pid_file="${PID_DIR}/${name}.pid" + if [[ -f "$pid_file" ]]; then + cat "$pid_file" 2>/dev/null || true + fi +} + +write_pid() { + local name="$1" pid="$2" + echo "$pid" > "${PID_DIR}/${name}.pid" +} + +remove_pid() { + local name="$1" + local pid_file="${PID_DIR}/${name}.pid" + [[ -f "$pid_file" ]] && rm -f "$pid_file" +} + +is_running() { + local pid="$1" + [[ -z "$pid" ]] && return 1 + kill -0 "$pid" 2>/dev/null +} + +find_binary() { + local name="$1" + local local_path="${BIN_DIR}/${name}" + if [[ -x "$local_path" ]]; then + echo "$local_path" + return 0 + fi + if command -v "$name" >/dev/null 2>&1; then + command -v "$name" + return 0 + fi + echo "Error: $name not found - run 'clawde update' or reinstall" >&2 + exit 1 +} + +get_proxy_port() { + local port="8080" + if [[ -n "${CLAWDE_CFG_proxy_port:-}" ]]; then + port="$CLAWDE_CFG_proxy_port" + fi + echo "$port" +} + +get_proxy_host() { + local host="127.0.0.1" + if [[ -n "${CLAWDE_CFG_proxy_host:-}" ]]; then + host="$CLAWDE_CFG_proxy_host" + fi + echo "$host" +} + +# --- Commands --- + +cmd_start() { + local extra_args=("$@") + eval "$(read_config)" + local port host + port="$(get_proxy_port)" + host="$(get_proxy_host)" + + local proxy_pid + proxy_pid="$(get_pid proxy)" + if is_running "$proxy_pid"; then + echo "[OK] CCProxy already running (PID $proxy_pid)" + else + echo "[INFO] Starting CCProxy..." + local ccproxy_bin + ccproxy_bin="$(find_binary ccproxy)" + local log_file="${LOG_DIR}/ccproxy.log" + nohup "$ccproxy_bin" serve --port "$port" >> "$log_file" 2>&1 & + local new_pid=$! + write_pid proxy "$new_pid" + + local healthy=false + for i in $(seq 1 30); do + sleep 0.5 + if curl -sf "http://${host}:${port}/health" >/dev/null 2>&1; then + healthy=true + break + fi + done + if $healthy; then + echo "[OK] CCProxy started (PID $new_pid) - healthy" + else + echo "[WARN] CCProxy started (PID $new_pid) - health check failed, may still be starting" + fi + fi + + local opencode_pid + opencode_pid="$(get_pid opencode)" + if is_running "$opencode_pid"; then + echo "[OK] OpenCode already running (PID $opencode_pid)" + else + echo "[INFO] Starting OpenCode..." + local opencode_bin + opencode_bin="$(find_binary opencode)" + # The ccproxy-claude provider (baseURL + apiKey) is defined authoritatively + # in opencode.json by the installer; OpenCode reads it from there. Don't + # export a second, divergent provider definition via env vars here. + "$opencode_bin" "${extra_args[@]}" & + local oc_pid=$! + write_pid opencode "$oc_pid" + echo "[OK] OpenCode started (PID $oc_pid)" + fi +} + +cmd_stop() { + for name in opencode proxy; do + local pid + pid="$(get_pid $name)" + if is_running "$pid"; then + kill "$pid" 2>/dev/null || true + sleep 2 + if is_running "$pid"; then + kill -9 "$pid" 2>/dev/null || true + fi + echo "[OK] $name stopped (was PID $pid)" + else + echo "[OK] $name not running" + fi + remove_pid "$name" + done +} + +cmd_status() { + eval "$(read_config)" + local port host + port="$(get_proxy_port)" + host="$(get_proxy_host)" + + local proxy_pid + proxy_pid="$(get_pid proxy)" + if is_running "$proxy_pid"; then + if curl -sf "http://${host}:${port}/health" >/dev/null 2>&1; then + echo "[OK] CCProxy running (PID $proxy_pid) - healthy" + else + echo "[WARN] CCProxy running (PID $proxy_pid) - not responding" + fi + else + echo "[FAIL] CCProxy not running" + fi + + local opencode_pid + opencode_pid="$(get_pid opencode)" + if is_running "$opencode_pid"; then + echo "[OK] OpenCode running (PID $opencode_pid)" + else + echo "[FAIL] OpenCode not running" + fi +} + +cmd_config() { + local edit=false + [[ "${1:-}" == "--edit" || "${1:-}" == "-e" ]] && edit=true + if $edit; then + local editor="${EDITOR:-vi}" + "$editor" "$CONFIG_FILE" + else + echo "Config file: $CONFIG_FILE" + echo "" + cat "$CONFIG_FILE" + fi +} + +cmd_auth() { + echo "[INFO] Starting Claude OAuth flow..." + echo " A browser window will open for you to log in." + echo "" + local ccproxy_bin + ccproxy_bin="$(find_binary ccproxy)" + + # Initialize CCProxy config if missing (otherwise auth provider can't be found) + local ccproxy_config_dir="${HOME}/.config/ccproxy" + local ccproxy_config_file="${ccproxy_config_dir}/ccproxy.config.settings" + if [[ ! -f "$ccproxy_config_file" ]]; then + echo " [INFO] Initializing CCProxy config (first-time setup)..." + "$ccproxy_bin" config init --output-dir "$ccproxy_config_dir" 2>/dev/null || true + fi + + if "$ccproxy_bin" auth login claude 2>&1 | grep -v -E '\[warning|cmd_id|config_file_missing|plugins_directories_missing|auth_provider_not_found' | grep -v '^\[2m'; then + echo "" + echo "[OK] Authentication complete" + else + echo "" + echo "[ERROR] Authentication failed" + exit 1 + fi +} + +cmd_update() { + echo "[INFO] Updating all components..." + echo "" + local any_errors=false + + # --- OpenCode --- + local opencode_bin + opencode_bin="$(find_binary opencode 2>/dev/null || true)" + if [[ -x "$opencode_bin" ]]; then + local ver new_ver + ver="$("$opencode_bin" --version 2>/dev/null | head -1)" + ver="${ver//[[:space:]]/ }"; ver="${ver#"${ver%%[![:space:]]*}"}"; ver="${ver%"${ver##*[![:space:]]}"}" + # Suppress upgrade noise: capture all output, only show tail on failure + local upgrade_output upgrade_rc + upgrade_output="$("$opencode_bin" upgrade 2>&1)" + upgrade_rc=$? + new_ver="$("$opencode_bin" --version 2>/dev/null | head -1)" + new_ver="${new_ver//[[:space:]]/ }"; new_ver="${new_ver#"${new_ver%%[![:space:]]*}"}"; new_ver="${new_ver%"${new_ver##*[![:space:]]}"}" + if [[ "$upgrade_rc" -eq 0 ]]; then + if [[ "$ver" != "$new_ver" ]]; then + echo " [OK] OpenCode $ver -> $new_ver" + else + echo " [OK] OpenCode $ver (already latest)" + fi + else + echo " [ERROR] OpenCode upgrade failed" + # Show last few non-empty lines for debugging + while IFS= read -r line; do + [[ -n "$line" ]] && echo " $line" + done < <(printf '%s\n' "$upgrade_output" | grep -v '^$' | tail -n 3) + any_errors=true + fi + else + echo " [ERROR] opencode not found" + any_errors=true + fi + + # --- CCProxy --- + local ccproxy_bin + ccproxy_bin="$(find_binary ccproxy 2>/dev/null || true)" + if [[ -n "$ccproxy_bin" && -x "$ccproxy_bin" ]]; then + # Suppress stderr (config_file_missing warning is harmless) + local ver + ver="$("$ccproxy_bin" --version 2>/dev/null | head -1)" + # Extract version from output like "ccproxy 0.2.10" + local current_ver + current_ver="$(echo "$ver" | sed -nE 's/.*ccproxy[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" + [[ -z "$current_ver" ]] && current_ver="$(echo "$ver" | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" + [[ -z "$current_ver" ]] && current_ver="unknown" + local release_json latest_tag + release_json="$(curl -fsSL --connect-timeout 10 --max-time 15 "https://api.github.com/repos/ClintonSarkar/ccproxy-api/releases/latest" 2>/dev/null)" || { + echo " [ERROR] Could not check CCProxy updates" + any_errors=true + } + if [[ -n "$release_json" ]]; then + latest_tag="$(echo "$release_json" | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + if [[ "v${current_ver}" != "$latest_tag" ]]; then + echo " [INFO] CCProxy v${current_ver} -> $latest_tag" + local arch asset_name + arch="$(uname -m)" + case "$(uname -s):${arch}" in + Linux:x86_64) asset_name="ccproxy-${latest_tag}-x86_64-unknown-linux-gnu.tar.gz" ;; + Linux:aarch64) asset_name="ccproxy-${latest_tag}-x86_64-unknown-linux-gnu.tar.gz" ;; + Darwin:x86_64) asset_name="ccproxy-${latest_tag}-x86_64-apple-darwin.tar.gz" ;; + Darwin:arm64|Darwin:aarch64) asset_name="ccproxy-${latest_tag}-aarch64-apple-darwin.tar.gz" ;; + *) echo " [ERROR] Unsupported platform"; any_errors=true ;; + esac + local download_url + download_url="$(echo "$release_json" | grep -o "\"browser_download_url\": *\"[^\"]*${asset_name}[^\"]*\"" | sed -E "s/.*\"([^\"]+)\".*/\1/" | head -1)" + if [[ -n "$download_url" ]]; then + local tmp_archive="/tmp/ccproxy-update.tar.gz" + local bin_dir + bin_dir="$(dirname "$ccproxy_bin")" + # Stop running ccproxy before overwriting + local proxy_pid + proxy_pid="$(get_pid proxy 2>/dev/null || true)" + if [[ -n "$proxy_pid" ]] && is_running "$proxy_pid"; then + echo " [INFO] Stopping CCProxy (PID $proxy_pid) for update..." + kill "$proxy_pid" 2>/dev/null || true + remove_pid proxy + sleep 0.5 + fi + if curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$tmp_archive" 2>/dev/null; then + # Extract to temp dir first, then move (avoids in-place overwrite issues) + local extract_dir="/tmp/ccproxy-extract" + rm -rf "$extract_dir" + mkdir -p "$extract_dir" + if tar -xzf "$tmp_archive" -C "$extract_dir" 2>/dev/null; then + local extracted_exe="$extract_dir/ccproxy" + if [[ ! -x "$extracted_exe" ]]; then + local found + found="$(find "$extract_dir" -name ccproxy -type f -executable 2>/dev/null | head -1)" + [[ -n "$found" ]] && extracted_exe="$found" + fi + if [[ -x "$extracted_exe" ]]; then + mv "$extracted_exe" "$ccproxy_bin" + chmod +x "$ccproxy_bin" + echo " [OK] CCProxy updated to $latest_tag" + else + echo " [ERROR] ccproxy binary not found after extraction" + any_errors=true + fi + rm -rf "$extract_dir" "$tmp_archive" + else + echo " [ERROR] CCProxy extraction failed" + rm -rf "$extract_dir" "$tmp_archive" + any_errors=true + fi + else + echo " [ERROR] CCProxy download failed" + any_errors=true + fi + else + echo " [WARN] No binary found for $latest_tag" + fi + else + echo " [OK] CCProxy v${current_ver} (already latest)" + fi + fi + else + echo " [ERROR] ccproxy not found - run installer" + any_errors=true + fi + + # --- Self-update --- + self_update + + echo "" + if $any_errors; then + echo "[WARN] Update completed with errors" + else + echo "[OK] Update complete" + fi +} + + +self_update() { + local self_url="https://raw.githubusercontent.com/ClintonSarkar/clawde/main/cli/clawde.sh" + local this_script + this_script="$(realpath "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo "$0")" + if [[ ! -f "$this_script" ]]; then + echo " [WARN] Could not determine script path" + return + fi + + # Stage the download next to the target so the final swap is an atomic + # rename on the same filesystem. + local tmp_file="${this_script}.new.$$" + if curl -fsSL --connect-timeout 10 --max-time 30 "$self_url" -o "$tmp_file" 2>/dev/null; then + local remote_hash local_hash + remote_hash="$(sha256sum "$tmp_file" 2>/dev/null | awk '{print $1}')" + local_hash="$(sha256sum "$this_script" 2>/dev/null | awk '{print $1}')" + + if [[ "$remote_hash" == "$local_hash" ]]; then + echo " [OK] clawde CLI (already latest)" + rm -f "$tmp_file" + else + cp "$this_script" "${this_script}.bak" + chmod +x "$tmp_file" + # Atomic rename over the running script: replaces the inode instead of + # truncating the one the current shell is still reading. + mv -f "$tmp_file" "$this_script" + echo " [OK] clawde CLI updated (backup: ${this_script}.bak; restart clawde to use it)" + fi + else + # Clean up any partial download so it doesn't litter the bin dir. + rm -f "$tmp_file" + echo " [WARN] clawde CLI not updated" + fi +} + +cmd_logs() { + local service="${1:-proxy}" + local follow=false + local line_count=50 + + shift || true + while [[ $# -gt 0 ]]; do + case "$1" in + -f|--follow) follow=true; shift ;; + -n|--lines) line_count="$2"; shift 2 ;; + *) service="$1"; shift ;; + esac + done + + local log_file="${LOG_DIR}/${service}.log" + if [[ ! -f "$log_file" ]]; then + echo "No logs found for $service at $log_file" + exit 1 + fi + + if $follow; then + tail -f "$log_file" + else + tail -n "$line_count" "$log_file" + fi +} + +# --- Main --- + +command="${1:-}" +shift || true + +if [[ -z "$command" || "$command" == "--help" || "$command" == "-h" ]]; then + echo "clawde - Claude Work to OpenCode bridge" + echo "" + echo "Usage: clawde [options]" + echo "" + echo "Commands:" + echo " start Start CCProxy + OpenCode" + echo " stop Stop both services" + echo " status Check health of both services" + echo " config View or edit configuration" + echo " auth Re-authenticate Claude" + echo " update Update to latest versions" + echo " logs Tail logs (proxy | opencode)" + echo "" + echo "Options:" + echo " --help, -h Show this help" + exit 0 +fi + +case "$command" in + start) cmd_start "$@" ;; + stop) cmd_stop ;; + status) cmd_status ;; + config) cmd_config "$@" ;; + auth) cmd_auth ;; + update) cmd_update ;; + logs) cmd_logs "$@" ;; + *) + echo "Unknown command: $command" + echo "Run 'clawde --help' for usage" + exit 1 + ;; +esac diff --git a/install.sh b/install.sh index 742b89d..370f552 100644 --- a/install.sh +++ b/install.sh @@ -1,2549 +1,2561 @@ -#!/usr/bin/env bash - -# clawde installer - Linux / WSL - -# Claude Work - OpenCode bridge - -# - -# Usage: - -# curl -fsSL https://clawde.dev/install.sh | bash - -# curl -fsSL https://clawde.dev/install.sh | bash -s -- --yes - -# curl -fsSL https://clawde.dev/install.sh | bash -s -- --uninstall - -# - -# Environment variables (for CI / automation): - -# CLAWDE_PORT Proxy port (default: 8080) - -# CLAWDE_AUTH_METHOD Auth method: oauth | cli_token (default: oauth) - -# CLAWDE_CLI_TOKEN_PATH Path to Claude CLI credentials (default: ~/.claude/credentials.json) - -# CLAWDE_AUTO_START Auto-start on boot: true | false (default: false) - -# CLAWDE_MODELS Models to expose (default: all) - -set -euo pipefail - - - -# ==================================================================== - -# Constants - -# ==================================================================== - -CLAWDE_VERSION="0.1.0" - -OPENCODE_REPO="ClintonSarkar/opencode" - -CCPROXY_PACKAGE="ccproxy-api" - - - -CLAWDE_CONFIG_DIR="${HOME}/.config/clawde" - -CLAWDE_DATA_DIR="${HOME}/.local/share/clawde" - -CLAWDE_BIN_DIR="${HOME}/.local/bin" - -OPENCODE_BIN="${CLAWDE_BIN_DIR}/opencode" - -CLAWDE_CONFIG_FILE="${CLAWDE_CONFIG_DIR}/clawde.toml" - -SYSTEMD_SERVICE_NAME="clawde-proxy" - -SYSTEMD_SERVICE_FILE="${HOME}/.config/systemd/user/${SYSTEMD_SERVICE_NAME}.service" - - - -# ==================================================================== - -# Flags & State - -# ==================================================================== - -VERBOSE=false - -NONINTERACTIVE=false - -UNINSTALL=false - -INSTALL_COMPLETED=false - -ROLLBACK_ITEMS=() - -IS_WSL=false - -OS="" - -ARCH="" - -SKIP_OPENCODE=false - -SKIP_CONFIG=false - -EXISTING_OPENCODE_PATH="" - -AUTH_PENDING=false - -PROGRESS_CURRENT=0 - -PROGRESS_TOTAL=6 - - - -# ==================================================================== - -# Colors & Logging - -# ==================================================================== - -RED='\033[0;31m' - -GREEN='\033[0;32m' - -YELLOW='\033[1;33m' - -CYAN='\033[0;36m' - -BOLD='\033[1m' - -NC='\033[0m' - - - -info() { echo -e "${CYAN}[INFO]${NC} $*"; } - -ok() { echo -e "${GREEN}[OK]${NC} $*"; } - -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } - -error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } - -debug() { [[ "$VERBOSE" == "true" ]] && echo -e "[DEBUG] $*"; } - - - -# ==================================================================== - -# Progress bar - -# ==================================================================== - -show_progress() { - - PROGRESS_CURRENT=$1 - - PROGRESS_TOTAL=$2 - - PROGRESS_LABEL="$3" - - _redraw_bar - -} - - - -_redraw_bar() { - - local percent filled empty bar bar_width=30 - - percent=$(( (PROGRESS_CURRENT - 1) * 100 / PROGRESS_TOTAL )) - - [[ $percent -lt 0 ]] && percent=0 - - filled=$(( percent * bar_width / 100 )) - - empty=$(( bar_width - filled )) - - bar="" - - for ((i=0; i %s... [%s] %d%%\n" "$PROGRESS_LABEL" "$bar" "$percent" - -} - - - -_draw_bar() { - - local percent filled empty bar bar_width=30 current=$1 - - percent=$(( current * 100 / PROGRESS_TOTAL )) - - [[ $percent -gt 100 ]] && percent=100 - - filled=$(( percent * bar_width / 100 )) - - empty=$(( bar_width - filled )) - - bar="" - - for ((i=0; i/dev/null || true - - debug " Removed file: $item" - - elif [[ -d "$item" ]]; then - - # Only remove empty dirs we created, unless it's our config/data - - if [[ "$item" == "$CLAWDE_CONFIG_DIR" ]] || [[ "$item" == "$CLAWDE_DATA_DIR" ]] || [[ "$item" == "$CLAWDE_BIN_DIR" ]]; then - - rm -rf "$item" 2>/dev/null || true - - debug " Removed directory: $item" - - else - - rmdir "$item" 2>/dev/null || true - - debug " Removed (empty) directory: $item" - - fi - - fi - - done - - warn "Rollback complete. Run the installer again to retry." - -} - - - -trap cleanup_on_exit EXIT - - - -register_rollback() { - - ROLLBACK_ITEMS+=("$1") - - debug "Registered rollback: $1" - -} - - - -clear_rollback() { - - ROLLBACK_ITEMS=() - -} - - - -# ==================================================================== - -# OS / Architecture detection - -# ==================================================================== - -detect_os() { - - local os arch os_name - - os="$(uname -s)" - - case "$os" in - - Linux*) OS="linux";; - - Darwin*) OS="macos";; - - *) error "Unsupported operating system: $os. clawde requires Linux, macOS, or WSL.";; - - esac - - - - arch="$(uname -m)" - - case "$arch" in - - x86_64|amd64) ARCH="x64";; - - aarch64|arm64) ARCH="arm64";; - - *) - - warn "Unrecognized architecture: $arch assuming x64 (may cause runtime issues)" - - ARCH="x64" - - ;; - - esac - - - - os_name="$(uname -o 2>/dev/null || echo "$OS")" - - info "Detected: $os_name on $arch ($ARCH)" - -} - - - -# ==================================================================== - -# WSL detection - -# ==================================================================== - -check_wsl() { - - if [[ -f /proc/version ]] && grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null; then - - IS_WSL=true - - local wsl_ver - - wsl_ver="$(wsl.exe --version 2>/dev/null | head -1 || echo "WSL (version unknown)")" - - info "Running under $wsl_ver" - - else - - IS_WSL=false - - fi - -} - - - -# ==================================================================== - -# Dependency checking - -# ==================================================================== - -check_deps() { - - local missing=() optional=() - - local has_uv=false has_pipx=false has_pip=false has_python=false - - - - # --- Required --- - - command -v curl >/dev/null 2>&1 || missing+=("curl") - - command -v bash >/dev/null 2>&1 || missing+=("bash") - - - - # Python (optional - not needed for clawde CLI or ccproxy binary) - - if command -v python3 >/dev/null 2>&1; then - - has_python=true - - PYTHON=python3 - - elif command -v python >/dev/null 2>&1; then - - local pyver - - pyver="$(python --version 2>&1 | grep -oP "\d+\.\d+")" - - if [[ "${pyver%%.*}" -ge 3 ]]; then - - has_python=true - - PYTHON=python - - fi - - fi - - - - # Python package managers (optional, for legacy uninstall only) - - command -v uv >/dev/null 2>&1 && has_uv=true - - command -v pipx >/dev/null 2>&1 && has_pipx=true - - if command -v pip3 >/dev/null 2>&1; then - - has_pip=true; PIP=pip3 - - elif command -v pip >/dev/null 2>&1; then - - has_pip=true; PIP=pip - - fi - - - - - # git (only needed for source builds) - - command -v git >/dev/null 2>&1 || optional+=("git https://git-scm.com/ (for source builds)") - - - - # --- Report --- - - if [[ ${#missing[@]} -gt 0 ]]; then - - echo "" - - error "Missing required dependencies: ${missing[*]} - - - -Install them with your package manager: - - sudo apt install ${missing[*]} # Debian/Ubuntu - - sudo dnf install ${missing[*]} # Fedora - - sudo pacman -S ${missing[*]} # Arch Linux - - brew install ${missing[*]} # macOS Homebrew" - - fi - - - - if [[ ${#optional[@]} -gt 0 ]]; then - - echo "" - - warn "Optional dependencies not found:" - - for dep in "${optional[@]}"; do - - echo " - $dep" - - done - - warn "The installer will attempt to install what it needs." - - echo "" - - fi - - - - debug "Dependency check: curl=$(command -v curl), python=${PYTHON:-none}, uv=$has_uv, pipx=$has_pipx, pip=$has_pip, git=$(command -v git >/dev/null 2>&1 && echo yes || echo no)" - -} - - - -# ==================================================================== - -# Version display - -# ==================================================================== - -show_version() { - - info "clawde installer v${CLAWDE_VERSION}" - - if [[ "$VERBOSE" == "true" ]]; then - - debug " Python: $($PYTHON --version 2>/dev/null || echo 'not found')" - - command -v uv >/dev/null 2>&1 && debug " uv: $(uv --version 2>/dev/null || echo 'unknown')" - - command -v pipx >/dev/null 2>&1 && debug " pipx: $(pipx --version 2>/dev/null || echo 'unknown')" - - debug " Shell: $SHELL" - - debug " WSL: $IS_WSL" - - debug " OS/Arch: $OS/$ARCH" - - fi - -} - - - -# ==================================================================== - -# Idempotency detect existing installation - -# ==================================================================== - -check_existing() { - - local have_opencode=false - - local have_config=false - - - - # Check for OpenCode in PATH (system-wide) - - local path_opencode="" - - path_opencode="$(command -v opencode 2>/dev/null || true)" - - if [[ -n "$path_opencode" ]] && [[ "$path_opencode" != "$OPENCODE_BIN" ]]; then - - warn "OpenCode found in PATH at: ${path_opencode}" - - EXISTING_OPENCODE_PATH="$path_opencode" - - fi - - - - if [[ -x "$OPENCODE_BIN" ]]; then - - have_opencode=true - - fi - - - - if [[ -f "$CLAWDE_CONFIG_FILE" ]]; then - - have_config=true - - fi - - - - if ! $have_opencode && ! $have_config; then - - if [[ -n "${EXISTING_OPENCODE_PATH:-}" ]]; then - - echo "" - - echo " What would you like to do?" - - echo " 1. [I]nstall new OpenCode binary (clawde's own copy)" - - echo " 2. [U]se existing OpenCode from PATH" - - echo " 3. [C]ancel" - - echo "" - - read -rp " Select (default: 2): " action - - action="${action:-2}" - - case "$action" in - - [Ii]|1) - - debug "User chose install new binary" - - ;; - - [Uu]|2) - - info "Using existing OpenCode from: ${EXISTING_OPENCODE_PATH}" - - SKIP_OPENCODE=true - - return 0 - - ;; - - [Cc]|3) - - info "Installation cancelled by user." - - exit 0 - - ;; - - *) - - warn "Invalid choice '$action', defaulting to use existing" - - info "Using existing OpenCode from: ${EXISTING_OPENCODE_PATH}" - - SKIP_OPENCODE=true - - return 0 - - ;; - - esac - - fi - - debug "No existing clawde installation detected" - - return 0 - - fi - - - - echo "" - - if $have_opencode; then - - local ver - - ver="$("$OPENCODE_BIN" version 2>/dev/null || echo "version unknown")" - - warn "OpenCode is already installed: ${OPENCODE_BIN} (${ver})" - - fi - - if $have_config; then - - warn "Existing config found at ${CLAWDE_CONFIG_FILE}" - - fi - - - - if [[ "$NONINTERACTIVE" == "true" ]]; then - - if [[ -n "${EXISTING_OPENCODE_PATH:-}" ]]; then - - info "OpenCode found in PATH at: ${EXISTING_OPENCODE_PATH} using existing binary" - - SKIP_OPENCODE=true - - else - - warn "Non-interactive mode reinstalling OpenCode and overwriting config" - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - fi - - return 0 - - fi - - - - echo "" - - echo " What would you like to do?" - - echo " 1. [R]einstall / update (removes existing installation)" - - echo " 2. [S]kip OpenCode and keep existing config" - - echo " 3. [U]se existing OpenCode from PATH" - - echo " 4. [C]ancel" - - echo "" - - read -rp " Select (default: 1): " action - - action="${action:-1}" - - - - case "$action" in - - [Rr]|1|"") - - debug "User chose reinstall" - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - ;; - - [Ss]|2) - - info "Keeping existing installation skipping OpenCode and config" - - SKIP_OPENCODE=true - - SKIP_CONFIG=true - - return 0 - - ;; - - [Uu]|3) - - if [[ -z "${EXISTING_OPENCODE_PATH:-}" ]]; then - - warn "No existing OpenCode found in PATH defaulting to reinstall" - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - else - - info "Using existing OpenCode from: ${EXISTING_OPENCODE_PATH}" - - SKIP_OPENCODE=true - - # Do NOT set SKIP_CONFIG still run config wizard - - fi - - ;; - - [Cc]|4) - - info "Installation cancelled by user." - - exit 0 - - ;; - - *) - - warn "Invalid choice '$action', defaulting to reinstall" - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - ;; - - esac - - - - if $have_config; then - - echo "" - - printf " Overwrite existing config? [y/N]: " - - read -r overwrite - - case "${overwrite:-N}" in - - [Yy]*) SKIP_CONFIG=false ;; - - *) SKIP_CONFIG=true; info "Keeping existing config" ;; - - esac - - fi - -} - - - -# ==================================================================== - -# PATH management - -# ==================================================================== - -setup_path() { - - mkdir -p "$CLAWDE_BIN_DIR" - - register_rollback "$CLAWDE_BIN_DIR" - - - - # If existing OpenCode is already in PATH, skip PATH management - - if [[ -n "${EXISTING_OPENCODE_PATH:-}" ]] && command -v opencode >/dev/null 2>&1; then - - debug "Existing OpenCode already in PATH skipping PATH management for binary dir" - - return 0 - - fi - - - - if [[ ":$PATH:" != *":${CLAWDE_BIN_DIR}:"* ]]; then - - warn "${CLAWDE_BIN_DIR} is not in your PATH" - - - - local rc_files=() - - [[ -f "${HOME}/.bashrc" ]] && rc_files+=("${HOME}/.bashrc") - - [[ -f "${HOME}/.bash_profile" ]] && rc_files+=("${HOME}/.bash_profile") - - [[ -f "${HOME}/.zshrc" ]] && rc_files+=("${HOME}/.zshrc") - - [[ -f "${HOME}/.config/fish/config.fish" ]] && rc_files+=("${HOME}/.config/fish/config.fish") - - - - local path_line="export PATH=\"${CLAWDE_BIN_DIR}:\$PATH\"" - - local found_rc=false - - - - if [[ ${#rc_files[@]} -gt 0 ]]; then - - for rc in "${rc_files[@]}"; do - - if grep -qsF "$CLAWDE_BIN_DIR" "$rc" 2>/dev/null; then - - found_rc=true - - continue - - fi - - { - - echo "" - - echo "# Added by clawde installer v${CLAWDE_VERSION}" - - echo "${path_line}" - - } >> "$rc" - - ok "Added ${CLAWDE_BIN_DIR} to PATH in ${rc}" - - found_rc=true - - done - - - - if $found_rc; then - - echo "" - - warn "To use clawde immediately: source ${rc_files[0]}" - - fi - - fi - - - - if ! $found_rc; then - - echo "" - - warn "No shell rc file found. Add this to your shell profile:" - - echo " ${path_line}" - - fi - - - - # Export for current process - - export PATH="${CLAWDE_BIN_DIR}:$PATH" - - fi - -} - - - -# ==================================================================== - -# Install OpenCode (binary from GitHub releases) - -# ==================================================================== - -install_opencode() { - - show_progress 1 6 "Installing OpenCode binary" - - - - if [[ "${SKIP_OPENCODE:-false}" == "true" ]]; then - - step_done "OpenCode skipped (existing installation preserved)" - - return 0 - - fi - - - - local latest_tag download_url - - latest_tag="" - - download_url="" - - - - # Fetch latest release tag - - info "Checking GitHub releases for ${OPENCODE_REPO}..." - - latest_tag="$(curl -fsSL --connect-timeout 10 --max-time 30 \ - - "https://api.github.com/repos/${OPENCODE_REPO}/releases/latest" \ - - | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4)" || true - - - - if [[ -z "$latest_tag" ]]; then - - warn "Could not find a pre-built release for ${OPENCODE_REPO}" - - warn "Falling back to source build..." - - install_opencode_from_source - - return - - fi - - - - # Build asset name: try OS-ARCH first, then platform-specific names - - local binary_name="opencode-${OS}-${ARCH}" - - download_url="https://github.com/${OPENCODE_REPO}/releases/download/${latest_tag}/${binary_name}" - - - - debug "Attempting download: ${download_url}" - - - - mkdir -p "$CLAWDE_BIN_DIR" - - - - if curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$OPENCODE_BIN" 2>/dev/null; then - - chmod +x "$OPENCODE_BIN" - - register_rollback "$OPENCODE_BIN" - - step_done "OpenCode ${latest_tag} installed" - - return - - fi - - - - # If first attempt failed, try with 'v' prefix or alternate naming - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - - - # Try alternative naming conventions (some releases use 'linux' or omit OS) - - local alt_names=() - - alt_names+=("opencode-linux-${ARCH}") - - alt_names+=("opencode-${ARCH}") - - - - for alt_name in "${alt_names[@]}"; do - - download_url="https://github.com/${OPENCODE_REPO}/releases/download/${latest_tag}/${alt_name}" - - debug "Retrying with: ${download_url}" - - if curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$OPENCODE_BIN" 2>/dev/null; then - - chmod +x "$OPENCODE_BIN" - - register_rollback "$OPENCODE_BIN" - - step_done "OpenCode ${latest_tag} installed" - - return - - fi - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - done - - - - warn "Binary download failed for release ${latest_tag}" - - warn "Falling back to source build..." - - rm -f "$OPENCODE_BIN" 2>/dev/null || true - - install_opencode_from_source - -} - - - -install_opencode_from_source() { - - command -v go >/dev/null 2>&1 || error "Go is required to build OpenCode from source. Install from https://go.dev/dl/" - - command -v git >/dev/null 2>&1 || error "Git is required to clone the OpenCode repository" - - - - local tmp_dir - - tmp_dir="$(mktemp -d)" - - register_rollback "$tmp_dir" - - - - debug "Cloning ${OPENCODE_REPO} (depth 1) into ${tmp_dir}" - - if ! git clone --depth 1 "https://github.com/${OPENCODE_REPO}.git" "$tmp_dir" 2>&1; then - - rm -rf "$tmp_dir" - - error "Failed to clone repository. Check your internet connection and git configuration." - - fi - - - - pushd "$tmp_dir" >/dev/null - - debug "Running 'go build -o ${OPENCODE_BIN} .'" - - if ! go build -o "$OPENCODE_BIN" . 2>&1; then - - popd >/dev/null - - rm -rf "$tmp_dir" - - error "Go build failed. You may need a newer Go version. See errors above." - - fi - - popd >/dev/null - - - - # Remove tmp_dir from rollback since we already cleaned it - - rm -rf "$tmp_dir" - - # Remove from rollback array - - local filtered=() - - for item in "${ROLLBACK_ITEMS[@]}"; do - - [[ "$item" != "$tmp_dir" ]] && filtered+=("$item") - - done - - ROLLBACK_ITEMS=("${filtered[@]}") - - - - chmod +x "$OPENCODE_BIN" - - register_rollback "$OPENCODE_BIN" - - - - step_done "OpenCode built from source and installed" - -} - - - -# ==================================================================== - -# Install CCProxy (binary from GitHub releases) - -# ==================================================================== - -install_ccproxy() { - - show_progress 2 6 "Installing CCProxy (Claude Work proxy)" - - - - mkdir -p "$CLAWDE_BIN_DIR" - - - - local ccproxy_exe="${CLAWDE_BIN_DIR}/ccproxy" - - if [[ -x "$ccproxy_exe" ]]; then - - local ver - - ver="$("$ccproxy_exe" --version 2>/dev/null || echo "unknown")" - - step_done "CCProxy already installed (${ver})" - - return - - fi - - - - # Fetch latest release info - - debug "Fetching latest CCProxy release..." - - local release_json - - release_json="$(curl -fsSL --connect-timeout 10 --max-time 15 \ - - "https://api.github.com/repos/ClintonSarkar/ccproxy-api/releases/latest" 2>/dev/null)" || { - - warn "Failed to fetch CCProxy release info" - - return - - } - - - - local tag_name - - tag_name="$(echo "$release_json" | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" - - debug "Latest CCProxy release: $tag_name" - - - - # Determine platform-specific asset - - local arch asset_name - - arch="$(uname -m)" - - case "$(uname -s):${arch}" in - - Linux:x86_64) asset_name="ccproxy-${tag_name}-x86_64-unknown-linux-gnu.tar.gz" ;; - - Linux:aarch64) asset_name="ccproxy-${tag_name}-x86_64-unknown-linux-gnu.tar.gz" ;; - - Darwin:x86_64) asset_name="ccproxy-${tag_name}-x86_64-apple-darwin.tar.gz" ;; - - Darwin:arm64|Darwin:aarch64) asset_name="ccproxy-${tag_name}-aarch64-apple-darwin.tar.gz" ;; - - *) warn "Unsupported platform: $(uname -s) ${arch}"; return ;; - - esac - - - - local download_url - - download_url="$(echo "$release_json" | grep -o "\"browser_download_url\": *\"[^\"]*${asset_name}[^\"]*\"" | sed -E 's/.*"([^"]+)".*/\1/' | head -1)" - - if [[ -z "$download_url" ]]; then - - warn "CCProxy binary not found for platform in release $tag_name" - - return - - fi - - - - # Download and extract - - local tmp_archive="/tmp/ccproxy-${tag_name}.tar.gz" - - debug "Downloading $asset_name..." - - if ! curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$tmp_archive" 2>/dev/null; then - - warn "Failed to download CCProxy" - - return - - fi - - - - register_rollback "$tmp_archive" - - register_rollback "$ccproxy_exe" - - - - debug "Extracting to $CLAWDE_BIN_DIR..." - - tar -xzf "$tmp_archive" -C "$CLAWDE_BIN_DIR" 2>/dev/null - - - - # The tarball may contain ccproxy at root or in a subfolder - - if [[ ! -x "$ccproxy_exe" ]]; then - - local found - - found="$(find "$CLAWDE_BIN_DIR" -name ccproxy -type f -executable | head -1)" - - if [[ -n "$found" ]]; then - - mv "$found" "$ccproxy_exe" - - chmod +x "$ccproxy_exe" - - fi - - fi - - - - if [[ -x "$ccproxy_exe" ]]; then - local ver - ver="$("$ccproxy_exe" --version 2>/dev/null || echo "unknown")" - - # Post-install check: detect if binary has no auth provider plugins - # (known upstream bug in v0.2.10 Windows/Linux builds) - if ! test_ccproxy_has_providers "$ccproxy_exe"; then - warn "CCProxy binary has no auth provider plugins (known upstream bug)." - warn "Upstream: https://github.com/CaddyGlow/ccproxy-api/issues/75" - if test_pipx_available; then - info "Replacing bare ccproxy with pipx-installed ccproxy-api[plugins-claude,plugins-codex]..." - if install_ccproxy_via_pipx; then - step_done "CCProxy ${tag_name} installed via pipx (full plugin set)" - # Cleanup - rm -f "$tmp_archive" - local filtered=() - for item in "${ROLLBACK_ITEMS[@]}"; do - [[ "$item" != "$tmp_archive" ]] && filtered+=("$item") - done - ROLLBACK_ITEMS=("${filtered[@]}") - return 0 - else - warn "pipx install failed; retry later with: pipx install ccproxy-api[plugins-claude,plugins-codex]" - fi - else - warn "Install Python 3.11+ and pipx, then run: pipx install ccproxy-api[plugins-claude,plugins-codex]" - fi - fi - - step_done "CCProxy ${tag_name} installed" - else - warn "ccproxy not found after extraction" - fi - - - - # Cleanup - - rm -f "$tmp_archive" - - local filtered=() - - for item in "${ROLLBACK_ITEMS[@]}"; do - - [[ "$item" != "$tmp_archive" ]] && filtered+=("$item") - - done - - ROLLBACK_ITEMS=("${filtered[@]}") - -} - - - - -# ==================================================================== -# CCProxy plugin fallback helpers (upstream Windows binary bug workaround) -# See: https://github.com/CaddyGlow/ccproxy-api/issues/75 -# ==================================================================== - -# Test whether a ccproxy binary has any auth provider plugins discoverable. -# Returns 0 (true) if 'ccproxy auth providers' reports at least one provider, -# 1 (false) if it returns "No OAuth providers found" or fails entirely. -test_ccproxy_has_providers() { - local binary_path="$1" - [[ ! -x "$binary_path" ]] && return 1 - - local output - output="$("$binary_path" auth providers 2>&1)" || return 1 - - # Check for "No OAuth providers found" or "No plugins found" - if echo "$output" | grep -qiE "no (oauth )?providers? found"; then - return 1 - fi - - # Check for real provider names (non-empty lines that aren't headers) - echo "$output" | grep -vE "^(Available|warning|Warning|Available OAuth Providers|providers found)$" | grep -q . -} - -# Check if pipx is on PATH (and Python 3.11+ is available). -test_pipx_available() { - local py_cmd="" - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - return 1 - fi - - command -v pipx >/dev/null 2>&1 || return 1 - - # Check Python version >= 3.11 - local pyver - pyver="$($py_cmd --version 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1)" - [[ -z "$pyver" ]] && return 1 - - local major="${pyver%%.*}" - local minor="${pyver##*.}" - [[ "$major" -lt 3 ]] && return 1 - [[ "$major" -eq 3 && "$minor" -lt 11 ]] && return 1 - - return 0 -} - -# Install ccproxy-api with full plugin set via pipx, then create a symlink -# in the clawde bin dir as ccproxy. Returns 0 on success, 1 on failure. -install_ccproxy_via_pipx() { - local py_cmd="python3" - command -v python3 >/dev/null 2>&1 || py_cmd="python" - - info "Installing ccproxy-api with full plugin set via pipx..." - if ! pipx install --python "$py_cmd" "ccproxy-api[plugins-claude,plugins-codex]" 2>&1; then - # pipx returns non-zero on upgrade if already installed; try 'pipx upgrade' instead - pipx upgrade ccproxy-api --python "$py_cmd" 2>&1 || return 1 - fi - - # Find the pipx-installed ccproxy - local pipx_ccproxy - pipx_ccproxy="$(command -v ccproxy)" - if [[ -z "$pipx_ccproxy" ]]; then - warn "pipx install succeeded but 'ccproxy' is not on PATH" - return 1 - fi - - # Create symlink in CLAWDE_BIN_DIR - local ccproxy_link="${CLAWDE_BIN_DIR}/ccproxy" - rm -f "$ccproxy_link" 2>/dev/null || true - if ln -s "$pipx_ccproxy" "$ccproxy_link" 2>/dev/null; then - ok "CCProxy installed via pipx (full plugin set)" - return 0 - else - # Fallback: copy instead of symlink - if cp "$pipx_ccproxy" "$ccproxy_link" 2>/dev/null && chmod +x "$ccproxy_link"; then - ok "CCProxy installed via pipx (full plugin set, copied)" - return 0 - fi - warn "Failed to create ccproxy symlink or copy" - return 1 - fi -} - -# ==================================================================== - -# Install clawde CLI wrapper (Bash script - no Python required) - -# ==================================================================== - -install_cli() { - - show_progress 3 6 "Installing clawde CLI" - - - - local clawde_url="https://raw.githubusercontent.com/ClintonSarkar/clawde/main/cli/clawde.sh" - - local clawde_path="${CLAWDE_BIN_DIR}/clawde" - - - - debug "Downloading clawde.sh to ${clawde_path}..." - - if ! curl -fsSL --connect-timeout 10 --max-time 30 \ - - "$clawde_url" -o "$clawde_path" 2>/dev/null; then - - warn "Failed to download clawde.sh" - - warn "clawde CLI was not installed. You can install it manually:" - - warn " curl -fsSL https://raw.githubusercontent.com/ClintonSarkar/clawde/main/cli/clawde.sh -o ${clawde_path} && chmod +x ${clawde_path}" - - return - - fi - - - - chmod +x "$clawde_path" - - register_rollback "$clawde_path" - - - - step_done "clawde CLI installed to ${clawde_path}" - -} - - - -# ==================================================================== - -# Config wizard interactive prompts - -# ==================================================================== - -do_interactive_config() { - - local auth_method="" cli_token_path="" port="" auto_start="" models="" - - local auth_choice="" - - - - show_progress 4 6 "Claude authentication" - - echo "" - - echo " Choose authentication method:" - - echo " 1. OAuth login (opens browser recommended)" - - echo " 2. Use existing Claude CLI token" - - echo "" - - while true; do - - read -rp " Select (default: 1): " auth_choice - - auth_choice="${auth_choice:-1}" - - case "$auth_choice" in - - 1) auth_method="oauth"; AUTH_PENDING=true; echo ""; info "You'll complete Claude authentication later. Run 'clawde auth' after install to log in."; break ;; - - 2) auth_method="cli_token"; break ;; - - *) warn "Please enter 1 (OAuth) or 2 (CLI token)" ;; - - esac - - done - - - - if [[ "$auth_method" == "cli_token" ]]; then - - local default_token_path="${HOME}/.claude/credentials.json" - - read -rp " Path to Claude CLI token [${default_token_path}]: " cli_token_path - - cli_token_path="${cli_token_path:-${default_token_path}}" - - if [[ -f "$cli_token_path" ]]; then - - ok "Found credentials file" - - else - - warn "File not found: ${cli_token_path} (you can set this later with 'clawde auth')" - - fi - - fi - - - - echo "" - - info "Configuration" - - echo "" - - - - # Port input with validation - - while true; do - - read -rp " Proxy port [8080]: " port - - port="${port:-8080}" - - if [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1024 && port <= 65535 )); then - - break - - else - - warn "Port must be a number between 1024 and 65535 (got: ${port})" - - fi - - done - - - - read -rp " Auto-start on boot? [y/N]: " auto_start_input - - auto_start_input="${auto_start_input:-N}" - - case "$auto_start_input" in - - [Yy]*) auto_start="true" ;; - - *) auto_start="false" ;; - - esac - - - - read -rp " Models to expose [all]: " models - - models="${models:-all}" - - - - write_config "$auth_method" "${cli_token_path:-}" "$port" "$auto_start" "$models" - -} - - - -# ==================================================================== - -# Write config file - -# ==================================================================== - -write_config() { - - local auth_method="$1" cli_token_path="$2" port="$3" auto_start="$4" models="$5" - - - - if [[ "${SKIP_CONFIG:-false}" == "true" ]]; then - - step_done "Configuration skipped (existing config preserved)" - - return 0 - - fi - - - - mkdir -p "$CLAWDE_CONFIG_DIR" - - register_rollback "$CLAWDE_CONFIG_DIR" - - - - # Only write cli_token_path if it's non-empty - - local token_line="" - - if [[ -n "$cli_token_path" ]]; then - - token_line="cli_token_path = \"${cli_token_path}\"" - - fi - - - - cat > "$CLAWDE_CONFIG_FILE" << EOF - -# clawde configuration generated by installer v${CLAWDE_VERSION} - -# Docs: https://github.com/ClintonSarkar/clawde - - - -[proxy] - -port = ${port} - -host = "127.0.0.1" - - - -[claude] - -auth_method = "${auth_method}" - -${token_line} - - - -[opencode] - -provider_name = "clawde" - -auto_start = ${auto_start} - - - -[models] - -expose = "${models}" - - - -[logging] - -level = "info" - -rotation_days = 7 - -EOF - - - - step_done "Configuration written to ${CLAWDE_CONFIG_FILE}" - - - - # Set up OpenCode provider config for ccproxy - - setup_opencode_provider "$port" - -} - - - -# ==================================================================== - -# Setup OpenCode provider config for ccproxy - -# ==================================================================== - -setup_opencode_provider() { - - local port="$1" - - - - # Determine OpenCode config path - - local opencode_config_dir="${HOME}/.config/opencode" - - local opencode_config_file="${opencode_config_dir}/opencode.json" - - - - # Create config directory if it doesn't exist - - mkdir -p "$opencode_config_dir" - - - - # Check if config already exists - - local existing_config="{}" - - if [[ -f "$opencode_config_file" ]]; then - - existing_config="$(cat "$opencode_config_file" 2>/dev/null || echo "{}")" - - # Validate JSON - - if ! echo "$existing_config" | jq -e . >/dev/null 2>&1; then - - warn "Could not parse existing OpenCode config, will create new" - - existing_config="{}" - - fi - - fi - - - - # Ensure provider section exists and add/update ccproxy provider - - local updated_config - - updated_config="$(echo "$existing_config" | jq --arg port "$port" ' - - .provider = (.provider // {}) | - - .provider["ccproxy-claude"] = { - - npm: "@ai-sdk/openai-compatible", - - name: "CCProxy (Claude Max)", - - options: { baseURL: "http://127.0.0.1:$port/api/v1" }, - - models: { - - "claude-sonnet-4-20250514": { name: "Claude Sonnet 4" }, - - "claude-opus-4-20250514": { name: "Claude Opus 4" } - - } - - } - - ' 2>/dev/null)" - - - - if [[ -z "$updated_config" ]] || [[ "$updated_config" == "null" ]]; then - - warn "Failed to generate OpenCode config (jq may not be installed), skipping" - - return 0 - - fi - - - - echo "$updated_config" > "$opencode_config_file" - - ok "OpenCode provider config written to $opencode_config_file" - -} - - - -# ==================================================================== - -# Service setup (systemd or WSL shell profile) - -# ==================================================================== - -setup_service() { - - show_progress 5 6 "Setting up service management" - - - - # Read auto_start from config - - local auto_start - - auto_start="false" - - if [[ -f "$CLAWDE_CONFIG_FILE" ]]; then - - auto_start="$(grep 'auto_start' "$CLAWDE_CONFIG_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "false")" - - elif [[ "$NONINTERACTIVE" == "true" ]]; then - - auto_start="${CLAWDE_AUTO_START:-false}" - - fi - - - - if [[ "$auto_start" != "true" ]]; then - - step_done "Auto-start disabled (use 'clawde start' to launch)" - - return 0 - - fi - - - - # --- WSL without systemd --- - - if $IS_WSL && ! command -v systemctl >/dev/null 2>&1; then - - info "WSL without systemd detected setting up shell-profile auto-start" - - - - local rc_file="" - - for f in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do - - [[ -f "$f" ]] && rc_file="$f" && break - - done - - - - if [[ -z "$rc_file" ]]; then - - rc_file="${HOME}/.bashrc" - - touch "$rc_file" - - fi - - - - local port - - port="$(grep -A5 '^\s*\[proxy\]' "$CLAWDE_CONFIG_FILE" 2>/dev/null | grep -E '^\s*port\s*=' | head -1 | cut -d'=' -f2 | tr -d ' ' || echo "8080")" - - - - if ! grep -qs "clawde" "$rc_file" 2>/dev/null; then - - { - - echo "" - - echo "# Start clawde proxy on shell launch (added by clawde installer v${CLAWDE_VERSION})" - - echo "command -v ccproxy >/dev/null 2>&1 && nohup ccproxy serve --port ${port} >/dev/null 2>&1 &" - - } >> "$rc_file" - - info "Added clawde auto-start to ${rc_file}" - - else - - warn "Auto-start entry already exists in ${rc_file}" - - fi - - - - step_done "Auto-start configured for WSL" - - info "Note: proxy will start when you open a terminal. To start manually: ccproxy serve --port ${port}" - - return 0 - - fi - - - - # --- systemd --- - - if command -v systemctl >/dev/null 2>&1; then - - local systemd_dir="${HOME}/.config/systemd/user" - - mkdir -p "$systemd_dir" - - register_rollback "$systemd_dir" - - - - local port - - port="$(grep -A5 '^\s*\[proxy\]' "$CLAWDE_CONFIG_FILE" 2>/dev/null | grep -E '^\s*port\s*=' | head -1 | cut -d'=' -f2 | tr -d ' ' || echo "8080")" - - - - cat > "$SYSTEMD_SERVICE_FILE" << EOF - -[Unit] - -Description=clawde CCProxy (Claude Work proxy) - -Documentation=https://github.com/ClintonSarkar/clawde - -After=network-online.target - -Wants=network-online.target - - - -[Service] - -Type=simple - -ExecStart=%h/.local/bin/ccproxy serve --port ${port} - -Restart=on-failure - -RestartSec=10 - -Environment=PYTHONUNBUFFERED=1 - - - -[Install] - -WantedBy=default.target - -EOF - - - - debug "systemd unit written to ${SYSTEMD_SERVICE_FILE}" - - - - # Enable lingering so user services start at boot even without login - - loginctl enable-linger "$(whoami)" 2>/dev/null || true - - - - systemctl --user daemon-reload 2>/dev/null || warn "systemctl daemon-reload failed; check systemd is functional" - - systemctl --user enable "${SYSTEMD_SERVICE_NAME}.service" 2>/dev/null || warn "Could not enable systemd service (is systemd available?)" - - step_done "Systemd user service configured" - - info " Service: ${SYSTEMD_SERVICE_NAME}.service" - - info " Start: systemctl --user start ${SYSTEMD_SERVICE_NAME}.service" - - return 0 - - fi - - - - warn "No service manager detected you'll need to start clawde manually" - -} - - - -# ==================================================================== - -# Final message - -# ==================================================================== - -final_message() { - - echo "" - - ok "clawde v${CLAWDE_VERSION} is installed and ready!" - - echo "" - - echo " ${BOLD}Quick start:${NC}" - - echo " clawde start launch proxy + OpenCode" - - echo " clawde stop stop all services" - - echo " clawde status check health" - - echo "" - - echo " ${BOLD}Management:${NC}" - - echo " clawde config reconfigure" - - echo " clawde auth re-authenticate Claude" - - echo " clawde update update to latest version" - - echo " clawde logs tail logs" - - echo "" - - if [[ "${AUTH_PENDING:-false}" == "true" ]]; then - - echo " ${BOLD}Note:${NC} Claude authentication not yet completed. Run 'clawde auth' to connect your Claude account." - - echo "" - - fi - - - - echo " ${BOLD}Resources:${NC}" - - echo " Config: ${CLAWDE_CONFIG_FILE}" - - echo " Logs: ${CLAWDE_DATA_DIR}/logs/" - - echo " Docs: https://github.com/ClintonSarkar/clawde" - - echo "" - -} - - - -# ==================================================================== - -# Uninstall - -# ==================================================================== - -uninstall() { - - banner - - info "Uninstalling clawde..." - - echo "" - - - - local removed_anything=false - - - - # --- Remove OpenCode binary --- - - if [[ -f "$OPENCODE_BIN" ]]; then - - rm -f "$OPENCODE_BIN" - - ok "Removed OpenCode binary: ${OPENCODE_BIN}" - - removed_anything=true - - fi - - - - # --- Remove config --- - - if [[ -d "$CLAWDE_CONFIG_DIR" ]]; then - - rm -rf "$CLAWDE_CONFIG_DIR" - - ok "Removed config directory: ${CLAWDE_CONFIG_DIR}" - - removed_anything=true - - fi - - - - # --- Remove data --- - - if [[ -d "$CLAWDE_DATA_DIR" ]]; then - - rm -rf "$CLAWDE_DATA_DIR" - - ok "Removed data directory: ${CLAWDE_DATA_DIR}" - - removed_anything=true - - fi - - - - # --- Remove systemd service --- - - if [[ -f "$SYSTEMD_SERVICE_FILE" ]]; then - - systemctl --user disable "${SYSTEMD_SERVICE_NAME}.service" 2>/dev/null || true - - rm -f "$SYSTEMD_SERVICE_FILE" - - systemctl --user daemon-reload 2>/dev/null || true - - ok "Removed systemd user service" - - removed_anything=true - - fi - - - - # --- Clean up shell rc PATH additions --- - - for rc in "${HOME}/.bashrc" "${HOME}/.bash_profile" "${HOME}/.zshrc" "${HOME}/.config/fish/config.fish"; do - - if [[ -f "$rc" ]]; then - - if grep -qsF "$CLAWDE_BIN_DIR" "$rc" 2>/dev/null; then - - cp "$rc" "${rc}.clawde-backup" - - # Remove the block added by the installer (comment line + path line) - - sed -i "\|# Added by clawde installer|,+1 d" "$rc" 2>/dev/null || true - - # Also remove any remaining reference (defensive) - - sed -i "\|${CLAWDE_BIN_DIR}|d" "$rc" 2>/dev/null || true - - ok "Removed PATH entry from ${rc} (backup saved to ${rc}.clawde-backup)" - - removed_anything=true - - fi - - fi - - done - - - - # --- Clean up WSL auto-start from shell rc --- - - for rc in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do - - if [[ -f "$rc" ]]; then - - if grep -qs "clawde" "$rc" 2>/dev/null; then - - cp "$rc" "${rc}.clawde-backup" - - sed -i "\|# Start clawde|,+1 d" "$rc" 2>/dev/null || true - - sed -i "\|ccproxy serve|d" "$rc" 2>/dev/null || true - - ok "Removed auto-start entries from ${rc} (backup saved to ${rc}.clawde-backup)" - - removed_anything=true - - fi - - fi - - done - - - - # --- Uninstall CCProxy (remove binary + legacy pip/uv/pipx if present) --- - - # Remove binary install - - local ccproxy_exe="${CLAWDE_BIN_DIR}/ccproxy" - - if [[ -x "$ccproxy_exe" ]]; then - - rm -f "$ccproxy_exe" - - ok "Removed ccproxy binary" - - removed_anything=true - - fi - - # Legacy cleanup: if someone installed via pip/uv/pipx before binary install - - if command -v uv >/dev/null 2>&1; then - - if uv tool uninstall "$CCPROXY_PACKAGE" 2>/dev/null; then - - ok "Removed legacy CCProxy via uv"; removed_anything=true - - fi - - fi - - if command -v pipx >/dev/null 2>&1; then - - if pipx uninstall "$CCPROXY_PACKAGE" 2>/dev/null; then - - ok "Removed legacy CCProxy via pipx"; removed_anything=true - - fi - - fi - - local pip_cmd="" - - command -v pip3 >/dev/null 2>&1 && pip_cmd="pip3" || command -v pip >/dev/null 2>&1 && pip_cmd="pip" || true - - if [[ -n "$pip_cmd" ]]; then - - if $pip_cmd uninstall -y "$CCPROXY_PACKAGE" 2>/dev/null; then - - ok "Removed legacy CCProxy via ${pip_cmd}"; removed_anything=true - - - fi - - fi - - - - if ! $removed_anything; then - - info "Nothing to uninstall clawde is not installed." - - else - - echo "" - - ok "clawde has been completely uninstalled." - - fi - -} - - - -# ==================================================================== - -# Validate environment variables (for non-interactive mode) - -# ==================================================================== - -validate_env_vars() { - - # Validate CLAWDE_PORT - - if [[ -n "${CLAWDE_PORT:-}" ]]; then - - if ! [[ "$CLAWDE_PORT" =~ ^[0-9]+$ ]] || (( CLAWDE_PORT < 1024 || CLAWDE_PORT > 65535 )); then - - warn "CLAWDE_PORT=${CLAWDE_PORT} is invalid; must be 1024-65535. Defaulting to 8080." - - CLAWDE_PORT="8080" - - fi - - fi - - - - # Validate CLAWDE_AUTH_METHOD - - if [[ -n "${CLAWDE_AUTH_METHOD:-}" ]]; then - - if [[ "$CLAWDE_AUTH_METHOD" != "oauth" && "$CLAWDE_AUTH_METHOD" != "cli_token" ]]; then - - warn "CLAWDE_AUTH_METHOD=${CLAWDE_AUTH_METHOD} is invalid. Defaulting to 'oauth'." - - CLAWDE_AUTH_METHOD="oauth" - - fi - - fi - - - - # Validate CLAWDE_AUTO_START - - if [[ -n "${CLAWDE_AUTO_START:-}" ]]; then - - if [[ "$CLAWDE_AUTO_START" != "true" && "$CLAWDE_AUTO_START" != "false" ]]; then - - warn "CLAWDE_AUTO_START=${CLAWDE_AUTO_START} is invalid. Defaulting to 'false'." - - CLAWDE_AUTO_START="false" - - fi - - fi - -} - - - -# ==================================================================== - -# Main entry point - -# ==================================================================== - -main() { - - parse_args "$@" - - - - if [[ "$UNINSTALL" == "true" ]]; then - - uninstall - - exit 0 - - fi - - - - banner - - detect_os - - check_wsl - - show_version - - check_deps - - - - # Installation steps - - setup_path - - check_existing - - - - install_opencode - - install_ccproxy - - install_cli - - - - # Config interactive or non-interactive - - if [[ "$NONINTERACTIVE" == "true" ]]; then - - if [[ "${SKIP_CONFIG:-false}" != "true" ]]; then - - validate_env_vars - - if [[ "${CLAWDE_AUTH_METHOD:-oauth}" == "oauth" ]]; then - - AUTH_PENDING=true - - fi - - show_progress 4 6 "Claude authentication" - - write_config \ - - "${CLAWDE_AUTH_METHOD:-oauth}" \ - - "${CLAWDE_CLI_TOKEN_PATH:-}" \ - - "${CLAWDE_PORT:-8080}" \ - - "${CLAWDE_AUTO_START:-false}" \ - - "${CLAWDE_MODELS:-all}" - - else - - show_progress 4 6 "Claude authentication" - - step_done "Configuration skipped (existing config preserved)" - - fi - - else - - do_interactive_config - - fi - - - - setup_service - - - - show_progress 6 6 "Finishing" - - step_done "Installation complete" - - - - # Mark success so the EXIT trap won't roll back - - INSTALL_COMPLETED=true - - clear_rollback - - final_message - -} - - - -main "$@" - - +#!/usr/bin/env bash + +# clawde installer - Linux / WSL + +# Claude Work - OpenCode bridge + +# + +# Usage: + +# curl -fsSL https://clawde.dev/install.sh | bash + +# curl -fsSL https://clawde.dev/install.sh | bash -s -- --yes + +# curl -fsSL https://clawde.dev/install.sh | bash -s -- --uninstall + +# + +# Environment variables (for CI / automation): + +# CLAWDE_PORT Proxy port (default: 8080) + +# CLAWDE_AUTH_METHOD Auth method: oauth | cli_token (default: oauth) + +# CLAWDE_CLI_TOKEN_PATH Path to Claude CLI credentials (default: ~/.claude/credentials.json) + +# CLAWDE_AUTO_START Auto-start on boot: true | false (default: false) + +# CLAWDE_MODELS Models to expose (default: all) + +set -euo pipefail + + + +# ==================================================================== + +# Constants + +# ==================================================================== + +CLAWDE_VERSION="0.1.0" + +OPENCODE_REPO="ClintonSarkar/opencode" + +CCPROXY_PACKAGE="ccproxy-api" + + + +CLAWDE_CONFIG_DIR="${HOME}/.config/clawde" + +CLAWDE_DATA_DIR="${HOME}/.local/share/clawde" + +CLAWDE_BIN_DIR="${HOME}/.local/bin" + +OPENCODE_BIN="${CLAWDE_BIN_DIR}/opencode" + +CLAWDE_CONFIG_FILE="${CLAWDE_CONFIG_DIR}/clawde.toml" + +SYSTEMD_SERVICE_NAME="clawde-proxy" + +SYSTEMD_SERVICE_FILE="${HOME}/.config/systemd/user/${SYSTEMD_SERVICE_NAME}.service" + + + +# ==================================================================== + +# Flags & State + +# ==================================================================== + +VERBOSE=false + +NONINTERACTIVE=false + +UNINSTALL=false + +INSTALL_COMPLETED=false + +ROLLBACK_ITEMS=() + +IS_WSL=false + +OS="" + +ARCH="" + +SKIP_OPENCODE=false + +SKIP_CONFIG=false + +EXISTING_OPENCODE_PATH="" + +AUTH_PENDING=false + +PROGRESS_CURRENT=0 + +PROGRESS_TOTAL=6 + + + +# ==================================================================== + +# Colors & Logging + +# ==================================================================== + +RED='\033[0;31m' + +GREEN='\033[0;32m' + +YELLOW='\033[1;33m' + +CYAN='\033[0;36m' + +BOLD='\033[1m' + +NC='\033[0m' + + + +info() { echo -e "${CYAN}[INFO]${NC} $*"; } + +ok() { echo -e "${GREEN}[OK]${NC} $*"; } + +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } + +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } + +debug() { [[ "$VERBOSE" == "true" ]] && echo -e "[DEBUG] $*"; } + + + +# ==================================================================== + +# Progress bar + +# ==================================================================== + +show_progress() { + + PROGRESS_CURRENT=$1 + + PROGRESS_TOTAL=$2 + + PROGRESS_LABEL="$3" + + _redraw_bar + +} + + + +_redraw_bar() { + + local percent filled empty bar bar_width=30 + + percent=$(( (PROGRESS_CURRENT - 1) * 100 / PROGRESS_TOTAL )) + + [[ $percent -lt 0 ]] && percent=0 + + filled=$(( percent * bar_width / 100 )) + + empty=$(( bar_width - filled )) + + bar="" + + for ((i=0; i %s... [%s] %d%%\n" "$PROGRESS_LABEL" "$bar" "$percent" + +} + + + +_draw_bar() { + + local percent filled empty bar bar_width=30 current=$1 + + percent=$(( current * 100 / PROGRESS_TOTAL )) + + [[ $percent -gt 100 ]] && percent=100 + + filled=$(( percent * bar_width / 100 )) + + empty=$(( bar_width - filled )) + + bar="" + + for ((i=0; i/dev/null || true + + debug " Removed file: $item" + + elif [[ -d "$item" ]]; then + + # Only remove empty dirs we created, unless it's our config/data + + if [[ "$item" == "$CLAWDE_CONFIG_DIR" ]] || [[ "$item" == "$CLAWDE_DATA_DIR" ]]; then + + rm -rf "$item" 2>/dev/null || true + + debug " Removed directory: $item" + + else + + rmdir "$item" 2>/dev/null || true + + debug " Removed (empty) directory: $item" + + fi + + fi + + done + + warn "Rollback complete. Run the installer again to retry." + +} + + + +trap cleanup_on_exit EXIT + + + +register_rollback() { + + ROLLBACK_ITEMS+=("$1") + + debug "Registered rollback: $1" + +} + + + +clear_rollback() { + + ROLLBACK_ITEMS=() + +} + + + +# ==================================================================== + +# OS / Architecture detection + +# ==================================================================== + +detect_os() { + + local os arch os_name + + os="$(uname -s)" + + case "$os" in + + Linux*) OS="linux";; + + Darwin*) OS="macos";; + + *) error "Unsupported operating system: $os. clawde requires Linux, macOS, or WSL.";; + + esac + + + + arch="$(uname -m)" + + case "$arch" in + + x86_64|amd64) ARCH="x64";; + + aarch64|arm64) ARCH="arm64";; + + *) + + warn "Unrecognized architecture: $arch assuming x64 (may cause runtime issues)" + + ARCH="x64" + + ;; + + esac + + + + os_name="$(uname -o 2>/dev/null || echo "$OS")" + + info "Detected: $os_name on $arch ($ARCH)" + +} + + + +# ==================================================================== + +# WSL detection + +# ==================================================================== + +check_wsl() { + + if [[ -f /proc/version ]] && grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null; then + + IS_WSL=true + + local wsl_ver + + wsl_ver="$(wsl.exe --version 2>/dev/null | head -1 || echo "WSL (version unknown)")" + + info "Running under $wsl_ver" + + else + + IS_WSL=false + + fi + +} + + + +# ==================================================================== + +# Dependency checking + +# ==================================================================== + +check_deps() { + + local missing=() optional=() + + local has_uv=false has_pipx=false has_pip=false has_python=false + + + + # --- Required --- + + command -v curl >/dev/null 2>&1 || missing+=("curl") + + command -v bash >/dev/null 2>&1 || missing+=("bash") + + + + # Python (optional - not needed for clawde CLI or ccproxy binary) + + if command -v python3 >/dev/null 2>&1; then + + has_python=true + + PYTHON=python3 + + elif command -v python >/dev/null 2>&1; then + + local pyver + + pyver="$(python --version 2>&1 | sed -nE 's/[^0-9]*([0-9]+\.[0-9]+).*/\1/p')" + + if [[ "${pyver%%.*}" -ge 3 ]]; then + + has_python=true + + PYTHON=python + + fi + + fi + + + + # Python package managers (optional, for legacy uninstall only) + + command -v uv >/dev/null 2>&1 && has_uv=true + + command -v pipx >/dev/null 2>&1 && has_pipx=true + + if command -v pip3 >/dev/null 2>&1; then + + has_pip=true; PIP=pip3 + + elif command -v pip >/dev/null 2>&1; then + + has_pip=true; PIP=pip + + fi + + + + + # git (only needed for source builds) + + command -v git >/dev/null 2>&1 || optional+=("git https://git-scm.com/ (for source builds)") + + + + # --- Report --- + + if [[ ${#missing[@]} -gt 0 ]]; then + + echo "" + + error "Missing required dependencies: ${missing[*]} + + + +Install them with your package manager: + + sudo apt install ${missing[*]} # Debian/Ubuntu + + sudo dnf install ${missing[*]} # Fedora + + sudo pacman -S ${missing[*]} # Arch Linux + + brew install ${missing[*]} # macOS Homebrew" + + fi + + + + if [[ ${#optional[@]} -gt 0 ]]; then + + echo "" + + warn "Optional dependencies not found:" + + for dep in "${optional[@]}"; do + + echo " - $dep" + + done + + warn "The installer will attempt to install what it needs." + + echo "" + + fi + + + + debug "Dependency check: curl=$(command -v curl), python=${PYTHON:-none}, uv=$has_uv, pipx=$has_pipx, pip=$has_pip, git=$(command -v git >/dev/null 2>&1 && echo yes || echo no)" + +} + + + +# ==================================================================== + +# Version display + +# ==================================================================== + +show_version() { + + info "clawde installer v${CLAWDE_VERSION}" + + if [[ "$VERBOSE" == "true" ]]; then + + debug " Python: $($PYTHON --version 2>/dev/null || echo 'not found')" + + command -v uv >/dev/null 2>&1 && debug " uv: $(uv --version 2>/dev/null || echo 'unknown')" + + command -v pipx >/dev/null 2>&1 && debug " pipx: $(pipx --version 2>/dev/null || echo 'unknown')" + + debug " Shell: $SHELL" + + debug " WSL: $IS_WSL" + + debug " OS/Arch: $OS/$ARCH" + + fi + +} + + + +# ==================================================================== + +# Idempotency detect existing installation + +# ==================================================================== + +check_existing() { + + local have_opencode=false + + local have_config=false + + + + # Check for OpenCode in PATH (system-wide) + + local path_opencode="" + + path_opencode="$(command -v opencode 2>/dev/null || true)" + + if [[ -n "$path_opencode" ]] && [[ "$path_opencode" != "$OPENCODE_BIN" ]]; then + + warn "OpenCode found in PATH at: ${path_opencode}" + + EXISTING_OPENCODE_PATH="$path_opencode" + + fi + + + + if [[ -x "$OPENCODE_BIN" ]]; then + + have_opencode=true + + fi + + + + if [[ -f "$CLAWDE_CONFIG_FILE" ]]; then + + have_config=true + + fi + + + + if ! $have_opencode && ! $have_config; then + + if [[ -n "${EXISTING_OPENCODE_PATH:-}" ]]; then + + echo "" + + echo " What would you like to do?" + + echo " 1. [I]nstall new OpenCode binary (clawde's own copy)" + + echo " 2. [U]se existing OpenCode from PATH" + + echo " 3. [C]ancel" + + echo "" + + read -rp " Select (default: 2): " action + + action="${action:-2}" + + case "$action" in + + [Ii]|1) + + debug "User chose install new binary" + + ;; + + [Uu]|2) + + info "Using existing OpenCode from: ${EXISTING_OPENCODE_PATH}" + + SKIP_OPENCODE=true + + return 0 + + ;; + + [Cc]|3) + + info "Installation cancelled by user." + + exit 0 + + ;; + + *) + + warn "Invalid choice '$action', defaulting to use existing" + + info "Using existing OpenCode from: ${EXISTING_OPENCODE_PATH}" + + SKIP_OPENCODE=true + + return 0 + + ;; + + esac + + fi + + debug "No existing clawde installation detected" + + return 0 + + fi + + + + echo "" + + if $have_opencode; then + + local ver + + ver="$("$OPENCODE_BIN" version 2>/dev/null || echo "version unknown")" + + warn "OpenCode is already installed: ${OPENCODE_BIN} (${ver})" + + fi + + if $have_config; then + + warn "Existing config found at ${CLAWDE_CONFIG_FILE}" + + fi + + + + if [[ "$NONINTERACTIVE" == "true" ]]; then + + if [[ -n "${EXISTING_OPENCODE_PATH:-}" ]]; then + + info "OpenCode found in PATH at: ${EXISTING_OPENCODE_PATH} using existing binary" + + SKIP_OPENCODE=true + + else + + warn "Non-interactive mode reinstalling OpenCode and overwriting config" + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + fi + + return 0 + + fi + + + + echo "" + + echo " What would you like to do?" + + echo " 1. [R]einstall / update (removes existing installation)" + + echo " 2. [S]kip OpenCode and keep existing config" + + echo " 3. [U]se existing OpenCode from PATH" + + echo " 4. [C]ancel" + + echo "" + + read -rp " Select (default: 1): " action + + action="${action:-1}" + + + + case "$action" in + + [Rr]|1|"") + + debug "User chose reinstall" + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + ;; + + [Ss]|2) + + info "Keeping existing installation skipping OpenCode and config" + + SKIP_OPENCODE=true + + SKIP_CONFIG=true + + return 0 + + ;; + + [Uu]|3) + + if [[ -z "${EXISTING_OPENCODE_PATH:-}" ]]; then + + warn "No existing OpenCode found in PATH defaulting to reinstall" + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + else + + info "Using existing OpenCode from: ${EXISTING_OPENCODE_PATH}" + + SKIP_OPENCODE=true + + # Do NOT set SKIP_CONFIG still run config wizard + + fi + + ;; + + [Cc]|4) + + info "Installation cancelled by user." + + exit 0 + + ;; + + *) + + warn "Invalid choice '$action', defaulting to reinstall" + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + ;; + + esac + + + + if $have_config; then + + echo "" + + printf " Overwrite existing config? [y/N]: " + + read -r overwrite + + case "${overwrite:-N}" in + + [Yy]*) SKIP_CONFIG=false ;; + + *) SKIP_CONFIG=true; info "Keeping existing config" ;; + + esac + + fi + +} + + + +# ==================================================================== + +# PATH management + +# ==================================================================== + +setup_path() { + + mkdir -p "$CLAWDE_BIN_DIR" + + # Do NOT register the shared bin dir (~/.local/bin) for rollback: it may hold + # unrelated tools. Rollback removes only the specific files we install into it. + + + + # If existing OpenCode is already in PATH, skip PATH management + + if [[ -n "${EXISTING_OPENCODE_PATH:-}" ]] && command -v opencode >/dev/null 2>&1; then + + debug "Existing OpenCode already in PATH skipping PATH management for binary dir" + + return 0 + + fi + + + + if [[ ":$PATH:" != *":${CLAWDE_BIN_DIR}:"* ]]; then + + warn "${CLAWDE_BIN_DIR} is not in your PATH" + + + + local rc_files=() + + [[ -f "${HOME}/.bashrc" ]] && rc_files+=("${HOME}/.bashrc") + + [[ -f "${HOME}/.bash_profile" ]] && rc_files+=("${HOME}/.bash_profile") + + [[ -f "${HOME}/.zshrc" ]] && rc_files+=("${HOME}/.zshrc") + + [[ -f "${HOME}/.config/fish/config.fish" ]] && rc_files+=("${HOME}/.config/fish/config.fish") + + + + local path_line="export PATH=\"${CLAWDE_BIN_DIR}:\$PATH\"" + + local found_rc=false + + + + if [[ ${#rc_files[@]} -gt 0 ]]; then + + for rc in "${rc_files[@]}"; do + + if grep -qsF "$CLAWDE_BIN_DIR" "$rc" 2>/dev/null; then + + found_rc=true + + continue + + fi + + { + + echo "" + + echo "# Added by clawde installer v${CLAWDE_VERSION}" + + echo "${path_line}" + + } >> "$rc" + + ok "Added ${CLAWDE_BIN_DIR} to PATH in ${rc}" + + found_rc=true + + done + + + + if $found_rc; then + + echo "" + + warn "To use clawde immediately: source ${rc_files[0]}" + + fi + + fi + + + + if ! $found_rc; then + + echo "" + + warn "No shell rc file found. Add this to your shell profile:" + + echo " ${path_line}" + + fi + + + + # Export for current process + + export PATH="${CLAWDE_BIN_DIR}:$PATH" + + fi + +} + + + +# ==================================================================== + +# Install OpenCode (binary from GitHub releases) + +# ==================================================================== + +install_opencode() { + + show_progress 1 6 "Installing OpenCode binary" + + + + if [[ "${SKIP_OPENCODE:-false}" == "true" ]]; then + + step_done "OpenCode skipped (existing installation preserved)" + + return 0 + + fi + + + + local latest_tag download_url + + latest_tag="" + + download_url="" + + + + # Fetch latest release tag + + info "Checking GitHub releases for ${OPENCODE_REPO}..." + + latest_tag="$(curl -fsSL --connect-timeout 10 --max-time 30 \ + "https://api.github.com/repos/${OPENCODE_REPO}/releases/latest" \ + | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4)" || true + + + + if [[ -z "$latest_tag" ]]; then + + warn "Could not find a pre-built release for ${OPENCODE_REPO}" + + warn "Falling back to source build..." + + install_opencode_from_source + + return + + fi + + + + # Build asset name: try OS-ARCH first, then platform-specific names + + local binary_name="opencode-${OS}-${ARCH}" + + download_url="https://github.com/${OPENCODE_REPO}/releases/download/${latest_tag}/${binary_name}" + + + + debug "Attempting download: ${download_url}" + + + + mkdir -p "$CLAWDE_BIN_DIR" + + + + if curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$OPENCODE_BIN" 2>/dev/null; then + + chmod +x "$OPENCODE_BIN" + + register_rollback "$OPENCODE_BIN" + + step_done "OpenCode ${latest_tag} installed" + + return + + fi + + + + # If first attempt failed, try with 'v' prefix or alternate naming + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + + + # Try alternative naming conventions (some releases use 'linux' or omit OS) + + local alt_names=() + + alt_names+=("opencode-linux-${ARCH}") + + alt_names+=("opencode-${ARCH}") + + + + for alt_name in "${alt_names[@]}"; do + + download_url="https://github.com/${OPENCODE_REPO}/releases/download/${latest_tag}/${alt_name}" + + debug "Retrying with: ${download_url}" + + if curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$OPENCODE_BIN" 2>/dev/null; then + + chmod +x "$OPENCODE_BIN" + + register_rollback "$OPENCODE_BIN" + + step_done "OpenCode ${latest_tag} installed" + + return + + fi + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + done + + + + warn "Binary download failed for release ${latest_tag}" + + warn "Falling back to source build..." + + rm -f "$OPENCODE_BIN" 2>/dev/null || true + + install_opencode_from_source + +} + + + +install_opencode_from_source() { + + command -v go >/dev/null 2>&1 || error "Go is required to build OpenCode from source. Install from https://go.dev/dl/" + + command -v git >/dev/null 2>&1 || error "Git is required to clone the OpenCode repository" + + + + local tmp_dir + + tmp_dir="$(mktemp -d)" + + register_rollback "$tmp_dir" + + + + debug "Cloning ${OPENCODE_REPO} (depth 1) into ${tmp_dir}" + + if ! git clone --depth 1 "https://github.com/${OPENCODE_REPO}.git" "$tmp_dir" 2>&1; then + + rm -rf "$tmp_dir" + + error "Failed to clone repository. Check your internet connection and git configuration." + + fi + + + + pushd "$tmp_dir" >/dev/null + + debug "Running 'go build -o ${OPENCODE_BIN} .'" + + if ! go build -o "$OPENCODE_BIN" . 2>&1; then + + popd >/dev/null + + rm -rf "$tmp_dir" + + error "Go build failed. You may need a newer Go version. See errors above." + + fi + + popd >/dev/null + + + + # Remove tmp_dir from rollback since we already cleaned it + + rm -rf "$tmp_dir" + + # Remove from rollback array + + local filtered=() + + for item in "${ROLLBACK_ITEMS[@]}"; do + + [[ "$item" != "$tmp_dir" ]] && filtered+=("$item") + + done + + ROLLBACK_ITEMS=("${filtered[@]}") + + + + chmod +x "$OPENCODE_BIN" + + register_rollback "$OPENCODE_BIN" + + + + step_done "OpenCode built from source and installed" + +} + + + +# ==================================================================== + +# Install CCProxy (binary from GitHub releases) + +# ==================================================================== + +install_ccproxy() { + + show_progress 2 6 "Installing CCProxy (Claude Work proxy)" + + + + mkdir -p "$CLAWDE_BIN_DIR" + + + + local ccproxy_exe="${CLAWDE_BIN_DIR}/ccproxy" + + if [[ -x "$ccproxy_exe" ]]; then + + local ver + + ver="$("$ccproxy_exe" --version 2>/dev/null || echo "unknown")" + + step_done "CCProxy already installed (${ver})" + + return + + fi + + + + # Fetch latest release info + + debug "Fetching latest CCProxy release..." + + local release_json + + release_json="$(curl -fsSL --connect-timeout 10 --max-time 15 \ + "https://api.github.com/repos/ClintonSarkar/ccproxy-api/releases/latest" 2>/dev/null)" || { + + warn "Failed to fetch CCProxy release info" + + return + + } + + + + local tag_name + + tag_name="$(echo "$release_json" | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + + debug "Latest CCProxy release: $tag_name" + + + + # Determine platform-specific asset + + local arch asset_name + + arch="$(uname -m)" + + case "$(uname -s):${arch}" in + + Linux:x86_64) asset_name="ccproxy-${tag_name}-x86_64-unknown-linux-gnu.tar.gz" ;; + + Linux:aarch64) asset_name="ccproxy-${tag_name}-aarch64-unknown-linux-gnu.tar.gz" ;; + + Darwin:x86_64) asset_name="ccproxy-${tag_name}-x86_64-apple-darwin.tar.gz" ;; + + Darwin:arm64|Darwin:aarch64) asset_name="ccproxy-${tag_name}-aarch64-apple-darwin.tar.gz" ;; + + *) warn "Unsupported platform: $(uname -s) ${arch}"; return ;; + + esac + + + + local download_url + + download_url="$(echo "$release_json" | grep -o "\"browser_download_url\": *\"[^\"]*${asset_name}[^\"]*\"" | sed -E 's/.*"([^"]+)".*/\1/' | head -1)" + + if [[ -z "$download_url" ]]; then + + warn "No CCProxy binary published for $(uname -s) ${arch} in release $tag_name (looked for ${asset_name})" + + warn "If you are on ARM with no ARM build available, install via pipx: pipx install \"ccproxy-api[plugins-claude,plugins-codex]\"" + + return + + fi + + + + # Download and extract + + local tmp_archive="/tmp/ccproxy-${tag_name}.tar.gz" + + debug "Downloading $asset_name..." + + if ! curl -fsSL --connect-timeout 10 --max-time 60 "$download_url" -o "$tmp_archive" 2>/dev/null; then + + warn "Failed to download CCProxy" + + return + + fi + + + + register_rollback "$tmp_archive" + + register_rollback "$ccproxy_exe" + + + + debug "Extracting to $CLAWDE_BIN_DIR..." + + tar -xzf "$tmp_archive" -C "$CLAWDE_BIN_DIR" 2>/dev/null + + + + # The tarball may contain ccproxy at root or in a subfolder + + if [[ ! -x "$ccproxy_exe" ]]; then + + local found + + found="$(find "$CLAWDE_BIN_DIR" -name ccproxy -type f -executable | head -1)" + + if [[ -n "$found" ]]; then + + mv "$found" "$ccproxy_exe" + + chmod +x "$ccproxy_exe" + + fi + + fi + + + + if [[ -x "$ccproxy_exe" ]]; then + local ver + ver="$("$ccproxy_exe" --version 2>/dev/null || echo "unknown")" + + # Post-install check: detect if binary has no auth provider plugins + # (known upstream bug in v0.2.10 Windows/Linux builds) + if ! test_ccproxy_has_providers "$ccproxy_exe"; then + warn "CCProxy binary has no auth provider plugins (known upstream bug)." + warn "Upstream: https://github.com/CaddyGlow/ccproxy-api/issues/75" + if test_pipx_available; then + info "Replacing bare ccproxy with pipx-installed ccproxy-api[plugins-claude,plugins-codex]..." + if install_ccproxy_via_pipx; then + step_done "CCProxy ${tag_name} installed via pipx (full plugin set)" + # Cleanup + rm -f "$tmp_archive" + local filtered=() + for item in "${ROLLBACK_ITEMS[@]}"; do + [[ "$item" != "$tmp_archive" ]] && filtered+=("$item") + done + ROLLBACK_ITEMS=("${filtered[@]}") + return 0 + else + warn "pipx install failed; retry later with: pipx install ccproxy-api[plugins-claude,plugins-codex]" + fi + else + warn "Install Python 3.11+ and pipx, then run: pipx install ccproxy-api[plugins-claude,plugins-codex]" + fi + fi + + step_done "CCProxy ${tag_name} installed" + else + warn "ccproxy not found after extraction" + fi + + + + # Cleanup + + rm -f "$tmp_archive" + + local filtered=() + + for item in "${ROLLBACK_ITEMS[@]}"; do + + [[ "$item" != "$tmp_archive" ]] && filtered+=("$item") + + done + + ROLLBACK_ITEMS=("${filtered[@]}") + +} + + + + +# ==================================================================== +# CCProxy plugin fallback helpers (upstream Windows binary bug workaround) +# See: https://github.com/CaddyGlow/ccproxy-api/issues/75 +# ==================================================================== + +# Test whether a ccproxy binary has any auth provider plugins discoverable. +# Returns 0 (true) if 'ccproxy auth providers' reports at least one provider, +# 1 (false) if it returns "No OAuth providers found" or fails entirely. +test_ccproxy_has_providers() { + local binary_path="$1" + [[ ! -x "$binary_path" ]] && return 1 + + local output + output="$("$binary_path" auth providers 2>&1)" || return 1 + + # Check for "No OAuth providers found" or "No plugins found" + if echo "$output" | grep -qiE "no (oauth )?providers? found"; then + return 1 + fi + + # Check for real provider names (non-empty lines that aren't headers) + echo "$output" | grep -vE "^(Available|warning|Warning|Available OAuth Providers|providers found)$" | grep -q . +} + +# Check if pipx is on PATH (and Python 3.11+ is available). +test_pipx_available() { + local py_cmd="" + if command -v python3 >/dev/null 2>&1; then + py_cmd="python3" + elif command -v python >/dev/null 2>&1; then + py_cmd="python" + else + return 1 + fi + + command -v pipx >/dev/null 2>&1 || return 1 + + # Check Python version >= 3.11 + local pyver + pyver="$($py_cmd --version 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1)" + [[ -z "$pyver" ]] && return 1 + + local major="${pyver%%.*}" + local minor="${pyver##*.}" + [[ "$major" -lt 3 ]] && return 1 + [[ "$major" -eq 3 && "$minor" -lt 11 ]] && return 1 + + return 0 +} + +# Install ccproxy-api with full plugin set via pipx, then create a symlink +# in the clawde bin dir as ccproxy. Returns 0 on success, 1 on failure. +install_ccproxy_via_pipx() { + local py_cmd="python3" + command -v python3 >/dev/null 2>&1 || py_cmd="python" + + info "Installing ccproxy-api with full plugin set via pipx..." + if ! pipx install --python "$py_cmd" "ccproxy-api[plugins-claude,plugins-codex]" 2>&1; then + # pipx returns non-zero on upgrade if already installed; try 'pipx upgrade' instead + pipx upgrade ccproxy-api --python "$py_cmd" 2>&1 || return 1 + fi + + # Find the pipx-installed ccproxy + local pipx_ccproxy + pipx_ccproxy="$(command -v ccproxy)" + if [[ -z "$pipx_ccproxy" ]]; then + warn "pipx install succeeded but 'ccproxy' is not on PATH" + return 1 + fi + + # Create symlink in CLAWDE_BIN_DIR + local ccproxy_link="${CLAWDE_BIN_DIR}/ccproxy" + rm -f "$ccproxy_link" 2>/dev/null || true + if ln -s "$pipx_ccproxy" "$ccproxy_link" 2>/dev/null; then + ok "CCProxy installed via pipx (full plugin set)" + return 0 + else + # Fallback: copy instead of symlink + if cp "$pipx_ccproxy" "$ccproxy_link" 2>/dev/null && chmod +x "$ccproxy_link"; then + ok "CCProxy installed via pipx (full plugin set, copied)" + return 0 + fi + warn "Failed to create ccproxy symlink or copy" + return 1 + fi +} + +# ==================================================================== + +# Install clawde CLI wrapper (Bash script - no Python required) + +# ==================================================================== + +install_cli() { + + show_progress 3 6 "Installing clawde CLI" + + + + local clawde_url="https://raw.githubusercontent.com/ClintonSarkar/clawde/main/cli/clawde.sh" + + local clawde_path="${CLAWDE_BIN_DIR}/clawde" + + + + debug "Downloading clawde.sh to ${clawde_path}..." + + if ! curl -fsSL --connect-timeout 10 --max-time 30 \ + "$clawde_url" -o "$clawde_path" 2>/dev/null; then + + warn "Failed to download clawde.sh" + + warn "clawde CLI was not installed. You can install it manually:" + + warn " curl -fsSL https://raw.githubusercontent.com/ClintonSarkar/clawde/main/cli/clawde.sh -o ${clawde_path} && chmod +x ${clawde_path}" + + return + + fi + + + + chmod +x "$clawde_path" + + register_rollback "$clawde_path" + + + + step_done "clawde CLI installed to ${clawde_path}" + +} + + + +# ==================================================================== + +# Config wizard interactive prompts + +# ==================================================================== + +do_interactive_config() { + + local auth_method="" cli_token_path="" port="" auto_start="" models="" + + local auth_choice="" + + + + show_progress 4 6 "Claude authentication" + + echo "" + + echo " Choose authentication method:" + + echo " 1. OAuth login (opens browser recommended)" + + echo " 2. Use existing Claude CLI token" + + echo "" + + while true; do + + read -rp " Select (default: 1): " auth_choice + + auth_choice="${auth_choice:-1}" + + case "$auth_choice" in + + 1) auth_method="oauth"; AUTH_PENDING=true; echo ""; info "You'll complete Claude authentication later. Run 'clawde auth' after install to log in."; break ;; + + 2) auth_method="cli_token"; break ;; + + *) warn "Please enter 1 (OAuth) or 2 (CLI token)" ;; + + esac + + done + + + + if [[ "$auth_method" == "cli_token" ]]; then + + local default_token_path="${HOME}/.claude/credentials.json" + + read -rp " Path to Claude CLI token [${default_token_path}]: " cli_token_path + + cli_token_path="${cli_token_path:-${default_token_path}}" + + if [[ -f "$cli_token_path" ]]; then + + ok "Found credentials file" + + else + + warn "File not found: ${cli_token_path} (you can set this later with 'clawde auth')" + + fi + + fi + + + + echo "" + + info "Configuration" + + echo "" + + + + # Port input with validation + + while true; do + + read -rp " Proxy port [8080]: " port + + port="${port:-8080}" + + if [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1024 && port <= 65535 )); then + + break + + else + + warn "Port must be a number between 1024 and 65535 (got: ${port})" + + fi + + done + + + + read -rp " Auto-start on boot? [y/N]: " auto_start_input + + auto_start_input="${auto_start_input:-N}" + + case "$auto_start_input" in + + [Yy]*) auto_start="true" ;; + + *) auto_start="false" ;; + + esac + + + + read -rp " Models to expose [all]: " models + + models="${models:-all}" + + + + write_config "$auth_method" "${cli_token_path:-}" "$port" "$auto_start" "$models" + +} + + + +# ==================================================================== + +# Write config file + +# ==================================================================== + +write_config() { + + local auth_method="$1" cli_token_path="$2" port="$3" auto_start="$4" models="$5" + + + + if [[ "${SKIP_CONFIG:-false}" == "true" ]]; then + + step_done "Configuration skipped (existing config preserved)" + + return 0 + + fi + + + + mkdir -p "$CLAWDE_CONFIG_DIR" + + register_rollback "$CLAWDE_CONFIG_DIR" + + + + # Only write cli_token_path if it's non-empty + + local token_line="" + + if [[ -n "$cli_token_path" ]]; then + + token_line="cli_token_path = \"${cli_token_path}\"" + + fi + + + + cat > "$CLAWDE_CONFIG_FILE" << EOF + +# clawde configuration generated by installer v${CLAWDE_VERSION} + +# Docs: https://github.com/ClintonSarkar/clawde + + + +[proxy] + +# [active] read by 'clawde start' / 'status'. + +port = ${port} + +host = "127.0.0.1" + + + +[claude] + +# [reserved] not consumed yet - auth is always the browser OAuth flow. + +auth_method = "${auth_method}" + +${token_line} + + + +[opencode] + +# [active] controls boot/login auto-start of the proxy. + +# The provider itself (id "ccproxy-claude") is defined in opencode.json. + +auto_start = ${auto_start} + + + +[models] + +# [reserved] not consumed yet. + +expose = "${models}" + + + +[logging] + +# [reserved] not consumed yet. + +level = "info" + +rotation_days = 7 + +EOF + + + + step_done "Configuration written to ${CLAWDE_CONFIG_FILE}" + + + + # Set up OpenCode provider config for ccproxy + + setup_opencode_provider "$port" + +} + + + +# ==================================================================== + +# Setup OpenCode provider config for ccproxy + +# ==================================================================== + +setup_opencode_provider() { + + local port="$1" + + + + # Determine OpenCode config path + + local opencode_config_dir="${HOME}/.config/opencode" + + local opencode_config_file="${opencode_config_dir}/opencode.json" + + + + # Create config directory if it doesn't exist + + mkdir -p "$opencode_config_dir" + + + + # Check if config already exists + + local existing_config="{}" + + if [[ -f "$opencode_config_file" ]]; then + + existing_config="$(cat "$opencode_config_file" 2>/dev/null || echo "{}")" + + # Validate JSON + + if ! echo "$existing_config" | jq -e . >/dev/null 2>&1; then + + warn "Could not parse existing OpenCode config, will create new" + + existing_config="{}" + + fi + + fi + + + + # Ensure provider section exists and add/update ccproxy provider + + local updated_config + + updated_config="$(echo "$existing_config" | jq --arg port "$port" ' + + .provider = (.provider // {}) | + + .provider["ccproxy-claude"] = { + + npm: "@ai-sdk/openai-compatible", + + name: "CCProxy (Claude Max)", + + options: { baseURL: "http://127.0.0.1:\($port)/api/v1", apiKey: "clawde" }, + + models: { + + "claude-sonnet-4-20250514": { name: "Claude Sonnet 4" }, + + "claude-opus-4-20250514": { name: "Claude Opus 4" } + + } + + } + + ' 2>/dev/null)" + + + + if [[ -z "$updated_config" ]] || [[ "$updated_config" == "null" ]]; then + + warn "Failed to generate OpenCode config (jq may not be installed), skipping" + + return 0 + + fi + + + + echo "$updated_config" > "$opencode_config_file" + + ok "OpenCode provider config written to $opencode_config_file" + +} + + + +# ==================================================================== + +# Service setup (systemd or WSL shell profile) + +# ==================================================================== + +setup_service() { + + show_progress 5 6 "Setting up service management" + + + + # Read auto_start from config + + local auto_start + + auto_start="false" + + if [[ -f "$CLAWDE_CONFIG_FILE" ]]; then + + auto_start="$(grep -E '^[[:space:]]*auto_start[[:space:]]*=' "$CLAWDE_CONFIG_FILE" 2>/dev/null | head -1 | cut -d'=' -f2 | tr -d ' ' || echo "false")" + + elif [[ "$NONINTERACTIVE" == "true" ]]; then + + auto_start="${CLAWDE_AUTO_START:-false}" + + fi + + + + if [[ "$auto_start" != "true" ]]; then + + step_done "Auto-start disabled (use 'clawde start' to launch)" + + return 0 + + fi + + + + # --- WSL without systemd --- + + if $IS_WSL && ! command -v systemctl >/dev/null 2>&1; then + + info "WSL without systemd detected setting up shell-profile auto-start" + + + + local rc_file="" + + for f in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do + + [[ -f "$f" ]] && rc_file="$f" && break + + done + + + + if [[ -z "$rc_file" ]]; then + + rc_file="${HOME}/.bashrc" + + touch "$rc_file" + + fi + + + + local port + + port="$(grep -A5 '^\s*\[proxy\]' "$CLAWDE_CONFIG_FILE" 2>/dev/null | grep -E '^\s*port\s*=' | head -1 | cut -d'=' -f2 | tr -d ' ' || echo "8080")" + + + + if ! grep -qs "clawde" "$rc_file" 2>/dev/null; then + + { + + echo "" + + echo "# Start clawde proxy on shell launch (added by clawde installer v${CLAWDE_VERSION})" + + echo "command -v ccproxy >/dev/null 2>&1 && nohup ccproxy serve --port ${port} >/dev/null 2>&1 &" + + } >> "$rc_file" + + info "Added clawde auto-start to ${rc_file}" + + else + + warn "Auto-start entry already exists in ${rc_file}" + + fi + + + + step_done "Auto-start configured for WSL" + + info "Note: proxy will start when you open a terminal. To start manually: ccproxy serve --port ${port}" + + return 0 + + fi + + + + # --- systemd --- + + if command -v systemctl >/dev/null 2>&1; then + + local systemd_dir="${HOME}/.config/systemd/user" + + mkdir -p "$systemd_dir" + + register_rollback "$systemd_dir" + + + + local port + + port="$(grep -A5 '^\s*\[proxy\]' "$CLAWDE_CONFIG_FILE" 2>/dev/null | grep -E '^\s*port\s*=' | head -1 | cut -d'=' -f2 | tr -d ' ' || echo "8080")" + + + + cat > "$SYSTEMD_SERVICE_FILE" << EOF + +[Unit] + +Description=clawde CCProxy (Claude Work proxy) + +Documentation=https://github.com/ClintonSarkar/clawde + +After=network-online.target + +Wants=network-online.target + + + +[Service] + +Type=simple + +ExecStart=%h/.local/bin/ccproxy serve --port ${port} + +Restart=on-failure + +RestartSec=10 + +Environment=PYTHONUNBUFFERED=1 + + + +[Install] + +WantedBy=default.target + +EOF + + + + debug "systemd unit written to ${SYSTEMD_SERVICE_FILE}" + + + + # Enable lingering so user services start at boot even without login + + loginctl enable-linger "$(whoami)" 2>/dev/null || true + + + + systemctl --user daemon-reload 2>/dev/null || warn "systemctl daemon-reload failed; check systemd is functional" + + systemctl --user enable "${SYSTEMD_SERVICE_NAME}.service" 2>/dev/null || warn "Could not enable systemd service (is systemd available?)" + + step_done "Systemd user service configured" + + info " Service: ${SYSTEMD_SERVICE_NAME}.service" + + info " Start: systemctl --user start ${SYSTEMD_SERVICE_NAME}.service" + + return 0 + + fi + + + + warn "No service manager detected you'll need to start clawde manually" + +} + + + +# ==================================================================== + +# Final message + +# ==================================================================== + +final_message() { + + echo "" + + ok "clawde v${CLAWDE_VERSION} is installed and ready!" + + echo "" + + echo " ${BOLD}Quick start:${NC}" + + echo " clawde start launch proxy + OpenCode" + + echo " clawde stop stop all services" + + echo " clawde status check health" + + echo "" + + echo " ${BOLD}Management:${NC}" + + echo " clawde config reconfigure" + + echo " clawde auth re-authenticate Claude" + + echo " clawde update update to latest version" + + echo " clawde logs tail logs" + + echo "" + + if [[ "${AUTH_PENDING:-false}" == "true" ]]; then + + echo " ${BOLD}Note:${NC} Claude authentication not yet completed. Run 'clawde auth' to connect your Claude account." + + echo "" + + fi + + + + echo " ${BOLD}Resources:${NC}" + + echo " Config: ${CLAWDE_CONFIG_FILE}" + + echo " Logs: ${CLAWDE_DATA_DIR}/logs/" + + echo " Docs: https://github.com/ClintonSarkar/clawde" + + echo "" + +} + + + +# ==================================================================== + +# Uninstall + +# ==================================================================== + +uninstall() { + + banner + + info "Uninstalling clawde..." + + echo "" + + + + local removed_anything=false + + + + # --- Remove OpenCode binary --- + + if [[ -f "$OPENCODE_BIN" ]]; then + + rm -f "$OPENCODE_BIN" + + ok "Removed OpenCode binary: ${OPENCODE_BIN}" + + removed_anything=true + + fi + + + + # --- Remove config --- + + if [[ -d "$CLAWDE_CONFIG_DIR" ]]; then + + rm -rf "$CLAWDE_CONFIG_DIR" + + ok "Removed config directory: ${CLAWDE_CONFIG_DIR}" + + removed_anything=true + + fi + + + + # --- Remove data --- + + if [[ -d "$CLAWDE_DATA_DIR" ]]; then + + rm -rf "$CLAWDE_DATA_DIR" + + ok "Removed data directory: ${CLAWDE_DATA_DIR}" + + removed_anything=true + + fi + + + + # --- Remove systemd service --- + + if [[ -f "$SYSTEMD_SERVICE_FILE" ]]; then + + systemctl --user disable "${SYSTEMD_SERVICE_NAME}.service" 2>/dev/null || true + + rm -f "$SYSTEMD_SERVICE_FILE" + + systemctl --user daemon-reload 2>/dev/null || true + + ok "Removed systemd user service" + + removed_anything=true + + fi + + + + # --- Clean up shell rc PATH additions --- + + for rc in "${HOME}/.bashrc" "${HOME}/.bash_profile" "${HOME}/.zshrc" "${HOME}/.config/fish/config.fish"; do + + if [[ -f "$rc" ]]; then + + # Only act if the installer's OWN marker is present - not merely any line + # that mentions the bin dir (the user may have their own PATH entry). + if grep -qsF "# Added by clawde installer" "$rc" 2>/dev/null; then + + cp "$rc" "${rc}.clawde-backup" + + # Remove ONLY the 2-line block the installer added (the comment plus the + # export line right after it). POSIX awk (universally present, and free + # of GNU/BSD sed -i differences). + + if awk '/# Added by clawde installer/ { skip=1; next } skip { skip=0; next } { print }' "$rc" > "${rc}.clawde-tmp" 2>/dev/null; then + mv -f "${rc}.clawde-tmp" "$rc" + ok "Removed PATH entry from ${rc} (backup saved to ${rc}.clawde-backup)" + removed_anything=true + else + rm -f "${rc}.clawde-tmp" + warn "Could not clean PATH entry from ${rc}; remove the clawde block manually" + fi + + fi + + fi + + done + + + + # --- Clean up WSL auto-start from shell rc --- + + for rc in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do + + if [[ -f "$rc" ]]; then + + # Match the installer's OWN marker, not any line mentioning clawde. + if grep -qsF "# Start clawde" "$rc" 2>/dev/null; then + + cp "$rc" "${rc}.clawde-backup" + + # Remove ONLY the 2-line auto-start block the installer added; do not + # blanket-delete every "ccproxy serve" line (the user may have their own). + + if awk '/# Start clawde/ { skip=1; next } skip { skip=0; next } { print }' "$rc" > "${rc}.clawde-tmp" 2>/dev/null; then + mv -f "${rc}.clawde-tmp" "$rc" + ok "Removed auto-start entries from ${rc} (backup saved to ${rc}.clawde-backup)" + removed_anything=true + else + rm -f "${rc}.clawde-tmp" + warn "Could not clean auto-start entries from ${rc}; remove the clawde block manually" + fi + + fi + + fi + + done + + + + # --- Uninstall CCProxy (remove binary + legacy pip/uv/pipx if present) --- + + # Remove binary install + + local ccproxy_exe="${CLAWDE_BIN_DIR}/ccproxy" + + if [[ -x "$ccproxy_exe" ]]; then + + rm -f "$ccproxy_exe" + + ok "Removed ccproxy binary" + + removed_anything=true + + fi + + # Legacy cleanup: if someone installed via pip/uv/pipx before binary install + + if command -v uv >/dev/null 2>&1; then + + if uv tool uninstall "$CCPROXY_PACKAGE" 2>/dev/null; then + + ok "Removed legacy CCProxy via uv"; removed_anything=true + + fi + + fi + + if command -v pipx >/dev/null 2>&1; then + + if pipx uninstall "$CCPROXY_PACKAGE" 2>/dev/null; then + + ok "Removed legacy CCProxy via pipx"; removed_anything=true + + fi + + fi + + local pip_cmd="" + + command -v pip3 >/dev/null 2>&1 && pip_cmd="pip3" || command -v pip >/dev/null 2>&1 && pip_cmd="pip" || true + + if [[ -n "$pip_cmd" ]]; then + + if $pip_cmd uninstall -y "$CCPROXY_PACKAGE" 2>/dev/null; then + + ok "Removed legacy CCProxy via ${pip_cmd}"; removed_anything=true + + + fi + + fi + + + + if ! $removed_anything; then + + info "Nothing to uninstall clawde is not installed." + + else + + echo "" + + ok "clawde has been completely uninstalled." + + fi + +} + + + +# ==================================================================== + +# Validate environment variables (for non-interactive mode) + +# ==================================================================== + +validate_env_vars() { + + # Validate CLAWDE_PORT + + if [[ -n "${CLAWDE_PORT:-}" ]]; then + + if ! [[ "$CLAWDE_PORT" =~ ^[0-9]+$ ]] || (( CLAWDE_PORT < 1024 || CLAWDE_PORT > 65535 )); then + + warn "CLAWDE_PORT=${CLAWDE_PORT} is invalid; must be 1024-65535. Defaulting to 8080." + + CLAWDE_PORT="8080" + + fi + + fi + + + + # Validate CLAWDE_AUTH_METHOD + + if [[ -n "${CLAWDE_AUTH_METHOD:-}" ]]; then + + if [[ "$CLAWDE_AUTH_METHOD" != "oauth" && "$CLAWDE_AUTH_METHOD" != "cli_token" ]]; then + + warn "CLAWDE_AUTH_METHOD=${CLAWDE_AUTH_METHOD} is invalid. Defaulting to 'oauth'." + + CLAWDE_AUTH_METHOD="oauth" + + fi + + fi + + + + # Validate CLAWDE_AUTO_START + + if [[ -n "${CLAWDE_AUTO_START:-}" ]]; then + + if [[ "$CLAWDE_AUTO_START" != "true" && "$CLAWDE_AUTO_START" != "false" ]]; then + + warn "CLAWDE_AUTO_START=${CLAWDE_AUTO_START} is invalid. Defaulting to 'false'." + + CLAWDE_AUTO_START="false" + + fi + + fi + +} + + + +# ==================================================================== + +# Main entry point + +# ==================================================================== + +main() { + + parse_args "$@" + + + + if [[ "$UNINSTALL" == "true" ]]; then + + uninstall + + exit 0 + + fi + + + + banner + + detect_os + + check_wsl + + show_version + + check_deps + + + + # Installation steps + + setup_path + + check_existing + + + + install_opencode + + install_ccproxy + + install_cli + + + + # Config interactive or non-interactive + + if [[ "$NONINTERACTIVE" == "true" ]]; then + + if [[ "${SKIP_CONFIG:-false}" != "true" ]]; then + + validate_env_vars + + if [[ "${CLAWDE_AUTH_METHOD:-oauth}" == "oauth" ]]; then + + AUTH_PENDING=true + + fi + + show_progress 4 6 "Claude authentication" + + write_config \ + "${CLAWDE_AUTH_METHOD:-oauth}" \ + "${CLAWDE_CLI_TOKEN_PATH:-}" \ + "${CLAWDE_PORT:-8080}" \ + "${CLAWDE_AUTO_START:-false}" \ + "${CLAWDE_MODELS:-all}" + + else + + show_progress 4 6 "Claude authentication" + + step_done "Configuration skipped (existing config preserved)" + + fi + + else + + do_interactive_config + + fi + + + + setup_service + + + + show_progress 6 6 "Finishing" + + step_done "Installation complete" + + + + # Mark success so the EXIT trap won't roll back + + INSTALL_COMPLETED=true + + clear_rollback + + final_message + +} + + + +main "$@" + +