From d1c7926ea97d12044e0b6e6f8cf075d7fb839b0d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:07:20 +0000 Subject: [PATCH 1/2] Rework setup.sh into a guided, professional install wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the setup experience interactive, transparent, and easy to follow. Live progress & health - Stop piping `docker compose pull`/`up` through `tail` — that forced Docker's silent/plain mode and hid the per-layer download bars. Stream the output so the native progress UI renders in real time. - Add a colorized, in-place live service-health dashboard that polls `docker compose ps` and refreshes until services report healthy. Interactive AI client choice (with sub-choices) - Provider menu (Claude / ChatGPT / DeepSeek); Claude expands to Claude Code (Docker), Claude Code (Web), or Claude Desktop. The choice drives which profile starts, whether the MCP Gateway is enabled, and which key (if any) is requested. Guided API keys - For each key, explain what it's for, where it's used, and the exact steps to obtain it; every key is optional and skippable. Secrets are read hidden and never echoed. Neo4j: none / local / Aura - Local starts the neo4j Docker profile; Aura walks through console.neo4j.io and stores the cloud connection details in .env without starting a local container. Safety/robustness - New set_env helper removes prior (commented or active) definitions and appends the value verbatim — never passing user values through sed (injection-safe; handles passwords with | & \). - Detect non-interactive stdin and fall back to flag-driven defaults; all existing flags preserved. up -d guarded so the health dashboard always renders. Docs: sync README install steps and DOCKER.md wizard description. https://claude.ai/code/session_0111sKEqbgpbAZKAYDuQmcHa --- DOCKER.md | 2 +- README.md | 18 +- setup.sh | 664 ++++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 512 insertions(+), 172 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 4b89068..0f024a1 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -98,7 +98,7 @@ Claude Desktop ──▶ MCP Gateway (localhost:8080/mcp) ──┬──▶ Kal ```bash git clone https://github.com/valITino/blhackbox.git cd blhackbox -./setup.sh # interactive wizard: prereqs, .env, pull, start, health +./setup.sh # guided wizard: prereqs, AI client, keys, Neo4j, live pull/health ``` ### Manual — Default Stack (7 containers) diff --git a/README.md b/README.md index 1c252bc..f177c80 100644 --- a/README.md +++ b/README.md @@ -306,14 +306,16 @@ cd blhackbox ./setup.sh ``` -The setup wizard will: - -1. Check prerequisites (Docker, Docker Compose, disk space) -2. Let you choose optional components (Neo4j, MCP Gateway) -3. Prompt for your `ANTHROPIC_API_KEY` (required for Claude Code in Docker) -4. Generate `.env` and create the `output/` directory -5. Pull Docker images and start all services -6. Wait for health checks to pass +The setup wizard walks you through everything interactively: + +1. **Check prerequisites** (Docker, Docker Compose, Git, disk space) +2. **Choose your AI client** — Claude (Claude Code in Docker, Claude Code Web, or Claude Desktop), ChatGPT/OpenAI, or DeepSeek. The choice decides which agent/profile starts and which key (if any) you need. +3. **Configure API keys with guidance** — for each key it explains what it's for, where it's used, and the exact steps to obtain it; every key is optional/skippable. +4. **Pick a knowledge graph** — none, local Neo4j (Docker), or Neo4j Aura (cloud). Aura mode walks you through `console.neo4j.io` and stores your connection details without starting a local container. +5. **Optionally add recon API keys** (WPScan, Shodan, VirusTotal, SecurityTrails) — all skippable; tools run without them. +6. **Generate `.env`** and create the `output/` directory +7. **Pull images with live progress** and start all services +8. **Show a live service-health dashboard** until services are healthy Non-interactive flags: diff --git a/setup.sh b/setup.sh index 38f3739..fd83875 100755 --- a/setup.sh +++ b/setup.sh @@ -1,12 +1,18 @@ #!/usr/bin/env bash # ── blhackbox v2.0 — Interactive Setup Wizard ──────────────────────── -# One-command setup: checks prerequisites, generates .env, pulls images, -# starts services, and verifies health. +# Professional, guided setup: checks prerequisites, lets you pick your AI +# client, optional knowledge graph (local Neo4j or Aura cloud), and API +# keys (with step-by-step help for each), then pulls images and starts +# services with LIVE progress and a live service-health dashboard. # # Usage: -# ./setup.sh # Interactive mode -# ./setup.sh --api-key sk-ant-... # Non-interactive with API key -# ./setup.sh --minimal # Skip optional profiles +# ./setup.sh # Interactive wizard (recommended) +# ./setup.sh --api-key sk-ant-... # Non-interactive: Claude Code container +# ./setup.sh --deepseek-key sk-.. # Non-interactive: DeepSeek container +# ./setup.sh --minimal # Core stack only, no prompts +# ./setup.sh --with-neo4j # Enable local Neo4j (Docker) +# ./setup.sh --with-gateway # Enable MCP Gateway (Desktop/ChatGPT) +# ./setup.sh --skip-pull # Use cached images # ./setup.sh --help # Show usage set -euo pipefail @@ -33,7 +39,68 @@ SKIP_PULL=false NEO4J_PASS="" PROFILES="" -# ── Functions ──────────────────────────────────────────────────────── +# Runtime selections (filled in by the wizard). +AI_VARIANT="" # claude-code | claude-web | claude-desktop | chatgpt | deepseek | "" +AGENT_PROFILE="" # claude-code | deepseek | "" (containerized agents only) +NEO4J_MODE="none" # none | local | aura +WRITE_ENV=true # whether we may (over)write .env +STATUS_LINES=0 # bookkeeping for the live status redraw + +# Interactivity detection: only prompt when stdin is a real terminal. +INTERACTIVE=true; [ -t 0 ] || INTERACTIVE=false +INTERACTIVE_OUT=true; [ -t 1 ] || INTERACTIVE_OUT=false + +# ── Generic helpers ────────────────────────────────────────────────── + +# Append a compose --profile flag once (no duplicates). +add_profile() { + case " $PROFILES " in + *" --profile $1 "*) ;; + *) PROFILES="${PROFILES:+$PROFILES }--profile $1" ;; + esac +} + +# Yes/no prompt. $1 = question, $2 = default (y|n). Returns 0 for yes. +ask_yes_no() { + local question="$1" def="${2:-n}" ans hint="[y/N]" + [ "$def" = "y" ] && hint="[Y/n]" + read -rp " $question $hint " ans &2 + printf '%s' "$val" +} + +# Set KEY=VALUE in .env safely. The key is always a literal we control, so +# the value is NEVER passed through sed (no escaping pitfalls, injection-safe). +# Removes any prior definition (commented placeholder or active line), then +# appends the real value verbatim. +set_env() { + local key="$1" val="$2" file="$SCRIPT_DIR/.env" + sed -i -E "/^[[:space:]]*#?[[:space:]]*${key}=/d" "$file" + printf '%s=%s\n' "$key" "$val" >> "$file" +} + +# ── Banner & usage ─────────────────────────────────────────────────── print_banner() { echo "" @@ -47,11 +114,15 @@ print_banner() { usage() { echo "Usage: $0 [OPTIONS]" echo "" + echo "Interactive mode (no flags) walks you through: AI client, knowledge" + echo "graph (local Neo4j or Aura cloud), and API keys — with guidance for" + echo "each — then pulls images and starts services with live progress." + echo "" echo "Options:" - echo " --api-key KEY Set ANTHROPIC_API_KEY for the Claude Code container" - echo " --deepseek-key KEY Set DEEPSEEK_API_KEY for the DeepSeek (Reasonix) container" - echo " --minimal Core stack only (no Neo4j)" - echo " --with-neo4j Enable Neo4j knowledge graph" + echo " --api-key KEY Set ANTHROPIC_API_KEY (Claude Code container)" + echo " --deepseek-key KEY Set DEEPSEEK_API_KEY (DeepSeek/Reasonix container)" + echo " --minimal Core stack only (no prompts, no Neo4j/Gateway)" + echo " --with-neo4j Enable local Neo4j knowledge graph (Docker)" echo " --with-gateway Enable MCP Gateway for Claude Desktop/ChatGPT" echo " --skip-pull Skip docker compose pull (use cached images)" echo " --help Show this help" @@ -59,7 +130,8 @@ usage() { exit 0 } -# Check if a command exists +# ── Prerequisites ──────────────────────────────────────────────────── + require_cmd() { if ! command -v "$1" &>/dev/null; then echo -e " ${CROSS} ${BOLD}$1${NC} is not installed." @@ -75,7 +147,8 @@ version_gte() { } check_prerequisites() { - echo -e "${BOLD}Checking prerequisites...${NC}" + echo -e "${BOLD}Step 1/8 · Checking prerequisites${NC}" + echo -e "${DIM} Verifying Docker, Compose, Git, and disk space before we begin.${NC}" echo "" local ok=true @@ -130,135 +203,303 @@ check_prerequisites() { fi } -configure_env() { - echo -e "${BOLD}Configuring environment...${NC}" +# ── .env preparation ───────────────────────────────────────────────── + +prepare_env() { + echo -e "${BOLD}Step 2/8 · Preparing configuration (.env)${NC}" + echo -e "${DIM} Your keys and settings live in .env (git-ignored — never committed).${NC}" echo "" - # If .env already exists, ask before overwriting if [ -f "$SCRIPT_DIR/.env" ]; then - echo -e " ${WARN} ${BOLD}.env${NC} already exists." - read -rp " Overwrite with fresh config? [y/N] " overwrite - if [[ ! "$overwrite" =~ ^[Yy] ]]; then - echo -e " ${ARROW} Keeping existing .env" - echo "" - return + if [ "$INTERACTIVE" = true ]; then + echo -e " ${WARN} ${BOLD}.env${NC} already exists." + if ask_yes_no "Overwrite it with a fresh, guided configuration?" "n"; then + cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env" + echo -e " ${CHECK} Started a fresh .env from the template" + else + WRITE_ENV=false + echo -e " ${ARROW} Keeping your existing .env (won't prompt for keys)" + fi + else + WRITE_ENV=false + echo -e " ${ARROW} Existing .env kept (non-interactive run)" fi + else + cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env" + echo -e " ${CHECK} Created .env from the template" fi + echo "" +} - # Start from template - cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env" +# ── AI client selection ────────────────────────────────────────────── + +print_anthropic_help() { + echo -e " ${BOLD}What it's for:${NC} authenticates the Claude Code agent running" + echo -e " inside Docker. ${DIM}Not needed for Claude Code Web or Claude Desktop.${NC}" + echo -e " ${BOLD}Where it's used:${NC} the ${CYAN}claude-code${NC} container (ANTHROPIC_API_KEY)." + echo -e " ${BOLD}How to get it:${NC}" + echo -e " ${DIM}1.${NC} Sign in at ${CYAN}https://console.anthropic.com${NC}" + echo -e " ${DIM}2.${NC} Add a payment method under ${BOLD}Billing${NC} (pay-as-you-go)" + echo -e " ${DIM}3.${NC} Open ${BOLD}Settings → API keys${NC} → ${BOLD}Create Key${NC}" + echo -e " ${DIM}4.${NC} Copy it now — Anthropic shows the key only once" +} - # --- ANTHROPIC_API_KEY --- - if [ -z "$API_KEY" ]; then - echo -e " ${BOLD}ANTHROPIC_API_KEY${NC} ${DIM}(required for Claude Code in Docker)${NC}" - echo -e " ${DIM}Get yours at: https://console.anthropic.com/settings/keys${NC}" - echo -e " ${DIM}Press Enter to skip if you'll only use Claude Code on host.${NC}" - read -rsp " API Key: " API_KEY - echo "" - fi +print_deepseek_help() { + echo -e " ${BOLD}What it's for:${NC} authenticates the DeepSeek (Reasonix) agent in Docker." + echo -e " ${BOLD}Where it's used:${NC} the ${CYAN}deepseek${NC} container (DEEPSEEK_API_KEY)." + echo -e " ${BOLD}How to get it:${NC}" + echo -e " ${DIM}1.${NC} Sign in at ${CYAN}https://platform.deepseek.com${NC}" + echo -e " ${DIM}2.${NC} Add balance under ${BOLD}Billing${NC} (the API needs a positive balance)" + echo -e " ${DIM}3.${NC} Open ${BOLD}API keys${NC} → ${BOLD}Create new API key${NC}" + echo -e " ${DIM}4.${NC} Copy it now — DeepSeek shows the key only once" +} - if [ -n "$API_KEY" ]; then - # Replace the commented-out line with the actual key - sed -i "s|^# ANTHROPIC_API_KEY=sk-ant-.*|ANTHROPIC_API_KEY=${API_KEY}|" "$SCRIPT_DIR/.env" - echo -e " ${CHECK} ANTHROPIC_API_KEY configured" - else - echo -e " ${WARN} ANTHROPIC_API_KEY skipped — Claude Code Docker won't start without it" - fi +print_openai_help() { + echo -e " ${BOLD}What it's for:${NC} used by your host-based ChatGPT/OpenAI MCP client" + echo -e " ${DIM}(and optional vector-memory embeddings). The Docker stack itself does${NC}" + echo -e " ${DIM}not require it — store it here only for convenience.${NC}" + echo -e " ${BOLD}How to get it:${NC}" + echo -e " ${DIM}1.${NC} Sign in at ${CYAN}https://platform.openai.com${NC}" + echo -e " ${DIM}2.${NC} Add billing, then open ${BOLD}API keys${NC} → ${BOLD}Create new secret key${NC}" + echo -e " ${DIM}3.${NC} Copy it now — OpenAI shows the key only once" +} - # --- DEEPSEEK_API_KEY (for the DeepSeek / Reasonix container) --- - if [ -z "$DEEPSEEK_KEY" ]; then - echo "" - echo -e " ${BOLD}DEEPSEEK_API_KEY${NC} ${DIM}(for the DeepSeek/Reasonix agent in Docker)${NC}" - echo -e " ${DIM}Get yours at: https://platform.deepseek.com/api_keys${NC}" - echo -e " ${DIM}Press Enter to skip if you won't use the DeepSeek agent.${NC}" - read -rsp " API Key: " DEEPSEEK_KEY - echo "" +# Prompt for a single agent key and write it (only when WRITE_ENV is true). +configure_agent_key() { + local var="$1" help_fn="$2" + echo "" + echo -e " ${BOLD}${var}${NC}" + "$help_fn" + echo "" + if [ "$WRITE_ENV" != true ]; then + echo -e " ${ARROW} Keeping the value already in your .env" + return fi - - if [ -n "$DEEPSEEK_KEY" ]; then - sed -i "s|^# DEEPSEEK_API_KEY=sk-.*|DEEPSEEK_API_KEY=${DEEPSEEK_KEY}|" "$SCRIPT_DIR/.env" - echo -e " ${CHECK} DEEPSEEK_API_KEY configured" + if ask_yes_no "Add ${var} now?" "y"; then + local val + val="$(prompt_secret)" + if [ -n "$val" ]; then + set_env "$var" "$val" + echo -e " ${CHECK} ${var} saved to .env" + else + echo -e " ${WARN} Skipped — add it later in .env, then restart the stack" + fi else - echo -e " ${WARN} DEEPSEEK_API_KEY skipped — the DeepSeek agent won't start without it" + echo -e " ${ARROW} Skipped — add it later in .env, then restart the stack" fi +} - # --- NEO4J_PASSWORD --- - if [[ "$PROFILES" == *"neo4j"* ]]; then - if [ -z "$NEO4J_PASS" ]; then - echo "" - echo -e " ${BOLD}NEO4J_PASSWORD${NC} ${DIM}(min 8 chars for Neo4j)${NC}" - read -rp " Password [changeme-min-8-chars]: " NEO4J_PASS - NEO4J_PASS="${NEO4J_PASS:-changeme-min-8-chars}" +choose_ai_client() { + echo -e "${BOLD}Step 3/8 · Choose your AI client${NC}" + echo -e "${DIM} This decides which agent/profile starts and which key (if any) you need.${NC}" + echo "" + + # Non-interactive: infer from flags (preserves scripted behavior). + if [ "$INTERACTIVE" != true ]; then + if [ -n "$API_KEY" ]; then + AI_VARIANT="claude-code"; AGENT_PROFILE="claude-code" + [ "$WRITE_ENV" = true ] && set_env "ANTHROPIC_API_KEY" "$API_KEY" + echo -e " ${ARROW} Claude Code (Docker) — key supplied via --api-key" + elif [ -n "$DEEPSEEK_KEY" ]; then + AI_VARIANT="deepseek"; AGENT_PROFILE="deepseek" + [ "$WRITE_ENV" = true ] && set_env "DEEPSEEK_API_KEY" "$DEEPSEEK_KEY" + echo -e " ${ARROW} DeepSeek (Docker) — key supplied via --deepseek-key" + else + echo -e " ${ARROW} No agent flag given — starting core stack only" fi - sed -i "s|^NEO4J_PASSWORD=.*|NEO4J_PASSWORD=${NEO4J_PASS}|" "$SCRIPT_DIR/.env" - echo -e " ${CHECK} NEO4J_PASSWORD configured" + echo "" + return fi - # --- Keys & tokens reference --- - # Print the full list of keys/tokens the framework can use so the user - # knows exactly what to provide. Only ANTHROPIC_API_KEY (Docker client) is - # ever required; every pentest tool runs without the rest — they only widen - # passive recon coverage and extra data sources. - echo "" - echo -e " ${BOLD}Keys & tokens${NC} ${DIM}(edit .env to add them, then restart the stack):${NC}" - echo "" - echo -e " ${BOLD}Agent keys (set the one for the in-Docker agent you run)${NC}" - echo -e " ${CYAN}ANTHROPIC_API_KEY${NC} ${DIM}Claude Code container (--profile claude-code).${NC}" - echo -e " ${DIM}Not needed for Claude Code Web/host.${NC}" - echo -e " ${DIM}→ https://console.anthropic.com/settings/keys${NC}" - echo -e " ${CYAN}DEEPSEEK_API_KEY${NC} ${DIM}DeepSeek (Reasonix) container (--profile deepseek).${NC}" - echo -e " ${DIM}→ https://platform.deepseek.com/api_keys${NC}" - echo "" - echo -e " ${BOLD}Optional — widen recon (all tools work without them)${NC}" - echo -e " ${CYAN}WPSCAN_API_TOKEN${NC} ${DIM}WordPress vuln database; wpscan auto-reads it.${NC}" - echo -e " ${DIM}→ https://wpscan.com/api (free: 25 req/day)${NC}" - echo -e " ${CYAN}SHODAN_API_KEY${NC} ${DIM}subfinder passive source + Shodan host data.${NC}" - echo -e " ${DIM}→ https://account.shodan.io${NC}" - echo -e " ${CYAN}VIRUSTOTAL_API_KEY${NC} ${DIM}subfinder passive source.${NC}" - echo -e " ${DIM}→ https://www.virustotal.com/gui/my-apikey${NC}" - echo -e " ${CYAN}SECURITYTRAILS_API_KEY${NC} ${DIM}subfinder passive source (subdomains/history).${NC}" - echo -e " ${DIM}→ https://securitytrails.com/app/account/credentials${NC}" - echo -e " ${CYAN}OPENAI_API_KEY${NC} ${DIM}Only for ChatGPT/OpenAI MCP clients (host-based).${NC}" - echo -e " ${DIM}→ https://platform.openai.com/api-keys${NC}" - echo "" - echo -e " ${DIM}theHarvester and amass read keys from their OWN config files, not${NC}" - echo -e " ${DIM}env vars: ~/.theHarvester/api-keys.yaml and ~/.config/amass/datasources.yaml${NC}" + echo -e " ${BOLD}Which AI assistant will drive the pentest?${NC}" + echo -e " ${CYAN}1)${NC} Claude ${DIM}(Anthropic)${NC}" + echo -e " ${CYAN}2)${NC} ChatGPT ${DIM}(OpenAI)${NC}" + echo -e " ${CYAN}3)${NC} DeepSeek" + local provider; provider="$(read_choice 3 1)" + echo "" + case "$provider" in + 1) # Claude → sub-choice + echo -e " ${BOLD}Which Claude experience?${NC}" + echo -e " ${CYAN}1)${NC} Claude Code — containerized in Docker ${DIM}(recommended; needs API key)${NC}" + echo -e " ${CYAN}2)${NC} Claude Code — Web at claude.ai/code ${DIM}(no key in .env)${NC}" + echo -e " ${CYAN}3)${NC} Claude Desktop — host app + MCP Gateway ${DIM}(no API key; app login)${NC}" + local v; v="$(read_choice 3 1)" + case "$v" in + 1) AI_VARIANT="claude-code"; AGENT_PROFILE="claude-code" + configure_agent_key "ANTHROPIC_API_KEY" print_anthropic_help ;; + 2) AI_VARIANT="claude-web" + echo "" + echo -e " ${ARROW} Claude Code Web uses the stdio MCP server from ${CYAN}.mcp.json${NC} —" + echo -e " ${DIM}no API key in .env. Open this repo at claude.ai/code; the full${NC}" + echo -e " ${DIM}Docker stack below is optional for that path.${NC}" ;; + 3) AI_VARIANT="claude-desktop"; add_profile gateway + echo "" + echo -e " ${ARROW} Claude Desktop is a host app (no API key — it uses your app login)." + echo -e " ${DIM}It connects through the MCP Gateway, which I'll enable for you.${NC}" ;; + esac + ;; + 2) # ChatGPT / OpenAI + AI_VARIANT="chatgpt"; add_profile gateway + echo -e " ${ARROW} ChatGPT/OpenAI is a host client; it connects through the MCP" + echo -e " ${DIM}Gateway (enabled for you). The stack itself needs no OpenAI key.${NC}" + configure_agent_key "OPENAI_API_KEY" print_openai_help + ;; + 3) # DeepSeek + AI_VARIANT="deepseek"; AGENT_PROFILE="deepseek" + echo -e " ${ARROW} DeepSeek (Reasonix) runs containerized in Docker." + configure_agent_key "DEEPSEEK_API_KEY" print_deepseek_help + ;; + esac echo "" } -select_profiles() { - if [ "$MINIMAL" = true ]; then - PROFILES="" - echo -e " ${ARROW} Minimal mode: core stack only (4 containers)" +# ── Neo4j selection ────────────────────────────────────────────────── + +choose_neo4j() { + echo -e "${BOLD}Step 4/8 · Knowledge graph (Neo4j)${NC}" + echo -e "${DIM} Optional cross-session memory. Skip it for one-off engagements.${NC}" + echo "" + + # Non-interactive: honor --with-neo4j (local) only. + if [ "$INTERACTIVE" != true ]; then + if [[ "$PROFILES" == *"--profile neo4j"* ]]; then + NEO4J_MODE="local" + [ "$WRITE_ENV" = true ] && { + set_env "NEO4J_URI" "bolt://neo4j:7687" + set_env "NEO4J_USER" "neo4j" + set_env "NEO4J_PASSWORD" "${NEO4J_PASS:-changeme-min-8-chars}" + } + echo -e " ${ARROW} Local Neo4j enabled via --with-neo4j" + else + echo -e " ${ARROW} Neo4j disabled" + fi echo "" return fi - echo -e "${BOLD}Select optional components:${NC}" + echo -e " ${BOLD}Use a Neo4j knowledge graph?${NC}" + echo -e " ${CYAN}1)${NC} No — single-engagement use ${DIM}(default)${NC}" + echo -e " ${CYAN}2)${NC} Local — Neo4j in Docker ${DIM}(--profile neo4j)${NC}" + echo -e " ${CYAN}3)${NC} Aura — Neo4j cloud, free tier ${DIM}(no local container)${NC}" + local c; c="$(read_choice 3 1)" + echo "" + + case "$c" in + 2) NEO4J_MODE="local"; add_profile neo4j + echo -e " ${ARROW} Local Neo4j (Docker)" + echo -e " ${DIM}Bolt URI: ${NC}bolt://neo4j:7687 ${DIM}(internal Docker network)${NC}" + echo -e " ${DIM}Browser: ${NC}http://localhost:7474 ${DIM}(after startup)${NC}" + echo "" + if [ "$WRITE_ENV" = true ]; then + local p; local def="changeme-min-8-chars" + echo -e " ${BOLD}Set a Neo4j password${NC} ${DIM}(min 8 chars; press Enter for '${def}').${NC}" + p="$(prompt_secret " Password (hidden): ")" + [ -n "$p" ] || p="$def" + set_env "NEO4J_URI" "bolt://neo4j:7687" + set_env "NEO4J_USER" "neo4j" + set_env "NEO4J_PASSWORD" "$p" + echo -e " ${CHECK} Local Neo4j configured in .env" + else + echo -e " ${ARROW} Using the Neo4j credentials already in your .env" + fi + ;; + 3) NEO4J_MODE="aura" + echo -e " ${ARROW} Neo4j Aura (cloud) — follow these steps to get your details:" + echo -e " ${DIM}1.${NC} Sign in / create a free account at ${CYAN}https://console.neo4j.io${NC}" + echo -e " ${DIM}2.${NC} Click ${BOLD}New Instance${NC} and pick the ${BOLD}Free${NC} tier ${DIM}(\$0, no card)${NC}" + echo -e " ${DIM}3.${NC} On creation a credentials ${BOLD}.txt downloads automatically${NC} —" + echo -e " ${DIM}it has your Connection URI, Username (neo4j) and Password.${NC}" + echo -e " ${DIM}Save it: the password is shown only once.${NC}" + echo -e " ${DIM}4.${NC} Copy the Connection URI ${DIM}(looks like neo4j+s://xxxx.databases.neo4j.io)${NC}" + echo -e " ${DIM}Note: free instances auto-pause after 72h idle and delete after 30 days paused.${NC}" + echo "" + if [ "$WRITE_ENV" = true ]; then + local uri user pass + read -rp " Connection URI (neo4j+s://...): " uri &1 | tail -5 - echo "" - echo -e " ${CHECK} Images pulled" + if docker compose $PROFILES pull; then + echo "" + echo -e " ${CHECK} Core images ready" + else + echo "" + echo -e " ${WARN} Some images failed to pull — will retry during startup." + fi + + # Pre-fetch the chosen containerized agent so first launch is instant. + if [ -n "$AGENT_PROFILE" ]; then + echo "" + echo -e " ${ARROW} Preparing the ${CYAN}${AGENT_PROFILE}${NC} agent image..." + docker compose --profile "$AGENT_PROFILE" pull "$AGENT_PROFILE" \ + || docker compose --profile "$AGENT_PROFILE" build "$AGENT_PROFILE" \ + || echo -e " ${WARN} Couldn't prepare it now; it will build on first launch." + echo -e " ${CHECK} ${AGENT_PROFILE} agent image ready" + fi echo "" } +# ── Start services (LIVE progress) ─────────────────────────────────── + start_services() { - echo -e "${BOLD}Starting services...${NC}" + echo -e "${BOLD}Step 8/8 · Starting services${NC}" + echo -e " ${DIM}Bringing up the core stack${NC}" + [ -n "$PROFILES" ] && echo -e " ${DIM}Extra profiles:${NC} ${PROFILES//--profile /}" echo "" + + # Streamed (not piped) so the per-container create/start status is visible. + # Guarded so a partial failure still reaches the health dashboard below. # shellcheck disable=SC2086 - docker compose $PROFILES up -d 2>&1 | tail -10 - echo "" - echo -e " ${CHECK} Services started" + if docker compose $PROFILES up -d; then + echo "" + echo -e " ${CHECK} Containers created" + else + echo "" + echo -e " ${WARN} Some containers failed to start — review their status below." + fi echo "" } +# ── Live health dashboard ──────────────────────────────────────────── + +# Render the current service table, colorized by health/state, redrawing +# in place on a TTY. Uses awk (which interprets \033) for color so we don't +# depend on echo -e semantics inside the loop. +render_status() { + local elapsed="$1" max="$2" header table block + header=$(printf ' \033[2mService health · %ss / %ss elapsed (auto-refresh)\033[0m' "$elapsed" "$max") + # shellcheck disable=SC2086 + table=$(docker compose $PROFILES ps --format "table {{.Name}}\t{{.State}}\t{{.Status}}" 2>/dev/null \ + | awk ' + NR==1 { printf " \033[2m%s\033[0m\n", $0; next } + { + if ($0 ~ /\(healthy\)/) code="32"; # green + else if ($0 ~ /starting/) code="33"; # yellow + else if ($0 ~ /unhealthy|Exit|Restart/) code="31"; # red + else code="36"; # cyan + printf " \033[%sm%s\033[0m\n", code, $0 + }') + block="${header}"$'\n'"${table}" + + if [ "$STATUS_LINES" -gt 0 ] && [ "$INTERACTIVE_OUT" = true ]; then + printf '\033[%dA\033[J' "$STATUS_LINES" + fi + printf '%s\n' "$block" + STATUS_LINES=$(printf '%s\n' "$block" | wc -l | tr -d ' ') +} + wait_for_health() { - echo -e "${BOLD}Waiting for services to become healthy...${NC}" - echo -e " ${DIM}Kali MCP takes 30-60s on first startup (Metasploit DB init).${NC}" + echo -e "${BOLD}Waiting for services to become healthy${NC}" + echo -e " ${DIM}Kali MCP takes 30–60s on first start (Metasploit DB init). Live status:${NC}" echo "" - local max_wait=90 - local interval=5 - local elapsed=0 - local all_healthy=false + local max_wait=120 interval=4 elapsed=0 + STATUS_LINES=0 - while [ $elapsed -lt $max_wait ]; do - local healthy - healthy=$(docker compose ps --format json 2>/dev/null | grep -c '"healthy"' || true) - local total - total=$(docker compose ps --format json 2>/dev/null | grep -c '"running\|healthy\|unhealthy"' || true) + while :; do + if [ "$INTERACTIVE_OUT" = true ]; then + render_status "$elapsed" "$max_wait" + fi - printf "\r ${DIM}[%02d/%02ds]${NC} %d/%d services healthy..." "$elapsed" "$max_wait" "$healthy" "$total" + local ps_txt healthy starting + # shellcheck disable=SC2086 + ps_txt=$(docker compose $PROFILES ps 2>/dev/null || true) + healthy=$(printf '%s\n' "$ps_txt" | grep -c '(healthy)' || true) + starting=$(printf '%s\n' "$ps_txt" | grep -c 'starting' || true) - # Check core services - if docker compose ps kali-mcp 2>/dev/null | grep -q "healthy" && \ - docker compose ps screenshot-mcp 2>/dev/null | grep -q "healthy"; then - all_healthy=true + if [ "${starting:-0}" -eq 0 ] && [ "${healthy:-0}" -ge 1 ]; then + break + fi + if [ "$elapsed" -ge "$max_wait" ]; then break fi - sleep "$interval" elapsed=$((elapsed + interval)) done - echo "" + # Final snapshot (also covers the non-TTY case where we skipped redraws). + render_status "$elapsed" "$max_wait" echo "" - if [ "$all_healthy" = true ]; then - echo -e " ${CHECK} ${GREEN}${BOLD}Core services are healthy!${NC}" + local healthy + # shellcheck disable=SC2086 + healthy=$(docker compose $PROFILES ps 2>/dev/null | grep -c '(healthy)' || true) + if [ "${healthy:-0}" -ge 1 ]; then + echo -e " ${CHECK} ${GREEN}${BOLD}${healthy} service(s) healthy.${NC} ${DIM}Run 'make health' anytime for a recheck.${NC}" else - echo -e " ${WARN} Some services may still be starting." - echo -e " ${DIM}Run 'make health' or 'docker compose ps' to check status.${NC}" + echo -e " ${WARN} Services may still be starting. Check with: ${CYAN}make health${NC} / ${CYAN}docker compose ps${NC}" fi echo "" } +# ── Summary ────────────────────────────────────────────────────────── + +print_launch_hint() { + case "$AI_VARIANT" in + claude-code) + echo -e " ${BOLD}Launch your agent:${NC}" + echo -e " ${CYAN}make claude-code${NC} ${DIM}Start Claude Code in Docker${NC}" ;; + deepseek) + echo -e " ${BOLD}Launch your agent:${NC}" + echo -e " ${CYAN}make deepseek${NC} ${DIM}Start the DeepSeek (Reasonix) agent${NC}" ;; + claude-web) + echo -e " ${BOLD}Using Claude Code Web:${NC}" + echo -e " ${DIM}Open this repository at ${NC}${CYAN}https://claude.ai/code${NC}${DIM}.${NC}" + echo -e " ${DIM}The .mcp.json starts the blhackbox MCP server automatically; type /mcp to verify.${NC}" ;; + claude-desktop) + echo -e " ${BOLD}Connect Claude Desktop${NC} ${DIM}(via the MCP Gateway, now enabled):${NC}" + echo -e " ${DIM}Add to your Claude Desktop config, then restart the app:${NC}" + echo -e " ${CYAN}{\"mcpServers\":{\"blhackbox\":{\"transport\":\"streamable-http\",\"url\":\"http://localhost:8080/mcp\"}}}${NC}" + echo -e " ${DIM}Config path — macOS: ~/Library/Application Support/Claude/claude_desktop_config.json${NC}" + echo -e " ${DIM} Windows: %APPDATA%\\Claude\\claude_desktop_config.json${NC}" + echo -e " ${DIM} Linux: ~/.config/Claude/claude_desktop_config.json${NC}" ;; + chatgpt) + echo -e " ${BOLD}Connect ChatGPT/OpenAI${NC} ${DIM}(via the MCP Gateway, now enabled):${NC}" + echo -e " ${DIM}Point your MCP-capable client at${NC} ${CYAN}http://localhost:8080/mcp${NC} ${DIM}(Streamable HTTP).${NC}" ;; + *) + echo -e " ${BOLD}Launch an agent:${NC}" + echo -e " ${CYAN}make claude-code${NC} ${DIM}or${NC} ${CYAN}make deepseek${NC}" ;; + esac +} + print_summary() { echo -e "${DIM}══════════════════════════════════════════════════${NC}" echo -e "${GREEN}${BOLD} Setup complete!${NC}" echo -e "${DIM}══════════════════════════════════════════════════${NC}" echo "" - echo -e " ${BOLD}Quick start:${NC}" - echo -e " ${CYAN}make claude-code${NC} ${DIM}Launch Claude Code in Docker${NC}" - echo -e " ${CYAN}make deepseek${NC} ${DIM}Launch the DeepSeek (Reasonix) agent in Docker${NC}" - echo -e " ${CYAN}make status${NC} ${DIM}Check service status${NC}" - echo -e " ${CYAN}make health${NC} ${DIM}Run health checks${NC}" + print_launch_hint + echo "" + echo -e " ${BOLD}Operate the stack:${NC}" + echo -e " ${CYAN}make status${NC} ${DIM}Container status${NC}" + echo -e " ${CYAN}make health${NC} ${DIM}MCP server health check${NC}" echo -e " ${CYAN}make logs${NC} ${DIM}View service logs${NC}" echo "" - echo -e " ${BOLD}Output files${NC} (accessible on your host):" + echo -e " ${BOLD}Output files${NC} (on your host):" echo -e " ${DIM}./output/reports/ Pentest reports (.md, .pdf)${NC}" echo -e " ${DIM}./output/screenshots/ PoC evidence screenshots${NC}" echo -e " ${DIM}./output/sessions/ Aggregated session data${NC}" echo "" - echo -e " ${BOLD}Portainer UI:${NC}" - echo -e " ${CYAN}https://localhost:9443${NC}" - echo -e " ${WARN} Create admin account within 5 minutes of first start!" - echo "" - if [[ "$PROFILES" == *"neo4j"* ]]; then - echo -e " ${BOLD}Neo4j Browser:${NC} ${CYAN}http://localhost:7474${NC}" - fi - if [[ "$PROFILES" == *"gateway"* ]]; then - echo -e " ${BOLD}MCP Gateway:${NC} ${CYAN}http://localhost:8080${NC}" + echo -e " ${BOLD}Portainer UI:${NC} ${CYAN}https://localhost:9443${NC}" + echo -e " ${WARN} Create the admin account within 5 minutes of first start!" + if [ "$NEO4J_MODE" = "local" ]; then + echo "" + echo -e " ${BOLD}Neo4j Browser:${NC} ${CYAN}http://localhost:7474${NC} ${DIM}(user: neo4j)${NC}" + elif [ "$NEO4J_MODE" = "aura" ]; then + echo "" + echo -e " ${BOLD}Neo4j Aura:${NC} manage your cloud instance at ${CYAN}https://console.neo4j.io${NC}" + echo -e " ${DIM}Connection details are in your .env (no local container started).${NC}" fi - echo -e " ${BOLD}For pentesting:${NC}" - echo -e " Use a pentest template or skill: ${DIM}full-pentest, quick-scan, etc.${NC}" echo "" - echo -e " ${BOLD}Optional API keys${NC} ${DIM}(none required — all tools run without them):${NC}" - echo -e " Add keys to ${CYAN}.env${NC} to widen recon coverage, then restart the stack." - echo -e " ${DIM}WPSCAN_API_TOKEN (WordPress vulns), VIRUSTOTAL_API_KEY / SHODAN_API_KEY /${NC}" - echo -e " ${DIM}SECURITYTRAILS_API_KEY (subfinder sources). See README → Optional API keys.${NC}" + echo -e " ${BOLD}For pentesting:${NC} use a skill — ${DIM}/full-pentest, /quick-scan, /bug-bounty, …${NC}" + echo "" + echo -e " ${DIM}Add or change keys anytime in ${NC}${CYAN}.env${NC}${DIM}, then: ${NC}${CYAN}docker compose up -d${NC}" echo "" } @@ -387,14 +716,15 @@ while [[ $# -gt 0 ]]; do ;; --minimal) MINIMAL=true + INTERACTIVE=false shift ;; --with-neo4j) - PROFILES="${PROFILES:+$PROFILES }--profile neo4j" + add_profile neo4j shift ;; --with-gateway) - PROFILES="${PROFILES:+$PROFILES }--profile gateway" + add_profile gateway shift ;; --skip-pull) @@ -411,14 +741,22 @@ while [[ $# -gt 0 ]]; do esac done +# --minimal means: core stack only, no optional profiles, no prompts. +if [ "$MINIMAL" = true ]; then + PROFILES="" + INTERACTIVE=false +fi + # ── Main ───────────────────────────────────────────────────────────── cd "$SCRIPT_DIR" print_banner check_prerequisites -select_profiles -configure_env +prepare_env +choose_ai_client +choose_neo4j +choose_optional_keys create_output_dirs pull_images start_services From cf5cb3acf5201a87947f2fafb0b01bba536a7654 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:35:34 +0000 Subject: [PATCH 2/2] Polish setup wizard: pre-flight review, safer keys, modern terminal UX Layered, opt-out-safe UX improvements on top of the guided wizard. No change to existing logic or contracts; all additions degrade gracefully. - Pre-flight confirmation: before pulling images or starting containers, show a review of every selection (AI client, knowledge graph, gateway) and the exact actions about to run, then ask "Proceed?". Declining exits cleanly with .env saved. Skipped in non-interactive runs. - Missing-key warning: if the chosen containerized client's key was skipped, the summary now says so prominently with the one-line fix, instead of pointing at a launch command that would fail to start. - Secret-file hardening: chmod 600 on .env (it holds live secrets). - Non-blocking input validation: warn on unexpected API-key prefixes (offer to save anyway) and loop until a Neo4j password is >=8 chars (Neo4j 5 rejects shorter ones and won't start). - Modern terminal output: honor NO_COLOR and auto-disable color when stdout isn't a TTY (clean piped/CI logs); render URLs as OSC 8 hyperlinks with a plain-text fallback for terminals without support. https://claude.ai/code/session_0111sKEqbgpbAZKAYDuQmcHa --- setup.sh | 173 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 151 insertions(+), 22 deletions(-) diff --git a/setup.sh b/setup.sh index fd83875..3de6d3a 100755 --- a/setup.sh +++ b/setup.sh @@ -16,15 +16,31 @@ # ./setup.sh --help # Show usage set -euo pipefail +# ── Interactivity & color detection ────────────────────────────────── +# Prompt only when stdin is a terminal; emit cursor-redraw codes only when +# stdout is a terminal; colorize only when stdout is a TTY AND the NO_COLOR +# convention (https://no-color.org — any non-empty value disables color) is +# not set. This keeps piped/CI logs clean (no raw escape codes). +INTERACTIVE=true; [ -t 0 ] || INTERACTIVE=false +INTERACTIVE_OUT=true; [ -t 1 ] || INTERACTIVE_OUT=false +USE_COLOR=true +if [ "$INTERACTIVE_OUT" != true ] || [ -n "${NO_COLOR:-}" ]; then + USE_COLOR=false +fi + # ── Colors & Symbols ───────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' +if [ "$USE_COLOR" = true ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + CYAN='\033[0;36m' + BOLD='\033[1m' + DIM='\033[2m' + NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; DIM=''; NC='' +fi CHECK="${GREEN}✔${NC}" CROSS="${RED}✘${NC}" WARN="${YELLOW}!${NC}" @@ -46,12 +62,21 @@ NEO4J_MODE="none" # none | local | aura WRITE_ENV=true # whether we may (over)write .env STATUS_LINES=0 # bookkeeping for the live status redraw -# Interactivity detection: only prompt when stdin is a real terminal. -INTERACTIVE=true; [ -t 0 ] || INTERACTIVE=false -INTERACTIVE_OUT=true; [ -t 1 ] || INTERACTIVE_OUT=false - # ── Generic helpers ────────────────────────────────────────────────── +# Render a clickable terminal hyperlink (OSC 8) when color/terminal support +# is on; otherwise print the bare text. Uses the BEL terminator (widely +# supported); terminals without OSC 8 simply show the text, so it's safe. +# $1 = URL, $2 = display text (defaults to the URL). +link() { + local url="$1" text="${2:-$1}" + if [ "$USE_COLOR" = true ]; then + printf '\033]8;;%s\007%s\033]8;;\007' "$url" "$text" + else + printf '%s' "$text" + fi +} + # Append a compose --profile flag once (no duplicates). add_profile() { case " $PROFILES " in @@ -100,6 +125,28 @@ set_env() { printf '%s=%s\n' "$key" "$val" >> "$file" } +# Return 0 if KEY is set to a non-empty value in .env. +env_has_value() { + grep -qE "^$1=.+" "$SCRIPT_DIR/.env" 2>/dev/null +} + +# Soft, non-blocking format check for a freshly entered API key. Returns 0 +# to proceed with saving, 1 to treat as skipped. Never prints the value. +key_format_ok() { + local var="$1" val="$2" + case "$var" in + ANTHROPIC_API_KEY) + [[ "$val" == sk-ant-* ]] && return 0 + echo -e " ${WARN} That doesn't look like an Anthropic key (expected prefix 'sk-ant-')." ;; + DEEPSEEK_API_KEY|OPENAI_API_KEY) + [[ "$val" == sk-* ]] && return 0 + echo -e " ${WARN} That doesn't look like a typical API key (expected prefix 'sk-')." ;; + *) return 0 ;; + esac + # Prefix didn't match — let the user decide (non-blocking). + ask_yes_no "Save it anyway?" "y" +} + # ── Banner & usage ─────────────────────────────────────────────────── print_banner() { @@ -228,6 +275,11 @@ prepare_env() { cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env" echo -e " ${CHECK} Created .env from the template" fi + + # Secrets live in .env — restrict it to the owner only. + if chmod 600 "$SCRIPT_DIR/.env" 2>/dev/null; then + echo -e " ${CHECK} Locked .env to owner-only ${DIM}(chmod 600)${NC}" + fi echo "" } @@ -278,7 +330,7 @@ configure_agent_key() { if ask_yes_no "Add ${var} now?" "y"; then local val val="$(prompt_secret)" - if [ -n "$val" ]; then + if [ -n "$val" ] && key_format_ok "$var" "$val"; then set_env "$var" "$val" echo -e " ${CHECK} ${var} saved to .env" else @@ -396,6 +448,12 @@ choose_neo4j() { echo -e " ${BOLD}Set a Neo4j password${NC} ${DIM}(min 8 chars; press Enter for '${def}').${NC}" p="$(prompt_secret " Password (hidden): ")" [ -n "$p" ] || p="$def" + # Neo4j 5 rejects passwords shorter than 8 chars and won't start. + while [ "${#p}" -lt 8 ]; do + echo -e " ${WARN} Too short — Neo4j needs at least 8 characters." + p="$(prompt_secret " Password (hidden, ≥8 chars): ")" + [ -n "$p" ] || p="$def" + done set_env "NEO4J_URI" "bolt://neo4j:7687" set_env "NEO4J_USER" "neo4j" set_env "NEO4J_PASSWORD" "$p" @@ -573,12 +631,17 @@ start_services() { # depend on echo -e semantics inside the loop. render_status() { local elapsed="$1" max="$2" header table block - header=$(printf ' \033[2mService health · %ss / %ss elapsed (auto-refresh)\033[0m' "$elapsed" "$max") + if [ "$USE_COLOR" = true ]; then + header=$(printf ' \033[2mService health · %ss / %ss elapsed (auto-refresh)\033[0m' "$elapsed" "$max") + else + header=$(printf ' Service health · %ss / %ss elapsed (auto-refresh)' "$elapsed" "$max") + fi # shellcheck disable=SC2086 table=$(docker compose $PROFILES ps --format "table {{.Name}}\t{{.State}}\t{{.Status}}" 2>/dev/null \ - | awk ' - NR==1 { printf " \033[2m%s\033[0m\n", $0; next } + | awk -v color="$USE_COLOR" ' + NR==1 { if (color=="true") printf " \033[2m%s\033[0m\n", $0; else printf " %s\n", $0; next } { + if (color!="true") { printf " %s\n", $0; next } if ($0 ~ /\(healthy\)/) code="32"; # green else if ($0 ~ /starting/) code="33"; # yellow else if ($0 ~ /unhealthy|Exit|Restart/) code="31"; # red @@ -644,13 +707,21 @@ print_launch_hint() { case "$AI_VARIANT" in claude-code) echo -e " ${BOLD}Launch your agent:${NC}" - echo -e " ${CYAN}make claude-code${NC} ${DIM}Start Claude Code in Docker${NC}" ;; + echo -e " ${CYAN}make claude-code${NC} ${DIM}Start Claude Code in Docker${NC}" + if ! env_has_value ANTHROPIC_API_KEY; then + echo -e " ${WARN} ${YELLOW}ANTHROPIC_API_KEY is not set${NC} — the claude-code container won't" + echo -e " start until you add it to ${CYAN}.env${NC} (then re-run ${CYAN}make claude-code${NC})." + fi ;; deepseek) echo -e " ${BOLD}Launch your agent:${NC}" - echo -e " ${CYAN}make deepseek${NC} ${DIM}Start the DeepSeek (Reasonix) agent${NC}" ;; + echo -e " ${CYAN}make deepseek${NC} ${DIM}Start the DeepSeek (Reasonix) agent${NC}" + if ! env_has_value DEEPSEEK_API_KEY; then + echo -e " ${WARN} ${YELLOW}DEEPSEEK_API_KEY is not set${NC} — the deepseek container won't" + echo -e " start until you add it to ${CYAN}.env${NC} (then re-run ${CYAN}make deepseek${NC})." + fi ;; claude-web) echo -e " ${BOLD}Using Claude Code Web:${NC}" - echo -e " ${DIM}Open this repository at ${NC}${CYAN}https://claude.ai/code${NC}${DIM}.${NC}" + echo -e " ${DIM}Open this repository at ${NC}$(link https://claude.ai/code "${CYAN}https://claude.ai/code${NC}")${DIM}.${NC}" echo -e " ${DIM}The .mcp.json starts the blhackbox MCP server automatically; type /mcp to verify.${NC}" ;; claude-desktop) echo -e " ${BOLD}Connect Claude Desktop${NC} ${DIM}(via the MCP Gateway, now enabled):${NC}" @@ -685,14 +756,14 @@ print_summary() { echo -e " ${DIM}./output/screenshots/ PoC evidence screenshots${NC}" echo -e " ${DIM}./output/sessions/ Aggregated session data${NC}" echo "" - echo -e " ${BOLD}Portainer UI:${NC} ${CYAN}https://localhost:9443${NC}" + echo -e " ${BOLD}Portainer UI:${NC} $(link https://localhost:9443 "${CYAN}https://localhost:9443${NC}")" echo -e " ${WARN} Create the admin account within 5 minutes of first start!" if [ "$NEO4J_MODE" = "local" ]; then echo "" - echo -e " ${BOLD}Neo4j Browser:${NC} ${CYAN}http://localhost:7474${NC} ${DIM}(user: neo4j)${NC}" + echo -e " ${BOLD}Neo4j Browser:${NC} $(link http://localhost:7474 "${CYAN}http://localhost:7474${NC}") ${DIM}(user: neo4j)${NC}" elif [ "$NEO4J_MODE" = "aura" ]; then echo "" - echo -e " ${BOLD}Neo4j Aura:${NC} manage your cloud instance at ${CYAN}https://console.neo4j.io${NC}" + echo -e " ${BOLD}Neo4j Aura:${NC} manage your cloud instance at $(link https://console.neo4j.io "${CYAN}https://console.neo4j.io${NC}")" echo -e " ${DIM}Connection details are in your .env (no local container started).${NC}" fi echo "" @@ -702,6 +773,63 @@ print_summary() { echo "" } +# ── Pre-flight confirmation ────────────────────────────────────────── + +# Human-readable label for the selected AI client. +ai_label() { + case "$AI_VARIANT" in + claude-code) echo "Claude Code (Docker container)" ;; + claude-web) echo "Claude Code (Web — claude.ai/code)" ;; + claude-desktop) echo "Claude Desktop (host app via MCP Gateway)" ;; + chatgpt) echo "ChatGPT / OpenAI (host app via MCP Gateway)" ;; + deepseek) echo "DeepSeek / Reasonix (Docker container)" ;; + *) echo "None selected (core stack only)" ;; + esac +} + +# Show a review of all selections and ask before any images are pulled or +# containers started. Skipped entirely in non-interactive runs. +confirm_plan() { + [ "$INTERACTIVE" = true ] || return 0 + + echo -e "${BOLD}Review your setup${NC}" + echo -e "${DIM} Nothing has been pulled or started yet. Here's what will happen:${NC}" + echo "" + echo -e " ${BOLD}AI client:${NC} $(ai_label)" + case "$NEO4J_MODE" in + local) echo -e " ${BOLD}Knowledge graph:${NC} Local Neo4j (Docker container)" ;; + aura) echo -e " ${BOLD}Knowledge graph:${NC} Neo4j Aura (cloud — no local container)" ;; + *) echo -e " ${BOLD}Knowledge graph:${NC} None" ;; + esac + if [[ "$PROFILES" == *"--profile gateway"* ]]; then + echo -e " ${BOLD}MCP Gateway:${NC} Enabled (port 8080)" + else + echo -e " ${BOLD}MCP Gateway:${NC} Disabled" + fi + echo "" + echo -e " ${BOLD}Actions:${NC}" + if [ "$SKIP_PULL" = true ]; then + echo -e " ${ARROW} Skip image pull (use cached images)" + else + echo -e " ${ARROW} Pull core Docker images ${DIM}(live progress)${NC}" + fi + [ -n "$AGENT_PROFILE" ] && echo -e " ${ARROW} Prepare the ${CYAN}${AGENT_PROFILE}${NC} agent image" + if [ -n "$PROFILES" ]; then + echo -e " ${ARROW} Start the core stack + profiles:${PROFILES//--profile /}" + else + echo -e " ${ARROW} Start the core stack" + fi + echo -e " ${ARROW} Wait for services to become healthy" + echo "" + if ! ask_yes_no "Proceed?" "y"; then + echo "" + echo -e " ${ARROW} Stopped before any images were pulled or containers started." + echo -e " ${DIM}Your .env is saved. Re-run ${NC}${CYAN}./setup.sh${NC}${DIM} anytime.${NC}" + exit 0 + fi + echo "" +} + # ── Parse Arguments ────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do @@ -758,6 +886,7 @@ choose_ai_client choose_neo4j choose_optional_keys create_output_dirs +confirm_plan pull_images start_services wait_for_health