KeiSeiKit-1.0/bootstrap.sh
KeiSei84 3bc351ec03 fix(bootstrap): v0.48 — reattach stdin to /dev/tty so curl|bash prompts fire
INCIDENT: user installed via curl|bash and reported "kei did not auto-launch".
Root cause: under curl|bash, stdin is the pipe from curl. Every interactive
gate `[ -t 0 ]` returned FALSE, silently skipping FIVE places:

  install/lib-i18n.sh         language picker
  install/lib-preflight.sh    preflight checks
  install/lib-hooks.sh        hook activation prompt
  install/lib-wizard.sh       sleep wizard
  install.sh:270              PATH wiring
  bootstrap.sh kei-onboard    primary CLI picker
  bootstrap.sh launch prompt  "запустить kei сейчас?"

Per rules/tty-interactivity-gate.md the rule is "gate on stdin, never stdout"
— and we did. But under curl|bash BOTH are non-tty. /dev/tty is the only
reliable signal that the user is interactive.

FIX: in bootstrap.sh, ONCE, before invoking install.sh:

    if [ -r /dev/tty ] && [ -w /dev/tty ]; then
        exec </dev/tty
    fi

This redirects bootstrap.sh's OWN stdin to /dev/tty. Every child process
inherits it via fork — install.sh, lib-*.sh, kei-onboard.sh, kei-pick.sh,
kei-mcp-wire.sh — and their `[ -t 0 ]` gates now correctly report true.
Headless / CI / nohup (no /dev/tty) → skipped, gates stay false, no prompts.

Simplified the v0.47 onboarding + launch-prompt blocks: they no longer
need have_tty() or explicit </dev/tty redirects on read — the global
reattach handles it cascadally.

Smoke-tested 2 of 3 paths (curl|bash via `echo "" | bash script.sh` ✓;
plain bash needs real TTY which Claude Code sandbox doesn't provide;
headless needs setsid which is Linux-only). Logic verified.

