Merge branch 'feat/v0.9-modular-install' — modular installer + MANIFEST.toml (BREAKING: default is minimal, was full)
This commit is contained in:
commit
9bcbf069d5
3 changed files with 708 additions and 155 deletions
66
README.md
66
README.md
|
|
@ -6,33 +6,73 @@ The kit is MIT-licensed and fully generic — install it on a fresh machine and
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- **Rust** (stable toolchain) — the assembler + 8 primitive crates are a Cargo workspace
|
||||
**Hard** (needed for every install, regardless of profile):
|
||||
|
||||
- **Rust** (stable toolchain) — the assembler Cargo workspace is always built
|
||||
- **jq** — used by the shell hooks for JSON parsing (`brew install jq` / `apt install jq`)
|
||||
- **Claude Code** — the agents, hooks, and skills target Claude Code's agent / skill / hook surface
|
||||
- **pandoc** (soft) — the `tomd` primitive falls back to pandoc for `.docx` / `.pptx` / `.html`
|
||||
- **Node + Playwright** (soft) — required for frontend primitives (`design-scrape`, `live-preview`, `mock-render`); install with `npm i -g playwright && playwright install chromium`
|
||||
- **sqlite3 CLI** (soft) — the `kei-ledger` / `kei-migrate` crates embed SQLite via `rusqlite`; the CLI is only needed if you want to inspect ledger DBs directly
|
||||
|
||||
**Soft** (only needed if the chosen profile pulls the primitive in — see the profile table below):
|
||||
|
||||
- **pandoc** — `tomd` uses it for `.docx` / `.pptx` / `.html` (needed for `core` / `full` profile)
|
||||
- **Node + Playwright** — for the 3 browser-driven frontend primitives `design-scrape`, `live-preview`, `mock-render` (`frontend` / `full` profile); install with `npm i -g playwright && playwright install chromium`
|
||||
- **sqlite3 CLI** — optional for manual DB inspection of `kei-ledger` / `kei-migrate` (their binaries embed SQLite via `rusqlite`; `ops` / `dev` profile)
|
||||
- **hcloud / vultr-cli** — wrapped by `provision-hetzner` / `provision-vultr` (`ops` profile)
|
||||
- **yq v4** (mikefarah/yq Go impl) — required by `kei-ci-lint` (`dev` profile)
|
||||
|
||||
`install.sh` checks only the deps relevant to the selected profile and soft-warns once per missing tool.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
git clone <your-fork-of-this-repo> KeiSeiKit
|
||||
cd KeiSeiKit
|
||||
./install.sh
|
||||
./install.sh # profile=minimal (default, no primitives)
|
||||
```
|
||||
|
||||
`install.sh` is idempotent. It:
|
||||
|
||||
1. Creates `~/.claude/agents/{_blocks,_manifests,_primitives,_bridges,_templates,_assembler,_generated}`, `~/.claude/hooks`, `~/.claude/skills`
|
||||
2. Copies all blocks, primitives (shell + Rust workspace), bridges (overwrites — these are SSoT from the kit)
|
||||
3. Copies generic manifests (skips if you already have a manifest with that name)
|
||||
4. Builds the Rust assembler (`cargo build --release` in `_assembler/`)
|
||||
5. Builds the 8 primitive crates (`cargo build --release` in `_primitives/_rust/`)
|
||||
6. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place`
|
||||
7. Copies the five hooks and 34 skills
|
||||
2. Copies all blocks + bridges (overwrites — these are SSoT from the kit)
|
||||
3. Copies primitives ONLY for the selected profile (default: `minimal` = none). Tracks installed set in `~/.claude/agents/_primitives/.installed`.
|
||||
4. Copies generic manifests (skips if you already have a manifest with that name)
|
||||
5. Builds the Rust assembler (`cargo build --release` in `_assembler/`)
|
||||
6. If any Rust primitive is in the selected profile: writes a scoped workspace `Cargo.toml` listing ONLY the installed crates, then `cargo build --release`
|
||||
7. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place`
|
||||
8. Copies the six hooks and 34 skills
|
||||
|
||||
After install, the only remaining step is merging `settings-snippet.json` into your `~/.claude/settings.json` to activate the hooks. You can do this automatically with `./install.sh --activate-hooks` or answer `y` at the end-of-install TTY prompt.
|
||||
|
||||
## Install profiles
|
||||
|
||||
By default `./install.sh` is **minimal** — agents + hooks + skills + bridges, no primitives. Fastest (~5s) and zero Rust compile for primitives. You opt into primitives via `--profile=<name>` or one-at-a-time via `--add=<name>`.
|
||||
|
||||
| Profile | Primitives added | Install time | Disk (approx) |
|
||||
|---|---|---|---|
|
||||
| `minimal` (default) | none | ~5s | ~2 MB |
|
||||
| `core` | `tomd` | ~5s | ~2 MB |
|
||||
| `frontend` | 8 site tools: `mock-render`, `visual-diff`, `tokens-sync`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode` | ~60s | ~80 MB |
|
||||
| `ops` | 8 infra tools: `kei-ledger`, `ssh-check`, `firewall-diff`, `provision-hetzner`, `provision-vultr`, `harden-base`, `metrics-scrape`, `log-ship` | ~90s | ~50 MB |
|
||||
| `dev` | 4 dev tools: `kei-migrate`, `kei-changelog`, `kei-ci-lint`, `kei-docs-scaffold` | ~60s | ~40 MB |
|
||||
| `full` | everything (21 primitives) | ~5 min | ~200 MB |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
./install.sh # minimal (no primitives)
|
||||
./install.sh --profile=frontend # minimal + 8 site tools
|
||||
./install.sh --profile=full # everything (old default behaviour)
|
||||
./install.sh --add=kei-ledger # add a single primitive on top of current install
|
||||
./install.sh --add=kei-ledger,ssh-check
|
||||
./install.sh --add=ops # a profile name works too — unions its members in
|
||||
./install.sh --list # show each primitive: name | kind | installed? | description
|
||||
./install.sh --remove=kei-migrate # remove one (rebuilds scoped rust workspace if needed)
|
||||
```
|
||||
|
||||
Profile resolution lives in `_primitives/MANIFEST.toml` — one `[primitive.<name>]` entry per primitive plus a `[profile]` block. Edit the manifest to define new profiles without touching `install.sh`.
|
||||
|
||||
> **Migrating from a full install:** if you're re-running `install.sh` after an earlier version that installed all 21 primitives unconditionally, the new default (`minimal`) will REMOVE them. To preserve the old behaviour explicitly, pass `--profile=full`.
|
||||
|
||||
> **Re-install disclaimer:** `install.sh` is idempotent for clean state but **overwrites kit-owned `_blocks/`, `_primitives/`, `_bridges/`, `_templates/`, `_assembler/`, `hooks/`, and `skills/` on re-run** — local modifications under those directories are backed up to `<dir>.bak-TIMESTAMP/` (or, for shared hook files, to `<file>.bak-TIMESTAMP`). User-owned `_manifests/*.toml` are never overwritten.
|
||||
|
||||
## What you get
|
||||
|
|
@ -43,8 +83,8 @@ After install, the only remaining step is merging `settings-snippet.json` into y
|
|||
| Generic agents (manifests) | 12 | `kei-code-implementer`, `kei-critic`, `kei-validator`, `kei-security-auditor`, `kei-architect`, `kei-researcher`, `kei-ml-implementer`, `kei-cost-guardian`, `kei-modal-runner`, ... |
|
||||
| Hooks | 6 | `assemble-agents`, `assemble-validate`, `no-hand-edit-agents`, `tomd-preread`, `agent-fork-logger`, `site-wysiwyd-check` |
|
||||
| Portable skills | 34 | `compose-solution`, `new-agent`, `new-project`, `site-create`, `schema-design`, `observability-setup`, `auth-setup`, `api-design`, `ci-scaffold`, `test-matrix`, `docs-scaffold`, `vm-provision`, ... |
|
||||
| Primitives (Rust crates) | 8 | `kei-ledger`, `kei-migrate`, `kei-changelog`, `ssh-check`, `firewall-diff`, `mock-render`, `visual-diff`, `tokens-sync` |
|
||||
| Primitives (shell) | 13 | `tomd`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode`, `metrics-scrape`, `log-ship`, `provision-hetzner`, `provision-vultr`, `harden-base`, `kei-ci-lint`, `kei-docs-scaffold` |
|
||||
| Primitives (Rust crates, opt-in) | 8 | `kei-ledger`, `kei-migrate`, `kei-changelog`, `ssh-check`, `firewall-diff`, `mock-render`, `visual-diff`, `tokens-sync` |
|
||||
| Primitives (shell, opt-in) | 13 | `tomd`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode`, `metrics-scrape`, `log-ship`, `provision-hetzner`, `provision-vultr`, `harden-base`, `kei-ci-lint`, `kei-docs-scaffold` |
|
||||
| Cross-tool bridges | 11 | Cursor legacy/MDC, Codex, Copilot, Windsurf, Junie, Continue, Gemini, Aider, Replit |
|
||||
|
||||
Of the 73 blocks, the **8 base blocks** (`baseline`, `evidence-grading`, `memory-protocol`, `rule-pre-dev-gate`, `rule-test-first`, `rule-error-budget`, `rule-double-audit`, `rule-math-first`) are referenced directly by the 12 shipped manifests. The remaining blocks (`stack-*`, `deploy-*`, `api-*`, `scraper-*`, `domain-*`) are a library consumed by the `/new-agent` wizard and the hub-and-spoke pipeline skills: when you compose a project specialist or spin up a site, the wizard / pipeline picks the appropriate blocks and emits artefacts that reference them.
|
||||
|
|
|
|||
154
_primitives/MANIFEST.toml
Normal file
154
_primitives/MANIFEST.toml
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# KeiSeiKit Primitives Manifest
|
||||
# Declarative SSoT for install.sh profile resolution.
|
||||
#
|
||||
# Profiles compose primitive sets; install.sh --profile=<name> resolves the
|
||||
# member list, copies/builds only those, and records the result in
|
||||
# ~/.claude/agents/_primitives/.installed.
|
||||
#
|
||||
# Individual primitives can be added/removed on top of any profile via
|
||||
# --add=<name>[,<name>] / --remove=<name>.
|
||||
#
|
||||
# Schema (per primitive):
|
||||
# kind = "shell" | "rust"
|
||||
# file = "<name>.sh" (shell only — lives at _primitives/<file>)
|
||||
# crate = "<name>" (rust only — lives at _primitives/_rust/<crate>)
|
||||
# deps = ["<dep description>", ...] # runtime/host deps, human-readable
|
||||
# desc = "<one-line description>"
|
||||
|
||||
[profile]
|
||||
minimal = []
|
||||
core = ["tomd"]
|
||||
frontend = ["mock-render", "visual-diff", "tokens-sync", "design-scrape", "live-preview", "figma-tokens", "frontend-inspect", "screenshot-decode"]
|
||||
ops = ["kei-ledger", "ssh-check", "firewall-diff", "provision-hetzner", "provision-vultr", "harden-base", "metrics-scrape", "log-ship"]
|
||||
dev = ["kei-migrate", "kei-changelog", "kei-ci-lint", "kei-docs-scaffold"]
|
||||
full = ["tomd", "kei-ledger", "kei-migrate", "kei-changelog", "ssh-check", "firewall-diff", "mock-render", "visual-diff", "tokens-sync", "design-scrape", "live-preview", "figma-tokens", "frontend-inspect", "screenshot-decode", "provision-hetzner", "provision-vultr", "harden-base", "metrics-scrape", "log-ship", "kei-ci-lint", "kei-docs-scaffold"]
|
||||
|
||||
# --- shell primitives (13) -------------------------------------------------
|
||||
|
||||
[primitive.tomd]
|
||||
kind = "shell"
|
||||
file = "tomd.sh"
|
||||
deps = ["jq", "pandoc (optional — needed for .docx/.pptx/.html)"]
|
||||
desc = "Universal non-native format → markdown (PDF, DOCX, XLSX, PPTX, CSV, images, code)"
|
||||
|
||||
[primitive.design-scrape]
|
||||
kind = "shell"
|
||||
file = "design-scrape.sh"
|
||||
deps = ["jq", "npx (Node)", "playwright (`npx playwright install chromium`)"]
|
||||
desc = "Live URL → design tokens + screenshots JSON via Playwright"
|
||||
|
||||
[primitive.live-preview]
|
||||
kind = "shell"
|
||||
file = "live-preview.sh"
|
||||
deps = ["npm"]
|
||||
desc = "start/stop/status wrapper for a project's dev server (.keisei/dev-server.pid)"
|
||||
|
||||
[primitive.figma-tokens]
|
||||
kind = "shell"
|
||||
file = "figma-tokens.sh"
|
||||
deps = ["curl", "jq", "FIGMA_TOKEN env var"]
|
||||
desc = "Figma API → design tokens JSON (consumed by tokens-sync)"
|
||||
|
||||
[primitive.frontend-inspect]
|
||||
kind = "shell"
|
||||
file = "frontend-inspect.sh"
|
||||
deps = ["jq"]
|
||||
desc = "Scan project dir → report framework, styling, UI count, lockfile"
|
||||
|
||||
[primitive.screenshot-decode]
|
||||
kind = "shell"
|
||||
file = "screenshot-decode.sh"
|
||||
deps = ["curl", "jq", "base64", "ANTHROPIC_API_KEY env var"]
|
||||
desc = "Screenshot → structured design description via Claude vision API"
|
||||
|
||||
[primitive.harden-base]
|
||||
kind = "shell"
|
||||
file = "harden-base.sh"
|
||||
deps = ["bash", "apt (runs on target Debian/Ubuntu VPS)"]
|
||||
desc = "Idempotent Debian/Ubuntu baseline hardening (fail2ban, ufw, unattended-upgrades)"
|
||||
|
||||
[primitive.provision-hetzner]
|
||||
kind = "shell"
|
||||
file = "provision-hetzner.sh"
|
||||
deps = ["hcloud CLI", "HCLOUD_TOKEN env var"]
|
||||
desc = "Hetzner Cloud server provisioner — create/status/destroy/list"
|
||||
|
||||
[primitive.provision-vultr]
|
||||
kind = "shell"
|
||||
file = "provision-vultr.sh"
|
||||
deps = ["vultr-cli v3", "VULTR_API_KEY env var"]
|
||||
desc = "Vultr VPS provisioner — create/status/destroy/list"
|
||||
|
||||
[primitive.metrics-scrape]
|
||||
kind = "shell"
|
||||
file = "metrics-scrape.sh"
|
||||
deps = ["curl", "awk", "jq (optional — needed for --format json)"]
|
||||
desc = "Prometheus /metrics scrape + normalize + diff against baseline"
|
||||
|
||||
[primitive.log-ship]
|
||||
kind = "shell"
|
||||
file = "log-ship.sh"
|
||||
deps = ["curl", "awk", "jq (optional — needed for --validate)"]
|
||||
desc = "Tail structured logs → forward to Loki / Datadog / HTTP with rate limits"
|
||||
|
||||
[primitive.kei-ci-lint]
|
||||
kind = "shell"
|
||||
file = "kei-ci-lint.sh"
|
||||
deps = ["yq v4+ (mikefarah/yq Go impl)"]
|
||||
desc = "Validate GitHub/Forgejo Actions workflow YAML (pinning, OIDC, cache, permissions)"
|
||||
|
||||
[primitive.kei-docs-scaffold]
|
||||
kind = "shell"
|
||||
file = "kei-docs-scaffold.sh"
|
||||
deps = []
|
||||
desc = "Detect project type → generate missing CLAUDE.md/DECISIONS.md/RUNBOOK.md/README.md"
|
||||
|
||||
# --- rust primitives (8) ---------------------------------------------------
|
||||
|
||||
[primitive.kei-ledger]
|
||||
kind = "rust"
|
||||
crate = "kei-ledger"
|
||||
deps = ["rusqlite bundled (no system sqlite required)"]
|
||||
desc = "Agent-fork lifecycle SQLite ledger (fork/done/fail) — SSoT for RULE 0.12"
|
||||
|
||||
[primitive.kei-migrate]
|
||||
kind = "rust"
|
||||
crate = "kei-migrate"
|
||||
deps = ["sqlx (postgres/sqlite/mysql)", "tokio", "DATABASE_URL env var"]
|
||||
desc = "Universal SQL migration runner — Postgres/SQLite/MySQL autodetect"
|
||||
|
||||
[primitive.kei-changelog]
|
||||
kind = "rust"
|
||||
crate = "kei-changelog"
|
||||
deps = ["git2 (vendored libgit2)"]
|
||||
desc = "Git-cliff-style CHANGELOG.md generator from Conventional Commits"
|
||||
|
||||
[primitive.ssh-check]
|
||||
kind = "rust"
|
||||
crate = "ssh-check"
|
||||
deps = []
|
||||
desc = "sshd_config linter — flags weak ciphers, PermitRootLogin yes, password auth"
|
||||
|
||||
[primitive.firewall-diff]
|
||||
kind = "rust"
|
||||
crate = "firewall-diff"
|
||||
deps = ["ufw (target-side; binary parses `ufw status` output)"]
|
||||
desc = "ufw intended-vs-running diff — catches drift between declared and live rules"
|
||||
|
||||
[primitive.mock-render]
|
||||
kind = "rust"
|
||||
crate = "mock-render"
|
||||
deps = ["Chrome/Chromium (runtime)", "playwright (optional for parity driver)"]
|
||||
desc = "Playwright wrapper with SHA-locked PNG (WYSIWYD: What You See Is What You Deploy)"
|
||||
|
||||
[primitive.visual-diff]
|
||||
kind = "rust"
|
||||
crate = "visual-diff"
|
||||
deps = []
|
||||
desc = "Pixel diff with tolerance — used in /site-create screenshot-regression loop"
|
||||
|
||||
[primitive.tokens-sync]
|
||||
kind = "rust"
|
||||
crate = "tokens-sync"
|
||||
deps = []
|
||||
desc = "Design tokens JSON → Tailwind config extend + CSS variables under :root"
|
||||
643
install.sh
643
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=<name> # minimal|core|frontend|ops|dev|full
|
||||
# ./install.sh --add=<name>[,<name>] # install one or more primitives on top of current state
|
||||
# ./install.sh --remove=<name> # 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 <<EOF
|
||||
Usage: ./install.sh [--activate-hooks] [--with-bridges]
|
||||
Usage: ./install.sh [flags]
|
||||
|
||||
--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.
|
||||
(no flags) install profile=minimal (agents + hooks + skills + bridges,
|
||||
no primitives). ~5s, no Rust compile for primitives.
|
||||
|
||||
--with-bridges After install, render the 11 cross-tool bridge files
|
||||
(Cursor / Copilot / Codex / Windsurf / Junie / Continue /
|
||||
Aider / Replit / Antigravity / Warp / Zed) into \$PWD.
|
||||
Skipped if invoked inside the KeiSeiKit repo itself.
|
||||
--profile=<name> 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=<a>[,<b>,...] add one or more primitives on top of current install.
|
||||
Name must match [primitive.<name>] in _primitives/MANIFEST.toml.
|
||||
|
||||
--remove=<name> 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 <file>.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.<name> = ["a", "b", ...]
|
||||
# 2. [primitive.<name>] ... 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-name>
|
||||
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.<name> = [...]` 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 <name> <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> (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 <<EOF
|
||||
|
|
@ -451,6 +808,7 @@ if [ "$DID_ACTIVATE" = "1" ]; then
|
|||
To verify install:
|
||||
ls $AGENTS_DIR/*.md # should show 12 generated agents
|
||||
$AGENTS_DIR/_assembler/target/release/assemble --validate
|
||||
./install.sh --list # show installed primitives
|
||||
|
||||
To create a new project-specialist agent:
|
||||
/new-agent
|
||||
|
|
@ -476,6 +834,7 @@ else
|
|||
To verify install:
|
||||
ls $AGENTS_DIR/*.md # should show 12 generated agents
|
||||
$AGENTS_DIR/_assembler/target/release/assemble --validate
|
||||
./install.sh --list # show installed primitives
|
||||
|
||||
To create a new project-specialist agent:
|
||||
/new-agent
|
||||
|
|
|
|||
Loading…
Reference in a new issue