Created
June 3, 2026 12:46
-
-
Save le-dawg/973207db6b3100d344ed3e1cebff3e4c to your computer and use it in GitHub Desktop.
AIR-GEvo-Regent-UI-azure portable Claude Code + repo bootstrap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # bootstrap/bootstrap.sh | |
| # Portable, idempotent bootstrap for the AIR-GEvo-Regent-UI-azure dev setup. | |
| # | |
| # PRIMARY TARGET: a BARE cloud shell / random SSH box where NOTHING is | |
| # pre-installed -- possibly not even git, node/npm, or the `claude` CLI, and the | |
| # repo may not even be cloned yet. This script tries to get such a box from zero | |
| # to "ready for an interactive `claude` session" using only steps that work over | |
| # plain SSH. Anything that genuinely needs a human (browser OAuth, secret | |
| # values, claude.ai-hosted MCP) is collected and printed as a MANUAL STEP block | |
| # at the end -- with copy-paste commands. | |
| # | |
| # Safe to re-run: every step is guarded (command -v / file-exists / jq-merge). | |
| # | |
| # ----------------------------------------------------------------------------- | |
| # TWO WAYS TO RUN (a bare shell may have neither the repo nor this file yet): | |
| # | |
| # A) Pipe straight from the raw URL (repo NOT cloned yet -- script clones it): | |
| # curl -fsSL https://raw.githubusercontent.com/solution8-com/AIR-GEvo-Regent-UI-azure/regent-postpause-direct-PICKUP/bootstrap/bootstrap.sh | bash | |
| # | |
| # B) Clone first, then run from inside the repo: | |
| # git clone https://github.com/solution8-com/AIR-GEvo-Regent-UI-azure.git \ | |
| # && cd AIR-GEvo-Regent-UI-azure \ | |
| # && git checkout regent-postpause-direct-PICKUP \ | |
| # && bash bootstrap/bootstrap.sh | |
| # | |
| # See bootstrap/README.md for the full rationale and the manual/interactive half. | |
| # ----------------------------------------------------------------------------- | |
| # | |
| # Docs grounding (verified 2026-06): | |
| # - Install Claude Code (native installer + npm): https://code.claude.com/docs/en/setup | |
| # - Plugins/marketplaces: https://code.claude.com/docs/en/discover-plugins | |
| # - settings precedence: https://code.claude.com/docs/en/settings | |
| # - MCP / .mcp.json: https://code.claude.com/docs/en/mcp | |
| # - devcontainer/dotfiles: https://code.claude.com/docs/en/devcontainer | |
| # ============================================================================= | |
| set -u | |
| # ---- repo identity (used when we must self-clone) -------------------------- | |
| REPO_SLUG="solution8-com/AIR-GEvo-Regent-UI-azure" | |
| REPO_URL="https://github.com/${REPO_SLUG}.git" | |
| REPO_BRANCH="regent-postpause-direct-PICKUP" | |
| RAW_BASE="https://raw.githubusercontent.com/${REPO_SLUG}/${REPO_BRANCH}" | |
| CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" | |
| SETTINGS="$CLAUDE_DIR/settings.json" | |
| MANUAL=() # human-needed steps, printed at the end | |
| INTERACTIVE=() # browser/OAuth steps, printed in their own block | |
| say() { printf '\033[1;34m[bootstrap]\033[0m %s\n' "$*"; } | |
| ok() { printf '\033[0;32m ok\033[0m %s\n' "$*"; } | |
| skip() { printf '\033[2m skip %s\033[0m\n' "$*"; } | |
| warn() { printf '\033[0;33m warn\033[0m %s\n' "$*"; } | |
| manual(){ MANUAL+=("$*"); printf '\033[0;35m MANUAL\033[0m %s\n' "$*"; } | |
| inter(){ INTERACTIVE+=("$*"); printf '\033[0;36m BROWSER\033[0m %s\n' "$*"; } | |
| have() { command -v "$1" >/dev/null 2>&1; } | |
| # Detect a usable privilege-escalation prefix for apt installs (bare boxes may | |
| # run as root with no sudo, or as a user with sudo). Empty if neither needed. | |
| SUDO="" | |
| if [ "$(id -u)" -ne 0 ]; then have sudo && SUDO="sudo"; fi | |
| # Best-effort apt install of a package, only on Debian/Ubuntu-like boxes. | |
| apt_install() { | |
| local pkg="$1" | |
| if have apt-get; then | |
| say " installing '$pkg' via apt-get..." | |
| $SUDO apt-get update -qq >/dev/null 2>&1 || true | |
| if $SUDO apt-get install -y -qq "$pkg" >/dev/null 2>&1; then | |
| ok "installed $pkg"; return 0 | |
| fi | |
| fi | |
| return 1 | |
| } | |
| # ============================================================================= | |
| # STAGE A -- FULLY AUTOMATABLE over plain SSH (no browser, no secrets) | |
| # ============================================================================= | |
| say "================ STAGE A: automatable (no browser needed) ================" | |
| # ---- A0. core CLIs: curl + git -------------------------------------------- | |
| say "A0/ core tools: curl, git, jq" | |
| have curl || apt_install curl || manual "Install 'curl' (needed to fetch the Claude installer): e.g. '$SUDO apt-get install -y curl'." | |
| if ! have git; then | |
| apt_install git || manual "Install 'git' (needed to clone the repo / wire hooks): e.g. '$SUDO apt-get install -y git'." | |
| fi | |
| have jq || apt_install jq || manual "Install 'jq' (needed for safe settings merge + statusline): e.g. '$SUDO apt-get install -y jq'." | |
| # ---- A1. locate OR clone the repo ------------------------------------------ | |
| # Under `curl ... | bash` there is no script file on disk, so BASH_SOURCE is | |
| # unreliable. Strategy: if CWD (or the script's dir) is inside a git checkout of | |
| # THIS repo, use it. Otherwise clone it fresh. | |
| say "A1/ locate or clone repo ($REPO_SLUG @ $REPO_BRANCH)" | |
| in_this_repo() { | |
| # $1 = candidate dir. Echoes the repo toplevel on success. | |
| local d="$1" top | |
| top="$(git -C "$d" rev-parse --show-toplevel 2>/dev/null)" || return 1 | |
| # Confirm it's our repo by a sentinel file unique to this tree. | |
| [ -f "$top/bootstrap/settings.snippet.json" ] || [ -f "$top/azure.yaml" ] || return 1 | |
| echo "$top" | |
| } | |
| REPO_ROOT="" | |
| # Candidate 1: directory of this script (only meaningful when run as a file). | |
| SCRIPT_SRC="${BASH_SOURCE[0]:-$0}" | |
| if [ -f "$SCRIPT_SRC" ]; then | |
| cand="$(cd "$(dirname "$SCRIPT_SRC")/.." 2>/dev/null && pwd)" | |
| REPO_ROOT="$(in_this_repo "$cand" 2>/dev/null || true)" | |
| fi | |
| # Candidate 2: current working directory. | |
| [ -z "$REPO_ROOT" ] && REPO_ROOT="$(in_this_repo "$PWD" 2>/dev/null || true)" | |
| if [ -n "$REPO_ROOT" ]; then | |
| ok "running inside repo: $REPO_ROOT" | |
| else | |
| if have git; then | |
| target="${BOOTSTRAP_CLONE_DIR:-$PWD/AIR-GEvo-Regent-UI-azure}" | |
| if [ -d "$target/.git" ]; then | |
| ok "repo already present at $target" | |
| REPO_ROOT="$target" | |
| else | |
| say " cloning $REPO_URL (branch $REPO_BRANCH) into $target" | |
| if git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$target" >/dev/null 2>&1; then | |
| ok "cloned into $target"; REPO_ROOT="$target" | |
| elif git clone "$REPO_URL" "$target" >/dev/null 2>&1; then | |
| # fallback: clone default branch then checkout PICKUP if it exists | |
| git -C "$target" checkout "$REPO_BRANCH" >/dev/null 2>&1 || true | |
| ok "cloned (default branch) into $target"; REPO_ROOT="$target" | |
| else | |
| manual "Could not clone $REPO_URL (private repo or no creds?). Authenticate (e.g. 'gh auth login' or an HTTPS PAT) and run: git clone --branch $REPO_BRANCH $REPO_URL && cd AIR-GEvo-Regent-UI-azure && bash bootstrap/bootstrap.sh" | |
| fi | |
| fi | |
| else | |
| manual "git is unavailable so the repo cannot be cloned. Install git, then re-run via the curl one-liner." | |
| fi | |
| fi | |
| SCRIPT_DIR="" | |
| SNIPPET="" | |
| if [ -n "$REPO_ROOT" ] && [ -d "$REPO_ROOT/bootstrap" ]; then | |
| SCRIPT_DIR="$REPO_ROOT/bootstrap" | |
| SNIPPET="$SCRIPT_DIR/settings.snippet.json" | |
| fi | |
| say "Repo root: ${REPO_ROOT:-<unresolved>}" | |
| say "Claude config dir: $CLAUDE_DIR" | |
| # ---- A2. git hooks path (CLAUDE.md fresh-clone requirement) ----------------- | |
| say "A2/ git core.hooksPath -> .githooks" | |
| if have git && [ -n "$REPO_ROOT" ] && [ -d "$REPO_ROOT/.githooks" ]; then | |
| current="$(git -C "$REPO_ROOT" config --local core.hooksPath || true)" | |
| if [ "$current" = ".githooks" ]; then | |
| skip "core.hooksPath already .githooks" | |
| else | |
| git -C "$REPO_ROOT" config core.hooksPath .githooks && ok "set core.hooksPath=.githooks" | |
| fi | |
| chmod +x "$REPO_ROOT"/.githooks/* 2>/dev/null || true | |
| else | |
| warn "git or .githooks/ missing; skipping hook wiring" | |
| fi | |
| # ---- A3. .env scaffold from template (NEVER writes secrets) ----------------- | |
| say "A3/ .env scaffold" | |
| if [ -z "$REPO_ROOT" ]; then | |
| warn "no repo root; skipping .env scaffold" | |
| elif [ -f "$REPO_ROOT/.env" ]; then | |
| skip ".env already exists (left untouched)" | |
| elif [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/env.template" ]; then | |
| cp "$SCRIPT_DIR/env.template" "$REPO_ROOT/.env" | |
| ok "created .env from bootstrap/env.template (PLACEHOLDERS only)" | |
| manual "Edit $REPO_ROOT/.env and replace <host>/<user>/<password> with the real Postgres creds from project memory (reference_db_connection.md). The .env is gitignored." | |
| else | |
| warn "bootstrap/env.template missing; cannot scaffold .env" | |
| fi | |
| # ---- A4. statusline script (mirrors shell PS1) ----------------------------- | |
| say "A4/ statusline script -> $CLAUDE_DIR/statusline-command.sh" | |
| mkdir -p "$CLAUDE_DIR" | |
| if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/statusline-command.sh" ]; then | |
| cp "$SCRIPT_DIR/statusline-command.sh" "$CLAUDE_DIR/statusline-command.sh" | |
| chmod +x "$CLAUDE_DIR/statusline-command.sh" | |
| ok "installed statusline-command.sh" | |
| have jq || manual "Install 'jq' (the statusline script needs it: e.g. '$SUDO apt-get install -y jq')." | |
| else | |
| warn "bootstrap/statusline-command.sh not found (repo unresolved?); skipping" | |
| fi | |
| # ---- A5. merge user settings.json (statusLine + plugins + marketplace) ----- | |
| # settings precedence (docs/en/settings): managed > CLI > .claude/settings.local.json | |
| # > .claude/settings.json > ~/.claude/settings.json. enabledPlugins + | |
| # extraKnownMarketplaces are declarative and committed-friendly; we put them at | |
| # USER scope here to match the captured state. | |
| say "A5/ merge ~/.claude/settings.json (statusLine, enabledPlugins, extraKnownMarketplaces)" | |
| if [ -z "$SNIPPET" ] || [ ! -f "$SNIPPET" ]; then | |
| warn "settings.snippet.json not found (repo unresolved?); skipping settings merge" | |
| elif have jq; then | |
| # rewrite the snippet's statusLine.command to THIS user's home, so it works | |
| # regardless of which box / username we landed on. | |
| snip_tmp="$(mktemp)" | |
| jq --arg sl "bash $CLAUDE_DIR/statusline-command.sh" \ | |
| '.statusLine.command = $sl' "$SNIPPET" > "$snip_tmp" 2>/dev/null || cp "$SNIPPET" "$snip_tmp" | |
| if [ -f "$SETTINGS" ]; then | |
| cp "$SETTINGS" "$SETTINGS.bak.$(date +%s)" && ok "backed up existing settings.json" | |
| tmp="$(mktemp)" | |
| jq -s ' | |
| (.[0] // {}) as $cur | (.[1] // {}) as $snip | |
| | ($snip | del(._comment)) as $snip | |
| | $cur | |
| * { enabledPlugins: (($cur.enabledPlugins // {}) + ($snip.enabledPlugins // {})), | |
| extraKnownMarketplaces:(($cur.extraKnownMarketplaces // {}) + ($snip.extraKnownMarketplaces // {})) } | |
| * ($snip | del(.enabledPlugins) | del(.extraKnownMarketplaces)) | |
| ' "$SETTINGS" "$snip_tmp" > "$tmp" && mv "$tmp" "$SETTINGS" && ok "merged snippet into settings.json" | |
| else | |
| jq 'del(._comment)' "$snip_tmp" > "$SETTINGS" && ok "created settings.json from snippet" | |
| fi | |
| rm -f "$snip_tmp" | |
| else | |
| warn "jq not found; cannot safely merge settings.json" | |
| manual "Install jq, then re-run; OR manually merge bootstrap/settings.snippet.json into $SETTINGS (keys: statusLine, enabledPlugins, extraKnownMarketplaces)." | |
| fi | |
| # ---- A6. ensure node/npm (only needed if we fall back to the npm installer) - | |
| say "A6/ node / npm (only required for the npm install fallback)" | |
| if have node && have npm; then | |
| ok "node $(node -v 2>/dev/null) / npm $(npm -v 2>/dev/null) present" | |
| else | |
| skip "node/npm absent -- not required for the recommended native installer (A7)" | |
| fi | |
| # ---- A7. install the `claude` CLI ------------------------------------------ | |
| # Canonical install (https://code.claude.com/docs/en/setup): | |
| # Recommended (native, no Node needed): curl -fsSL https://claude.ai/install.sh | bash | |
| # -> installs to ~/.local/bin/claude and auto-updates in the background. | |
| # Fallback (npm global, needs Node 18+): npm install -g @anthropic-ai/claude-code | |
| # -> do NOT use `sudo npm install -g` (docs warn: permission/security risk). | |
| say "A7/ Claude Code CLI" | |
| if have claude; then | |
| ok "claude CLI present: $(claude --version 2>/dev/null || echo unknown)" | |
| else | |
| installed=0 | |
| if have curl; then | |
| say " installing via native installer: curl -fsSL https://claude.ai/install.sh | bash" | |
| if curl -fsSL https://claude.ai/install.sh | bash; then | |
| installed=1 | |
| ok "native installer finished" | |
| else | |
| warn "native installer failed; will try npm fallback" | |
| fi | |
| fi | |
| if [ "$installed" -ne 1 ] && have npm; then | |
| # Node 18+ is required; warn (but still try) on older node. | |
| nodemajor="$(node -v 2>/dev/null | sed 's/^v//; s/\..*//')" | |
| if [ -n "$nodemajor" ] && [ "$nodemajor" -lt 18 ]; then | |
| warn "node $(node -v) is < 18; the npm package needs Node 18+. Upgrade Node (e.g. 'nvm install --lts') then re-run." | |
| fi | |
| say " installing via npm: npm install -g @anthropic-ai/claude-code" | |
| if npm install -g @anthropic-ai/claude-code >/dev/null 2>&1; then | |
| installed=1; ok "npm global install finished" | |
| else | |
| warn "npm global install failed (permissions? try a user-writable npm prefix; do NOT use sudo npm -g)" | |
| fi | |
| fi | |
| # ~/.local/bin is where the native installer drops the binary; add to PATH for | |
| # the remainder of this script and tell the user how to persist it. | |
| case ":$PATH:" in | |
| *":$HOME/.local/bin:"*) : ;; | |
| *) export PATH="$HOME/.local/bin:$PATH" ;; | |
| esac | |
| if have claude; then | |
| ok "claude now on PATH: $(claude --version 2>/dev/null || echo unknown)" | |
| manual "Persist PATH for future shells: add 'export PATH=\"\$HOME/.local/bin:\$PATH\"' to ~/.bashrc (the native installer drops the binary there)." | |
| else | |
| manual "Could not install the 'claude' CLI automatically. Run one of (from https://code.claude.com/docs/en/setup): | |
| Native (recommended, no Node): curl -fsSL https://claude.ai/install.sh | bash | |
| npm (needs Node 18+): npm install -g @anthropic-ai/claude-code | |
| then ensure \$HOME/.local/bin is on PATH and re-run this script." | |
| fi | |
| fi | |
| # ---- A8. plugins + marketplaces via the non-interactive claude CLI ---------- | |
| # Authoritative CLI (docs/en/discover-plugins): | |
| # claude plugin marketplace add <owner/repo|git-url|path> | |
| # claude plugin install <name>@<marketplace> [--scope user|project|local] | |
| # Reconciles with the declarative settings from A5 (idempotent). | |
| say "A8/ plugins & marketplaces (claude plugin CLI)" | |
| if have claude; then | |
| if claude plugin marketplace list 2>/dev/null | grep -qi 'trailofbits'; then | |
| skip "marketplace 'trailofbits' already registered" | |
| else | |
| claude plugin marketplace add trailofbits/skills >/dev/null 2>&1 \ | |
| && ok "added marketplace trailofbits/skills" \ | |
| || warn "could not add trailofbits/skills non-interactively (add later: claude plugin marketplace add trailofbits/skills)" | |
| fi | |
| OFFICIAL_PLUGINS="superpowers context7 typescript-lsp security-guidance pyright-lsp microsoft-docs atomic-agents azure pydantic-ai" | |
| for p in $OFFICIAL_PLUGINS; do | |
| claude plugin install "${p}@claude-plugins-official" --scope user >/dev/null 2>&1 \ | |
| && ok "installed ${p}@claude-plugins-official" \ | |
| || warn "install ${p}@claude-plugins-official failed (already installed or needs interactive trust)" | |
| done | |
| claude plugin install "gh-cli@trailofbits" --scope user >/dev/null 2>&1 \ | |
| && ok "installed gh-cli@trailofbits" \ | |
| || warn "install gh-cli@trailofbits failed (already installed or needs the marketplace)" | |
| else | |
| warn "claude CLI not on PATH; skipping plugin install" | |
| manual "After installing the claude CLI (A7), re-run this script to install plugins. The declarative ~/.claude/settings.json was still written, so a future 'claude' will offer to install enabledPlugins on folder-trust." | |
| fi | |
| # ============================================================================= | |
| # STAGE B -- NEEDS A BROWSER / INTERACTIVE AUTH (cannot run over plain SSH) | |
| # These are collected and reprinted at the very end with copy-paste commands. | |
| # ============================================================================= | |
| say "================ STAGE B: interactive (browser / OAuth) ==================" | |
| # ---- B1. claude login ------------------------------------------------------- | |
| say "B1/ claude login (OAuth)" | |
| LOGGED_IN=0 | |
| if have claude; then | |
| # No fully reliable headless probe; treat presence of credentials dir as hint. | |
| if [ -f "$HOME/.claude.json" ] || [ -d "$HOME/.claude/credentials" ]; then | |
| ok "existing claude credentials detected (login may already be done)" | |
| LOGGED_IN=1 | |
| fi | |
| fi | |
| if [ "$LOGGED_IN" -ne 1 ]; then | |
| inter "Log in to Claude Code. On a box WITH a browser: run 'claude' and follow the prompts. On a HEADLESS SSH box: run 'claude' -> it prints a URL; open it on your laptop, approve, paste the code back. (Pro/Max/Team/Enterprise/Console account required -- free claude.ai does NOT include Claude Code. Or use ANTHROPIC_API_KEY / Bedrock / Vertex / Foundry for non-interactive auth -- but claude.ai-hosted MCP servers then will NOT load.) Docs: https://code.claude.com/docs/en/authentication" | |
| fi | |
| # ---- B2. activate plugins in a live session -------------------------------- | |
| say "B2/ activate plugins" | |
| if have claude; then | |
| inter "Open an interactive 'claude' once and run '/reload-plugins', then '/plugin' to confirm (~10 plugins + the trailofbits marketplace). Plugin installation (A8) is scriptable; activation in a running session is not." | |
| fi | |
| # ---- B3. trailofbits security skills (CLAUDE.md pre-implementation gate) ---- | |
| say "B3/ trailofbits security skills (pre-implementation gate)" | |
| inter "CLAUDE.md requires these BEFORE any implementation work. In an interactive 'claude': | |
| /plugin marketplace add trailofbits/skills | |
| /plugin install openai-security-threat-model@trailofbits | |
| /plugin install openai-security-best-practices@trailofbits | |
| NOTE: CLAUDE.md spells the marketplace 'trailofbits/skills-curated' but the source registered this session is 'trailofbits/skills'. If install fails, try the '-curated' repo and confirm exact plugin slugs in the '/plugin' Discover tab." | |
| # ---- B4. claude.ai-hosted MCP servers -------------------------------------- | |
| say "B4/ claude.ai-hosted MCP servers" | |
| inter "claude.ai-hosted MCP servers (Context7, Excalidraw, Microsoft 365, Miro, Gamma, ...) are OAuth/session state and are NOT reproducible headlessly. Log in with your claude.ai account (B1) and they reappear automatically (docs/en/mcp -> 'Use MCP servers from Claude.ai'). Plugin-provided MCP servers (e.g. microsoft-docs) return automatically once their plugin is installed (A8). For a committed, headless-capable server, add a project-scoped .mcp.json (see bootstrap/README.md) -- none is required for this repo today." | |
| # ============================================================================= | |
| echo | |
| say "DONE. Stage A (automated) complete." | |
| if [ "${#MANUAL[@]}" -gt 0 ]; then | |
| printf '\033[1;35m=== MANUAL STEPS (no browser, but need you) (%d) ===\033[0m\n' "${#MANUAL[@]}" | |
| i=1; for m in "${MANUAL[@]}"; do printf '\033[0;35m%2d.\033[0m %s\n\n' "$i" "$m"; i=$((i+1)); done | |
| fi | |
| if [ "${#INTERACTIVE[@]}" -gt 0 ]; then | |
| printf '\033[1;36m=== INTERACTIVE STEPS (need a browser / OAuth) (%d) ===\033[0m\n' "${#INTERACTIVE[@]}" | |
| i=1; for m in "${INTERACTIVE[@]}"; do printf '\033[0;36m%2d.\033[0m %s\n\n' "$i" "$m"; i=$((i+1)); done | |
| fi | |
| if [ -n "$REPO_ROOT" ]; then | |
| say "Next: cd $REPO_ROOT && claude" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment