Previously cp -f clobbered user-edited _blocks/, _templates/, _assembler/, hooks/, and skills/ silently — violating the README's "idempotent" claim on re-run. Now each of those targets is snapshotted to a timestamped <target>.bak-$(date +%s)/ sibling before overwrite, but only when the target actually contains regular files (freshly-mkdir'd scaffolds are skipped). _manifests/ unchanged — already uses per-file skip-if-exists. Kit source ($KIT_DIR) is never backed up.
159 lines
5.9 KiB
Bash
Executable file
159 lines
5.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# KeiSeiKit — Constructor-Pattern Agent Kit installer
|
|
# Idempotent: safe to re-run. Never overwrites settings.json or existing user manifests.
|
|
|
|
set -euo pipefail
|
|
|
|
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
HOME_DIR="${HOME:?HOME not set}"
|
|
AGENTS_DIR="$HOME_DIR/.claude/agents"
|
|
HOOKS_DIR="$HOME_DIR/.claude/hooks"
|
|
SKILLS_DIR="$HOME_DIR/.claude/skills"
|
|
|
|
say() { printf '\033[1;36m[install]\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
|
|
err() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; }
|
|
|
|
# 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, $HOOKS_DIR,
|
|
# $SKILLS_DIR — never on $KIT_DIR source.
|
|
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
|
|
local backup="${target}.bak-$(date +%s)"
|
|
cp -a "$target" "$backup"
|
|
say "backed up existing $target to $backup"
|
|
}
|
|
|
|
# --- prerequisites ----------------------------------------------------------
|
|
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
|
|
fi
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
warn "jq not found. Hooks use jq for JSON parsing — install before running Claude:"
|
|
warn " brew install jq (macOS)"
|
|
warn " apt install jq (Debian/Ubuntu)"
|
|
fi
|
|
|
|
# --- create target dirs -----------------------------------------------------
|
|
say "creating directories"
|
|
mkdir -p \
|
|
"$AGENTS_DIR/_blocks" \
|
|
"$AGENTS_DIR/_manifests" \
|
|
"$AGENTS_DIR/_templates" \
|
|
"$AGENTS_DIR/_assembler/src" \
|
|
"$AGENTS_DIR/_generated" \
|
|
"$HOOKS_DIR" \
|
|
"$SKILLS_DIR/new-agent"
|
|
|
|
# --- 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 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
|
|
name="$(basename "$f")"
|
|
if [[ -f "$AGENTS_DIR/_manifests/$name" ]]; then
|
|
skipped=$((skipped+1))
|
|
else
|
|
cp "$f" "$AGENTS_DIR/_manifests/$name"
|
|
copied=$((copied+1))
|
|
fi
|
|
done
|
|
say " copied $copied, skipped $skipped (already present)"
|
|
|
|
# --- copy template ---------------------------------------------------------
|
|
if compgen -G "$KIT_DIR/_templates/*.template" >/dev/null; then
|
|
say "copying specialist template"
|
|
backup_dir "$AGENTS_DIR/_templates"
|
|
cp -f "$KIT_DIR/_templates/"*.template "$AGENTS_DIR/_templates/"
|
|
fi
|
|
|
|
# --- copy assembler source (always refresh) --------------------------------
|
|
say "copying assembler source"
|
|
backup_dir "$AGENTS_DIR/_assembler"
|
|
cp -f "$KIT_DIR/_assembler/Cargo.toml" "$AGENTS_DIR/_assembler/"
|
|
cp -f "$KIT_DIR/_assembler/src/"*.rs "$AGENTS_DIR/_assembler/src/"
|
|
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) ---------------------
|
|
say "copying hooks -> $HOOKS_DIR/"
|
|
backup_dir "$HOOKS_DIR"
|
|
for h in assemble-agents.sh assemble-validate.sh no-hand-edit-agents.sh; do
|
|
cp -f "$KIT_DIR/hooks/$h" "$HOOKS_DIR/$h"
|
|
chmod +x "$HOOKS_DIR/$h"
|
|
done
|
|
|
|
# --- copy skills -----------------------------------------------------------
|
|
if [[ -d "$KIT_DIR/skills" ]]; then
|
|
say "copying skills"
|
|
backup_dir "$SKILLS_DIR"
|
|
for skill_dir in "$KIT_DIR/skills/"*/; do
|
|
[ -d "$skill_dir" ] || continue
|
|
skill_name="$(basename "$skill_dir")"
|
|
mkdir -p "$SKILLS_DIR/$skill_name"
|
|
cp -rf "$skill_dir"* "$SKILLS_DIR/$skill_name/" 2>/dev/null || true
|
|
say " -> $skill_name"
|
|
done
|
|
fi
|
|
|
|
# --- build assembler -------------------------------------------------------
|
|
say "building Rust assembler (cargo build --release)"
|
|
( cd "$AGENTS_DIR/_assembler" && cargo build --release )
|
|
if [[ ! -x "$AGENTS_DIR/_assembler/target/release/assemble" ]]; then
|
|
err "build succeeded but binary not found at $AGENTS_DIR/_assembler/target/release/assemble"
|
|
exit 2
|
|
fi
|
|
|
|
# --- generate .md agents in-place ------------------------------------------
|
|
say "generating agent .md files (--in-place)"
|
|
AGENT_ROOT="$AGENTS_DIR" "$AGENTS_DIR/_assembler/target/release/assemble" --in-place
|
|
|
|
# --- done -----------------------------------------------------------------
|
|
echo
|
|
say "install complete"
|
|
echo
|
|
cat <<EOF
|
|
==========================================================================
|
|
NEXT STEP: merge settings-snippet.json into ~/.claude/settings.json
|
|
==========================================================================
|
|
|
|
KeiSeiKit ships 3 hooks (assemble-agents, assemble-validate, no-hand-edit).
|
|
To activate them, merge entries from:
|
|
$KIT_DIR/settings-snippet.json
|
|
into your:
|
|
$HOME_DIR/.claude/settings.json
|
|
|
|
If you have no settings.json yet, you can simply copy the snippet:
|
|
cp $KIT_DIR/settings-snippet.json $HOME_DIR/.claude/settings.json
|
|
|
|
Otherwise, open both files and append the PostToolUse / PreToolUse
|
|
entries to the matching arrays in your existing settings.json.
|
|
|
|
To verify install:
|
|
ls $AGENTS_DIR/*.md # should show ~14 generated agents
|
|
$AGENTS_DIR/_assembler/target/release/assemble --validate
|
|
|
|
To create a new project-specialist agent:
|
|
/new-agent
|
|
|
|
==========================================================================
|
|
EOF
|