# shellcheck shell=bash # lib-dev-hub-mdbook.sh — install mdBook (Rust static-site doc-hub) via cargo + launchd. # # Renders every ~/Projects//{CLAUDE,DECISIONS,RUNBOOK,DESIGN}.md as a # single browsable web book served on http://127.0.0.1:7080/. Two launchd # agents: `mdbook-server` (long-lived, --no-rebuild) and `mdbook-rebuilder` # (timer, regenerates SUMMARY.md + chapters + `mdbook build` every 3600s). # # The rebuild logic is owned by ${KIT}/dev-hub/mdbook-rebuild.sh — emitted by # write_rebuild_wrapper. regenerate_summary is a thin wrapper that invokes it # synchronously at install time so the user sees the first render immediately. # # Sources: lib-log.sh (say/warn/err), lib-launchd.sh (install_service/unload_plist). # Globals read: $KIT_DIR, $HOME_DIR. # ---------- helpers (private) ---------- # Verify cargo is on PATH (rustup default stable). 0 OK, 1 missing. _mdbook_check_cargo() { if ! command -v cargo >/dev/null 2>&1; then err "cargo not found — install via: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" return 1 fi say " → cargo $(cargo --version 2>/dev/null) OK" } # Idempotent `cargo install mdbook`. Cargo no-ops if already at latest crate. _mdbook_install_binary() { say " → cargo install mdbook (no-op if already at latest)" cargo install mdbook 2>&1 | grep -vE "^(\s*Compiling|\s*Finished|\s*Updating)" || true } # Render install/launchd-templates/book.toml.tmpl → ${DATA}/book.toml. # Args: . _mdbook_render_book_toml() { local data_dir="$1" local tmpl="$KIT_DIR/install/launchd-templates/book.toml.tmpl" if [ ! -f "$tmpl" ]; then err "book.toml.tmpl missing: $tmpl" return 1 fi mkdir -p "$data_dir" sed \ -e "s|\${HOME}|${HOME_DIR}|g" \ -e "s|\${USER}|${USER}|g" \ -e "s|\${DATA}|${data_dir}|g" \ "$tmpl" > "$data_dir/book.toml" say " → wrote $data_dir/book.toml" } # Emit ${KIT}/dev-hub/mdbook-serve.sh — wraps `mdbook serve` with binary discovery. write_serve_wrapper() { local wrapper="$HOME_DIR/.claude/agents/_primitives/dev-hub/mdbook-serve.sh" mkdir -p "$(dirname "$wrapper")" cat > "$wrapper" <<'WRAPPER_EOF' #!/usr/bin/env bash # Auto-generated by lib-dev-hub-mdbook.sh — do not edit by hand. # Resolves mdbook binary (cargo / brew / PATH) and serves the rendered book. set -eu DATA="$HOME/Library/Application Support/keisei/mdbook" MDBOOK="$(command -v mdbook || echo "$HOME/.cargo/bin/mdbook")" [ -x "$MDBOOK" ] || { echo "mdbook not found (tried PATH and ~/.cargo/bin/mdbook)" >&2; exit 1; } exec "$MDBOOK" serve "$DATA" \ --hostname 127.0.0.1 --port 7080 --no-rebuild WRAPPER_EOF chmod +x "$wrapper" say " → wrote $wrapper" } # Emit ${KIT}/dev-hub/mdbook-rebuild.sh — regenerates src/ tree + `mdbook build`. # Idempotent: every run wipes & rebuilds src/ from ~/Projects/. write_rebuild_wrapper() { local wrapper="$HOME_DIR/.claude/agents/_primitives/dev-hub/mdbook-rebuild.sh" mkdir -p "$(dirname "$wrapper")" cat > "$wrapper" <<'WRAPPER_EOF' #!/usr/bin/env bash # Auto-generated by lib-dev-hub-mdbook.sh — do not edit by hand. # Regenerate SUMMARY.md + chapters from ~/Projects/ + rebuild book. set -e DATA="$HOME/Library/Application Support/keisei/mdbook" LOGS="$HOME/Library/Logs/keisei/mdbook" mkdir -p "$DATA/src" "$LOGS" # Helper: slugify project name (lowercase, non-alphanum → '-', collapse, trim). slugify() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/--*/-/g; s/^-//; s/-$//' } # Find mdbook binary (cargo or brew). MDBOOK="$(command -v mdbook || echo "$HOME/.cargo/bin/mdbook")" [ -x "$MDBOOK" ] || { echo "mdbook not found" >&2; exit 1; } # Wipe + regen src tree (chapter content only — book.toml stays at $DATA/book.toml). rm -rf "$DATA/src" mkdir -p "$DATA/src" SUMMARY="$DATA/src/SUMMARY.md" echo "# Summary" > "$SUMMARY" echo "" >> "$SUMMARY" for proj in "$HOME/Projects"/*/; do [ -d "$proj.git" ] || continue name="$(basename "$proj")" slug="$(slugify "$name")" chapter_dir="$DATA/src/$slug" mkdir -p "$chapter_dir" if [ -f "$proj/CLAUDE.md" ]; then cp "$proj/CLAUDE.md" "$chapter_dir/index.md" elif [ -f "$proj/README.md" ]; then cp "$proj/README.md" "$chapter_dir/index.md" else # No CLAUDE.md and no README.md → skip empty chapter. rmdir "$chapter_dir" 2>/dev/null || true continue fi echo "- [$name](./$slug/index.md)" >> "$SUMMARY" for doc in DECISIONS.md RUNBOOK.md DESIGN.md; do if [ -f "$proj/$doc" ]; then lower="$(echo "${doc%.md}" | tr '[:upper:]' '[:lower:]')" cp "$proj/$doc" "$chapter_dir/${lower}.md" 2>/dev/null || true echo " - [${doc%.md}](./$slug/${lower}.md)" >> "$SUMMARY" fi done done "$MDBOOK" build "$DATA" 2>>"$LOGS/rebuilder.err.log" echo "rebuilt at $(date -u +%FT%TZ)" WRAPPER_EOF chmod +x "$wrapper" say " → wrote $wrapper" } # ---------- public API ---------- # Walk ~/Projects/ and (re)generate ${DATA}/src/* + SUMMARY.md. # Delegates to mdbook-rebuild.sh so install-time and timer use identical logic. regenerate_summary() { local rebuild="$HOME_DIR/.claude/agents/_primitives/dev-hub/mdbook-rebuild.sh" if [ ! -x "$rebuild" ]; then err "mdbook-rebuild.sh not found — call write_rebuild_wrapper first" return 1 fi say " → regenerating chapter tree from $HOME_DIR/Projects/" "$rebuild" >/dev/null } # Install mdBook + render config + first build + register both launchd agents. # Idempotent. install_dev_hub_mdbook() { say "installing dev-hub-mdbook" _mdbook_check_cargo || return 1 _mdbook_install_binary local data_dir="$HOME_DIR/Library/Application Support/keisei/mdbook" mkdir -p "$data_dir/src" "$data_dir/book" _mdbook_render_book_toml "$data_dir" || return 1 write_serve_wrapper write_rebuild_wrapper regenerate_summary || return 1 # shellcheck source=./lib-launchd.sh . "$KIT_DIR/install/lib-launchd.sh" install_service mdbook-server install_service mdbook-rebuilder say "mdBook served on http://127.0.0.1:7080/. Rebuilds every 1h." } # Unload + remove both plists. Optional --purge also `cargo uninstall mdbook`. uninstall_dev_hub_mdbook() { say "uninstalling dev-hub-mdbook" # shellcheck source=./lib-launchd.sh . "$KIT_DIR/install/lib-launchd.sh" unload_plist mdbook-server unload_plist mdbook-rebuilder rm -f "$HOME_DIR/.claude/agents/_primitives/dev-hub/mdbook-serve.sh" rm -f "$HOME_DIR/.claude/agents/_primitives/dev-hub/mdbook-rebuild.sh" if [ "${1:-}" = "--purge" ] && command -v cargo >/dev/null 2>&1; then say " → cargo uninstall mdbook" cargo uninstall mdbook || true fi } # Verify mdBook server returns 200 on http://127.0.0.1:7080/. 0 OK, 1 fail. verify_dev_hub_mdbook() { local code code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:7080/ 2>/dev/null || echo 000)" if [ "$code" = "200" ]; then say "mdbook server health OK (200)" return 0 fi err "mdbook server health failed (HTTP $code) — check logs at $HOME_DIR/Library/Logs/keisei/mdbook-server/" return 1 }