#!/usr/bin/env bash # One-time installer for claude-mode (Linux / macOS). # # Installs to ~/.claude-mode, symlinks the entry point into ~/.local/bin, and # adds a marked block to your shell rc for the `claude` wrapper. # # Idempotent: safe to re-run to upgrade. Existing presets are kept unless # --force is passed. ~/.claude/settings.json is NOT touched here - only by an # actual `claude-mode `. set -euo pipefail FORCE=0 SKIP_KEY=0 for arg in "$@"; do case "$arg" in --force) FORCE=1 ;; --skip-key-prompt) SKIP_KEY=1 ;; -h|--help) echo "usage: install.sh [--force] [--skip-key-prompt]"; exit 0 ;; esac done ROOT="${CM_ROOT:-$HOME/.claude-mode}" BINDIR="$HOME/.local/bin" green() { printf ' \033[32mok \033[0m %s\n' "$*"; } warn() { printf ' \033[33mwarn\033[0m %s\n' "$*"; } fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; } # A checkout next to this script is the normal source. Piped in # (curl ... | bash) there is no script file and no checkout, so fetch the # repository archive and install from that instead. SRC="$(cd "$(dirname "${BASH_SOURCE[0]:-''}")" 2>/dev/null && pwd)" || SRC="" if [ ! -f "$SRC/claude-mode" ] || [ ! -f "$SRC/cm-json.py" ]; then REPO_URL="${CM_REPO_URL:-https://git.nebulm.com/smoido/claude-mode}" command -v curl >/dev/null 2>&1 || { fail 'curl required to fetch the source'; exit 1; } TMP="$(mktemp -d "${TMPDIR:-/tmp}/claude-mode-src.XXXXXX")" printf 'no local checkout found - fetching source from %s\n' "$REPO_URL" curl -fsSL "$REPO_URL/archive/master.tar.gz" | tar -xz -C "$TMP" # a plain glob rather than find -print -quit: BSD find (macOS) has no -quit SRC='' for _c in "$TMP"/*/linux/claude-mode "$TMP"/linux/claude-mode; do [ -f "$_c" ] && { SRC="$(dirname "$_c")"; break; } done [ -n "$SRC" ] || { fail 'archive did not contain linux/claude-mode - repo layout changed?'; exit 1; } fi printf '\ninstalling claude-mode -> %s\n' "$ROOT" # --- preflight ------------------------------------------------------------- PY="${CLAUDE_MODE_PYTHON:-python3}" if ! command -v "$PY" >/dev/null 2>&1; then fail "python3 not found. claude-mode uses it for JSON handling." fail "install it (apt install python3 / dnf install python3 / brew install python) and re-run." exit 1 fi green "python3: $(command -v "$PY")" if ! command -v curl >/dev/null 2>&1; then warn 'curl not found - `models` and `doctor` network checks will not work' fi if ! command -v claude >/dev/null 2>&1; then warn 'claude is not on PATH - claude-mode installs anyway, but nothing uses its config yet' fi # --- directories ----------------------------------------------------------- mkdir -p "$ROOT/bin" "$ROOT/presets" "$ROOT/vault" "$ROOT/backups" "$BINDIR" chmod 700 "$ROOT" "$ROOT/vault" 2>/dev/null || true # --- payload --------------------------------------------------------------- install -m 0755 "$SRC/claude-mode" "$ROOT/bin/claude-mode" install -m 0755 "$SRC/claude-key-helper.sh" "$ROOT/bin/claude-key-helper.sh" install -m 0644 "$SRC/cm-json.py" "$ROOT/bin/cm-json.py" install -m 0644 "$SRC/cm-vault.sh" "$ROOT/bin/cm-vault.sh" # The CLI is split across lib/*.sh, loaded from beside bin/claude-mode. Cleared # first, so a module removed from the source does not linger and get loaded. mkdir -p "$ROOT/bin/lib" rm -f "$ROOT/bin/lib/"*.sh for f in "$SRC"/lib/*.sh; do install -m 0644 "$f" "$ROOT/bin/lib/"; done green 'copied claude-mode + helpers' # cm_version() reads this; without it every health.json reported 0.0.0. VERSION_SRC="$SRC/../VERSION" [ -f "$VERSION_SRC" ] || VERSION_SRC="$SRC/VERSION" if [ -f "$VERSION_SRC" ]; then install -m 0644 "$VERSION_SRC" "$ROOT/VERSION" green "version $(tr -d '[:space:]' < "$ROOT/VERSION")" fi # --- providers (shared with the Windows build) ----------------------------- # Always replaced, unlike presets: it is the tool's own table of what each # provider is, not something a user edits. cm-json.py looks for it one level # above bin/. PROVIDERS_SRC="$SRC/../providers.json" [ -f "$PROVIDERS_SRC" ] || PROVIDERS_SRC="$SRC/providers.json" if [ -f "$PROVIDERS_SRC" ]; then install -m 0644 "$PROVIDERS_SRC" "$ROOT/providers.json" green "providers: $("$PY" -c 'import json,sys; print(", ".join(p["id"] for p in json.load(open(sys.argv[1]))["providers"]))' "$ROOT/providers.json")" else fail 'providers.json missing from the payload - claude-mode cannot run without it' exit 1 fi # --- presets (shared with the Windows build) ------------------------------- PRESET_SRC="$SRC/../presets" [ -d "$PRESET_SRC" ] || PRESET_SRC="$SRC/presets" if [ -d "$PRESET_SRC" ]; then for f in "$PRESET_SRC"/*.json; do [ -e "$f" ] || continue base="$(basename "$f")" if [ -e "$ROOT/presets/$base" ] && [ "$FORCE" -eq 0 ]; then printf ' skip preset %s (exists; --force to overwrite)\n' "${base%.json}" else install -m 0644 "$f" "$ROOT/presets/$base" green "preset ${base%.json}" fi done else warn 'no presets directory found in the payload' fi # --- initial state --------------------------------------------------------- if [ ! -f "$ROOT/state.json" ]; then printf '{\n "mode": "anthropic",\n "preset": "",\n "writtenEnvKeys": []\n}\n' > "$ROOT/state.json" green 'state.json initialised (mode=anthropic)' fi # --- PATH entry ------------------------------------------------------------ ln -sf "$ROOT/bin/claude-mode" "$BINDIR/claude-mode" green "linked $BINDIR/claude-mode" case ":$PATH:" in *":$BINDIR:"*) ;; *) warn "$BINDIR is not on PATH - add it in your shell rc: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;; esac # --- shell rc block -------------------------------------------------------- SNIPPET="$SRC/shell-snippet.sh" START='# >>> claude-mode >>>' END='# <<< claude-mode <<<' add_block() { local rc="$1" [ -f "$rc" ] || return 0 if grep -qF "$START" "$rc" 2>/dev/null; then # replace the existing block in place "$PY" - "$rc" "$SNIPPET" "$START" "$END" <<'PYEOF' import sys, re rc, snip, start, end = sys.argv[1:5] body = open(rc, encoding='utf-8').read() new = open(snip, encoding='utf-8').read().rstrip('\n') pattern = re.compile(re.escape(start) + r'.*?' + re.escape(end), re.S) open(rc, 'w', encoding='utf-8').write(pattern.sub(lambda _: new, body)) PYEOF green "updated claude-mode block in $rc" else printf '\n%s\n' "$(cat "$SNIPPET")" >> "$rc" green "appended claude-mode block to $rc" fi } RC_TOUCHED=0 for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do if [ -f "$rc" ]; then add_block "$rc"; RC_TOUCHED=1; fi done [ "$RC_TOUCHED" -eq 0 ] && warn 'no ~/.bashrc or ~/.zshrc found - the `claude` wrapper was not installed' # --- key ------------------------------------------------------------------- if [ "$SKIP_KEY" -eq 0 ]; then # shellcheck source=/dev/null . "$ROOT/bin/cm-vault.sh" if cm_vault_has openrouter && [ "$FORCE" -eq 0 ]; then green 'OpenRouter key already stored' else printf '\n' "$ROOT/bin/claude-mode" set-key openrouter || warn 'key not stored - run `claude-mode set-key openrouter` later' fi fi # --- machine-readable state ------------------------------------------------ "$ROOT/bin/claude-mode" health >/dev/null 2>&1 && green 'health.json seeded' printf '\ndone. Open a new shell, then:\n' printf ' claude-mode\n' printf ' claude-mode status\n' printf ' claude-mode doctor\n'