#!/bin/bash # VantaBrain installer # curl -fsSL https://vb.darkcode.ai | bash set -u VB_VERSION="3.2.0" REPO="vantagrid/vantabrain" BRAIN_HOME="${BRAIN_HOME:-$HOME/VantaBrain}" # hakcer-statusline HAKCER_REPO="https://github.com/haKC-ai/hakcer.git" HAKCER_BRANCH="feat/ink-port" HAKCER_DIR="${HAKCER_DIR:-$HOME/hakcer-statusline}" # 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 # Draw _draw_multi() { for ((i = 0; i < count; i++)); do printf "\033[2K" >&2 local check if [ "${selected[$i]}" = "1" ]; then check="${GREEN}■${NC}" else check="${DIM}□${NC}" fi if [ $i -eq $cursor ]; then printf " ${CYAN}❯${NC} %b ${WHITE}%s${NC}\n" "$check" "${options[$i]}" >&2 else printf " %b ${DIM}%s${NC}\n" "$check" "${options[$i]}" >&2 fi done } _draw_multi 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: set everything to the inverse of current "All" state 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" # Uncheck "All" if any individual is unchecked else selected[$cursor]="1" # Check if all individuals are selected → auto-check "All" 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 printf "\033[%dA" "$count" >&2 _draw_multi 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() { command -v hakcer-statusline >/dev/null 2>&1 || [ -d "$HAKCER_DIR/ink" ] } 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 export PATH="$BRAIN_HOME/bin:$PATH" # 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 # Detect shell profile local profile profile="$(detect_profile)" # 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() { if [ -d "$HAKCER_DIR/.git" ]; then spin_start "Updating hakcer-statusline ${DIM}(${HAKCER_BRANCH})${NC}" git -C "$HAKCER_DIR" fetch origin "$HAKCER_BRANCH" >/dev/null 2>&1 || { spin_fail "fetch failed"; return 1; } git -C "$HAKCER_DIR" checkout "$HAKCER_BRANCH" >/dev/null 2>&1 || { spin_fail "checkout failed"; return 1; } git -C "$HAKCER_DIR" reset --hard "origin/$HAKCER_BRANCH" >/dev/null 2>&1 || { spin_fail "reset failed"; return 1; } elif [ -d "$HAKCER_DIR" ]; then spin_start "Backing up ${DIM}$HAKCER_DIR${NC} and cloning fresh" mv "$HAKCER_DIR" "${HAKCER_DIR}.bak.$(date +%s)" git clone --quiet -b "$HAKCER_BRANCH" "$HAKCER_REPO" "$HAKCER_DIR" >/dev/null 2>&1 || { spin_fail "clone failed"; return 1; } else spin_start "Cloning hakcer-statusline ${DIM}(${HAKCER_BRANCH})${NC}" git clone --quiet -b "$HAKCER_BRANCH" "$HAKCER_REPO" "$HAKCER_DIR" >/dev/null 2>&1 || { spin_fail "clone failed"; return 1; } fi if [ ! -d "$HAKCER_DIR/ink" ]; then spin_fail "ink/ directory not found in hakcer repo" return 1 fi (cd "$HAKCER_DIR/ink" && npm install --silent >/dev/null 2>&1) || { spin_fail "npm install failed"; return 1; } (cd "$HAKCER_DIR/ink" && npm run build --silent >/dev/null 2>&1) || { spin_fail "npm build failed"; return 1; } (cd "$HAKCER_DIR/ink" && npm link --silent >/dev/null 2>&1) || { spin_fail "npm link failed"; return 1; } local hakcer_path hakcer_path="$(command -v hakcer-statusline 2>/dev/null || true)" if [ -n "$hakcer_path" ]; then spin_ok "hakcer-statusline installed ${DIM}($hakcer_path)${NC}" else spin_fail "hakcer-statusline built but not on \$PATH" return 1 fi # Patch Claude Code settings spin_start "Wiring hakcer-statusline into ~/.claude/settings.json" if patch_claude_statusline; then spin_ok "Claude Code statusLine → ${WHITE}hakcer-statusline${NC}" else spin_fail "settings.json patch failed" 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 "hakcer-statusline not installed — nothing to do" return 0 fi spin_start "Removing hakcer-statusline" # Unlink the global binary if [ -d "$HAKCER_DIR/ink" ]; then (cd "$HAKCER_DIR/ink" && npm unlink --silent >/dev/null 2>&1) || true fi # Remove the repo rm -rf "$HAKCER_DIR" # Remove statusLine from Claude settings unpatch_claude_statusline spin_ok "hakcer-statusline 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 ----------------------------------------------------- patch_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'); let cfg = {}; if (fs.existsSync(p)) { fs.copyFileSync(p, p + '.bak.' + Date.now()); try { cfg = JSON.parse(fs.readFileSync(p, 'utf-8')); } catch (e) { cfg = {}; } } cfg.statusLine = { type: 'command', command: 'hakcer-statusline' }; fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n'); NODE } 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 && cfg.statusLine.command === 'hakcer-statusline') { 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 hakcer-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 hakcer-statusline 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; /hakcer-statusline/d' "$profile" 2>/dev/null || \ sed -i '' '/VantaBrain/d; /vantabrain/d; /hakcer-statusline/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 local hk_path hk_path="$(command -v hakcer-statusline 2>/dev/null || echo "$HAKCER_DIR")" echo -e " ${GREEN}■${NC} hakcer-statusline ${DIM}$hk_path${NC}" >&2 else echo -e " ${DIM}□ hakcer-statusline 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 20 ]; then spin_fail "Node.js >= 20 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+=("hakcer-statusline (update)") else local_opts+=("hakcer-statusline") 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 command -v vantabrain >/dev/null 2>&1; then echo -e " ${DIM}Run ${WHITE}vantabrain${DIM} from any terminal.${NC}" >&2 fi if command -v hakcer-statusline >/dev/null 2>&1; then echo -e " ${DIM}Preview:${NC} ${WHITE}echo '{}' | hakcer-statusline --preview --scene core_port_scan --width 80${NC}" >&2 fi echo "" >&2 fi # -- UNINSTALL ------------------------------------------------------------- if [ $MODE -eq 1 ]; then # Build list of installed components uninst_opts=("All installed") uninst_keys=() if has_vantabrain; then uninst_opts+=("VantaBrain") uninst_keys+=("vantabrain") fi if has_hakcer; then uninst_opts+=("hakcer-statusline") 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 local 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