diff --git a/install.sh b/install.sh index 252fac2..3264eff 100755 --- a/install.sh +++ b/install.sh @@ -3,12 +3,13 @@ # Idempotent: safe to re-run. Never overwrites settings.json or existing user manifests. # # Usage: -# ./install.sh # install agents + hooks + skills + bridges/ -# ./install.sh --with-bridges # also render cross-tool bridges into $PWD -# (AGENTS.md, .cursorrules, .cursor/rules/main.mdc, -# .github/copilot-instructions.md, Windsurf, Junie, -# Continue, Gemini, Aider, Replit — 11 files total) -# Skipped if $PWD is the KeiSeiKit repo itself. +# ./install.sh # profile=minimal (agents + hooks + skills + bridges, NO primitives) +# ./install.sh --profile= # minimal|core|frontend|ops|dev|full +# ./install.sh --add=[,] # install one or more primitives on top of current state +# ./install.sh --remove= # remove a single primitive +# ./install.sh --list # list installed primitives (name | kind | desc | path) +# ./install.sh --with-bridges # also render cross-tool bridges into $PWD +# ./install.sh --activate-hooks # jq-merge settings-snippet.json into ~/.claude/settings.json set -euo pipefail @@ -17,26 +18,58 @@ HOME_DIR="${HOME:?HOME not set}" AGENTS_DIR="$HOME_DIR/.claude/agents" HOOKS_DIR="$HOME_DIR/.claude/hooks" SKILLS_DIR="$HOME_DIR/.claude/skills" +MANIFEST="$KIT_DIR/_primitives/MANIFEST.toml" +INSTALLED_FILE="$AGENTS_DIR/_primitives/.installed" # --- flag parsing ---------------------------------------------------------- ACTIVATE_HOOKS=0 WITH_BRIDGES=0 +PROFILE="" +ADD_LIST="" +REMOVE_NAME="" +LIST_MODE=0 + for arg in "$@"; do case "$arg" in --activate-hooks) ACTIVATE_HOOKS=1 ;; --with-bridges) WITH_BRIDGES=1 ;; + --profile=*) PROFILE="${arg#--profile=}" ;; + --add=*) ADD_LIST="${arg#--add=}" ;; + --remove=*) REMOVE_NAME="${arg#--remove=}" ;; + --list) LIST_MODE=1 ;; --help|-h) cat < set installed-primitive set to one of: + minimal (no primitives) + core (tomd) + frontend (8 site tools: mock-render / visual-diff / ...) + ops (8 infra tools: kei-ledger / ssh-check / ...) + dev (4 dev tools: kei-migrate / kei-changelog / ...) + full (all 21 primitives) + + --add=[,,...] add one or more primitives on top of current install. + Name must match [primitive.] in _primitives/MANIFEST.toml. + + --remove= remove a single primitive (shell file or rust crate dir + + scoped workspace Cargo.toml regenerated + rebuilt). + + --list list installed primitives from .installed state file. + + --with-bridges render the 11 cross-tool bridge files into \$PWD + (Cursor / Copilot / Codex / Windsurf / Junie / Continue / + Aider / Replit / Antigravity / Warp / Zed). + Skipped if invoked inside the KeiSeiKit repo itself. + + --activate-hooks jq-merge settings-snippet.json into ~/.claude/settings.json + non-interactively. Without this flag, a TTY prompt asks + at the end; non-TTY runs print manual instructions. + + --help, -h this help. EOF exit 0 ;; @@ -80,8 +113,6 @@ rollback() { orig="${pair%%|*}" bak="${pair#*|}" if [ -e "$bak" ]; then - # Guard rm -rf: only remove $orig if it actually exists as a file or - # directory. Harmless either way, but explicit is safer than brittle. if [ -d "$orig" ] || [ -f "$orig" ]; then rm -rf "$orig" fi @@ -93,15 +124,9 @@ rollback() { } trap rollback ERR -# Backup a populated target directory to a timestamped sibling before clobber. -# No-op if target is absent or contains no regular files (recursively). This -# means freshly-mkdir'd scaffolds are NOT backed up — only real user content. -# Only called on $AGENTS_DIR/_blocks, _templates, _assembler, $SKILLS_DIR — -# never on $KIT_DIR source. (hooks are now per-file; see backup_file.) backup_dir() { local target="$1" [ -d "$target" ] || return 0 - # No regular files anywhere under target → nothing worth preserving if [ -z "$(find "$target" -type f -print -quit 2>/dev/null)" ]; then return 0 fi @@ -111,9 +136,6 @@ backup_dir() { say "backed up existing $target to $backup" } -# Per-file backup for shared directories like $HOOKS_DIR, where other kits -# may drop sibling files we must not touch. Only the specific file is moved -# aside to .bak-TIMESTAMP. backup_file() { local target="$1" [ -f "$target" ] || return 0 @@ -123,35 +145,336 @@ backup_file() { say "backed up existing $target to $backup" } -# Activate KeiSeiKit hooks by merging settings-snippet.json into the user's -# settings.json. Idempotent: -# - If settings.json is absent, copy snippet verbatim (minus _comment key). -# - If present, concatenate the snippet's PostToolUse / PreToolUse entries -# onto existing arrays, then de-dupe by the nested hooks[].command field -# so re-runs do not stack duplicate entries. -# - .hooks itself (the root object key) is merged with `*` — snippet wins on -# scalar keys, arrays are unioned then de-duped. -# Requires jq (already checked earlier in prerequisites). Writes atomically -# via a tmpfile in the same dir. +# --- MANIFEST.toml parsing -------------------------------------------------- +# Tiny awk-based TOML reader. We only need two shapes: +# 1. profile. = ["a", "b", ...] +# 2. [primitive.] ... kind = "..." file = "..." crate = "..." deps = [...] desc = "..." +# If a real TOML parser (python -c "import tomllib" or python -c "import toml") is +# available, prefer it for robustness. Otherwise fall back to awk. + +have_python_toml() { + if command -v python3 >/dev/null 2>&1; then + python3 -c 'import tomllib' >/dev/null 2>&1 && return 0 + python3 -c 'import toml' >/dev/null 2>&1 && return 0 + fi + return 1 +} + +# Echo space-separated primitive names for a given profile. +# Usage: profile_members +profile_members() { + local profile="$1" + [ -f "$MANIFEST" ] || { err "MANIFEST.toml not found at $MANIFEST"; return 1; } + if have_python_toml; then + python3 - "$MANIFEST" "$profile" <<'PY' 2>/dev/null || return 1 +import sys +try: + import tomllib + mode = "rb" +except ImportError: + import toml as tomllib + mode = "r" +path, prof = sys.argv[1], sys.argv[2] +with open(path, mode) as f: + data = tomllib.load(f) if mode == "rb" else tomllib.load(f) +members = data.get("profile", {}).get(prof) +if members is None: + sys.exit(2) +print(" ".join(members)) +PY + else + # awk fallback — only handles `profile. = [...]` on one line + awk -v prof="$profile" ' + /^\[profile\]/ { in_profile=1; next } + /^\[/ && !/^\[profile\]/ { in_profile=0 } + in_profile && $0 ~ "^[[:space:]]*" prof "[[:space:]]*=" { + # extract between [ and ] + line = $0 + sub(/^[^\[]*\[/, "", line) + sub(/\].*$/, "", line) + gsub(/"/, "", line) + gsub(/,/, " ", line) + print line + exit + } + ' "$MANIFEST" + fi +} + +# Echo a field of a primitive. Usage: primitive_field +# field ∈ { kind, file, crate, desc } +primitive_field() { + local name="$1" field="$2" + [ -f "$MANIFEST" ] || return 1 + if have_python_toml; then + python3 - "$MANIFEST" "$name" "$field" <<'PY' 2>/dev/null +import sys +try: + import tomllib + mode = "rb" +except ImportError: + import toml as tomllib + mode = "r" +path, name, field = sys.argv[1], sys.argv[2], sys.argv[3] +with open(path, mode) as f: + data = tomllib.load(f) if mode == "rb" else tomllib.load(f) +p = data.get("primitive", {}).get(name) +if p is None: + sys.exit(2) +v = p.get(field, "") +if isinstance(v, list): + print("; ".join(v)) +else: + print(v) +PY + else + awk -v pname="$name" -v fname="$field" ' + $0 ~ "^\\[primitive\\." pname "\\]" { in_p=1; next } + /^\[/ && in_p { in_p=0 } + in_p && $0 ~ "^[[:space:]]*" fname "[[:space:]]*=" { + line = $0 + sub(/^[^=]*=[[:space:]]*/, "", line) + # strip surrounding quotes + gsub(/^"/, "", line) + gsub(/"$/, "", line) + print line + exit + } + ' "$MANIFEST" + fi +} + +# Echo all primitive names defined in MANIFEST. +all_primitive_names() { + [ -f "$MANIFEST" ] || return 1 + awk ' + /^\[primitive\./ { + name = $0 + sub(/^\[primitive\./, "", name) + sub(/\]$/, "", name) + print name + } + ' "$MANIFEST" +} + +# --- .installed state helpers --------------------------------------------- +read_installed() { + [ -f "$INSTALLED_FILE" ] && cat "$INSTALLED_FILE" || true +} + +write_installed() { + # stdin = newline-separated names; writes sorted-unique to INSTALLED_FILE. + mkdir -p "$(dirname "$INSTALLED_FILE")" + sort -u > "$INSTALLED_FILE" +} + +# --- per-primitive install/remove ------------------------------------------ +copy_shell_primitive() { + local name="$1" + local file + file="$(primitive_field "$name" file)" + [ -n "$file" ] || { err "no 'file' for shell primitive $name"; return 1; } + local src="$KIT_DIR/_primitives/$file" + local dst="$AGENTS_DIR/_primitives/$file" + [ -f "$src" ] || { err "source missing: $src"; return 1; } + mkdir -p "$AGENTS_DIR/_primitives" + cp -f "$src" "$dst" + chmod +x "$dst" + say " + shell: $name ($file)" +} + +remove_shell_primitive() { + local name="$1" + local file + file="$(primitive_field "$name" file)" + [ -n "$file" ] || return 0 + rm -f "$AGENTS_DIR/_primitives/$file" + say " - shell: $name ($file)" +} + +copy_rust_primitive() { + local name="$1" + local crate + crate="$(primitive_field "$name" crate)" + [ -n "$crate" ] || { err "no 'crate' for rust primitive $name"; return 1; } + local src="$KIT_DIR/_primitives/_rust/$crate" + [ -d "$src" ] || { err "source missing: $src"; return 1; } + local dst_root="$AGENTS_DIR/_primitives/_rust" + local dst="$dst_root/$crate" + mkdir -p "$dst/src" + cp -f "$src/Cargo.toml" "$dst/Cargo.toml" + [ -d "$src/src" ] && cp -rf "$src/src/"* "$dst/src/" 2>/dev/null || true + if [ -d "$src/tests" ]; then + mkdir -p "$dst/tests" + cp -rf "$src/tests/"* "$dst/tests/" 2>/dev/null || true + fi + say " + rust: $name (crate $crate)" +} + +remove_rust_primitive() { + local name="$1" + local crate + crate="$(primitive_field "$name" crate)" + [ -n "$crate" ] || return 0 + rm -rf "$AGENTS_DIR/_primitives/_rust/$crate" + say " - rust: $name (crate $crate)" +} + +# Echo the list of rust crates currently installed (by scanning .installed + +# cross-checking MANIFEST kind = "rust" + dir presence). +installed_rust_crates() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + local name kind crate + while IFS= read -r name; do + [ -z "$name" ] && continue + kind="$(primitive_field "$name" kind)" + [ "$kind" = "rust" ] || continue + crate="$(primitive_field "$name" crate)" + [ -n "$crate" ] && [ -d "$dst_root/$crate" ] && echo "$crate" + done <<< "$(read_installed)" +} + +# Write a scoped Cargo.toml listing only the given members (stdin: one per line). +# The workspace.package / workspace.dependencies / profile.release blocks are +# copied verbatim from the kit source so shared deps stay in sync. +write_rust_workspace_manifest() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + local src_wkspc="$KIT_DIR/_primitives/_rust/Cargo.toml" + local tmp="$dst_root/Cargo.toml.tmp" + { + echo '[workspace]' + echo 'resolver = "2"' + echo 'members = [' + local m + while IFS= read -r m; do + [ -n "$m" ] && echo " \"$m\"," + done + echo ']' + awk '/^\[workspace\.package\]/,0' "$src_wkspc" + } > "$tmp" + mv "$tmp" "$dst_root/Cargo.toml" + if [ -f "$KIT_DIR/_primitives/_rust/Cargo.lock" ]; then + cp -f "$KIT_DIR/_primitives/_rust/Cargo.lock" "$dst_root/Cargo.lock" + fi +} + +# Build the scoped rust workspace. Offline-first, online fallback. +build_rust_workspace() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + if ! ( cd "$dst_root" && cargo build --workspace --release --offline ) 2>/tmp/keiseikit-primitives-offline.log; then + say " offline build failed — fetching deps from crates.io" + if ! ( cd "$dst_root" && cargo build --workspace --release ); then + warn "Rust primitive workspace build failed; shell primitives still work" + warn " see log: /tmp/keiseikit-primitives-offline.log" + return 0 + fi + fi +} + +# Orchestrator: installed rust crates -> scoped manifest -> cargo build -> report. +# No-op when no rust primitives are installed. +regenerate_rust_workspace() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + mkdir -p "$dst_root" + local members_nl + members_nl="$(installed_rust_crates)" + if [ -z "$members_nl" ]; then + rm -f "$dst_root/Cargo.toml" "$dst_root/Cargo.lock" + return 0 + fi + local n + n="$(printf '%s\n' "$members_nl" | grep -c .)" + printf '%s\n' "$members_nl" | write_rust_workspace_manifest + say "building Rust primitives ($n crate(s))" + build_rust_workspace + local built=0 m + while IFS= read -r m; do + [ -n "$m" ] && [ -x "$dst_root/target/release/$m" ] && built=$((built+1)) + done <<< "$members_nl" + say " $built / $n Rust primitive binaries available" +} + +# Install primitives from a name list (newline-separated on stdin). +# Updates .installed as a superset. +install_primitives() { + local names existing combined new_file + names="$(cat)" + existing="$(read_installed)" + combined="$(printf '%s\n%s\n' "$existing" "$names" | grep -v '^$' || true)" + local kind + local any_rust=0 + while IFS= read -r p; do + [ -z "$p" ] && continue + kind="$(primitive_field "$p" kind)" + case "$kind" in + shell) copy_shell_primitive "$p" ;; + rust) copy_rust_primitive "$p"; any_rust=1 ;; + *) warn "unknown primitive: $p (skipping)"; continue ;; + esac + done <<< "$names" + printf '%s\n' "$combined" | write_installed + if [ "$any_rust" = "1" ]; then + regenerate_rust_workspace + fi +} + +# Remove a single primitive by name. +remove_primitive() { + local name="$1" kind + kind="$(primitive_field "$name" kind)" + case "$kind" in + shell) remove_shell_primitive "$name" ;; + rust) remove_rust_primitive "$name" ;; + *) err "unknown primitive: $name"; return 1 ;; + esac + local existing + existing="$(read_installed)" + printf '%s\n' "$existing" | grep -vFx "$name" | grep -v '^$' | write_installed || true + # Rust removal => rewrite scoped workspace + if [ "$kind" = "rust" ]; then + regenerate_rust_workspace + fi +} + +# --- --list implementation -------------------------------------------------- +cmd_list() { + echo + printf '%-22s %-6s %-10s %s\n' "NAME" "KIND" "STATUS" "DESCRIPTION" + printf '%-22s %-6s %-10s %s\n' "----" "----" "------" "-----------" + local installed names kind desc status + installed="$(read_installed)" + while IFS= read -r name; do + [ -z "$name" ] && continue + kind="$(primitive_field "$name" kind)" + desc="$(primitive_field "$name" desc)" + if printf '%s\n' "$installed" | grep -qFx "$name"; then + status="INSTALLED" + else + status="-" + fi + printf '%-22s %-6s %-10s %s\n' "$name" "$kind" "$status" "$desc" + done < <(all_primitive_names) + echo + local count + count="$(printf '%s\n' "$installed" | grep -c . || true)" + printf '%s primitives installed (state: %s)\n' "${count:-0}" "$INSTALLED_FILE" + echo +} + +# --- hook activation (unchanged jq-merge) ---------------------------------- activate_hooks() { local snippet="$KIT_DIR/settings-snippet.json" local target="$HOME_DIR/.claude/settings.json" local tmp [ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; } if [ ! -f "$target" ]; then - # Strip _comment, keep the rest. Create atomically. tmp="$(mktemp "$target.XXXXXX")" jq 'del(._comment)' "$snippet" > "$tmp" mv "$tmp" "$target" say "created $target from snippet (no prior settings.json)" return 0 fi - # Merge path: back up the pre-merge settings.json so rollback can restore - # it if a later step ERR-traps. The "create new" path above exits before - # reaching here, so backup_file is only invoked when $target exists. backup_file "$target" - # Merge: walk each matcher-group in PostToolUse / PreToolUse, append hooks, - # unique_by command. jq filter is written for readability, not golf. tmp="$(mktemp "$target.XXXXXX")" jq --slurpfile snip "$snippet" ' . as $orig @@ -167,7 +490,6 @@ activate_hooks() { ) ) ' "$target" > "$tmp" - # Only replace if jq produced non-empty valid JSON if [ -s "$tmp" ] && jq -e . "$tmp" >/dev/null 2>&1; then mv "$tmp" "$target" say "merged hooks into $target (idempotent)" @@ -178,13 +500,64 @@ activate_hooks() { fi } -# --- prerequisites ---------------------------------------------------------- +# --- --list short-circuit --------------------------------------------------- +if [ "$LIST_MODE" = "1" ]; then + [ -f "$MANIFEST" ] || { err "MANIFEST.toml missing: $MANIFEST"; exit 2; } + cmd_list + exit 0 +fi + +# --- incremental --add / --remove short-circuit --------------------------- +# If either flag is set, skip the full agent/hook/skills sync and just mutate +# the primitive set. Assumes a prior install already wrote _blocks etc. +if [ -n "$ADD_LIST" ] || [ -n "$REMOVE_NAME" ]; then + [ -f "$MANIFEST" ] || { err "MANIFEST.toml missing: $MANIFEST"; exit 2; } + mkdir -p "$AGENTS_DIR/_primitives" + + if [ -n "$REMOVE_NAME" ]; then + say "removing primitive: $REMOVE_NAME" + remove_primitive "$REMOVE_NAME" + fi + + if [ -n "$ADD_LIST" ]; then + # Resolve --add=x,y,z OR --add= (profile expands in-place) + tr ',' '\n' <<< "$ADD_LIST" | grep -v '^$' | while IFS= read -r token; do + # Is token a known profile? + local_members="$(profile_members "$token" 2>/dev/null || true)" + if [ -n "$local_members" ]; then + printf '%s\n' "$local_members" | tr ' ' '\n' + else + printf '%s\n' "$token" + fi + done | grep -v '^$' | sort -u | install_primitives + say "added: $ADD_LIST" + fi + + echo + say "incremental change complete" + cmd_list + exit 0 +fi + +# --- resolve profile ------------------------------------------------------ +# Default profile is minimal. +PROFILE="${PROFILE:-minimal}" +case "$PROFILE" in + minimal|core|frontend|ops|dev|full) ;; + *) + err "unknown profile: $PROFILE. Valid: minimal | core | frontend | ops | dev | full" + exit 1 + ;; +esac +say "profile: $PROFILE" + +# --- prerequisites --------------------------------------------------------- +# HARD: cargo, jq. SOFT: deps based on the primitives that will be installed. say "checking prerequisites" if ! command -v cargo >/dev/null 2>&1; then err "cargo not found. Install Rust: https://rustup.rs/" exit 1 fi -# Verify cargo actually runs (catches "rustup has no default toolchain") if ! cargo --version >/dev/null 2>&1; then err "cargo is installed but not functional. Run: rustup default stable" exit 1 @@ -197,24 +570,48 @@ if ! command -v jq >/dev/null 2>&1; then err " apt install jq (Debian/Ubuntu)" exit 1 fi -# Soft-warn on pandoc — the tomd primitive works without it for CSV / code / -# JSON / images, but fails on .docx / .pptx / .html. Opt-in use, so not -# promoted to a hard-fail. -if ! command -v pandoc >/dev/null 2>&1; then + +# Profile-aware soft-warn: only check deps for primitives actually being installed. +# Build a unique set of substrings to check. +PROFILE_PRIMS="$(profile_members "$PROFILE" 2>/dev/null || true)" +needs_pandoc=0 +needs_playwright=0 +needs_sqlite=0 +needs_hcloud=0 +needs_vultr=0 +needs_yq=0 +for p in $PROFILE_PRIMS; do + case "$p" in + tomd) needs_pandoc=1 ;; + design-scrape|live-preview|mock-render) needs_playwright=1 ;; + kei-ledger|kei-migrate) needs_sqlite=1 ;; + provision-hetzner) needs_hcloud=1 ;; + provision-vultr) needs_vultr=1 ;; + kei-ci-lint) needs_yq=1 ;; + esac +done +if [ "$needs_pandoc" = "1" ] && ! command -v pandoc >/dev/null 2>&1; then warn "pandoc not found — tomd primitive will fail on .docx/.pptx. Install: brew install pandoc" fi -# Soft-warn on playwright — frontend primitives (design-scrape, live-preview, -# mock-render) need the Playwright browser driver. Not used by the core fleet. -if ! command -v playwright >/dev/null 2>&1 && ! command -v npx >/dev/null 2>&1; then - warn "playwright/npx not found — frontend primitives (design-scrape, live-preview, mock-render) will fail. Install: npm i -g playwright && playwright install chromium" +if [ "$needs_playwright" = "1" ] \ + && ! command -v playwright >/dev/null 2>&1 \ + && ! command -v npx >/dev/null 2>&1; then + warn "playwright/npx not found — frontend primitives need them. Install: npm i -g playwright && playwright install chromium" fi -# Soft-warn on sqlite3 CLI — kei-ledger / kei-migrate embed rusqlite, so the -# CLI is optional. Only surfaced so users can manually inspect the ledger DB. -if ! command -v sqlite3 >/dev/null 2>&1; then - warn "sqlite3 CLI not found — kei-ledger/kei-migrate work without it (rusqlite embedded). Install if you want manual DB inspection: brew install sqlite" +if [ "$needs_sqlite" = "1" ] && ! command -v sqlite3 >/dev/null 2>&1; then + warn "sqlite3 CLI not found — kei-ledger/kei-migrate work without it (rusqlite embedded). Install for manual DB inspection: brew install sqlite" +fi +if [ "$needs_hcloud" = "1" ] && ! command -v hcloud >/dev/null 2>&1; then + warn "hcloud CLI not found — provision-hetzner requires it. Install: brew install hcloud" +fi +if [ "$needs_vultr" = "1" ] && ! command -v vultr-cli >/dev/null 2>&1; then + warn "vultr-cli not found — provision-vultr requires it. Install: brew install vultr/vultr-cli/vultr-cli" +fi +if [ "$needs_yq" = "1" ] && ! command -v yq >/dev/null 2>&1; then + warn "yq not found — kei-ci-lint requires yq v4+ (mikefarah/yq). Install: brew install yq" fi -# --- create target dirs ----------------------------------------------------- +# --- create target dirs --------------------------------------------------- say "creating directories" mkdir -p \ "$AGENTS_DIR/_blocks" \ @@ -227,9 +624,7 @@ mkdir -p \ "$SKILLS_DIR/new-agent" \ "$HOME_DIR/.claude/memory" -# --- scaffold MEMORY.md placeholder (user-respecting) ---------------------- -# _blocks/memory-protocol.md references ~/.claude/memory/MEMORY.md; without -# this file the first agent following the protocol fails on read. +# --- scaffold MEMORY.md placeholder -------------------------------------- MEMORY_INDEX="$HOME_DIR/.claude/memory/MEMORY.md" if [[ ! -f "$MEMORY_INDEX" ]]; then cat > "$MEMORY_INDEX" <<'EOF' @@ -241,49 +636,45 @@ EOF say "scaffolded $MEMORY_INDEX" fi -# --- copy blocks (overwrite ours; blocks are SSoT from kit) ---------------- +# --- copy blocks (overwrite ours; blocks are SSoT from kit) -------------- say "copying shared blocks -> $AGENTS_DIR/_blocks/" backup_dir "$AGENTS_DIR/_blocks" cp -f "$KIT_DIR/_blocks/"*.md "$AGENTS_DIR/_blocks/" -# --- copy primitives (overwrite; primitives are SSoT from kit) ------------- -# Shell primitives live at _primitives/*.sh, Rust primitives under -# _primitives/_rust/ (Cargo workspace). The Rust workspace is copied wholesale -# but the compile artefacts (target/) are excluded — we rebuild locally. -if [[ -d "$KIT_DIR/_primitives" ]]; then - say "copying primitives -> $AGENTS_DIR/_primitives/" - backup_dir "$AGENTS_DIR/_primitives" - cp -f "$KIT_DIR/_primitives/"*.sh "$AGENTS_DIR/_primitives/" 2>/dev/null || true - cp -f "$KIT_DIR/_primitives/README.md" "$AGENTS_DIR/_primitives/" 2>/dev/null || true - chmod +x "$AGENTS_DIR/_primitives/"*.sh 2>/dev/null || true - if [[ -d "$KIT_DIR/_primitives/_rust" ]]; then - say " copying Rust primitive workspace (excluding target/)" - mkdir -p "$AGENTS_DIR/_primitives/_rust" - # Copy workspace manifest + each crate source, skip target/ - cp -f "$KIT_DIR/_primitives/_rust/Cargo.toml" "$AGENTS_DIR/_primitives/_rust/" - if [[ -f "$KIT_DIR/_primitives/_rust/Cargo.lock" ]]; then - cp -f "$KIT_DIR/_primitives/_rust/Cargo.lock" "$AGENTS_DIR/_primitives/_rust/" - fi - for crate_dir in "$KIT_DIR/_primitives/_rust/"*/; do - [ -d "$crate_dir" ] || continue - crate_name="$(basename "$crate_dir")" - [ "$crate_name" = "target" ] && continue - mkdir -p "$AGENTS_DIR/_primitives/_rust/$crate_name" - # Copy Cargo.toml + src/ + tests/ (if present) - cp -f "$crate_dir/Cargo.toml" "$AGENTS_DIR/_primitives/_rust/$crate_name/" 2>/dev/null || true - if [[ -d "$crate_dir/src" ]]; then - mkdir -p "$AGENTS_DIR/_primitives/_rust/$crate_name/src" - cp -rf "$crate_dir/src/"* "$AGENTS_DIR/_primitives/_rust/$crate_name/src/" 2>/dev/null || true - fi - if [[ -d "$crate_dir/tests" ]]; then - mkdir -p "$AGENTS_DIR/_primitives/_rust/$crate_name/tests" - cp -rf "$crate_dir/tests/"* "$AGENTS_DIR/_primitives/_rust/$crate_name/tests/" 2>/dev/null || true - fi - done - fi +# --- copy primitives (profile-driven) ------------------------------------- +# Always copy MANIFEST.toml + README.md so subsequent --list works. +mkdir -p "$AGENTS_DIR/_primitives" +cp -f "$KIT_DIR/_primitives/MANIFEST.toml" "$AGENTS_DIR/_primitives/MANIFEST.toml" 2>/dev/null || true +cp -f "$KIT_DIR/_primitives/README.md" "$AGENTS_DIR/_primitives/" 2>/dev/null || true + +say "resolving primitives for profile=$PROFILE" +# Clean slate: drop every shell .sh + rust crate dir from the installed set +# FAST (no per-rust rebuild). A single regenerate_rust_workspace at the end +# of the install phase handles the final state. +existing_installed="$(read_installed)" +if [ -n "${existing_installed:-}" ]; then + while IFS= read -r n; do + [ -z "$n" ] && continue + k="$(primitive_field "$n" kind 2>/dev/null || true)" + case "$k" in + shell) f="$(primitive_field "$n" file)"; [ -n "$f" ] && rm -f "$AGENTS_DIR/_primitives/$f" ;; + rust) c="$(primitive_field "$n" crate)"; [ -n "$c" ] && rm -rf "$AGENTS_DIR/_primitives/_rust/$c" ;; + esac + done <<< "$existing_installed" + : > "$INSTALLED_FILE" fi -# --- copy bridges (overwrite; templates are SSoT from kit) ----------------- +# Install fresh per profile. install_primitives rebuilds rust workspace once +# at the end if any rust crate was added; for minimal we still need to scrub +# any stale workspace Cargo.toml. +if [ -n "${PROFILE_PRIMS:-}" ]; then + printf '%s\n' "$PROFILE_PRIMS" | tr ' ' '\n' | grep -v '^$' | install_primitives +else + regenerate_rust_workspace + say " (no primitives — minimal profile)" +fi + +# --- copy bridges (overwrite; templates are SSoT from kit) ---------------- if [[ -d "$KIT_DIR/_bridges" ]]; then say "copying bridge templates -> $AGENTS_DIR/_bridges/" mkdir -p "$AGENTS_DIR/_bridges" @@ -294,7 +685,7 @@ if [[ -d "$KIT_DIR/_bridges" ]]; then chmod +x "$AGENTS_DIR/_bridges/emit.sh" fi -# --- copy generic manifests, DO NOT overwrite user's existing manifests ----- +# --- copy generic manifests, DO NOT overwrite user's existing manifests --- say "copying generic manifests -> $AGENTS_DIR/_manifests/ (skip if exists)" copied=0; skipped=0 for f in "$KIT_DIR/_manifests/"*.toml; do @@ -308,8 +699,7 @@ for f in "$KIT_DIR/_manifests/"*.toml; do done say " copied $copied, skipped $skipped (already present)" -# --- copy template --------------------------------------------------------- -# bash-3.2-portable glob detection: iterate, break on first hit. +# --- copy template -------------------------------------------------------- has_templates=0 for t in "$KIT_DIR/_templates/"*.template; do [ -f "$t" ] && { has_templates=1; break; } @@ -320,7 +710,7 @@ if [ "$has_templates" = "1" ]; then cp -f "$KIT_DIR/_templates/"*.template "$AGENTS_DIR/_templates/" fi -# --- copy assembler source (always refresh) -------------------------------- +# --- copy assembler source (always refresh) ------------------------------- say "copying assembler source" backup_dir "$AGENTS_DIR/_assembler" cp -f "$KIT_DIR/_assembler/Cargo.toml" "$AGENTS_DIR/_assembler/" @@ -329,12 +719,7 @@ if [[ -f "$KIT_DIR/_assembler/.gitignore" ]]; then cp -f "$KIT_DIR/_assembler/.gitignore" "$AGENTS_DIR/_assembler/" fi -# --- copy hooks (refresh; hooks are logic, not config) --------------------- -# $HOOKS_DIR is shared with other kits — back up each KeiSeiKit-owned hook -# individually rather than the whole directory, so foreign hooks are not -# dragged into .bak-TIMESTAMP snapshots on every re-run. -# Discover hooks dynamically from $KIT_DIR/hooks/*.sh so new hooks land -# automatically without editing install.sh. +# --- copy hooks (refresh; hooks are logic, not config) -------------------- say "copying hooks -> $HOOKS_DIR/" hook_count=0 for hook_src in "$KIT_DIR/hooks/"*.sh; do @@ -347,7 +732,7 @@ for hook_src in "$KIT_DIR/hooks/"*.sh; do done say " installed $hook_count hook(s)" -# --- copy skills ----------------------------------------------------------- +# --- copy skills ---------------------------------------------------------- if [[ -d "$KIT_DIR/skills" ]]; then say "copying skills" backup_dir "$SKILLS_DIR" @@ -360,9 +745,7 @@ if [[ -d "$KIT_DIR/skills" ]]; then done fi -# --- build assembler ------------------------------------------------------- -# Prefer offline build (fresh-clone on a no-network machine should still work -# if the registry cache is warm). Fall back to online fetch on failure. +# --- build assembler ------------------------------------------------------ say "building Rust assembler (cargo build --release, offline first)" if ! ( cd "$AGENTS_DIR/_assembler" && cargo build --release --offline ) 2>/tmp/keiseikit-cargo-offline.log; then say "offline build failed — fetching deps from crates.io" @@ -373,40 +756,17 @@ if [[ ! -x "$AGENTS_DIR/_assembler/target/release/assemble" ]]; then exit 2 fi -# --- build Rust primitives workspace (8 crates) ---------------------------- -# Offline-first like the assembler. Failure here is non-fatal: the fleet and -# shell primitives still work without the 8 Rust binaries. -if [[ -d "$AGENTS_DIR/_primitives/_rust" && -f "$AGENTS_DIR/_primitives/_rust/Cargo.toml" ]]; then - say "building Rust primitive workspace (8 crates, cargo build --release)" - if ! ( cd "$AGENTS_DIR/_primitives/_rust" && cargo build --workspace --release --offline ) 2>/tmp/keiseikit-primitives-offline.log; then - say " offline build failed — fetching deps from crates.io" - if ! ( cd "$AGENTS_DIR/_primitives/_rust" && cargo build --workspace --release ); then - warn "Rust primitive workspace build failed; fleet still functional without binaries" - warn " see log: /tmp/keiseikit-primitives-offline.log" - fi - fi - # Report which binaries built successfully. - built=0 - for bin in kei-ledger kei-migrate kei-changelog ssh-check firewall-diff mock-render visual-diff tokens-sync; do - if [[ -x "$AGENTS_DIR/_primitives/_rust/target/release/$bin" ]]; then - built=$((built+1)) - fi - done - say " $built / 8 Rust primitive binaries available" -fi - -# --- generate .md agents in-place ------------------------------------------ +# --- generate .md agents in-place ----------------------------------------- say "generating agent .md files (--in-place)" AGENT_ROOT="$AGENTS_DIR" "$AGENTS_DIR/_assembler/target/release/assemble" --in-place -# --- activate hooks (flag, or interactive prompt on TTY) ------------------- +# --- activate hooks (flag, or interactive prompt on TTY) ------------------ SETTINGS_FILE="$HOME_DIR/.claude/settings.json" DID_ACTIVATE=0 if [ "$ACTIVATE_HOOKS" = "1" ]; then say "activating hooks (--activate-hooks)" activate_hooks && DID_ACTIVATE=1 elif [ ! -f "$SETTINGS_FILE" ]; then - # No existing settings — merge is trivial, do it unconditionally. say "no existing settings.json; installing snippet" activate_hooks && DID_ACTIVATE=1 elif [ -t 0 ] && [ -t 1 ]; then @@ -422,10 +782,7 @@ elif [ -t 0 ] && [ -t 1 ]; then esac fi -# --- optional: render cross-tool bridges into $PWD ------------------------- -# If a prior step ERR-trapped into rollback(), we MUST NOT keep writing into -# $PWD — the install is now aborted, and bridges should not land as -# collateral on a failed run. rollback() sets ROLLED_BACK=1 before returning. +# --- optional: render cross-tool bridges into $PWD ----------------------- if [ "${ROLLED_BACK:-0}" = "1" ]; then exit 2 fi @@ -438,9 +795,9 @@ if [[ "$WITH_BRIDGES" == "1" ]]; then fi fi -# --- done ----------------------------------------------------------------- +# --- done ---------------------------------------------------------------- echo -say "install complete" +say "install complete (profile=$PROFILE)" echo if [ "$DID_ACTIVATE" = "1" ]; then cat <