#!/usr/bin/env bash set -euo pipefail STATE_HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" GITEA_CONF="${GITEA_CONF:-$HOME/.railiance_gitea.conf}" GITEA_URL="${GITEA_URL:-http://92.205.130.254:32166}" GITEA_USER="${GITEA_USER:-}" GITEA_TOKEN="${GITEA_TOKEN:-}" GIT_HELPER="${GIT_HELPER:-auto}" INSTALL_MISSING=0 NON_INTERACTIVE=0 DRY_RUN=0 AUTHORIZE_SSH=0 ALLOW_PLAINTEXT_STORE=0 SKIP_GITEA=0 SKIP_MCP=0 SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}" SSH_TARGETS=( "tegwick@92.205.62.239" "tegwick@92.205.130.254" ) usage() { cat <<'USAGE' Usage: scripts/bootstrap-env.sh [options] Idempotently prepares a State Hub operator or collaborator environment. Options: --install-missing Install missing apt packages when possible. --non-interactive Do not prompt; warn instead of asking for secrets. --dry-run Show intended actions without changing local config. --git-helper MODE auto, libsecret, cache, or store. Default: auto. --allow-plaintext-store Allow git credential.helper=store in auto mode. --authorize-ssh Run ssh-copy-id for configured SSH targets. --ssh-target USER@HOST Add an SSH authorization target. May repeat. --gitea-url URL Gitea base URL for ~/.railiance_gitea.conf. --gitea-user USER Gitea user for ~/.railiance_gitea.conf. --gitea-token TOKEN Gitea token; otherwise prompted when interactive. --skip-gitea Do not create or update ~/.railiance_gitea.conf. --skip-mcp Do not run make register-mcp. -h, --help Show this help. USAGE } ok() { printf '[OK] %s\n' "$*"; } warn() { printf '[WARN] %s\n' "$*"; } err() { printf '[ERR] %s\n' "$*" >&2; } step() { printf '\n==> %s\n' "$*"; } run() { if [ "$DRY_RUN" -eq 1 ]; then printf 'DRY-RUN: %s\n' "$*" else "$@" fi } need_arg() { if [ -z "${2:-}" ]; then err "$1 requires a value" exit 2 fi } while [ "$#" -gt 0 ]; do case "$1" in --install-missing) INSTALL_MISSING=1 shift ;; --non-interactive) NON_INTERACTIVE=1 shift ;; --dry-run) DRY_RUN=1 shift ;; --git-helper) need_arg "$1" "${2:-}" GIT_HELPER="$2" shift 2 ;; --allow-plaintext-store) ALLOW_PLAINTEXT_STORE=1 shift ;; --authorize-ssh) AUTHORIZE_SSH=1 shift ;; --ssh-target) need_arg "$1" "${2:-}" SSH_TARGETS+=("$2") shift 2 ;; --gitea-url) need_arg "$1" "${2:-}" GITEA_URL="$2" shift 2 ;; --gitea-user) need_arg "$1" "${2:-}" GITEA_USER="$2" shift 2 ;; --gitea-token) need_arg "$1" "${2:-}" GITEA_TOKEN="$2" shift 2 ;; --skip-gitea) SKIP_GITEA=1 shift ;; --skip-mcp) SKIP_MCP=1 shift ;; -h|--help) usage exit 0 ;; *) err "unknown argument: $1" usage >&2 exit 2 ;; esac done case "$GIT_HELPER" in auto|libsecret|cache|store) ;; *) err "--git-helper must be auto, libsecret, cache, or store" exit 2 ;; esac apt_install() { local packages=("$@") if [ "$INSTALL_MISSING" -ne 1 ]; then warn "Missing packages: ${packages[*]}" warn "Rerun with --install-missing or install them manually." return fi if ! command -v sudo >/dev/null 2>&1; then warn "sudo is not available; cannot install: ${packages[*]}" return fi run sudo apt-get update run sudo apt-get install -y "${packages[@]}" } check_commands() { step "Checking prerequisites" local missing=() local commands=(git curl ssh-keygen ssh-copy-id python3 make) local optional=(sops age helm kubectl uv claude) for cmd in "${commands[@]}"; do if command -v "$cmd" >/dev/null 2>&1; then ok "$cmd found" else missing+=("$cmd") warn "$cmd missing" fi done for cmd in "${optional[@]}"; do if command -v "$cmd" >/dev/null 2>&1; then ok "$cmd found" else warn "$cmd missing" fi done if [ "${#missing[@]}" -gt 0 ]; then apt_install "${missing[@]}" fi } libsecret_helper_path() { local candidates=( "/usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret" "/usr/lib/git-core/git-credential-libsecret" "/usr/libexec/git-core/git-credential-libsecret" ) local candidate for candidate in "${candidates[@]}"; do if [ -x "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done return 1 } build_libsecret_helper() { local source_dir="/usr/share/doc/git/contrib/credential/libsecret" if [ ! -d "$source_dir" ]; then apt_install libsecret-1-0 libsecret-1-dev make gcc fi if [ -d "$source_dir" ]; then run sudo make -C "$source_dir" fi } configure_git_helper() { step "Configuring Git credential helper" local current current="$(git config --global --get credential.helper || true)" if [ -n "$current" ]; then ok "credential.helper already set: $current" return fi local helper="$GIT_HELPER" if [ "$helper" = "auto" ]; then if libsecret_helper_path >/dev/null 2>&1; then helper="libsecret" elif [ "$ALLOW_PLAINTEXT_STORE" -eq 1 ]; then helper="store" else helper="cache" fi fi case "$helper" in libsecret) local path path="$(libsecret_helper_path || true)" if [ -z "$path" ]; then build_libsecret_helper path="$(libsecret_helper_path || true)" fi if [ -z "$path" ]; then warn "libsecret helper is not available; using cache helper for this machine." run git config --global credential.helper "cache --timeout=3600" else run git config --global credential.helper "$path" fi ;; cache) run git config --global credential.helper "cache --timeout=3600" ;; store) if [ "$ALLOW_PLAINTEXT_STORE" -ne 1 ]; then err "credential.helper=store writes plaintext credentials." err "Rerun with --allow-plaintext-store if that is intended for this host." exit 1 fi run git config --global credential.helper store ;; esac ok "credential.helper configured" } setup_ssh_key() { step "Checking SSH key" mkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" if [ -f "$SSH_KEY" ]; then ok "SSH key exists: $SSH_KEY" else run ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -C "$USER@$(hostname)-state-hub" ok "SSH key generated: $SSH_KEY" fi if [ -f "${SSH_KEY}.pub" ]; then printf '\nPublic key to authorize on managed hosts:\n\n' sed 's/^/ /' "${SSH_KEY}.pub" printf '\n' fi if [ "$AUTHORIZE_SSH" -eq 1 ]; then local target for target in "${SSH_TARGETS[@]}"; do run ssh-copy-id -i "${SSH_KEY}.pub" "$target" done else warn "SSH authorization not attempted. Use --authorize-ssh after confirming host access." fi } write_gitea_conf() { step "Checking Gitea config" if [ "$SKIP_GITEA" -eq 1 ]; then warn "Skipping Gitea config by request." return fi if [ -f "$GITEA_CONF" ]; then chmod 600 "$GITEA_CONF" ok "$GITEA_CONF already exists" return fi if [ -z "$GITEA_USER" ] && [ "$NON_INTERACTIVE" -eq 0 ]; then read -r -p "Gitea username: " GITEA_USER fi if [ -z "$GITEA_TOKEN" ] && [ "$NON_INTERACTIVE" -eq 0 ]; then read -r -s -p "Gitea token (requires read:user and repository write scopes): " GITEA_TOKEN printf '\n' fi if [ -z "$GITEA_USER" ] || [ -z "$GITEA_TOKEN" ]; then warn "Gitea config not written. Set GITEA_USER/GITEA_TOKEN or rerun interactively." return fi if [ "$DRY_RUN" -eq 1 ]; then printf 'DRY-RUN: would write %s with GITEA_URL and GITEA_USER; token hidden\n' "$GITEA_CONF" return fi umask 077 { printf 'GITEA_URL="%s"\n' "$GITEA_URL" printf 'GITEA_USER="%s"\n' "$GITEA_USER" printf 'GITEA_TOKEN="%s"\n' "$GITEA_TOKEN" } >"$GITEA_CONF" chmod 600 "$GITEA_CONF" ok "Wrote $GITEA_CONF" } register_mcp() { step "Registering State Hub MCP" if [ "$SKIP_MCP" -eq 1 ]; then warn "Skipping MCP registration by request." return fi if [ "$DRY_RUN" -eq 1 ]; then run make -C "$STATE_HUB_DIR" register-mcp DRY_RUN=1 else make -C "$STATE_HUB_DIR" register-mcp fi } health_check() { step "Checking State Hub reachability" if curl -fsS --max-time 2 "http://127.0.0.1:8000/state/health" >/dev/null 2>&1; then ok "State Hub API reachable at http://127.0.0.1:8000" elif curl -fsS --max-time 2 "http://127.0.0.1:18000/state/health" >/dev/null 2>&1; then ok "State Hub API reachable through tunnel at http://127.0.0.1:18000" else warn "State Hub API is not reachable locally or through the default tunnel." warn "Start it with 'make api' or run 'make bridges' if this machine uses ops-bridge." fi } main() { step "State Hub environment bootstrap" printf 'Repository: %s\n' "$STATE_HUB_DIR" check_commands configure_git_helper setup_ssh_key write_gitea_conf register_mcp health_check ok "Bootstrap checks complete." } main "$@"