feat(install): optional --activate-hooks jq-merge into settings.json

Adds --activate-hooks flag for non-interactive hook activation, plus a
TTY prompt at end-of-install. Merge is jq-based, groups by matcher, and
de-dupes hook entries by command — idempotent across re-runs. Existing
user hooks on the same matcher are preserved. Non-TTY without the flag
keeps the manual instructions.
This commit is contained in:
Parfii-bot 2026-04-21 03:35:55 +08:00
parent 5f51822214
commit 3c9e89e8c8

View file

@ -10,6 +10,24 @@ AGENTS_DIR="$HOME_DIR/.claude/agents"
HOOKS_DIR="$HOME_DIR/.claude/hooks"
SKILLS_DIR="$HOME_DIR/.claude/skills"
# --- flag parsing ----------------------------------------------------------
ACTIVATE_HOOKS=0
for arg in "$@"; do
case "$arg" in
--activate-hooks) ACTIVATE_HOOKS=1 ;;
--help|-h)
cat <<EOF
Usage: ./install.sh [--activate-hooks]
--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.
EOF
exit 0
;;
esac
done
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; }
@ -62,6 +80,57 @@ backup_dir() {
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.
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: 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
| ($snip[0] | del(._comment)) as $add
| reduce ($add.hooks | keys[]) as $phase ($orig;
.hooks[$phase] = (
((.hooks[$phase] // []) + ($add.hooks[$phase] // []))
| group_by(.matcher)
| map({
matcher: .[0].matcher,
hooks: (map(.hooks // []) | add | unique_by(.command))
})
)
)
' "$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)"
else
rm -f "$tmp"
err "jq-merge produced invalid output; $target unchanged"
return 1
fi
}
# --- prerequisites ----------------------------------------------------------
say "checking prerequisites"
if ! command -v cargo >/dev/null 2>&1; then
@ -176,27 +245,35 @@ fi
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) -------------------
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
printf '\033[1;36m[install]\033[0m activate hooks now? [y/N] '
read -r reply
case "$reply" in
y|Y|yes|YES) activate_hooks && DID_ACTIVATE=1 ;;
*) say "skipping hook activation" ;;
esac
fi
# --- done -----------------------------------------------------------------
echo
say "install complete"
echo
cat <<EOF
if [ "$DID_ACTIVATE" = "1" ]; then
cat <<EOF
==========================================================================
NEXT STEP: merge settings-snippet.json into ~/.claude/settings.json
Hooks activated. Settings merged into $SETTINGS_FILE
==========================================================================
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
@ -206,3 +283,28 @@ cat <<EOF
==========================================================================
EOF
else
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:
$SETTINGS_FILE
Or re-run with automatic activation:
./install.sh --activate-hooks
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
fi