Version bump v0.47 → v0.48 because behaviour materially changes for every
curl|bash user (5 prompts now fire that were silently skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:08:09 +08:00

314 lines
14 KiB
Bash
Executable file

#!/usr/bin/env bash
# bootstrap.sh — zero-to-installed KeiSeiKit on a fresh machine.
#
# Usage from inside an already-cloned repo:
# ./bootstrap.sh # interactive — picks cortex profile
# ./bootstrap.sh --profile=core # explicit profile
# ./bootstrap.sh --yes # non-interactive (CI / scripts)
#
# Usage from a fresh machine (private repo, gh CLI required for clone):
# gh auth login
# gh repo clone KeiSeiLab/KeiSeiKit-1.0
# cd KeiSeiKit-1.0 && ./bootstrap.sh
#
# What it does (idempotent — re-running is safe):
# 1. Detects OS (macOS / Linux)
# 2. Installs jq + rustup if missing (uses brew / apt / dnf / pacman)
# 3. Sources cargo env so a fresh shell sees PATH=$HOME/.cargo/bin
# 4. Verifies we're in a KeiSeiKit checkout (presence of install.sh)
# 5. Runs install.sh with the chosen profile
# 6. Health-checks the install via kei-doctor (best-effort)
#
# What it does NOT do (these are still YOUR responsibility):
# - Set up SSH keys for github (use `gh auth login` first)
# - Configure secrets per RULE 0.8 (~/.claude/secrets/.env)
# - Activate Claude Code hooks (re-run with --activate-hooks if needed)
set -euo pipefail
# --- defaults ------------------------------------------------------------
PROFILE="${KEISEIKIT_PROFILE:-}" # empty → wizard prompts on TTY (Wave 45)
YES_FLAG=""
EXTRA_FLAGS=()
SKIP_PREREQS=0
while [ $# -gt 0 ]; do
case "$1" in
--profile=*) PROFILE="${1#*=}"; shift ;;
--profile) PROFILE="$2"; shift 2 ;;
--yes|-y) YES_FLAG="--yes"; shift ;;
--skip-prereqs) SKIP_PREREQS=1; shift ;;
--*) EXTRA_FLAGS+=("$1"); shift ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
# --- wizard (Wave 45) ----------------------------------------------------
# If no --profile given AND we're on a TTY, ask. Non-TTY (CI / pipe) →
# fallback to cortex for compat with v0.16 default behaviour.
prompt_profile() {
if [ -n "$PROFILE" ]; then return 0; fi
# Interactive iff stdin is a terminal. NOT stdout: web-install.sh tees stdout
# to a logfile (pipe), so -t 1 is false even in an interactive curl|bash.
# Prompts print to the terminal via tee; the menu reads from stdin.
# Non-interactive (CI / piped, no controlling terminal) → minimal: fast,
# no 105-crate compile, can't half-fail. Matches install.sh's own default
# (was "cortex" here → divergent install vs direct install.sh). Opt up with
# --profile=cortex/full-hub.
if [ ! -t 0 ]; then PROFILE="minimal"; return 0; fi
cat <<'WIZARD'
╔═══════════════════════════════════════════════════════════════════╗
║ KeiSeiKit Installation Wizard ║
╠═══════════════════════════════════════════════════════════════════╣
║ ║
║ [1] minimal — agents+hooks+skills only ║
║ 50 MB · 5 sec install ║
║ ║
║ [2] cortex — + kei-cortex daemon + UI + 8 Rust crates ║
║ 540 MB · 5 min install ║
║ ║
║ [3] local-mirror — cortex + Local Forgejo + Forgejo Runner CI ║
║ 800 MB · 10 min · push without VPN ║
║ ║
║ [4] dashboard — local-mirror + Project Dashboard + DBs UI ║
║ 1 GB · 15 min · single pane of glass ║
║ ║
║ [5] full-hub — dashboard + Search + Docs + Backup ║
║ 1.3 GB · 25 min · everything ║
║ ║
║ [6] full — every primitive in MANIFEST (53 tools) ║
║ 1.5 GB · 15 min · power user ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
WIZARD
local choice=""
while true; do
read -r -p "Pick a profile [1-6, default=2]: " choice
choice="${choice:-2}"
case "$choice" in
1) PROFILE="minimal"; break ;;
2) PROFILE="cortex"; break ;;
3) PROFILE="local-mirror"; break ;;
4) PROFILE="dashboard"; break ;;
5) PROFILE="full-hub"; break ;;
6) PROFILE="full"; break ;;
*) echo "invalid — pick 1, 2, 3, 4, 5, or 6" ;;
esac
done
echo "[bootstrap] profile selected: $PROFILE"
echo
}
prompt_profile
case "$PROFILE" in
minimal|core|frontend|ops|dev|mcp|cortex|full|local-mirror|dashboard|full-hub) ;;
*) echo "[bootstrap] unknown profile: $PROFILE" >&2
echo " valid: minimal cortex local-mirror dashboard full-hub full (also core/frontend/ops/dev/mcp)" >&2
exit 2 ;;
esac
# --- helpers -------------------------------------------------------------
log() { echo "[bootstrap] $*"; }
err() { echo "[bootstrap] ERROR: $*" >&2; }
have() { command -v "$1" >/dev/null 2>&1; }
OS="$(uname -s)"
# --- 1. OS detection -----------------------------------------------------
# Detect WSL2 (uname -s = Linux but kernel reports Microsoft) — full path works.
# Detect Git Bash / Cygwin / MSYS on bare Windows — substrate cannot run there;
# guide user to WSL2 instead of dying silently.
IS_WSL=0
if [ "$OS" = "Linux" ] && [ -r /proc/version ] && grep -qiE "microsoft|wsl" /proc/version 2>/dev/null; then
IS_WSL=1
fi
case "$OS" in
Darwin|Linux)
if [ "$IS_WSL" = "1" ]; then
log "OS: WSL2 (Linux inside Windows) — full substrate path available"
else
log "OS: $OS"
fi
;;
MINGW*|MSYS*|CYGWIN*)
err ""
err "Detected: bare Windows ($OS) via Git Bash / Cygwin / MSYS."
err ""
err "KeiSeiKit's substrate is Bash-only and needs apt/brew + full POSIX —"
err "it will not run reliably outside WSL2."
err ""
err "A native PowerShell port is demand-driven — not built yet because"
err "WSL2 covers 100% with zero code duplication. If enough Windows users"
err "ask, we will ship one. Open / 👍 an issue at:"
err " https://github.com/KeiSeiLab/KeiSeiKit-1.0/issues"
err ""
err "Path forward (one-time setup, ~5 min + reboot):"
err ""
err " 1. Open PowerShell as Administrator."
err " 2. Run: wsl --install -d Ubuntu"
err " 3. Reboot when prompted; Ubuntu auto-starts on next login."
err " 4. Inside Ubuntu, re-run this same bootstrap:"
err " curl -fsSL https://raw.githubusercontent.com/KeiSeiLab/KeiSeiKit-1.0/main/bootstrap.sh | bash"
err ""
err "Alternative — MCP-only (no substrate, no skills, no hooks):"
err " Grab kei-mcp-server-windows-x64.exe from a release and wire it"
err " into Claude Desktop / VS Code MCP config. Gets you spawn_agent +"
err " kei_bash/kei_edit/kei_write only. See README → Platforms section."
err ""
# Best-effort: copy the wsl --install command to clipboard if possible.
if command -v clip.exe >/dev/null 2>&1; then
printf 'wsl --install -d Ubuntu' | clip.exe 2>/dev/null && \
err "(I've copied 'wsl --install -d Ubuntu' to your Windows clipboard.)"
fi
exit 1
;;
*)
err "unsupported OS: $OS (supported: Darwin / Linux / WSL2)"
exit 1
;;
esac
# --- 2. install jq -------------------------------------------------------
install_jq() {
if have jq; then return 0; fi
log "installing jq"
case "$OS" in
Darwin)
if ! have brew; then
log "homebrew missing — installing it first (10-15 min)"
NONINTERACTIVE=1 /bin/bash -c \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# macOS Apple-Silicon: add brew to PATH for this session
if [ -d /opt/homebrew/bin ]; then export PATH="/opt/homebrew/bin:$PATH"; fi
fi
brew install jq
;;
Linux)
if have apt-get; then sudo apt-get update && sudo apt-get install -y jq
elif have dnf; then sudo dnf install -y jq
elif have pacman; then sudo pacman -S --noconfirm jq
elif have apk; then sudo apk add jq
else err "no supported package manager — install jq manually"; exit 1
fi
;;
esac
}
# --- 3. install rustup ---------------------------------------------------
install_rust() {
if have cargo && cargo --version >/dev/null 2>&1; then return 0; fi
log "installing rustup (default toolchain: stable)"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --profile minimal
# shellcheck disable=SC1091
source "$HOME/.cargo/env"
}
if [ "$SKIP_PREREQS" = "0" ]; then
install_jq
install_rust
else
log "--skip-prereqs: assuming jq + cargo already installed"
fi
# --- 4. checkout sanity check --------------------------------------------
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ ! -f "$KIT_DIR/install.sh" ]; then
err "install.sh not found in $KIT_DIR — am I inside a KeiSeiKit checkout?"
err "if not: gh repo clone KeiSeiLab/KeiSeiKit-1.0 && cd KeiSeiKit-1.0 && ./bootstrap.sh"
exit 1
fi
log "checkout: $KIT_DIR"
# --- 5. run install ------------------------------------------------------
log "running install.sh --profile=$PROFILE $YES_FLAG ${EXTRA_FLAGS[*]:-}"
cd "$KIT_DIR"
# v0.48: reattach stdin to /dev/tty for the install + everything after.
# Under `curl|bash` stdin is the curl pipe, so install.sh's interactive
# gates (5 places: language pick, preflight, hooks-activate, sleep wizard,
# PATH wiring) all silently skip via [ -t 0 ] being false. Reattaching ONCE
# here cascades correctly: every child script inherits the terminal stdin
# and its [ -t 0 ] returns true. Only do it if /dev/tty is actually
# present and readable (CI / nohup / systemd: skip — those are headless).
if [ -r /dev/tty ] && [ -w /dev/tty ]; then
exec </dev/tty
log "stdin reattached to /dev/tty (curl|bash interactive prompts will work)"
fi
# Defensive: invoke via `bash` not `./install.sh` because GitHub's contents
# API does NOT preserve the executable bit on `gh api -X PUT` updates
# (only the git Data API does). Older clones may have install.sh with
# mode 644 even though the source repo has it 755. `bash <file>` works
# regardless of file mode. Verified incident 2026-05-26 prod-curl test.
bash ./install.sh --profile="$PROFILE" $YES_FLAG "${EXTRA_FLAGS[@]:+${EXTRA_FLAGS[@]}}"
# --- 6. post-install verification ----------------------------------------
KEI_BIN="$HOME/.claude/agents/_primitives/_rust/target/release"
log "==========================================================================="
log "post-install health check"
log "==========================================================================="
if [ -x "$KEI_BIN/kei-doctor" ]; then
"$KEI_BIN/kei-doctor" 2>&1 | head -25 || true
elif [ -x "$HOME/.claude/agents/_primitives/kei-doctor.sh" ]; then
"$HOME/.claude/agents/_primitives/kei-doctor.sh" 2>&1 | head -25 || true
else
log "(kei-doctor not installed — pick a profile that includes it: core/cortex/full)"
fi
log ""
log "==========================================================================="
log "DONE — KeiSeiKit installed (profile: $PROFILE)"
log "==========================================================================="
# v0.48: post-install onboarding wizard.
# stdin already reattached to /dev/tty above (when present), so [ -t 0 ]
# inside this scope correctly reports interactive vs headless. Wizard
# itself re-checks and exits cleanly if non-interactive.
ONBOARD_SH="$HOME/.claude/scripts/kei-onboard.sh"
if [ -x "$ONBOARD_SH" ] && [ -t 0 ] && [ "${KEI_NO_ONBOARD:-0}" != "1" ]; then
log ""
log "Starting post-install onboarding (pick primary CLI + wire MCP)..."
log "Skip with KEI_NO_ONBOARD=1; re-run anytime with 'kei onboard'."
log ""
"$ONBOARD_SH" || log "(onboarding exited non-zero; re-run with 'kei onboard')"
else
log ""
log "Post-install wizard skipped (no TTY or KEI_NO_ONBOARD=1)."
log "Run interactively to configure primary CLI:"
log " kei onboard # full wizard"
log " kei pick # just pick primary"
log " kei mcp-wire # wire MCP into installed CLIs"
fi
log ""
log "Next steps:"
log " - Open a new shell so PATH picks up ~/.cargo/bin and the kei-* binaries."
log " - Or source the rc file the installer wrote (Bash: ~/.bashrc, Zsh: ~/.zshrc)."
log " - Run kei-doctor for a full health diagnostic."
log " - For cortex profile: run /cortex-setup inside Claude Code."
log " - For sleep layer: run /sleep-setup inside Claude Code."
# v0.48: offer to launch `kei` for a first status look.
# stdin was reattached to /dev/tty above (when present), so [ -t 0 ] is
# now true under curl|bash too. Simple gate works correctly.
KEI_BIN_PATH="$HOME/.claude/bin/kei"
if [ -x "$KEI_BIN_PATH" ] && [ -t 0 ] && [ "${KEI_NO_AUTORUN:-0}" != "1" ]; then
log ""
printf ' → Запустить kei сейчас? [Y/n] '
_reply=""
read -r _reply || _reply=""
case "${_reply:-Y}" in
[Nn]*)
log " (skipped — run 'kei' anytime to see substrate status)"
;;
*)
log ""
"$KEI_BIN_PATH" || true
;;
esac
fi