Skip to content

Instantly share code, notes, and snippets.

@le-dawg
Created June 3, 2026 12:46
Show Gist options
  • Select an option

  • Save le-dawg/973207db6b3100d344ed3e1cebff3e4c to your computer and use it in GitHub Desktop.

Select an option

Save le-dawg/973207db6b3100d344ed3e1cebff3e4c to your computer and use it in GitHub Desktop.
AIR-GEvo-Regent-UI-azure portable Claude Code + repo bootstrap
#!/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