Last active
April 16, 2026 23:27
-
-
Save NoDataFound/4dfd14bf575c469ee9865848f6ad1d78 to your computer and use it in GitHub Desktop.
VantaBrain installer — includes hakcerline (Claude Code statusline). Standalone hakcerline install: npx hakcerline install
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| VantaBrain installer — includes hakcerline (Claude Code statusline). Standalone hakcerline install: npx hakcerline install | |
| #!/bin/bash | |
| # VantaBrain installer | |
| # curl -fsSL https://vb.darkcode.ai | bash | |
| set -u | |
| VB_VERSION="3.2.1" | |
| REPO="vantagrid/vantabrain" | |
| BRAIN_HOME="${BRAIN_HOME:-$HOME/VantaBrain}" | |
| # hakcerline — npm package, no local clone needed | |
| HAKCER_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/hakcerline" | |
| # Lobotomy marker — if this exists, installer refuses to run | |
| LOBOTOMY_MARKER="$HOME/.vantabrain-lobotomy" | |
| # -- styling --------------------------------------------------------------- | |
| CYAN='\033[38;2;0;180;220m' | |
| GREEN='\033[38;2;0;200;100m' | |
| RED='\033[38;2;220;50;30m' | |
| YELLOW='\033[38;2;255;200;0m' | |
| DIM='\033[2m' | |
| BOLD='\033[1m' | |
| WHITE='\033[38;2;255;255;255m' | |
| NC='\033[0m' | |
| INVERT='\033[7m' | |
| # Spinner | |
| SPIN_FRAMES=('·' '✢' '✳' '✶' '✻' '✽' '✻' '✶' '✳' '✢') | |
| SPIN_PID="" | |
| spin_start() { | |
| local msg="$1" | |
| ( | |
| local i=0 | |
| while true; do | |
| local ch="${SPIN_FRAMES[$((i % ${#SPIN_FRAMES[@]}))]}" | |
| printf "\r\033[2K ${CYAN}%s${NC} %b" "$ch" "$msg" >&2 | |
| i=$((i + 1)) | |
| sleep 0.12 | |
| done | |
| ) & | |
| SPIN_PID=$! | |
| } | |
| spin_ok() { | |
| [ -n "$SPIN_PID" ] && kill "$SPIN_PID" 2>/dev/null && wait "$SPIN_PID" 2>/dev/null | |
| SPIN_PID="" | |
| printf "\r\033[2K ${GREEN}✓${NC} %b\n" "$*" >&2 | |
| } | |
| spin_fail() { | |
| [ -n "$SPIN_PID" ] && kill "$SPIN_PID" 2>/dev/null && wait "$SPIN_PID" 2>/dev/null | |
| SPIN_PID="" | |
| printf "\r\033[2K ${RED}✗${NC} %b\n" "$*" >&2 | |
| } | |
| trap '[ -n "$SPIN_PID" ] && kill "$SPIN_PID" 2>/dev/null' EXIT | |
| # -- TTY handling ---------------------------------------------------------- | |
| # When piped via curl|bash, stdin is the pipe. All interactive reads use TTY. | |
| if [ -t 0 ]; then | |
| TTY=/dev/stdin | |
| else | |
| TTY=/dev/tty | |
| fi | |
| # Read a single keypress (including arrow key sequences) | |
| read_key() { | |
| local key | |
| IFS= read -rsn1 key <"$TTY" | |
| # If escape, read the rest of the sequence | |
| if [ "$key" = $'\x1b' ]; then | |
| local seq | |
| IFS= read -rsn2 seq <"$TTY" | |
| key="${key}${seq}" | |
| fi | |
| printf '%s' "$key" | |
| } | |
| # -- menu: single select -------------------------------------------------- | |
| # Usage: menu_select "prompt" "option1" "option2" ... | |
| # Returns: index (0-based) in $MENU_RESULT | |
| MENU_RESULT=0 | |
| menu_select() { | |
| local prompt="$1" | |
| shift | |
| local options=("$@") | |
| local count=${#options[@]} | |
| local cursor=0 | |
| # Hide cursor | |
| printf '\033[?25l' >&2 | |
| # Print prompt | |
| printf "\n ${BOLD}${WHITE}%s${NC}\n\n" "$prompt" >&2 | |
| # Draw initial menu | |
| local i | |
| for ((i = 0; i < count; i++)); do | |
| if [ $i -eq $cursor ]; then | |
| printf " ${CYAN}❯${NC} ${WHITE}%s${NC}\n" "${options[$i]}" >&2 | |
| else | |
| printf " ${DIM}%s${NC}\n" "${options[$i]}" >&2 | |
| fi | |
| done | |
| while true; do | |
| local key | |
| key="$(read_key)" | |
| case "$key" in | |
| $'\x1b[A') # Up arrow | |
| [ $cursor -gt 0 ] && cursor=$((cursor - 1)) | |
| ;; | |
| $'\x1b[B') # Down arrow | |
| [ $cursor -lt $((count - 1)) ] && cursor=$((cursor + 1)) | |
| ;; | |
| '') # Enter | |
| break | |
| ;; | |
| esac | |
| # Redraw: move up $count lines, redraw | |
| printf "\033[%dA" "$count" >&2 | |
| for ((i = 0; i < count; i++)); do | |
| printf "\033[2K" >&2 | |
| if [ $i -eq $cursor ]; then | |
| printf " ${CYAN}❯${NC} ${WHITE}%s${NC}\n" "${options[$i]}" >&2 | |
| else | |
| printf " ${DIM}%s${NC}\n" "${options[$i]}" >&2 | |
| fi | |
| done | |
| done | |
| # Show cursor, print selection | |
| printf '\033[?25h' >&2 | |
| printf "\033[%dA" "$count" >&2 | |
| for ((i = 0; i < count; i++)); do | |
| printf "\033[2K" >&2 | |
| if [ $i -eq $cursor ]; then | |
| printf " ${GREEN}✓${NC} ${WHITE}%s${NC}\n" "${options[$i]}" >&2 | |
| else | |
| printf "\033[1B" >&2 | |
| fi | |
| done | |
| # Move to end | |
| printf "\033[%dB" "$((count - cursor - 1))" >&2 | |
| # Clear the unselected lines | |
| printf "\033[%dA" "$((count))" >&2 | |
| for ((i = 0; i < count; i++)); do | |
| if [ $i -eq $cursor ]; then | |
| printf " ${GREEN}✓${NC} ${WHITE}%s${NC}\n" "${options[$i]}" >&2 | |
| else | |
| printf "\033[2K\n" >&2 | |
| fi | |
| done | |
| MENU_RESULT=$cursor | |
| } | |
| # -- menu: multiselect ---------------------------------------------------- | |
| # Usage: menu_multiselect "prompt" "opt1" "opt2" ... | |
| # Returns: space-separated indices in $MULTI_RESULT | |
| # First option is always "All" — toggling it toggles everything. | |
| MULTI_RESULT="" | |
| menu_multiselect() { | |
| local prompt="$1" | |
| shift | |
| local options=("$@") | |
| local count=${#options[@]} | |
| local cursor=0 | |
| local selected=() | |
| # Initialize all unselected | |
| local i | |
| for ((i = 0; i < count; i++)); do | |
| selected+=("0") | |
| done | |
| printf '\033[?25l' >&2 | |
| printf "\n ${BOLD}${WHITE}%s${NC}\n" "$prompt" >&2 | |
| printf " ${DIM}↑↓ navigate · space toggle · enter confirm${NC}\n\n" >&2 | |
| # Initial draw | |
| for ((i = 0; i < count; i++)); do | |
| printf "\033[2K" >&2 | |
| if [ "${selected[$i]}" = "1" ]; then | |
| local _chk="${GREEN}■${NC}" | |
| else | |
| local _chk="${DIM}□${NC}" | |
| fi | |
| if [ $i -eq $cursor ]; then | |
| printf " ${CYAN}❯${NC} %b ${WHITE}%s${NC}\n" "$_chk" "${options[$i]}" >&2 | |
| else | |
| printf " %b ${DIM}%s${NC}\n" "$_chk" "${options[$i]}" >&2 | |
| fi | |
| done | |
| while true; do | |
| local key | |
| key="$(read_key)" | |
| case "$key" in | |
| $'\x1b[A') # Up | |
| [ $cursor -gt 0 ] && cursor=$((cursor - 1)) | |
| ;; | |
| $'\x1b[B') # Down | |
| [ $cursor -lt $((count - 1)) ] && cursor=$((cursor + 1)) | |
| ;; | |
| ' ') # Space — toggle | |
| if [ $cursor -eq 0 ]; then | |
| # "All" toggle | |
| local new_state | |
| if [ "${selected[0]}" = "1" ]; then new_state="0"; else new_state="1"; fi | |
| for ((i = 0; i < count; i++)); do | |
| selected[$i]="$new_state" | |
| done | |
| else | |
| # Toggle individual | |
| if [ "${selected[$cursor]}" = "1" ]; then | |
| selected[$cursor]="0" | |
| selected[0]="0" | |
| else | |
| selected[$cursor]="1" | |
| local all_on=1 | |
| for ((i = 1; i < count; i++)); do | |
| [ "${selected[$i]}" = "0" ] && all_on=0 && break | |
| done | |
| [ $all_on -eq 1 ] && selected[0]="1" | |
| fi | |
| fi | |
| ;; | |
| '') # Enter | |
| break | |
| ;; | |
| esac | |
| # Redraw | |
| printf "\033[%dA" "$count" >&2 | |
| for ((i = 0; i < count; i++)); do | |
| printf "\033[2K" >&2 | |
| if [ "${selected[$i]}" = "1" ]; then | |
| _chk="${GREEN}■${NC}" | |
| else | |
| _chk="${DIM}□${NC}" | |
| fi | |
| if [ $i -eq $cursor ]; then | |
| printf " ${CYAN}❯${NC} %b ${WHITE}%s${NC}\n" "$_chk" "${options[$i]}" >&2 | |
| else | |
| printf " %b ${DIM}%s${NC}\n" "$_chk" "${options[$i]}" >&2 | |
| fi | |
| done | |
| done | |
| printf '\033[?25h' >&2 | |
| # Collapse to final display | |
| printf "\033[%dA" "$count" >&2 | |
| MULTI_RESULT="" | |
| for ((i = 0; i < count; i++)); do | |
| printf "\033[2K" >&2 | |
| if [ "${selected[$i]}" = "1" ] && [ $i -gt 0 ]; then | |
| printf " ${GREEN}✓${NC} ${WHITE}%s${NC}\n" "${options[$i]}" >&2 | |
| MULTI_RESULT="${MULTI_RESULT} ${i}" | |
| else | |
| printf "\n" >&2 | |
| fi | |
| done | |
| MULTI_RESULT="${MULTI_RESULT# }" | |
| } | |
| # -- confirm prompt -------------------------------------------------------- | |
| confirm() { | |
| local prompt="$1" | |
| local default="${2:-n}" | |
| printf "\n ${BOLD}${WHITE}%s${NC} " "$prompt" >&2 | |
| if [ "$default" = "y" ]; then | |
| printf "${DIM}[Y/n]${NC} " >&2 | |
| else | |
| printf "${DIM}[y/N]${NC} " >&2 | |
| fi | |
| local answer | |
| IFS= read -rn1 answer <"$TTY" | |
| printf "\n" >&2 | |
| case "$answer" in | |
| [Yy]) return 0 ;; | |
| [Nn]) return 1 ;; | |
| '') | |
| [ "$default" = "y" ] && return 0 || return 1 | |
| ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| # -- detection functions --------------------------------------------------- | |
| has_vantabrain() { | |
| [ -d "$BRAIN_HOME/.git" ] | |
| } | |
| has_hakcer() { | |
| # Check for config dir (installed via npx hakcerline install) | |
| [ -f "$HAKCER_CONFIG_DIR/config.json" ] && return 0 | |
| # Check for statusLine in Claude settings | |
| if [ -f "$HOME/.claude/settings.json" ]; then | |
| node -e " | |
| const s = JSON.parse(require('fs').readFileSync('$HOME/.claude/settings.json','utf8')); | |
| process.exit(s.statusLine && String(s.statusLine.command || '').includes('hakcerline') ? 0 : 1); | |
| " 2>/dev/null && return 0 | |
| fi | |
| return 1 | |
| } | |
| has_vscode_ext() { | |
| # Check code-server first, then regular VS Code | |
| if command -v code-server >/dev/null 2>&1; then | |
| code-server --list-extensions 2>/dev/null | grep -qi "vantabrain" && return 0 | |
| fi | |
| if command -v code >/dev/null 2>&1; then | |
| code --list-extensions 2>/dev/null | grep -qi "vantabrain" && return 0 | |
| fi | |
| # Check known extension directories | |
| [ -d "$HOME/.vscode/extensions/vantagrid.vantabrain-"* ] 2>/dev/null && return 0 | |
| [ -d "$HOME/.local/share/code-server/extensions/vantagrid.vantabrain-"* ] 2>/dev/null && return 0 | |
| return 1 | |
| } | |
| has_anything() { | |
| has_vantabrain || has_hakcer || has_vscode_ext | |
| } | |
| # -- install functions ----------------------------------------------------- | |
| install_vantabrain() { | |
| # Clone or update | |
| if [ -d "$BRAIN_HOME/.git" ]; then | |
| spin_start "Updating VantaBrain ${DIM}$BRAIN_HOME${NC}" | |
| git -C "$BRAIN_HOME" pull --rebase --autostash >/dev/null 2>&1 || { | |
| spin_fail "git pull failed — continuing with existing version" | |
| } | |
| spin_ok "Updated ${DIM}$BRAIN_HOME${NC}" | |
| elif [ -d "$BRAIN_HOME" ]; then | |
| spin_start "Backing up and cloning fresh..." | |
| mv "$BRAIN_HOME" "${BRAIN_HOME}.bak.$(date +%s)" | |
| gh repo clone "$REPO" "$BRAIN_HOME" -- --quiet | |
| spin_ok "Cloned ${DIM}(old dir backed up)${NC}" | |
| else | |
| spin_start "Cloning VantaBrain..." | |
| gh repo clone "$REPO" "$BRAIN_HOME" -- --quiet | |
| spin_ok "Cloned to ${DIM}$BRAIN_HOME${NC}" | |
| fi | |
| # CLI deps | |
| spin_start "Installing CLI dependencies..." | |
| (cd "$BRAIN_HOME/cli" && npm install --silent 2>/dev/null) | |
| spin_ok "CLI dependencies installed" | |
| # Web deps | |
| spin_start "Installing web dependencies..." | |
| (cd "$BRAIN_HOME/web" && npm install --silent 2>/dev/null) | |
| spin_ok "Web dependencies installed" | |
| # PATH — persist to shell profile so it survives reboots | |
| export PATH="$BRAIN_HOME/bin:$PATH" | |
| export BRAIN_HOME="$BRAIN_HOME" | |
| local profile | |
| profile="$(detect_profile)" | |
| if [ -n "$profile" ]; then | |
| # Add PATH + BRAIN_HOME if not already present | |
| if ! grep -q 'VantaBrain/bin' "$profile" 2>/dev/null; then | |
| { | |
| echo "" | |
| echo "# VantaBrain" | |
| echo "export BRAIN_HOME=\"$BRAIN_HOME\"" | |
| echo "export PATH=\"\$BRAIN_HOME/bin:\$PATH\"" | |
| } >> "$profile" | |
| spin_ok "PATH added to ${DIM}$profile${NC}" | |
| else | |
| spin_ok "PATH already in ${DIM}$profile${NC}" | |
| fi | |
| fi | |
| # Make all bin scripts executable | |
| chmod +x "$BRAIN_HOME/bin/"* 2>/dev/null || true | |
| # Database setup | |
| if [ ! -f "$BRAIN_HOME/.env" ]; then | |
| echo "" >&2 | |
| echo -e " ${BOLD}${WHITE}Database Setup${NC}" >&2 | |
| echo -e " ${DIM}VantaBrain uses Neon Postgres for multi-machine state.${NC}" >&2 | |
| echo -e " ${DIM}Get your connection string from: https://console.neon.tech${NC}" >&2 | |
| echo "" >&2 | |
| printf " DATABASE_URL: " >&2 | |
| local db_url | |
| IFS= read -r db_url <"$TTY" | |
| if [ -n "$db_url" ]; then | |
| echo "DATABASE_URL=$db_url" > "$BRAIN_HOME/.env" | |
| spin_ok "Database configured" | |
| else | |
| echo -e " ${DIM}Skipped. Add later: echo 'DATABASE_URL=...' > $BRAIN_HOME/.env${NC}" >&2 | |
| fi | |
| fi | |
| # Run Ink TUI onboard | |
| spin_start "Launching setup..." | |
| sleep 0.3 | |
| spin_ok "Ready" | |
| echo "" >&2 | |
| if [ -t 0 ]; then | |
| (cd "$BRAIN_HOME/cli" && npx tsx src/cli.tsx onboard) | |
| else | |
| (cd "$BRAIN_HOME/cli" && npx tsx src/cli.tsx onboard <"$TTY") | |
| fi | |
| # Source profile | |
| if [ -n "$profile" ]; then | |
| . "$profile" 2>/dev/null || true | |
| fi | |
| } | |
| install_hakcer() { | |
| spin_start "Installing hakcerline via npm" | |
| # Run the built-in interactive installer (TUI with scene picker) | |
| spin_ok "Launching hakcerline installer" | |
| echo "" >&2 | |
| if [ -t 0 ]; then | |
| npx -y hakcerline@latest install | |
| else | |
| npx -y hakcerline@latest install <"$TTY" | |
| fi | |
| local exit_code=$? | |
| if [ $exit_code -ne 0 ]; then | |
| spin_fail "hakcerline install failed (exit $exit_code)" | |
| return 1 | |
| fi | |
| # Verify it landed | |
| if has_hakcer; then | |
| spin_ok "Claude Code statusLine → ${WHITE}hakcerline${NC}" | |
| else | |
| spin_fail "hakcerline installed but statusLine not detected — run ${WHITE}npx hakcerline install${NC} manually" | |
| return 1 | |
| fi | |
| } | |
| install_vscode_ext() { | |
| local vsix="$BRAIN_HOME/vscode/vantabrain-0.2.4.vsix" | |
| if [ ! -f "$vsix" ]; then | |
| # Try to pull latest if brain is installed | |
| if has_vantabrain; then | |
| vsix="$(ls -t "$BRAIN_HOME/vscode/"*.vsix 2>/dev/null | head -1)" | |
| fi | |
| if [ -z "$vsix" ] || [ ! -f "$vsix" ]; then | |
| spin_fail "VantaBrain VSIX not found — install VantaBrain first" | |
| return 1 | |
| fi | |
| fi | |
| local installed=0 | |
| if command -v code-server >/dev/null 2>&1; then | |
| spin_start "Installing VantaBrain extension into code-server..." | |
| if code-server --install-extension "$vsix" --force >/dev/null 2>&1; then | |
| spin_ok "Extension installed in code-server" | |
| installed=1 | |
| else | |
| spin_fail "code-server install failed" | |
| fi | |
| fi | |
| if command -v code >/dev/null 2>&1; then | |
| spin_start "Installing VantaBrain extension into VS Code..." | |
| if code --install-extension "$vsix" --force >/dev/null 2>&1; then | |
| spin_ok "Extension installed in VS Code" | |
| installed=1 | |
| else | |
| spin_fail "VS Code install failed" | |
| fi | |
| fi | |
| if [ $installed -eq 0 ]; then | |
| spin_fail "Neither code-server nor VS Code found on PATH" | |
| return 1 | |
| fi | |
| } | |
| # -- uninstall functions --------------------------------------------------- | |
| uninstall_vantabrain() { | |
| if ! has_vantabrain; then | |
| spin_ok "VantaBrain not installed — nothing to do" | |
| return 0 | |
| fi | |
| spin_start "Removing VantaBrain ${DIM}$BRAIN_HOME${NC}" | |
| # Remove PATH entry from shell profile | |
| local profile | |
| profile="$(detect_profile)" | |
| if [ -n "$profile" ] && [ -f "$profile" ]; then | |
| # Remove lines that add BRAIN_HOME/bin to PATH | |
| sed -i.bak '/VantaBrain\/bin/d' "$profile" 2>/dev/null || \ | |
| sed -i '' '/VantaBrain\/bin/d' "$profile" 2>/dev/null || true | |
| fi | |
| rm -rf "$BRAIN_HOME" | |
| spin_ok "VantaBrain removed" | |
| } | |
| uninstall_hakcer() { | |
| if ! has_hakcer; then | |
| spin_ok "hakcerline not installed — nothing to do" | |
| return 0 | |
| fi | |
| spin_start "Removing hakcerline" | |
| # Use the built-in uninstaller (removes statusLine from settings + config dir) | |
| npx -y hakcerline@latest uninstall --yes >/dev/null 2>&1 || true | |
| # Belt + suspenders: ensure statusLine is gone from Claude settings | |
| unpatch_claude_statusline | |
| # Remove config dir if the npm uninstaller missed it | |
| [ -d "$HAKCER_CONFIG_DIR" ] && rm -rf "$HAKCER_CONFIG_DIR" | |
| spin_ok "hakcerline removed" | |
| } | |
| uninstall_vscode_ext() { | |
| local removed=0 | |
| if command -v code-server >/dev/null 2>&1; then | |
| spin_start "Removing VantaBrain extension from code-server..." | |
| code-server --uninstall-extension vantagrid.vantabrain >/dev/null 2>&1 && removed=1 | |
| spin_ok "Extension removed from code-server" | |
| fi | |
| if command -v code >/dev/null 2>&1; then | |
| spin_start "Removing VantaBrain extension from VS Code..." | |
| code --uninstall-extension vantagrid.vantabrain >/dev/null 2>&1 && removed=1 | |
| spin_ok "Extension removed from VS Code" | |
| fi | |
| if [ $removed -eq 0 ]; then | |
| spin_ok "VantaBrain extension not found — nothing to do" | |
| fi | |
| } | |
| # -- settings patchers ----------------------------------------------------- | |
| unpatch_claude_statusline() { | |
| node - >/dev/null 2>&1 <<'NODE' | |
| const fs = require('fs'); | |
| const os = require('os'); | |
| const path = require('path'); | |
| const p = path.join(os.homedir(), '.claude', 'settings.json'); | |
| if (!fs.existsSync(p)) return; | |
| try { | |
| const cfg = JSON.parse(fs.readFileSync(p, 'utf-8')); | |
| if (cfg.statusLine && String(cfg.statusLine.command || '').includes('hakcerline')) { | |
| delete cfg.statusLine; | |
| fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n'); | |
| } | |
| } catch (e) {} | |
| NODE | |
| } | |
| # -- lobotomy -------------------------------------------------------------- | |
| lobotomy() { | |
| echo "" >&2 | |
| echo -e " ${RED}${BOLD}╔══════════════════════════════════════════════╗${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║ L O B O T O M Y ║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}╠══════════════════════════════════════════════╣${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} This will: ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE}• Remove VantaBrain repo + all notes${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE}• Remove hakcerline config + statusLine${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE}• Uninstall VS Code extension${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE}• Nuke ~/.claude/ (settings, sessions,${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE} memory, project data, CLAUDE.md files)${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE}• Remove vantabrain from PATH${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${WHITE}• Drop a marker to prevent re-enrollment${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}║${NC} ${YELLOW}This cannot be undone.${NC} ${RED}${BOLD}║${NC}" >&2 | |
| echo -e " ${RED}${BOLD}╚══════════════════════════════════════════════╝${NC}" >&2 | |
| echo "" >&2 | |
| if ! confirm "Type 'y' to proceed with lobotomy" "n"; then | |
| echo -e " ${DIM}Aborted.${NC}" >&2 | |
| return 1 | |
| fi | |
| echo "" >&2 | |
| # 1. Uninstall VS Code extension | |
| uninstall_vscode_ext | |
| # 2. Uninstall hakcerline | |
| uninstall_hakcer | |
| # 3. Remove VantaBrain repo | |
| if [ -d "$BRAIN_HOME" ]; then | |
| spin_start "Removing VantaBrain ${DIM}$BRAIN_HOME${NC}" | |
| rm -rf "$BRAIN_HOME" | |
| spin_ok "VantaBrain repo removed" | |
| fi | |
| # 4. Remove PATH entries from shell profile | |
| local profile | |
| profile="$(detect_profile)" | |
| if [ -n "$profile" ] && [ -f "$profile" ]; then | |
| spin_start "Cleaning shell profile ${DIM}$profile${NC}" | |
| sed -i.bak '/VantaBrain/d; /vantabrain/d; /hakcerline/d' "$profile" 2>/dev/null || \ | |
| sed -i '' '/VantaBrain/d; /vantabrain/d; /hakcerline/d' "$profile" 2>/dev/null || true | |
| spin_ok "Shell profile cleaned" | |
| fi | |
| # 5. Nuke ~/.claude/ | |
| if [ -d "$HOME/.claude" ]; then | |
| spin_start "Removing ~/.claude/ ${DIM}(settings, sessions, memory, CLAUDE.md files)${NC}" | |
| rm -rf "$HOME/.claude" | |
| spin_ok "~/.claude/ removed" | |
| fi | |
| # 6. Remove any CLAUDE.md files in home or common project dirs | |
| spin_start "Scanning for stray CLAUDE.md files..." | |
| local found_md=0 | |
| while IFS= read -r -d '' f; do | |
| rm -f "$f" | |
| found_md=$((found_md + 1)) | |
| done < <(find "$HOME" -maxdepth 3 -name "CLAUDE.md" -print0 2>/dev/null) | |
| if [ $found_md -gt 0 ]; then | |
| spin_ok "Removed $found_md CLAUDE.md file(s)" | |
| else | |
| spin_ok "No stray CLAUDE.md files found" | |
| fi | |
| # 7. Drop lobotomy marker | |
| echo "lobotomized=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$LOBOTOMY_MARKER" | |
| spin_ok "Lobotomy marker set ${DIM}($LOBOTOMY_MARKER)${NC}" | |
| echo "" >&2 | |
| echo -e " ${RED}✗${NC} ${BOLD}${WHITE}Lobotomy complete${NC}" >&2 | |
| echo -e " ${DIM}VantaBrain has been fully removed and will not auto-enroll.${NC}" >&2 | |
| echo -e " ${DIM}To reverse: rm $LOBOTOMY_MARKER && re-run this installer.${NC}" >&2 | |
| echo "" >&2 | |
| } | |
| # -- helpers --------------------------------------------------------------- | |
| detect_profile() { | |
| local shell_name | |
| shell_name="$(basename "${SHELL:-/bin/sh}")" | |
| case "$shell_name" in | |
| zsh) [ -f "$HOME/.zshrc" ] && echo "$HOME/.zshrc" ;; | |
| bash) | |
| if [ -f "$HOME/.bashrc" ]; then echo "$HOME/.bashrc" | |
| elif [ -f "$HOME/.bash_profile" ]; then echo "$HOME/.bash_profile" | |
| fi ;; | |
| fish) [ -f "$HOME/.config/fish/config.fish" ] && echo "$HOME/.config/fish/config.fish" ;; | |
| *) [ -f "$HOME/.profile" ] && echo "$HOME/.profile" ;; | |
| esac | |
| } | |
| # -- status display -------------------------------------------------------- | |
| show_status() { | |
| echo "" >&2 | |
| echo -e " ${BOLD}${WHITE}Installed Components${NC}" >&2 | |
| echo "" >&2 | |
| if has_vantabrain; then | |
| local vb_ver | |
| vb_ver="$(git -C "$BRAIN_HOME" describe --tags 2>/dev/null || git -C "$BRAIN_HOME" log -1 --format='%h' 2>/dev/null || echo 'unknown')" | |
| echo -e " ${GREEN}■${NC} VantaBrain ${DIM}$BRAIN_HOME ($vb_ver)${NC}" >&2 | |
| else | |
| echo -e " ${DIM}□ VantaBrain not installed${NC}" >&2 | |
| fi | |
| if has_hakcer; then | |
| echo -e " ${GREEN}■${NC} hakcerline ${DIM}npm · config: $HAKCER_CONFIG_DIR${NC}" >&2 | |
| else | |
| echo -e " ${DIM}□ hakcerline not installed${NC}" >&2 | |
| fi | |
| if has_vscode_ext; then | |
| echo -e " ${GREEN}■${NC} VS Code extension ${DIM}vantagrid.vantabrain${NC}" >&2 | |
| else | |
| echo -e " ${DIM}□ VS Code extension not installed${NC}" >&2 | |
| fi | |
| echo "" >&2 | |
| } | |
| # ========================================================================= | |
| # MAIN | |
| # ========================================================================= | |
| # -- banner ---------------------------------------------------------------- | |
| echo "" >&2 | |
| echo -e " ${BOLD}${WHITE}VantaBrain${NC} ${DIM}v${VB_VERSION}${NC}" >&2 | |
| echo "" >&2 | |
| # -- lobotomy check -------------------------------------------------------- | |
| if [ -f "$LOBOTOMY_MARKER" ]; then | |
| echo -e " ${RED}✗${NC} This machine has been lobotomized." >&2 | |
| echo -e " ${DIM}VantaBrain will not install or auto-enroll.${NC}" >&2 | |
| echo -e " ${DIM}To reverse: rm $LOBOTOMY_MARKER${NC}" >&2 | |
| echo "" >&2 | |
| exit 0 | |
| fi | |
| # -- preflight ------------------------------------------------------------- | |
| spin_start "Checking dependencies..." | |
| missing=0 | |
| for dep in gh git node npm; do | |
| if ! command -v "$dep" >/dev/null 2>&1; then | |
| spin_fail "$dep is required but not installed." | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" -eq 1 ]; then | |
| echo "" >&2 | |
| echo -e " ${DIM}macOS: brew install gh git node${NC}" >&2 | |
| echo -e " ${DIM}Linux: apt install gh git nodejs npm${NC}" >&2 | |
| exit 1 | |
| fi | |
| if ! gh auth status >/dev/null 2>&1; then | |
| spin_fail "gh is not authenticated. Run: ${WHITE}gh auth login${NC}" | |
| exit 1 | |
| fi | |
| NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])") | |
| if [ "$NODE_MAJOR" -lt 18 ]; then | |
| spin_fail "Node.js >= 18 required (found $(node -v))" | |
| echo -e " ${DIM}brew install node or nvm install 20${NC}" >&2 | |
| exit 1 | |
| fi | |
| spin_ok "Dependencies ${DIM}gh $(gh --version | head -1 | awk '{print $3}'), git $(git --version | awk '{print $3}'), node $(node -v | tr -d v)${NC}" | |
| # -- mode selection -------------------------------------------------------- | |
| if has_anything; then | |
| show_status | |
| menu_select "What do you want to do?" \ | |
| "Install / update components" \ | |
| "Uninstall components" \ | |
| "Lobotomy (remove everything, prevent re-enrollment)" | |
| MODE=$MENU_RESULT | |
| else | |
| # Nothing installed — go straight to install | |
| MODE=0 | |
| fi | |
| # -- INSTALL --------------------------------------------------------------- | |
| if [ $MODE -eq 0 ]; then | |
| # Build option list based on what needs install vs update | |
| local_opts=("All") | |
| if has_vantabrain; then | |
| local_opts+=("VantaBrain (update)") | |
| else | |
| local_opts+=("VantaBrain") | |
| fi | |
| if has_hakcer; then | |
| local_opts+=("hakcerline (update)") | |
| else | |
| local_opts+=("hakcerline") | |
| fi | |
| if has_vscode_ext; then | |
| local_opts+=("VS Code extension (update)") | |
| else | |
| local_opts+=("VS Code extension") | |
| fi | |
| menu_multiselect "Select components to install:" "${local_opts[@]}" | |
| if [ -z "$MULTI_RESULT" ]; then | |
| echo -e "\n ${DIM}Nothing selected.${NC}" >&2 | |
| exit 0 | |
| fi | |
| echo "" >&2 | |
| for idx in $MULTI_RESULT; do | |
| case $idx in | |
| 1) install_vantabrain ;; | |
| 2) install_hakcer ;; | |
| 3) install_vscode_ext ;; | |
| esac | |
| done | |
| # Final message | |
| echo "" >&2 | |
| echo -e " ${GREEN}✓${NC} ${BOLD}${WHITE}VantaBrain v${VB_VERSION}${NC} — install complete" >&2 | |
| if has_hakcer; then | |
| echo -e " ${DIM}Preview: ${WHITE}npx hakcerline preview${NC}" >&2 | |
| fi | |
| echo "" >&2 | |
| fi | |
| # -- UNINSTALL ------------------------------------------------------------- | |
| if [ $MODE -eq 1 ]; then | |
| # Build list of installed components | |
| uninst_opts=("All installed") | |
| uninst_keys=() | |
| key_idx=0 | |
| if has_vantabrain; then | |
| uninst_opts+=("VantaBrain") | |
| uninst_keys+=("vantabrain") | |
| fi | |
| if has_hakcer; then | |
| uninst_opts+=("hakcerline") | |
| uninst_keys+=("hakcer") | |
| fi | |
| if has_vscode_ext; then | |
| uninst_opts+=("VS Code extension") | |
| uninst_keys+=("vscode") | |
| fi | |
| if [ ${#uninst_keys[@]} -eq 0 ]; then | |
| echo -e "\n ${DIM}Nothing installed to remove.${NC}" >&2 | |
| exit 0 | |
| fi | |
| menu_multiselect "Select components to uninstall:" "${uninst_opts[@]}" | |
| if [ -z "$MULTI_RESULT" ]; then | |
| echo -e "\n ${DIM}Nothing selected.${NC}" >&2 | |
| exit 0 | |
| fi | |
| if ! confirm "Uninstall selected components?" "n"; then | |
| echo -e " ${DIM}Aborted.${NC}" >&2 | |
| exit 0 | |
| fi | |
| echo "" >&2 | |
| for idx in $MULTI_RESULT; do | |
| key_idx=$((idx - 1)) | |
| case "${uninst_keys[$key_idx]}" in | |
| vantabrain) uninstall_vantabrain ;; | |
| hakcer) uninstall_hakcer ;; | |
| vscode) uninstall_vscode_ext ;; | |
| esac | |
| done | |
| echo "" >&2 | |
| echo -e " ${GREEN}✓${NC} ${BOLD}${WHITE}Uninstall complete${NC}" >&2 | |
| echo "" >&2 | |
| fi | |
| # -- LOBOTOMY -------------------------------------------------------------- | |
| if [ $MODE -eq 2 ]; then | |
| lobotomy | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment