← Back to Blog

AI Terminal Kickstart — Set Up Claude Code, Codex, ChatGPT CLI & GitHub Copilot On A Fresh Box

· Updated August 18, 2026 · 216 min read AI Terminal Kickstart — Set Up Claude Code, Codex, ChatGPT CLI & GitHub Copilot On A Fresh Box

Getting a fresh machine set up for AI coding, Claude Code, OpenAI Codex, ChatGPT in the terminal, GitHub Copilot CLI, plus Netlify CLI for deploys, is usually a scavenger hunt across five docs pages, three package managers, and one inevitable PATH issue. This is the condensed walk-through. Every command is copy-pasteable, paired for macOS / Linux (Bash) and Windows (PowerShell) so whichever box you're on, you're one paste away from a working environment. There are now three one-shot scripts — a dedicated macOS edition (Homebrew, zsh, Apple Silicon + Intel), the Linux edition (apt/dnf), and the Windows edition (PowerShell).

The order matters: runtimes first, then CLIs that depend on them, then auth. If you skip a step you'll hit it later.

🧭 Already got the CLI working? The follow-on picks up where this post leaves off: You've Got The Claude Code CLI Installed — Now What? covers plan mode, CLAUDE.md as a living contract, the Q1 2026 slash commands (/btw /fork /rewind /compact /loop /schedule /simplify /batch /powerup /insights /debug /claude-api), Skills vs Rules vs Memory, Auto Mode boundaries, and a Day 0 → Day 7 ramp plan. Bookmark it for when you're through this install.


📚 The full workflow series after this install primer (this post is #1):

Claude Code track:

  1. This post — install the CLIs.
  2. CLI installed — now what? — plan mode, CLAUDE.md, the slash commands that matter, Day 0 → Day 7 ramp.
  3. Skills, Rules, Memory deep-dive — the four-layer hierarchy that keeps CLAUDE.md from bloating.
  4. Adversarial subagents + worktrees — parallel Claudes with deliberately different review roles.
  5. Hooks implementation guide — stop approving the same prompt 40 times a day.
  6. /loop vs /schedule — in-session polling vs cron-style automation.
  7. CLAUDE.md anti-patterns — the 8 patterns that rot the file over time.
  8. Auto Mode case studies — four real classifier calls, wins and losses.
  9. /insights-driven iteration — the 30-minute Monday ritual.
  10. Remote Control + Channels — running your session from iMessage, Telegram, Discord.
  11. Multi-model routing — when to break out of Claude-only.
  12. Claude Fable 5 in Claude Code — switching models by ID, and why the new top tier was pulled by a US export-control order days after launch.
  13. Agent skill marketplace — Skills 2.0 ecosystem, evaluating before installing, publishing your own.

Codex track (for readers using OpenAI's CLI or running both):

  1. Codex CLI after install — OpenAI-specific first-week habits, model selection, cost mechanics.
  2. Codex vs Claude Code — task-level comparison — where each one actually wins.
  3. Prompt style — GPT vs Claude — the four stylistic differences that matter in practice.
  4. ChatGPT CLI / Codex CLI / GitHub Copilot CLI — positioning — three products that sound alike and do different jobs.
  5. Two-CLI workflow — split-screen routine for running Codex + Claude Code in parallel.

Open-weights + Google-stack track:

  1. Qwen self-hosted at home — when open-weights beats proprietary, hardware tiers, Ollama setup, quantization trade-offs.
  2. Gemini CLI — when to use — long context, multimodal, Google-ecosystem fit, and where Gemini falls short against Claude Code.
  3. Gemma — where it fits next to Qwen — Google's open-weights counterpart; when to pick it over Qwen and vice versa.

What you're installing

Layer Tools
Runtimes Node.js LTS, Python 3.12, Git
Package managers Homebrew (macOS), Chocolatey / winget (Windows), apt / dnf (Linux), npm, pip / pipx
AI terminals Claude Code (Anthropic), OpenAI Codex CLI, ChatGPT CLI via shell-gpt (sgpt), GitHub Copilot CLI
AI frameworks aider (pair programmer), llm (pluggable LLM CLI), repomix (repo → one prompt), Gemini CLI, aichat, files-to-prompt
Conversions pandoc (docs), poppler / pdftotext, tesseract (OCR), markitdown (Office/PDF → Markdown for LLMs), ffmpeg, ImageMagick, yt-dlp, jc
Document forensics exiftool (read/strip metadata from any file), qpdf (PDF structural rewrite, linearize, split, merge), PyMuPDF + pdfplumber (read PDFs from Python)
Headless browser (opt-in) puppeteer + puppeteer-extra + puppeteer-extra-plugin-stealth, playwright, pdf-lib, sharp, lighthouse
Deploy Netlify CLI, Cloudflare Wrangler (Pages + Workers), optional Vercel CLI
Utilities ripgrep, jq, fd, bat, fzf, yq, tree, git-delta, shellcheck, tmux, sqlite3, httpie, ast-grep, gron, tldr, just
Optional model (opt-in) Kimi Code (Moonshot AI) — add with --with-kimi / -WithKimi; leave it off and the stack stays US-only

The AI terminals differ in what they're good at. Claude Code edits files and runs commands end-to-end inside a repo, best for real work. Codex / ChatGPT CLI is better for quick questions and one-shot generations. Copilot CLI suggests shell commands from natural language. The AI frameworks layer adds agentic helpers (aider, llm, aichat) and repo/document packers (repomix, files-to-prompt, markitdown), while the conversions layer turns PDFs, scans, Office files, audio and video into text an AI model can actually read.

One-shot install: full scripts you can save and run

If you'd rather run an automated installer than paste each of the Step 1-7 blocks individually, the three scripts below do every install + auth wiring in one go. Save the matching file for your OS, run it once, and you end up with Claude Code, OpenAI Codex, ChatGPT CLI, GitHub Copilot CLI, Netlify CLI, the AI framework + conversion bundle (aider, llm, repomix, pandoc, tesseract, markitdown …), and every dependency on your PATH.

All three are downloadable:

Why a separate macOS script? The Linux edition installs through apt/dnf and runs with sudo. macOS has neither — it uses Homebrew, which refuses to run as root, defaults to zsh (not bash), and ships an ancient bash 3.2. The macOS edition is written for those realities: no sudo, Homebrew-native, zsh-profile aware, and Apple-Silicon/Intel aware.

Save and run — macOS (Bash + Homebrew)

  1. Copy the macOS script (expand the "Full macOS script" block below, click inside the code block, then Ctrl+ACmd+C), or download it from the link above.

  2. Save it as start-ai-terminal-kickstart-macos.sh in your home folder:

    nano ~/start-ai-terminal-kickstart-macos.sh
    # paste, then Ctrl+O  Enter  Ctrl+X
    
  3. Make it executable and run it — without sudo (Homebrew will abort if you use root):

    chmod +x ~/start-ai-terminal-kickstart-macos.sh
    ~/start-ai-terminal-kickstart-macos.sh
    # unattended (yes to everything):
    ~/start-ai-terminal-kickstart-macos.sh --auto
    # also install a local model runner / LibreOffice:
    ~/start-ai-terminal-kickstart-macos.sh --with-ollama --with-office
    # opt in to the non-US Kimi model (off by default; the stack is US-only otherwise):
    ~/start-ai-terminal-kickstart-macos.sh --with-kimi
    

    First run will install Apple's Command Line Tools and Homebrew if they're missing (Homebrew may prompt once for your password — that's expected).

  4. Open a fresh terminal so the zsh profile (~/.zprofile) reloads. Then claude, codex, sgpt, copilot, aider, llm, and netlify are all on your PATH.

Save and run — Linux / WSL (Bash)

  1. Copy the Bash script (expand the "Full Linux script" block below and click inside the code block, then Ctrl+ACtrl+C).

  2. Save it as start-ai-terminal-kickstart.sh in your home directory:

    nano ~/start-ai-terminal-kickstart.sh
    # paste, then Ctrl+O  Enter  Ctrl+X
    

    Or, if you downloaded the file from the link above, move it to ~/ and skip to step 3.

  3. Make it executable and run it:

    chmod +x ~/start-ai-terminal-kickstart.sh
    sudo ~/start-ai-terminal-kickstart.sh
    # or, unattended (install everything, no prompts):
    sudo ~/start-ai-terminal-kickstart.sh --auto
    # opt in to the non-US Kimi model (off by default; US-only otherwise):
    sudo ~/start-ai-terminal-kickstart.sh --with-kimi
    
  4. Close the terminal and open a fresh one so PATH updates take effect. Then claude, codex, sgpt, copilot, and netlify are all on your PATH.

Save and run — Windows (PowerShell)

  1. Copy the PowerShell script (expand the "Full PowerShell script" block below, click inside the code block, Ctrl+ACtrl+C).

  2. Save as Start-AITerminalKickstart.ps1 in your Downloads folder (or wherever you prefer).

    Open Notepad, paste, File → Save As…, set "Save as type" to All Files, name the file Start-AITerminalKickstart.ps1, and save with encoding UTF-8. Or if you downloaded the file from the link above, skip to step 3.

  3. Open PowerShell as Administrator (Start menu → type PowerShell → right-click → Run as administrator), then:

    # Allow the script to run for this session only
    Set-ExecutionPolicy Bypass -Scope Process -Force
    
    # cd to where you saved it
    cd $env:USERPROFILE\Downloads
    
    # Run it (interactive menu)
    .\Start-AITerminalKickstart.ps1
    
    # Or unattended — install everything, no prompts:
    .\Start-AITerminalKickstart.ps1 -AutoMode
    # Opt in to the non-US Kimi model (off by default; US-only otherwise):
    .\Start-AITerminalKickstart.ps1 -WithKimi
    
  4. Close the PowerShell window and open a fresh one. claude, codex, sgpt, copilot, and netlify should all resolve.

If PowerShell refuses the script with "cannot be loaded because running scripts is disabled", you skipped step 3's Set-ExecutionPolicy line. The -Scope Process flag is the safe version, it only lifts the restriction for the current terminal, not system-wide.

Two known install errors on Server 2022 / Win 11 (and the one-liner fixes)

The script auto-detects and works around both of these — they only matter if you're running the install commands by hand outside the script. They are also documented in the script's .NOTES header for offline reference.

1. Chocolatey: "Cannot overwrite variable result because the variable has been optimized."

Chocolatey's install.ps1 was authored for Windows PowerShell 5.1 and trips a known optimization bug in PowerShell 7+. Manual workaround — run from an elevated Windows PowerShell 5.1 (powershell.exe), not pwsh 7:

Set-ExecutionPolicy Bypass -Scope Process -Force
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

If you are already in pwsh 7, shell out to PS 5.1 in one line:

& "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"

2. Python: "Python not found after install"

The installer succeeded, but the new install path isn't in the current shell's $env:Path. Common locations: C:\Python312 (python.org InstallAllUsers=1), C:\Program Files\Python312 (winget system scope), $env:LOCALAPPDATA\Programs\Python\Python312 (winget per-user, default). To refresh PATH in the current session:

$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')

Or skip winget and Chocolatey entirely and install Python 3.12 directly from python.org:

$v = '3.12.13'
$u = "https://www.python.org/ftp/python/$v/python-$v-amd64.exe"
$f = "$env:TEMP\python-$v-amd64.exe"
Invoke-WebRequest $u -OutFile $f -UseBasicParsing
Start-Process $f -ArgumentList '/quiet','InstallAllUsers=1','PrependPath=1' -Wait

What the scripts set up — and why

These aren't just "install the CLI" scripts anymore. Each one stands up a full AI coding workbench: the terminals, the agentic frameworks, a document/media conversion toolkit, power utilities, and the MCP plumbing that lets Claude Code reach your files and the web. Here's every layer and the reasoning behind it.

Three editions, not one

There's a dedicated script per OS because the realities are genuinely different:

  • macOS uses Homebrew, which refuses to run as root — so the macOS script never uses sudo. macOS also defaults to zsh (not bash) and ships an ancient bash 3.2, so that script is written to be zsh-profile aware and 3.2-safe, and it detects Apple Silicon vs Intel (/opt/homebrew vs /usr/local).
  • Linux installs through apt/dnf and does run with sudo.
  • Windows uses winget/Chocolatey under PowerShell.

Trying to force one file to do all three meant either breaking macOS (its bash can't run the Linux script's associative arrays) or weakening the Linux path. Separate scripts keep each one correct.

The four AI terminals

Tool Best at Auth
Claude Code deep, multi-file, agentic coding inside a repo browser login or ANTHROPIC_API_KEY
OpenAI Codex CLI OpenAI-model agentic coding in the terminal ChatGPT login or OPENAI_API_KEY
ChatGPT CLI (shell-gpt / sgpt) quick questions, one-shot generations OPENAI_API_KEY
GitHub Copilot CLI shell-command suggestions, git help gh auth login

Codex is now installed by the Bash editions too (it was already covered on Windows), so all four are one selection away.

Which of these is actually fastest, what each one costs per million tokens, and where your code physically goes when you hit enter is its own deep dive. The full speed, cost, and data-jurisdiction breakdown now spans eight stacks, adding Grok, Kimi, Qwen, and DeepSeek to the four above: CLI or Browser? Speed, Cost, and Who Holds Your Code.

Optional: Kimi (Moonshot AI) — opt-in, off by default

The four terminals above run US-hosted models. There's a fifth worth knowing about when cost is the binding constraint: Kimi, from Moonshot AI. It is deliberately not installed by default. Pass --with-kimi (PowerShell: -WithKimi) and the script adds the Kimi Code CLI and prints the wiring notes below; leave the flag off and nothing about your stack changes — it stays US-only.

Why you'd reach for it. The current flagship (kimi-k2.7-code) is a trillion-parameter mixture-of-experts model (≈32B active per token) with a 256K context window, tuned for long-horizon agentic tool-use — the exact profile for a terminal agent grinding through a 40-item audit fix list without losing the thread. The original Kimi K2 scored 65.8 on SWE-bench Verified, in the same class as frontier US models at the time. The real draw is price: roughly $0.95 per million input tokens and $4.00 per million output as of this update, an order of magnitude cheaper on output than frontier models, with steep cache-hit discounts that compound in agentic loops resending the same context.

How to use it — two ways, and you don't have to pick just one:

  1. As a drop-in inside Claude Code. Keep all your Claude Code muscle memory and point it at Kimi through Moonshot's Anthropic-compatible endpoint:

    export ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic
    export ANTHROPIC_AUTH_TOKEN=your_moonshot_key   # from platform.kimi.ai
    export ANTHROPIC_MODEL=kimi-k2.7-code
    

    Leave those unset and Claude Code stays on Anthropic models. The tidy middle path: set only CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.7-code so your subagents fan out cheap on Kimi while the main thread keeps Claude's judgment. (The multi-model routing post has the full when-to-switch logic.)

  2. As its own CLI. --with-kimi installs kimi (Kimi Code). Grab a key at platform.kimi.ai — it needs a $1 activation top-up, and the free starter tier is only 3 requests/min, so budget the $10 tier for real agentic work — then run kimi and /login.

Why it's opt-in. Moonshot is a Chinese company, and both API endpoints (api.moonshot.ai and api.moonshot.cn) are China-operated with no published Western data-residency or zero-retention guarantee. For an agency touching client sites that's a real line to draw: route bulk, non-sensitive grunt work to Kimi for the price, but keep confidential client material on a US model or on self-hosted open weights. Want US-only? Just don't pass the flag. The lineup also moves fast (K2 → K2.6 → K2.7 Code inside a year), so check platform.kimi.ai for the current flagship model id before you pin one.

AI frameworks — the agentic + code-gen layer

  • aider — an AI pair programmer that edits code directly in your git repo and commits as it goes.
  • llm (Simon Willison) — a pluggable one-shot/-c LLM CLI with a large plugin ecosystem; great for scripting prompts into pipelines.
  • repomix — packs an entire repository into a single, token-efficient file you can hand to any model for whole-repo context.
  • files-to-prompt — turns a folder of files into one clean prompt.
  • aichat — an all-in-one Rust LLM CLI (chat, REPL, RAG, shell assistant) that talks to many providers.

The conversion toolkit — feed anything to a model

Models read text. Most of what you want to ask about isn't text yet — it's a PDF, a scan, a slide deck, a video. This layer fixes that:

  • markitdown (Microsoft) — converts Office files, PDFs, images and more into clean Markdown for LLM ingestion.
  • poppler (pdftotext) — pulls text straight out of PDFs.
  • tesseract — OCR; turns images and scans into text.
  • pandoc — converts between just about any document formats (Markdown ↔ docx ↔ HTML ↔ PDF…).
  • yt-dlp + ffmpeg — pull and transcode audio/video (pair with a transcription model).
  • jc — converts the output of common CLI tools into JSON, so an AI (or jq) can parse it.

Document forensics — know what a file is really carrying

Conversion gets text out of a document. This layer tells you what else is inside one, and lets you change it. It matters the moment you send a file to somebody else.

  • exiftool — reads and strips metadata from PDFs, images and video. Worth running once on anything you are about to publish. A PDF printed from a headless browser, for example, records the full browser user agent in /Creator and the rendering engine in /Producer, so "generated by automation" travels with the file into the reader's document properties.
  • qpdf — rewrites a PDF's object graph: linearize, split, merge, decrypt, repair. It is also the honest half of a metadata strip, because exiftool edits a PDF incrementally, appending changes while the original bytes stay recoverable further up the file. A structural rewrite is what actually removes them.
  • PyMuPDF and pdfplumber — read PDFs from Python. PyMuPDF for text, page counts, bookmarks and metadata; pdfplumber when you need tables rather than a text dump.

One habit worth forming: check the magic bytes and the page count before you believe an extraction. A tag-stripping parser will happily open an HTML error page as a one-page "PDF" and print the block message as document text, which reads exactly like a successful fetch of a bad document.

Headless browser toolkit — the opt-in layer

Every script offers this as a separate prompt, because puppeteer downloads a private Chromium build of roughly 150 MB and playwright fetches three engines on first use. That is not something to install silently on a metered link or a small VM.

  • puppeteer with puppeteer-extra and the stealth plugin (puppeteer-extra-plugin-stealth) — drives real Chromium. This is what gets you a page that returns 403 to plain curl, and it is also how you render HTML to PDF with real fonts and CSS.
  • playwright — a second engine for when Chromium is the thing being blocked. Its browsers are a separate download: npx playwright install, plus npx playwright install-deps on headless Linux.
  • pdf-lib — merge and split PDFs and write bookmarks, from Node, with no native dependency.
  • sharp — fast image resize and format conversion.
  • lighthouse — performance, SEO and accessibility audits from the command line.

A trap that costs an hour the first time: Node resolves a global package from the requiring script's own directory, not from your working directory. A standalone script sitting outside any project will not find a globally installed puppeteer-extra no matter how correctly you installed it. Point createRequire at the output of npm root -g, or keep a tiny package.json beside the script.

Power utilities

  • httpie (the command is http) — a human-friendly HTTP client for poking at APIs.
  • ast-grep — structural, AST-aware code search and rewrite; far more precise than regex for AI-assisted refactors.
  • gron — flattens JSON into greppable lines (and back with gron -u).
  • tldr — example-first, community man pages.
  • just — a make-style task runner for saving and re-running project commands.

…on top of the originals: ripgrep, jq, fd, bat, fzf, yq, tree, git-delta, shellcheck, tmux, sqlite3, ImageMagick, GhostScript, and (macOS) the GNU userland.

Deploy targets — Netlify, Cloudflare, Vercel

Each script installs Netlify CLI and Cloudflare Wrangler (with Vercel CLI optional), so you can push a site live from the terminal on whichever host you prefer — including the same edge platform this site's contingency plan already favors:

  • Netlify: netlify deploy --prod
  • Cloudflare Pages (static sites): wrangler pages deploy ./dist --project-name=my-site — and wrangler deploy for Workers (edge functions)
  • Vercel: vercel --prod

Auth is a one-time browser login (netlify login / wrangler login / vercel login); for CI/headless, Cloudflare uses CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID. Wrangler is npm install -g wrangler, so it stays current the same way every other tool here does — no pinned version.

Auto-deploy Cloudflare Pages on every git push

wrangler pages deploy is the manual one-shot. To make it automatic — every push to main builds and ships to Cloudflare Pages — drop this GitHub Actions workflow in your repo at .github/workflows/deploy.yml. It uses Cloudflare's official wrangler-action:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      # Build your site — swap these for your own build command + output dir
      - run: npm ci && npm run build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy ./dist --project-name=my-site

Two one-time setup steps:

  1. Create the Pages project oncewrangler pages project create my-site (or just run wrangler pages deploy ./dist --project-name=my-site locally once; the first deploy creates it). The --project-name in the workflow must match.
  2. Add two repo secrets — in GitHub: Settings → Secrets and variables → Actions. Create CLOUDFLARE_API_TOKEN (Cloudflare dashboard → My Profile → API Tokens, use the "Edit Cloudflare Workers" / Pages template) and CLOUDFLARE_ACCOUNT_ID (shown on the Workers & Pages overview). GitHub encrypts them and wrangler-action keeps them out of the logs.

That's it — push to main and the Action builds and deploys. Pull requests get their own preview deployments automatically.

No workflow file at all? Cloudflare Pages can also auto-build straight from Git: in the dashboard, Workers & Pages → Create → Pages → Connect to Git, pick the repo, set the build command and output directory, and every push deploys with zero YAML. The Action route above is the better fit when you want the build to run in your CI (custom steps, tests, monorepo control) before the deploy.

"Install everything" mode

Each script now opens with an install-mode choice: pick Everything to install the full recommended stack unattended, or Choose to be asked per component. You can skip the prompt entirely with --all (macOS/Linux) / -All (Windows) — the same as --auto/-AutoMode. API keys, Ollama, and LibreOffice stay opt-in even in "everything" mode, so nothing heavyweight or secret-bearing happens without you asking for it.

MCP servers + aligned repos

Model Context Protocol servers are how Claude Code reaches beyond the chat — your filesystem, the web, a browser, persistent memory. The scripts offer to register a current, maintained set with claude mcp add --scope user:

  • filesystem, memory, sequential-thinking (Node, via npx)
  • fetch, time (Python, via uvx — installed only if uv is present)
  • playwright (Microsoft's browser-automation server)

The reference servers are a mix of Node and Python runtimes — fetch/time have no npm package and must run via uvx. The scripts handle that split for you. (The old SQLite and Puppeteer reference servers were archived, so they're deliberately not included.)

The scripts also surface — and optionally git clone into ~/ai-tools — a curated set of aligned, actively-maintained repos: awesome-claude-code, awesome-mcp-servers, the official MCP reference servers, OpenAI Codex, and Aider.

Why Gemini CLI is opt-in

It is not abandoned. The repo ships nightly releases (the most recent as I write this is dated today), and @google/gemini-cli published 0.55.1 on 2026-08-11. What is deprecated is the Homebrew formula, which Homebrew marks unsupported — so the scripts install Gemini CLI from npm on every platform, macOS included, rather than through brew.

It stays behind an explicit, default-no prompt for a duller reason than a shutdown: a fourth assistant on a fresh box is a choice, not a default. --auto skips it so unattended runs stay lean.

Correction, 2026-08-18: this section previously said Google was sunsetting Gemini CLI around 2026-06-18. That was wrong — the deprecated Homebrew formula is not the project.

Staying current without editing the script

This was a deliberate design goal: the scripts pin no framework versions. Every AI tool is installed with a floating selector that always resolves to the latest published release:

  • npm tools (Codex, repomix, ast-grep, tldr…) → npm install -g <pkg> (no version)
  • pip/pipx tools (aider, llm, markitdown, httpie, files-to-prompt…) → latest on install, and the scripts run pipx upgrade-all to refresh
  • Homebrew / apt / dnf / winget / choco → the package manager's current version
  • MCP serversnpx -y/uvx fetch the latest each run

So when aider or Codex ships a new release next week, you don't touch the script — re-run it (or run npm update -g, pipx upgrade-all, brew upgrade) and you're current. The only deliberate version reference anywhere is the Node LTS major (NODE_MAJOR, currently 22) — a once-a-year stability knob, not a per-release pin.

Full Linux script

Expand start-ai-terminal-kickstart.sh — Linux / WSL edition (apt / dnf)
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════════════
# AI Terminal Kickstart - Linux Edition
# Supports: Ubuntu 20.04+, Linux Mint 20+, RHEL 8+/9+, Fedora, AlmaLinux
#
# One script to prep a Linux PC for AI-powered terminal work:
# 1. Claude Code (Anthropic)
# 2. OpenAI Codex CLI (OpenAI)
# 3. ChatGPT terminal (shell-gpt, command `sgpt`)
# 4. GitHub Copilot CLI (GitHub, npm @github/copilot)
# + Netlify CLI for direct site deployment
#
# Usage:
# chmod +x start-ai-terminal-kickstart.sh
# sudo ./start-ai-terminal-kickstart.sh
# sudo ./start-ai-terminal-kickstart.sh --auto # unattended mode
# sudo ./start-ai-terminal-kickstart.sh --with-kimi # opt-in: also install Kimi Code CLI
#
# Default is US-only models. Kimi (Moonshot AI) is China-operated and is NEVER
# installed unless you explicitly pass --with-kimi. It is intentionally skipped
# even under --auto/--all/--everything.
#
# Author : AI Terminal ops
# Date : 2026-08-18
# ═══════════════════════════════════════════════════════════════════════════════

set -uo pipefail
# NOTE: -e (errexit) is intentionally omitted. This script uses manual error
# handling with || true and explicit checks. set -e causes false exits on
# arithmetic operations returning 0 and optional package installs failing.

# ─── Configuration ────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMP_DIR="/tmp/ai-kickstart-$(date +%Y%m%d)"
LOG_FILE="$TEMP_DIR/kickstart.log"
AUTO_MODE=false
WITH_KIMI=false # opt-in only. Kimi (Moonshot AI) is China-operated; US-only is the default.
ACTUAL_USER="${SUDO_USER:-$USER}"
ACTUAL_HOME=$(eval echo "~$ACTUAL_USER")

# Parse arguments
for arg in "$@"; do
  case "$arg" in
  --auto | -a | --all | --everything) AUTO_MODE=true ;;
  --with-kimi) WITH_KIMI=true ;;
  --help | -h)
    echo "Usage: sudo $0 [--auto|--all] [--with-kimi]"
    echo " --auto, --all  Install the full recommended stack without prompts"
    echo " --with-kimi    Opt-in: also install the Kimi Code CLI (Moonshot AI)."
    echo "                Default is US-only models; Kimi is China-operated and is"
    echo "                NEVER installed without this flag (skipped even under --auto)."
    exit 0
    ;;
  esac
done

mkdir -p "$TEMP_DIR"
touch "$LOG_FILE"

# ─── Detect Distro ───────────────────────────────────────────────────────────

DISTRO="unknown"
PKG_MGR="unknown"
PKG_INSTALL=""
PKG_UPDATE=""

if [ -f /etc/os-release ]; then
  . /etc/os-release
  case "$ID" in
  ubuntu | linuxmint | pop | elementary | zorin)
    DISTRO="debian"
    PKG_MGR="apt"
    PKG_INSTALL="apt-get install -y"
    PKG_UPDATE="apt-get update -qq"
    ;;
  rhel | centos | rocky | alma | ol)
    DISTRO="rhel"
    PKG_MGR="dnf"
    PKG_INSTALL="dnf install -y"
    PKG_UPDATE="dnf check-update || true"
    ;;
  fedora)
    DISTRO="rhel"
    PKG_MGR="dnf"
    PKG_INSTALL="dnf install -y"
    PKG_UPDATE="dnf check-update || true"
    ;;
  *)
    # Try to detect by package manager
    if command -v apt-get &>/dev/null; then
      DISTRO="debian"
      PKG_MGR="apt"
      PKG_INSTALL="apt-get install -y"
      PKG_UPDATE="apt-get update -qq"
    elif command -v dnf &>/dev/null; then
      DISTRO="rhel"
      PKG_MGR="dnf"
      PKG_INSTALL="dnf install -y"
      PKG_UPDATE="dnf check-update || true"
    elif command -v yum &>/dev/null; then
      DISTRO="rhel"
      PKG_MGR="yum"
      PKG_INSTALL="yum install -y"
      PKG_UPDATE="yum check-update || true"
    fi
    ;;
  esac
fi

# ─── Helper Functions ────────────────────────────────────────────────────────

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
GRAY='\033[0;37m'
NC='\033[0m' # No Color

log() { echo "[$(date +%H:%M:%S)] $*" >>"$LOG_FILE"; }
section() {
  echo -e "\n ${CYAN}=================================================================${NC}"
  echo -e " ${CYAN}$1${NC}"
  echo -e " ${CYAN}=================================================================${NC}"
  log "SECTION: $1"
}
step() {
  echo -e "\n ${CYAN}>> $1${NC}"
  log "STEP: $1"
}
ok() {
  echo -e " ${GREEN}[OK]${NC} $1"
  log " OK: $1"
}
warn() {
  echo -e " ${YELLOW}[WARN]${NC} $1"
  log "WARN: $1"
}
fail() {
  echo -e " ${RED}[FAIL]${NC} $1"
  log "FAIL: $1"
}
info() { echo -e " ${GRAY}$1${NC}"; }
tip() { echo -e " ${YELLOW}[TIP]${NC} $1"; }

cmd_exists() { command -v "$1" &>/dev/null; }

# True if ANY of the space-separated candidate commands exists. Some tools have
# a different binary name per distro (fd vs fdfind) or per major version
# (ImageMagick 7 ships only 'magick', not 'convert').
cmd_exists_any() {
  local c
  for c in $1; do
    command -v "$c" &>/dev/null && return 0
  done
  return 1
}

# True when the space-separated list in $2 contains the exact word $1. Used to
# tell "this install command failed" (FAILED) apart from "it installed but is
# not on PATH yet" (PARTIAL) when a batch installs several tools at once.
list_has() {
  case " $2 " in
  *" $1 "*) return 0 ;;
  *) return 1 ;;
  esac
}

# ─── Progress bar ────────────────────────────────────────────────────────────

TOTAL_PHASES=6
CURRENT_PHASE=0

show_progress() {
  local label="$1"
  CURRENT_PHASE=$((CURRENT_PHASE + 1))
  local pct=$((CURRENT_PHASE * 100 / TOTAL_PHASES))
  local bar_len=30
  local filled=$((bar_len * CURRENT_PHASE / TOTAL_PHASES))
  local empty=$((bar_len - filled))
  local bar=$(printf '%0.s█' $(seq 1 $filled 2>/dev/null) || true)
  local space=$(printf '%0.s░' $(seq 1 $empty 2>/dev/null) || true)
  printf "\r ${CYAN}[%s%s] %3d%% - Phase %d/%d: %s${NC} \n" \
    "$bar" "$space" "$pct" "$CURRENT_PHASE" "$TOTAL_PHASES" "$label"
}

# Spinner for long-running commands
run_with_spinner() {
  local label="$1"
  shift
  local cmd="$*"
  local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
  local i=0
  eval "$cmd" &
  local pid=$!
  while kill -0 "$pid" 2>/dev/null; do
    local c="${spin:i++%${#spin}:1}"
    printf "\r ${CYAN}%s${NC} %s " "$c" "$label"
    sleep 0.15
  done
  wait "$pid"
  local rc=$?
  printf "\r \r"
  return $rc
}

# ─── Version Configuration (change these to update targets) ──────────────────

NODE_MAJOR="22" # Node.js LTS major — the ONLY pinned version; all AI tools below install latest
# Python version is auto-detected from system package manager

ask_yn() {
  local prompt="$1"
  local default="${2:-y}"
  if $AUTO_MODE; then
    [[ "$default" == "y" ]] && return 0 || return 1
  fi
  local hint="Y/n"
  [[ "$default" == "n" ]] && hint="y/N"
  echo -ne " ${YELLOW}$prompt ($hint): ${NC}"
  # Read the answer from the terminal, not from stdin. Callers run ask_yn inside
  # `while read ... done <<< "$table"` loops, and a bare `read` there consumes the
  # NEXT TABLE ROW as the user's answer - silently skipping half the table.
  if [ -r /dev/tty ]; then
    read -r answer </dev/tty
  else
    read -r answer
  fi
  answer="${answer:-$default}"
  [[ "$answer" =~ ^[Yy] ]]
}

run_as_user() {
  # Run a command as the actual user (not root)
  # Uses "$@" quoting pattern via a helper to preserve spaces in arguments
  if [ "$EUID" -eq 0 ] && [ -n "${SUDO_USER:-}" ]; then
    sudo -u "$SUDO_USER" -- bash -c "$1"
  else
    bash -c "$1"
  fi
}

# PEP 668-aware pip. Debian 12+, Ubuntu 23.04+ and Fedora 38+ ship an
# "externally managed" system Python that REFUSES a plain
# `pip install --user`, so the second attempt adds --break-system-packages.
# Defined up here because Phase 1 installs Python packages long before the
# AI-framework section does.
ai_pip() {
  run_as_user "$PYTHON_CMD -m pip install --user --upgrade $* --quiet" >>"$LOG_FILE" 2>&1 ||
    run_as_user "$PYTHON_CMD -m pip install --user --break-system-packages --upgrade $* --quiet" >>"$LOG_FILE" 2>&1 ||
    true
}

# ─── Shell profile target (bash OR zsh — do not assume .bashrc) ──────────────
# Kali defaults to zsh, and plenty of Ubuntu users switch to it. Appending PATH
# lines to .bashrc on a zsh box does nothing at all, so detect the real login
# shell and write there. ~/.profile is the catch-all for anything else.
LOGIN_SHELL="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f7)"
[ -z "$LOGIN_SHELL" ] && LOGIN_SHELL="${SHELL:-/bin/bash}"
case "$LOGIN_SHELL" in
*/zsh) PROFILE_FILE="$ACTUAL_HOME/.zshrc" ;;
*/bash) PROFILE_FILE="$ACTUAL_HOME/.bashrc" ;;
*) PROFILE_FILE="$ACTUAL_HOME/.bashrc" ;;
esac
PROFILE_FALLBACK="$ACTUAL_HOME/.profile"

# Append a line to the shell profile ONCE (idempotent on a marker), mirrored
# into ~/.profile so a non-bash non-zsh login still picks it up.
add_to_profile() {
  local line="$1"
  local marker="$2"
  local f
  for f in "$PROFILE_FILE" "$PROFILE_FALLBACK"; do
    touch "$f" 2>/dev/null || true
    if ! grep -qF "$marker" "$f" 2>/dev/null; then
      printf '\n%s\n' "$line" >>"$f"
    fi
    chown "$ACTUAL_USER" "$f" 2>/dev/null || true
  done
}

# Write (or REPLACE) an `export VAR="value"` line, anchored to the assignment so
# a rotated key actually takes effect on a re-run and re-runs stop stacking
# duplicate exports. The file now holds a secret, so lock it down to mode 600.
# PRIMARY PROFILE ONLY: add_to_profile() above mirrors PATH lines into
# ~/.profile, but a secret belongs in exactly one file, never in two.
add_to_profile_kv() {
  local var="$1"
  local val="$2"
  local f tmp grep_rc
  f="$PROFILE_FILE"
  touch "$f" 2>/dev/null || true
  if grep -q "^export ${var}=" "$f" 2>/dev/null; then
    tmp="$(mktemp)"
    # Strip the stale line, then swap the file in atomically. grep -v exits 1
    # when it selects NO lines - exactly what happens when the profile held
    # nothing BUT that one export - so the swap must not hang off grep's exit
    # status or the stale export survives. Only a real grep error (rc >= 2)
    # skips the swap, because then $tmp cannot be trusted.
    grep -v "^export ${var}=" "$f" >"$tmp" 2>/dev/null
    grep_rc=$?
    [ "$grep_rc" -le 1 ] && mv "$tmp" "$f"
    rm -f "$tmp" 2>/dev/null || true
  fi
  printf '\nexport %s="%s"\n' "$var" "$val" >>"$f"
  chmod 600 "$f" 2>/dev/null || true
  chown "$ACTUAL_USER" "$f" 2>/dev/null || true
}

# Track results for final summary
declare -a RESULT_NAMES=()
declare -a RESULT_STATUS=()
declare -a RESULT_DETAIL=()

add_result() {
  RESULT_NAMES+=("$1")
  RESULT_STATUS+=("$2")
  RESULT_DETAIL+=("${3:-}")
}

# ─── Banner ───────────────────────────────────────────────────────────────────

echo -e "${MAGENTA}"
cat <<'BANNER'

 ╔═══════════════════════════════════════════════════════════════════╗
 ║                                                                   ║
 ║             A I   T E R M I N A L   K I C K S T A R T             ║
 ║             ─────────────────────────────────────────             ║
 ║               Linux Edition (Ubuntu / RHEL / Mint)                ║
 ║                                                                   ║
 ║     Claude Code | Codex | ChatGPT (sgpt) | GitHub Copilot CLI     ║
 ║                                                                   ║
 ╚═══════════════════════════════════════════════════════════════════╝

BANNER
echo -e "${NC}"

# ─── System Info ──────────────────────────────────────────────────────────────

echo -e " ${WHITE}System Info:${NC}"
echo -e " ${GRAY}OS : $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')${NC}"
echo -e " ${GRAY}Kernel : $(uname -r)${NC}"
echo -e " ${GRAY}Distro Type : $DISTRO ($PKG_MGR)${NC}"
echo -e " ${GRAY}User : $ACTUAL_USER${NC}"
echo -e " ${GRAY}Bash : $BASH_VERSION${NC}"
echo -e " ${GRAY}Login Shell : $LOGIN_SHELL${NC}"
echo -e " ${GRAY}Shell Profile : $PROFILE_FILE (+ $PROFILE_FALLBACK)${NC}"
echo -e " ${GRAY}Log File : $LOG_FILE${NC}"
echo ""

if [ "$EUID" -ne 0 ]; then
  echo -e " ${YELLOW}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e " ${YELLOW}│ WARNING: Not running as root. Re-run with sudo:              │${NC}"
  echo -e " ${YELLOW}│ sudo $0${NC}"
  echo -e " ${YELLOW}└──────────────────────────────────────────────────────────────┘${NC}"
  echo ""
fi

if [ "$DISTRO" = "unknown" ]; then
  fail "Could not detect your Linux distribution."
  echo -e " Supported: Ubuntu, Linux Mint, RHEL, CentOS, Rocky, Alma, Fedora"
  exit 1
fi

# ─── Install mode (Everything vs Choose) ─────────────────────────────────────
if ! $AUTO_MODE; then
  echo -e " ${WHITE}Install mode:${NC}"
  echo -e " ${GRAY}    1) Everything — install the full recommended stack, unattended${NC}"
  echo -e " ${GRAY}    2) Choose     — ask before each component${NC}"
  echo -ne " ${YELLOW}Pick [1/2] (default 2): ${NC}"
  read -r INSTALL_MODE
  if [ "${INSTALL_MODE:-2}" = "1" ]; then
    AUTO_MODE=true
    info "Mode: install everything (unattended)."
  else
    info "Mode: choose each component."
  fi
  echo ""
fi

# ─── Pre-flight: Admin, Network, Disk, Repos, TLS ────────────────────────────

show_progress "Pre-flight Checks"
section "PRE-FLIGHT CHECKS"

# Log session separator
echo "" >>"$LOG_FILE"
echo "======================================================================" >>"$LOG_FILE"
echo "Session started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOG_FILE"
echo "OS: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')" >>"$LOG_FILE"
echo "Distro type: $DISTRO ($PKG_MGR) | User: $ACTUAL_USER | EUID: $EUID" >>"$LOG_FILE"
echo "======================================================================" >>"$LOG_FILE"

# Admin / root access check
step "Checking root/sudo privileges"
if [ "$EUID" -eq 0 ]; then
  ok "Running as root - full install capability"
  add_result "Root Access" "OK" "Elevated"
else
  warn "NOT running as root. Package installs WILL fail."
  info "Re-run with: sudo $0"
  info "The following require root: apt/dnf install, repo setup, locale changes"
  add_result "Root Access" "FAILED" "Not root"
fi

# TLS / certificates check
step "Checking TLS certificates"
if [ -f /etc/ssl/certs/ca-certificates.crt ] || [ -d /etc/pki/tls/certs ]; then
  ok "CA certificate bundle present"
else
  warn "CA certificates may be missing - HTTPS downloads could fail"
  if [ "$DISTRO" = "debian" ]; then
    $PKG_INSTALL ca-certificates >>"$LOG_FILE" 2>&1 || true
  elif [ "$DISTRO" = "rhel" ]; then
    $PKG_INSTALL ca-certificates >>"$LOG_FILE" 2>&1 || true
  fi
fi

# OpenSSL version check
if cmd_exists openssl; then
  SSL_VER=$(openssl version 2>/dev/null || true)
  ok "OpenSSL: $SSL_VER"
else
  warn "openssl not found - some tools may have TLS issues"
fi
if [ -f /etc/ssl/certs/ca-certificates.crt ] || [ -d /etc/pki/tls/certs ]; then
  add_result "TLS/Certs" "OK" ""
else
  add_result "TLS/Certs" "FAILED" "no CA bundle"
fi

# Network connectivity - test multiple download sources
step "Testing internet connectivity to download sources"
REACHABLE=0
UNREACHABLE=0
declare -A NET_ENDPOINTS=(
  ["GitHub API"]="https://api.github.com"
  ["NodeSource"]="https://deb.nodesource.com"
  ["Claude installer"]="https://claude.ai"
  ["npm registry"]="https://registry.npmjs.org"
  ["PyPI"]="https://pypi.org"
)

for name in "${!NET_ENDPOINTS[@]}"; do
  url="${NET_ENDPOINTS[$name]}"
  if curl -sf --connect-timeout 8 --max-time 12 -o /dev/null "$url" 2>/dev/null; then
    ok "$name reachable"
    ((REACHABLE++)) || true
  else
    warn "$name ($url) - UNREACHABLE"
    ((UNREACHABLE++)) || true
  fi
done

TOTAL_EP=$((REACHABLE + UNREACHABLE))
if [ "$UNREACHABLE" -eq 0 ]; then
  ok "All $REACHABLE download sources reachable"
  add_result "Network" "OK" "$REACHABLE/$TOTAL_EP reachable"
elif [ "$UNREACHABLE" -lt 3 ]; then
  warn "$UNREACHABLE of $TOTAL_EP sources unreachable. Some installs may fail."
  add_result "Network" "PARTIAL" "$REACHABLE/$TOTAL_EP reachable"
else
  fail "Most download sources unreachable. Script will likely fail."
  info "Check: DNS, proxy, firewall, or VPN settings."
  add_result "Network" "FAILED" "$REACHABLE/$TOTAL_EP reachable"
fi

# Disk space check
step "Checking available disk space"
FREE_GB=$(df -BG / 2>/dev/null | awk 'NR==2{print $4}' | tr -d 'G')
TOTAL_GB=$(df -BG / 2>/dev/null | awk 'NR==2{print $2}' | tr -d 'G')
if [ "${FREE_GB:-0}" -lt 2 ]; then
  fail "CRITICAL: Only ${FREE_GB}GB free on / (${TOTAL_GB}GB total). Need at least 2GB."
  info "Free up disk space before continuing."
  add_result "Disk Space" "FAILED" "${FREE_GB}GB free"
elif [ "${FREE_GB:-0}" -lt 5 ]; then
  warn "Low: ${FREE_GB}GB free on / (${TOTAL_GB}GB total). Recommend 5+ GB."
  add_result "Disk Space" "WARN" "${FREE_GB}GB free"
else
  ok "Disk space: ${FREE_GB}GB free on / (${TOTAL_GB}GB total)"
  add_result "Disk Space" "OK" "${FREE_GB}GB free"
fi

# Check for package manager lock files
step "Checking for package manager locks"
if [ "$DISTRO" = "debian" ]; then
  if fuser /var/lib/dpkg/lock-frontend &>/dev/null 2>&1 || fuser /var/lib/apt/lists/lock &>/dev/null 2>&1; then
    warn "apt/dpkg is locked by another process. Wait for it to finish."
    info "Run: sudo lsof /var/lib/dpkg/lock-frontend to see what holds it."
    add_result "Package Lock" "WARN" "apt locked"
  else
    ok "No package manager locks detected"
    add_result "Package Lock" "OK" ""
  fi
elif [ "$DISTRO" = "rhel" ]; then
  if [ -f /var/run/yum.pid ] || [ -f /var/run/dnf.pid ]; then
    warn "dnf/yum lock file found. Another install may be running."
    add_result "Package Lock" "WARN" "dnf locked"
  else
    ok "No package manager locks detected"
    add_result "Package Lock" "OK" ""
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 1: SYSTEM UPDATE & CORE PACKAGES
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "System Update & Core Packages"
section "PHASE 1: System Update & Core Packages"

step "Updating package lists"
eval "$PKG_UPDATE" >>"$LOG_FILE" 2>&1 || true
ok "Package lists updated"

# ─── Essential build tools ────────────────────────────────────────────────────
step "Installing essential build tools"
if [ "$DISTRO" = "debian" ]; then
  $PKG_INSTALL build-essential curl wget ca-certificates gnupg lsb-release \
    software-properties-common unzip tar >>"$LOG_FILE" 2>&1 || true
elif [ "$DISTRO" = "rhel" ]; then
  $PKG_INSTALL gcc gcc-c++ make curl wget ca-certificates gnupg2 unzip tar \
    >>"$LOG_FILE" 2>&1 || true
  # Enable EPEL for extra packages
  if ! rpm -q epel-release &>/dev/null; then
    $PKG_INSTALL epel-release >>"$LOG_FILE" 2>&1 || true
  fi
fi
# Don't claim a package batch landed just because apt/dnf was invoked.
if cmd_exists make && cmd_exists curl; then
  ok "Build tools installed"
  add_result "Build Tools" "INSTALLED" ""
else
  fail "Build tools incomplete (make and/or curl missing) - see $LOG_FILE"
  add_result "Build Tools" "FAILED" "see log"
fi

step "Checking locale and UTF-8 support"
CURRENT_LANG="${LANG:-}"
if [[ "$CURRENT_LANG" == *"UTF-8"* ]] || [[ "$CURRENT_LANG" == *"utf8"* ]]; then
  ok "Locale is UTF-8: $CURRENT_LANG"
else
  warn "Locale is '$CURRENT_LANG' - may cause display issues with AI terminal output"
  if [ "$DISTRO" = "debian" ]; then
    $PKG_INSTALL locales >>"$LOG_FILE" 2>&1 || true
    locale-gen en_US.UTF-8 >>"$LOG_FILE" 2>&1 || true
    update-locale LANG=en_US.UTF-8 >>"$LOG_FILE" 2>&1 || true
  fi
  tip "Adding to $PROFILE_FILE: export LANG=en_US.UTF-8"
  add_to_profile 'export LANG=en_US.UTF-8' 'LANG=en_US.UTF-8'
fi

# ─── Git ──────────────────────────────────────────────────────────────────────
step "Checking Git"
if cmd_exists git; then
  ok "Git already installed ($(git --version))"
  add_result "Git" "PRESENT" "$(git --version)"
else
  info "Git is REQUIRED for Claude Code and version control."
  if ask_yn "Install Git?"; then
    $PKG_INSTALL git >>"$LOG_FILE" 2>&1 || true
    if cmd_exists git; then
      ok "Git installed ($(git --version))"
      add_result "Git" "INSTALLED" "$(git --version)"
    else
      fail "Git did NOT install - see $LOG_FILE"
      add_result "Git" "FAILED" "see log"
    fi
  else
    warn "Skipping Git - Claude Code WILL NOT WORK without it!"
    add_result "Git" "SKIPPED" "WARNING"
  fi
fi

# Git user config check
if cmd_exists git; then
  step "Checking Git configuration"
  GIT_USER=$(run_as_user "git config --global user.name" 2>/dev/null)
  GIT_EMAIL=$(run_as_user "git config --global user.email" 2>/dev/null)
  if [ -z "$GIT_USER" ] || [ -z "$GIT_EMAIL" ]; then
    warn "Git user.name or user.email not set. Commits will fail without these."
    tip "Run: git config --global user.name 'Your Name'"
    tip "Run: git config --global user.email 'you@example.com'"
  else
    ok "Git user: $GIT_USER <$GIT_EMAIL>"
  fi
  run_as_user "git config --global init.defaultBranch main" 2>/dev/null
  run_as_user "git config --global core.autocrlf input" 2>/dev/null
fi

# ─── Node.js (via NodeSource) ────────────────────────────────────────────────
step "Checking Node.js"
if cmd_exists node; then
  ok "Node.js already installed ($(node --version))"
  cmd_exists npm && ok "npm $(npm --version)"
  cmd_exists npx && ok "npx available"
  add_result "Node.js" "PRESENT" "$(node --version)"
else
  info "Node.js is required for Claude Code, npm tools, and MCP servers."
  if ask_yn "Install Node.js ${NODE_MAJOR} LTS?"; then
    if [ "$DISTRO" = "debian" ]; then
      # NodeSource setup for Debian/Ubuntu
      curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - >>"$LOG_FILE" 2>&1 || true
      $PKG_INSTALL nodejs >>"$LOG_FILE" 2>&1 || true
    elif [ "$DISTRO" = "rhel" ]; then
      curl -fsSL https://rpm.nodesource.com/setup_${NODE_MAJOR}.x | bash - >>"$LOG_FILE" 2>&1 || true
      $PKG_INSTALL nodejs >>"$LOG_FILE" 2>&1 || true
    fi

    if cmd_exists node; then
      ok "Node.js $(node --version) installed"
      ok "npm $(npm --version)"
      add_result "Node.js" "INSTALLED" "$(node --version)"
    else
      fail "Node.js installation failed"
      add_result "Node.js" "FAILED" ""
    fi
  else
    add_result "Node.js" "SKIPPED" ""
  fi
fi

# Fix npm global permissions for the actual user
if cmd_exists npm; then
  step "Configuring npm global directory (no-sudo installs)"
  NPM_GLOBAL="$ACTUAL_HOME/.npm-global"
  run_as_user "mkdir -p '$NPM_GLOBAL'"
  run_as_user "npm config set prefix '$NPM_GLOBAL'"

  # Add to PATH in the detected shell profile if not already there
  add_to_profile 'export PATH="$HOME/.npm-global/bin:$PATH"' 'npm-global'
  export PATH="$NPM_GLOBAL/bin:$PATH"
  ok "npm global dir: $NPM_GLOBAL (PATH line added to $PROFILE_FILE)"
fi

# ─── Python ───────────────────────────────────────────────────────────────────
step "Checking Python"
PYTHON_CMD=""
if cmd_exists python3; then
  PYTHON_CMD="python3"
elif cmd_exists python; then
  PYTHON_CMD="python"
fi

if [ -n "$PYTHON_CMD" ]; then
  ok "Python already installed ($($PYTHON_CMD --version 2>&1))"
  add_result "Python" "PRESENT" "$($PYTHON_CMD --version 2>&1)"
else
  info "Python is used for MCP servers, AI SDKs, and data processing."
  if ask_yn "Install Python 3?"; then
    if [ "$DISTRO" = "debian" ]; then
      $PKG_INSTALL python3 python3-pip python3-venv python3-dev >>"$LOG_FILE" 2>&1 || true
    elif [ "$DISTRO" = "rhel" ]; then
      $PKG_INSTALL python3 python3-pip python3-devel >>"$LOG_FILE" 2>&1 || true
    fi
    PYTHON_CMD="python3"
    if cmd_exists python3; then
      ok "Python $(python3 --version 2>&1) installed"
      add_result "Python" "INSTALLED" "$(python3 --version 2>&1)"
    else
      fail "Python installation failed"
      add_result "Python" "FAILED" ""
    fi
  else
    add_result "Python" "SKIPPED" ""
  fi
fi

# Upgrade pip
if [ -n "$PYTHON_CMD" ] && cmd_exists "$PYTHON_CMD"; then
  step "Upgrading pip"
  run_as_user "$PYTHON_CMD -m pip install --upgrade pip --quiet" 2>/dev/null || true
  ok "pip upgraded"
fi

# ─── CLI Utilities ────────────────────────────────────────────────────────────
step "Installing CLI utilities"

declare -A CLI_TOOLS
if [ "$DISTRO" = "debian" ]; then
  CLI_TOOLS=(
    [ripgrep]="ripgrep"
    [jq]="jq"
    [fd]="fd-find"
    [imagemagick]="imagemagick"
    [p7zip]="p7zip-full"
    [ghostscript]="ghostscript"
    [ffmpeg]="ffmpeg"
    [bat]="bat"
    [fzf]="fzf"
    [yq]="yq"
    [tree]="tree"
    [delta]="git-delta"
    [shellcheck]="shellcheck"
    [tmux]="tmux"
    [sqlite3]="sqlite3"
  )
elif [ "$DISTRO" = "rhel" ]; then
  CLI_TOOLS=(
    [ripgrep]="ripgrep"
    [jq]="jq"
    [fd]="fd-find"
    [imagemagick]="ImageMagick"
    [p7zip]="p7zip"
    [ghostscript]="ghostscript"
    [ffmpeg]="ffmpeg"
    [bat]="bat"
    [fzf]="fzf"
    [yq]="yq"
    [tree]="tree"
    [delta]="git-delta"
    [shellcheck]="ShellCheck"
    [tmux]="tmux"
    [sqlite3]="sqlite"
  )
fi

# Probe commands. Space-separated = "any one of these counts as installed".
# declare -A matters: without it bash treats [key]= as an ARITHMETIC index, every
# entry collapses onto index 0, and every tool gets probed with the last value.
declare -A CLI_CMDS
CLI_CMDS=(
  [ripgrep]="rg"
  [jq]="jq"
  [fd]="fdfind fd"
  [imagemagick]="magick convert"
  [p7zip]="7z 7zz 7za"
  [ghostscript]="gs"
  [ffmpeg]="ffmpeg"
  [bat]="batcat bat"
  [fzf]="fzf"
  [yq]="yq"
  [tree]="tree"
  [delta]="delta"
  [shellcheck]="shellcheck"
  [tmux]="tmux"
  [sqlite3]="sqlite3"
)

declare -A CLI_DESC
CLI_DESC=(
  [ripgrep]="Ultra-fast code search (used by Claude Code)"
  [jq]="JSON processor for API responses"
  [fd]="Fast file finder"
  [imagemagick]="Image processing (convert, resize)"
  [p7zip]="Archive compression"
  [ghostscript]="PDF/PostScript engine (required by ImageMagick for PDFs)"
  [ffmpeg]="Video/audio processing and media conversion"
  [bat]="Syntax-highlighted file viewer (better cat)"
  [fzf]="Fuzzy finder for files, history, and commands"
  [yq]="YAML processor (like jq but for YAML)"
  [tree]="Directory structure viewer"
  [delta]="Enhanced git diff viewer with syntax highlighting"
  [shellcheck]="Shell script linter and validator"
  [tmux]="Terminal multiplexer for persistent sessions"
  [sqlite3]="Lightweight database engine and client"
)

for tool in ripgrep jq fd imagemagick p7zip ghostscript ffmpeg bat fzf yq tree delta shellcheck tmux sqlite3; do
  pkg="${CLI_TOOLS[$tool]:-}"
  cmd="${CLI_CMDS[$tool]:-$tool}"
  desc="${CLI_DESC[$tool]:-}"

  if cmd_exists_any "$cmd"; then
    ok "$tool is installed"
    add_result "$tool" "PRESENT" ""
  else
    info "$tool: $desc"
    if ask_yn "Install $tool?"; then
      $PKG_INSTALL "$pkg" >>"$LOG_FILE" 2>&1 || true
      # The install line ends in || true, so re-probe instead of assuming.
      if cmd_exists_any "$cmd"; then
        ok "$tool installed"
        add_result "$tool" "INSTALLED" ""
      else
        fail "$tool ($pkg) did NOT install - see $LOG_FILE"
        add_result "$tool" "FAILED" "see log"
      fi
    else
      add_result "$tool" "SKIPPED" ""
    fi
  fi
done

# ─── GitHub CLI (gh) ─────────────────────────────────────────────────────────
# Installed with the rest of the CLI tools, NOT buried inside the Copilot
# branch: gh is how you do repo/PR/issue work and how git gets its credentials,
# whether or not you ever touch Copilot. It needs its own repo setup because
# most distros don't carry gh in their default repos.
step "Checking GitHub CLI (gh)"
if cmd_exists gh; then
  ok "GitHub CLI already installed ($(gh --version 2>/dev/null | head -1))"
  add_result "GitHub CLI" "PRESENT" ""
else
  info "gh: GitHub from the terminal (repos, PRs, issues) and the login that"
  info "     git itself uses to push. Also powers GitHub Copilot CLI auth."
  if ask_yn "Install GitHub CLI?"; then
    if [ "$DISTRO" = "debian" ]; then
      curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null
      chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg || true
      echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" >/etc/apt/sources.list.d/github-cli.list
      apt-get update -qq >>"$LOG_FILE" 2>&1 || true
      $PKG_INSTALL gh >>"$LOG_FILE" 2>&1 || true
    elif [ "$DISTRO" = "rhel" ]; then
      dnf install -y 'dnf-command(config-manager)' >>"$LOG_FILE" 2>&1 || true
      dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo >>"$LOG_FILE" 2>&1 || true
      $PKG_INSTALL gh >>"$LOG_FILE" 2>&1 || true
    fi
    if cmd_exists gh; then
      ok "GitHub CLI installed ($(gh --version 2>/dev/null | head -1))"
      add_result "GitHub CLI" "INSTALLED" ""
    else
      fail "GitHub CLI installation failed - see $LOG_FILE"
      add_result "GitHub CLI" "FAILED" ""
    fi
  else
    add_result "GitHub CLI" "SKIPPED" ""
  fi
fi
if cmd_exists gh; then
  tip "To authenticate: gh auth login   then   gh auth setup-git"
  tip "'gh auth setup-git' is the step that makes 'git push' work over HTTPS."
fi

# ─── Python Packages ─────────────────────────────────────────────────────────
if [ -n "$PYTHON_CMD" ] && cmd_exists "$PYTHON_CMD"; then
  step "Python packages for AI development"
  info "These packages enhance what Claude Code and AI tools can do:"
  info " Data: numpy, pandas, polars, matplotlib, openpyxl"
  info " Docs: PyMuPDF, pdfplumber (read PDFs so an agent can quote them)"
  info " Web: requests, httpx, beautifulsoup4"
  info " AI: anthropic, openai, fastmcp, mcp"
  info " Dev: pydantic, rich, pyyaml, python-dotenv"

  if ask_yn "Install recommended Python packages?"; then
    PACKAGES="numpy pandas polars matplotlib openpyxl requests httpx beautifulsoup4 lxml PyMuPDF pdfplumber anthropic openai fastmcp mcp pydantic rich pyyaml python-dotenv Pillow chardet tabulate jsonlines"
    # Count the packages instead of hardcoding a number that goes stale.
    PKG_COUNT=$(echo "$PACKAGES" | wc -w | tr -d ' ')
    # ai_pip retries with --break-system-packages on PEP 668 distros.
    ai_pip $PACKAGES
    # Do NOT assert success - actually import three of them before claiming it.
    if run_as_user "$PYTHON_CMD -c 'import anthropic, mcp, pandas'" >>"$LOG_FILE" 2>&1; then
      ok "Python packages installed ($PKG_COUNT packages)"
      add_result "Python Packages" "INSTALLED" "$PKG_COUNT packages"
    else
      fail "Python packages did NOT import - the install failed"
      info "Log: $LOG_FILE"
      info "Debian 12+/Ubuntu 23.04+/Fedora 38+ mark the system Python externally"
      info "managed (PEP 668). Try: $PYTHON_CMD -m venv ~/.ai-venv && ~/.ai-venv/bin/pip install $PACKAGES"
      add_result "Python Packages" "FAILED" "see log"
    fi
  else
    add_result "Python Packages" "SKIPPED" ""
  fi

  # uv package manager
  step "Checking uv (fast Python package manager)"
  if cmd_exists uv; then
    ok "uv already installed ($(uv --version 2>/dev/null))"
    add_result "uv" "PRESENT" ""
  else
    if ask_yn "Install uv?"; then
      if run_as_user "curl -LsSf https://astral.sh/uv/install.sh | sh" >>"$LOG_FILE" 2>&1; then UV_RC=0; else UV_RC=1; fi
      # uv's installer targets ~/.local/bin on current versions and ~/.cargo/bin
      # on older ones - put both on PATH before probing for it.
      export PATH="$ACTUAL_HOME/.local/bin:$ACTUAL_HOME/.cargo/bin:$PATH"
      if cmd_exists uv || run_as_user "command -v uv" >/dev/null 2>&1; then
        ok "uv installed"
        add_result "uv" "INSTALLED" ""
      elif [ "$UV_RC" -eq 0 ]; then
        warn "uv installed but 'uv' is not on PATH in this shell yet"
        add_result "uv" "PARTIAL" "restart shell"
      else
        fail "uv install failed - see $LOG_FILE"
        add_result "uv" "FAILED" "see log"
      fi
    else
      add_result "uv" "SKIPPED" ""
    fi
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 2: AI TERMINAL INSTALLATIONS
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "AI Terminal Solutions"
section "PHASE 2: AI Terminal Solutions"

echo ""
echo -e " ${WHITE}Which AI terminal solutions would you like to set up?${NC}"
echo ""
echo -e " ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
echo -e " ${CYAN}│ 1. Claude Code (Anthropic)                                   │${NC}"
echo -e " ${GRAY}│ Best for: Deep code understanding, multi-file edits,         │${NC}"
echo -e " ${GRAY}│ agentic coding, MCP servers, security research               │${NC}"
echo -e " ${GRAY}│ Auth: Browser login (OAuth) or ANTHROPIC_API_KEY             │${NC}"
echo -e " ${CYAN}│                                                              │${NC}"
echo -e " ${CYAN}│ 2. OpenAI Codex CLI (OpenAI)                                 │${NC}"
echo -e " ${GRAY}│ Best for: OpenAI-model agentic coding in the terminal        │${NC}"
echo -e " ${GRAY}│ Auth: sign in with ChatGPT, or OPENAI_API_KEY                │${NC}"
echo -e " ${CYAN}│                                                              │${NC}"
echo -e " ${CYAN}│ 3. ChatGPT terminal (shell-gpt)                              │${NC}"
echo -e " ${GRAY}│ Best for: asking ChatGPT straight from the shell, plus the   │${NC}"
echo -e " ${GRAY}│ OpenAI Python SDK for scripting                              │${NC}"
echo -e " ${GRAY}│ Auth: OPENAI_API_KEY environment variable                    │${NC}"
echo -e " ${CYAN}│                                                              │${NC}"
echo -e " ${CYAN}│ 4. GitHub Copilot CLI (GitHub)                               │${NC}"
echo -e " ${GRAY}│ Best for: Shell command suggestions, git operations          │${NC}"
echo -e " ${GRAY}│ Auth: gh auth login (browser-based)                          │${NC}"
echo -e " ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"
echo ""

INSTALL_CLAUDE=false
INSTALL_CODEX=false
INSTALL_CHATGPT=false
INSTALL_COPILOT=false

ask_yn "Install Claude Code?" && INSTALL_CLAUDE=true
ask_yn "Install OpenAI Codex CLI?" && INSTALL_CODEX=true
ask_yn "Install ChatGPT terminal (shell-gpt)?" && INSTALL_CHATGPT=true
ask_yn "Install GitHub Copilot CLI?" && INSTALL_COPILOT=true

# ─── Claude Code ──────────────────────────────────────────────────────────────
if $INSTALL_CLAUDE; then
  section "Installing Claude Code"

  if cmd_exists claude; then
    ok "Claude Code already installed ($(claude --version 2>/dev/null || echo 'installed'))"
    add_result "Claude Code" "PRESENT" ""
  else
    step "Installing Claude Code via official installer..."
    info "This uses Anthropic's official install script."

    run_as_user "curl -fsSL https://claude.ai/install.sh | sh" >>"$LOG_FILE" 2>&1 || true

    # Add to PATH
    export PATH="$ACTUAL_HOME/.local/bin:$PATH"
    add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'

    if run_as_user "command -v claude" &>/dev/null; then
      ok "Claude Code installed!"
      add_result "Claude Code" "INSTALLED" ""
    else
      # Fallback: npm install
      warn "Official installer may need a new shell. Trying npm fallback..."
      if cmd_exists npm; then
        # Keep the install's exit status: "needs a new shell" is only honest when
        # the install itself exited 0. Otherwise this is a FAILED install.
        if run_as_user "npm install -g @anthropic-ai/claude-code" >>"$LOG_FILE" 2>&1; then CLAUDE_NPM_RC=0; else CLAUDE_NPM_RC=1; fi
        if run_as_user "command -v claude" &>/dev/null || cmd_exists claude; then
          ok "Claude Code installed via npm"
          add_result "Claude Code" "INSTALLED" "via npm"
        elif [ "$CLAUDE_NPM_RC" -eq 0 ]; then
          warn "Claude Code installed but 'claude' is not on PATH in this shell yet"
          tip "Close this terminal, open a new one, and run: claude"
          add_result "Claude Code" "PARTIAL" "restart terminal"
        else
          fail "Claude Code install failed - see $LOG_FILE"
          add_result "Claude Code" "FAILED" "see log"
        fi
      else
        fail "npm not available for fallback install"
        add_result "Claude Code" "FAILED" "npm missing"
      fi
    fi
  fi

  # Claude Code authentication guide
  echo ""
  echo -e " ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e " ${CYAN}│ CLAUDE CODE AUTHENTICATION                                   │${NC}"
  echo -e " ${CYAN}├──────────────────────────────────────────────────────────────┤${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${GRAY}│ Option A - Browser Login (easiest):                          │${NC}"
  echo -e " ${WHITE}│ 1. Run: claude                                               │${NC}"
  echo -e " ${WHITE}│ 2. Browser opens -> log in at claude.ai                      │${NC}"
  echo -e " ${WHITE}│ 3. Click Authorize -> return to terminal                     │${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${GRAY}│ Option B - API Key:                                          │${NC}"
  echo -e " ${WHITE}│ 1. Get key from console.anthropic.com/settings/keys          │${NC}"
  echo -e " ${WHITE}│ 2. export ANTHROPIC_API_KEY=\"sk-ant-your-key\"                │${NC}"
  echo -e " ${WHITE}│ 3. Add it to your shell profile to persist                   │${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

  if ask_yn "Set ANTHROPIC_API_KEY now?" "n"; then
    # -s so the key never echoes (screen shares, shoulders, scrollback).
    # -s also swallows your Enter, so print the newline ourselves.
    echo -ne " ${YELLOW}Enter your Anthropic API key (input hidden): ${NC}"
    read -rs ANTHROPIC_KEY
    echo ""
    if [[ "$ANTHROPIC_KEY" == sk-ant-* ]]; then
      add_to_profile_kv ANTHROPIC_API_KEY "$ANTHROPIC_KEY"
      export ANTHROPIC_API_KEY="$ANTHROPIC_KEY"
      ok "ANTHROPIC_API_KEY saved to $PROFILE_FILE"
      warn "That file now holds a secret, so its permissions were changed to 600"
      warn "(owner read/write only). The key is written to that ONE file."
    else
      warn "Key doesn't start with sk-ant- . Skipped. Use browser login instead."
    fi
  fi
fi

# ─── OpenAI Codex CLI ────────────────────────────────────────────────────────
# Its own top-level prompt, not buried in the framework bundle: declining that
# bundle should not silently cost you Codex.
if $INSTALL_CODEX; then
  section "Installing OpenAI Codex CLI"

  if cmd_exists codex || run_as_user "command -v codex" >/dev/null 2>&1; then
    ok "Codex already installed ($(run_as_user 'codex --version' 2>/dev/null | head -1 || echo 'installed'))"
    add_result "OpenAI Codex" "PRESENT" ""
  elif cmd_exists npm; then
    step "Installing Codex (npm @openai/codex)..."
    # Capture the install's exit status. A missing binary after a FAILED install
    # is a failure, not a PATH problem, and must never be reported as INSTALLED.
    if run_as_user "npm install -g @openai/codex" >>"$LOG_FILE" 2>&1; then CODEX_RC=0; else CODEX_RC=1; fi
    if cmd_exists codex || run_as_user "command -v codex" >/dev/null 2>&1; then
      ok "Codex installed (run: codex)"
      add_result "OpenAI Codex" "INSTALLED" ""
    elif [ "$CODEX_RC" -eq 0 ]; then
      warn "Codex installed but 'codex' is not on PATH in this shell yet"
      tip "Close this terminal, open a new one, then run: codex"
      add_result "OpenAI Codex" "PARTIAL" "restart terminal"
    else
      fail "Codex install failed - see $LOG_FILE"
      add_result "OpenAI Codex" "FAILED" "see log"
    fi
  else
    fail "npm not available - install Node.js first"
    add_result "OpenAI Codex" "FAILED" "npm missing"
  fi
  info "Codex auth: run 'codex' and sign in with ChatGPT, or set OPENAI_API_KEY."
fi

# ─── ChatGPT terminal (shell-gpt) + OpenAI Python SDK + key ──────────────
# shell-gpt is the maintained "ChatGPT in your terminal" CLI (PyPI shell-gpt,
# command 'sgpt'). It reads OPENAI_API_KEY - the SAME key this branch captures
# further down, so the script never asks you for an OpenAI key twice.
if $INSTALL_CHATGPT; then
  section "Installing ChatGPT Terminal (shell-gpt)"

  step "Installing shell-gpt (command: sgpt) via pipx"
  # pipx gives it its own venv, which is the only way the PEP 668 distros
  # (Debian 12+, Ubuntu 23.04+, Fedora 38+) will install a Python app at all.
  SGPT_RC=1
  if ! cmd_exists pipx; then
    $PKG_INSTALL pipx >>"$LOG_FILE" 2>&1 || ai_pip pipx
    run_as_user "pipx ensurepath" >>"$LOG_FILE" 2>&1 || true
  fi
  add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'
  export PATH="$ACTUAL_HOME/.local/bin:$PATH"
  if cmd_exists pipx; then
    if run_as_user "pipx install shell-gpt" >>"$LOG_FILE" 2>&1 || run_as_user "pipx install --force shell-gpt" >>"$LOG_FILE" 2>&1; then
      SGPT_RC=0
    fi
  elif [ -n "${PYTHON_CMD:-}" ] && cmd_exists "$PYTHON_CMD"; then
    warn "pipx unavailable - falling back to pip --user for shell-gpt"
    # Same two attempts as ai_pip(), but WITHOUT its trailing '|| true': the
    # exit status is what decides FAILED vs PARTIAL below.
    if run_as_user "$PYTHON_CMD -m pip install --user --upgrade shell-gpt --quiet" >>"$LOG_FILE" 2>&1 || run_as_user "$PYTHON_CMD -m pip install --user --break-system-packages --upgrade shell-gpt --quiet" >>"$LOG_FILE" 2>&1; then
      SGPT_RC=0
    fi
  else
    fail "Neither pipx nor Python is available - cannot install shell-gpt"
  fi

  # Never claim INSTALLED unless the binary is really there. PARTIAL only when
  # the install command itself exited 0 and PATH is the only thing missing.
  if cmd_exists sgpt || run_as_user "command -v sgpt" >/dev/null 2>&1; then
    # Probe 'sgpt --version' ONLY when shell-gpt cannot prompt. On a first run
    # with no key and no config it reads an API key straight from the terminal
    # (getpass reads /dev/tty), which closing stdin does NOT prevent - it would
    # hang this installer. INSTALLED is gated on the binary either way.
    SGPT_VER=""
    if [ -n "${OPENAI_API_KEY:-}" ] || [ -f "$ACTUAL_HOME/.config/shell_gpt/.sgptrc" ]; then
      SGPT_VER="$(run_as_user 'sgpt --version' 2>/dev/null </dev/null | head -1 || true)"
    fi
    ok "ChatGPT terminal installed${SGPT_VER:+ ($SGPT_VER)}"
    add_result "ChatGPT Terminal" "INSTALLED" "sgpt"
  elif [ "$SGPT_RC" -eq 0 ]; then
    warn "shell-gpt installed but 'sgpt' is not on PATH in this shell yet"
    tip "Close this terminal, open a new one, then run: sgpt --version"
    add_result "ChatGPT Terminal" "PARTIAL" "restart shell"
  else
    fail "shell-gpt install failed - see $LOG_FILE"
    add_result "ChatGPT Terminal" "FAILED" "see log"
  fi
  info "Ask it anything: sgpt \"how do I tail a log file?\""
  info "Shell commands : sgpt --shell \"find files over 100MB\""
  info "Code only      : sgpt --code \"python: read a csv with pandas\""

  step "Installing the OpenAI Python SDK..."

  # Install the Python openai package for scripting
  if [ -n "${PYTHON_CMD:-}" ] && cmd_exists "$PYTHON_CMD"; then
    ai_pip openai
    if run_as_user "$PYTHON_CMD -c 'import openai'" >>"$LOG_FILE" 2>&1; then
      ok "OpenAI Python SDK installed"
      add_result "OpenAI Python SDK" "INSTALLED" ""
    else
      fail "OpenAI Python SDK did NOT import - see $LOG_FILE"
      add_result "OpenAI Python SDK" "FAILED" "see log"
    fi
  fi

  # Authentication guide
  echo ""
  echo -e " ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e " ${CYAN}│ OPENAI API AUTHENTICATION                                    │${NC}"
  echo -e " ${CYAN}├──────────────────────────────────────────────────────────────┤${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${GRAY}│ One key drives both 'sgpt' and the openai Python SDK.        │${NC}"
  echo -e " ${WHITE}│ 1. Get API key: https://platform.openai.com/api-keys         │${NC}"
  echo -e " ${WHITE}│ 2. Set it: export OPENAI_API_KEY=\"sk-your-key\"               │${NC}"
  echo -e " ${WHITE}│ 3. Persist: add the export line to your shell profile        │${NC}"
  echo -e " ${WHITE}│ 4. Test: curl https://api.openai.com/v1/models \\             │${NC}"
  echo -e " ${WHITE}│ -H \"Authorization: Bearer \$OPENAI_API_KEY\"                   │${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

  if ask_yn "Set OPENAI_API_KEY now?" "n"; then
    # -s so the key never echoes; -s eats the Enter, so echo the newline back.
    echo -ne " ${YELLOW}Enter your OpenAI API key (input hidden): ${NC}"
    read -rs OPENAI_KEY
    echo ""
    if [[ "$OPENAI_KEY" == sk-* ]]; then
      add_to_profile_kv OPENAI_API_KEY "$OPENAI_KEY"
      export OPENAI_API_KEY="$OPENAI_KEY"
      ok "OPENAI_API_KEY saved to $PROFILE_FILE - sgpt reads it from there"
      warn "That file now holds a secret, so its permissions were changed to 600"
      warn "(owner read/write only). The key is written to that ONE file."
    else
      warn "Key format not recognized. Set it manually later."
    fi
  fi
fi

# ─── GitHub Copilot CLI ──────────────────────────────────────────────────────
if $INSTALL_COPILOT; then
  section "Installing GitHub Copilot CLI"

  # GitHub CLI now installs in the main CLI tool set back in Phase 1, so
  # declining Copilot no longer costs you gh.
  if cmd_exists gh; then
    ok "GitHub CLI: $(gh --version 2>/dev/null | head -1)"
  else
    warn "GitHub CLI (gh) is not installed - you'll need it to authenticate Copilot."
  fi

  # The old 'gh extension install github/gh-copilot' extension is retired.
  # Copilot CLI now ships as its own npm package; the command is 'copilot'.
  step "Installing GitHub Copilot CLI (npm @github/copilot)..."
  if cmd_exists npm; then
    if run_as_user "npm install -g @github/copilot" >>"$LOG_FILE" 2>&1; then COPILOT_RC=0; else COPILOT_RC=1; fi
    if cmd_exists copilot || run_as_user "command -v copilot" >/dev/null 2>&1; then
      ok "GitHub Copilot CLI installed (run: copilot)"
      add_result "GitHub Copilot CLI" "INSTALLED" "copilot"
    elif [ "$COPILOT_RC" -eq 0 ]; then
      warn "Copilot CLI installed but 'copilot' is not on PATH in this shell yet"
      tip "Close this terminal, open a new one, then run: copilot"
      add_result "GitHub Copilot CLI" "PARTIAL" "restart terminal"
    else
      fail "GitHub Copilot CLI install failed - see $LOG_FILE"
      add_result "GitHub Copilot CLI" "FAILED" "see log"
    fi
  else
    fail "npm not available - cannot install GitHub Copilot CLI"
    add_result "GitHub Copilot CLI" "FAILED" "npm missing"
  fi

  if cmd_exists gh; then
    # Authentication guide
    echo ""
    echo -e " ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
    echo -e " ${CYAN}│ GITHUB COPILOT AUTHENTICATION                                │${NC}"
    echo -e " ${CYAN}├──────────────────────────────────────────────────────────────┤${NC}"
    echo -e " ${GRAY}│                                                              │${NC}"
    echo -e " ${WHITE}│ 1. Run: gh auth login                                        │${NC}"
    echo -e " ${WHITE}│ 2. Choose: GitHub.com                                        │${NC}"
    echo -e " ${WHITE}│ 3. Choose: Login with a web browser                          │${NC}"
    echo -e " ${WHITE}│ 4. Copy the one-time code shown                              │${NC}"
    echo -e " ${WHITE}│ 5. Browser opens -> paste code -> Authorize                  │${NC}"
    echo -e " ${WHITE}│ 6. Return to terminal -> authenticated                       │${NC}"
    echo -e " ${GRAY}│                                                              │${NC}"
    echo -e " ${GRAY}│ Requires: GitHub Copilot subscription (\$10/mo or free        │${NC}"
    echo -e " ${GRAY}│ for students and open source maintainers)                    │${NC}"
    echo -e " ${GRAY}│                                                              │${NC}"
    echo -e " ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

    if ask_yn "Log in to GitHub now?" "n"; then
      if run_as_user "gh auth login"; then
        # Without this, git push over HTTPS still prompts for a password that
        # GitHub no longer accepts. gh becomes git's credential helper.
        step "Wiring gh in as git's credential helper (gh auth setup-git)"
        if run_as_user "gh auth setup-git" >>"$LOG_FILE" 2>&1; then
          ok "git credential helper configured - 'git push' now uses your gh login"
          add_result "git credential helper" "OK" "gh auth setup-git"
        else
          warn "gh auth setup-git failed - run it yourself after logging in"
          add_result "git credential helper" "FAILED" "run gh auth setup-git"
        fi
      else
        warn "gh auth login did not complete - skipping git credential setup"
      fi
    fi
  else
    warn "GitHub CLI not available - install gh, then run: gh auth login"
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 2.5: AI FRAMEWORKS & CONVERSION CLIs
# ═══════════════════════════════════════════════════════════════════════════════

section "AI Frameworks & Conversion CLIs"
info "Agentic helpers (aider, llm + llm-anthropic), repo/document packers (repomix,"
info "files-to-prompt, markitdown), Python linting (ruff), a usage report for your"
info "agent CLIs (ccusage), and converters (pandoc, poppler, tesseract OCR, yt-dlp,"
info "jc) that feed code and documents into any AI model."

# Make sure pipx / pip --user entry points (~/.local/bin) are on PATH
add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'
export PATH="$ACTUAL_HOME/.local/bin:$PATH"

# NOTE: ai_pip() (the PEP 668-aware pip wrapper) is defined near the top of this
# script, next to run_as_user(), because Phase 1 needs it too.

if ask_yn "Install the AI framework + conversion bundle?"; then

  # ── Document/media conversion tools via the system package manager ──
  step "Conversion tools (pandoc, poppler, tesseract, jc, glow, yt-dlp, gron, just)"
  # Install one at a time. apt/dnf abort the ENTIRE batch if any single package
  # is missing in the configured repos (e.g. 'just'/'gron' aren't in older
  # Ubuntu/Debian or EL8), so a per-package loop lets the resolvable subset
  # install instead of taking everything down with it.
  if [ "$DISTRO" = "debian" ]; then
    conv_pkgs="pandoc poppler-utils tesseract-ocr jc glow yt-dlp gron just libimage-exiftool-perl qpdf"
  elif [ "$DISTRO" = "rhel" ]; then
    # jc / glow / yt-dlp / tesseract / gron are in EPEL on RHEL (enabled in Phase 1);
    # 'just' is packaged as 'rust-just' on Fedora/RHEL.
    # exiftool ships as the Perl module package on RHEL/Fedora, not as 'exiftool'.
    conv_pkgs="pandoc poppler-utils tesseract jc glow yt-dlp gron rust-just perl-Image-ExifTool qpdf"
  else
    conv_pkgs=""
  fi
  for cpkg in $conv_pkgs; do
    $PKG_INSTALL "$cpkg" >>"$LOG_FILE" 2>&1 || true
  done
  for probe in "pandoc:pandoc" "pdftotext:poppler" "tesseract:tesseract" "jc:jc" "glow:glow" "yt-dlp:yt-dlp" "gron:gron" "just:just" "exiftool:exiftool" "qpdf:qpdf"; do
    fw_bin="${probe%%:*}"
    fw_name="${probe#*:}"
    if cmd_exists "$fw_bin"; then
      ok "$fw_name ($fw_bin) installed"
      add_result "$fw_name" "INSTALLED" ""
    elif [ -z "$conv_pkgs" ]; then
      # Unknown distro: no package list, so nothing was even attempted.
      warn "$fw_name ($fw_bin) not installed - no package list for this distro"
      add_result "$fw_name" "SKIPPED" "unknown distro"
    else
      # The install ran and the binary still is not here: a failure, not a skip.
      fail "$fw_name ($fw_bin) did NOT install - see $LOG_FILE"
      add_result "$fw_name" "FAILED" "see log"
    fi
  done

  # ── Standalone Python AI CLIs via pipx (isolated, PEP 668-safe) ──
  if [ -n "${PYTHON_CMD:-}" ] && cmd_exists "$PYTHON_CMD"; then
    step "pipx + aider, llm, files-to-prompt, markitdown"
    if ! cmd_exists pipx; then
      $PKG_INSTALL pipx >>"$LOG_FILE" 2>&1 || ai_pip pipx
      run_as_user "pipx ensurepath" >>"$LOG_FILE" 2>&1 || true
    fi
    # Keep each app's exit status. A missing binary after a FAILED install is a
    # failure, not a PATH problem, and must never be softened to "restart shell".
    # PIPX_FAILED collects the RESULT names of the apps whose install failed.
    PIPX_FAILED=""
    if cmd_exists pipx; then
      for pair in "aider-chat:aider" "llm:llm" "files-to-prompt:files-to-prompt" "markitdown[all]:markitdown" "httpie:httpie"; do
        app="${pair%%:*}"
        app_name="${pair#*:}"
        if ! run_as_user "pipx install '$app'" >>"$LOG_FILE" 2>&1; then
          if ! run_as_user "pipx install --force '$app'" >>"$LOG_FILE" 2>&1; then
            PIPX_FAILED="$PIPX_FAILED $app_name"
          fi
        fi
      done
      # Refresh already-installed pipx apps to latest (keeps a re-run current)
      run_as_user "pipx upgrade-all" >>"$LOG_FILE" 2>&1 || true
    else
      warn "pipx unavailable - falling back to pip --user"
      # The same two attempts ai_pip() makes, but WITHOUT its trailing '|| true':
      # the exit status is what decides FAILED vs PARTIAL below.
      if ! run_as_user "$PYTHON_CMD -m pip install --user --upgrade aider-chat llm files-to-prompt 'markitdown[all]' httpie --quiet" >>"$LOG_FILE" 2>&1; then
        if ! run_as_user "$PYTHON_CMD -m pip install --user --break-system-packages --upgrade aider-chat llm files-to-prompt 'markitdown[all]' httpie --quiet" >>"$LOG_FILE" 2>&1; then
          PIPX_FAILED="aider llm files-to-prompt markitdown httpie"
        fi
      fi
    fi
    for probe in "aider:aider" "llm:llm" "files-to-prompt:files-to-prompt" "markitdown:markitdown" "http:httpie"; do
      fw_bin="${probe%%:*}"
      fw_name="${probe#*:}"
      if cmd_exists "$fw_bin" || run_as_user "command -v $fw_bin" >/dev/null 2>&1; then
        ok "$fw_name installed"
        add_result "$fw_name" "INSTALLED" ""
      elif list_has "$fw_name" "$PIPX_FAILED"; then
        fail "$fw_name did NOT install - see $LOG_FILE"
        add_result "$fw_name" "FAILED" "see log"
      else
        warn "$fw_name installed but is not on PATH in this shell yet"
        add_result "$fw_name" "PARTIAL" "restart shell"
      fi
    done

    # 'llm' ships with OpenAI models registered and NO Anthropic support - the
    # Claude models live in a plugin. Only run this if llm actually installed.
    if cmd_exists llm || run_as_user "command -v llm" >/dev/null 2>&1; then
      step "llm-anthropic plugin (Claude models for 'llm')"
      if run_as_user "llm install llm-anthropic" >>"$LOG_FILE" 2>&1; then LLM_PLUGIN_RC=0; else LLM_PLUGIN_RC=1; fi
      # Don't take the installer's word for it - ask llm which plugins it has.
      LLM_PLUGINS="$(run_as_user 'llm plugins' 2>/dev/null || true)"
      if printf '%s\n' "$LLM_PLUGINS" | grep -q "llm-anthropic"; then
        ok "llm-anthropic installed (try: llm -m claude-sonnet-4-5 'hello')"
        add_result "llm-anthropic" "INSTALLED" ""
      elif [ "$LLM_PLUGIN_RC" -eq 0 ]; then
        warn "llm-anthropic installed but 'llm plugins' does not list it - check it yourself"
        add_result "llm-anthropic" "PARTIAL" "run: llm plugins"
      else
        warn "llm-anthropic failed - run it yourself: llm install llm-anthropic"
        add_result "llm-anthropic" "FAILED" "see log"
      fi
    else
      info "llm not installed - skipping the llm-anthropic plugin."
      add_result "llm-anthropic" "SKIPPED" "llm missing"
    fi

    # ── ruff: Python linter + formatter ──
    # This box installs Python, a 23-package bundle and several Python CLI apps
    # but had zero Python quality tooling, while shell already gets shellcheck.
    step "ruff (Python linter + formatter)"
    if cmd_exists ruff || run_as_user "command -v ruff" >/dev/null 2>&1; then
      ok "ruff already installed"
      add_result "ruff" "PRESENT" ""
    else
      # Keep the installer's exit status so a missing 'ruff' can be told apart:
      # FAILED when the install itself failed, PARTIAL when only PATH is stale.
      RUFF_TRIED=false
      RUFF_RC=1
      if cmd_exists uv || run_as_user "command -v uv" >/dev/null 2>&1; then
        RUFF_TRIED=true
        run_as_user "uv tool install ruff" >>"$LOG_FILE" 2>&1 && RUFF_RC=0
      fi
      if ! cmd_exists ruff && cmd_exists pipx; then
        RUFF_TRIED=true
        run_as_user "pipx install ruff" >>"$LOG_FILE" 2>&1 && RUFF_RC=0
      fi
      if cmd_exists ruff || run_as_user "command -v ruff" >/dev/null 2>&1; then
        ok "ruff installed (ruff check . / ruff format .)"
        add_result "ruff" "INSTALLED" ""
      elif ! $RUFF_TRIED; then
        fail "ruff needs uv or pipx and neither is available"
        add_result "ruff" "FAILED" "no uv or pipx"
      elif [ "$RUFF_RC" -eq 0 ]; then
        warn "ruff installed but is not on PATH in this shell yet"
        add_result "ruff" "PARTIAL" "restart shell"
      else
        fail "ruff install failed - see $LOG_FILE"
        add_result "ruff" "FAILED" "see log"
      fi
    fi
  fi

  # ── npm-based helpers (Codex has its own Phase 2 prompt, not this bundle) ──
  if cmd_exists npm; then
    info "ccusage prints a usage report for your agent CLIs - deepest for Claude"
    info "Code, thinner for the others. It is not a unified bill."
    step "repomix, ccusage, ast-grep, tldr (npm)"
    # One npm call installs all four, so its exit status decides FAILED vs
    # PARTIAL for every row below. Each row still gates on its OWN command.
    if run_as_user "npm install -g repomix ccusage @ast-grep/cli tldr" >>"$LOG_FILE" 2>&1; then NPM_HELPERS_RC=0; else NPM_HELPERS_RC=1; fi
    if cmd_exists repomix || run_as_user "command -v repomix" >/dev/null 2>&1; then
      ok "repomix installed"
      add_result "repomix" "INSTALLED" ""
    elif [ "$NPM_HELPERS_RC" -eq 0 ]; then
      warn "repomix installed but is not on PATH in this shell yet"
      add_result "repomix" "PARTIAL" "restart shell"
    else
      fail "repomix install failed - see $LOG_FILE"
      add_result "repomix" "FAILED" "see log"
    fi
    if cmd_exists ccusage || run_as_user "command -v ccusage" >/dev/null 2>&1; then
      ok "ccusage installed (usage report for your agent CLIs)"
      add_result "ccusage" "INSTALLED" ""
    elif [ "$NPM_HELPERS_RC" -eq 0 ]; then
      warn "ccusage installed but is not on PATH in this shell yet"
      add_result "ccusage" "PARTIAL" "restart shell"
    else
      fail "ccusage install failed - see $LOG_FILE"
      add_result "ccusage" "FAILED" "see log"
    fi

    # ── Headless browser + document toolkit (npm) ──
    # Separate prompt on purpose: puppeteer pulls a private Chromium build
    # (~150 MB) and playwright fetches three engines on first use. Not something
    # to install silently on a metered link or a small VM disk.
    info ""
    info "Headless browser + document toolkit: read pages that block plain curl,"
    info "render HTML to PDF, merge and bookmark PDFs, audit a live site."
    info "  puppeteer + puppeteer-extra + stealth plugin, playwright,"
    info "  pdf-lib, sharp, lighthouse   (~150 MB of browser on install)"
    if ask_yn "Install the headless browser + document toolkit?"; then
      step "puppeteer, playwright, pdf-lib, sharp, lighthouse (npm)"
      if run_as_user "npm install -g puppeteer puppeteer-extra puppeteer-extra-plugin-stealth playwright pdf-lib sharp lighthouse" >>"$LOG_FILE" 2>&1; then HB_RC=0; else HB_RC=1; fi
      if cmd_exists lighthouse || run_as_user "command -v lighthouse" >/dev/null 2>&1; then
        ok "lighthouse installed"
        add_result "lighthouse" "INSTALLED" ""
      elif [ "$HB_RC" -eq 0 ]; then
        warn "lighthouse installed but is not on PATH in this shell yet"
        add_result "lighthouse" "PARTIAL" "restart shell"
      else
        fail "lighthouse install failed - see $LOG_FILE"
        add_result "lighthouse" "FAILED" "see log"
      fi
      NPM_ROOT="$(run_as_user 'npm root -g' 2>/dev/null | tail -1)"
      # These are libraries, not commands, so the check is "is the package
      # directory there". No directory after a failed npm call is a FAILURE;
      # with no npm root to look in, the honest answer is UNKNOWN.
      for lib in puppeteer puppeteer-extra puppeteer-extra-plugin-stealth playwright pdf-lib sharp; do
        if [ -n "$NPM_ROOT" ] && [ -d "$NPM_ROOT/$lib" ]; then
          ok "$lib installed"
          add_result "$lib" "INSTALLED" ""
        elif [ -z "$NPM_ROOT" ]; then
          warn "could not read 'npm root -g' - cannot verify $lib"
          add_result "$lib" "UNKNOWN" "could not verify"
        else
          fail "$lib did NOT install - see $LOG_FILE"
          add_result "$lib" "FAILED" "see log"
        fi
      done
      info "Playwright browser engines are a separate download: npx playwright install"
      info "On headless Linux they also need system libs: npx playwright install-deps"
      info "Node resolves globals from the script's own directory, not your cwd."
      info "From a standalone script, point createRequire at: ${NPM_ROOT:-\$(npm root -g)}"
    else
      add_result "Headless browser toolkit" "SKIPPED" ""
    fi
    if cmd_exists ast-grep || run_as_user "command -v ast-grep" >/dev/null 2>&1; then
      ok "ast-grep installed (structural code search/rewrite)"
      add_result "ast-grep" "INSTALLED" ""
    elif [ "$NPM_HELPERS_RC" -eq 0 ]; then
      warn "ast-grep installed but is not on PATH in this shell yet"
      add_result "ast-grep" "PARTIAL" "restart shell"
    else
      fail "ast-grep install failed - see $LOG_FILE"
      add_result "ast-grep" "FAILED" "see log"
    fi
    if cmd_exists tldr || run_as_user "command -v tldr" >/dev/null 2>&1; then
      ok "tldr installed"
      add_result "tldr" "INSTALLED" ""
    elif [ "$NPM_HELPERS_RC" -eq 0 ]; then
      warn "tldr installed but is not on PATH in this shell yet"
      add_result "tldr" "PARTIAL" "restart shell"
    else
      fail "tldr install failed - see $LOG_FILE"
      add_result "tldr" "FAILED" "see log"
    fi
  fi
else
  info "Skipped AI framework bundle. Re-run anytime to add it."
fi

# ─── Google Gemini CLI (optional, default NO) ────────────────────────────
# Its own top-level prompt, not buried two levels deep inside the framework
# bundle: declining that bundle should not silently cost you Gemini with no
# explanation. Installed from npm on every platform - the Homebrew formula is
# deprecated ("unsupported"), npm @google/gemini-cli is the maintained path.
# Default NO purely to keep the default install lean, and skipped under --auto.
if ! cmd_exists npm; then
  info "npm not available - skipping Google Gemini CLI (install Node.js first)."
  add_result "gemini-cli" "SKIPPED" "npm missing"
elif $AUTO_MODE; then
  info "Skipping Google Gemini CLI under --auto (opt-in to keep the default install lean)."
  add_result "gemini-cli" "SKIPPED" "opt-in only"
elif ask_yn "Install Google Gemini CLI? (optional - keeps the default install lean)" "n"; then
  step "Google Gemini CLI (npm @google/gemini-cli)"
  if run_as_user "npm install -g @google/gemini-cli" >>"$LOG_FILE" 2>&1; then GEMINI_RC=0; else GEMINI_RC=1; fi
  if cmd_exists gemini || run_as_user "command -v gemini" >/dev/null 2>&1; then
    ok "gemini installed (command: gemini)"
    add_result "gemini-cli" "INSTALLED" ""
  elif [ "$GEMINI_RC" -eq 0 ]; then
    warn "gemini installed but is not on PATH in this shell yet"
    tip "Close this terminal, open a new one, then run: gemini --version"
    add_result "gemini-cli" "PARTIAL" "restart shell"
  else
    fail "Google Gemini CLI install failed - see $LOG_FILE"
    add_result "gemini-cli" "FAILED" "see log"
  fi
else
  add_result "gemini-cli" "SKIPPED" ""
fi

# ─── Optional: Ollama (offline / local models) ───────────────────────────────
# HEAVY install, so it gets its own prompt and defaults to No: the runtime is a
# few hundred MB and every model you pull is another 1-5 GB on top. The model
# pull is a SECOND prompt for the same reason. Under --auto both answer No.
if ask_yn "Install Ollama to run local models offline? (large download)" "n"; then
  section "Installing Ollama (local model runner)"
  if cmd_exists ollama; then
    ok "Ollama already installed ($(ollama --version 2>/dev/null | head -1))"
    add_result "Ollama" "PRESENT" ""
  else
    step "Installing Ollama via the official installer..."
    info "Source: https://ollama.com/install.sh"
    curl -fsSL https://ollama.com/install.sh | sh >>"$LOG_FILE" 2>&1 || true
    if cmd_exists ollama; then
      ok "Ollama installed"
      add_result "Ollama" "INSTALLED" ""
    else
      fail "Ollama install failed - see $LOG_FILE"
      add_result "Ollama" "FAILED" "see log"
    fi
  fi

  if cmd_exists ollama; then
    info "The installer sets up a systemd service. If it isn't running: ollama serve"
    info "A small model is ~2 GB; 7B-class models are 4-5 GB each."
    if ask_yn "Pull a small local model now (llama3.2, ~2 GB download)?" "n"; then
      step "Pulling llama3.2 (this takes a while)"
      if run_as_user "ollama pull llama3.2" >>"$LOG_FILE" 2>&1; then OLLAMA_PULL_RC=0; else OLLAMA_PULL_RC=1; fi
      # Ask Ollama what it actually holds instead of trusting the pull's status.
      OLLAMA_MODELS="$(run_as_user 'ollama list' 2>/dev/null || true)"
      if printf '%s\n' "$OLLAMA_MODELS" | grep -q "llama3.2"; then
        ok "llama3.2 pulled (run: ollama run llama3.2)"
        add_result "Ollama model" "INSTALLED" "llama3.2"
      elif [ "$OLLAMA_PULL_RC" -eq 0 ]; then
        warn "Pull reported success but 'ollama list' does not show llama3.2"
        add_result "Ollama model" "PARTIAL" "run: ollama list"
      else
        warn "Pull failed - run 'ollama pull llama3.2' later"
        add_result "Ollama model" "FAILED" "see log"
      fi
    else
      info "No model pulled. Grab one later with: ollama pull llama3.2"
      add_result "Ollama model" "SKIPPED" ""
    fi
  fi
else
  add_result "Ollama" "SKIPPED" ""
fi

# ─── Kimi Code CLI (OPT-IN — China-operated; US-only is the default) ─────────
# Hard requirement: Kimi is NEVER installed unattended. It is NOT installed
# under --auto/--all/--everything (those set AUTO_MODE, not WITH_KIMI, and
# ask_yn returns its "n" default under AUTO_MODE), so the unattended
# "everything" path stays US-only by default.
# Interactive users who did NOT pass --with-kimi are still offered it here
# (default No). --with-kimi skips the question; --auto without the flag auto-
# answers No (ask_yn returns the default under AUTO_MODE), so it stays US-only.
# KIMI_GO records the ACTUAL opt-in decision: true only via --with-kimi or an
# explicit "yes" at the prompt. Under --auto (without --with-kimi) ask_yn
# returns its "n" default, so KIMI_GO stays false and Kimi is skipped.
KIMI_GO=false
if $WITH_KIMI; then
  KIMI_GO=true
elif ask_yn "Install Kimi Code CLI (Moonshot AI — China-operated, off by default)?" "n"; then
  KIMI_GO=true
fi

if $KIMI_GO; then
  section "Installing Kimi Code CLI (opt-in)"
  info "Kimi Code is Moonshot AI's standalone 'kimi' terminal coding agent."
  warn "Moonshot AI is a China-operated company. Both endpoints"
  warn "(api.moonshot.ai global, api.moonshot.cn mainland) are China-operated and"
  warn "there is NO published Western data-residency or zero-retention guarantee."

  if run_as_user "command -v kimi" >/dev/null 2>&1 || cmd_exists kimi; then
    ok "Kimi Code already installed ($(run_as_user 'kimi --version' 2>/dev/null | head -1 || echo 'installed'))"
    add_result "Kimi Code" "PRESENT" ""
  else
    step "Installing Kimi Code CLI via official installer (as $ACTUAL_USER)..."
    # Per-user CLI — install AS THE REAL INVOKING USER, never root.
    if run_as_user "curl -LsSf https://code.kimi.com/install.sh | bash" >>"$LOG_FILE" 2>&1; then KIMI_RC=0; else KIMI_RC=1; fi
    # Kimi installs to the user's home (~/.local/bin); make sure it's on PATH.
    export PATH="$ACTUAL_HOME/.local/bin:$PATH"
    if run_as_user "command -v kimi" >/dev/null 2>&1 || cmd_exists kimi; then
      KIMI_VER="$(run_as_user 'kimi --version' 2>/dev/null | head -1 || echo '')"
      ok "Kimi Code installed${KIMI_VER:+ ($KIMI_VER)}"
      add_result "Kimi Code" "INSTALLED" "${KIMI_VER:-opt-in}"
    elif [ "$KIMI_RC" -eq 0 ]; then
      warn "Kimi Code installed but 'kimi' is not on PATH in this shell yet"
      tip "Close this terminal, open a new one, then run: kimi --version"
      add_result "Kimi Code" "PARTIAL" "restart terminal"
    else
      fail "Kimi Code install failed - see $LOG_FILE"
      add_result "Kimi Code" "FAILED" "see log"
    fi
  fi

  # Auth + drop-in recipe + caveat (PRINT only — never auto-export anything).
  echo ""
  echo -e " ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e " ${CYAN}│ KIMI CODE AUTHENTICATION & USAGE                             │${NC}"
  echo -e " ${CYAN}├──────────────────────────────────────────────────────────────┤${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${WHITE}│ 1. Verify: kimi --version                                    │${NC}"
  echo -e " ${WHITE}│ 2. First launch: run 'kimi' -> /login -> \"Kimi Code\"         │${NC}"
  echo -e " ${WHITE}│ browser OAuth, or paste an API key                           │${NC}"
  echo -e " ${WHITE}│ 3. API key: https://platform.kimi.ai (Console -> create)     │${NC}"
  echo -e " ${GRAY}│ Account needs >= \$1 recharge to activate. The \$1             │${NC}"
  echo -e " ${GRAY}│ starter tier is only 3 RPM; for real agentic loops           │${NC}"
  echo -e " ${GRAY}│ budget the \$10 tier.                                         │${NC}"
  echo -e " ${GRAY}│                                                              │${NC}"
  echo -e " ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

  echo ""
  info "OPTIONAL: use Kimi as a drop-in model INSIDE Claude Code (no separate CLI)"
  info "via Moonshot's Anthropic-compatible endpoint. Export these in your shell:"
  tip "export ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic"
  tip "export ANTHROPIC_AUTH_TOKEN=<your Moonshot key>"
  tip "export ANTHROPIC_MODEL=kimi-k2.7-code"
  info "Flagship model id: kimi-k2.7-code (released 2026-06-16); also"
  info "kimi-k2.6, kimi-k2-thinking. 256K context."
  info "(Recipe is PRINTED only — nothing was written to your shell or env.)"

  echo ""
  warn "CAVEAT: Moonshot AI is China-operated. Neither endpoint"
  warn "(api.moonshot.ai global, api.moonshot.cn mainland) publishes a Western"
  warn "data-residency or zero-retention guarantee. Want US-only models? Just don't"
  warn "pass --with-kimi (that is the default) and Kimi is skipped entirely."
else
  info "Skipped Kimi Code at the prompt."
  add_result "Kimi Code" "SKIPPED" "declined"
fi

# ═══════════════════════════════════════════════════════════════════════════════
# AI ECOSYSTEM: MCP SERVERS & ALIGNED REPOS
# ═══════════════════════════════════════════════════════════════════════════════

section "AI Ecosystem: MCP Servers & Aligned Repos"

if cmd_exists claude || run_as_user "command -v claude" >/dev/null 2>&1; then
  if ask_yn "Register common MCP servers with Claude Code (user scope)?"; then
    run_as_user "mkdir -p '$ACTUAL_HOME/.ai-tools'"
    # npm/npx servers (need only Node) — flags BEFORE the name, command after --
    run_as_user "claude mcp add --scope user filesystem -- npx -y @modelcontextprotocol/server-filesystem '$ACTUAL_HOME'" >>"$LOG_FILE" 2>&1 || true
    run_as_user "claude mcp add --scope user memory -e MEMORY_FILE_PATH='$ACTUAL_HOME/.ai-tools/memory.json' -- npx -y @modelcontextprotocol/server-memory" >>"$LOG_FILE" 2>&1 || true
    run_as_user "claude mcp add --scope user sequential-thinking -- npx -y @modelcontextprotocol/server-sequential-thinking" >>"$LOG_FILE" 2>&1 || true
    # python/uvx servers (need uv) — fetch + time live on PyPI, not npm
    if cmd_exists uvx || run_as_user "command -v uvx" >/dev/null 2>&1; then
      run_as_user "claude mcp add --scope user fetch -- uvx mcp-server-fetch" >>"$LOG_FILE" 2>&1 || true
      run_as_user "claude mcp add --scope user time -- uvx mcp-server-time" >>"$LOG_FILE" 2>&1 || true
    else
      info "uvx not found - skipping Python MCP servers (fetch, time). Install uv to add them."
    fi
    run_as_user "claude mcp add --scope user playwright -- npx -y @playwright/mcp@latest" >>"$LOG_FILE" 2>&1 || true
    MCP_EXPECTED="filesystem memory sequential-thinking playwright"
    if cmd_exists uvx || run_as_user "command -v uvx" >/dev/null 2>&1; then
      MCP_EXPECTED="$MCP_EXPECTED fetch time"
    fi

    # GitHub's own hosted MCP server (HTTP transport). Do NOT use the npm
    # package @modelcontextprotocol/server-github - npm flags it DEPRECATED.
    # This script CANNOT finish the OAuth step, so it is reported separately.
    # --scope user like every other add above: without it the server lands in
    # Claude Code's default LOCAL scope, which is keyed to whatever directory
    # this installer happened to run from, so it would not exist in your real
    # projects.
    run_as_user "claude mcp add --scope user --transport http github https://api.githubcopilot.com/mcp/" >>"$LOG_FILE" 2>&1 || true
    MCP_EXPECTED="$MCP_EXPECTED github"
    MCP_NEEDS_AUTH="github"

    # Every add above ends in || true, so DON'T claim success - ask Claude Code
    # which servers it actually has. 'claude mcp list' prints "name: command".
    step "Verifying MCP registration (claude mcp list)"
    info "This can take a minute: 'claude mcp list' health-checks every server"
    info "by LAUNCHING it, which means cold npx/uvx downloads plus one HTTP"
    info "handshake. It is bounded to 90s, and skipped outright when nothing can"
    info "bound it, so it can never hang the installer."
    # There is no unbounded path here on purpose. Without a timeout utility the
    # verification is SKIPPED rather than run bare, because an unbounded
    # 'claude mcp list' is the exact hang this guard exists to prevent.
    MCP_LIST_CMD=""
    if cmd_exists timeout; then
      MCP_LIST_CMD="timeout 90 claude mcp list"
    elif cmd_exists gtimeout; then
      MCP_LIST_CMD="gtimeout 90 claude mcp list"
    fi
    # Capture the exit status BEFORE any || true - appending "|| true" to the
    # assignment makes $? always 0. timeout exits 124 when it kills the
    # command, and a timeout is UNKNOWN: never FAILED, never INSTALLED.
    MCP_LIST=""
    MCP_RC=0
    if [ -n "$MCP_LIST_CMD" ]; then
      MCP_LIST="$(run_as_user "$MCP_LIST_CMD" 2>/dev/null)"
      MCP_RC=$?
      log "claude mcp list rc=$MCP_RC"
    else
      log "no timeout/gtimeout - skipped 'claude mcp list' instead of running it unbounded"
    fi
    if [ -z "$MCP_LIST_CMD" ]; then
      warn "No 'timeout' (or 'gtimeout') on this box - MCP verification SKIPPED."
      info "'claude mcp list' starts every server to health-check it and can run"
      info "for minutes, so this installer never calls it without a time bound."
      info "Nothing was undone; the servers above were registered."
      tip "Check them yourself with: claude mcp list"
      tip "Or install coreutils (which provides 'timeout') and re-run this script."
      add_result "MCP servers" "UNKNOWN" "no timeout utility"
      add_result "MCP github" "UNKNOWN" "no timeout utility"
    elif [ "$MCP_RC" -eq 124 ]; then
      warn "'claude mcp list' did not finish within 90s - could not verify."
      info "Nothing was undone; the servers above may well be registered."
      tip "Check it yourself later with: claude mcp list"
      add_result "MCP servers" "UNKNOWN" "could not verify"
      add_result "MCP github" "UNKNOWN" "could not verify"
    else
      # github gets its own row below ("registered, needs /mcp login"), so it
      # is routed into MCP_AUTH and deliberately kept OUT of the MCP_OK pool
      # that the aggregate INSTALLED row counts. Never report it twice.
      MCP_OK=""
      MCP_MISSING=""
      MCP_AUTH=""
      for srv in $MCP_EXPECTED; do
        if printf '%s\n' "$MCP_LIST" | grep -qE "(^|[[:space:]])${srv}:"; then
          case " $MCP_NEEDS_AUTH " in
          *" $srv "*) MCP_AUTH="$MCP_AUTH $srv" ;;
          *) MCP_OK="$MCP_OK $srv" ;;
          esac
        else
          MCP_MISSING="$MCP_MISSING $srv"
        fi
      done

      if [ -n "$MCP_OK" ]; then
        ok "MCP servers listed by Claude Code:$MCP_OK"
        add_result "MCP servers" "INSTALLED" "$(echo $MCP_OK | tr ' ' ',')"
      else
        fail "No MCP servers showed up in 'claude mcp list'"
        info "Log: $LOG_FILE"
        add_result "MCP servers" "FAILED" "none listed"
      fi
      if [ -n "$MCP_AUTH" ]; then
        warn "Registered but NOT authorised yet:$MCP_AUTH"
        info "GitHub's MCP server uses OAuth, which this script cannot complete."
        tip "Run 'claude', type /mcp, pick github, and authorise it in the browser."
        add_result "MCP github" "PARTIAL" "registered, /mcp login"
      fi
      if [ -n "$MCP_MISSING" ]; then
        fail "MCP servers that did NOT register:$MCP_MISSING"
        info "See the 'claude mcp add --scope user' output in $LOG_FILE"
        add_result "MCP missing" "FAILED" "$(echo $MCP_MISSING | tr ' ' ',')"
      fi
    fi
  else
    add_result "MCP servers" "SKIPPED" ""
  fi
else
  info "Claude Code not installed - skipping MCP server registration."
fi

echo ""
info "Aligned GitHub repos worth bookmarking:"
info "  awesome-claude-code     https://github.com/hesreallyhim/awesome-claude-code"
info "  awesome-mcp-servers     https://github.com/punkpeye/awesome-mcp-servers"
info "  MCP reference servers   https://github.com/modelcontextprotocol/servers"
info "  OpenAI Codex CLI        https://github.com/openai/codex"
info "  Aider                   https://github.com/Aider-AI/aider"
if cmd_exists git && ask_yn "Clone these reference repos into ~/ai-tools?" "n"; then
  AI_TOOLS_DIR="$ACTUAL_HOME/ai-tools"
  run_as_user "mkdir -p '$AI_TOOLS_DIR'"
  REPOS_OK=0
  REPOS_FAIL=0
  for repo in \
    "https://github.com/hesreallyhim/awesome-claude-code.git" \
    "https://github.com/punkpeye/awesome-mcp-servers.git" \
    "https://github.com/modelcontextprotocol/servers.git" \
    "https://github.com/openai/codex.git" \
    "https://github.com/Aider-AI/aider.git"; do
    rname="$(basename "$repo" .git)"
    if [ -d "$AI_TOOLS_DIR/$rname/.git" ]; then
      ok "$rname already cloned"
      REPOS_OK=$((REPOS_OK + 1))
    elif run_as_user "git clone --depth 1 '$repo' '$AI_TOOLS_DIR/$rname'" >>"$LOG_FILE" 2>&1; then
      ok "cloned $rname"
      REPOS_OK=$((REPOS_OK + 1))
    else
      warn "clone failed: $rname"
      REPOS_FAIL=$((REPOS_FAIL + 1))
    fi
  done
  # Don't report five repos as cloned when some of them failed.
  if [ "$REPOS_FAIL" -eq 0 ]; then
    add_result "Aligned repos" "INSTALLED" "$AI_TOOLS_DIR"
  elif [ "$REPOS_OK" -gt 0 ]; then
    add_result "Aligned repos" "PARTIAL" "$REPOS_OK ok, $REPOS_FAIL failed"
  else
    add_result "Aligned repos" "FAILED" "see log"
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 3: NETLIFY CLI & DEPLOYMENT TOOLS
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Deployment & Extras"
section "PHASE 3: Netlify CLI & Deployment"

step "Checking Netlify CLI"
if cmd_exists netlify; then
  ok "Netlify CLI already installed ($(netlify --version 2>/dev/null))"
  add_result "Netlify CLI" "PRESENT" ""
else
  info "Netlify CLI lets you deploy websites directly from your terminal."
  info "Free tier: 100GB bandwidth, 300 build minutes/month, custom domains."
  if ask_yn "Install Netlify CLI?"; then
    if cmd_exists npm; then
      if run_as_user "npm install -g netlify-cli" >>"$LOG_FILE" 2>&1; then NETLIFY_RC=0; else NETLIFY_RC=1; fi
      export PATH="${NPM_GLOBAL:-$ACTUAL_HOME/.npm-global}/bin:$PATH"
      if run_as_user "command -v netlify" &>/dev/null || cmd_exists netlify; then
        ok "Netlify CLI installed"
        add_result "Netlify CLI" "INSTALLED" ""
      elif [ "$NETLIFY_RC" -eq 0 ]; then
        warn "Netlify CLI installed but needs a terminal restart for PATH"
        add_result "Netlify CLI" "PARTIAL" "restart terminal"
      else
        fail "Netlify CLI install failed - see $LOG_FILE"
        add_result "Netlify CLI" "FAILED" "see log"
      fi
    else
      fail "npm not available - install Node.js first"
      add_result "Netlify CLI" "FAILED" "npm missing"
    fi
  else
    add_result "Netlify CLI" "SKIPPED" ""
  fi
fi

# Netlify authentication guide
echo ""
echo -e " ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
echo -e " ${CYAN}│ NETLIFY AUTHENTICATION & DEPLOYMENT                          │${NC}"
echo -e " ${CYAN}├──────────────────────────────────────────────────────────────┤${NC}"
echo -e " ${GRAY}│                                                              │${NC}"
echo -e " ${WHITE}│ LOGIN:                                                       │${NC}"
echo -e " ${WHITE}│ netlify login                                                │${NC}"
echo -e " ${WHITE}│ (browser opens -> click Authorize -> return to terminal)     │${NC}"
echo -e " ${GRAY}│                                                              │${NC}"
echo -e " ${WHITE}│ FIRST DEPLOY (link a folder to a Netlify site):              │${NC}"
echo -e " ${WHITE}│ cd /path/to/my-website                                       │${NC}"
echo -e " ${WHITE}│ netlify init                                                 │${NC}"
echo -e " ${WHITE}│ netlify deploy # preview draft                               │${NC}"
echo -e " ${WHITE}│ netlify deploy --prod # push live                            │${NC}"
echo -e " ${GRAY}│                                                              │${NC}"
echo -e " ${WHITE}│ AI-POWERED DEPLOY (use Claude to edit then deploy):          │${NC}"
echo -e " ${WHITE}│ cd /path/to/my-website                                       │${NC}"
echo -e " ${WHITE}│ claude                                                       │${NC}"
echo -e " ${WHITE}│ > fix the broken nav menu and deploy a draft to Netlify      │${NC}"
echo -e " ${GRAY}│                                                              │${NC}"
echo -e " ${WHITE}│ ENVIRONMENT VARIABLES:                                       │${NC}"
echo -e " ${WHITE}│ netlify env:set MY_VAR \"value\"                               │${NC}"
echo -e " ${WHITE}│ netlify env:list                                             │${NC}"
echo -e " ${GRAY}│                                                              │${NC}"
echo -e " ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

if cmd_exists netlify || run_as_user "command -v netlify" &>/dev/null; then
  if ask_yn "Log in to Netlify now?" "n"; then
    run_as_user "netlify login"
  fi
fi

# ─── Cloudflare Wrangler (Pages + Workers) ───────────────────────────────────
step "Checking Cloudflare Wrangler"
if cmd_exists wrangler || run_as_user "command -v wrangler" &>/dev/null; then
  ok "Wrangler already installed"
  add_result "Cloudflare Wrangler" "PRESENT" ""
else
  info "Wrangler deploys to Cloudflare Pages (static sites) and Workers (edge)."
  if ask_yn "Install Cloudflare Wrangler?"; then
    if cmd_exists npm; then
      if run_as_user "npm install -g wrangler" >>"$LOG_FILE" 2>&1; then WRANGLER_RC=0; else WRANGLER_RC=1; fi
      if cmd_exists wrangler || run_as_user "command -v wrangler" &>/dev/null; then
        ok "Wrangler installed"
        add_result "Cloudflare Wrangler" "INSTALLED" ""
      elif [ "$WRANGLER_RC" -eq 0 ]; then
        warn "Wrangler installed but needs a terminal restart for PATH"
        add_result "Cloudflare Wrangler" "PARTIAL" "restart terminal"
      else
        fail "Wrangler install failed - see $LOG_FILE"
        add_result "Cloudflare Wrangler" "FAILED" "see log"
      fi
    else
      fail "npm not available - install Node.js first"
      add_result "Cloudflare Wrangler" "FAILED" "npm missing"
    fi
  else
    add_result "Cloudflare Wrangler" "SKIPPED" ""
  fi
fi
info "Cloudflare deploy quickstart:"
tip "wrangler login                                     # browser OAuth"
tip "wrangler pages deploy ./dist --project-name=NAME   # static site -> Pages"
tip "wrangler deploy                                    # deploy a Worker (edge)"
tip "Headless/CI: export CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID"
if cmd_exists wrangler || run_as_user "command -v wrangler" &>/dev/null; then
  if ask_yn "Log in to Cloudflare now?" "n"; then run_as_user "wrangler login"; fi
fi

# ─── Vercel CLI ───────────────────────────────────────────────────────────────
step "Checking Vercel CLI"
if cmd_exists vercel || run_as_user "command -v vercel" &>/dev/null; then
  ok "Vercel CLI already installed"
  add_result "Vercel CLI" "PRESENT" ""
else
  info "Vercel is another popular hosting platform (like Netlify)."
  if ask_yn "Install Vercel CLI?" "n"; then
    if cmd_exists npm; then
      if run_as_user "npm install -g vercel" >>"$LOG_FILE" 2>&1; then VERCEL_RC=0; else VERCEL_RC=1; fi
      if cmd_exists vercel || run_as_user "command -v vercel" &>/dev/null; then
        ok "Vercel CLI installed"
        add_result "Vercel CLI" "INSTALLED" ""
      elif [ "$VERCEL_RC" -eq 0 ]; then
        warn "Vercel CLI installed but needs a terminal restart for PATH"
        add_result "Vercel CLI" "PARTIAL" "restart terminal"
      else
        fail "Vercel CLI install failed - see $LOG_FILE"
        add_result "Vercel CLI" "FAILED" "see log"
      fi
    else
      fail "npm not available"
      add_result "Vercel CLI" "FAILED" "npm missing"
    fi
  else
    add_result "Vercel CLI" "SKIPPED" ""
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 4: ENVIRONMENT HEALTH CHECK
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Validation & Health Check"
section "PHASE 4: Environment Health Check"

echo ""
printf " ${WHITE}%-18s %-10s %-35s${NC}\n" "Component" "Status" "Version / Details"
printf " %-18s %-10s %-35s\n" "──────────────────" "──────────" "───────────────────────────────────"

# ─── Functional Validation (not just version checks) ──────────────────────────
step "Running functional validation tests"

# Test Node.js can execute JavaScript
if cmd_exists node; then
  NODE_TEST=$(run_as_user "node -e \"console.log('node-ok')\"" 2>/dev/null || true)
  if [[ "$NODE_TEST" == *"node-ok"* ]]; then
    ok "Node.js exec: PASS"
  else
    warn "Node.js exec: FAIL - node installed but cannot run JS"
  fi
fi

# Test Python SSL and package imports
if [ -n "${PYTHON_CMD:-}" ] && cmd_exists "$PYTHON_CMD"; then
  PY_SSL=$(run_as_user "$PYTHON_CMD -c \"import ssl; print('py-ssl-ok:', ssl.OPENSSL_VERSION)\"" 2>/dev/null || true)
  if [[ "$PY_SSL" == *"py-ssl-ok"* ]]; then
    ok "Python SSL: PASS (${PY_SSL#*: })"
  else
    warn "Python SSL: FAIL - HTTPS requests may not work"
  fi

  PY_IMPORT=$(run_as_user "$PYTHON_CMD -c \"import requests, pydantic; print('imports-ok')\"" 2>/dev/null || true)
  if [[ "$PY_IMPORT" == *"imports-ok"* ]]; then
    ok "Python packages: PASS"
  else
    warn "Python packages: FAIL - some packages not importable"
  fi
fi

# Test npm registry connectivity
if cmd_exists npm; then
  # Capture the exit status BEFORE any || true - appending "|| true" to the
  # assignment makes $? always 0, so the old check could never fail.
  NPM_PING=$(run_as_user "npm ping" 2>&1)
  NPM_RC=$?
  log "npm ping rc=$NPM_RC: $NPM_PING"
  if [ "$NPM_RC" -eq 0 ]; then
    ok "npm registry: PASS"
  else
    warn "npm registry: FAIL - npm cannot reach registry (rc=$NPM_RC)"
  fi
fi

# Test Git HTTPS connectivity
if cmd_exists git; then
  GIT_LS=$(run_as_user "git ls-remote --heads https://github.com/anthropics/claude-code.git" 2>/dev/null || true)
  if [ -n "$GIT_LS" ]; then
    ok "Git HTTPS: PASS"
  else
    warn "Git HTTPS: FAIL - git cannot reach GitHub"
  fi
fi

echo ""

# Name:command. The command may contain `||` fallbacks where the binary name
# differs by distro or major version (fd/fdfind, magick/convert, bat/batcat).
CHECKS=(
  "Git:git --version"
  "Node.js:node --version"
  "npm:npm --version"
  "npx:npx --version"
  "Python:python3 --version"
  "pip:python3 -m pip --version"
  "pipx:pipx --version"
  "uv:uv --version"
  "Python AI libs:python3 -c \"import anthropic, mcp, pandas; print('anthropic, mcp, pandas OK')\""
  "Claude Code:claude --version"
  "OpenAI Codex:codex --version"
  # sgpt reads a key from /dev/tty on first run, which no stdin redirect can
  # prevent, so this row checks presence only - never 'sgpt --version'.
  "ChatGPT (sgpt):command -v sgpt"
  "Copilot CLI:copilot --version"
  "Kimi Code:kimi --version"
  "gemini-cli:gemini --version"
  "Ollama:ollama --version"
  "GitHub CLI:gh --version"
  "Netlify CLI:netlify --version"
  "Cloudflare Wrangler:wrangler --version"
  "Vercel CLI:vercel --version"
  "aider:aider --version"
  "llm:llm --version"
  "files-to-prompt:files-to-prompt --version"
  "markitdown:markitdown --version"
  "httpie:http --version"
  "repomix:repomix --version"
  "ccusage:ccusage --version"
  "ast-grep:ast-grep --version"
  "tldr:tldr --version"
  "ruff:ruff --version"
  "lighthouse:lighthouse --version"
  "ripgrep:rg --version"
  "jq:jq --version"
  "fd:fd --version || fdfind --version"
  "yq:yq --version"
  "gron:gron --version"
  "jc:jc -v"
  "glow:glow --version"
  "just:just --version"
  "tree:tree --version"
  "delta:delta --version"
  "ImageMagick:magick --version || convert --version"
  "GhostScript:gs --version"
  "FFmpeg:ffmpeg -version"
  "pandoc:pandoc --version"
  # pdftotext and tesseract print their version on stderr, so they need 2>&1 -
  # guarded by command -v first, otherwise "command not found" itself would be
  # captured and reported as a PASS.
  "pdftotext:command -v pdftotext >/dev/null 2>&1 && pdftotext -v 2>&1"
  "tesseract:command -v tesseract >/dev/null 2>&1 && tesseract --version 2>&1"
  "exiftool:exiftool -ver"
  "qpdf:qpdf --version"
  "yt-dlp:yt-dlp --version"
  "tmux:tmux -V"
  "sqlite3:sqlite3 --version"
  "bat:bat --version || batcat --version"
  "fzf:fzf --version"
  "shellcheck:shellcheck --version"
)

PASS_COUNT=0
FAIL_COUNT=0

for check in "${CHECKS[@]}"; do
  name="${check%%:*}"
  cmd="${check#*:}"
  padded_name=$(printf "%-18s" "$name")

  ver=$(run_as_user "$cmd" 2>/dev/null | head -1 || true)
  if [ -n "$ver" ]; then
    truncated="${ver:0:35}"
    printf " %s ${GREEN}%-10s${NC} ${GRAY}%s${NC}\n" "$padded_name" "PASS" "$truncated"
    PASS_COUNT=$((PASS_COUNT + 1))
  else
    printf " %s ${RED}%-10s${NC} ${GRAY}%s${NC}\n" "$padded_name" "MISSING" "Not installed"
    FAIL_COUNT=$((FAIL_COUNT + 1))
  fi
done

echo ""
echo -e " ─────────────────────────────────────────────────────────────────"
if [ "$FAIL_COUNT" -eq 0 ]; then
  echo -e " ${GREEN}Total: $PASS_COUNT passed, $FAIL_COUNT missing${NC}"
else
  echo -e " ${YELLOW}Total: $PASS_COUNT passed, $FAIL_COUNT missing${NC}"
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 5: QUICK REFERENCE GUIDE
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Complete"
section "PHASE 5: Quick Reference Guide"

echo -e "${CYAN}"
cat <<'GUIDE'

 ┌─────────────────────────────────────────────────────────────────┐
 │ HOW TO USE YOUR AI TERMINALS                                    │
 ├─────────────────────────────────────────────────────────────────┤
 │                                                                 │
 │ CLAUDE CODE (Best for coding, files, agents)                    │
 │ ─────────────────────────────────────────────                   │
 │ claude Start interactive session                                │
 │ claude "fix this bug" One-shot command                          │
 │ claude doctor Diagnose configuration                            │
 │ /help In-session help                                           │
 │                                                                 │
 │ First time: run 'claude' -> browser opens -> log in ->          │
 │ click Authorize -> return to terminal -> start coding!          │
 │                                                                 │
 │ OPENAI CODEX (Best for OpenAI-model agentic coding)             │
 │ ─────────────────────────────────────────────                   │
 │ codex Start interactive session                                 │
 │ Sign in with ChatGPT, or set OPENAI_API_KEY first               │
 │                                                                 │
 │ CHATGPT TERMINAL (Best for quick questions in the shell)        │
 │ ─────────────────────────────────────────                       │
 │ sgpt "why is this failing?" One-shot question                   │
 │ sgpt --shell "find files >100MB" Suggest a shell command        │
 │ sgpt --code "python: read a csv" Code-only answer               │
 │ Reads OPENAI_API_KEY; asks for it on the first run              │
 │                                                                 │
 │ GITHUB COPILOT CLI (Best for shell commands)                    │
 │ ─────────────────────────────────────────────                   │
 │ copilot Start interactive session                               │
 │ copilot -p "..." One-shot prompt                                │
 │ gh auth login Log in to GitHub first                            │
 │ gh auth setup-git Let git push with that login                  │
 │                                                                 │
 │ NETLIFY DEPLOYMENT                                              │
 │ ──────────────────                                              │
 │ netlify login Authenticate                                      │
 │ cd /path/to/site                                                │
 │ netlify init Link folder to Netlify site                        │
 │ netlify deploy Preview deploy                                   │
 │ netlify deploy --prod Production deploy                         │
 │                                                                 │
 │ AI + DEPLOY WORKFLOW (the magic combo)                          │
 │ ─────────────────────────────────────                           │
 │ cd /path/to/site && claude                                      │
 │ > make the hero section more engaging, then deploy              │
 │ a draft to Netlify so I can preview it                          │
 │                                                                 │
 │ GUI & WEB INTERFACES (when you prefer a visual tool)            │
 │ ───────────────────────────────────────────────                 │
 │ Claude web app: https://claude.ai/code                          │
 │ Claude VS Code: Extensions > search "Claude Code"               │
 │ ChatGPT web: https://chatgpt.com                                │
 │ ChatGPT desktop: https://openai.com/chatgpt/download            │
 │ Copilot VS Code: Extensions > "GitHub Copilot"                  │
 │ Netlify GUI: https://app.netlify.com                            │
 │                                                                 │
 │ APP CONNECTORS (via Claude Code MCP Servers)                    │
 │ ────────────────────────────────────────────                    │
 │ claude mcp add --scope user gdrive -- npx @anthropic/gdrive-mcp │
 │ claude mcp add --scope user slack -- npx @anthropic/slack-mcp   │
 │ claude mcp add --scope user --transport http github \           │
 │ https://api.githubcopilot.com/mcp/                              │
 │ (then run /mcp inside Claude Code to authorise GitHub)          │
 │ claude mcp add --scope user playwright -- npx \                 │
 │ @playwright/mcp@latest                                          │
 │ claude mcp list # see all connected services                    │
 │                                                                 │
 │ COMMON TIPS                                                     │
 │ ───────────                                                     │
 │ - Always cd into your project folder BEFORE starting claude     │
 │ - Be specific in prompts for better results                     │
 │ - Open a NEW terminal after install for PATH changes            │
 │ - Run 'claude doctor' if something seems broken                 │
 │                                                                 │
 └─────────────────────────────────────────────────────────────────┘

GUIDE
echo -e "${NC}"

# ═══════════════════════════════════════════════════════════════════════════════
# FINAL SUMMARY
# ═══════════════════════════════════════════════════════════════════════════════

ELAPSED=$((SECONDS / 60))m$((SECONDS % 60))s

echo -e " ${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
echo -e " ${GREEN}║ KICKSTART COMPLETE                                                ║${NC}"
echo -e " ${GREEN}╠═══════════════════════════════════════════════════════════════════╣${NC}"
echo -e " ${GREEN}║                                                                   ║${NC}"

printf " ${GREEN}║${NC} ${WHITE}%-22s %-12s %-25s${NC}     ${GREEN}║${NC}\n" "Component" "Status" "Detail"
printf " ${GREEN}║${NC} %-22s %-12s %-25s     ${GREEN}║${NC}\n" "──────────────────────" "────────────" "─────────────────────────"

for i in "${!RESULT_NAMES[@]}"; do
  name="${RESULT_NAMES[$i]:0:22}"
  status="${RESULT_STATUS[$i]}"
  detail="${RESULT_DETAIL[$i]:0:25}"

  case "$status" in
  INSTALLED | PRESENT | RAN | OK) color="$GREEN" ;;
  SKIPPED | PARTIAL) color="$YELLOW" ;;
  FAILED | ERROR) color="$RED" ;;
  *) color="$GRAY" ;;
  esac

  printf " ${GREEN}║${NC} ${color}%-22s %-12s${NC} ${GRAY}%-25s${NC}     ${GREEN}║${NC}\n" "$name" "$status" "$detail"
done

echo -e " ${GREEN}║                                                                   ║${NC}"
echo -e " ${GREEN}║ Time elapsed: $ELAPSED$(printf '%*s' $((52 - ${#ELAPSED})) '')${GREEN}║${NC}"
echo -e " ${GREEN}║ Log file : $LOG_FILE${GREEN}$(printf '%*s' $((55 - ${#LOG_FILE})) '')║${NC}"
echo -e " ${GREEN}║                                                                   ║${NC}"
echo -e " ${GREEN}║ NEXT STEPS:                                                       ║${NC}"
echo -e " ${GREEN}║ 1. CLOSE this terminal and open a NEW one (PATH refresh)          ║${NC}"
echo -e " ${GREEN}║ 2. Run 'claude' to start Claude Code (browser auth first time)    ║${NC}"
echo -e " ${GREEN}║ 3. Run 'gh auth login' then 'gh auth setup-git' for GitHub        ║${NC}"
echo -e " ${GREEN}║ 4. Run 'netlify login' to authenticate Netlify                    ║${NC}"
echo -e " ${GREEN}║ 5. cd into your project folder, then 'claude' to start!           ║${NC}"
echo -e " ${GREEN}║                                                                   ║${NC}"
echo -e " ${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
echo ""

Full macOS script

Expand start-ai-terminal-kickstart-macos.sh — macOS / Homebrew edition (zsh, Apple Silicon + Intel)
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════════════
# AI Terminal Kickstart - macOS Edition
# Supports: macOS 12 Monterey and newer, Apple Silicon (arm64) + Intel (x86_64)
#
# One script to prep a Mac for AI-powered terminal work:
#   1. Claude Code (Anthropic)
#   2. OpenAI Codex CLI
#   3. ChatGPT terminal (shell-gpt)
#   4. GitHub Copilot CLI (GitHub)
#   + Netlify CLI for direct site deployment
#   + AI frameworks & conversion toolkit (aider, llm, repomix, gemini-cli,
#     files-to-prompt, markitdown, pandoc, poppler, tesseract, yt-dlp, jc, glow ...)
#
# Usage:
#   chmod +x start-ai-terminal-kickstart-macos.sh
#   ./start-ai-terminal-kickstart-macos.sh                 # interactive
#   ./start-ai-terminal-kickstart-macos.sh --auto          # unattended (yes to all)
#   ./start-ai-terminal-kickstart-macos.sh --with-ollama   # also install local-model runner
#   ./start-ai-terminal-kickstart-macos.sh --with-office   # also install LibreOffice (doc conversions)
#   ./start-ai-terminal-kickstart-macos.sh --with-kimi     # also install Kimi Code CLI (Moonshot AI, China-operated; OFF by default)
#
# IMPORTANT: Do NOT run with sudo. Homebrew refuses to run as root and will
# abort. The script installs into your user-owned Homebrew prefix and never
# needs elevation (Homebrew itself may prompt once for your password when it
# first creates its directories — that is expected).
#
# Author : AI Terminal ops
# Date   : 2026-08-18
# ═══════════════════════════════════════════════════════════════════════════════

set -uo pipefail
# NOTE: -e (errexit) is intentionally omitted. This script uses manual error
# handling with || true and explicit checks. set -e causes false exits on
# arithmetic returning 0 and on optional package installs failing.
#
# NOTE: written to run on the bash that ships with macOS (3.2). No bash-4+
# features (associative arrays, mapfile, ${var^^}) are used anywhere.

# ─── Configuration ────────────────────────────────────────────────────────────

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Private, unpredictable temp dir (created below). Installer output can be
# verbose, so keep it out of a world-writable, predictable /tmp path.
TEMP_DIR=""
LOG_FILE=""
AUTO_MODE=false
WITH_OLLAMA=false
WITH_OFFICE=false
WITH_KIMI=false
ACTUAL_USER="$USER"
ACTUAL_HOME="$HOME"
ARCH="$(uname -m)"

# Parse arguments
for arg in "$@"; do
  case "$arg" in
    --auto|-a|--all|--everything)  AUTO_MODE=true ;;
    --with-ollama)  WITH_OLLAMA=true ;;
    --with-office)  WITH_OFFICE=true ;;
    --with-kimi)  WITH_KIMI=true ;;
    --help|-h)
      echo "Usage: $0 [--auto|--all] [--with-ollama] [--with-office] [--with-kimi]"
      echo "  --auto, --all   Install the full recommended stack, no prompts"
      echo "  --with-ollama   Also install Ollama + a small local model"
      echo "  --with-office   Also install LibreOffice (for document conversions)"
      echo "  --with-kimi     Also install Kimi Code CLI (Moonshot AI, China-operated; OFF by default)"
      exit 0
      ;;
  esac
done

TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ai-kickstart-macos.XXXXXX" 2>/dev/null)" || TEMP_DIR="/tmp/ai-kickstart-macos-$$"
mkdir -p "$TEMP_DIR" 2>/dev/null || true
chmod 700 "$TEMP_DIR" 2>/dev/null || true
LOG_FILE="$TEMP_DIR/kickstart.log"
( umask 077; : > "$LOG_FILE" )

# Node.js LTS major — the ONLY pinned version in this script (a ~yearly stability
# knob). Every AI tool below installs its latest published release, so the script
# never needs editing when those tools ship new versions.
NODE_MAJOR="22"

# ─── Helper Functions ────────────────────────────────────────────────────────

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
GRAY='\033[0;37m'
NC='\033[0m' # No Color

log()     { echo "[$(date +%H:%M:%S)] $*" >> "$LOG_FILE"; }
section() { echo -e "\n  ${CYAN}=================================================================${NC}"; echo -e "  ${CYAN}$1${NC}"; echo -e "  ${CYAN}=================================================================${NC}"; log "SECTION: $1"; }
step()    { echo -e "\n  ${CYAN}>> $1${NC}"; log "STEP: $1"; }
ok()      { echo -e "  ${GREEN}[OK]${NC} $1"; log "OK: $1"; }
warn()    { echo -e "  ${YELLOW}[WARN]${NC} $1"; log "WARN: $1"; }
fail()    { echo -e "  ${RED}[FAIL]${NC} $1"; log "FAIL: $1"; }
info()    { echo -e "  ${GRAY}$1${NC}"; }
tip()     { echo -e "  ${YELLOW}[TIP]${NC} $1"; }

cmd_exists() { command -v "$1" >/dev/null 2>&1; }

# True if ANY of the space-separated candidate commands exists (handles tools
# whose binary name differs across installers, e.g. "fd" vs "fdfind").
cmd_exists_any() {
  local c
  for c in $1; do
    command -v "$c" >/dev/null 2>&1 && return 0
  done
  return 1
}

# ─── Progress bar ────────────────────────────────────────────────────────────

TOTAL_PHASES=6
CURRENT_PHASE=0

show_progress() {
  local label="$1"
  CURRENT_PHASE=$((CURRENT_PHASE + 1))
  local pct=$((CURRENT_PHASE * 100 / TOTAL_PHASES))
  local filled=$((30 * CURRENT_PHASE / TOTAL_PHASES))
  local empty=$((30 - filled))
  local bar=""; local space=""; local i=0
  while [ "$i" -lt "$filled" ]; do bar="${bar}█"; i=$((i + 1)); done
  i=0
  while [ "$i" -lt "$empty" ]; do space="${space}░"; i=$((i + 1)); done
  printf "\r  ${CYAN}[%s%s] %3d%% - Phase %d/%d: %s${NC}    \n" \
    "$bar" "$space" "$pct" "$CURRENT_PHASE" "$TOTAL_PHASES" "$label"
}

# ─── Prompt helper ───────────────────────────────────────────────────────────

ask_yn() {
  local prompt="$1"
  local default="${2:-y}"
  if $AUTO_MODE; then
    [ "$default" = "y" ] && return 0 || return 1
  fi
  local hint="Y/n"
  [ "$default" = "n" ] && hint="y/N"
  echo -ne "  ${YELLOW}$prompt ($hint): ${NC}"
  # Read the answer from the terminal, not from stdin. Callers run ask_yn inside
  # `while read ... done <<< "$table"` loops, and a bare `read` there consumes the
  # NEXT TABLE ROW as the user's answer - silently skipping half the table.
  if [ -r /dev/tty ]; then
    read -r answer < /dev/tty
  else
    read -r answer
  fi
  answer="${answer:-$default}"
  [[ "$answer" =~ ^[Yy] ]]
}

# Center text inside the banner's inner width (ASCII text only).
BANNER_W=67
center() {
  local s="$1"
  local len=${#s}
  local total=$((BANNER_W - len))
  [ "$total" -lt 0 ] && total=0
  local left=$((total / 2))
  local right=$((total - left))
  printf "%*s%s%*s" "$left" "" "$s" "$right" ""
}

# Append a line to the shell profile once (idempotent on a marker).
add_to_profile() {
  local line="$1"
  local marker="$2"
  touch "$PROFILE_FILE" 2>/dev/null || true
  if ! grep -qF "$marker" "$PROFILE_FILE" 2>/dev/null; then
    printf '\n%s\n' "$line" >> "$PROFILE_FILE"
  fi
}

# Write (or REPLACE) an `export VAR="value"` line, anchored to the assignment so
# a rotated key actually takes effect on a re-run (a plain marker check would
# keep the stale key). The profile now holds a secret, so lock it to mode 600.
add_to_profile_kv() {
  local var="$1"
  local val="$2"
  touch "$PROFILE_FILE" 2>/dev/null || true
  if grep -q "^export ${var}=" "$PROFILE_FILE" 2>/dev/null; then
    local tmp
    tmp="$(mktemp)"
    grep -v "^export ${var}=" "$PROFILE_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$PROFILE_FILE"
  fi
  printf '\nexport %s="%s"\n' "$var" "$val" >> "$PROFILE_FILE"
  chmod 600 "$PROFILE_FILE" 2>/dev/null || true
}

# Homebrew install wrapper (formulae). Never elevated.
# Still returns 0 (callers rely on that), but records the real exit status in
# BREW_LAST_RC so a caller can tell "installed, PATH lags" from "install failed".
BREW_LAST_RC=0
brew_install() {
  BREW_LAST_RC=0
  HOMEBREW_NO_AUTO_UPDATE=1 brew install "$@" >> "$LOG_FILE" 2>&1 || BREW_LAST_RC=$?
}
brew_install_cask() {
  HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask "$@" >> "$LOG_FILE" 2>&1 || true
}

# pip install into the brew Python. Modern Python is "externally managed"
# (PEP 668), so --break-system-packages is required; fall back to --user.
pip_install() {
  PIP_LAST_RC=0
  [ -z "${PYTHON_CMD:-}" ] && return 0
  # Homebrew/Apple Python are PEP 668 "externally managed" AND built with
  # ENABLE_USER_SITE=False, so --user is rejected here; --break-system-packages
  # is the working path. (Standalone CLI apps go through pipx instead.)
  "$PYTHON_CMD" -m pip install --break-system-packages --upgrade --quiet "$@" >> "$LOG_FILE" 2>&1 || PIP_LAST_RC=$?
}

# pipx install a CLI app (isolated venv, the correct tool for standalone CLIs).
pipx_install() {
  cmd_exists pipx || return 1
  pipx install "$@" >> "$LOG_FILE" 2>&1 || pipx install --force "$@" >> "$LOG_FILE" 2>&1 || true
}

# Track results for the final summary (plain indexed arrays — bash 3.2 safe).
RESULT_NAMES=()
RESULT_STATUS=()
RESULT_DETAIL=()
add_result() {
  RESULT_NAMES[${#RESULT_NAMES[@]}]="$1"
  RESULT_STATUS[${#RESULT_STATUS[@]}]="$2"
  RESULT_DETAIL[${#RESULT_DETAIL[@]}]="${3:-}"
}

# ─── Shell profile target (zsh is the macOS default since Catalina) ──────────

case "${SHELL:-/bin/zsh}" in
  */bash) PROFILE_FILE="$ACTUAL_HOME/.bash_profile" ;;
  */zsh)  PROFILE_FILE="$ACTUAL_HOME/.zprofile" ;;
  *)      PROFILE_FILE="$ACTUAL_HOME/.zprofile" ;;
esac

# ─── Banner ───────────────────────────────────────────────────────────────────

echo -e "${MAGENTA}"
printf "  ╔"; i=0; while [ "$i" -lt "$BANNER_W" ]; do printf "═"; i=$((i + 1)); done; printf "╗\n"
printf "  ║%s║\n" "$(center "")"
printf "  ║%s║\n" "$(center "A I   T E R M I N A L   K I C K S T A R T")"
printf "  ║%s║\n" "$(center "macOS Edition  (Apple Silicon + Intel)")"
printf "  ║%s║\n" "$(center "")"
printf "  ║%s║\n" "$(center "Claude Code | Codex | ChatGPT (sgpt) | Copilot CLI")"
printf "  ║%s║\n" "$(center "")"
printf "  ╚"; i=0; while [ "$i" -lt "$BANNER_W" ]; do printf "═"; i=$((i + 1)); done; printf "╝\n"
echo -e "${NC}"

# ─── Guards: must be macOS, must NOT be root ─────────────────────────────────

if [ "$(uname -s)" != "Darwin" ]; then
  fail "This is the macOS edition. You're not on macOS ($(uname -s))."
  info "Use start-ai-terminal-kickstart.sh (Linux) or Start-AITerminalKickstart.ps1 (Windows)."
  exit 1
fi

if [ "$(id -u)" -eq 0 ]; then
  fail "Do NOT run this script with sudo / as root."
  info "Homebrew refuses to run as root and the install will fail."
  info "Re-run as your normal user:  ./start-ai-terminal-kickstart-macos.sh"
  exit 1
fi

# ─── System Info ──────────────────────────────────────────────────────────────

MACOS_VER="$(sw_vers -productVersion 2>/dev/null || echo 'unknown')"
MACOS_NAME="$(sw_vers -productName 2>/dev/null || echo 'macOS')"
if [ "$ARCH" = "arm64" ]; then
  CHIP_LABEL="Apple Silicon (arm64)"
  BREW_PREFIX_DEFAULT="/opt/homebrew"
else
  CHIP_LABEL="Intel (x86_64)"
  BREW_PREFIX_DEFAULT="/usr/local"
fi

echo -e "  ${WHITE}System Info:${NC}"
echo -e "  ${GRAY}OS          : $MACOS_NAME $MACOS_VER${NC}"
echo -e "  ${GRAY}Chip        : $CHIP_LABEL${NC}"
echo -e "  ${GRAY}User        : $ACTUAL_USER${NC}"
echo -e "  ${GRAY}Shell       : ${SHELL:-unknown}${NC}"
echo -e "  ${GRAY}Profile     : $PROFILE_FILE${NC}"
echo -e "  ${GRAY}Bash        : ${BASH_VERSION:-?}${NC}"
echo -e "  ${GRAY}Log File    : $LOG_FILE${NC}"
echo ""

# ─── Install mode (Everything vs Choose) ─────────────────────────────────────
if ! $AUTO_MODE; then
  echo -e "  ${WHITE}Install mode:${NC}"
  echo -e "  ${GRAY}    1) Everything — install the full recommended stack, unattended${NC}"
  echo -e "  ${GRAY}    2) Choose     — ask before each component${NC}"
  echo -ne "  ${YELLOW}Pick [1/2] (default 2): ${NC}"
  read -r INSTALL_MODE
  if [ "${INSTALL_MODE:-2}" = "1" ]; then
    AUTO_MODE=true
    info "Mode: install everything. (API keys, Ollama and LibreOffice stay opt-in via flags.)"
  else
    info "Mode: choose each component."
  fi
  echo ""
fi

# ─── Pre-flight: Xcode CLT, Homebrew, Network, Disk ──────────────────────────

show_progress "Pre-flight Checks"
section "PRE-FLIGHT CHECKS"

{
  echo ""
  echo "======================================================================"
  echo "Session started: $(date '+%Y-%m-%d %H:%M:%S')"
  echo "OS: $MACOS_NAME $MACOS_VER | Chip: $CHIP_LABEL | User: $ACTUAL_USER"
  echo "======================================================================"
} >> "$LOG_FILE"

# --- Xcode Command Line Tools (provides git, clang, make) ---
step "Checking Xcode Command Line Tools"
if xcode-select -p >/dev/null 2>&1; then
  ok "Xcode Command Line Tools present ($(xcode-select -p))"
  add_result "Xcode CLT" "PRESENT" ""
elif command -v brew >/dev/null 2>&1 || [ -x /opt/homebrew/bin/brew ] || [ -x /usr/local/bin/brew ]; then
  # Homebrew is already here but CLT is somehow missing — let Apple fix it.
  warn "Xcode Command Line Tools not installed (needed for git + compilers)."
  info "Triggering Apple's installer — a GUI dialog will appear. Click Install."
  xcode-select --install >/dev/null 2>&1 || true
  info "When the install finishes, re-run this script if git is missing."
  add_result "Xcode CLT" "TRIGGERED" "complete GUI dialog"
else
  # No Homebrew yet: the Homebrew installer (next step) installs the Command
  # Line Tools itself. Don't fire a second, racing xcode-select install.
  warn "Xcode Command Line Tools not installed."
  info "The Homebrew installer below will set them up for you."
  add_result "Xcode CLT" "VIA-BREW" "Homebrew installs it"
fi

# --- Homebrew ---
step "Checking Homebrew"
if ! cmd_exists brew; then
  # Try known install locations before deciding to install
  if [ -x "$BREW_PREFIX_DEFAULT/bin/brew" ]; then
    eval "$("$BREW_PREFIX_DEFAULT/bin/brew" shellenv)"
  elif [ -x /opt/homebrew/bin/brew ]; then
    eval "$(/opt/homebrew/bin/brew shellenv)"
  elif [ -x /usr/local/bin/brew ]; then
    eval "$(/usr/local/bin/brew shellenv)"
  fi
fi

if ! cmd_exists brew; then
  info "Homebrew is the macOS package manager — it installs everything else."
  if ask_yn "Install Homebrew now?"; then
    step "Installing Homebrew (this may prompt once for your password)"
    # Run visibly (not redirected) so the sudo password prompt is shown.
    NONINTERACTIVE=1 /bin/bash -c \
      "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || true
    # Load brew into this shell from whichever prefix it landed in
    if [ -x "$BREW_PREFIX_DEFAULT/bin/brew" ]; then
      eval "$("$BREW_PREFIX_DEFAULT/bin/brew" shellenv)"
    elif [ -x /opt/homebrew/bin/brew ]; then
      eval "$(/opt/homebrew/bin/brew shellenv)"
    elif [ -x /usr/local/bin/brew ]; then
      eval "$(/usr/local/bin/brew shellenv)"
    fi
  fi
fi

if cmd_exists brew; then
  BREW_BIN="$(command -v brew)"
  ok "Homebrew ready ($(brew --version 2>/dev/null | head -1))"
  # Persist brew into the login shell so future terminals find it
  add_to_profile "eval \"\$($BREW_BIN shellenv)\"" "brew shellenv"
  add_result "Homebrew" "OK" "$BREW_BIN"
else
  fail "Homebrew is not available. Cannot continue without it."
  info "Install manually from https://brew.sh then re-run this script."
  add_result "Homebrew" "FAILED" ""
  exit 1
fi

# --- Network connectivity ---
step "Testing internet connectivity to download sources"
REACHABLE=0
UNREACHABLE=0
NET_ENDPOINTS="
GitHub API|https://api.github.com
Homebrew|https://formulae.brew.sh
Claude installer|https://claude.ai
npm registry|https://registry.npmjs.org
PyPI|https://pypi.org
"
while IFS='|' read -r ep_name ep_url; do
  [ -z "$ep_name" ] && continue
  if curl -sf --connect-timeout 8 --max-time 12 -o /dev/null "$ep_url" 2>/dev/null; then
    ok "$ep_name reachable"
    REACHABLE=$((REACHABLE + 1))
  else
    warn "$ep_name ($ep_url) - UNREACHABLE"
    UNREACHABLE=$((UNREACHABLE + 1))
  fi
done <<< "$NET_ENDPOINTS"

TOTAL_EP=$((REACHABLE + UNREACHABLE))
if [ "$UNREACHABLE" -eq 0 ]; then
  ok "All $REACHABLE download sources reachable"
  add_result "Network" "OK" "$REACHABLE/$TOTAL_EP reachable"
elif [ "$UNREACHABLE" -lt 3 ]; then
  warn "$UNREACHABLE of $TOTAL_EP sources unreachable. Some installs may fail."
  add_result "Network" "PARTIAL" "$REACHABLE/$TOTAL_EP reachable"
else
  fail "Most download sources unreachable. Script will likely fail."
  info "Check: DNS, proxy, firewall, or VPN settings."
  add_result "Network" "FAILED" "$REACHABLE/$TOTAL_EP reachable"
fi

# --- Disk space (BSD df: -g reports GB) ---
step "Checking available disk space"
FREE_GB="$(df -g / 2>/dev/null | awk 'NR==2{print $4}')"
TOTAL_GB="$(df -g / 2>/dev/null | awk 'NR==2{print $2}')"
if [ "${FREE_GB:-0}" -lt 3 ]; then
  fail "CRITICAL: only ${FREE_GB}GB free on / (${TOTAL_GB}GB total). Need at least 3GB."
  add_result "Disk Space" "FAILED" "${FREE_GB}GB free"
elif [ "${FREE_GB:-0}" -lt 8 ]; then
  warn "Low: ${FREE_GB}GB free on / (${TOTAL_GB}GB total). Recommend 8+ GB."
  add_result "Disk Space" "WARN" "${FREE_GB}GB free"
else
  ok "Disk space: ${FREE_GB}GB free on / (${TOTAL_GB}GB total)"
  add_result "Disk Space" "OK" "${FREE_GB}GB free"
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 1: CORE RUNTIMES & CLI UTILITIES
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Core Runtimes & Utilities"
section "PHASE 1: Core Runtimes & CLI Utilities"

step "Updating Homebrew"
brew update >> "$LOG_FILE" 2>&1 || true
ok "Homebrew updated"

# ─── Git ──────────────────────────────────────────────────────────────────────
step "Checking Git"
if cmd_exists git; then
  ok "Git already installed ($(git --version))"
  add_result "Git" "PRESENT" "$(git --version)"
else
  info "Git is REQUIRED for Claude Code and version control."
  if ask_yn "Install Git (Homebrew, newer than the Xcode one)?"; then
    brew_install git
    cmd_exists git && ok "Git installed ($(git --version))" && add_result "Git" "INSTALLED" "$(git --version)"
  else
    add_result "Git" "SKIPPED" ""
  fi
fi

# Git identity check
if cmd_exists git; then
  step "Checking Git configuration"
  GIT_USER="$(git config --global user.name 2>/dev/null || true)"
  GIT_EMAIL="$(git config --global user.email 2>/dev/null || true)"
  if [ -z "$GIT_USER" ] || [ -z "$GIT_EMAIL" ]; then
    warn "Git user.name or user.email not set. Commits will fail without these."
    tip "git config --global user.name 'Your Name'"
    tip "git config --global user.email 'you@example.com'"
  else
    ok "Git user: $GIT_USER <$GIT_EMAIL>"
  fi
  git config --global init.defaultBranch main 2>/dev/null || true
fi

# ─── Node.js ──────────────────────────────────────────────────────────────────
step "Checking Node.js"
if cmd_exists node; then
  ok "Node.js already installed ($(node --version))"
  cmd_exists npm && ok "npm $(npm --version)"
  cmd_exists npx && ok "npx available"
  add_result "Node.js" "PRESENT" "$(node --version)"
else
  info "Node.js is required for Claude Code, npm tools, and MCP servers."
  if ask_yn "Install Node.js ${NODE_MAJOR} LTS?"; then
    brew_install "node@${NODE_MAJOR}"
    # node@NN is keg-only; link it onto PATH
    brew link --overwrite --force "node@${NODE_MAJOR}" >> "$LOG_FILE" 2>&1 || true
    if ! cmd_exists node; then brew_install node; fi
    if cmd_exists node; then
      ok "Node.js $(node --version) installed"
      cmd_exists npm && ok "npm $(npm --version)"
      add_result "Node.js" "INSTALLED" "$(node --version)"
    else
      fail "Node.js installation failed"
      add_result "Node.js" "FAILED" ""
    fi
  else
    add_result "Node.js" "SKIPPED" ""
  fi
fi

# npm global prefix (user-owned; brew node already installs globals without sudo,
# but a dedicated prefix keeps things tidy and survives node upgrades).
if cmd_exists npm; then
  step "Configuring npm global directory"
  NPM_GLOBAL="$ACTUAL_HOME/.npm-global"
  mkdir -p "$NPM_GLOBAL"
  npm config set prefix "$NPM_GLOBAL" >> "$LOG_FILE" 2>&1 || true
  add_to_profile 'export PATH="$HOME/.npm-global/bin:$PATH"' 'npm-global'
  export PATH="$NPM_GLOBAL/bin:$PATH"
  ok "npm global dir: $NPM_GLOBAL"
fi

# ─── Python (prefer Homebrew Python, not Apple's externally-managed one) ─────
step "Checking Python"
# Apple's /usr/bin/python3 is externally-managed and its user-site bin is not on
# PATH, so we deliberately ignore it and prefer a Homebrew python3.x.
PYTHON_CMD=""
pick_brew_python() {
  local cand p
  for cand in python3.13 python3.12 python3.11 python3; do
    if command -v "$cand" >/dev/null 2>&1; then
      p="$(command -v "$cand")"
      case "$p" in /usr/bin/python3|/usr/bin/python3.*) continue ;; esac
      PYTHON_CMD="$cand"; return 0
    fi
  done
  return 1
}
pick_brew_python || true

if [ -n "$PYTHON_CMD" ]; then
  ok "Python: $("$PYTHON_CMD" --version 2>&1) ($PYTHON_CMD)"
  add_result "Python" "PRESENT" "$("$PYTHON_CMD" --version 2>&1)"
else
  info "Python is used for MCP servers, AI SDKs, and data processing."
  info "(Apple's system python3 is externally-managed; installing Homebrew Python.)"
  if ask_yn "Install Python 3 (Homebrew)?"; then
    brew_install python   # unversioned formula = latest stable Python; no version pin
    pick_brew_python || PYTHON_CMD="python3"
    if "$PYTHON_CMD" --version >/dev/null 2>&1; then
      ok "Python $("$PYTHON_CMD" --version 2>&1) installed ($PYTHON_CMD)"
      add_result "Python" "INSTALLED" "$("$PYTHON_CMD" --version 2>&1)"
    else
      fail "Python installation failed"
      add_result "Python" "FAILED" ""
    fi
  else
    add_result "Python" "SKIPPED" ""
  fi
fi

# pipx — the right tool for installing standalone Python CLI apps in isolation.
if [ -n "$PYTHON_CMD" ]; then
  step "Checking pipx (isolated installs for Python CLI apps)"
  if cmd_exists pipx; then
    ok "pipx already installed"
    add_result "pipx" "PRESENT" ""
  else
    brew_install pipx
    if cmd_exists pipx; then
      pipx ensurepath >> "$LOG_FILE" 2>&1 || true
      add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'
      export PATH="$ACTUAL_HOME/.local/bin:$PATH"
      ok "pipx installed"
      add_result "pipx" "INSTALLED" ""
    else
      fail "pipx install failed - see $LOG_FILE"
      info "Python CLI apps (aider, llm, shell-gpt, markitdown) need it."
      add_result "pipx" "FAILED" "see log"
    fi
  fi
fi

# uv — ultra-fast Python package/venv manager.
step "Checking uv (fast Python package manager)"
if cmd_exists uv; then
  ok "uv already installed ($(uv --version 2>/dev/null))"
  add_result "uv" "PRESENT" ""
else
  if ask_yn "Install uv?"; then
    brew_install uv
    cmd_exists uv && ok "uv installed" && add_result "uv" "INSTALLED" "" || add_result "uv" "SKIPPED" ""
  else
    add_result "uv" "SKIPPED" ""
  fi
fi

# ─── CLI Utilities + Conversion Toolkit (single bash-3.2-safe table) ─────────
step "Installing CLI utilities + conversion toolkit"

# key | candidate-commands | brew-formula | description
CLI_TABLE="
ripgrep|rg|ripgrep|Ultra-fast code search (used by Claude Code)
jq|jq|jq|JSON processor for API responses
fd|fd fdfind|fd|Fast file finder
imagemagick|magick convert|imagemagick|Image processing (convert, resize)
ghostscript|gs|ghostscript|PDF/PostScript engine (needed by ImageMagick for PDFs)
ffmpeg|ffmpeg|ffmpeg|Video/audio processing and media conversion
p7zip|7z 7zz 7za|p7zip|Archive compression
bat|bat batcat|bat|Syntax-highlighted file viewer (better cat)
fzf|fzf|fzf|Fuzzy finder for files, history, and commands
yq|yq|yq|YAML processor (like jq but for YAML)
tree|tree|tree|Directory structure viewer
delta|delta|git-delta|Enhanced git diff viewer with syntax highlighting
shellcheck|shellcheck|shellcheck|Shell script linter and validator
tmux|tmux|tmux|Terminal multiplexer for persistent sessions
sqlite3|sqlite3|sqlite|Lightweight database engine and client
wget|wget|wget|Classic downloader (curl alternative)
gnupg|gpg|gnupg|GnuPG for signing and encryption
pandoc|pandoc|pandoc|Universal document converter (docx/md/pdf/html ...)
poppler|pdftotext pdfinfo|poppler|PDF text + image extraction (feed PDFs to AI)
exiftool|exiftool|exiftool|Read/strip metadata from any file (PDF, image, video)
qpdf|qpdf|qpdf|PDF structural rewrite, linearize, split, merge, decrypt
tesseract|tesseract|tesseract|OCR engine (turn images/scans into text)
yt-dlp|yt-dlp|yt-dlp|Media downloader (pairs with ffmpeg)
jc|jc|jc|Convert command output to JSON for AI parsing
glow|glow|glow|Render Markdown beautifully in the terminal
gh|gh|gh|GitHub CLI (repos, PRs, issues, and GitHub auth)
gron|gron|gron|Greppable JSON (gron / gron -u) for AI + jq workflows
just|just|just|Command/task runner (justfile recipes)
"
# macOS GNU userland + quality-of-life (binaries are g-prefixed; check formula)
MAC_TABLE="
coreutils|gdate gtimeout|coreutils|GNU core utilities (gdate, gtimeout, gln ...)
gnu-sed|gsed|gnu-sed|GNU sed (gsed) — matches Linux sed behavior
findutils|gfind|findutils|GNU find/xargs (gfind, gxargs)
gawk|gawk|gawk|GNU awk
grep|ggrep|grep|GNU grep (ggrep)
watch|watch|watch|Re-run a command on an interval
trash|trash|trash|Move files to Trash instead of rm
terminal-notifier|terminal-notifier|terminal-notifier|macOS notifications from the CLI
mas|mas|mas|Mac App Store command-line installer
"

install_table() {
  local table="$1"
  local key cmds formula desc pretty rc
  while IFS='|' read -r key cmds formula desc; do
    [ -z "$key" ] && continue
    if cmd_exists_any "$cmds"; then
      ok "$key is installed"
      add_result "$key" "PRESENT" ""
    else
      info "$key: $desc"
      if ask_yn "Install $key?"; then
        brew_install "$formula"
        rc="$BREW_LAST_RC"
        # INSTALLED is gated on this row's OWN command. One silent brew failure
        # in a 37-formula table must not print a green row.
        if cmd_exists_any "$cmds"; then
          ok "$key installed"
          add_result "$key" "INSTALLED" ""
        elif [ "$rc" -eq 0 ]; then
          warn "$key installed but is not on PATH yet - open a new terminal"
          add_result "$key" "PARTIAL" "restart shell"
        else
          fail "$key install failed - see $LOG_FILE"
          add_result "$key" "FAILED" "see log"
        fi
      else
        add_result "$key" "SKIPPED" ""
      fi
    fi
  done <<< "$table"
}

install_table "$CLI_TABLE"

step "Installing macOS power tools (GNU userland + quality-of-life)"
install_table "$MAC_TABLE"

# ─── Python packages for AI development ──────────────────────────────────────
if [ -n "$PYTHON_CMD" ]; then
  step "Python packages for AI development"
  info "These libraries enhance what Claude Code and AI tools can do:"
  info "  Data: numpy, pandas, polars, matplotlib, openpyxl"
  info "  Docs: PyMuPDF, pdfplumber (read PDFs so an agent can quote them)"
  info "  Web : requests, httpx, beautifulsoup4"
  info "  AI  : anthropic, openai, fastmcp, mcp"
  info "  Dev : pydantic, rich, pyyaml, python-dotenv"
  if ask_yn "Install recommended Python packages?"; then
    pip_install numpy pandas polars matplotlib openpyxl requests httpx \
      beautifulsoup4 lxml PyMuPDF pdfplumber anthropic openai fastmcp mcp \
      pydantic rich pyyaml python-dotenv Pillow chardet tabulate jsonlines
    # Don't take pip's word for it: pip_install swallows failures, so confirm a
    # representative slice actually imports before calling the row INSTALLED.
    if [ "$PIP_LAST_RC" -eq 0 ] && "$PYTHON_CMD" -c 'import numpy, pandas, requests, anthropic, openai' >/dev/null 2>&1; then
      ok "Python packages installed (into Homebrew Python via --break-system-packages)"
      add_result "Python Packages" "INSTALLED" "AI + data libs"
    else
      fail "Python package install did not complete - see $LOG_FILE"
      add_result "Python Packages" "FAILED" "see log"
    fi
  else
    add_result "Python Packages" "SKIPPED" ""
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 2: AI TERMINALS, FRAMEWORKS & CONVERSIONS
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "AI Terminals & Frameworks"
section "PHASE 2: AI Terminal Solutions"

echo ""
echo -e "  ${WHITE}Which AI terminal solutions would you like to set up?${NC}"
echo ""
echo -e "  ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
echo -e "  ${CYAN}│  1. Claude Code (Anthropic)                                  │${NC}"
echo -e "  ${GRAY}│     Best for: deep code understanding, multi-file edits,     │${NC}"
echo -e "  ${GRAY}│     agentic coding, MCP servers, security research           │${NC}"
echo -e "  ${CYAN}│  2. OpenAI Codex CLI                                         │${NC}"
echo -e "  ${GRAY}│     Best for: OpenAI-model agentic coding in the terminal    │${NC}"
echo -e "  ${CYAN}│  3. ChatGPT terminal (shell-gpt)                             │${NC}"
echo -e "  ${GRAY}│     Best for: quick questions, shell commands, one-shot Q&A  │${NC}"
echo -e "  ${CYAN}│  4. GitHub Copilot CLI (GitHub)                              │${NC}"
echo -e "  ${GRAY}│     Best for: shell command suggestions, git operations      │${NC}"
echo -e "  ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"
echo ""

INSTALL_CLAUDE=false
INSTALL_CODEX=false
INSTALL_CHATGPT=false
INSTALL_COPILOT=false
ask_yn "Install Claude Code?"                    && INSTALL_CLAUDE=true
ask_yn "Install OpenAI Codex CLI?"               && INSTALL_CODEX=true
ask_yn "Install ChatGPT terminal (shell-gpt)?"   && INSTALL_CHATGPT=true
ask_yn "Install GitHub Copilot CLI?"             && INSTALL_COPILOT=true

# ─── Claude Code ──────────────────────────────────────────────────────────────
if $INSTALL_CLAUDE; then
  section "Installing Claude Code"
  if cmd_exists claude; then
    ok "Claude Code already installed ($(claude --version 2>/dev/null || echo installed))"
    add_result "Claude Code" "PRESENT" ""
  else
    step "Installing Claude Code via official installer..."
    # Track whether an install command actually SUCCEEDED. "Installed, restart
    # your terminal" is only honest when one of them exited 0.
    CLAUDE_INSTALL_OK=false
    curl -fsSL https://claude.ai/install.sh | sh >> "$LOG_FILE" 2>&1 && CLAUDE_INSTALL_OK=true
    add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'
    export PATH="$ACTUAL_HOME/.local/bin:$PATH"
    if cmd_exists claude; then
      ok "Claude Code installed!"
      add_result "Claude Code" "INSTALLED" ""
    elif cmd_exists npm; then
      warn "Official installer may need a new shell. Trying npm fallback..."
      npm install -g @anthropic-ai/claude-code >> "$LOG_FILE" 2>&1 && CLAUDE_INSTALL_OK=true
      if cmd_exists claude; then
        ok "Claude Code installed via npm"
        add_result "Claude Code" "INSTALLED" "via npm"
      elif $CLAUDE_INSTALL_OK; then
        warn "Claude Code installed but 'claude' is not on PATH yet - open a new terminal"
        tip "Open a new terminal, then run: claude"
        add_result "Claude Code" "PARTIAL" "restart terminal"
      else
        fail "Claude Code install failed - see $LOG_FILE"
        tip "Open a new terminal, then run: claude"
        add_result "Claude Code" "FAILED" "see log"
      fi
    else
      fail "npm not available for fallback install"
      add_result "Claude Code" "FAILED" "npm missing"
    fi
  fi

  echo ""
  echo -e "  ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e "  ${CYAN}│  CLAUDE CODE AUTHENTICATION                                  │${NC}"
  echo -e "  ${CYAN}├──────────────────────────────────────────────────────────────┤${NC}"
  echo -e "  ${GRAY}│  Option A - Browser login (easiest):                         │${NC}"
  echo -e "  ${WHITE}│    1. Run: claude                                            │${NC}"
  echo -e "  ${WHITE}│    2. Browser opens -> log in at claude.ai -> Authorize      │${NC}"
  echo -e "  ${GRAY}│  Option B - API key:                                         │${NC}"
  echo -e "  ${WHITE}│    export ANTHROPIC_API_KEY=\"sk-ant-your-key\"                │${NC}"
  echo -e "  ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

  if ask_yn "Set ANTHROPIC_API_KEY now?" "n"; then
    # Typed keys are not echoed (-s), so print the newline the user's Enter
    # would normally have produced.
    echo -ne "  ${YELLOW}Enter your Anthropic API key (sk-ant-...): ${NC}"
    read -rs ANTHROPIC_KEY
    echo ""
    if [[ "$ANTHROPIC_KEY" == sk-ant-* ]]; then
      add_to_profile_kv ANTHROPIC_API_KEY "$ANTHROPIC_KEY"
      export ANTHROPIC_API_KEY="$ANTHROPIC_KEY"
      ok "ANTHROPIC_API_KEY saved to $PROFILE_FILE (mode 600)"
    else
      warn "Key doesn't start with sk-ant- . Skipped. Use browser login instead."
    fi
  fi
fi

# ─── OpenAI Codex CLI ─────────────────────────────────────────────────────────
if $INSTALL_CODEX; then
  section "Installing OpenAI Codex CLI"
  if cmd_exists codex; then
    ok "Codex already installed ($(codex --version 2>/dev/null || echo installed))"
    add_result "OpenAI Codex" "PRESENT" ""
  else
    step "Installing Codex..."
    # Track whether an install command actually SUCCEEDED. "Installed, restart
    # your terminal" is only honest when one of them exited 0.
    CODEX_INSTALL_OK=false
    if cmd_exists npm; then
      npm install -g @openai/codex >> "$LOG_FILE" 2>&1 && CODEX_INSTALL_OK=true
    fi
    if ! cmd_exists codex; then
      # Codex also ships as a Homebrew cask, not a formula. Invoked inline
      # rather than via brew_install_cask so its exit status survives.
      HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask codex >> "$LOG_FILE" 2>&1 && CODEX_INSTALL_OK=true
    fi
    if cmd_exists codex; then
      ok "Codex installed (run: codex)"
      add_result "OpenAI Codex" "INSTALLED" ""
    elif $CODEX_INSTALL_OK; then
      warn "Codex installed but 'codex' is not on PATH yet - open a new terminal"
      add_result "OpenAI Codex" "PARTIAL" "restart terminal"
    else
      fail "Codex install failed - see $LOG_FILE"
      add_result "OpenAI Codex" "FAILED" "see log"
    fi
  fi
  info "Auth: run 'codex' and sign in with ChatGPT, or set OPENAI_API_KEY."
fi

# ─── ChatGPT terminal (shell-gpt) ─────────────────────────────────────────────
# The old `chatgpt-cli` npm package is dead. shell-gpt (PyPI: shell-gpt,
# command: sgpt) is the maintained ChatGPT-in-the-terminal CLI. It is a
# standalone Python app, so it installs through pipx like aider/llm/markitdown,
# and it reads the same OPENAI_API_KEY captured at the end of this step.
if $INSTALL_CHATGPT; then
  section "Installing ChatGPT terminal (shell-gpt)"
  if cmd_exists sgpt; then
    ok "shell-gpt already installed (command: sgpt)"
    add_result "ChatGPT Terminal" "PRESENT" "sgpt"
  elif cmd_exists pipx; then
    step "Installing shell-gpt via pipx (command: sgpt)..."
    pipx_install shell-gpt
    if cmd_exists sgpt; then
      # Probe 'sgpt --version' only when shell-gpt cannot prompt. On a first run
      # with no key and no config it asks for an API key straight from the
      # terminal (getpass), which closing stdin does NOT prevent - it would hang
      # this installer. INSTALLED is gated on the binary either way.
      SGPT_VER=""
      if [ -n "${OPENAI_API_KEY:-}" ] || [ -f "$ACTUAL_HOME/.config/shell_gpt/.sgptrc" ]; then
        SGPT_VER="$(sgpt --version 2>/dev/null </dev/null | head -1)"
      fi
      ok "shell-gpt installed (command: sgpt${SGPT_VER:+, $SGPT_VER})"
      add_result "ChatGPT Terminal" "INSTALLED" "sgpt"
    elif pipx list --short 2>/dev/null | grep -q '^shell-gpt'; then
      warn "shell-gpt installed but 'sgpt' is not on PATH yet - open a new terminal"
      add_result "ChatGPT Terminal" "PARTIAL" "restart shell"
    else
      fail "shell-gpt install failed - see $LOG_FILE"
      add_result "ChatGPT Terminal" "FAILED" "see log"
    fi
  else
    fail "pipx not available - cannot install shell-gpt"
    add_result "ChatGPT Terminal" "FAILED" "pipx missing"
  fi
  [ -n "${PYTHON_CMD:-}" ] && pip_install openai && ok "OpenAI Python SDK installed"

  echo ""
  echo -e "  ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e "  ${CYAN}│  CHATGPT / OPENAI AUTHENTICATION                             │${NC}"
  echo -e "  ${WHITE}│    1. Get a key: https://platform.openai.com/api-keys        │${NC}"
  echo -e "  ${WHITE}│    2. export OPENAI_API_KEY=\"sk-your-key\"                    │${NC}"
  echo -e "  ${GRAY}│    One key: sgpt, the OpenAI SDK and Codex all read it.      │${NC}"
  echo -e "  ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

  if ask_yn "Set OPENAI_API_KEY now?" "n"; then
    # Typed keys are not echoed (-s), so print the newline the user's Enter
    # would normally have produced.
    echo -ne "  ${YELLOW}Enter your OpenAI API key (sk-...): ${NC}"
    read -rs OPENAI_KEY
    echo ""
    if [[ "$OPENAI_KEY" == sk-* ]]; then
      add_to_profile_kv OPENAI_API_KEY "$OPENAI_KEY"
      export OPENAI_API_KEY="$OPENAI_KEY"
      ok "OPENAI_API_KEY saved to $PROFILE_FILE (mode 600)"
      info "sgpt reads OPENAI_API_KEY - it will pick this up in a new shell."
    else
      warn "Key format not recognized. Set it manually later."
    fi
  fi
fi

# ─── GitHub Copilot CLI ───────────────────────────────────────────────────────
if $INSTALL_COPILOT; then
  section "Installing GitHub Copilot CLI"
  # The old GitHub CLI Copilot extension is retired. The supported CLI is now
  # the npm package @github/copilot (command: copilot).
  if cmd_exists npm; then
    step "Installing GitHub Copilot CLI (npm @github/copilot)..."
    # Keep the npm exit status: "installed, restart your terminal" is only
    # honest when the install itself succeeded.
    COPILOT_INSTALL_OK=false
    npm install -g @github/copilot >> "$LOG_FILE" 2>&1 && COPILOT_INSTALL_OK=true
    if cmd_exists copilot; then
      ok "GitHub Copilot CLI installed (run: copilot)"
      add_result "GitHub Copilot CLI" "INSTALLED" "copilot"
    elif $COPILOT_INSTALL_OK; then
      warn "Copilot CLI installed but 'copilot' is not on PATH yet - open a new terminal"
      add_result "GitHub Copilot CLI" "PARTIAL" "restart terminal"
    else
      fail "npm install -g @github/copilot failed - see $LOG_FILE"
      add_result "GitHub Copilot CLI" "FAILED" "see log"
    fi
  else
    fail "npm not available - cannot install GitHub Copilot CLI"
    add_result "GitHub Copilot CLI" "FAILED" "npm missing"
  fi
  # gh is no longer required by Copilot CLI, but "gh auth login" is still the
  # easiest way to get a GitHub session on this machine.
  if ! cmd_exists gh; then
    info "GitHub CLI (gh) is handy for GitHub auth and repo work."
    if ask_yn "Install GitHub CLI?"; then brew_install gh; fi
  fi
  if cmd_exists gh; then
    ok "GitHub CLI: $(gh --version 2>/dev/null | head -1)"
    add_result "GitHub CLI" "PRESENT" ""
  fi
  echo ""
  echo -e "  ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
  echo -e "  ${CYAN}│  GITHUB COPILOT AUTHENTICATION                               │${NC}"
  echo -e "  ${WHITE}│    1. Run: copilot  ->  then /login inside the CLI           │${NC}"
  echo -e "  ${WHITE}│    2. Or: gh auth login  ->  GitHub.com  ->  web browser     │${NC}"
  echo -e "  ${GRAY}│  Requires a GitHub Copilot subscription.                     │${NC}"
  echo -e "  ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"
  if cmd_exists gh && ask_yn "Log in to GitHub now?" "n"; then gh auth login; fi
fi

# ─── AI frameworks & conversion CLIs (code generation + data conversions) ────
section "AI Frameworks & Conversion CLIs"
info "These power code generation, repo-to-prompt packing, and feeding"
info "documents/media into any AI model."
echo ""

if ask_yn "Install the AI framework + conversion bundle?"; then
  # npm-based agentic / repo tooling
  if cmd_exists npm; then
    step "repomix, ast-grep, tldr (npm)"
    npm install -g repomix @ast-grep/cli tldr >> "$LOG_FILE" 2>&1 || true
    cmd_exists repomix  && ok "repomix installed"  && add_result "repomix" "INSTALLED" ""  || add_result "repomix" "SKIPPED" "restart shell"

    # ── Headless browser + document toolkit (npm) ──
    # Its own prompt: puppeteer pulls a private Chromium build (~150 MB) and
    # playwright fetches three engines on first use. On Apple Silicon both are
    # native arm64, so no Rosetta step is needed.
    info ""
    info "Headless browser + document toolkit: read pages that block plain curl,"
    info "render HTML to PDF, merge and bookmark PDFs, audit a live site."
    info "  puppeteer + puppeteer-extra + stealth plugin, playwright,"
    info "  pdf-lib, sharp, lighthouse   (~150 MB of browser on install)"
    if ask_yn "Install the headless browser + document toolkit?"; then
      step "puppeteer, playwright, pdf-lib, sharp, lighthouse (npm)"
      npm install -g puppeteer puppeteer-extra puppeteer-extra-plugin-stealth playwright pdf-lib sharp lighthouse >> "$LOG_FILE" 2>&1 || true
      if cmd_exists lighthouse; then ok "lighthouse installed"; add_result "lighthouse" "INSTALLED" ""
      else add_result "lighthouse" "SKIPPED" "restart shell"; fi
      NPM_ROOT="$(npm root -g 2>/dev/null | tail -1)"
      for lib in puppeteer puppeteer-extra puppeteer-extra-plugin-stealth playwright pdf-lib sharp; do
        if [ -n "$NPM_ROOT" ] && [ -d "$NPM_ROOT/$lib" ]; then
          ok "$lib installed"; add_result "$lib" "INSTALLED" ""
        else
          add_result "$lib" "SKIPPED" ""
        fi
      done
      info "Playwright browser engines are a separate download: npx playwright install"
      info "Node resolves globals from the script's own directory, not your cwd."
      info "From a standalone script, point createRequire at: ${NPM_ROOT:-\$(npm root -g)}"
    else
      add_result "Headless browser toolkit" "SKIPPED" ""
    fi
    cmd_exists ast-grep && ok "ast-grep installed (structural code search/rewrite)" && add_result "ast-grep" "INSTALLED" "" || add_result "ast-grep" "SKIPPED" "restart shell"
    cmd_exists tldr     && ok "tldr installed"     && add_result "tldr" "INSTALLED" ""     || add_result "tldr" "SKIPPED" "restart shell"

    # ccusage — usage report for your agent CLIs. Coverage is deepest for
    # Claude Code and thinner for the others, so read it as a usage report,
    # not as one unified bill.
    step "ccusage (usage report for your agent CLIs)"
    npm install -g ccusage >> "$LOG_FILE" 2>&1 || true
    cmd_exists ccusage && ok "ccusage installed (usage report for your agent CLIs)" && add_result "ccusage" "INSTALLED" "" || add_result "ccusage" "SKIPPED" "restart shell"

    # Google Gemini CLI installs the '@google/gemini-cli' package (command:
    # 'gemini'). Installed from npm on every platform, including macOS: the
    # Homebrew formula is deprecated ("unsupported"), the npm package is not.
    # Opt-in to keep the default install lean, so it is skipped under --auto.
    if $AUTO_MODE; then
      info "Skipping Google Gemini CLI under --auto (opt-in to keep the default install lean)."
    elif ask_yn "Install Google Gemini CLI? (opt-in to keep the default install lean)" "n"; then
      npm install -g @google/gemini-cli >> "$LOG_FILE" 2>&1 || true
      cmd_exists gemini && ok "gemini installed (command: gemini)" && add_result "gemini-cli" "INSTALLED" "" || add_result "gemini-cli" "SKIPPED" "restart shell"
    fi
  fi

  # pipx-based standalone Python AI CLIs (isolated, no PEP 668 conflicts)
  if cmd_exists pipx; then
    step "aider (AI pair programmer in the terminal)"
    pipx_install aider-chat
    cmd_exists aider && ok "aider installed" && add_result "aider" "INSTALLED" "" || add_result "aider" "SKIPPED" "restart shell"

    step "llm (Simon Willison's pluggable LLM CLI)"
    pipx_install llm
    cmd_exists llm && ok "llm installed" && add_result "llm" "INSTALLED" "" || add_result "llm" "SKIPPED" "restart shell"

    # llm ships with OpenAI models registered and no Anthropic support at all —
    # Claude models live in the llm-anthropic plugin. Only run this if llm
    # actually landed, so a declined/failed llm doesn't print a confusing error.
    if cmd_exists llm; then
      step "llm-anthropic (Claude models for llm)"
      llm install llm-anthropic >> "$LOG_FILE" 2>&1 || true
      llm plugins 2>/dev/null | grep -q llm-anthropic \
        && ok "llm-anthropic installed (list Claude models with: llm models)" \
        && add_result "llm-anthropic" "INSTALLED" "" \
        || add_result "llm-anthropic" "SKIPPED" "run: llm install llm-anthropic"
    fi

    step "files-to-prompt (turn a tree of files into one prompt)"
    pipx_install files-to-prompt
    cmd_exists files-to-prompt && ok "files-to-prompt installed" && add_result "files-to-prompt" "INSTALLED" "" || add_result "files-to-prompt" "SKIPPED" "restart shell"

    step "markitdown (Office/PDF/images -> Markdown for LLMs)"
    pipx_install "markitdown[all]"
    cmd_exists markitdown && ok "markitdown installed" && add_result "markitdown" "INSTALLED" "" || add_result "markitdown" "SKIPPED" "restart shell"

    step "httpie (friendly HTTP client; the command is 'http')"
    pipx_install httpie
    cmd_exists http && ok "httpie installed (command: http)" && add_result "httpie" "INSTALLED" "" || add_result "httpie" "SKIPPED" "restart shell"

    # Refresh any already-installed pipx apps to their latest release (so a
    # re-run keeps the toolchain current without editing the script).
    pipx upgrade-all >> "$LOG_FILE" 2>&1 || true
  else
    warn "pipx unavailable - skipping aider/llm/files-to-prompt/markitdown"
  fi

  # ruff — Python linter + formatter, the Python counterpart to the shellcheck
  # this script already installs. uv (installed by the uv prompt above) is the
  # fast path; pipx is the fallback when uv was declined.
  step "ruff (Python linter + formatter)"
  if cmd_exists ruff; then
    ok "ruff already installed"
    add_result "ruff" "PRESENT" ""
  else
    if cmd_exists uv; then
      uv tool install ruff >> "$LOG_FILE" 2>&1 || true
      add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'
      export PATH="$ACTUAL_HOME/.local/bin:$PATH"
    fi
    cmd_exists ruff || pipx_install ruff
    if cmd_exists ruff; then
      ok "ruff installed (ruff check . / ruff format .)"
      add_result "ruff" "INSTALLED" ""
    elif cmd_exists uv || cmd_exists pipx; then
      warn "ruff may need a new shell to appear on PATH"
      add_result "ruff" "SKIPPED" "restart shell"
    else
      # Neither installer is present, so nothing was ever attempted here - a
      # new shell will not make ruff appear.
      warn "ruff needs uv or pipx and neither is available - nothing was installed"
      add_result "ruff" "SKIPPED" "needs uv or pipx"
    fi
  fi

  # aichat — all-in-one Rust LLM CLI (brew formula)
  step "aichat (all-in-one LLM CLI: chat, REPL, RAG, shell assistant)"
  if cmd_exists aichat; then
    ok "aichat already installed"
    add_result "aichat" "PRESENT" ""
  else
    brew_install aichat
    cmd_exists aichat && ok "aichat installed" && add_result "aichat" "INSTALLED" "" || add_result "aichat" "SKIPPED" ""
  fi
else
  info "Skipped AI framework bundle. Re-run anytime to add them."
fi

# ─── Optional: Ollama (local models) ─────────────────────────────────────────
if $WITH_OLLAMA || ask_yn "Install Ollama for offline/local models?" "n"; then
  section "Installing Ollama (local model runner)"
  if cmd_exists ollama; then
    ok "Ollama already installed"
    add_result "Ollama" "PRESENT" ""
  else
    brew_install ollama
    cmd_exists ollama && ok "Ollama installed" && add_result "Ollama" "INSTALLED" "" || add_result "Ollama" "SKIPPED" ""
  fi
  if cmd_exists ollama; then
    info "Start the server with:  ollama serve   (or it runs as a brew service)"
    if ask_yn "Pull a small starter model now (llama3.2, ~2GB)?" "n"; then
      brew services start ollama >> "$LOG_FILE" 2>&1 || (ollama serve >> "$LOG_FILE" 2>&1 &) || true
      sleep 2
      ollama pull llama3.2 || warn "Pull failed - run 'ollama pull llama3.2' later"
    fi
  fi
fi

# ─── Optional: Kimi Code CLI (Moonshot AI — China-operated, OFF by default) ──
# Gated EXACTLY like Ollama: opt-in via --with-kimi, otherwise prompted with a
# default of "n". Under --auto/--all/--everything, ask_yn returns the "n"
# default, so the full-stack run does NOT install Kimi. US-only models stay the
# default; a user must explicitly choose this China-operated tool.
if $WITH_KIMI || ask_yn "Install Kimi Code CLI (Moonshot AI — China-operated, off by default)?" "n"; then
  section "Installing Kimi Code CLI (Moonshot AI)"
  if cmd_exists kimi; then
    ok "Kimi Code CLI already installed ($(kimi --version 2>/dev/null || echo installed))"
    add_result "Kimi Code CLI" "PRESENT" ""
  else
    step "Installing Kimi Code CLI via official installer (run as your user, no sudo)..."
    # Keep the installer's exit status: "installed, restart your shell" is only
    # honest when the install itself succeeded.
    KIMI_INSTALL_OK=false
    curl -LsSf https://code.kimi.com/install.sh | bash >> "$LOG_FILE" 2>&1 && KIMI_INSTALL_OK=true
    add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' '.local/bin'
    export PATH="$ACTUAL_HOME/.local/bin:$PATH"
    if cmd_exists kimi; then
      ok "Kimi Code CLI installed ($(kimi --version 2>/dev/null || echo installed))"
      add_result "Kimi Code CLI" "INSTALLED" ""
    elif $KIMI_INSTALL_OK; then
      warn "Kimi Code CLI installed but 'kimi' is not on PATH yet - open a new terminal"
      add_result "Kimi Code CLI" "PARTIAL" "restart shell"
    else
      fail "Kimi Code CLI install failed - see $LOG_FILE"
      add_result "Kimi Code CLI" "FAILED" "see log"
    fi
  fi
  echo ""
  info "Kimi Code CLI auth: run 'kimi' then '/login' (browser OAuth: \"Kimi Code\"),"
  info "or paste an API key from https://platform.kimi.ai (Console -> create key)."
  info "Account needs a >= \$1 recharge to activate; the \$1 starter tier is only"
  info "3 RPM, so for real agentic loops budget the \$10 tier."
  echo ""
  info "OPTIONAL — use Kimi as a drop-in model INSIDE Claude Code (no separate CLI)"
  info "via Moonshot's Anthropic-compatible endpoint. Set these in your shell ONLY"
  info "when you want Claude Code to talk to Kimi (do NOT set them to keep US models):"
  info "    export ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic"
  info "    export ANTHROPIC_AUTH_TOKEN=<your Moonshot key>"
  info "    export ANTHROPIC_MODEL=kimi-k2.7-code"
  info "Leaving those three unset keeps Claude Code on US (Anthropic) models."
  warn "Moonshot AI is China-operated (both api.moonshot.ai and api.moonshot.cn) — do NOT send confidential client data through it."
fi

# ─── Optional: LibreOffice (document conversions) ────────────────────────────
if $WITH_OFFICE; then
  section "Installing LibreOffice (document conversions)"
  if [ -d "/Applications/LibreOffice.app" ] || cmd_exists soffice; then
    ok "LibreOffice already installed"
    add_result "LibreOffice" "PRESENT" ""
  else
    brew_install_cask libreoffice
    if [ -d "/Applications/LibreOffice.app" ] || cmd_exists soffice; then
      ok "LibreOffice installed"
      add_result "LibreOffice" "INSTALLED" ""
      info "Convert via:  soffice --headless --convert-to pdf file.docx"
    else
      fail "LibreOffice install failed - see $LOG_FILE"
      add_result "LibreOffice" "FAILED" "see log"
    fi
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# AI ECOSYSTEM: MCP SERVERS & ALIGNED REPOS
# ═══════════════════════════════════════════════════════════════════════════════

section "AI Ecosystem: MCP Servers & Aligned Repos"

if cmd_exists claude; then
  if ask_yn "Register common MCP servers with Claude Code (user scope)?"; then
    mkdir -p "$ACTUAL_HOME/.ai-tools"
    # npm/npx servers (need only Node) — flags BEFORE the name, command after `--`
    claude mcp add --scope user filesystem -- npx -y @modelcontextprotocol/server-filesystem "$ACTUAL_HOME" >> "$LOG_FILE" 2>&1 || true
    claude mcp add --scope user memory -e MEMORY_FILE_PATH="$ACTUAL_HOME/.ai-tools/memory.json" -- npx -y @modelcontextprotocol/server-memory >> "$LOG_FILE" 2>&1 || true
    claude mcp add --scope user sequential-thinking -- npx -y @modelcontextprotocol/server-sequential-thinking >> "$LOG_FILE" 2>&1 || true
    # python/uvx servers (need uv) — fetch + time live on PyPI, not npm
    if cmd_exists uvx; then
      claude mcp add --scope user fetch -- uvx mcp-server-fetch >> "$LOG_FILE" 2>&1 || true
      claude mcp add --scope user time  -- uvx mcp-server-time  >> "$LOG_FILE" 2>&1 || true
    else
      info "uvx not found — skipping Python MCP servers (fetch, time). 'brew install uv' to add them."
    fi
    claude mcp add --scope user playwright -- npx -y @playwright/mcp@latest >> "$LOG_FILE" 2>&1 || true
    MCP_EXPECTED="filesystem memory sequential-thinking playwright"
    cmd_exists uvx && MCP_EXPECTED="$MCP_EXPECTED fetch time"

    # GitHub's hosted MCP server (HTTP). This script can register it but CANNOT
    # complete the OAuth handshake — that happens inside Claude Code.
    claude mcp add --scope user --transport http github https://api.githubcopilot.com/mcp/ >> "$LOG_FILE" 2>&1 || true

    # Every 'claude mcp add' above ends in '|| true', so ask Claude Code what
    # actually got registered instead of assuming it worked.
    #
    # 'claude mcp list' is NOT a cheap read: it health-checks every registered
    # server by LAUNCHING it, so it triggers cold `npx -y` / `uvx` downloads
    # plus an HTTP handshake and can sit silent for minutes. Warn, then bound
    # it. macOS has no GNU `timeout`; coreutils installs it as `gtimeout`.
    # '-k 10' escalates to SIGKILL 10s after the deadline SIGTERM, and - as long
    # as --foreground is NOT passed - timeout runs the command in its own process
    # group and signals the GROUP, so the cold `npx -y` / `uvx` servers it spawned
    # die with it instead of being left as orphans. Do not add --foreground.
    MCP_TIMEOUT=""
    if cmd_exists timeout; then
      MCP_TIMEOUT="timeout -k 10 90"
    elif cmd_exists gtimeout; then
      MCP_TIMEOUT="gtimeout -k 10 90"
    fi
    MCP_TIMED_OUT=false
    MCP_UNVERIFIED=false
    MCP_LIST=""
    if [ -n "$MCP_TIMEOUT" ]; then
      info "Verifying with 'claude mcp list' - it starts every server, so give it up to a minute..."
      MCP_LIST="$($MCP_TIMEOUT claude mcp list 2>/dev/null)"
      MCP_RC=$?
      # 124 = killed by the deadline SIGTERM, 137 = killed by the -k SIGKILL.
      if [ "$MCP_RC" -eq 124 ] || [ "$MCP_RC" -eq 137 ]; then MCP_TIMED_OUT=true; fi
    else
      # With no timeout utility there is NO way to bound this call, and an
      # unbounded 'claude mcp list' can block the installer for minutes. Skip
      # the verification rather than risk hanging the run.
      warn "No 'timeout'/'gtimeout' on PATH - SKIPPING the MCP health check so this installer cannot hang."
      info "'claude mcp list' starts every registered server, and without a timeout there is no way to bound it."
      tip "Get one with: brew install coreutils   (installs gtimeout), then check by hand: claude mcp list"
      MCP_UNVERIFIED=true
    fi
    MCP_OK=""
    MCP_MISSING=""
    for srv in $MCP_EXPECTED; do
      if printf '%s\n' "$MCP_LIST" | grep -qE "(^|[[:space:]])${srv}:"; then
        MCP_OK="$MCP_OK $srv"
      else
        MCP_MISSING="$MCP_MISSING $srv"
      fi
    done
    MCP_OK_N="$(echo $MCP_OK | wc -w | tr -d ' ')"
    MCP_TOTAL_N="$(echo $MCP_EXPECTED | wc -w | tr -d ' ')"

    if $MCP_UNVERIFIED; then
      warn "MCP registration was NOT verified - the health check needs a timeout utility"
      info "The servers may well be registered; nothing checked them."
      tip "Check it yourself with: claude mcp list"
      add_result "MCP servers" "UNKNOWN" "not verified - no timeout"
    elif $MCP_TIMED_OUT; then
      warn "'claude mcp list' did not finish within 90s - could not verify registration"
      info "The servers may well be registered; the health-check just ran long."
      tip "Check it yourself later with: claude mcp list"
      add_result "MCP servers" "UNKNOWN" "could not verify"
    elif [ -z "$MCP_MISSING" ]; then
      ok "MCP servers verified in 'claude mcp list':$MCP_OK"
      add_result "MCP servers" "INSTALLED" "$MCP_OK_N/$MCP_TOTAL_N verified"
    elif [ -n "$MCP_OK" ]; then
      ok "MCP servers verified in 'claude mcp list':$MCP_OK"
      warn "MCP servers NOT listed (registration failed):$MCP_MISSING"
      tip "Re-run 'claude mcp add --scope user' for those, or check $LOG_FILE"
      add_result "MCP servers" "PARTIAL" "$MCP_OK_N/$MCP_TOTAL_N verified"
    else
      fail "No MCP servers showed up in 'claude mcp list'"
      tip "Check $LOG_FILE, then re-run this script"
      add_result "MCP servers" "FAILED" "none verified"
    fi

    # Deliberately NOT counted in the aggregate above: this script can register
    # the server but cannot complete its OAuth, so it is reported on its own.
    if $MCP_UNVERIFIED; then
      info "github MCP: registration not verified (health check skipped - no timeout utility)."
      tip "Check it yourself with: claude mcp list"
      add_result "MCP github" "UNKNOWN" "not verified - no timeout"
    elif $MCP_TIMED_OUT; then
      info "github MCP: registration could not be verified (list timed out)."
      add_result "MCP github" "UNKNOWN" "could not verify"
    elif printf '%s\n' "$MCP_LIST" | grep -qE "(^|[[:space:]])github:"; then
      ok "github MCP registered (user scope)"
      # Listed is the same before and after OAuth, so this is a note either way
      # - it is not evidence that anything is wrong.
      info "Note: if you have not authorised it yet, run '/mcp' inside Claude Code once."
      add_result "MCP github" "REGISTERED" "needs /mcp login"
    else
      fail "github MCP is not listed by 'claude mcp list'"
      add_result "MCP github" "FAILED" "not registered"
    fi
  else
    add_result "MCP servers" "SKIPPED" ""
  fi
else
  info "Claude Code not installed — skipping MCP server registration."
fi

echo ""
info "Aligned GitHub repos worth bookmarking:"
info "  awesome-claude-code     https://github.com/hesreallyhim/awesome-claude-code"
info "  awesome-mcp-servers     https://github.com/punkpeye/awesome-mcp-servers"
info "  MCP reference servers   https://github.com/modelcontextprotocol/servers"
info "  OpenAI Codex CLI        https://github.com/openai/codex"
info "  Aider                   https://github.com/Aider-AI/aider"
if cmd_exists git && ask_yn "Clone these reference repos into ~/ai-tools?" "n"; then
  AI_TOOLS_DIR="$ACTUAL_HOME/ai-tools"
  mkdir -p "$AI_TOOLS_DIR"
  REPOS_OK=0
  REPOS_TOTAL=0
  for repo in \
    "https://github.com/hesreallyhim/awesome-claude-code.git" \
    "https://github.com/punkpeye/awesome-mcp-servers.git" \
    "https://github.com/modelcontextprotocol/servers.git" \
    "https://github.com/openai/codex.git" \
    "https://github.com/Aider-AI/aider.git"; do
    rname="$(basename "$repo" .git)"
    REPOS_TOTAL=$((REPOS_TOTAL + 1))
    if [ -d "$AI_TOOLS_DIR/$rname/.git" ]; then
      ok "$rname already cloned"
      REPOS_OK=$((REPOS_OK + 1))
    elif git clone --depth 1 "$repo" "$AI_TOOLS_DIR/$rname" >> "$LOG_FILE" 2>&1 && [ -d "$AI_TOOLS_DIR/$rname/.git" ]; then
      ok "cloned $rname"
      REPOS_OK=$((REPOS_OK + 1))
    else
      warn "clone failed: $rname"
    fi
  done
  # Only claim INSTALLED for repos that are actually on disk.
  if [ "$REPOS_OK" -eq "$REPOS_TOTAL" ]; then
    add_result "Aligned repos" "INSTALLED" "$REPOS_OK/$REPOS_TOTAL in ~/ai-tools"
  elif [ "$REPOS_OK" -gt 0 ]; then
    warn "Only $REPOS_OK of $REPOS_TOTAL reference repos cloned - see $LOG_FILE"
    add_result "Aligned repos" "PARTIAL" "$REPOS_OK/$REPOS_TOTAL cloned"
  else
    fail "No reference repos cloned - see $LOG_FILE"
    add_result "Aligned repos" "FAILED" "see log"
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 3: DEPLOYMENT TOOLS
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Deployment & Extras"
section "PHASE 3: Netlify CLI & Deployment"

step "Checking Netlify CLI"
if cmd_exists netlify; then
  ok "Netlify CLI already installed ($(netlify --version 2>/dev/null))"
  add_result "Netlify CLI" "PRESENT" ""
else
  info "Netlify CLI deploys websites directly from your terminal."
  if ask_yn "Install Netlify CLI?"; then
    if cmd_exists npm; then
      NETLIFY_INSTALL_OK=false
      npm install -g netlify-cli >> "$LOG_FILE" 2>&1 && NETLIFY_INSTALL_OK=true
      if cmd_exists netlify; then
        ok "Netlify CLI installed"
        add_result "Netlify CLI" "INSTALLED" ""
      elif $NETLIFY_INSTALL_OK; then
        warn "Netlify CLI installed but 'netlify' is not on PATH yet - open a new terminal"
        add_result "Netlify CLI" "PARTIAL" "restart terminal"
      else
        fail "npm install -g netlify-cli failed - see $LOG_FILE"
        add_result "Netlify CLI" "FAILED" "see log"
      fi
    else
      fail "npm not available - install Node.js first"
      add_result "Netlify CLI" "FAILED" "npm missing"
    fi
  else
    add_result "Netlify CLI" "SKIPPED" ""
  fi
fi

echo ""
echo -e "  ${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}"
echo -e "  ${CYAN}│  NETLIFY: LOGIN & DEPLOY                                     │${NC}"
echo -e "  ${WHITE}│    netlify login                                             │${NC}"
echo -e "  ${WHITE}│    cd /path/to/site && netlify init                          │${NC}"
echo -e "  ${WHITE}│    netlify deploy            # preview draft                 │${NC}"
echo -e "  ${WHITE}│    netlify deploy --prod     # push live                     │${NC}"
echo -e "  ${CYAN}└──────────────────────────────────────────────────────────────┘${NC}"

if cmd_exists netlify; then
  if ask_yn "Log in to Netlify now?" "n"; then netlify login; fi
fi

# ─── Cloudflare Wrangler (Pages + Workers) ───────────────────────────────────
step "Checking Cloudflare Wrangler"
if cmd_exists wrangler; then
  ok "Wrangler already installed ($(wrangler --version 2>/dev/null | head -1))"
  add_result "Cloudflare Wrangler" "PRESENT" ""
else
  info "Wrangler deploys to Cloudflare Pages (static sites) and Workers (edge)."
  info "Free tier: unlimited static requests, generous Workers quota, custom domains."
  if ask_yn "Install Cloudflare Wrangler?"; then
    if cmd_exists npm; then
      WRANGLER_INSTALL_OK=false
      npm install -g wrangler >> "$LOG_FILE" 2>&1 && WRANGLER_INSTALL_OK=true
      if cmd_exists wrangler; then
        ok "Wrangler installed"
        add_result "Cloudflare Wrangler" "INSTALLED" ""
      elif $WRANGLER_INSTALL_OK; then
        warn "Wrangler installed but 'wrangler' is not on PATH yet - open a new terminal"
        add_result "Cloudflare Wrangler" "PARTIAL" "restart terminal"
      else
        fail "npm install -g wrangler failed - see $LOG_FILE"
        add_result "Cloudflare Wrangler" "FAILED" "see log"
      fi
    else
      fail "npm not available - install Node.js first"
      add_result "Cloudflare Wrangler" "FAILED" "npm missing"
    fi
  else
    add_result "Cloudflare Wrangler" "SKIPPED" ""
  fi
fi
info "Cloudflare deploy quickstart:"
tip "wrangler login                                     # browser OAuth"
tip "wrangler pages deploy ./dist --project-name=NAME   # static site -> Pages"
tip "wrangler deploy                                    # deploy a Worker (edge)"
tip "Headless/CI: export CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID"
if cmd_exists wrangler; then
  if ask_yn "Log in to Cloudflare now?" "n"; then wrangler login; fi
fi

step "Checking Vercel CLI"
if cmd_exists vercel; then
  ok "Vercel CLI already installed"
  add_result "Vercel CLI" "PRESENT" ""
else
  if ask_yn "Install Vercel CLI?" "n"; then
    if cmd_exists npm; then
      VERCEL_INSTALL_OK=false
      npm install -g vercel >> "$LOG_FILE" 2>&1 && VERCEL_INSTALL_OK=true
      if cmd_exists vercel; then
        ok "Vercel CLI installed"
        add_result "Vercel CLI" "INSTALLED" ""
      elif $VERCEL_INSTALL_OK; then
        warn "Vercel CLI installed but 'vercel' is not on PATH yet - open a new terminal"
        add_result "Vercel CLI" "PARTIAL" "restart terminal"
      else
        fail "npm install -g vercel failed - see $LOG_FILE"
        add_result "Vercel CLI" "FAILED" "see log"
      fi
    else
      fail "npm not available"
      add_result "Vercel CLI" "FAILED" "npm missing"
    fi
  else
    add_result "Vercel CLI" "SKIPPED" ""
  fi
fi

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 4: ENVIRONMENT HEALTH CHECK
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Validation & Health Check"
section "PHASE 4: Environment Health Check"

echo ""
step "Running functional validation tests"

if cmd_exists node; then
  NODE_TEST="$(node -e "console.log('node-ok')" 2>/dev/null || true)"
  [[ "$NODE_TEST" == *node-ok* ]] && ok "Node.js exec: PASS" || warn "Node.js exec: FAIL"
fi
if [ -n "${PYTHON_CMD:-}" ]; then
  PY_SSL="$($PYTHON_CMD -c "import ssl; print('py-ssl-ok:', ssl.OPENSSL_VERSION)" 2>/dev/null || true)"
  [[ "$PY_SSL" == *py-ssl-ok* ]] && ok "Python SSL: PASS (${PY_SSL#*: })" || warn "Python SSL: FAIL"
fi
if cmd_exists git; then
  GIT_LS="$(git ls-remote --heads https://github.com/anthropics/claude-code.git 2>/dev/null || true)"
  [ -n "$GIT_LS" ] && ok "Git HTTPS: PASS" || warn "Git HTTPS: FAIL"
fi

echo ""
printf "  ${WHITE}%-18s %-10s %-35s${NC}\n" "Component" "Status" "Version / Details"
printf "  %-18s %-10s %-35s\n" "──────────────────" "──────────" "───────────────────────────────────"

# Name|command (the command may contain `||` fallbacks for alt binary names)
# The ChatGPT row guards `sgpt --version` with the SAME condition the install
# step uses (key in the environment OR .sgptrc present). Unguarded, a first-run
# sgpt asks for an API key with getpass and would hang this table; and testing
# only for .sgptrc would skip the version for a user who exported the key.
CHECKS="
Homebrew|brew --version
Git|git --version
Node.js|node --version
npm|npm --version
Python|python3 --version
pipx|pipx --version
uv|uv --version
Claude Code|claude --version
OpenAI Codex|codex --version
ChatGPT Terminal|{ [ -n \"\${OPENAI_API_KEY:-}\" ] || test -f ~/.config/shell_gpt/.sgptrc; } && sgpt --version </dev/null || command -v sgpt
GitHub Copilot|copilot --version
GitHub CLI|gh --version
Netlify CLI|netlify --version
Cloudflare Wrangler|wrangler --version
aider|aider --version
llm|llm --version
llm-anthropic|llm plugins 2>/dev/null </dev/null | grep -o llm-anthropic | head -1
repomix|repomix --version
ast-grep|ast-grep --version
tldr|tldr --version
httpie|http --version
gron|gron --version
just|just --version
gemini-cli|gemini --version
aichat|aichat --version
ccusage|ccusage --version
ripgrep|rg --version
jq|jq --version
fd|fd --version || fdfind --version
ImageMagick|magick --version || convert --version
ffmpeg|ffmpeg -version
pandoc|pandoc --version
pdftotext|pdftotext -v
tesseract|tesseract --version
yt-dlp|yt-dlp --version
markitdown|markitdown --version
jc|jc -v
bat|bat --version || batcat --version
fzf|fzf --version
shellcheck|shellcheck --version
ruff|ruff --version
tmux|tmux -V
sqlite3|sqlite3 --version
"

PASS_COUNT=0
FAIL_COUNT=0
while IFS='|' read -r name cmd; do
  [ -z "$name" ] && continue
  # Capture stderr too (tesseract/pdftotext print version there) but drop shell
  # "command not found" noise (forced English via LC_ALL=C) and runtime errors,
  # so a missing/broken tool reads as empty instead of a false PASS.
  ver="$(LC_ALL=C bash -c "$cmd" 2>&1 | grep -viE 'command not found|no such file|dyld|library not loaded|symbol not found|abort trap|illegal instruction|killed|traceback' | head -1 || true)"
  padded_name="$(printf "%-18s" "$name")"
  if [ -n "$ver" ]; then
    printf "  %s ${GREEN}%-10s${NC} ${GRAY}%s${NC}\n" "$padded_name" "PASS" "${ver:0:35}"
    PASS_COUNT=$((PASS_COUNT + 1))
  else
    printf "  %s ${YELLOW}%-10s${NC} ${GRAY}%s${NC}\n" "$padded_name" "—" "not installed / skipped"
    FAIL_COUNT=$((FAIL_COUNT + 1))
  fi
done <<< "$CHECKS"

echo ""
echo -e "  ─────────────────────────────────────────────────────────────────"
echo -e "  ${GREEN}Installed/Present: $PASS_COUNT${NC}   ${GRAY}Not installed: $FAIL_COUNT${NC}"

# ═══════════════════════════════════════════════════════════════════════════════
# PHASE 5: QUICK REFERENCE GUIDE
# ═══════════════════════════════════════════════════════════════════════════════

show_progress "Complete"
section "PHASE 5: Quick Reference Guide"

echo -e "${CYAN}"
cat << 'GUIDE'

  ┌─────────────────────────────────────────────────────────────────┐
  │  HOW TO USE YOUR AI TERMINALS (macOS)                           │
  ├─────────────────────────────────────────────────────────────────┤
  │                                                                 │
  │  CLAUDE CODE        claude              start a session         │
  │                     claude "fix bug"    one-shot                │
  │                     claude doctor       diagnose config         │
  │  OPENAI CODEX       codex               OpenAI agentic coding   │
  │  CHATGPT TERMINAL   sgpt "question"     quick Q&A + shell help  │
  │  COPILOT CLI        copilot             shell command help      │
  │                                                                 │
  │  AI FRAMEWORKS                                                  │
  │  ─────────────                                                  │
  │  aider              AI pair programmer over your git repo       │
  │  llm "prompt"       one-shot prompt; llm -c to continue         │
  │  repomix            pack a repo into one file for any model     │
  │  files-to-prompt .  turn a folder into a single prompt          │
  │  gemini             Google Gemini in the terminal               │
  │  aichat             chat / REPL / shell assistant               │
  │  ccusage            usage report for your agent CLIs            │
  │                                                                 │
  │  CONVERSIONS (feed anything to an AI)                           │
  │  ───────────                                                    │
  │  markitdown f.docx  Office/PDF/images -> Markdown               │
  │  pdftotext f.pdf -   PDF -> text on stdout                      │
  │  tesseract img out  OCR an image/scan to text                   │
  │  pandoc a.md -o b.pdf   convert between doc formats             │
  │  jc -p df            command output -> JSON                     │
  │                                                                 │
  │  DEPLOY             netlify deploy --prod                       │
  │                                                                 │
  │  THE MAGIC COMBO                                                │
  │  cd /path/to/site && claude                                     │
  │  > improve the hero section, then deploy a draft to Netlify     │
  │                                                                 │
  │  MCP CONNECTORS (extend Claude Code)                            │
  │  claude mcp add --scope user playwright -- \                    │
  │      npx @playwright/mcp@latest                                 │
  │  claude mcp add --scope user filesystem -- npx -y \             │
  │      @modelcontextprotocol/server-filesystem ~/                 │
  │  claude mcp list                                                │
  │                                                                 │
  │  TIPS                                                           │
  │  - cd into your project BEFORE starting claude                  │
  │  - Open a NEW terminal after install so PATH updates load       │
  │  - Homebrew on Apple Silicon lives in /opt/homebrew             │
  │                                                                 │
  └─────────────────────────────────────────────────────────────────┘

GUIDE
echo -e "${NC}"

# ═══════════════════════════════════════════════════════════════════════════════
# FINAL SUMMARY
# ═══════════════════════════════════════════════════════════════════════════════

ELAPSED="$((SECONDS / 60))m$((SECONDS % 60))s"

echo -e "  ${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
echo -e "  ${GREEN}║                       KICKSTART COMPLETE                          ║${NC}"
echo -e "  ${GREEN}╠═══════════════════════════════════════════════════════════════════╣${NC}"
printf "  ${GREEN}║${NC}  ${WHITE}%-22s %-12s %-25s${NC}    ${GREEN}║${NC}\n" "Component" "Status" "Detail"
printf "  ${GREEN}║${NC}  %-22s %-12s %-25s    ${GREEN}║${NC}\n" "──────────────────────" "────────────" "─────────────────────────"

idx=0
while [ "$idx" -lt "${#RESULT_NAMES[@]}" ]; do
  name="${RESULT_NAMES[$idx]:0:22}"
  status="${RESULT_STATUS[$idx]}"
  detail="${RESULT_DETAIL[$idx]:0:25}"
  case "$status" in
    INSTALLED|PRESENT|OK)        color="$GREEN" ;;
    SKIPPED|PARTIAL|TRIGGERED|REGISTERED|UNKNOWN)   color="$YELLOW" ;;
    FAILED|ERROR)                color="$RED" ;;
    *)                           color="$GRAY" ;;
  esac
  printf "  ${GREEN}║${NC}  ${color}%-22s %-12s${NC} ${GRAY}%-25s${NC}    ${GREEN}║${NC}\n" "$name" "$status" "$detail"
  idx=$((idx + 1))
done

echo -e "  ${GREEN}║                                                                   ║${NC}"
echo -e "  ${GREEN}║  Time elapsed: $ELAPSED$(printf '%*s' $((51 - ${#ELAPSED})) '')║${NC}"
echo -e "  ${GREEN}║                                                                   ║${NC}"
echo -e "  ${GREEN}║  NEXT STEPS:                                                      ║${NC}"
echo -e "  ${GREEN}║   1. Open a NEW terminal (or: source your shell profile)          ║${NC}"
echo -e "  ${GREEN}║   2. Run 'claude' to start Claude Code (browser auth first time)  ║${NC}"
echo -e "  ${GREEN}║   3. Run 'copilot' then /login for GitHub Copilot                 ║${NC}"
echo -e "  ${GREEN}║   4. Run 'netlify login' to authenticate Netlify                  ║${NC}"
echo -e "  ${GREEN}║   5. cd into your project folder, then 'claude' to start!         ║${NC}"
echo -e "  ${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "  ${GRAY}Profile updated: $PROFILE_FILE  ·  Log: $LOG_FILE${NC}"
echo ""

Full PowerShell script

Expand Start-AITerminalKickstart.ps1 — Windows / PowerShell edition
#Requires -Version 5.1
<#
.SYNOPSIS
    AI Terminal Kickstart - One script to prep any Windows PC for AI-powered terminal work.

.DESCRIPTION
    Interactive, menu-driven installer that bootstraps a Windows PC with everything
    needed for the top 3 AI terminal solutions:

      1. Claude Code   (Anthropic)  - Terminal AI coding assistant
      2. ChatGPT CLI   (OpenAI)     - shell-gpt: ChatGPT as the 'sgpt' command
      3. GitHub Copilot CLI (GitHub) - AI-powered shell suggestions

    The script:
      - Self-upgrades to PowerShell 7 if running from PS 5.x
      - Installs all prerequisites (Git, Node.js, Python, etc.)
      - Runs your existing installer scripts as sub-modules
      - Provides a guided, color-coded experience any junior tech can follow
      - Validates every install step with logic tests
      - Generates a final health-check report

.PARAMETER AutoMode
    Skip all menus and install everything. Great for unattended setup.
    NOTE: -AutoMode (and -All) install US-operated tooling only. They do NOT
    install Kimi - that is opt-in via -WithKimi (see below).

.PARAMETER WithKimi
    OPT-IN. Additionally install the Kimi Code CLI (the standalone 'kimi'
    terminal) from Moonshot AI. Default is OFF and it is deliberately NOT
    pulled in by -AutoMode/-All, because Moonshot AI is a China-operated
    company and neither endpoint (api.moonshot.ai global, api.moonshot.cn
    mainland) publishes a Western data-residency or zero-retention guarantee.
    Users who want US-only models leave this off (the default) and Kimi is
    skipped entirely.

.PARAMETER SkipPowerShell7
    Don't attempt to upgrade PowerShell (stay on 5.x).

.PARAMETER ScriptsDir
    Directory containing your helper PS1 scripts. Defaults to the same folder as this script.

.EXAMPLE
    .\Start-AITerminalKickstart.ps1
    # Interactive menu - pick what you want

.EXAMPLE
    .\Start-AITerminalKickstart.ps1 -AutoMode
    # Install everything (US-operated stack only), no prompts (except API keys)

.EXAMPLE
    .\Start-AITerminalKickstart.ps1 -AutoMode -WithKimi
    # Everything PLUS the China-operated Kimi Code CLI (explicit opt-in)

.NOTES
    Author : AI Terminal ops
    Date   : 2026-08-18
    Tested : Windows Server 2022, Windows 10/11, PowerShell 5.1+

    US-only by default: without -WithKimi, no China-operated tooling is
    installed. Kimi (Moonshot AI) is the only opt-in addition and never rides
    along with -AutoMode/-All. See -WithKimi above for the data-residency caveat.

    Known issues + manual workarounds (Server 2022 / Win 11):

    1. Chocolatey install fails with:
         "Cannot overwrite variable result because the variable has been optimized."
       Cause:  Chocolatey's install.ps1 was written for Windows PowerShell 5.1
               and trips a PowerShell 7+ variable-optimization bug.
       Auto-fix in this script: detects PS 7+ and shells out to powershell.exe (5.1).
       Manual fallback (run from elevated Windows PowerShell 5.1):
         Set-ExecutionPolicy Bypass -Scope Process -Force; `
         [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072; `
         iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
       From pwsh 7+:
         & "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile `
           -ExecutionPolicy Bypass -Command "<the line above>"

    2. Python "not found after install":
       Cause:  Installer succeeded but $env:Path in the current shell doesn't see
               the new install dir yet (e.g. C:\Python313 for InstallAllUsers=1,
               or C:\Program Files\Python313 for winget system scope).
       Auto-fix in this script: Refresh-Path covers all common locations and the
               Python install falls back to the direct python.org installer.
       Manual fallback (refresh in current session):
         $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + `
                     [Environment]::GetEnvironmentVariable('Path','User')
       Manual install (direct python.org, no winget/choco needed):
         $v='3.13.15'; $u="https://www.python.org/ftp/python/$v/python-$v-amd64.exe"; `
         $f="$env:TEMP\python-$v-amd64.exe"; iwr $u -OutFile $f; `
         Start-Process $f -ArgumentList '/quiet','InstallAllUsers=1','PrependPath=1' -Wait
#>

[CmdletBinding()]
param(
    [switch]$AutoMode,
    [switch]$All,
    [switch]$WithKimi,
    [switch]$SkipPowerShell7,
    [string]$ScriptsDir = ""
)

# ═══════════════════════════════════════════════════════════════════════════════
#  CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════

$ErrorActionPreference = 'Continue'
Set-StrictMode -Version Latest

if (-not $ScriptsDir) {
    $ScriptsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
    if (-not $ScriptsDir) { $ScriptsDir = Get-Location }
}

$script:TempDir   = Join-Path $env:TEMP "ai-kickstart-$(Get-Date -Format 'yyyyMMdd')"
$script:LogFile   = Join-Path $script:TempDir "kickstart.log"
$script:Results   = [System.Collections.ArrayList]::new()
$script:StartTime = Get-Date
$script:PipxState = $null   # cached Initialize-Pipx result (Set-StrictMode needs it to exist)

# ═══════════════════════════════════════════════════════════════════════════════
#  HELPER FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════

function Write-Banner {
    $banner = @"

    ╔═══════════════════════════════════════════════════════════════════╗
    ║                                                                   ║
    ║         A I   T E R M I N A L   K I C K S T A R T                 ║
    ║         ─────────────────────────────────────────                 ║
    ║         One Script To Prep Them All                               ║
    ║                                                                   ║
    ║         Claude Code  |  ChatGPT CLI  |  GitHub Copilot CLI        ║
    ║                                                                   ║
    ╚═══════════════════════════════════════════════════════════════════╝

"@
    Write-Host $banner -ForegroundColor Magenta
}

function Write-Section {
    param([string]$Title)
    $line = "=" * 65
    Write-Host ""
    Write-Host "  $line" -ForegroundColor DarkCyan
    Write-Host "    $Title" -ForegroundColor Cyan
    Write-Host "  $line" -ForegroundColor DarkCyan
}

function Write-Step {
    param([string]$Msg)
    Write-Host "`n  >> $Msg" -ForegroundColor Cyan
    Add-Content -Path $script:LogFile -Value "[$(Get-Date -Format 'HH:mm:ss')] STEP: $Msg" -ErrorAction SilentlyContinue
}

function Write-OK {
    param([string]$Msg)
    Write-Host "     [OK] $Msg" -ForegroundColor Green
    Add-Content -Path $script:LogFile -Value "[$(Get-Date -Format 'HH:mm:ss')]  OK: $Msg" -ErrorAction SilentlyContinue
}

function Write-Warn {
    param([string]$Msg)
    Write-Host "     [WARN] $Msg" -ForegroundColor Yellow
    Add-Content -Path $script:LogFile -Value "[$(Get-Date -Format 'HH:mm:ss')] WARN: $Msg" -ErrorAction SilentlyContinue
}

function Write-Fail {
    param([string]$Msg)
    Write-Host "     [FAIL] $Msg" -ForegroundColor Red
    Add-Content -Path $script:LogFile -Value "[$(Get-Date -Format 'HH:mm:ss')] FAIL: $Msg" -ErrorAction SilentlyContinue
}

function Write-Info {
    param([string]$Msg)
    Write-Host "     $Msg" -ForegroundColor Gray
}

function Write-Tip {
    param([string]$Msg)
    Write-Host "     [TIP] $Msg" -ForegroundColor DarkYellow
}

function Add-Result {
    param([string]$Component, [string]$Status, [string]$Detail = "")
    [void]$script:Results.Add([PSCustomObject]@{
        Component = $Component
        Status    = $Status
        Detail    = $Detail
        Time      = Get-Date -Format 'HH:mm:ss'
    })
}

function Test-CmdExists {
    param([string]$Name)
    $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
}

function ConvertTo-HashtableDeep {
    # 'ConvertFrom-Json -AsHashtable' is PowerShell 7+ ONLY - on Windows
    # PowerShell 5.1 it throws ParameterBindingException, and a swallowed throw
    # there would leave us writing an EMPTY settings file over the user's own
    # config. So on 5.1 we parse with plain ConvertFrom-Json and walk the
    # resulting PSCustomObject into hashtables here, and the caller can treat
    # both hosts identically.
    param($InputObject)
    if ($null -eq $InputObject) { return $null }
    if ($InputObject -is [string] -or $InputObject -is [datetime] -or
        $InputObject -is [decimal] -or $InputObject.GetType().IsPrimitive) {
        return $InputObject
    }
    if ($InputObject -is [System.Collections.IDictionary]) {
        $copy = @{}
        foreach ($k in @($InputObject.Keys)) { $copy[$k] = ConvertTo-HashtableDeep $InputObject[$k] }
        return $copy
    }
    if ($InputObject -is [System.Collections.IEnumerable]) {
        # Leading comma keeps PowerShell from unrolling the array on return.
        return ,@(foreach ($item in $InputObject) { ConvertTo-HashtableDeep $item })
    }
    if ($InputObject.PSObject -and $InputObject.PSObject.Properties) {
        $copy = @{}
        foreach ($p in $InputObject.PSObject.Properties) { $copy[$p.Name] = ConvertTo-HashtableDeep $p.Value }
        return $copy
    }
    return $InputObject
}

function Test-GitForWindowsRoot {
    # A shim can steer the caller badly wrong: 'git' resolving through
    # C:\ProgramData\chocolatey\bin\git.exe walks two Split-Path hops up to
    # C:\ProgramData\chocolatey, whose bin\bash.exe may exist and belong to an
    # entirely different package. So require the actual Git for Windows layout -
    # bin\bash.exe PLUS git-bash.exe or cmd\git.exe at the expected position -
    # not just any bash.exe under the candidate root.
    param([string]$Root)
    if (-not $Root) { return $false }
    if (-not (Test-Path (Join-Path $Root "bin\bash.exe"))) { return $false }
    return ((Test-Path (Join-Path $Root "git-bash.exe")) -or (Test-Path (Join-Path $Root "cmd\git.exe")))
}

function Get-GitBashPath {
    # Git for Windows does NOT always live in %ProgramFiles%. An unelevated
    # "winget install --id Git.Git" (no --scope machine) drops it in
    # %LOCALAPPDATA%\Programs\Git instead, so a hardcoded ProgramFiles probe
    # misses it and Claude Code silently ends up with no shell.
    # Resolve from git.exe itself first (...\Git\cmd\git.exe -> ...\Git\bin\bash.exe),
    # then fall back to probing both roots. Returns $null if nothing is found.
    # Every candidate root is validated with Test-GitForWindowsRoot first, so a
    # shim on PATH (chocolatey, scoop) cannot point us at some other package's
    # bash.exe - if the resolved root is not a real Git for Windows layout we
    # fall through to the explicit ProgramFiles / LOCALAPPDATA probes.
    $gitCmd = Get-Command git -ErrorAction SilentlyContinue
    if ($gitCmd -and $gitCmd.Source) {
        $gitRoot = Split-Path -Parent (Split-Path -Parent $gitCmd.Source)
        if (Test-GitForWindowsRoot $gitRoot) { return (Join-Path $gitRoot "bin\bash.exe") }
    }
    $roots = @(
        "$env:ProgramFiles\Git",
        "${env:ProgramFiles(x86)}\Git",
        "$env:LOCALAPPDATA\Programs\Git"
    )
    foreach ($root in $roots) {
        if (Test-GitForWindowsRoot $root) { return (Join-Path $root "bin\bash.exe") }
    }
    return $null
}

function Refresh-Path {
    # Rebuild from registry but PRESERVE any paths added during this session
    $machine = [Environment]::GetEnvironmentVariable('Path', 'Machine')
    $user    = [Environment]::GetEnvironmentVariable('Path', 'User')
    $basePath = "$machine;$user"
    # Preserve session-added paths that aren't in the registry yet
    $currentParts = $env:Path -split ';' | Where-Object { $_ -ne '' }
    $baseParts    = $basePath -split ';' | Where-Object { $_ -ne '' }
    $sessionOnly  = $currentParts | Where-Object { $_ -notin $baseParts }
    $env:Path = $basePath
    foreach ($sp in $sessionOnly) {
        if ($env:Path -notlike "*$sp*") { $env:Path += ";$sp" }
    }
    # Also add common tool paths that might not be registered yet.
    # NOTE: python.org "InstallAllUsers=1" lands at C:\Python3xx (no Programs path),
    # winget for Python.Python.3.xx -e on system scope lands at C:\Program Files\Python3xx.
    # Per-user (default) winget lands at $env:LOCALAPPDATA\Programs\Python\Python3xx.
    $extras = @(
        "$env:ProgramFiles\Git\cmd",
        "$env:ProgramFiles\Git\bin",
        "$env:ProgramFiles\nodejs",
        "$env:USERPROFILE\.local\bin",
        "$env:USERPROFILE\.cargo\bin",
        "$env:APPDATA\npm",
        "$env:LOCALAPPDATA\Programs\Python\Python312",
        "$env:LOCALAPPDATA\Programs\Python\Python312\Scripts",
        "$env:LOCALAPPDATA\Programs\Python\Python313",
        "$env:LOCALAPPDATA\Programs\Python\Python313\Scripts",
        "C:\Python312",
        "C:\Python312\Scripts",
        "C:\Python313",
        "C:\Python313\Scripts",
        "$env:ProgramFiles\Python312",
        "$env:ProgramFiles\Python312\Scripts",
        "$env:ProgramFiles\Python313",
        "$env:ProgramFiles\Python313\Scripts",
        "${env:ProgramFiles(x86)}\Python312",
        "${env:ProgramFiles(x86)}\Python312\Scripts"
    )
    foreach ($p in $extras) {
        if ((Test-Path $p) -and $env:Path -notlike "*$p*") {
            $env:Path += ";$p"
        }
    }
}

function Test-IsAdmin {
    ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Test-IsPwsh7Plus {
    # PowerShell 7+ (Core) trips a known optimization bug in the Chocolatey
    # install.ps1 -> "Cannot overwrite variable result because the variable
    # has been optimized." Detect so we can shell out to Windows PowerShell 5.1.
    $PSVersionTable.PSEdition -eq 'Core' -and $PSVersionTable.PSVersion.Major -ge 7
}

function Get-WindowsPowerShell51 {
    # Returns the path to powershell.exe (Windows PowerShell 5.1) if present.
    $candidate = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe'
    if (Test-Path $candidate) { return $candidate }
    $cmd = Get-Command powershell.exe -ErrorAction SilentlyContinue
    if ($cmd) { return $cmd.Source }
    return $null
}

function Get-LatestPythonPatch {
    # Scrape https://www.python.org/ftp/python/ for the highest published
    # patch (e.g. 3.13.15) under the requested major.minor (e.g. 3.13).
    # Returns $null on any failure so callers can fall back to a pinned version.
    param([string]$MajorMinor = '3.13')
    try {
        $ProgressPreference = 'SilentlyContinue'
        $resp = Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/' -UseBasicParsing -TimeoutSec 30
        $escMM = [regex]::Escape($MajorMinor)
        # Match "3.13.<digits>/" only - excludes alpha/beta/rc tags
        $pattern = ">($escMM\.\d+)/<"
        $matches = [regex]::Matches($resp.Content, $pattern)
        if (-not $matches.Count) { return $null }
        $versions = $matches | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique
        $sorted   = $versions | Sort-Object { [version]$_ } -Descending
        return $sorted[0]
    }
    catch { return $null }
}

function Resolve-PythonInstallerUrl {
    # Returns @{ Version = '3.13.x'; Url = '...' } for the python.org direct
    # installer fallback. Tries auto-detection first, then the pinned
    # $script:PythonFullVersion as a safety net.
    param(
        [string]$MajorMinor = $script:PythonVersion,
        [string]$Pinned     = $script:PythonFullVersion
    )
    $ver = Get-LatestPythonPatch -MajorMinor $MajorMinor
    if (-not $ver) { $ver = $Pinned }
    if (-not $ver) { return $null }
    @{
        Version = $ver
        Url     = "https://www.python.org/ftp/python/$ver/python-$ver-amd64.exe"
    }
}

function Show-Progress {
    param(
        [string]$Activity,
        [string]$Status,
        [int]$Step,
        [int]$TotalSteps
    )
    $pct = [math]::Round(($Step / $TotalSteps) * 100)
    $barLen = 30
    $filled = [math]::Round($barLen * $Step / $TotalSteps)
    $empty  = $barLen - $filled
    $bar    = "$([char]0x2588)" * $filled + "$([char]0x2591)" * $empty
    Write-Host "`r  [$bar] $pct% - $Status                    " -NoNewline -ForegroundColor Cyan
    Write-Progress -Activity $Activity -Status "$Status ($pct%)" -PercentComplete $pct
}

function Complete-Progress {
    param([string]$Activity)
    Write-Progress -Activity $Activity -Completed
    Write-Host ""  # newline after progress bar
}

function Invoke-Download {
    # Download with progress display, retry on failure
    param(
        [string]$Uri,
        [string]$OutFile,
        [string]$Description = "Downloading",
        [int]$MaxRetries = 2
    )
    $attempt = 0
    while ($attempt -le $MaxRetries) {
        try {
            Write-Info "$Description (attempt $($attempt + 1))..."
            $ProgressPreference = 'SilentlyContinue'  # Invoke-WebRequest's built-in progress is slow
            Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing -TimeoutSec 120
            $ProgressPreference = 'Continue'
            $sizeKB = [math]::Round((Get-Item $OutFile).Length / 1KB)
            Write-OK "$Description complete (${sizeKB} KB)"
            return $true
        }
        catch {
            $attempt++
            if ($attempt -gt $MaxRetries) {
                Write-Fail "$Description failed after $($MaxRetries + 1) attempts: $_"
                return $false
            }
            Write-Warn "Download failed, retrying in 3 seconds... ($_)"
            Start-Sleep -Seconds 3
        }
    }
    return $false
}

function Initialize-Pipx {
    # pipx has no chocolatey or winget package - "python -m pip install --user
    # pipx" is the official Windows install path. Returns a hashtable so callers
    # can tell "pipx works" from "pipx is missing" instead of guessing:
    #   Ok   - $true only when pipx can actually be invoked
    #   Mode - 'pipx' (shim on PATH), 'module' ('python -m pipx'), or 'none'
    #   Log  - file holding the pip output when the bootstrap failed
    # The result is cached BOTH ways - success and failure. Phase 3 (shell-gpt)
    # and Phase 3h (the CLI apps) both need pipx, and a bootstrap that already
    # failed must not be re-run for every later caller either.
    if ($null -ne $script:PipxState) { return $script:PipxState }

    if (Test-CmdExists 'pipx') {
        $script:PipxState = @{ Ok = $true; Mode = 'pipx'; Log = "" }
        return $script:PipxState
    }
    if (-not (Test-CmdExists 'python')) {
        $script:PipxState = @{ Ok = $false; Mode = 'none'; Log = "" }
        return $script:PipxState
    }

    $pipxLog = Join-Path $script:TempDir "pipx-bootstrap.log"
    $pipOk   = $false
    try {
        python -m pip install --user pipx 2>&1 | Tee-Object -FilePath $pipxLog | Out-Null
        $pipOk = ($LASTEXITCODE -eq 0)
    }
    catch {
        "$_" | Out-File -FilePath $pipxLog -Append -Encoding UTF8
    }
    if ($pipOk) {
        try { python -m pipx ensurepath 2>&1 | Out-Null } catch {}
        Refresh-Path
    }

    if (Test-CmdExists 'pipx') {
        $script:PipxState = @{ Ok = $true; Mode = 'pipx'; Log = $pipxLog }
        return $script:PipxState
    }
    # The shim may not be on PATH until a new shell, but 'python -m pipx' works
    # right now if the package landed - that is a usable pipx, so say so.
    $moduleOk = $false
    try {
        python -m pipx --version 2>&1 | Out-Null
        $moduleOk = ($LASTEXITCODE -eq 0)
    }
    catch {}
    if ($moduleOk) { $script:PipxState = @{ Ok = $true; Mode = 'module'; Log = $pipxLog } }
    else           { $script:PipxState = @{ Ok = $false; Mode = 'none';  Log = $pipxLog } }
    return $script:PipxState
}

function Invoke-PipxInstall {
    # Installs one package through whichever pipx entry point Initialize-Pipx
    # found, capturing the output. Returns $true only when the install command
    # itself exited 0 - the caller still has to check that the binary appeared.
    param(
        [hashtable]$Pipx,
        [string]$Package,
        [string]$LogFile
    )
    if (-not $Pipx.Ok) { return $false }
    try {
        if ($Pipx.Mode -eq 'pipx') { pipx install $Package 2>&1 | Tee-Object -FilePath $LogFile | Out-Null }
        else { python -m pipx install $Package 2>&1 | Tee-Object -FilePath $LogFile | Out-Null }
        return ($LASTEXITCODE -eq 0)
    }
    catch {
        "$_" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        return $false
    }
}

function Get-IndependentTlsClient {
    # Finds an HTTPS client that carries its OWN CA bundle, so a corporate
    # inspection root installed in the Windows store does NOT silently make it
    # trust the proxy. Windows' built-in curl.exe is Schannel-based (it uses the
    # Windows store) so it does NOT qualify here - it is only used for the
    # insecure "-k" comparison in Test-TlsInterception.
    $roots = @(
        "$env:ProgramFiles\Git",
        "${env:ProgramFiles(x86)}\Git",
        "$env:LOCALAPPDATA\Programs\Git"
    )
    foreach ($root in $roots) {
        $c = Join-Path $root "mingw64\bin\curl.exe"     # Git for Windows curl: OpenSSL + its own ca-bundle.crt
        if (Test-Path $c) { return @{ Kind = 'Git curl'; Path = $c; Mode = 'curl' } }
    }
    $node = Get-Command node -ErrorAction SilentlyContinue
    if ($node -and $node.Source) { return @{ Kind = 'node'; Path = $node.Source; Mode = 'node' } }
    return $null
}

function Test-TlsInterception {
    # Returns @{ Verdict; Client; Detail } where Verdict is one of:
    #   CLEAN       - a client with its own CA bundle verified the endpoint
    #   INTERCEPTED - verification failed but the same request worked with -k
    #   CERT-ERROR  - verification failed and the insecure retry failed too
    #   NO-CONNECT  - never got a connection (DNS, proxy, firewall, captive portal)
    #   UNKNOWN     - no independent-CA client available to ask
    param([string]$Url = 'https://registry.npmjs.org/')

    $client = Get-IndependentTlsClient
    if (-not $client) {
        return @{ Verdict = 'UNKNOWN'; Client = 'none'; Detail = 'no client with its own CA bundle is installed yet' }
    }

    $probeOut = Join-Path $script:TempDir "tls-probe.out"
    $verified = $false
    $certFail = $false
    $connFail = $false
    $detail   = ""

    if ($client.Mode -eq 'curl') {
        & $client.Path -sS --max-time 15 -o $probeOut $Url 2>&1 | Out-Null
        $code = $LASTEXITCODE
        if ($code -eq 0) { $verified = $true }
        elseif (@(35, 51, 58, 60, 77, 91) -contains $code) { $certFail = $true; $detail = "curl exit $code - certificate verification failed" }
        elseif (@(5, 6, 7, 28) -contains $code)            { $connFail = $true; $detail = "curl exit $code - could not connect" }
        else { $detail = "curl exit $code" }
    }
    else {
        # node's https uses its own bundled CA set, not the Windows store.
        $js = "require('https').get(process.argv[1],r=>{console.log('TLSOK');r.destroy()}).on('error',e=>console.log('TLSERR '+(e.code||e.message)))"
        $out = ""
        try { $out = (& $client.Path -e $js $Url 2>&1 | Out-String) } catch { $out = "$_" }
        if ($out -match 'TLSOK') { $verified = $true }
        elseif ($out -match 'TLSERR\s+(\S+)') {
            $err = $Matches[1]
            $detail = "node error $err"
            if ($err -match 'CERT|SIGN|SSL|VERIFY')                              { $certFail = $true }
            elseif ($err -match 'ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN')    { $connFail = $true }
        }
        else { $detail = "node probe returned no usable answer" }
    }

    if ($verified) { return @{ Verdict = 'CLEAN';      Client = $client.Kind; Detail = 'verified against the client''s own CA bundle' } }
    if ($connFail) { return @{ Verdict = 'NO-CONNECT'; Client = $client.Kind; Detail = $detail } }
    if ($certFail) {
        # Separate "the network is rewriting certificates" from "the host is
        # unreachable": if the SAME request succeeds with verification turned
        # off, bytes are flowing and only the certificate was wrong.
        $insecureOk = $false
        $sysCurl = Get-Command curl.exe -ErrorAction SilentlyContinue
        if ($sysCurl -and $sysCurl.Source) {
            & $sysCurl.Source -sS -k --max-time 15 -o $probeOut $Url 2>&1 | Out-Null
            if ($LASTEXITCODE -eq 0) { $insecureOk = $true }
        }
        if ($insecureOk) { return @{ Verdict = 'INTERCEPTED'; Client = $client.Kind; Detail = $detail } }
        return @{ Verdict = 'CERT-ERROR'; Client = $client.Kind; Detail = $detail }
    }
    return @{ Verdict = 'UNKNOWN'; Client = $client.Kind; Detail = $detail }
}

function Show-TlsVerdict {
    # Prints a Test-TlsInterception result. -Final marks the DEFINITIVE pass -
    # the one that runs after Git for Windows and Node.js are installed, because
    # those are the only two clients on this box that carry their own CA bundle.
    # Only the final pass writes a summary row, so the report never shows two
    # contradicting TLS lines.
    param([hashtable]$Tls, [switch]$Final)

    switch ($Tls.Verdict) {
        'CLEAN' {
            Write-OK "No TLS interception detected (probed with $($Tls.Client), which uses its own CA bundle)"
            if ($Final) { Add-Result "TLS inspection" "OK" "None detected" }
        }
        'INTERCEPTED' {
            Write-Fail "This network is INSPECTING TLS."
            Write-Info "Certificate verification failed for $($Tls.Client) ($($Tls.Detail)), but the"
            Write-Info "same request succeeded with verification turned off - so the bytes flow"
            Write-Info "and only the certificate is being rewritten by a proxy."
            Write-Info "Windows itself will look fine, because the proxy's root is trusted via"
            Write-Info "GPO. These will NOT work until you point them at that same root:"
            Write-Info "  npm and node          (bundled OpenSSL CA set)"
            Write-Info "  pip, requests, httpx, uv, pipx  (certifi)"
            Write-Info "  Git for Windows       (its own ca-bundle.crt)"
            Write-Tip "Get the proxy root .crt from IT, then:"
            Write-Tip "  npm config set cafile C:\path\to\corp-root.crt"
            Write-Tip "  git config --global http.sslCAInfo C:\path\to\corp-root.crt"
            Write-Tip "  setx NODE_EXTRA_CA_CERTS C:\path\to\corp-root.crt"
            Write-Tip "  setx REQUESTS_CA_BUNDLE C:\path\to\corp-root.crt   (also SSL_CERT_FILE for uv/pipx)"
            if ($Final) { Add-Result "TLS inspection" "FAILED" "Network inspects TLS" }
        }
        'CERT-ERROR' {
            Write-Warn "Certificate verification failed ($($Tls.Detail)) and the insecure retry failed too."
            Write-Info "That reads as a blocked or broken connection rather than TLS inspection."
            if ($Final) { Add-Result "TLS inspection" "PARTIAL" "Cert verify failed" }
        }
        'NO-CONNECT' {
            Write-Warn "Could not connect at all ($($Tls.Detail))."
            Write-Info "This is a DNS, proxy, firewall or captive-portal problem - NOT a certificate problem."
            if ($Final) { Add-Result "TLS inspection" "PARTIAL" "Could not connect" }
        }
        default {
            Write-Warn "TLS inspection could NOT be checked: $($Tls.Detail)."
            Write-Info "Everything else on this box used the Windows trust store, which cannot see"
            Write-Info "a proxy root installed by GPO. Read this as UNKNOWN - it is NOT a finding"
            Write-Info "that the network is clean."
            if ($Final) {
                Write-Info "Nothing here carries its own CA bundle. Git for Windows or Node.js would;"
                Write-Info "install either one and re-run to get a real answer."
                Add-Result "TLS inspection" "UNKNOWN" "No independent client"
            }
            else {
                Write-Info "Expected on a fresh box - Git and Node.js are not installed yet. The"
                Write-Info "definitive check runs again at the end of Phase 2, once they are."
            }
        }
    }
}

# Version-flexible package names (auto-detect latest where possible)
#
# WHY one Python pin survives: winget has NO version-less Python id.
# "Python.Python.3" is a namespace DIRECTORY, not an installable package - only
# Python.Python.3.12 / .3.13 / .3.14 exist - so the winget path has to name a
# minor. Chocolatey's "python" package already floats to the current release, so
# that path is unpinned, and the python.org fallback auto-detects the newest
# patch. Change $script:PythonVersion alone to move the winget target.
$script:PythonVersion     = "3.13"     # Change this single value to update the winget Python target
$script:PythonFullVersion = "3.13.15"  # Safety-net pin for python.org direct-download fallback (auto-detector tries the latest first)
$script:PythonWingetId    = "Python.Python.$($script:PythonVersion)"
$script:PythonChocoId     = "python"    # Chocolatey's 'python' floats to the current release - deliberately not pinned
$script:NodeMajor         = "22"       # Change this to update Node.js LTS target

# Phase tracking for progress bar
$script:TotalPhases  = 6
$script:CurrentPhase = 0

function Enter-Phase {
    param([string]$Name)
    $script:CurrentPhase++
    Show-Progress -Activity "AI Terminal Kickstart" -Status "Phase $($script:CurrentPhase)/$($script:TotalPhases): $Name" -Step $script:CurrentPhase -TotalSteps $script:TotalPhases
}

function Get-UserChoice {
    param(
        [string]$Prompt,
        [string[]]$Options,
        [int]$Default = 0
    )
    if ($AutoMode) { return $Default }

    Write-Host ""
    for ($i = 0; $i -lt $Options.Count; $i++) {
        $marker = if ($i -eq $Default) { "*" } else { " " }
        $color  = if ($i -eq $Default) { "White" } else { "Gray" }
        Write-Host "     [$($i+1)]$marker $($Options[$i])" -ForegroundColor $color
    }
    Write-Host ""
    Write-Host "  $Prompt [$($Default+1)]: " -NoNewline -ForegroundColor Yellow
    $input = Read-Host
    if ($input -match '^\d+$') {
        $idx = [int]$input - 1
        if ($idx -ge 0 -and $idx -lt $Options.Count) { return $idx }
    }
    return $Default
}

function Get-YesNo {
    param([string]$Prompt, [bool]$Default = $true)
    if ($AutoMode) { return $Default }
    $hint = if ($Default) { "Y/n" } else { "y/N" }
    Write-Host "  $Prompt ($hint): " -NoNewline -ForegroundColor Yellow
    $answer = Read-Host
    if ([string]::IsNullOrWhiteSpace($answer)) { return $Default }
    return ($answer -match '^[Yy]')
}

function Read-MaskedInput {
    # Reads a secret WITHOUT echoing it to the screen or the scrollback.
    # PowerShell 7.1+ has 'Read-Host -MaskInput'; Windows PowerShell 5.1 only has
    # -AsSecureString, so there we read a SecureString and convert it back (we
    # need the plain value to write it into the user environment).
    if ((Get-Command Read-Host).Parameters.ContainsKey('MaskInput')) {
        return (Read-Host -MaskInput)
    }
    $secure = Read-Host -AsSecureString
    if (-not $secure) { return "" }
    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
    try {
        return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
    }
    finally {
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
    }
}

function Invoke-SubScript {
    param(
        [string]$Name,
        [string]$FileName,
        [hashtable]$Params = @{}
    )
    $path = Join-Path $ScriptsDir $FileName
    if (-not (Test-Path $path)) {
        Write-Warn "Script not found: $path"
        Write-Info "Skipping $Name - file missing from scripts directory"
        Add-Result $Name "SKIPPED" "Script file not found"
        return $false
    }
    Write-Info "Running: $FileName"
    try {
        & $path @Params
        Add-Result $Name "RAN" "Sub-script executed"
        return $true
    }
    catch {
        Write-Fail "$Name script error: $_"
        Add-Result $Name "ERROR" "$_"
        return $false
    }
}

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 0 - INITIALIZATION
# ═══════════════════════════════════════════════════════════════════════════════

if (-not (Test-Path $script:TempDir)) {
    New-Item -Path $script:TempDir -ItemType Directory -Force | Out-Null
}

Write-Banner

# Show system info
$osInfo  = Get-CimInstance Win32_OperatingSystem
$psVer   = $PSVersionTable.PSVersion
$isAdmin = Test-IsAdmin

Write-Host "  System Info:" -ForegroundColor White
Write-Host "     OS          : $($osInfo.Caption)" -ForegroundColor Gray
Write-Host "     Build       : $($osInfo.BuildNumber)" -ForegroundColor Gray
Write-Host "     PowerShell  : $psVer" -ForegroundColor Gray
Write-Host "     Admin       : $isAdmin" -ForegroundColor $(if ($isAdmin) { 'Green' } else { 'Yellow' })
Write-Host "     Scripts Dir : $ScriptsDir" -ForegroundColor Gray
Write-Host "     Log File    : $($script:LogFile)" -ForegroundColor Gray
Write-Host ""

if (-not $isAdmin) {
    Write-Host "  ┌──────────────────────────────────────────────────────────────┐" -ForegroundColor Yellow
    Write-Host "  │  WARNING: Not running as Administrator.                      │" -ForegroundColor Yellow
    Write-Host "  │  Some installations may fail or prompt UAC.                  │" -ForegroundColor Yellow
    Write-Host "  │  Right-click PowerShell > Run as Administrator for best      │" -ForegroundColor Yellow
    Write-Host "  │  results.                                                    │" -ForegroundColor Yellow
    Write-Host "  └──────────────────────────────────────────────────────────────┘" -ForegroundColor Yellow
    Write-Host ""
}

# ─── Pre-flight: TLS, Network, Disk, Admin, Repos ─────────────────────────────
Enter-Phase "Pre-flight Checks"
if ($All) { $AutoMode = $true }
if (-not $AutoMode) {
    $modeChoice = Get-UserChoice -Prompt "Install mode" -Options @(
        "Everything - install the full recommended stack, unattended",
        "Choose - ask before each component"
    ) -Default 1
    if ($modeChoice -eq 0) { $AutoMode = $true; Write-OK "Mode: install everything (unattended)." }
}

Write-Section "PRE-FLIGHT CHECKS"

# Force TLS 1.2+ for all web requests in this session
Write-Step "Enforcing TLS 1.2+ for all downloads"
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13
Write-OK "TLS 1.2/1.3 enforced for all Invoke-WebRequest/RestMethod calls"
Add-Result "TLS Security" "OK" "TLS 1.2+"

# Admin access validation
Write-Step "Checking administrator privileges"
if ($isAdmin) {
    Write-OK "Running as Administrator - full install capability"
    Add-Result "Admin Access" "OK" "Elevated"
}
else {
    Write-Warn "NOT running as Administrator"
    Write-Info "The following will fail without admin: Chocolatey, execution policy,"
    Write-Info "system PATH changes, some MSI installers."
    Write-Info "Recommendation: Close and re-run as Administrator."
    Add-Result "Admin Access" "WARN" "Not elevated"
}

# Network connectivity - test multiple download sources
Write-Step "Testing internet connectivity to download sources"
$script:NetworkOK = $true
$endpoints = @(
    @{ Name = "GitHub API";       URL = "https://api.github.com" },
    @{ Name = "Node.js";          URL = "https://nodejs.org/dist/index.json" },
    @{ Name = "Claude installer"; URL = "https://claude.ai" },
    @{ Name = "Chocolatey";       URL = "https://community.chocolatey.org" },
    @{ Name = "npm registry";     URL = "https://registry.npmjs.org" },
    @{ Name = "PyPI";             URL = "https://pypi.org" }
)
$reachable = 0
$unreachable = 0
foreach ($ep in $endpoints) {
    try {
        $null = Invoke-WebRequest -Uri $ep.URL -UseBasicParsing -TimeoutSec 8 -Method Head -ErrorAction Stop
        Write-OK "$($ep.Name) reachable"
        $reachable++
    }
    catch {
        Write-Warn "$($ep.Name) ($($ep.URL)) - UNREACHABLE"
        $unreachable++
    }
}
if ($unreachable -gt 0) {
    Write-Warn "$unreachable of $($endpoints.Count) download sources unreachable."
    Write-Info "Check your network, proxy, or firewall settings."
    if ($unreachable -ge 4) {
        Write-Fail "Most download sources are unreachable. Script will likely fail."
        $script:NetworkOK = $false
    }
}
else {
    Write-OK "All $reachable download sources reachable"
    Write-Info "That used Invoke-WebRequest, which trusts the WINDOWS certificate store."
    Write-Info "The TLS check below is what tells you whether npm, pip and Git - which"
    Write-Info "each ship their own CA bundle - will also work on this network."
}
Add-Result "Network" $(if ($unreachable -eq 0) { "OK" } elseif ($unreachable -lt 4) { "PARTIAL" } else { "FAILED" }) "$reachable/$($endpoints.Count) reachable"

# TLS interception check
# The probes above trust the Windows certificate store - exactly where a
# Zscaler / Netskope / Palo Alto root lands via GPO. That is why they can report
# "all reachable" on a box where npm (bundled OpenSSL CA set), Python
# requests/httpx (certifi) and Git for Windows all fail on the same network.
# Re-probe with a client that carries its OWN CA bundle.
# This EARLY pass is a heads-up only, and it deliberately records no summary
# row: Get-IndependentTlsClient only knows about Git for Windows' mingw64 curl
# and node, and on a fresh box neither exists yet - so at this point the probe
# usually has nothing to ask and returns UNKNOWN. The pass that counts runs at
# the end of Phase 2, after the runtimes are installed.
Write-Step "Checking whether this network inspects TLS (preliminary)"
Show-TlsVerdict -Tls (Test-TlsInterception)

# Disk space check
Write-Step "Checking available disk space"
try {
    $sysDrive = (Get-Item $env:SystemRoot).PSDrive
    $freeGB = [math]::Round(($sysDrive.Free / 1GB), 1)
    $totalGB = [math]::Round(($sysDrive.Used + $sysDrive.Free) / 1GB, 1)
    if ($freeGB -lt 2) {
        Write-Fail "CRITICAL: Only $freeGB GB free on $($sysDrive.Name): ($totalGB GB total). Need at least 2 GB."
        Write-Info "Free up disk space before continuing."
        Add-Result "Disk Space" "FAILED" "$freeGB GB free"
    }
    elseif ($freeGB -lt 5) {
        Write-Warn "Low: $freeGB GB free on $($sysDrive.Name): ($totalGB GB total). Recommend 5+ GB."
        Add-Result "Disk Space" "WARN" "$freeGB GB free"
    }
    else {
        Write-OK "Disk space: $freeGB GB free on $($sysDrive.Name): ($totalGB GB total)"
        Add-Result "Disk Space" "OK" "$freeGB GB free"
    }
}
catch {
    # Fallback for non-standard PSDrive
    Write-Warn "Could not determine disk space: $_"
    Add-Result "Disk Space" "UNKNOWN" ""
}

# Log session separator
Add-Content -Path $script:LogFile -Value "`n$('=' * 70)" -ErrorAction SilentlyContinue
Add-Content -Path $script:LogFile -Value "Session started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ErrorAction SilentlyContinue
Add-Content -Path $script:LogFile -Value "OS: $($osInfo.Caption) Build $($osInfo.BuildNumber) | PS: $psVer | Admin: $isAdmin" -ErrorAction SilentlyContinue
Add-Content -Path $script:LogFile -Value "$('=' * 70)" -ErrorAction SilentlyContinue

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 1 - POWERSHELL 7 CHECK / UPGRADE
# ═══════════════════════════════════════════════════════════════════════════════

Enter-Phase "PowerShell & Core Prerequisites"
Write-Section "PHASE 1: PowerShell Version Check"

if ($psVer.Major -ge 7) {
    Write-OK "Already running PowerShell $psVer - no upgrade needed"
    Add-Result "PowerShell 7" "PRESENT" "v$psVer"
}
elseif ($SkipPowerShell7) {
    Write-Warn "PowerShell 7 upgrade skipped (--SkipPowerShell7 flag)"
    Add-Result "PowerShell 7" "SKIPPED" "User opted out"
}
else {
    Write-Step "You are on PowerShell $psVer (Windows built-in)"
    Write-Info "PowerShell 7 is recommended for AI terminals. It offers:"
    Write-Info "  - Better performance and modern language features"
    Write-Info "  - Native JSON/REST support improvements"
    Write-Info "  - Cross-platform compatibility"
    Write-Info "  - Required by some advanced Claude Code features"
    Write-Host ""

    if (Get-YesNo "Install PowerShell 7?") {
        Write-Step "Installing PowerShell 7..."

        $installed = $false

        # Try winget first
        if (Test-CmdExists 'winget') {
            Write-Info "Using winget to install PowerShell 7..."
            try {
                $result = winget install --id Microsoft.PowerShell --accept-source-agreements --accept-package-agreements --silent 2>&1
                if ($LASTEXITCODE -eq 0 -or ($result -match 'already installed')) {
                    $installed = $true
                    Write-OK "PowerShell 7 installed via winget"
                }
            }
            catch { Write-Info "winget method failed, trying MSI..." }
        }

        # Fallback to MSI download
        if (-not $installed) {
            Write-Info "Downloading PowerShell 7 MSI from GitHub..."
            try {
                $releases = Invoke-RestMethod 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest'
                $msiAsset = $releases.assets | Where-Object { $_.name -match 'win-x64\.msi$' } | Select-Object -First 1
                if ($msiAsset) {
                    $msiPath = Join-Path $script:TempDir $msiAsset.name
                    Invoke-WebRequest -Uri $msiAsset.browser_download_url -OutFile $msiPath -UseBasicParsing
                    Write-OK "Downloaded: $($msiAsset.name)"

                    Write-Info "Installing silently..."
                    $msiArgs = "/i `"$msiPath`" /qn /norestart ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=0 REGISTER_MANIFEST=1 USE_MU=1 ENABLE_MU=1"
                    # msiexec's exit code is the only signal here. 1641 and 3010
                    # are "succeeded, reboot pending", not failures.
                    $msiProc = Start-Process msiexec.exe -ArgumentList $msiArgs -Wait -NoNewWindow -PassThru
                    if (@(0, 1641, 3010) -contains $msiProc.ExitCode) {
                        $installed = $true
                        Write-OK "PowerShell 7 MSI reported success"
                    }
                    else {
                        Write-Fail "msiexec exited with code $($msiProc.ExitCode)"
                    }
                }
            }
            catch {
                Write-Fail "MSI download/install failed: $_"
            }
        }

        # An installer that "reported success" is not the same as pwsh.exe being
        # on the box, so look before reporting INSTALLED.
        Refresh-Path
        $pwshHere = (Test-CmdExists 'pwsh') -or (Test-Path "$env:ProgramFiles\PowerShell\7\pwsh.exe")
        if ($pwshHere) {
            Write-OK "pwsh.exe is present"
            Add-Result "PowerShell 7" "INSTALLED" "Restart terminal to use pwsh.exe"
        }
        elseif ($installed) {
            # The installer itself exited 0, so a stale PATH is the only thing
            # left to blame. That is the one case where PARTIAL is honest.
            Write-Warn "PowerShell 7 installed but pwsh.exe is not visible yet - restart your terminal"
            Add-Result "PowerShell 7" "PARTIAL" "Restart terminal"
        }
        else {
            Add-Result "PowerShell 7" "FAILED" "Manual install needed"
            Write-Warn "Install manually from: https://aka.ms/powershell-release?tag=stable"
        }
        if ($pwshHere -or $installed) {
            Write-Host ""
            Write-Tip "After this script finishes, open a NEW terminal and type 'pwsh' to use PS7."
            Write-Tip "You can also right-click > 'Open in Terminal' for Windows Terminal with PS7."
        }
    }
    else {
        Write-Info "Staying on PowerShell $psVer"
        Add-Result "PowerShell 7" "SKIPPED" "User declined"
    }
}

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 2 - CORE PREREQUISITES
# ═══════════════════════════════════════════════════════════════════════════════

Write-Section "PHASE 2: Core Prerequisites"

# ─── 2a. Execution Policy ─────────────────────────────────────────────────────
Write-Step "Checking execution policy"
$policy = Get-ExecutionPolicy -Scope LocalMachine
if ($policy -in 'Restricted', 'AllSigned') {
    if ($isAdmin) {
        Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force
        Write-OK "Execution policy set to RemoteSigned"
    }
    else {
        Write-Warn "Execution policy is '$policy' - run as Admin to change it"
    }
}
else {
    Write-OK "Execution policy is '$policy'"
}
Add-Result "Execution Policy" "OK" $policy

# ─── 2b. Package Manager (Chocolatey) ─────────────────────────────────────────
Write-Step "Checking Chocolatey package manager"
if (Test-CmdExists 'choco') {
    $chocoVer = choco --version 2>$null
    Write-OK "Chocolatey already installed ($chocoVer)"
    Add-Result "Chocolatey" "PRESENT" "v$chocoVer"
}
else {
    Write-Info "Chocolatey is used to install many developer tools automatically."
    if (Get-YesNo "Install Chocolatey?") {
        $chocoInstalled = $false
        $chocoErr = $null

        # Chocolatey's install.ps1 trips a PowerShell 7+ optimization bug
        # ("Cannot overwrite variable result because the variable has been
        # optimized.") - so when running under pwsh 7+, we shell out to
        # Windows PowerShell 5.1 which the script was authored against.
        if (Test-IsPwsh7Plus) {
            $ps5 = Get-WindowsPowerShell51
            if ($ps5) {
                Write-Info "Detected PowerShell 7+ - running Chocolatey installer via Windows PowerShell 5.1..."
                try {
                    $ps5Cmd = @"
Set-ExecutionPolicy Bypass -Scope Process -Force
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
"@
                    & $ps5 -NoProfile -ExecutionPolicy Bypass -Command $ps5Cmd
                    if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq $null) { $chocoInstalled = $true }
                }
                catch { $chocoErr = $_ }
            }
            else {
                Write-Warn "Windows PowerShell 5.1 (powershell.exe) not found - falling back to in-process install"
            }
        }

        # In-process install (Windows PowerShell 5.1, or fallback if PS 5.1 was missing)
        if (-not $chocoInstalled) {
            try {
                [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
                Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
                $chocoInstalled = $true
            }
            catch { $chocoErr = $_ }
        }

        Refresh-Path
        if ($chocoInstalled -and (Test-CmdExists 'choco')) {
            choco feature enable -n allowGlobalConfirmation 2>$null | Out-Null
            Write-OK "Chocolatey installed"
            Add-Result "Chocolatey" "INSTALLED" ""
        }
        else {
            Write-Fail "Chocolatey install failed: $chocoErr"
            Write-Tip "Manual one-liner workaround (run in an elevated Windows PowerShell 5.1, NOT pwsh 7):"
            Write-Info "  Set-ExecutionPolicy Bypass -Scope Process -Force; [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
            Write-Tip "Or from pwsh 7+, shell out to Windows PowerShell 5.1:"
            Write-Info "  & `"`$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe`" -NoProfile -ExecutionPolicy Bypass -Command `"Set-ExecutionPolicy Bypass -Scope Process -Force; [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))`""
            Add-Result "Chocolatey" "FAILED" "$chocoErr"
        }
    }
    else {
        Add-Result "Chocolatey" "SKIPPED" "User declined"
    }
}

# ─── 2c. Git for Windows ──────────────────────────────────────────────────────
Write-Step "Checking Git for Windows"
Refresh-Path
if (Test-CmdExists 'git') {
    $gitVer = (git --version) -replace 'git version ', ''
    Write-OK "Git already installed ($gitVer)"
    $gitBashFound = Get-GitBashPath
    if ($gitBashFound) { Write-Info "Git Bash: $gitBashFound" }
    else { Write-Warn "Git Bash (bash.exe) not found under this Git install - Claude Code needs it" }
    Add-Result "Git" "PRESENT" "v$gitVer"
}
else {
    Write-Info "Git is REQUIRED for Claude Code and most AI terminal tools."
    Write-Info "It also provides Git Bash which Claude Code uses on Windows."
    if (Get-YesNo "Install Git for Windows?" $true) {
        $gitInstalled = $false

        # Try winget first
        if (Test-CmdExists 'winget') {
            Write-Info "Installing via winget..."
            try {
                winget install --id Git.Git --accept-source-agreements --accept-package-agreements --silent 2>$null
                if ($LASTEXITCODE -eq 0) { $gitInstalled = $true }
            }
            catch { Write-Info "winget install failed: $_ - trying next method..." }
        }

        # Try Chocolatey
        if (-not $gitInstalled -and (Test-CmdExists 'choco')) {
            Write-Info "Installing via Chocolatey..."
            try {
                choco install git --yes --no-progress
                $gitInstalled = $true
            }
            catch { Write-Info "Chocolatey install failed: $_ - trying direct download..." }
        }

        # Direct download fallback (auto-detect latest version from GitHub API)
        if (-not $gitInstalled) {
            Write-Info "Downloading Git from GitHub (auto-detecting latest release)..."
            try {
                $gitRelease = Invoke-RestMethod 'https://api.github.com/repos/git-for-windows/git/releases/latest'
                $gitAsset = $gitRelease.assets | Where-Object { $_.name -match '64-bit\.exe$' -and $_.name -notmatch 'portable' } | Select-Object -First 1
                if ($gitAsset) {
                    $gitInstaller = Join-Path $script:TempDir $gitAsset.name
                    $dlOk = Invoke-Download -Uri $gitAsset.browser_download_url -OutFile $gitInstaller -Description "Git for Windows ($($gitAsset.name))"
                    if (-not $dlOk) { throw "Download failed" }
                    $gitArgs = '/VERYSILENT', '/NORESTART', '/SP-', '/CLOSEAPPLICATIONS'
                    Start-Process -FilePath $gitInstaller -ArgumentList $gitArgs -Wait -NoNewWindow
                    $gitInstalled = $true
                }
                else {
                    Write-Fail "Could not find Git installer asset in release"
                }
            }
            catch {
                Write-Fail "Git download failed: $_"
            }
        }

        Refresh-Path
        if (Test-CmdExists 'git') {
            Write-OK "Git installed: $(git --version)"
            Add-Result "Git" "INSTALLED" (git --version)
        }
        else {
            Write-Fail "Git not found after install attempt"
            Add-Result "Git" "FAILED" "Not on PATH"
        }
    }
    else {
        Write-Warn "Skipping Git - Claude Code WILL NOT WORK without it!"
        Add-Result "Git" "SKIPPED" "User declined (WARNING)"
    }
}

# ─── 2d. Node.js ──────────────────────────────────────────────────────────────
Write-Step "Checking Node.js"
Refresh-Path
if (Test-CmdExists 'node') {
    $nodeVer = node --version 2>$null
    Write-OK "Node.js already installed ($nodeVer)"
    if (Test-CmdExists 'npm') {
        Write-OK "npm $(npm --version 2>$null)"
    }
    if (Test-CmdExists 'npx') {
        Write-OK "npx available"
    }
    Add-Result "Node.js" "PRESENT" $nodeVer
}
else {
    Write-Info "Node.js is required for Claude Code, npm packages, and MCP servers."
    if (Get-YesNo "Install Node.js LTS?" $true) {
        $nodeInstalled = $false

        if (Test-CmdExists 'winget') {
            Write-Info "Installing via winget..."
            try {
                winget install --id OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent 2>$null
                if ($LASTEXITCODE -eq 0) { $nodeInstalled = $true }
            }
            catch { Write-Info "winget install failed: $_ - trying next method..." }
        }

        if (-not $nodeInstalled -and (Test-CmdExists 'choco')) {
            Write-Info "Installing via Chocolatey..."
            try {
                choco install nodejs-lts --yes --no-progress
                $nodeInstalled = $true
            }
            catch { Write-Info "Chocolatey install failed: $_ - trying direct download..." }
        }

        if (-not $nodeInstalled) {
            Write-Info "Downloading from nodejs.org..."
            try {
                $nodeVersions = Invoke-RestMethod 'https://nodejs.org/dist/index.json'
                $latestLts = $nodeVersions | Where-Object { $_.lts -ne $false } | Select-Object -First 1
                $ver = $latestLts.version
                $msi = "node-$ver-x64.msi"
                $url = "https://nodejs.org/dist/$ver/$msi"
                $installer = Join-Path $script:TempDir $msi
                Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing
                Start-Process msiexec.exe -ArgumentList "/i `"$installer`" /qn /norestart" -Wait -NoNewWindow
            }
            catch {
                Write-Fail "Node.js download failed: $_"
            }
        }

        Refresh-Path
        if (Test-CmdExists 'node') {
            Write-OK "Node.js $(node --version 2>$null) installed"
            Add-Result "Node.js" "INSTALLED" (node --version 2>$null)
        }
        else {
            Write-Fail "Node.js not found after install"
            Add-Result "Node.js" "FAILED" "Not on PATH"
        }
    }
    else {
        Add-Result "Node.js" "SKIPPED" "User declined"
    }
}

# ─── 2e. Python ───────────────────────────────────────────────────────────────
Write-Step "Checking Python"
Refresh-Path
if (Test-CmdExists 'python') {
    $pyVer = python --version 2>&1
    Write-OK "Python already installed ($pyVer)"
    Add-Result "Python" "PRESENT" "$pyVer"
}
else {
    Write-Info "Python is used for MCP server development, data processing,"
    Write-Info "security tools, and many Claude Code plugins."
    if (Get-YesNo "Install Python $($script:PythonVersion)?" $true) {
        $pyInstalled = $false

        if (Test-CmdExists 'winget') {
            Write-Info "Installing Python $($script:PythonVersion) via winget..."
            try {
                winget install --id $script:PythonWingetId --accept-source-agreements --accept-package-agreements --silent 2>$null
                if ($LASTEXITCODE -eq 0) { $pyInstalled = $true }
            }
            catch { Write-Info "winget install failed: $_ - trying next method..." }
        }

        if (-not $pyInstalled -and (Test-CmdExists 'choco')) {
            Write-Info "Installing Python $($script:PythonVersion) via Chocolatey..."
            try {
                choco install $script:PythonChocoId --yes --no-progress
                $pyInstalled = $true
            }
            catch { Write-Info "Chocolatey install failed: $_" }
        }

        # Direct python.org installer fallback - works on Server 2022 / Win 11
        # even when winget and Chocolatey are unavailable.
        # The patch version is auto-detected (latest published 3.x.y) so this
        # keeps working as Python releases new patches; $script:PythonFullVersion
        # is the safety-net pin used if auto-detect fails.
        if (-not $pyInstalled) {
            $pyTarget = Resolve-PythonInstallerUrl
            if ($pyTarget) {
                $pyFull = $pyTarget.Version
                $pyUrl  = $pyTarget.Url
                $pyExe  = Join-Path $script:TempDir "python-$pyFull-amd64.exe"
                Write-Info "Falling back to direct python.org installer ($pyFull)..."
                try {
                    $dlOk = Invoke-Download -Uri $pyUrl -OutFile $pyExe -Description "Python $pyFull installer"
                    if ($dlOk) {
                        $pyArgs = '/quiet','InstallAllUsers=1','PrependPath=1','Include_test=0','Include_pip=1','Include_launcher=1'
                        $proc = Start-Process -FilePath $pyExe -ArgumentList $pyArgs -Wait -PassThru -NoNewWindow
                        if ($proc.ExitCode -eq 0) { $pyInstalled = $true }
                        else { Write-Warn "python.org installer exited with code $($proc.ExitCode)" }
                    }
                }
                catch { Write-Info "python.org direct install failed: $_" }
            }
            else {
                Write-Warn "Could not resolve a Python $($script:PythonVersion).x installer URL from python.org"
            }
        }

        Refresh-Path
        if (Test-CmdExists 'python') {
            Write-OK "Python $(python --version 2>&1) installed"
            python -m pip install --upgrade pip --quiet 2>$null
            Write-OK "pip upgraded"
            Add-Result "Python" "INSTALLED" (python --version 2>&1)
        }
        else {
            Write-Fail "Python not found after install"
            Write-Tip "PATH not refreshed yet? Open a NEW PowerShell window and re-run, or in this session:"
            Write-Info "  `$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')"
            Write-Tip "Manual install one-liner (direct from python.org, pinned version):"
            Write-Info "  `$v='$($script:PythonFullVersion)'; `$u=`"https://www.python.org/ftp/python/`$v/python-`$v-amd64.exe`"; `$f=`"`$env:TEMP\python-`$v-amd64.exe`"; iwr `$u -OutFile `$f; Start-Process `$f -ArgumentList '/quiet','InstallAllUsers=1','PrependPath=1' -Wait"
            Add-Result "Python" "FAILED" "Not on PATH"
        }
    }
    else {
        Add-Result "Python" "SKIPPED" "User declined"
    }
}

# ─── 2f. Additional CLI tools ─────────────────────────────────────────────────
Write-Step "Checking additional CLI tools"

$cliTools = @(
    @{ Name = "ripgrep";      Cmd = "rg";  Desc = "Ultra-fast code search (used by Claude Code)" },
    @{ Name = "jq";           Cmd = "jq";  Desc = "JSON processor for API responses" },
    @{ Name = "fd";           Cmd = "fd";  Desc = "Fast file finder" },
    @{ Name = "7zip";         Cmd = "7z";  Desc = "Archive utility" },
    @{ Name = "imagemagick";  Cmd = "magick"; Desc = "Image processing (convert, resize, etc.)" },
    @{ Name = "ghostscript"; Cmd = "gswin64c"; Desc = "PDF/PostScript engine (required by ImageMagick for PDFs)" },
    @{ Name = "ffmpeg";      Cmd = "ffmpeg";   Desc = "Video/audio processing and media conversion" },
    @{ Name = "bat";         Cmd = "bat";      Desc = "Syntax-highlighted file viewer (better cat)" },
    @{ Name = "fzf";         Cmd = "fzf";      Desc = "Fuzzy finder for files, history, and commands" },
    @{ Name = "yq";          Cmd = "yq";       Desc = "YAML processor (like jq but for YAML)" },
    @{ Name = "tree";        Cmd = "tree";     Desc = "Directory structure viewer" },
    @{ Name = "delta";       Cmd = "delta";    Desc = "Enhanced git diff viewer with syntax highlighting" },
    @{ Name = "shellcheck";  Cmd = "shellcheck"; Desc = "Shell script linter and validator" },
    @{ Name = "gh";          Cmd = "gh";       Desc = "GitHub CLI - repos, PRs, issues and releases from the terminal" },
    @{ Name = "SQLite";      Cmd = "sqlite3";  Desc = "SQL shell for .db files (sqlite3)" },
    @{ Name = "gnupg";       Cmd = "gpg";      Desc = "Sign and encrypt files and git commits (gpg)" },
    @{ Name = "Wget";        Cmd = "wget";     Desc = "Recursive and resumable file downloader" }
)

foreach ($tool in $cliTools) {
    Refresh-Path
    if (Test-CmdExists $tool.Cmd) {
        Write-OK "$($tool.Name) is installed"
        Add-Result $tool.Name "PRESENT" ""
    }
    else {
        Write-Info "$($tool.Name): $($tool.Desc)"
        if (Get-YesNo "Install $($tool.Name)?") {
            $toolLog     = Join-Path $script:TempDir "cli-$($tool.Cmd)-install.log"
            $toolCmdOk   = $false   # did the install command itself exit 0?
            $toolTried   = $false   # did we actually attempt anything?
            $toolFailMsg = ""

            if (Test-CmdExists 'choco') {
                $toolTried = $true
                try {
                    choco install $tool.Name --yes --no-progress 2>&1 | Tee-Object -FilePath $toolLog | Out-Host
                    # 1641 / 3010 are "succeeded, reboot pending", not failures.
                    $toolCmdOk = (@(0, 1641, 3010) -contains $LASTEXITCODE)
                    if (-not $toolCmdOk) { $toolFailMsg = "choco exit $LASTEXITCODE" }
                }
                catch {
                    $toolFailMsg = "$_"
                    "$_" | Out-File -FilePath $toolLog -Append -Encoding UTF8
                }
                Refresh-Path
            }
            elseif (Test-CmdExists 'winget') {
                $wingetIds = @{
                    "ripgrep"     = "BurntSushi.ripgrep.MSVC"
                    "jq"          = "jqlang.jq"
                    "fd"          = "sharkdp.fd"
                    "7zip"        = "7zip.7zip"
                    "imagemagick" = "ImageMagick.ImageMagick"
                    "ghostscript" = "ArtifexSoftware.GhostScript"
                    "ffmpeg"      = "Gyan.FFmpeg"
                    "bat"         = "sharkdp.bat"
                    "fzf"         = "junegunn.fzf"
                    "yq"          = "MikeFarah.yq"
                    "tree"        = ""
                    "delta"       = "dandavison.delta"
                    "shellcheck"  = "koalaman.shellcheck"
                    "gh"          = "GitHub.cli"
                    "SQLite"      = "SQLite.SQLite"
                    "gnupg"       = "GnuPG.Gpg4win"
                    "Wget"        = "JernejSimoncic.Wget"
                }
                $id = $wingetIds[$tool.Name]
                if ($id) {
                    $toolTried = $true
                    try {
                        winget install --id $id --accept-source-agreements --accept-package-agreements --silent 2>&1 |
                            Tee-Object -FilePath $toolLog | Out-Host
                        $toolCmdOk = ($LASTEXITCODE -eq 0)
                        if (-not $toolCmdOk) { $toolFailMsg = "winget exit $LASTEXITCODE" }
                    }
                    catch {
                        $toolFailMsg = "$_"
                        "$_" | Out-File -FilePath $toolLog -Append -Encoding UTF8
                    }
                }
                else {
                    $toolFailMsg = "no winget package id for $($tool.Name)"
                }
                Refresh-Path
            }

            # Direct-MSI fallback for the GitHub CLI, reinstated from the old
            # Copilot section: with neither choco nor winget on the box, gh
            # still installs straight from its own GitHub release.
            if ($tool.Cmd -eq 'gh' -and -not (Test-CmdExists 'gh')) {
                $toolTried = $true
                Write-Info "Falling back to the GitHub CLI MSI from the GitHub releases API..."
                try {
                    $ghRelease = Invoke-RestMethod 'https://api.github.com/repos/cli/cli/releases/latest' -UseBasicParsing -TimeoutSec 30
                    $ghAsset   = $ghRelease.assets | Where-Object { $_.name -match 'windows_amd64\.msi$' } | Select-Object -First 1
                    if ($ghAsset) {
                        $ghMsi = Join-Path $script:TempDir $ghAsset.name
                        if (Invoke-Download -Uri $ghAsset.browser_download_url -OutFile $ghMsi -Description "GitHub CLI MSI") {
                            $ghProc    = Start-Process msiexec.exe -ArgumentList "/i `"$ghMsi`" /qn /norestart" -Wait -NoNewWindow -PassThru
                            $toolCmdOk = (@(0, 1641, 3010) -contains $ghProc.ExitCode)
                            if (-not $toolCmdOk) { $toolFailMsg = "msiexec exit $($ghProc.ExitCode)" }
                            Refresh-Path
                        }
                        else { $toolFailMsg = "MSI download failed" }
                    }
                    else { $toolFailMsg = "no windows_amd64.msi asset in the latest gh release" }
                }
                catch {
                    $toolFailMsg = "$_"
                    Write-Fail "GitHub CLI MSI fallback failed: $_"
                }
            }

            Refresh-Path
            if (Test-CmdExists $tool.Cmd) {
                Write-OK "$($tool.Name) installed"
                Add-Result $tool.Name "INSTALLED" ""
            }
            elseif ($toolCmdOk) {
                # The installer exited 0, so PATH really may only need a new
                # shell. That is the only case where PARTIAL is honest here.
                Write-Warn "$($tool.Name) installed but not yet on PATH - restart your terminal"
                Add-Result $tool.Name "PARTIAL" "Restart terminal for PATH"
            }
            elseif ($toolTried) {
                Write-Fail "$($tool.Name) install failed$(if ($toolFailMsg) { " ($toolFailMsg)" })"
                Write-Info "Log: $toolLog"
                Add-Result $tool.Name "FAILED" "See $toolLog"
            }
            else {
                Write-Warn "No package manager available. Install $($tool.Name) manually."
                Add-Result $tool.Name "SKIPPED" "No package manager"
            }
        }
        else {
            Add-Result $tool.Name "SKIPPED" "User declined"
        }
    }
}

# ─── GitHub CLI sign-in ───────────────────────────────────────────────────────
# Offered here (not inside the Copilot section) so declining Copilot does not
# cost you a working 'gh'. 'gh auth setup-git' wires gh in as git's credential
# helper, which is what makes 'git push' work over HTTPS without a PAT prompt.
Refresh-Path
if (Test-CmdExists 'gh') {
    $ghLoggedIn = $false
    try {
        gh auth status 2>&1 | Out-Null
        if ($LASTEXITCODE -eq 0) { $ghLoggedIn = $true }
    }
    catch {}
    if ($ghLoggedIn) {
        Write-OK "GitHub CLI already signed in"
        Add-Result "GitHub CLI auth" "PRESENT" ""
    }
    else {
        Write-Info "Signing in to GitHub lets gh push, open PRs and read private repos."
        if (Get-YesNo "Log in to GitHub now (gh auth login)?" $false) {
            gh auth login
            if ($LASTEXITCODE -eq 0) {
                gh auth setup-git 2>&1 | Out-Null
                Write-OK "GitHub CLI signed in and set as git's credential helper"
                Add-Result "GitHub CLI auth" "OK" "gh auth setup-git ran"
            }
            else {
                Write-Warn "gh auth login did not complete - run it again later"
                Add-Result "GitHub CLI auth" "SKIPPED" "Login incomplete"
            }
        }
        else {
            Write-Tip "Later: gh auth login    then    gh auth setup-git"
            Add-Result "GitHub CLI auth" "SKIPPED" ""
        }
    }
}

# ─── 2g. Python packages ──────────────────────────────────────────────────────
Write-Step "Python package libraries"
if (Test-CmdExists 'python') {
    Write-Info "These packages enhance what Claude Code and AI tools can do:"
    Write-Info "  Data: numpy, pandas, polars, matplotlib, openpyxl"
    Write-Info "  Docs: PyMuPDF, pdfplumber (read PDFs so an agent can quote them)"
    Write-Info "  Web:  requests, httpx, beautifulsoup4"
    Write-Info "  AI:   anthropic, openai, fastmcp, mcp"
    Write-Info "  Dev:  pydantic, rich, pyyaml, python-dotenv"

    if (Get-YesNo "Install recommended Python packages?") {
        $packages = @(
            'numpy', 'pandas', 'polars', 'matplotlib', 'openpyxl',
            'requests', 'httpx', 'beautifulsoup4', 'lxml',
            'PyMuPDF', 'pdfplumber',
            'anthropic', 'openai', 'fastmcp', 'mcp',
            'pydantic', 'rich', 'pyyaml', 'python-dotenv',
            'Pillow', 'chardet', 'tabulate', 'jsonlines'
        )
        # Pass the array, not a joined string: a single space-separated argument
        # reaches pip as ONE requirement name and fails the whole command.
        $pkgLog = Join-Path $script:TempDir "python-packages-install.log"
        $pipOk  = $false
        try {
            python -m pip install --upgrade $packages --quiet 2>&1 | Tee-Object -FilePath $pkgLog | Out-Null
            $pipOk = ($LASTEXITCODE -eq 0)
        }
        catch { "$_" | Out-File -FilePath $pkgLog -Append -Encoding UTF8 }

        # A zero exit code is not proof that every name landed, so ask pip what
        # it actually holds and gate the row on that.
        $pipHave    = ""
        $pkgPresent = @()
        $pkgMissing = @()
        try { $pipHave = (python -m pip list --format=freeze 2>$null | Out-String) } catch {}
        if (-not [string]::IsNullOrWhiteSpace($pipHave)) {
            $haveNames = @()
            foreach ($line in $pipHave.Split([char]10)) {
                $nm = (($line -split '==')[0]).Trim()
                if ($nm) { $haveNames += ($nm -replace '[-_.]+', '-').ToLower() }
            }
            foreach ($p in $packages) {
                if ($haveNames -contains ($p -replace '[-_.]+', '-').ToLower()) { $pkgPresent += $p }
                else { $pkgMissing += $p }
            }
        }
        if ($pkgPresent.Count -eq $packages.Count) {
            Write-OK "Installed $($packages.Count) Python packages"
            Add-Result "Python Packages" "INSTALLED" "$($packages.Count) packages"
        }
        elseif ($pkgPresent.Count -gt 0) {
            Write-Warn "Missing: $($pkgMissing -join ', ')"
            Write-Info "Log: $pkgLog"
            Add-Result "Python Packages" "PARTIAL" "$($pkgPresent.Count)/$($packages.Count) present"
        }
        elseif ($pipOk) {
            Write-Warn "pip exited 0 but 'pip list' could not be read - packages not verified"
            Write-Info "Log: $pkgLog"
            Add-Result "Python Packages" "PARTIAL" "Not verified"
        }
        else {
            Write-Fail "Python package install failed - log: $pkgLog"
            Add-Result "Python Packages" "FAILED" "See $pkgLog"
        }
    }
    else {
        Add-Result "Python Packages" "SKIPPED" ""
    }

    # uv package manager
    if (-not (Test-CmdExists 'uv')) {
        Write-Info "uv is a fast Python package manager used by MCP servers."
        if (Get-YesNo "Install uv?") {
            $uvErr = ""
            try {
                Invoke-RestMethod https://astral.sh/uv/install.ps1 | Invoke-Expression
            }
            catch { $uvErr = "$_" }
            # The astral installer drops uv in ~\.local\bin and edits the user
            # PATH, so look there too before calling this installed.
            Refresh-Path
            if ((Test-CmdExists 'uv') -or (Test-Path "$env:USERPROFILE\.local\bin\uv.exe")) {
                Write-OK "uv installed"
                Add-Result "uv" "INSTALLED" ""
            }
            elseif (-not $uvErr) {
                Write-Warn "uv installer ran but 'uv' is not on PATH yet - restart your terminal"
                Add-Result "uv" "PARTIAL" "Restart terminal"
            }
            else {
                Write-Fail "uv install failed: $uvErr"
                Write-Tip "Try by hand: irm https://astral.sh/uv/install.ps1 | iex"
                Add-Result "uv" "FAILED" "$uvErr"
            }
        }
    }
    else {
        Write-OK "uv already installed"
        Add-Result "uv" "PRESENT" ""
    }
}

# ─── TLS interception: the definitive pass ────────────────────────────────────
# The pre-flight pass ran before Git for Windows and Node.js existed, and those
# are the only two clients here that carry their OWN CA bundle - so on a fresh
# box it had nothing to ask and answered UNKNOWN. Now that Phase 2 has installed
# the runtimes, ask again. This is the answer that goes in the report.
Refresh-Path
Write-Step "Re-checking TLS interception now that the runtimes are installed"
Show-TlsVerdict -Tls (Test-TlsInterception) -Final

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 3 - AI TERMINAL INSTALLATIONS
# ═══════════════════════════════════════════════════════════════════════════════

Enter-Phase "AI Terminal Solutions"
Write-Section "PHASE 3: AI Terminal Solutions"

Write-Host ""
Write-Host "  Which AI terminal solutions would you like to set up?" -ForegroundColor White
Write-Host ""
Write-Host "  ┌──────────────────────────────────────────────────────────────┐" -ForegroundColor DarkCyan
Write-Host "  │  1. Claude Code (Anthropic)                                  │" -ForegroundColor DarkCyan
Write-Host "  │     Best for: Deep code understanding, multi-file edits,     │" -ForegroundColor Gray
Write-Host "  │     agentic coding, MCP servers, security research           │" -ForegroundColor Gray
Write-Host "  │     Needs: Git Bash, Node.js, Anthropic API key or login     │" -ForegroundColor Gray
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  2. OpenAI Codex CLI (OpenAI)                                │" -ForegroundColor DarkCyan
Write-Host "  │     Best for: Agentic coding in the terminal, repo-wide      │" -ForegroundColor Gray
Write-Host "  │     edits, running and fixing tests                          │" -ForegroundColor Gray
Write-Host "  │     Needs: ChatGPT sign-in or OpenAI API key                 │" -ForegroundColor Gray
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  3. ChatGPT terminal (shell-gpt)                             │" -ForegroundColor DarkCyan
Write-Host "  │     Best for: Quick questions, shell-command suggestions,    │" -ForegroundColor Gray
Write-Host "  │     brainstorming - ChatGPT as the 'sgpt' command            │" -ForegroundColor Gray
Write-Host "  │     Needs: OpenAI API key                                    │" -ForegroundColor Gray
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  4. GitHub Copilot CLI (GitHub)                              │" -ForegroundColor DarkCyan
Write-Host "  │     Best for: Shell command suggestions, git operations,     │" -ForegroundColor Gray
Write-Host "  │     explaining commands, GitHub integration                  │" -ForegroundColor Gray
Write-Host "  │     Needs: GitHub account with Copilot subscription          │" -ForegroundColor Gray
Write-Host "  └──────────────────────────────────────────────────────────────┘" -ForegroundColor DarkCyan
Write-Host ""

$installClaude  = Get-YesNo "Install Claude Code?" $true
$installCodex   = Get-YesNo "Install OpenAI Codex CLI?" $true
$installChatGPT = Get-YesNo "Install ChatGPT terminal (shell-gpt)?" $true
$installCopilot = Get-YesNo "Install GitHub Copilot CLI?" $true

# ─── 3a. Claude Code ──────────────────────────────────────────────────────────
if ($installClaude) {
    Write-Section "Installing Claude Code"

    # Check for existing install
    Refresh-Path
    $claudeExists = (Test-CmdExists 'claude') -or (Test-Path "$env:USERPROFILE\.local\bin\claude.exe")

    if ($claudeExists) {
        Write-OK "Claude Code already installed"
        try {
            $ccVer = claude --version 2>$null
            Write-OK "Version: $ccVer"
        }
        catch { Write-Info "Could not retrieve version" }
        Add-Result "Claude Code" "PRESENT" ""
    }
    else {
        Write-Step "Installing Claude Code..."

        # Method 1: Use existing Install-ClaudeCode.ps1 if available.
        # It used to record INSTALLED the moment the sub-script returned, which
        # claimed success for a custom installer that had done nothing - and
        # added a second "Claude Code" row on top of the final check below. Now
        # it only notes HOW claude arrived; the final check decides the status.
        $ccVia    = ""
        $ccScript = Join-Path $ScriptsDir "Install-ClaudeCode.ps1"
        if (Test-Path $ccScript) {
            Write-Info "Found Install-ClaudeCode.ps1 - using your custom installer"
            try {
                & $ccScript
                Refresh-Path
                if (Test-CmdExists 'claude') { $ccVia = "via Install-ClaudeCode.ps1" }
                else {
                    Write-Warn "Install-ClaudeCode.ps1 finished but 'claude' is still not available"
                    Write-Info "Trying the official method..."
                }
            }
            catch {
                Write-Warn "Custom installer had an issue: $_"
                Write-Info "Trying the official method..."
            }
        }

        # Method 2: Official native installer
        if (-not (Test-CmdExists 'claude')) {
            Write-Info "Using official native installer..."
            try {
                $installScript = Invoke-RestMethod -Uri "https://claude.ai/install.ps1" -UseBasicParsing
                Invoke-Expression $installScript
                Refresh-Path
            }
            catch {
                Write-Warn "Native installer issue: $_"
            }
        }

        # Method 3: npm fallback
        if (-not (Test-CmdExists 'claude') -and (Test-CmdExists 'npm')) {
            Write-Info "Trying npm install as fallback..."
            try {
                npm install -g @anthropic-ai/claude-code 2>&1 | Out-Host
                Refresh-Path
            }
            catch { Write-Info "npm fallback also failed: $_" }
        }

        # Final check
        $env:Path += ";$env:USERPROFILE\.local\bin"
        if (Test-CmdExists 'claude') {
            Write-OK "Claude Code installed successfully!"
            try { Write-OK "Version: $(claude --version 2>$null)" } catch {}
            Add-Result "Claude Code" "INSTALLED" $ccVia
        }
        else {
            Write-Fail "Claude Code could not be installed automatically"
            Write-Tip "Try manually: irm https://claude.ai/install.ps1 | iex"
            Add-Result "Claude Code" "FAILED" "Manual install needed"
        }
    }

    # Run Setup-ClaudeCodeEnv.ps1 if available
    $envScript = Join-Path $ScriptsDir "Setup-ClaudeCodeEnv.ps1"
    if (Test-Path $envScript) {
        Write-Host ""
        if (Get-YesNo "Run Setup-ClaudeCodeEnv.ps1 (Python libs, CLI tools, Git config)?") {
            Write-Step "Running Claude Code environment setup..."
            try {
                & $envScript
                Add-Result "Claude Env Setup" "RAN" ""
            }
            catch {
                Write-Warn "Env setup error: $_"
                Add-Result "Claude Env Setup" "ERROR" "$_"
            }
        }
    }

    # Run Install-ClaudePlugins.ps1 if available
    $pluginScript = Join-Path $ScriptsDir "Install-ClaudePlugins.ps1"
    if (Test-Path $pluginScript) {
        Write-Host ""
        if (Get-YesNo "Run Install-ClaudePlugins.ps1 (security, frontend, playground, MCP)?") {
            Write-Step "Installing Claude Code plugins..."
            try {
                & $pluginScript
                Add-Result "Claude Plugins" "RAN" ""
            }
            catch {
                Write-Warn "Plugin install error: $_"
                Add-Result "Claude Plugins" "ERROR" "$_"
            }
        }
    }

    # Run Install-PlaygroundPlugin.ps1 if available
    $pgScript = Join-Path $ScriptsDir "Install-PlaygroundPlugin.ps1"
    if (Test-Path $pgScript) {
        Write-Host ""
        if (Get-YesNo "Run Install-PlaygroundPlugin.ps1?") {
            Write-Step "Installing Playground plugin..."
            try {
                & $pgScript
                Add-Result "Playground Plugin" "RAN" ""
            }
            catch {
                Write-Warn "Playground plugin error: $_"
                Add-Result "Playground Plugin" "ERROR" "$_"
            }
        }
    }

    # ─── Anthropic API key (optional - browser login is the easy path) ────────
    if ($env:ANTHROPIC_API_KEY) {
        Write-OK "ANTHROPIC_API_KEY already set"
    }
    else {
        Write-Host ""
        Write-Host "  ┌──────────────────────────────────────────────────────────────┐" -ForegroundColor DarkCyan
        Write-Host "  │  Option A - Browser login (easiest):                         │" -ForegroundColor Gray
        Write-Host "  │    1. Run: claude                                            │" -ForegroundColor White
        Write-Host "  │    2. Browser opens > log in at claude.ai > Authorize        │" -ForegroundColor White
        Write-Host "  │  Option B - API key (for headless/CI boxes):                 │" -ForegroundColor Gray
        Write-Host "  │    set ANTHROPIC_API_KEY to an sk-ant-... key                │" -ForegroundColor White
        Write-Host "  └──────────────────────────────────────────────────────────────┘" -ForegroundColor DarkCyan
        if (Get-YesNo "Set ANTHROPIC_API_KEY now?" $false) {
            Write-Host "  Enter your Anthropic API key - input is hidden (sk-ant-...): " -NoNewline -ForegroundColor Yellow
            $anthropicKey = Read-MaskedInput
            if ($anthropicKey -and $anthropicKey.StartsWith('sk-ant-')) {
                [System.Environment]::SetEnvironmentVariable('ANTHROPIC_API_KEY', $anthropicKey, 'User')
                $env:ANTHROPIC_API_KEY = $anthropicKey
                Write-OK "ANTHROPIC_API_KEY saved to your user environment"
                Add-Result "Anthropic API key" "OK" "Saved (User scope)"
            }
            else {
                Write-Warn "Key does not start with sk-ant- . Skipped - use browser login instead."
                Add-Result "Anthropic API key" "SKIPPED" "Bad prefix"
            }
        }
        else {
            Write-Info "No key stored - run 'claude' and log in through the browser."
            Add-Result "Anthropic API key" "SKIPPED" "Browser login"
        }
    }

    # Configure Git Bash path for Claude Code
    Write-Step "Configuring Claude Code settings..."
    $gitBashExe = Get-GitBashPath
    $claudeSettingsDir = Join-Path $env:USERPROFILE ".claude"
    $claudeSettingsFile = Join-Path $claudeSettingsDir "settings.json"

    if ($gitBashExe) {
        if (-not (Test-Path $claudeSettingsDir)) {
            New-Item -Path $claudeSettingsDir -ItemType Directory -Force | Out-Null
        }

        # settings.json is the user's OWN Claude Code configuration, so three
        # things have to hold before we touch it:
        #   1. back it up first, and say where the backup is
        #   2. parse it on BOTH hosts (-AsHashtable is PowerShell 7+ only and
        #      throws on 5.1 - see ConvertTo-HashtableDeep)
        #   3. if it will not parse, leave it exactly as it is
        $settings       = @{}
        $settingsUsable = $true
        $backupPath     = ""

        if (Test-Path $claudeSettingsFile) {
            $backupPath = "$claudeSettingsFile.$(Get-Date -Format 'yyyyMMdd-HHmmss').bak"
            try {
                Copy-Item -LiteralPath $claudeSettingsFile -Destination $backupPath -Force -ErrorAction Stop
                Write-OK "Backed up your existing Claude settings to: $backupPath"
            }
            catch {
                $backupPath = ""
                Write-Fail "Could not back up $claudeSettingsFile - leaving it untouched: $_"
                Add-Result "Git Bash" "FAILED" "Backup failed"
                $settingsUsable = $false
            }

            if ($settingsUsable) {
                try {
                    $rawSettings = Get-Content -LiteralPath $claudeSettingsFile -Raw -ErrorAction Stop
                    if ([string]::IsNullOrWhiteSpace($rawSettings)) {
                        $settings = @{}
                    }
                    elseif ($PSVersionTable.PSVersion.Major -ge 7) {
                        $settings = ConvertTo-HashtableDeep ($rawSettings | ConvertFrom-Json -AsHashtable -ErrorAction Stop)
                    }
                    else {
                        $settings = ConvertTo-HashtableDeep ($rawSettings | ConvertFrom-Json -ErrorAction Stop)
                    }
                    if ($settings -isnot [System.Collections.IDictionary]) {
                        throw "settings.json does not contain a JSON object at the top level"
                    }
                }
                catch {
                    Write-Fail "Existing $claudeSettingsFile could not be parsed - NOT overwriting it: $_"
                    Write-Tip "Repair the JSON by hand (a copy is at $backupPath), then re-run this script."
                    Add-Result "Git Bash" "FAILED" "settings.json unparsable"
                    $settingsUsable = $false
                }
            }
        }

        if ($settingsUsable) {
            if (-not $settings.Contains('env')) { $settings['env'] = @{} }
            elseif ($settings['env'] -isnot [System.Collections.IDictionary]) { $settings['env'] = @{} }
            $settings['env']['CLAUDE_CODE_GIT_BASH_PATH'] = $gitBashExe
            try {
                # NOT Set-Content -Encoding UTF8: on Windows PowerShell 5.1 that
                # writes a UTF-8 BOM, and Claude Code is a Node app whose
                # JSON.parse throws on a leading U+FEFF. Write BOM-less on both hosts.
                $settingsJson = $settings | ConvertTo-Json -Depth 10
                [System.IO.File]::WriteAllText($claudeSettingsFile, $settingsJson, (New-Object System.Text.UTF8Encoding($false)))
                Write-OK "Git Bash path configured in Claude settings: $gitBashExe"
                Add-Result "Git Bash" "OK" "Configured"
            }
            catch {
                Write-Fail "Could not write $claudeSettingsFile - $_"
                if ($backupPath) { Write-Tip "Your original settings are still at $backupPath" }
                Add-Result "Git Bash" "FAILED" "Write failed"
            }
        }
    }
    else {
        # This used to be a success path with no else branch, so an unelevated
        # install (Git under %LOCALAPPDATA%\Programs\Git) skipped the whole step
        # silently and Claude Code was left with no shell and no warning.
        Write-Fail "Git Bash (bash.exe) not found - Claude Code has no shell on Windows"
        Write-Info "Looked at (Get-Command git).Source, $env:ProgramFiles\Git and $env:LOCALAPPDATA\Programs\Git"
        Write-Tip "Install Git for Windows, then re-run this script."
        Add-Result "Git Bash" "FAILED" "bash.exe not found"
    }

    # Git performance tweaks
    if (Test-CmdExists 'git') {
        git config --global core.fscache true 2>$null
        git config --global core.preloadindex true 2>$null
        git config --global gc.auto 256 2>$null
        Write-OK "Git performance settings applied"

    # Check for git user config
    $gitUser = git config --global user.name 2>$null
    $gitEmail = git config --global user.email 2>$null
    if (-not $gitUser -or -not $gitEmail) {
        Write-Warn "Git user.name or user.email not set. Git commits will fail without these."
        Write-Tip "Run: git config --global user.name 'Your Name'"
        Write-Tip "Run: git config --global user.email 'you@example.com'"
    }
    else {
        Write-OK "Git user: $gitUser <$gitEmail>"
    }
    git config --global init.defaultBranch main 2>$null
    }
}

# ─── 3b. ChatGPT terminal (shell-gpt) ─────────────────────────────────────────
# ONE branch covers the whole OpenAI side: the ChatGPT terminal itself
# (shell-gpt, which gives you the 'sgpt' command), the OpenAI Python SDK, and
# the single OPENAI_API_KEY capture that both of them - and 'codex' below - read.
# The old npm 'chatgpt-cli' this step used to pull has not been published since
# 2022; shell-gpt is the maintained package and lands via the same pipx path the
# other Python CLI apps use.
if ($installChatGPT) {
    Write-Section "Installing ChatGPT terminal (shell-gpt)"

    Write-Info "shell-gpt puts ChatGPT in your terminal as the 'sgpt' command. It reads"
    Write-Info "OPENAI_API_KEY, so the key you enter here also works for the OpenAI"
    Write-Info "Python SDK and for 'codex'. You are only asked for it once."

    # --- OPENAI_API_KEY (asked once, shared by sgpt / the SDK / codex) --------
    if (-not $env:OPENAI_API_KEY) {
        Write-Host ""
        Write-Host "  An OpenAI API key is needed. Get one at: https://platform.openai.com/api-keys" -ForegroundColor Yellow
        Write-Host "  Enter your OpenAI API key - input is hidden (or press Enter to skip): " -NoNewline -ForegroundColor Yellow
        $openaiKey = Read-MaskedInput
        if ($openaiKey -and $openaiKey.StartsWith('sk-')) {
            [System.Environment]::SetEnvironmentVariable('OPENAI_API_KEY', $openaiKey, 'User')
            $env:OPENAI_API_KEY = $openaiKey
            Write-OK "OPENAI_API_KEY saved to your user environment"
            Add-Result "OpenAI API key" "OK" "Saved (User scope)"
        }
        else {
            Write-Info "No key stored - set OPENAI_API_KEY later, or sign in through 'codex'."
            Add-Result "OpenAI API key" "SKIPPED" "Not provided"
        }
    }
    else {
        Write-OK "OPENAI_API_KEY already set"
        Add-Result "OpenAI API key" "PRESENT" ""
    }

    # --- shell-gpt itself (the 'sgpt' command), via pipx ----------------------
    Refresh-Path
    if (Test-CmdExists 'sgpt') {
        $sgptVer = ""
        if ($env:OPENAI_API_KEY -or (Test-Path (Join-Path $env:USERPROFILE ".config\shell_gpt\.sgptrc"))) {
            try { $sgptVer = (sgpt --version 2>$null | Select-Object -First 1) } catch {}
        }
        Write-OK "shell-gpt already installed$(if ($sgptVer) { " ($sgptVer)" })"
        Add-Result "ChatGPT Terminal" "PRESENT" "sgpt"
    }
    else {
        Write-Step "Installing shell-gpt (pipx install shell-gpt)..."
        $pipx = Initialize-Pipx
        if ($pipx.Ok) {
            $sgptLog       = Join-Path $script:TempDir "shell-gpt-install.log"
            $sgptInstallOk = Invoke-PipxInstall -Pipx $pipx -Package 'shell-gpt' -LogFile $sgptLog
            Refresh-Path
            if (Test-CmdExists 'sgpt') {
                # Guarded: sgpt reads a key from the console on first run when
                # OPENAI_API_KEY is unset, which would block the installer.
                $sgptVer = ""
                if ($env:OPENAI_API_KEY -or (Test-Path (Join-Path $env:USERPROFILE ".config\shell_gpt\.sgptrc"))) {
                    try { $sgptVer = (sgpt --version 2>$null | Select-Object -First 1) } catch {}
                }
                Write-OK "shell-gpt installed$(if ($sgptVer) { " ($sgptVer)" })"
                Write-Info ""
                Write-Info "Usage examples:"
                Write-Info "  sgpt 'explain this stack trace'      # one-shot question"
                Write-Info "  sgpt --shell 'find files over 100MB' # suggest a shell command"
                Write-Info "  sgpt --repl chat                     # interactive session"
                Write-Info ""
                Add-Result "ChatGPT Terminal" "INSTALLED" "sgpt"
            }
            elseif ($sgptInstallOk) {
                # The install command exited 0, so the shim really may only need
                # a new shell. That is the ONLY case where PARTIAL is honest.
                Write-Warn "shell-gpt installed but 'sgpt' is not on PATH yet - restart your terminal"
                Add-Result "ChatGPT Terminal" "PARTIAL" "sgpt after restart"
            }
            else {
                Write-Fail "shell-gpt install failed - log: $sgptLog"
                Write-Tip "Try by hand: pipx install shell-gpt"
                Add-Result "ChatGPT Terminal" "FAILED" "See $sgptLog"
            }
        }
        else {
            Write-Fail "shell-gpt needs pipx (Python) and pipx is not available"
            if ($pipx.Log) { Write-Info "pipx bootstrap log: $($pipx.Log)" }
            Write-Tip "Install Python, then: python -m pip install --user pipx; pipx install shell-gpt"
            Add-Result "ChatGPT Terminal" "FAILED" "pipx unavailable"
        }
    }

    # --- OpenAI Python SDK (same key, for your own scripts) ------------------
    if (Test-CmdExists 'python') {
        $sdkLog = Join-Path $script:TempDir "openai-sdk-install.log"
        $sdkOk  = $false
        try { python -c "import openai" 2>&1 | Out-Null; $sdkOk = ($LASTEXITCODE -eq 0) } catch {}
        if (-not $sdkOk) {
            Write-Step "OpenAI Python SDK (python -m pip install openai)..."
            try { python -m pip install --upgrade openai 2>&1 | Tee-Object -FilePath $sdkLog | Out-Null }
            catch { "$_" | Out-File -FilePath $sdkLog -Append -Encoding UTF8 }
            try { python -c "import openai" 2>&1 | Out-Null; $sdkOk = ($LASTEXITCODE -eq 0) } catch {}
        }
        if ($sdkOk) {
            Write-OK "OpenAI Python SDK importable (python -c 'import openai')"
            Add-Result "OpenAI SDK" "INSTALLED" "import openai"
        }
        else {
            Write-Fail "OpenAI Python SDK could not be imported - log: $sdkLog"
            Add-Result "OpenAI SDK" "FAILED" "See $sdkLog"
        }
    }
    else {
        Write-Info "Python not installed - skipping the OpenAI Python SDK."
        Add-Result "OpenAI SDK" "SKIPPED" "Python missing"
    }

    # --- optional: your own Setup-ChatGPT-Terminal.ps1 profile helper ---------
    # Kept as a bonus, not as the ChatGPT terminal itself. If you dropped this
    # script next to the kickstart it adds its own PowerShell profile functions
    # on top of 'sgpt'; without it nothing here is missing.
    $chatGPTScript = Join-Path $ScriptsDir "Setup-ChatGPT-Terminal.ps1"
    if (Test-Path $chatGPTScript) {
        Write-Host ""
        Write-Info "Found Setup-ChatGPT-Terminal.ps1 - it adds PowerShell profile helpers"
        Write-Info "(chatgpt / cgpt / gpt) alongside 'sgpt'."
        Write-Step "Running Setup-ChatGPT-Terminal.ps1..."
        try {
            & $chatGPTScript -InstallOnly
            Add-Result "ChatGPT profile helper" "RAN" "Setup-ChatGPT-Terminal.ps1"
        }
        catch {
            Write-Warn "ChatGPT profile helper error: $_"
            Add-Result "ChatGPT profile helper" "ERROR" "$_"
        }
    }
}

# ─── 3c. OpenAI Codex CLI ─────────────────────────────────────────────────────
# Its own prompt (it used to be buried in the AI framework bundle with no
# fallback, so declining that bundle silently cost you Codex).
if ($installCodex) {
    Write-Section "Installing OpenAI Codex CLI"

    Refresh-Path
    if (Test-CmdExists 'codex') {
        $codexVer = ""
        try { $codexVer = codex --version 2>$null } catch {}
        Write-OK "Codex already installed$(if ($codexVer) { " ($codexVer)" })"
        Add-Result "OpenAI Codex" "PRESENT" "$codexVer"
    }
    elseif (Test-CmdExists 'npm') {
        Write-Step "Installing OpenAI Codex CLI (npm @openai/codex)..."
        $codexLog       = Join-Path $script:TempDir "codex-install.log"
        $codexInstallOk = $false
        try {
            npm install -g '@openai/codex' 2>&1 | Tee-Object -FilePath $codexLog | Out-Host
            $codexInstallOk = ($LASTEXITCODE -eq 0)
        }
        catch {
            Write-Warn "Codex install error: $_"
            "$_" | Out-File -FilePath $codexLog -Append -Encoding UTF8
        }
        Refresh-Path

        if (Test-CmdExists 'codex') {
            Write-OK "Codex installed (run: codex)"
            Add-Result "OpenAI Codex" "INSTALLED" ""
        }
        elseif ($codexInstallOk) {
            # npm exited 0, so the shim genuinely may only need a new shell.
            Write-Warn "Codex installed but not yet on PATH - restart your terminal"
            Add-Result "OpenAI Codex" "PARTIAL" "Restart terminal"
        }
        else {
            Write-Fail "Codex install failed - log: $codexLog"
            Write-Tip "Try by hand: npm install -g @openai/codex"
            Add-Result "OpenAI Codex" "FAILED" "See $codexLog"
        }
        Write-Info "Auth: run 'codex' and sign in with ChatGPT, or set OPENAI_API_KEY."
        Write-Info "Codex on native Windows is experimental; OpenAI recommends WSL2."
    }
    else {
        Write-Fail "npm not available - install Node.js first"
        Add-Result "OpenAI Codex" "FAILED" "npm missing"
    }
}

# ─── 3d. GitHub Copilot CLI ───────────────────────────────────────────────────
# The old "gh extension install github/gh-copilot" extension is retired. GitHub
# now ships Copilot CLI as a standalone npm package that provides the 'copilot'
# command, so the GitHub CLI is no longer a prerequisite. 'gh' is installed with
# the other CLI tools in Phase 2, so declining Copilot no longer costs you 'gh'.
if ($installCopilot) {
    Write-Section "Installing GitHub Copilot CLI"

    Refresh-Path
    if (Test-CmdExists 'copilot') {
        Write-OK "Copilot CLI already installed"
        Add-Result "GitHub Copilot CLI" "PRESENT" "copilot"
    }
    elseif (Test-CmdExists 'npm') {
        Write-Step "Installing GitHub Copilot CLI (npm @github/copilot)..."
        $copilotLog       = Join-Path $script:TempDir "copilot-install.log"
        $copilotInstallOk = $false
        try {
            npm install -g '@github/copilot' 2>&1 | Tee-Object -FilePath $copilotLog | Out-Host
            $copilotInstallOk = ($LASTEXITCODE -eq 0)
            Refresh-Path
        }
        catch {
            Write-Warn "Copilot CLI install error: $_"
            "$_" | Out-File -FilePath $copilotLog -Append -Encoding UTF8
        }
        Refresh-Path

        if (Test-CmdExists 'copilot') {
            Write-OK "copilot installed (run: copilot)"
            Write-Info ""
            Write-Info "Usage examples:"
            Write-Info "  copilot                                  # interactive session"
            Write-Info "  copilot -p 'find files over 100MB'       # one-shot prompt"
            Write-Info ""
            Write-Tip "You need a GitHub Copilot subscription. Sign in with '/login' inside copilot."
            Add-Result "GitHub Copilot CLI" "INSTALLED" "copilot"
        }
        elseif ($copilotInstallOk) {
            # npm exited 0, so the shim genuinely may only need a new shell.
            Write-Warn "copilot installed but not yet on PATH - restart your terminal"
            Write-Tip "You need a GitHub Copilot subscription. Sign in with '/login' inside copilot."
            Add-Result "GitHub Copilot CLI" "PARTIAL" "Restart terminal"
        }
        else {
            Write-Fail "Copilot CLI install failed - log: $copilotLog"
            Write-Tip "Try by hand: npm install -g @github/copilot"
            Add-Result "GitHub Copilot CLI" "FAILED" "See $copilotLog"
        }
    }
    else {
        Write-Fail "npm not available - install Node.js first"
        Add-Result "GitHub Copilot CLI" "FAILED" "npm missing"
    }
}

# ─── 3e. Kimi Code CLI (OPT-IN: -WithKimi, or interactive prompt) ─────────────
# US-only is the DEFAULT. Kimi (Moonshot AI) is China-operated, so it is NEVER
# installed under -AutoMode / -All (Get-YesNo returns its default - here $false -
# when $AutoMode is set). It installs when -WithKimi is passed, OR when an
# interactive operator answers Yes to the prompt below (which defaults to No).
# Leaving the flag off and answering No - or any unattended run - skips Kimi.
if ($WithKimi -or (Get-YesNo "Install Kimi Code CLI (Moonshot AI - China-operated, off by default)?" $false)) {
    Write-Section "Installing Kimi Code CLI (opt-in: -WithKimi)"

    Refresh-Path
    if (Test-CmdExists 'kimi') {
        try { $kimiVer = kimi --version 2>$null } catch { $kimiVer = "" }
        Write-OK "Kimi Code CLI already installed$(if ($kimiVer) { " ($kimiVer)" })"
        Add-Result "Kimi Code CLI" "PRESENT" "$kimiVer"
    }
    else {
        Write-Step "Installing Kimi Code CLI (the standalone 'kimi' terminal)..."
        $kimiInstalled = $false
        $kimiErr = $null

        # Official Windows installer (mirrors the claude.ai / astral.sh pattern).
        # macOS/Linux equivalent: curl -LsSf https://code.kimi.com/install.sh | bash
        try {
            $kimiScript = Invoke-RestMethod -Uri "https://code.kimi.com/install.ps1" -UseBasicParsing
            Invoke-Expression $kimiScript
            Refresh-Path
            if (Test-CmdExists 'kimi') { $kimiInstalled = $true }
        }
        catch { $kimiErr = $_ ; Write-Warn "Kimi official installer issue: $_" }

        # Fallback: uv tool install (Kimi Code is published as a uv-installable tool)
        if (-not $kimiInstalled -and (Test-CmdExists 'uv')) {
            Write-Info "Trying uv tool fallback..."
            try {
                uv tool install kimi-cli 2>&1 | Out-Null
                Refresh-Path
                if (Test-CmdExists 'kimi') { $kimiInstalled = $true }
            }
            catch { $kimiErr = $_ ; Write-Info "uv fallback also failed: $_" }
        }

        # Verify with 'kimi --version'
        Refresh-Path
        if (Test-CmdExists 'kimi') {
            $kimiVer = ""
            try { $kimiVer = kimi --version 2>$null } catch {}
            Write-OK "Kimi Code CLI installed$(if ($kimiVer) { " ($kimiVer)" })"
            Add-Result "Kimi Code CLI" "INSTALLED" "$kimiVer"
        }
        else {
            Write-Fail "Kimi Code CLI could not be installed automatically"
            Write-Tip "Manual: irm https://code.kimi.com/install.ps1 | iex   (or: uv tool install kimi-cli)"
            Add-Result "Kimi Code CLI" "FAILED" "$kimiErr"
        }
    }

    # First-launch + API key guidance (never persisted to the environment here)
    Write-Host ""
    Write-Info "First launch: run 'kimi', then '/login' -> 'Kimi Code' browser OAuth,"
    Write-Info "or paste an API key. Keys come from https://platform.kimi.ai"
    Write-Info "(Console -> create key). The account needs a >= `$1 recharge to"
    Write-Info "activate; the `$1 starter tier is only 3 RPM, so for real agentic"
    Write-Info "loops budget the `$10 tier."

    # OPTIONAL: use Kimi as a drop-in model INSIDE Claude Code (no separate CLI).
    # We only PRINT this recipe - we deliberately do NOT set these variables, so
    # leaving them unset keeps Claude Code pointed at US-operated Anthropic models.
    Write-Host ""
    Write-Host "  ┌───────────────────────────────────────────────────────────────────┐" -ForegroundColor DarkCyan
    Write-Host "  │  OPTIONAL: Kimi as a drop-in model inside Claude Code             │" -ForegroundColor DarkCyan
    Write-Host "  ├───────────────────────────────────────────────────────────────────┤" -ForegroundColor DarkCyan
    Write-Host "  │  Moonshot ships an Anthropic-compatible endpoint. Set these       │" -ForegroundColor Gray
    Write-Host "  │  THREE env vars in a shell BEFORE launching 'claude' to run       │" -ForegroundColor Gray
    Write-Host "  │  Claude Code against Kimi instead of Anthropic:                   │" -ForegroundColor Gray
    Write-Host "  │                                                                   │" -ForegroundColor DarkCyan
    Write-Host "  │    `$env:ANTHROPIC_BASE_URL  = 'https://api.moonshot.ai/anthropic' │" -ForegroundColor White
    Write-Host "  │    `$env:ANTHROPIC_AUTH_TOKEN = '<your Moonshot key>'              │" -ForegroundColor White
    Write-Host "  │    `$env:ANTHROPIC_MODEL     = 'kimi-k2.7-code'                    │" -ForegroundColor White
    Write-Host "  │                                                                   │" -ForegroundColor DarkCyan
    Write-Host "  │  Models: kimi-k2.7-code (flagship), kimi-k2.6,                    │" -ForegroundColor Gray
    Write-Host "  │  kimi-k2-thinking. 256K context.                                  │" -ForegroundColor Gray
    Write-Host "  └───────────────────────────────────────────────────────────────────┘" -ForegroundColor DarkCyan
    Write-Host ""
    Write-Warn "China-operated caveat: Moonshot AI is a China-operated company."
    Write-Info "Both endpoints (api.moonshot.ai global, api.moonshot.cn mainland) are"
    Write-Info "China-operated with NO published Western data-residency or zero-retention"
    Write-Info "guarantee. Do NOT send confidential client data through Kimi. Leaving the"
    Write-Info "three ANTHROPIC_* variables UNSET keeps Claude Code on US-operated models."
}
else {
    # Default path (US-only): a documented skip, no-op.
    Write-Info "Skipping Kimi (Moonshot AI) - it is China-operated and off by default. Pass -WithKimi (or answer Yes to the prompt) to opt in."
}

# ─── 3f. Google Gemini CLI (opt-in, default NO) ───────────────────────────────
# Installed from npm on every platform. The Homebrew formula is deprecated
# ("unsupported"), so npm is the maintained path even on macOS - and on Windows
# npm is the only path. Default NO purely to keep the default install lean;
# because Get-YesNo returns its default under -AutoMode/-All, an unattended run
# skips it.
# The presence check has to run BEFORE the prompt: with it inside the yes-branch
# a declined prompt reported SKIPPED even on a box where gemini was already
# installed, which read as "not here" when it was.
Refresh-Path
if (Test-CmdExists 'gemini') {
    $gemVer = ""
    try { $gemVer = gemini --version 2>$null } catch {}
    Write-OK "Gemini CLI already installed$(if ($gemVer) { " ($gemVer)" })"
    Add-Result "Gemini CLI" "PRESENT" "$gemVer"
}
elseif (Get-YesNo "Install Google Gemini CLI (opt-in, keeps the default install lean)?" $false) {
    Write-Section "Installing Google Gemini CLI"

    if (Test-CmdExists 'npm') {
        Write-Step "Installing Gemini CLI (npm @google/gemini-cli)..."
        $gemLog       = Join-Path $script:TempDir "gemini-cli-install.log"
        $gemInstallOk = $false
        try {
            npm install -g '@google/gemini-cli' 2>&1 | Tee-Object -FilePath $gemLog | Out-Host
            $gemInstallOk = ($LASTEXITCODE -eq 0)
        }
        catch {
            Write-Warn "Gemini CLI install error: $_"
            "$_" | Out-File -FilePath $gemLog -Append -Encoding UTF8
        }
        Refresh-Path

        if (Test-CmdExists 'gemini') {
            Write-OK "Gemini CLI installed (run: gemini)"
            Add-Result "Gemini CLI" "INSTALLED" ""
        }
        elseif ($gemInstallOk) {
            Write-Warn "Gemini CLI installed but not yet on PATH - restart your terminal"
            Add-Result "Gemini CLI" "PARTIAL" "Restart terminal"
        }
        else {
            Write-Fail "Gemini CLI install failed - log: $gemLog"
            Write-Tip "Try by hand: npm install -g @google/gemini-cli"
            Add-Result "Gemini CLI" "FAILED" "See $gemLog"
        }
        Write-Info "Auth: run 'gemini' and sign in with a Google account, or set GEMINI_API_KEY."
    }
    else {
        Write-Fail "npm not available - install Node.js first"
        Add-Result "Gemini CLI" "FAILED" "npm missing"
    }
}
else {
    Write-Info "Skipping Gemini CLI - opt-in to keep the default install lean. Answer Yes to add it."
    Add-Result "Gemini CLI" "SKIPPED" "Opt-in"
}

# ─── 3g. Ollama - local models (opt-in, default NO) ───────────────────────────
# Heavy on both counts, so BOTH halves are gated separately: the runtime is a
# few hundred MB, and every model you pull is gigabytes on top of that. Under
# -AutoMode/-All Get-YesNo returns these defaults, so an unattended run installs
# neither. Matches the macOS kickstart's Ollama gating.
Refresh-Path
if (Test-CmdExists 'ollama') {
    Write-OK "Ollama already installed"
    Add-Result "Ollama" "PRESENT" ""
}
else {
    Write-Info "Ollama runs open-weight models on this machine - no API key, and no"
    Write-Info "prompt text leaves the box. The runtime alone is a few hundred MB."
    if (Get-YesNo "Install Ollama for offline/local models?" $false) {
        Write-Section "Installing Ollama (local model runner)"
        $ollamaInstalled = $false
        if (Test-CmdExists 'choco') {
            try {
                choco install Ollama --yes --no-progress 2>$null
                Refresh-Path
                if (Test-CmdExists 'ollama') { $ollamaInstalled = $true }
            }
            catch { Write-Info "Chocolatey install note: $_" }
        }
        if (-not $ollamaInstalled -and (Test-CmdExists 'winget')) {
            try {
                winget install --id Ollama.Ollama --accept-source-agreements --accept-package-agreements --silent 2>$null
                Refresh-Path
                if (Test-CmdExists 'ollama') { $ollamaInstalled = $true }
            }
            catch { Write-Info "winget install note: $_" }
        }
        if ($ollamaInstalled) {
            Write-OK "Ollama installed"
            Add-Result "Ollama" "INSTALLED" ""
        }
        else {
            Write-Warn "Ollama not installed (needs choco/winget, or a terminal restart)"
            Add-Result "Ollama" "SKIPPED" "Restart terminal"
        }
    }
    else {
        Add-Result "Ollama" "SKIPPED" "User declined"
    }
}

# Second gate: Ollama ships with NO models, and the smallest useful one is a
# multi-gigabyte download, so the pull gets its own default-NO prompt.
if (Test-CmdExists 'ollama') {
    Write-Info "Start the server with:  ollama serve   (Windows also runs it from the tray)"
    if (Get-YesNo "Pull a small starter model now (llama3.2, ~2GB)?" $false) {
        Write-Step "Pulling llama3.2 - this is a multi-gigabyte download..."
        try { ollama pull llama3.2 2>&1 | Out-Host } catch { Write-Warn "ollama pull note: $_" }
        $ollamaModels = ""
        try { $ollamaModels = (ollama list 2>&1 | Out-String) } catch {}
        if ($ollamaModels -match 'llama3\.2') {
            Write-OK "llama3.2 ready (run: ollama run llama3.2)"
            Add-Result "Ollama model" "INSTALLED" "llama3.2"
        }
        else {
            Write-Warn "llama3.2 not confirmed - pull it later with: ollama pull llama3.2"
            Add-Result "Ollama model" "SKIPPED" "Pull later"
        }
    }
    else {
        Write-Tip "Later: ollama pull llama3.2    then    ollama run llama3.2"
        Add-Result "Ollama model" "SKIPPED" "User declined"
    }
}

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 3h - AI FRAMEWORKS & CONVERSION CLIs
# ═══════════════════════════════════════════════════════════════════════════════

Write-Section "AI Frameworks & Conversion CLIs"
Write-Info "Agentic helpers (aider, llm), repo/document packers (repomix,"
Write-Info "files-to-prompt, markitdown) and converters (pandoc, poppler, tesseract"
Write-Info "OCR, yt-dlp, jc) that feed code and documents into any AI model."
Write-Info "Also: ruff (Python lint/format), gron (greppable JSON), and ccusage -"
Write-Info "a usage report for your agent CLIs (deepest for Claude Code, thinner"
Write-Info "for the others, so it is a usage report and not a unified bill)."

if (Get-YesNo "Install the AI framework + conversion bundle?") {

    # --- npm-based helpers ---
    # OpenAI Codex has its own prompt back in Phase 3 now, so declining this
    # bundle no longer silently costs it.
    if (Test-CmdExists 'npm') {
        Write-Step "repomix, ast-grep, tldr, ccusage (npm)"
        try { npm install -g repomix '@ast-grep/cli' tldr ccusage 2>&1 | Out-Null } catch { Write-Info "npm install note: $_" }
        Refresh-Path
        if (Test-CmdExists 'repomix') { Write-OK "repomix installed"; Add-Result "repomix" "INSTALLED" "" }
        else { Add-Result "repomix" "SKIPPED" "Restart terminal" }
        if (Test-CmdExists 'ast-grep') { Write-OK "ast-grep installed"; Add-Result "ast-grep" "INSTALLED" "" }
        else { Add-Result "ast-grep" "SKIPPED" "Restart terminal" }
        if (Test-CmdExists 'tldr') { Write-OK "tldr installed"; Add-Result "tldr" "INSTALLED" "" }
        else { Add-Result "tldr" "SKIPPED" "Restart terminal" }
        if (Test-CmdExists 'ccusage') {
            Write-OK "ccusage installed - a usage report for your agent CLIs (run: ccusage)"
            Write-Info "Its coverage is deepest for Claude Code and thinner for the others,"
            Write-Info "so read it as a usage report, not a unified bill."
            Add-Result "ccusage" "INSTALLED" "Usage report, agent CLIs"
        }
        else { Add-Result "ccusage" "SKIPPED" "Restart terminal" }
    }

    # --- headless browser + document toolkit (npm) ---
    # Kept behind its own prompt: puppeteer downloads a private Chromium build
    # (~150 MB) and playwright pulls three browser engines on first use, so this
    # is not something to install silently on a metered or small-disk machine.
    if (Test-CmdExists 'npm') {
        Write-Info ""
        Write-Info "Headless browser + document toolkit. This is what lets an agent"
        Write-Info "read a page that blocks plain fetch, render HTML to PDF, merge"
        Write-Info "and bookmark PDFs, and audit a live site."
        Write-Info "  puppeteer + puppeteer-extra + stealth plugin - drive real Chromium"
        Write-Info "  playwright                                   - second engine when Chromium is blocked"
        Write-Info "  pdf-lib                                      - merge/split PDFs, write bookmarks"
        Write-Info "  sharp                                        - fast image resize/convert"
        Write-Info "  lighthouse                                   - performance/SEO/a11y audit CLI"
        Write-Info "Downloads roughly 150 MB of browser on install."
        if (Get-YesNo "Install the headless browser + document toolkit?") {
            Write-Step "puppeteer, playwright, pdf-lib, sharp, lighthouse (npm)"
            try {
                npm install -g puppeteer puppeteer-extra puppeteer-extra-plugin-stealth playwright pdf-lib sharp lighthouse 2>&1 | Out-Null
            } catch { Write-Info "npm install note: $_" }
            Refresh-Path
            if (Test-CmdExists 'lighthouse') { Write-OK "lighthouse installed"; Add-Result "lighthouse" "INSTALLED" "" }
            else { Add-Result "lighthouse" "SKIPPED" "Restart terminal" }
            # The rest are libraries, not commands, so verify via the global root.
            $npmRoot = (npm root -g 2>$null)
            foreach ($lib in @('puppeteer', 'puppeteer-extra', 'puppeteer-extra-plugin-stealth', 'playwright', 'pdf-lib', 'sharp')) {
                if ($npmRoot -and (Test-Path (Join-Path $npmRoot $lib))) {
                    Write-OK "$lib installed"; Add-Result $lib "INSTALLED" ""
                } else { Add-Result $lib "SKIPPED" "" }
            }
            Write-Info "Playwright browsers are a separate download: npx playwright install"
            Write-Info "Node resolves globals from the script's own folder, not your cwd."
            Write-Info "To use these from a standalone script, point createRequire at:"
            Write-Info "  $npmRoot"
        }
        else { Add-Result "Headless browser toolkit" "SKIPPED" "" }
    }

    # --- pipx-isolated standalone Python CLI apps ---
    # These six are APPLICATIONS, not libraries. Dropping them into the same
    # global interpreter that already carries the 23-package data/AI bundle is
    # exactly the dependency-conflict scenario pipx exists to prevent, so each
    # one gets its own venv here (this is what the Linux and macOS kickstarts
    # already do). pipx itself has no chocolatey or winget package -
    # "python -m pip install --user pipx" is the official Windows install path.
    if (Test-CmdExists 'python') {
        Write-Step "pipx (one isolated venv per CLI app)"
        # The old code sent pip's stderr to Out-Null and then told the user to
        # "Restart terminal" whether or not pipx had actually installed. Now the
        # bootstrap captures a log, and a failure is reported as a failure - and
        # we do NOT go on to run pipx installs as though pipx existed.
        $pipx = Initialize-Pipx
        $pipxApps = @(
            @{ Pkg = 'aider-chat';      Cmd = 'aider';           Name = 'aider' },
            @{ Pkg = 'llm';             Cmd = 'llm';             Name = 'llm' },
            @{ Pkg = 'files-to-prompt'; Cmd = 'files-to-prompt'; Name = 'files-to-prompt' },
            @{ Pkg = 'markitdown[all]'; Cmd = 'markitdown';      Name = 'markitdown' },
            @{ Pkg = 'jc';              Cmd = 'jc';              Name = 'jc' },
            @{ Pkg = 'httpie';          Cmd = 'http';            Name = 'httpie' }
        )

        if ($pipx.Ok -and $pipx.Mode -eq 'pipx') {
            Write-OK "pipx ready"
            Add-Result "pipx" "INSTALLED" ""
        }
        elseif ($pipx.Ok) {
            Write-Warn "pipx shim not on PATH yet - using 'python -m pipx' for this run"
            Add-Result "pipx" "PARTIAL" "Restart terminal"
        }
        else {
            Write-Fail "pipx could not be installed"
            if ($pipx.Log) { Write-Info "Log: $($pipx.Log)" }
            Write-Tip "Fix it by hand: python -m pip install --user pipx"
            Add-Result "pipx" "FAILED" $(if ($pipx.Log) { "See $($pipx.Log)" } else { "python -m pip failed" })
        }

        if ($pipx.Ok) {
            Write-Step "aider, llm, files-to-prompt, markitdown, jc, httpie (pipx)"
            $pipxFailed = @{}
            foreach ($app in $pipxApps) {
                $appLog = Join-Path $script:TempDir "pipx-$($app.Cmd)-install.log"
                if (-not (Invoke-PipxInstall -Pipx $pipx -Package $app.Pkg -LogFile $appLog)) {
                    $pipxFailed[$app.Name] = $appLog
                }
            }
            Refresh-Path
            foreach ($app in $pipxApps) {
                if (Test-CmdExists $app.Cmd) {
                    Write-OK "$($app.Name) installed"
                    Add-Result $app.Name "INSTALLED" ""
                }
                elseif ($pipxFailed.ContainsKey($app.Name)) {
                    Write-Fail "$($app.Name) install failed - log: $($pipxFailed[$app.Name])"
                    Add-Result $app.Name "FAILED" "See $($pipxFailed[$app.Name])"
                }
                else {
                    Write-Warn "$($app.Name) installed but not on PATH yet - restart your terminal"
                    Add-Result $app.Name "PARTIAL" "Restart terminal"
                }
            }
        }
        else {
            Write-Info "Skipping the pipx-isolated CLI apps - pipx is not available."
            foreach ($app in $pipxApps) { Add-Result $app.Name "SKIPPED" "pipx unavailable" }
        }

        # 'llm' ships with OpenAI models registered and NO Anthropic support -
        # Claude models live in a plugin. Only try this if llm actually landed.
        if (Test-CmdExists 'llm') {
            Write-Step "llm-anthropic plugin (Claude models for 'llm')"
            try { llm install llm-anthropic 2>&1 | Out-Null }
            catch { Write-Info "llm-anthropic install note: $_" }
            $llmPlugins = ""
            try { $llmPlugins = (llm plugins 2>&1 | Out-String) } catch {}
            if ($llmPlugins -match 'llm-anthropic') {
                Write-OK "llm-anthropic installed - Claude models now show in 'llm models'"
                Add-Result "llm-anthropic" "INSTALLED" ""
            }
            else {
                Write-Warn "llm-anthropic not confirmed - run: llm install llm-anthropic"
                Add-Result "llm-anthropic" "PARTIAL" "Verify with: llm plugins"
            }
        }
        else {
            Write-Info "llm was not installed - skipping its llm-anthropic plugin."
            Add-Result "llm-anthropic" "SKIPPED" "llm missing"
        }

        # ruff - lint + format for Python. This box installs shellcheck for shell
        # scripts and had nothing equivalent for Python until now.
        if (Test-CmdExists 'ruff') {
            Write-OK "ruff is installed"
            Add-Result "ruff" "PRESENT" ""
        }
        else {
            Write-Info "ruff: Python linter and formatter (ruff check . / ruff format .)"
            $ruffLog      = Join-Path $script:TempDir "ruff-install.log"
            $ruffCmdOk    = $false
            # Track whether uv was actually tried. Without this, a failed
            # 'uv tool install ruff' on a box with no pipx was reported as
            # "neither uv nor pipx is available" - which was simply untrue.
            $ruffUvTried  = $false
            if (Test-CmdExists 'uv') {
                $ruffUvTried = $true
                try { uv tool install ruff 2>&1 | Tee-Object -FilePath $ruffLog | Out-Null }
                catch {
                    Write-Info "uv tool install note: $_"
                    "$_" | Out-File -FilePath $ruffLog -Append -Encoding UTF8
                }
                Refresh-Path
            }
            if (-not (Test-CmdExists 'ruff') -and $pipx.Ok) {
                $ruffCmdOk = Invoke-PipxInstall -Pipx $pipx -Package 'ruff' -LogFile $ruffLog
                Refresh-Path
            }
            if (Test-CmdExists 'ruff') {
                Write-OK "ruff installed"
                Add-Result "ruff" "INSTALLED" ""
            }
            elseif ($ruffCmdOk) {
                Write-Warn "ruff installed but not on PATH yet - restart your terminal"
                Add-Result "ruff" "PARTIAL" "Restart terminal"
            }
            elseif ($pipx.Ok -or $ruffUvTried) {
                # Something was actually attempted and 'ruff' still is not here.
                # Say which installer was tried instead of blaming a missing one.
                $ruffVia = if ($ruffUvTried -and $pipx.Ok) { "uv and pipx both failed" }
                           elseif ($ruffUvTried)          { "uv tool install failed" }
                           else                           { "pipx install failed" }
                Write-Fail "ruff install failed ($ruffVia) - log: $ruffLog"
                Add-Result "ruff" "FAILED" "See $ruffLog"
            }
            else {
                Write-Warn "ruff not installed - it needs uv or pipx, and neither is available"
                Add-Result "ruff" "SKIPPED" "No uv or pipx"
            }
        }
    }

    # --- Conversion + extra CLIs via choco / winget ---
    $aiBundle = @(
        @{ Name = 'pandoc';    Cmd = 'pandoc';    Choco = 'pandoc';    Winget = 'JohnMacFarlane.Pandoc';   Desc = 'Universal document converter' },
        @{ Name = 'poppler';   Cmd = 'pdftotext'; Choco = 'poppler';   Winget = '';                         Desc = 'PDF text extraction (pdftotext)' },
        @{ Name = 'exiftool';  Cmd = 'exiftool';  Choco = 'exiftool';  Winget = 'OliverBetz.ExifTool';      Desc = 'Read/strip metadata from any file (PDF, image, video)' },
        @{ Name = 'qpdf';      Cmd = 'qpdf';      Choco = 'qpdf';      Winget = '';                         Desc = 'PDF structural rewrite, linearize, split, merge, decrypt' },
        @{ Name = 'tesseract'; Cmd = 'tesseract'; Choco = 'tesseract'; Winget = 'UB-Mannheim.TesseractOCR'; Desc = 'OCR engine (image/scan -> text)' },
        @{ Name = 'yt-dlp';    Cmd = 'yt-dlp';    Choco = 'yt-dlp';    Winget = 'yt-dlp.yt-dlp';            Desc = 'Media downloader (pairs with ffmpeg)' },
        @{ Name = 'glow';      Cmd = 'glow';      Choco = 'glow';      Winget = 'charmbracelet.glow';       Desc = 'Render Markdown in the terminal' },
        @{ Name = 'gron';      Cmd = 'gron';      Choco = 'gron';      Winget = '';                         Desc = 'Flatten JSON into greppable lines (gron | grep | gron -u)' },
        @{ Name = 'aichat';    Cmd = 'aichat';    Choco = '';          Winget = 'sigoden.AIChat';           Desc = 'All-in-one LLM CLI' },
        @{ Name = 'just';      Cmd = 'just';      Choco = 'just';      Winget = 'Casey.Just';               Desc = 'Command/task runner (justfile)' }
    )
    foreach ($tool in $aiBundle) {
        Refresh-Path
        if (Test-CmdExists $tool.Cmd) { Write-OK "$($tool.Name) is installed"; Add-Result $tool.Name "PRESENT" ""; continue }
        Write-Info "$($tool.Name): $($tool.Desc)"
        $installed = $false
        if ($tool.Choco -and (Test-CmdExists 'choco')) {
            try { choco install $tool.Choco --yes --no-progress 2>$null; Refresh-Path; if (Test-CmdExists $tool.Cmd) { $installed = $true } } catch {}
        }
        if (-not $installed -and $tool.Winget -and (Test-CmdExists 'winget')) {
            try { winget install --id $tool.Winget --accept-source-agreements --accept-package-agreements --silent 2>$null; Refresh-Path; if (Test-CmdExists $tool.Cmd) { $installed = $true } } catch {}
        }
        if ($installed) { Write-OK "$($tool.Name) installed"; Add-Result $tool.Name "INSTALLED" "" }
        else { Write-Warn "$($tool.Name) not installed (needs choco/winget or a terminal restart)"; Add-Result $tool.Name "SKIPPED" "" }
    }
}
else {
    Write-Info "Skipped AI framework bundle. Re-run anytime to add it."
}

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 3i - AI ECOSYSTEM: MCP SERVERS & ALIGNED REPOS
# ═══════════════════════════════════════════════════════════════════════════════

Write-Section "AI Ecosystem: MCP Servers & Aligned Repos"

if (Test-CmdExists 'claude') {
    if (Get-YesNo "Register common MCP servers with Claude Code (user scope)?") {
        $aiCfgDir = Join-Path $env:USERPROFILE ".ai-tools"
        New-Item -ItemType Directory -Force -Path $aiCfgDir | Out-Null
        $memPath = Join-Path $aiCfgDir "memory.json"

        # Every 'claude mcp add --scope user' below swallows its own errors, so we record the
        # name we asked for and then check it against 'claude mcp list' at the
        # end. Reporting these as INSTALLED without looking was a lie waiting to
        # happen - an add that silently fails used to still print success.
        #
        # Two separate pools on purpose. $mcpWanted feeds the "N verified"
        # aggregate; $mcpNeedAuth holds servers that are only REGISTERED until
        # the user completes an interactive login this script cannot perform.
        # Putting github in both pools made the summary claim 7 verified while
        # the very next line said github still needed '/mcp login'.
        $mcpWanted   = @()
        $mcpNeedAuth = @()

        # npm/npx servers (Node only). @-scoped package names are single-quoted
        # so PowerShell does not treat the leading @ as a splat.
        try { claude mcp add --scope user filesystem -- npx -y '@modelcontextprotocol/server-filesystem' "$env:USERPROFILE" 2>&1 | Out-Null } catch {}
        $mcpWanted += 'filesystem'
        try { claude mcp add --scope user memory -e "MEMORY_FILE_PATH=$memPath" -- npx -y '@modelcontextprotocol/server-memory' 2>&1 | Out-Null } catch {}
        $mcpWanted += 'memory'
        try { claude mcp add --scope user sequential-thinking -- npx -y '@modelcontextprotocol/server-sequential-thinking' 2>&1 | Out-Null } catch {}
        $mcpWanted += 'sequential-thinking'
        # python/uvx servers (need uv) — fetch + time live on PyPI, not npm
        if (Test-CmdExists 'uvx') {
            try { claude mcp add --scope user fetch -- uvx mcp-server-fetch 2>&1 | Out-Null } catch {}
            $mcpWanted += 'fetch'
            try { claude mcp add --scope user time -- uvx mcp-server-time 2>&1 | Out-Null } catch {}
            $mcpWanted += 'time'
        } else {
            Write-Info "uvx not found - skipping Python MCP servers (fetch, time). Install uv to add them."
        }
        try { claude mcp add --scope user playwright -- npx -y '@playwright/mcp@latest' 2>&1 | Out-Null } catch {}
        $mcpWanted += 'playwright'
        # GitHub's own hosted MCP server (HTTP transport). Registration is all we
        # can do from here - the OAuth handshake only happens inside Claude Code,
        # so this one is tracked in $mcpNeedAuth ONLY and never counted as
        # verified alongside the local servers.
        try { claude mcp add --scope user --transport http github https://api.githubcopilot.com/mcp/ 2>&1 | Out-Null } catch {}
        $mcpNeedAuth += 'github'

        # Verify: a server only counts if 'claude mcp list' actually lists it.
        # 'claude mcp list' is NOT a cheap read - it health-checks every server
        # by launching it, which on a first run means six cold npx/uvx downloads
        # plus an HTTP handshake. Unbounded, that hangs the installer with no
        # output, so it runs as a job with a hard 90-second ceiling. A timeout is
        # UNKNOWN - it is not evidence of failure and not evidence of success.
        Write-Step "Verifying MCP registration with 'claude mcp list'"
        Write-Info "This launches every registered server to health-check it, so on a first"
        Write-Info "run it can take a minute (cold npx/uvx downloads). Waiting up to 90s..."
        $mcpList     = ""
        $mcpTimedOut = $false
        try {
            $mcpJob = Start-Job -ScriptBlock { claude mcp list 2>&1 | Out-String }
            if (Wait-Job -Job $mcpJob -Timeout 90) {
                $mcpList = (Receive-Job -Job $mcpJob -ErrorAction SilentlyContinue | Out-String)
            }
            else {
                $mcpTimedOut = $true
                Stop-Job -Job $mcpJob -ErrorAction SilentlyContinue
            }
            Remove-Job -Job $mcpJob -Force -ErrorAction SilentlyContinue
        }
        catch {
            Write-Info "Could not run 'claude mcp list' as a job: $_"
        }

        if ($mcpTimedOut) {
            Write-Warn "'claude mcp list' did not finish within 90 seconds - could not verify."
            Write-Info "That is a slow health check, not proof the servers are broken."
            Write-Tip "Check by hand when you have a minute: claude mcp list"
            Add-Result "MCP servers" "UNKNOWN" "Verification timed out"
        }
        elseif ([string]::IsNullOrWhiteSpace($mcpList)) {
            Write-Warn "'claude mcp list' returned nothing - registration could not be verified."
            Write-Tip "Check by hand: claude mcp list"
            Add-Result "MCP servers" "UNKNOWN" "Could not verify"
        }
        else {
            $mcpOk      = @()
            $mcpMissing = @()
            foreach ($name in $mcpWanted) {
                if ($mcpList -match "(?m)^\s*$([regex]::Escape($name))\s*:") { $mcpOk += $name }
                else { $mcpMissing += $name }
            }
            foreach ($name in $mcpOk)      { Write-OK "MCP '$name' is registered" }
            foreach ($name in $mcpMissing) { Write-Fail "MCP '$name' did NOT register" }

            if ($mcpMissing.Count -eq 0) {
                Write-OK "All $($mcpOk.Count) MCP servers verified"
                Add-Result "MCP servers" "INSTALLED" "$($mcpOk.Count) verified"
            }
            elseif ($mcpOk.Count -gt 0) {
                Write-Warn "Missing: $($mcpMissing -join ', ')"
                Write-Tip "Re-run this script, or add the missing ones with 'claude mcp add --scope user'."
                Add-Result "MCP servers" "PARTIAL" "$($mcpOk.Count)/$($mcpWanted.Count) verified"
            }
            else {
                Write-Fail "No MCP servers verified - inspect 'claude mcp list' by hand."
                Add-Result "MCP servers" "FAILED" "None verified"
            }

            # Registered is not the same as usable: these still need an
            # interactive OAuth step this script cannot perform, so they get
            # their own row and stay OUT of the verified aggregate above.
            foreach ($name in $mcpNeedAuth) {
                if ($mcpList -match "(?m)^\s*$([regex]::Escape($name))\s*:") {
                    Write-Warn "MCP '$name' is registered but NOT authorised yet."
                    Write-Tip "Run 'claude', type '/mcp', and authorise '$name' in the browser."
                    Add-Result "MCP $name" "REGISTERED" "needs /mcp login"
                }
                else {
                    Write-Fail "MCP '$name' did NOT register"
                    Add-Result "MCP $name" "FAILED" "Not registered"
                }
            }
        }
    } else {
        Add-Result "MCP servers" "SKIPPED" ""
    }
} else {
    Write-Info "Claude Code not installed - skipping MCP server registration."
}

Write-Host ""
Write-Info "Aligned GitHub repos worth bookmarking:"
Write-Info "  awesome-claude-code     https://github.com/hesreallyhim/awesome-claude-code"
Write-Info "  awesome-mcp-servers     https://github.com/punkpeye/awesome-mcp-servers"
Write-Info "  MCP reference servers   https://github.com/modelcontextprotocol/servers"
Write-Info "  OpenAI Codex CLI        https://github.com/openai/codex"
Write-Info "  Aider                   https://github.com/Aider-AI/aider"
if ((Test-CmdExists 'git') -and (Get-YesNo "Clone these reference repos into ~/ai-tools?" $false)) {
    $aiReposDir = Join-Path $env:USERPROFILE "ai-tools"
    New-Item -ItemType Directory -Force -Path $aiReposDir | Out-Null
    $repos = @(
        "https://github.com/hesreallyhim/awesome-claude-code.git",
        "https://github.com/punkpeye/awesome-mcp-servers.git",
        "https://github.com/modelcontextprotocol/servers.git",
        "https://github.com/openai/codex.git",
        "https://github.com/Aider-AI/aider.git"
    )
    # Count what is actually on disk afterwards. The old row said INSTALLED even
    # when every single clone had failed.
    $repoOk     = @()
    $repoFailed = @()
    foreach ($r in $repos) {
        $rn = [System.IO.Path]::GetFileNameWithoutExtension($r)
        $dest = Join-Path $aiReposDir $rn
        if (Test-Path (Join-Path $dest ".git")) { Write-OK "$rn already cloned" }
        else {
            git clone --depth 1 $r $dest 2>&1 | Out-Null
            if ($LASTEXITCODE -eq 0) { Write-OK "cloned $rn" } else { Write-Warn "clone failed: $rn" }
        }
        if (Test-Path (Join-Path $dest ".git")) { $repoOk += $rn } else { $repoFailed += $rn }
    }
    if ($repoFailed.Count -eq 0) {
        Add-Result "Aligned repos" "INSTALLED" "$aiReposDir"
    }
    elseif ($repoOk.Count -gt 0) {
        Write-Warn "Not cloned: $($repoFailed -join ', ')"
        Add-Result "Aligned repos" "PARTIAL" "$($repoOk.Count)/$($repos.Count) cloned"
    }
    else {
        Write-Fail "No reference repos could be cloned - check network/proxy access to github.com"
        Add-Result "Aligned repos" "FAILED" "0 cloned"
    }
}

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 3j - NETLIFY CLI & DEPLOYMENT
# ═══════════════════════════════════════════════════════════════════════════════

Enter-Phase "Deployment & Extras"
Write-Section "Netlify CLI & Deployment Tools"

Write-Step "Checking Netlify CLI"
Refresh-Path
if (Test-CmdExists 'netlify') {
    $netlifyVer = netlify --version 2>$null
    Write-OK "Netlify CLI already installed ($netlifyVer)"
    Add-Result "Netlify CLI" "PRESENT" "$netlifyVer"
}
else {
    Write-Info "Netlify CLI lets you deploy websites directly from your terminal."
    Write-Info "Free tier: 100GB bandwidth, 300 build minutes/month, custom domains."
    Write-Info "Works beautifully with Claude Code - AI edits your site, then deploys."
    if (Get-YesNo "Install Netlify CLI?" $true) {
        if (Test-CmdExists 'npm') {
            Write-Info "Installing netlify-cli globally via npm..."
            $netlifyLog   = Join-Path $script:TempDir "netlify-cli-install.log"
            $netlifyCmdOk = $false
            try {
                npm install -g netlify-cli 2>&1 | Tee-Object -FilePath $netlifyLog | Out-Host
                $netlifyCmdOk = ($LASTEXITCODE -eq 0)
            }
            catch {
                Write-Warn "Netlify CLI install error: $_"
                "$_" | Out-File -FilePath $netlifyLog -Append -Encoding UTF8
            }
            Refresh-Path
            if (Test-CmdExists 'netlify') {
                Write-OK "Netlify CLI installed ($(netlify --version 2>$null))"
                Add-Result "Netlify CLI" "INSTALLED" ""
            }
            elseif ($netlifyCmdOk) {
                # npm exited 0, so the shim genuinely may only need a new shell.
                Write-Warn "Netlify CLI installed but not yet on PATH - restart your terminal"
                Add-Result "Netlify CLI" "PARTIAL" "Restart terminal"
            }
            else {
                Write-Fail "Netlify CLI install failed - log: $netlifyLog"
                Write-Tip "Try by hand: npm install -g netlify-cli"
                Add-Result "Netlify CLI" "FAILED" "See $netlifyLog"
            }
        }
        else {
            Write-Fail "npm not available - install Node.js first"
            Add-Result "Netlify CLI" "FAILED" "npm missing"
        }
    }
    else {
        Add-Result "Netlify CLI" "SKIPPED" ""
    }
}

# Netlify authentication guide
Write-Host ""
Write-Host "  ┌──────────────────────────────────────────────────────────────┐" -ForegroundColor DarkCyan
Write-Host "  │  NETLIFY AUTHENTICATION & DEPLOYMENT                         │" -ForegroundColor DarkCyan
Write-Host "  ├──────────────────────────────────────────────────────────────┤" -ForegroundColor DarkCyan
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  LOGIN:                                                      │" -ForegroundColor DarkCyan
Write-Host "  │    netlify login                                             │" -ForegroundColor White
Write-Host "  │    (browser opens > click Authorize > return to terminal)    │" -ForegroundColor Gray
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  DEPLOY FROM ANY AI TERMINAL:                                │" -ForegroundColor DarkCyan
Write-Host "  │    cd C:\path\to\my-website                                  │" -ForegroundColor White
Write-Host "  │    netlify init             # link folder to Netlify site    │" -ForegroundColor White
Write-Host "  │    netlify deploy           # preview draft                  │" -ForegroundColor White
Write-Host "  │    netlify deploy --prod    # push live                      │" -ForegroundColor White
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  AI + DEPLOY (Claude Code):                                  │" -ForegroundColor DarkCyan
Write-Host "  │    cd C:\my-site && claude                                   │" -ForegroundColor White
Write-Host "  │    > fix the nav menu and deploy a draft to Netlify          │" -ForegroundColor White
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  AI + DEPLOY (ChatGPT terminal > manual):                    │" -ForegroundColor DarkCyan
Write-Host "  │    sgpt 'write me CSS for a responsive hero'                 │" -ForegroundColor White
Write-Host "  │    (copy output to your file, then: netlify deploy --prod)   │" -ForegroundColor Gray
Write-Host "  │                                                              │" -ForegroundColor DarkCyan
Write-Host "  │  AI + DEPLOY (Copilot CLI):                                  │" -ForegroundColor DarkCyan
Write-Host "  │    copilot -p 'deploy this folder to Netlify'                │" -ForegroundColor White
Write-Host "  └──────────────────────────────────────────────────────────────┘" -ForegroundColor DarkCyan

if (Test-CmdExists 'netlify') {
    Write-Host ""
    if (Get-YesNo "Log in to Netlify now?" $false) {
        Write-Step "Opening Netlify login..."
        netlify login
    }
}

# ─── Cloudflare Wrangler (Pages + Workers) ──────────────────────────────────────
Write-Step "Checking Cloudflare Wrangler"
if (Test-CmdExists 'wrangler') {
    Write-OK "Wrangler already installed"
    Add-Result "Cloudflare Wrangler" "PRESENT" ""
}
else {
    Write-Info "Wrangler deploys to Cloudflare Pages (static sites) and Workers (edge)."
    if (Get-YesNo "Install Cloudflare Wrangler?") {
        if (Test-CmdExists 'npm') {
            $wranglerLog   = Join-Path $script:TempDir "wrangler-install.log"
            $wranglerCmdOk = $false
            try {
                npm install -g wrangler 2>&1 | Tee-Object -FilePath $wranglerLog | Out-Null
                $wranglerCmdOk = ($LASTEXITCODE -eq 0)
            }
            catch {
                Write-Info "npm note: $_"
                "$_" | Out-File -FilePath $wranglerLog -Append -Encoding UTF8
            }
            Refresh-Path
            if (Test-CmdExists 'wrangler') { Write-OK "Wrangler installed"; Add-Result "Cloudflare Wrangler" "INSTALLED" "" }
            elseif ($wranglerCmdOk) { Write-Warn "Wrangler installed but needs terminal restart for PATH"; Add-Result "Cloudflare Wrangler" "PARTIAL" "Restart terminal" }
            else { Write-Fail "Wrangler install failed - log: $wranglerLog"; Add-Result "Cloudflare Wrangler" "FAILED" "See $wranglerLog" }
        }
        else { Write-Fail "npm not available - install Node.js first"; Add-Result "Cloudflare Wrangler" "FAILED" "npm missing" }
    }
    else { Add-Result "Cloudflare Wrangler" "SKIPPED" "" }
}
Write-Info "Cloudflare deploy quickstart:"
Write-Tip "wrangler login                                     # browser OAuth"
Write-Tip "wrangler pages deploy .\dist --project-name=NAME    # static site -> Pages"
Write-Tip "wrangler deploy                                    # deploy a Worker (edge)"
Write-Tip "Headless/CI: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID"
if (Test-CmdExists 'wrangler') {
    if (Get-YesNo "Log in to Cloudflare now?" $false) { wrangler login }
}

# ─── Additional: Windows Terminal & Fonts ──────────────────────────────────────
Write-Step "Checking Windows Terminal"
# Get-AppxPackage lives in the Appx module, which FAILS TO LOAD on PowerShell 7
# ("Operation is not supported on this platform"). That is a module-load error, so
# -ErrorAction SilentlyContinue does not suppress it and the whole statement is
# abandoned - no result row, raw red error on screen. Probe for the cmdlet first.
$wtInstalled = $null
if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) {
    try { $wtInstalled = Get-AppxPackage -Name "Microsoft.WindowsTerminal" -ErrorAction SilentlyContinue } catch { $wtInstalled = $null }
}
if ($wtInstalled) {
    Write-OK "Windows Terminal installed"
    Add-Result "Windows Terminal" "PRESENT" ""
}
else {
    Write-Info "Windows Terminal provides tabs, GPU rendering, and Nerd Font support."
    if (Get-YesNo "Install Windows Terminal?") {
        if (Test-CmdExists 'winget') {
            $wtCmdOk = $false
            $wtExit  = "no exit code"
            try {
                winget install --id Microsoft.WindowsTerminal --accept-source-agreements --accept-package-agreements --silent 2>$null
                $wtExit  = $LASTEXITCODE
                $wtCmdOk = ($LASTEXITCODE -eq 0)
            }
            catch { Write-Info "winget note: $_" }
            # Appx registration is what actually decides this, not winget's say-so.
            $wtNow = $null
            if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) {
                try { $wtNow = Get-AppxPackage -Name "Microsoft.WindowsTerminal" -ErrorAction SilentlyContinue } catch { $wtNow = $null }
            }
            if ($wtNow) {
                Write-OK "Windows Terminal installed"
                Add-Result "Windows Terminal" "INSTALLED" ""
            }
            elseif ($wtCmdOk) {
                Write-Warn "winget reported success but the package is not registered for this user yet"
                Write-Tip "Sign out and back in, or open Windows Terminal once from the Start menu."
                Add-Result "Windows Terminal" "PARTIAL" "Sign out/in"
            }
            else {
                Write-Fail "Windows Terminal install failed (winget exit $wtExit)"
                Write-Tip "Install from Microsoft Store: search 'Windows Terminal'"
                Add-Result "Windows Terminal" "FAILED" "winget install failed"
            }
        }
        else {
            Write-Info "Install from Microsoft Store: search 'Windows Terminal'"
            Add-Result "Windows Terminal" "SKIPPED" "Install from Store"
        }
    }
    else {
        Add-Result "Windows Terminal" "SKIPPED" ""
    }
}

# UTF-8 console
Write-Step "Setting console to UTF-8"
$currentCP = chcp 2>$null
if ($currentCP -notmatch '65001') {
    chcp 65001 > $null 2>&1
    Write-OK "Console code page set to UTF-8 (65001)"
}
else {
    Write-OK "Console already UTF-8"
}

# Vercel CLI
Write-Step "Checking Vercel CLI"
Refresh-Path
if (Test-CmdExists 'vercel') {
    Write-OK "Vercel CLI already installed"
    Add-Result "Vercel CLI" "PRESENT" ""
}
else {
    Write-Info "Vercel is another popular hosting platform (like Netlify)."
    if (Get-YesNo "Install Vercel CLI?" $false) {
        if (Test-CmdExists 'npm') {
            $vercelLog   = Join-Path $script:TempDir "vercel-install.log"
            $vercelCmdOk = $false
            try {
                npm install -g vercel 2>&1 | Tee-Object -FilePath $vercelLog | Out-Null
                $vercelCmdOk = ($LASTEXITCODE -eq 0)
            }
            catch {
                Write-Warn "Vercel CLI install error: $_"
                "$_" | Out-File -FilePath $vercelLog -Append -Encoding UTF8
            }
            Refresh-Path
            if (Test-CmdExists 'vercel') {
                Write-OK "Vercel CLI installed"
                Add-Result "Vercel CLI" "INSTALLED" ""
            }
            elseif ($vercelCmdOk) {
                # npm exited 0, so the shim genuinely may only need a new shell.
                Write-Warn "Vercel CLI installed but not yet on PATH - restart your terminal"
                Add-Result "Vercel CLI" "PARTIAL" "Restart terminal"
            }
            else {
                Write-Fail "Vercel CLI install failed - log: $vercelLog"
                Write-Tip "Try by hand: npm install -g vercel"
                Add-Result "Vercel CLI" "FAILED" "See $vercelLog"
            }
        }
        else {
            Write-Fail "npm not available - install Node.js first"
            Add-Result "Vercel CLI" "FAILED" "npm missing"
        }
    }
    else {
        Add-Result "Vercel CLI" "SKIPPED" ""
    }
}

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 4 - VALIDATION & HEALTH CHECK
# ═══════════════════════════════════════════════════════════════════════════════

Enter-Phase "Validation & Health Check"
Write-Section "PHASE 4: Environment Health Check"

Refresh-Path

# Resolve Git Bash the same way Phase 3 does. If it cannot be found at all we
# still put the ProgramFiles path in the table so the row reads MISSING (a real
# FAIL) instead of quietly passing.
$gitBashCheckPath = Get-GitBashPath
if (-not $gitBashCheckPath) { $gitBashCheckPath = "$env:ProgramFiles\Git\bin\bash.exe" }

$checks = @(
    @{ Name = "PowerShell";     Cmd = "pwsh --version";           Alt = "`$PSVersionTable.PSVersion" },
    @{ Name = "Git";            Cmd = "git --version";            Alt = $null },
    @{ Name = "Git Bash";       Cmd = $null;                      Path = $gitBashCheckPath },
    @{ Name = "Node.js";        Cmd = "node --version";           Alt = $null },
    @{ Name = "npm";            Cmd = "npm --version";            Alt = $null },
    @{ Name = "npx";            Cmd = "npx --version";            Alt = $null },
    @{ Name = "Python";         Cmd = "python --version";         Alt = $null },
    @{ Name = "pip";            Cmd = "pip --version";            Alt = $null },
    @{ Name = "uv";             Cmd = "uv --version";            Alt = $null },
    @{ Name = "Claude Code";    Cmd = "claude --version";         Alt = $null; Path2 = "$env:USERPROFILE\.local\bin\claude.exe" },
    @{ Name = "ChatGPT (sgpt)"; Cmd = $null;                      Alt = $null; Path2 = (Join-Path $env:USERPROFILE ".local\bin\sgpt.exe") },
    @{ Name = "OpenAI Codex";   Cmd = "codex --version";          Alt = $null },
    @{ Name = "GitHub CLI";     Cmd = "gh --version";             Alt = $null },
    @{ Name = "Copilot CLI";    Cmd = "copilot --version";        Alt = $null },
    @{ Name = "Kimi Code CLI";  Cmd = "kimi --version";           Alt = $null },
    @{ Name = "ripgrep";        Cmd = "rg --version";             Alt = $null },
    @{ Name = "jq";             Cmd = "jq --version";             Alt = $null },
    @{ Name = "ImageMagick";    Cmd = "magick --version";         Alt = $null },
    @{ Name = "GhostScript";    Cmd = "gswin64c --version";       Alt = $null },
    @{ Name = "FFmpeg";         Cmd = "ffmpeg -version";          Alt = $null },
    @{ Name = "bat";            Cmd = "bat --version";            Alt = $null },
    @{ Name = "fzf";            Cmd = "fzf --version";            Alt = $null },
    @{ Name = "delta";          Cmd = "delta --version";          Alt = $null },
    @{ Name = "shellcheck";     Cmd = "shellcheck --version";     Alt = $null },
    @{ Name = "yq";             Cmd = "yq --version";             Alt = $null },
    @{ Name = "exiftool";       Cmd = "exiftool -ver";            Alt = $null },
    @{ Name = "qpdf";           Cmd = "qpdf --version";           Alt = $null },
    @{ Name = "lighthouse";     Cmd = "lighthouse --version";     Alt = $null },
    @{ Name = "Netlify CLI";    Cmd = "netlify --version";        Alt = $null },
    @{ Name = "Vercel CLI";     Cmd = "vercel --version";         Alt = $null },
    @{ Name = "Chocolatey";     Cmd = "choco --version";          Alt = $null }
)

# ─── Functional Validation (not just version checks) ──────────────────────────
Write-Step "Running functional validation tests"

$funcTests = @()

# Test Node.js can actually execute JavaScript
if (Test-CmdExists 'node') {
    try {
        $nodeTest = node -e "console.log('node-ok')" 2>&1
        if ($nodeTest -match 'node-ok') { $funcTests += @{ Name = "Node.js exec"; Pass = $true } }
        else { $funcTests += @{ Name = "Node.js exec"; Pass = $false } }
    }
    catch { $funcTests += @{ Name = "Node.js exec"; Pass = $false } }
}

# Test Python can import key AI packages
if (Test-CmdExists 'python') {
    try {
        $pyTest = python -c "import ssl; print('py-ssl-ok:', ssl.OPENSSL_VERSION)" 2>&1
        if ($pyTest -match 'py-ssl-ok') { $funcTests += @{ Name = "Python SSL"; Pass = $true } }
        else { $funcTests += @{ Name = "Python SSL"; Pass = $false } }
    }
    catch { $funcTests += @{ Name = "Python SSL"; Pass = $false } }

    try {
        $pyImport = python -c "import requests, pydantic; print('imports-ok')" 2>&1
        if ($pyImport -match 'imports-ok') { $funcTests += @{ Name = "Python packages"; Pass = $true } }
        else { $funcTests += @{ Name = "Python packages"; Pass = $false } }
    }
    catch { $funcTests += @{ Name = "Python packages"; Pass = $false } }
}

# Test npm registry connectivity
if (Test-CmdExists 'npm') {
    try {
        $npmPing = npm ping 2>&1
        if ($LASTEXITCODE -eq 0) { $funcTests += @{ Name = "npm registry"; Pass = $true } }
        else { $funcTests += @{ Name = "npm registry"; Pass = $false } }
    }
    catch { $funcTests += @{ Name = "npm registry"; Pass = $false } }
}

# Test Git can actually connect (credential check)
if (Test-CmdExists 'git') {
    try {
        $gitLs = git ls-remote --heads https://github.com/anthropics/claude-code.git 2>&1
        if ($LASTEXITCODE -eq 0) { $funcTests += @{ Name = "Git HTTPS"; Pass = $true } }
        else { $funcTests += @{ Name = "Git HTTPS"; Pass = $false } }
    }
    catch { $funcTests += @{ Name = "Git HTTPS"; Pass = $false } }
}

# Test ImageMagick delegates (GhostScript for PDF)
if (Test-CmdExists 'magick') {
    try {
        $delegates = magick identify -list format 2>&1
        if ($delegates -match 'PDF') { $funcTests += @{ Name = "ImageMagick PDF"; Pass = $true } }
        else { $funcTests += @{ Name = "ImageMagick PDF"; Pass = $false } }
    }
    catch { $funcTests += @{ Name = "ImageMagick PDF"; Pass = $false } }
}

foreach ($ft in $funcTests) {
    if ($ft.Pass) {
        Write-Host "     $($ft.Name.PadRight(18)) " -NoNewline
        Write-Host "PASS" -ForegroundColor Green
    }
    else {
        Write-Host "     $($ft.Name.PadRight(18)) " -NoNewline
        Write-Host "FAIL" -ForegroundColor Red
    }
}

Write-Host ""
$colName   = "Component".PadRight(18)
$colStatus = "Status".PadRight(10)
$colVer    = "Version / Details"
Write-Host "     $colName $colStatus $colVer" -ForegroundColor White
Write-Host "     $('-' * 18) $('-' * 10) $('-' * 35)" -ForegroundColor DarkGray

$passCount = 0
$failCount = 0

foreach ($check in $checks) {
    $name = $check.Name.PadRight(18)
    $ver = ""
    $found = $false

    # Check via command
    if ($check.Cmd) {
        try {
            $ver = Invoke-Expression $check.Cmd 2>&1 | Select-Object -First 1
            if ($LASTEXITCODE -eq 0 -or $ver) { $found = $true }
        }
        catch {}
    }

    # Check via path
    if (-not $found -and $check.ContainsKey('Path') -and $check.Path) {
        if (Test-Path $check.Path) {
            $found = $true
            $ver = $check.Path
        }
    }
    if (-not $found -and $check.ContainsKey('Path2') -and $check.Path2) {
        if (Test-Path $check.Path2) {
            $found = $true
            $ver = $check.Path2
        }
    }

    # Check via alt expression
    if (-not $found -and $check.Alt) {
        try {
            $ver = Invoke-Expression $check.Alt 2>&1
            if ($ver) { $found = $true }
        }
        catch {}
    }

    if ($found) {
        $status = "PASS".PadRight(10)
        $verStr = "$ver".Trim()
        if ($verStr.Length -gt 35) { $verStr = $verStr.Substring(0, 35) }
        Write-Host "     $name " -NoNewline
        Write-Host $status -NoNewline -ForegroundColor Green
        Write-Host $verStr -ForegroundColor Gray
        $passCount++
    }
    else {
        $status = "MISSING".PadRight(10)
        Write-Host "     $name " -NoNewline
        Write-Host $status -NoNewline -ForegroundColor Red
        Write-Host "Not installed" -ForegroundColor DarkGray
        $failCount++
    }
}

Write-Host ""
Write-Host "     $('-' * 65)" -ForegroundColor DarkGray
Write-Host "     Total: $passCount passed, $failCount missing" -ForegroundColor $(if ($failCount -eq 0) { 'Green' } else { 'Yellow' })

# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 5 - QUICK REFERENCE GUIDE
# ═══════════════════════════════════════════════════════════════════════════════

Complete-Progress "AI Terminal Kickstart"
Write-Section "PHASE 5: Quick Reference Guide"

$elapsed = (Get-Date) - $script:StartTime

Write-Host @"

  ┌───────────────────────────────────────────────────────────────────────────┐
  │                    HOW TO USE YOUR AI TERMINALS                           │
  ├───────────────────────────────────────────────────────────────────────────┤
  │                                                                           │
  │  CLAUDE CODE (Best for coding, files, agents)                             │
  │  ─────────────────────────────────────────────                            │
  │    claude                    Start interactive session                    │
  │    claude "fix this bug"     One-shot command                             │
  │    claude --help             See all options                              │
  │    claude doctor             Diagnose configuration                       │
  │    claude mcp list           Show MCP servers                             │
  │    /help                     In-session help                              │
  │                                                                           │
  │  First-time auth: run 'claude' > browser opens > log in at                │
  │  claude.ai > click Authorize > return to terminal > done!                 │
  │                                                                           │
  │  Keyboard: Enter = send | Ctrl+C = cancel | Escape = exit                 │
  │  Power moves: Ask it to edit files, run tests, create PRs                 │
  │                                                                           │
  │  CHATGPT TERMINAL - shell-gpt (quick Q&A, shell commands)                 │
  │  ─────────────────────────────────────────────────                        │
  │    sgpt "explain X"          Quick one-shot question                      │
  │    sgpt --shell "..."        Suggest a shell command                      │
  │    sgpt --code "..."         Return code only, no prose                   │
  │    sgpt --repl chat          Interactive session                          │
  │    sgpt --model gpt-4o       Choose the model                             │
  │    OPENAI_API_KEY            The key sgpt reads                           │
  │                                                                           │
  │  GITHUB COPILOT CLI (Best for shell commands)                             │
  │  ─────────────────────────────────────────────                            │
  │    copilot                   Start an interactive session                 │
  │    copilot -p "..."          One-shot prompt                              │
  │    /login (in copilot)       Sign in to GitHub Copilot                    │
  │                                                                           │
  ├───────────────────────────────────────────────────────────────────────────┤
  │  NETLIFY DEPLOYMENT (direct deploy from any AI terminal)                  │
  │  ──────────────────────────────────────────────────                       │
  │    netlify login             Authenticate (one-time)                      │
  │    cd C:\path\to\site        Go to your site folder                       │
  │    netlify init              Link to Netlify site (one-time)              │
  │    netlify deploy            Preview deploy (draft URL)                   │
  │    netlify deploy --prod     Push to production (live URL)                │
  │    netlify open              Open live site in browser                    │
  │    netlify env:set KEY val   Set environment variables                    │
  │                                                                           │
  │  AI + DEPLOY WORKFLOWS:                                                   │
  │    Claude:  cd site && claude                                             │
  │             > fix the footer and deploy a draft to Netlify                │
  │    ChatGPT: sgpt "..." > paste the answer into your file                  │
  │    Copilot: copilot -p "deploy to Netlify"                                │
  │                                                                           │
  ├───────────────────────────────────────────────────────────────────────────┤
  │  GUI & WEB INTERFACES                                                     │
  │  ────────────────────                                                     │
  │    Claude:   https://claude.ai/code (web app)                             │
  │              VS Code extension: "Claude Code" in marketplace              │
  │              JetBrains extension: search "Claude" in plugins              │
  │    ChatGPT:  https://chatgpt.com (web interface)                          │
  │              Desktop app: https://openai.com/chatgpt/download             │
  │    Copilot:  Built into VS Code (GitHub Copilot extension)                │
  │              Built into github.com (Copilot Chat in repos)                │
  │    Netlify:  https://app.netlify.com (dashboard)                          │
  │                                                                           │
  ├───────────────────────────────────────────────────────────────────────────┤
  │  GOOGLE DRIVE & APP CONNECTORS (via MCP Servers)                          │
  │  ───────────────────────────────────────────────                          │
  │    Claude Code can connect to external apps via MCP:                      │
  │                                                                           │
  │    Google Drive:                                                          │
  │      claude mcp add --scope user gdrive -- npx @anthropic/gdrive-mcp      │
  │      (requires Google OAuth - follow the prompts)                         │
  │      Then: > list my Google Drive files                                   │
  │            > read the doc called "Project Plan"                           │
  │                                                                           │
  │    Slack:                                                                 │
  │      claude mcp add --scope user slack -- npx @anthropic/slack-mcp        │
  │      Then: > check #general for recent messages                           │
  │                                                                           │
  │    GitHub:                                                                │
  │      (this script registers it; run /mcp in claude to authorise)          │
  │      Then: > list open PRs in my repo                                     │
  │                                                                           │
  │    Filesystem (already built-in):                                         │
  │      Claude Code reads/writes files in your current directory             │
  │      by default - no extra setup needed.                                  │
  │                                                                           │
  │    Playwright (browser automation):                                       │
  │      claude mcp add --scope user playwright -- npx @playwright/mcp@latest │
  │      Then: > take a screenshot of my site at localhost:8080               │
  │                                                                           │
  │  COMMON TIPS FOR ALL TERMINALS                                            │
  │  ─────────────────────────────                                            │
  │    - Be specific in your prompts for better results                       │
  │    - Use quotes around multi-word arguments                               │
  │    - Start simple, then add detail if output is wrong                     │
  │    - Check 'claude doctor' or '/help' if something breaks                 │
  │    - Restart terminal after fresh installs for PATH updates               │
  │                                                                           │
  └───────────────────────────────────────────────────────────────────────────┘

"@ -ForegroundColor Cyan

# ═══════════════════════════════════════════════════════════════════════════════
#  FINAL SUMMARY
# ═══════════════════════════════════════════════════════════════════════════════

Write-Host ""
Write-Host "  ╔═══════════════════════════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host "  ║                    KICKSTART COMPLETE                             ║" -ForegroundColor Green
Write-Host "  ╠═══════════════════════════════════════════════════════════════════╣" -ForegroundColor Green
Write-Host "  ║                                                                   ║" -ForegroundColor Green

# Display results table
$colComp   = "Component".PadRight(22)
$colStat   = "Status".PadRight(12)
$colDetail = "Detail"
Write-Host "  ║  $colComp $colStat $colDetail" -ForegroundColor Green
Write-Host "  ║  $('-' * 22) $('-' * 12) $('-' * 25)" -ForegroundColor DarkGreen

foreach ($r in $script:Results) {
    $comp = $r.Component.PadRight(22)
    $stat = $r.Status.PadRight(12)
    $det  = if ($r.Detail.Length -gt 25) { $r.Detail.Substring(0, 25) } else { $r.Detail }
    $color = switch -Regex ($r.Status) {
        'INSTALL|PRESENT|RAN|OK' { 'Green' }
        'SKIPPED|PARTIAL'        { 'Yellow' }
        'FAILED|ERROR'           { 'Red' }
        default                  { 'Gray' }
    }
    Write-Host "  ║  " -NoNewline -ForegroundColor Green
    Write-Host "$comp " -NoNewline -ForegroundColor $color
    Write-Host "$stat " -NoNewline -ForegroundColor $color
    Write-Host "$det" -ForegroundColor Gray
}

Write-Host "  ║                                                                   ║" -ForegroundColor Green
Write-Host "  ║  Time elapsed: $($elapsed.ToString('mm\:ss'))                                              ║" -ForegroundColor Green
Write-Host "  ║  Log file    : $($script:LogFile.PadRight(45))      ║" -ForegroundColor Green
Write-Host "  ║                                                                   ║" -ForegroundColor Green
Write-Host "  ║  NEXT STEPS:                                                      ║" -ForegroundColor Green
Write-Host "  ║    1. CLOSE this terminal and open a NEW one (PATH refresh)       ║" -ForegroundColor Green
Write-Host "  ║    2. Run 'claude' to start Claude Code                           ║" -ForegroundColor Green
Write-Host "  ║    3. Run 'sgpt' for the ChatGPT terminal (shell-gpt)             ║" -ForegroundColor Green
Write-Host "  ║    4. Run 'copilot' for GitHub Copilot CLI                        ║" -ForegroundColor Green
Write-Host "  ║                                                                   ║" -ForegroundColor Green
Write-Host "  ╚═══════════════════════════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""

# Cleanup temp if empty
if ((Get-ChildItem $script:TempDir -File -ErrorAction SilentlyContinue).Count -le 1) {
    # Keep the log file, remove installers
    Get-ChildItem $script:TempDir -File -Exclude "kickstart.log" -ErrorAction SilentlyContinue |
        Remove-Item -Force -ErrorAction SilentlyContinue
}

Write-Host "  Press any key to exit..." -ForegroundColor Gray
try { $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') } catch {}

Step 1 — Install the runtimes

Git, Node.js, Python and the core CLI utilities. Everything after this step assumes they are on your PATH.

Windows (PowerShell)

# One-time: allow the current session to run scripts
Set-ExecutionPolicy Bypass -Scope Process -Force

# Install Chocolatey (Windows package manager)
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

# Install runtimes + utilities
choco install -y git nodejs-lts python312 ripgrep jq fd 7zip imagemagick pwsh

# Verify
node --version ; npm --version ; python --version ; git --version

Linux (Bash, Ubuntu / Debian / Mint)

# Update + install runtimes + utilities
sudo apt update
sudo apt install -y git curl build-essential python3 python3-pip python3-venv ripgrep jq fd-find imagemagick

# Install Node.js LTS via NodeSource (the apt package is usually out of date)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs

# Verify
node --version ; npm --version ; python3 --version ; git --version

Linux (Bash, Fedora / RHEL / Rocky)

sudo dnf install -y git curl python3 python3-pip ripgrep jq fd-find ImageMagick
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -
sudo dnf install -y nodejs

Close the terminal and open a new one after runtime installs. PATH updates only take effect in fresh sessions.

Step 2 — Install Claude Code

Anthropic's AI coding terminal. Reads your repo, edits files, runs commands, opens PRs.

Windows (PowerShell)

# Official installer (puts 'claude' on your PATH)
irm https://claude.ai/install.ps1 | iex

Linux (Bash)

curl -fsSL https://claude.ai/install.sh | sh

Both platforms verify:

claude --version

Authenticate once by just running claude in any directory, it opens a browser to claude.ai, you click Authorize, and you're done. No API key needed for the browser flow. If you prefer an API key (required for CI):

# Windows (persists across sessions):
[Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", "sk-ant-your-key-here", "User")
# Linux:
echo 'export ANTHROPIC_API_KEY="sk-ant-your-key-here"' >> ~/.bashrc
source ~/.bashrc

Get an API key at console.anthropic.com/settings/keys.

Step 3 — Install OpenAI Codex / ChatGPT CLI

The official OpenAI CLI for coding (Codex) and a quick ChatGPT terminal for one-shot questions. For the second job the scripts install shell-gpt, which gives you the sgpt command:

pipx install shell-gpt
# One-shot
sgpt "explain rsync incremental backups in one paragraph"
# Shell command, ready to run
sgpt --shell "find all PNG files larger than 1MB here"

It reads the same OPENAI_API_KEY you set below.

Both platforms

# Official OpenAI Codex CLI (Node-based)
npm install -g @openai/codex

# Verify
codex --version

Authenticate by setting your OpenAI key. Get one at platform.openai.com/api-keys (requires a paid account, $5 prepaid is enough).

# Windows
[Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "sk-your-key-here", "User")
# Linux
echo 'export OPENAI_API_KEY="sk-your-key-here"' >> ~/.bashrc
source ~/.bashrc

Step 4 — Install GitHub Copilot CLI

Copilot CLI used to live inside gh as an extension. That extension is retired — the CLI is now its own package, @github/copilot, and the command is copilot. You still want gh on the box for auth and everything else it does.

Windows (PowerShell)

choco install -y gh
gh auth login          # web browser flow — select GitHub.com + browser
npm install -g @github/copilot

Linux (Bash, Ubuntu / Debian / Mint)

# Add GitHub CLI apt repo
type -p curl >/dev/null || sudo apt install -y curl
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install -y gh

gh auth login          # choose GitHub.com → web browser → paste one-time code
npm install -g @github/copilot

Linux (Bash, Fedora / RHEL)

sudo dnf install -y 'dnf-command(config-manager)'
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
sudo dnf install -y gh
gh auth login
npm install -g @github/copilot

Verify:

copilot --version
copilot                 # launch it, then run /login to authenticate

Copilot CLI requires a Copilot subscription ($10/month individual, free for students / verified OSS maintainers).

Step 5 — Install Netlify CLI

For deploying sites directly from the terminal after Claude / Codex writes them.

Both platforms

npm install -g netlify-cli
netlify --version
netlify login          # browser opens → Authorize → back to terminal

Verify auth:

netlify status

Step 6 — Install Python AI SDKs (optional but useful)

For scripting against the APIs directly (no terminal UI).

# Both platforms — use pip (Linux may need python3 -m pip)
pip install anthropic openai google-generativeai fastmcp mcp requests httpx pandas

Step 7 — Your first session

From your project directory:

cd ~/my-website        # or: cd $env:USERPROFILE\my-website  on Windows
claude

Then at the Claude prompt:

> give me a summary of this project
> add a dark-mode toggle to the header, commit, and show me the diff
> deploy a draft to Netlify so I can preview it

Claude reads your files, edits them, runs commands, and reports back. When the draft looks right:

> deploy to production

One-shot mode (any of the three CLIs)

For quick answers without entering a session:

claude "explain what package.json does in this repo"
codex "write a bash script that rotates log files older than 14 days"
sgpt "what's the difference between sync and async in Node.js?"
copilot                 # then describe the task, e.g. rebase this branch on main

Common gotchas

  • command not found after install. Close and reopen your terminal. PATH changes only take effect in new sessions.
  • Execution policy error on Windows. Set-ExecutionPolicy Bypass -Scope Process -Force then re-run.
  • Permission denied on Linux scripts. chmod +x ./script.sh then rerun.
  • Netlify deploy shows wrong files. Specify the directory explicitly: netlify deploy --dir=_site --prod.
  • claude says API key missing. Either run claude with no args (triggers browser OAuth) or echo $ANTHROPIC_API_KEY to confirm it's set.
  • OpenAI insufficient_quota error. The API tier is separate from ChatGPT Plus, prepay $5 at platform.openai.com/account/billing.
  • Copilot CLI says "no subscription." A GitHub Pro / Team / Enterprise account doesn't include Copilot, you need the $10/mo Copilot plan or be in the free student/OSS program.

Each script writes a full log to a temp file (/tmp/ai-kickstart-*/kickstart.log on Linux, $TMPDIR/ai-kickstart-*/kickstart.log on macOS, %TEMP%\ai-kickstart-*\kickstart.log on Windows), so you can read exactly what happened if something did not install the first time.

Cheat sheet — the commands you'll use daily

# Start AI sessions
claude                              # Claude Code interactive
claude "one-shot prompt"            # Quick Claude query
codex                               # OpenAI Codex interactive
sgpt "…"                            # ChatGPT one-shot answer
sgpt --shell "…"                    # Shell command, ready to run
copilot                             # GitHub Copilot coding agent

# Inside Claude Code
/help          # slash commands
/clear         # reset conversation
/mcp list      # show connected services
Esc            # exit

# Git (used constantly)
git status ; git diff ; git add . ; git commit -m "msg"

# Deploy
netlify deploy                      # preview URL
netlify deploy --prod               # live
netlify deploy --dir=_site --prod   # for SSG builds (Eleventy etc.)

Optional — connect Claude to Google Drive, Slack, GitHub via MCP

MCP (Model Context Protocol) servers let Claude read external apps from inside the terminal.

# Google Drive
claude mcp add gdrive -- npx @anthropic/gdrive-mcp

# Slack
claude mcp add slack -- npx @anthropic/slack-mcp

# GitHub (enhanced — reads issues, PRs, etc.)
claude mcp add github -- npx @anthropic/github-mcp

# Playwright (browser automation — screenshots, form-filling)
claude mcp add playwright -- npx @playwright/mcp@latest

# List / remove
claude mcp list
claude mcp remove gdrive

First run of each MCP prompts for OAuth. The tokens live locally; nothing is sent to Anthropic or OpenAI outside the normal API calls.

Why the terminal wins over the web UI

The 30-second argument: your code lives on the filesystem; your git history lives in the terminal; your builds run in the terminal; your deploys happen from the terminal. The web GUI makes you leave where the work happens, describe your problem in isolation, receive a response in isolation, then manually carry that response back to where the work happens. The terminal eliminates the round trip entirely.

On a 50-file refactor, the terminal approach takes 2 minutes. The GUI approach takes 45 minutes of copy-paste-switch-paste-switch-paste.

Side-by-side on one concrete task

Task: "Add a dark mode toggle to my website."

Web GUI (ChatGPT / Claude.ai):

  1. Open browser → claude.ai (8s)
  2. "add dark mode toggle" → AI asks for HTML → open file explorer, find index.html, copy → paste in browser
  3. AI asks for CSS → repeat for styles.css
  4. AI returns three code blocks → copy HTML → switch to editor → find insertion point → paste
  5. Copy CSS → find insertion point → paste
  6. Copy JS → create toggle.js → paste → add <script> tag to HTML
  7. Refresh browser, hit a bug, copy the error, paste back, get fix, copy, switch, paste, test again

~20 steps, ~15 window switches, ~130 seconds of pure mechanical copy-paste.

Terminal (Claude Code):

  1. cd ~/my-website && claude
  2. > add a dark mode toggle to the site
  3. Press y to approve the diff.

3 steps, 0 window switches, ~7 seconds of input. Time saved: ~2 minutes per change. Over a workday of 30 changes, that's an hour recovered.

What the terminal gives the model that the GUI can't

  • Real filesystem access, every .md, .njk, .toml, .env in your working directory, read on demand. No more copy-pasting individual files and forgetting one.
  • Command executiongit status, npm run build, netlify deploy --prod, curl health checks, run and reacted to in the same loop, not copied out and run by you.
  • Multi-file atomic edits, rename a variable across 40 files, update imports, run tests, revert if anything breaks, one session. Browser UI is one file at a time and loses context each round-trip.
  • Full git integration, commits with meaningful messages, branches, PR open, diff review, all native.
  • No token bloat from pasted context, CLI streams file contents only when the model asks. Browser burns context on every copy-paste.

Speed, memory, battery

Metric Terminal Web GUI
Startup 1–2 s 5–15 s
RAM 50–150 MB 300–800 MB per tab
Input latency Instant 50–100 ms (JS overhead)
File read <1 ms (local) Manual upload (2–30 s)
File write <1 ms (local) Copy-paste (10–30 s)
Battery hit (1 hr laptop) ~2 % ~8–15 %

Over an 8-hour day of heavy AI use: terminal ~400 MB RAM, minimal battery hit. Browser ~3–5 GB RAM across tabs, significant battery drain.

Multi-file tasks the GUI cannot do

Task Terminal Web GUI
Rename a function across 30 files 15 s 45 min
Add a header to 50 HTML pages 20 s 2 hrs
Update import paths across 100 files after a folder rename 10 s impossible
Find & fix every console.log before deploy 5 s 30 min
Add error handling to every API call in the project 30 s 1 hr
Update copyright year in every page footer 3 s 20 min

Web GUI fundamentally has no filesystem access, you'd paste files one at a time, get changes one at a time, apply them one at a time.

Security and audit

Risk Terminal Web GUI
Browser extensions reading your code / API keys Not possible Major, extensions can read all page content
Clipboard exposure Minimal (direct file writes) Every copy-paste puts code on the clipboard
Session hijacking API key in env var Session cookie in browser (XSS surface)
Data in browser cache None Conversations cached in localStorage
Audit trail Shell history (local file) Proprietary, deletable

API keys in the terminal are stored in environment variables, never displayed, OS-file-permission protected, invisible to browser extensions, and not sent to analytics or tracking services.

Where the terminal is the only option

  • Remote servers & SSH, you can't open claude.ai on a headless server. ssh prod && claude is the whole workflow. Read nginx logs, fix config, restart service, one prompt.
  • Persistent sessionstmux new -s ai-session + claude, detach, close SSH, go home. Come back next morning, tmux attach -t ai-session, Claude is still there reporting what it found.
  • CI/CDclaude --print "run the release checklist" in a GitHub Action, Vercel hook, or cron job. The browser cannot be scripted.
  • MCP connectors, Google Drive, Slack, GitHub, Playwright, databases. All terminal-only. The web GUI has zero MCP support.

When the GUI actually wins

  • You want an image or voice conversation.
  • You're on a Chromebook / iPad where you can't install tools.
  • You want to share a conversation link.
  • You're drag-dropping a PDF for one-off analysis.
  • You're walking or driving and want voice interaction (mobile app).

That's ~10–20 % of professional AI coding work. The terminal handles the other 80–90 % faster, safer, and more reliably.

How Claude Code CLI + Netlify CLI work together for easy administration

Once both are installed and authenticated, the whole admin surface of a website collapses into prompts. Here's the pattern for the most common ops:

Deploy a change end-to-end

cd ~/my-website
claude
> add a privacy policy page that covers cookies and analytics, link it
  from the footer, run the build, and deploy a draft to Netlify

Claude writes the page, updates the footer, runs npm run build (or npx @11ty/eleventy), runs netlify deploy, returns the preview URL. You check the preview in a browser, then:

> looks good, deploy to production and ping IndexNow

Add a redirect without touching config files

> add a 301 redirect from /old-page to /new-page in netlify.toml
  and verify it works after deploy with curl

Claude edits netlify.toml, deploys, runs curl -I https://site.com/old-page to check for the Location: header. One prompt, proof-of-fix included.

Set environment variables securely

> set a Netlify environment variable SENDGRID_API_KEY to the value
  I'll paste next, then redeploy

Claude uses netlify env:set SENDGRID_API_KEY ... (the value stays in the terminal session, never written to a file). Then it triggers a redeploy so the new env is picked up.

Investigate a production issue

> pull the last 3 Netlify deploy logs, summarize any errors, and
  suggest a fix for each

netlify api listSiteDeploys returns the deploy history; Claude reads the logs, spots the pattern (build failure, timeout, bad env), and proposes the patch.

Migrate or clone a site

> this folder is a working Eleventy site. Create a new Netlify site
  called "alpha-staging", link this folder to it, and do a prod deploy

Claude runs netlify sites:create --name alpha-staging, then netlify link --name alpha-staging, then netlify deploy --dir=_site --prod. Zero clicks in the Netlify dashboard.

Rollback fast

> the last deploy broke the homepage, restore the previous production
  deploy

Claude uses netlify api listSiteDeploys to find the previous successful deploy ID, then netlify api restoreSiteDeploy to roll back. ~5 seconds.

The through-line: every Netlify admin action has a CLI equivalent, and every CLI equivalent is something Claude Code can run from a prompt. Once you accept that, the Netlify dashboard becomes a read-only status page and everything mutating happens via prompt.

Cross-promote: pair the terminal with the prompt generators on this site

The terminal gives Claude real filesystem + command access. The jwatte.com tool suite gives Claude the exact thing to do. That pairing is the whole productivity story.

Write better prompts before you hand them to Claude:

  • Prompt Enhancer, wraps any prompt in research-backed patterns (ExpertPrompting, OPRO, EmotionPrompt, self-evaluation). Paste your rough instruction, pick an intensity, copy the enhanced version, hand it to claude or codex. Companion post →

Generate ready-to-paste prompts from audit data:

  • Mega Analyzer, audits one URL across SEO, schema, E-E-A-T, voice, mobile, performance, AI-search. The output ends in a multi-thousand-word AI fix prompt that names exact file paths and expected score deltas. Pipe that into claude and you go from "here's what's broken" to "here's the fix, committed and deployed" in one session.
  • Site Analyzer, same pattern, 70+ checks, different scoring buckets. Also emits a copy-paste AI fix prompt.
  • Batch Compare, up to 10 URLs compared side-by-side with a combined AI prompt covering the whole portfolio. Feed it to Claude and fix 10 sites from one session.
  • Link Graph, crawls a site, finds orphans / hubs / dead-ends / noindex pages, and emits an AI fix prompt that proposes exact internal-link additions (source page + destination + anchor text). Paste, tell Claude "apply every recommendation, commit grouped by destination page, deploy", done.

Scaffold new sites from a prompt:

  • Single Site Gen, emits a full AI site-build prompt with every best practice (schema, llms.txt, IndexNow, security headers, E-E-A-T signals, WCAG 2.2) baked in. claude < singlesitegen-prompt.txt and Claude scaffolds the whole thing in ~/new-site/.
  • Monoclone Generator, for when you're spinning up an industry site from a template. Generates the deploy prompt for the whole network.

Generate JSON-LD / schema blocks Claude can drop in directly:

  • E-E-A-T Generator, Person / Organization / sameAs / Wikidata / ORCID / rel=me JSON-LD from a single author profile.
  • Speakable Generator, SpeakableSpecification JSON-LD for voice + AI-citation.
  • FAQ Harvester, pulls every FAQ from the Google top 10, dedupes, emits ready-to-paste FAQPage JSON-LD.
  • ItemList / Carousel, Google-compliant ItemList JSON-LD from a URL list.

Audit without leaving the terminal:

  • .well-known Audit, 13-file audit of /.well-known/ with copy-paste fix kit.
  • ai.txt Generator, Spawning-style AI training opt-in/out policy across every bot in the shared registry (robots.txt companion, Netlify/Apache deploy config).

Concrete workflow that ties all of it together:

# 1. Browser: run /tools/mega-analyzer/ on your site, click "Copy AI fix prompt"
# 2. Terminal:
cd ~/my-site
claude
> [paste the Mega Analyzer fix prompt]
# Claude works through every check, edits the files, commits
> run a mobile parity check, then deploy to Netlify prod if pass
# Claude runs /tools/mobile-parity/ mentally, or you paste its URL as input

The tools produce prompts; the terminal consumes prompts. The browser is just the scratchpad where you collect the prompt.

When to reach for which tool

  • Editing a real project, multi-file changes, deploys. → Claude Code. It's the only one that reads + writes files end-to-end in your working directory.
  • Quick one-off code snippet or explanation. → Codex or sgpt one-shot mode. Faster, cheaper, no project context needed.
  • "What's the shell command for X?" → Copilot CLI. Specifically trained for the one-line shell suggestion task.
  • GUI preference / on a tablet or Chromebook.claude.ai/code (browser-based Claude Code), chatgpt.com, VS Code Copilot extension.

The terminal versions are faster for anything that involves your local repo. The GUI versions are better for pure conversation or when you can't install locally.

Related reading

Next up — the follow-on walkthrough: You've Got The Claude Code CLI Installed — Now What? picks up where this post leaves off. Plan mode, CLAUDE.md as a living contract, the slash commands that matter (/btw /fork /rewind /compact /loop /schedule /simplify /batch /powerup /insights /debug /claude-api), Skills vs Rules vs Memory, Auto Mode boundaries, and a Day 0 → Day 7 ramp plan.

Companion coverage from the methodology stack:

  • The $97 Launch, Chapter 1 (domain + hosting + deploy). Claude Code + Netlify CLI is the end-state of the workflow that chapter sets up.
  • The $20 Dollar Agency, Chapters 5-11 (SEO, schema, keywords). Every audit pattern the Site Analyzer and Mega Analyzer run is something Claude Code can implement in one prompt once the environment is live.
  • The $100 Network, Chapter 6 (the provider stack) + Chapter 26 (monitoring at scale). Where the AI terminal fits when you're running more than one site.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026