KeiSeiKit-1.0/install/lib-hooks.sh
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

121 lines
4.3 KiB
Bash

# shellcheck shell=bash
# lib-hooks.sh — hook file copy + settings.json jq-merge.
#
# Hooks are logic (not config) → always refreshed, every install.
# settings.json merge is idempotent: it groups by matcher and unions .hooks
# by unique command so repeated runs never duplicate entries.
#
# Requires: say / warn / err from lib-log.sh.
# Requires: backup_file from lib-backup.sh.
# Reads globals: $KIT_DIR, $HOOKS_DIR, $HOME_DIR.
# Copy every *.sh hook from the kit into $HOOKS_DIR, +x, with per-file backup.
# Also copies hooks/_lib/*.sh → $HOOKS_DIR/_lib/ (v0.17 gate-extract). The
# hooks dot-source "$(dirname "$0")/_lib/gate.sh" — the _lib/ sub-tree MUST
# land alongside the hook scripts or the gate becomes a no-op (fail-open).
install_hooks() {
say "copying hooks -> $HOOKS_DIR/"
local hook_count=0 hook_src h
for hook_src in "$KIT_DIR/hooks/"*.sh; do
[ -f "$hook_src" ] || continue
h="$(basename "$hook_src")"
backup_file "$HOOKS_DIR/$h"
cp -f "$hook_src" "$HOOKS_DIR/$h"
chmod +x "$HOOKS_DIR/$h"
hook_count=$((hook_count+1))
done
say " installed $hook_count hook(s)"
# v0.17 — shared hook library (gate.sh + test-gate.sh)
if [ -d "$KIT_DIR/hooks/_lib" ]; then
mkdir -p "$HOOKS_DIR/_lib"
local lib_count=0 lib_src lib_name
for lib_src in "$KIT_DIR/hooks/_lib/"*.sh; do
[ -f "$lib_src" ] || continue
lib_name="$(basename "$lib_src")"
cp -f "$lib_src" "$HOOKS_DIR/_lib/$lib_name"
chmod +x "$HOOKS_DIR/_lib/$lib_name"
lib_count=$((lib_count+1))
done
say " installed $lib_count hook library file(s) -> $HOOKS_DIR/_lib/"
fi
}
# Merge settings-snippet.json into ~/.claude/settings.json non-interactively
# via jq. On first run (no settings.json) we strip _comment and drop in the
# snippet verbatim. On subsequent runs we group by matcher and dedupe .hooks
# by command so re-runs are true no-ops.
# jq-merge snippet into existing target. group_by matcher + dedup by command
# so re-runs are no-ops. Args: $1=snippet, $2=target.
_jq_merge_hooks() {
local snippet="$1" target="$2" tmp
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"
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
}
activate_hooks() {
local snippet="$KIT_DIR/settings-snippet.json"
local target="$HOME_DIR/.claude/settings.json"
[ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; }
if [ ! -f "$target" ]; then
local tmp
tmp="$(mktemp "$target.XXXXXX")"
jq 'del(._comment)' "$snippet" > "$tmp"
mv "$tmp" "$target"
say "created $target from snippet (no prior settings.json)"
return 0
fi
backup_file "$target"
_jq_merge_hooks "$snippet" "$target"
}
# Flag-or-prompt dispatcher, mirroring the v0.15 behavior:
# --activate-hooks → always activate, no prompt
# no existing settings.json → activate silently (drop in snippet)
# TTY stdin+stdout → interactive [y/N] prompt
# otherwise → skip (manual-merge hint printed by summary)
# Sets global DID_ACTIVATE=1 when activation ran + succeeded.
maybe_activate_hooks() {
local 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
say "no existing settings.json; installing snippet"
activate_hooks && DID_ACTIVATE=1
elif [ -t 0 ] && [ -t 1 ]; then
if [ "$COLOR" = "1" ]; then
printf '\033[1;36m[install]\033[0m activate hooks now? [y/N] '
else
printf '[install] activate hooks now? [y/N] '
fi
local reply
read -r reply
case "$reply" in
y|Y|yes|YES) activate_hooks && DID_ACTIVATE=1 ;;
*) say "skipping hook activation" ;;
esac
fi
}