Skip to content

Commit a121405

Browse files
laurigatesclaude
andauthored
feat(just): add domain-scoped bulk plugin recipes (#324)
## What Adds an orchestration layer over the existing per-repo plugin-config judgment skills so Claude plugins can be **installed / checked / re-evaluated across many repos**, scoped by domain (`fvh` | `laurigates` | `all`), with an `fzf --multi` checkbox picker so each run's repo set is chosen explicitly (no surprises). The plumbing already existed (`plugins-audit` whole-tree, `plugins-sync-repo` uniform-canonical, `plugins-setup-repo`/`-check-repo` cwd-bound). The gap was the missing enumeration → picker → batch-loop layer over those skills, plus a deterministic alias post-pass. This builds only that layer and reuses everything else. ## Recipes (in `private_dot_config/just/plugins.just`) | Recipe | Role | |---|---| | `_plugins-repos scope` | Guarded domain-scope enumerator. Guards on `-e "$e/.git"` (dir **or** worktree file), **not** `git rev-parse` — `~/repos` is itself a git repo, so rev-parse would falsely pass `archive/`, `tmp/`, `io.fvh.io/`, … A non-repo carrying `.claude/settings.json` (e.g. FVH `ezpolls`) is FLAGged on stderr and skipped. | | `_plugins-annotate repo` | `jq` state annotator → one `path<TAB>summary` line (enabled/lauri counts, alias + `ok`/`DRIFT`, marketplace key, workflows, `tracked`/`gitignored`) for the fzf row, `--preview`, and the `check` report. | | `plugins-normalize-alias repo` | Deterministic post-pass reconciling every laurigates alias to `laurigates-claude-plugins`. settings.json keyed on `source.repo == laurigates/claude-plugins` (robust to whatever alias a skill wrote); workflows via a `perl` lookahead that **spares `@claude-plugins-official`**. Idempotent; edits local files even if gitignored. | | `plugins-bulk scope action="check"` | Main driver: enumerate → annotate → fzf picker → **sequential** per-repo action → alias post-pass after any write. Never commits. `check` = local jq (read-only); `setup` = `/configure-claude-plugins --fix`; `reeval` = `/health:check --scope=stack --fix`; `full` = `/configure:repo --fix`. | | `plugins-audit-scope scope` | Scoped drift audit sharing a new private `_plugins-audit-roots` body; `plugins-audit` now delegates to it for the whole-tree variant (behaviour-preserving). | `reeval` targets `/health:check --scope=stack --fix` because `/health:audit` is unreachable (`health-audit` is `user-invocable: false`); `health-check --scope=stack` reaches its fix flow. ## Verification - `just -g _plugins-repos fvh` → only real repos; `ezpolls` FLAGged on stderr; `archive`/`tmp`/`io.fvh.io` absent. - `just -g _plugins-annotate ~/repos/laurigates/gitops` → reports `lgates-claude-plugins` alias as **DRIFT**. - `plugins-normalize-alias` on gitops rewrites key + suffixes; re-run → "already canonical" (idempotent), exit 0. On pal-mcp-server, the 6 `@claude-plugins-official` LSP pins are **byte-identical** after (untouched); the `claude-plugins`/`lgates-claude-plugins` laurigates pins are normalized. - `plugins-audit` (whole tree, refactored) scanned 5 = `laurigates` 2 + `fvh` 3 — exact union. - Pre-commit "check justfiles parse and evaluate" passes. ## Caveat `reeval`'s `/health:check` fix flow uses per-category `AskUserQuestion`; under `-p` there is no operator, so it may no-op — documented in the recipe as best-run-interactively. `setup` (`--fix`) is reliably non-interactive. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014xTWB98DPccsiCpkbbpzde Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5cdd917 commit a121405

1 file changed

Lines changed: 201 additions & 20 deletions

File tree

private_dot_config/just/plugins.just

Lines changed: 201 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,198 @@ plugins-check-repo:
9393
@echo "{{BLUE}}Checking Claude plugin configuration for repository at $(pwd)...{{NORMAL}}"
9494
CLAUDECODE= claude -p "/configure-claude-plugins --check-only" --model {{claude_model}}
9595

96-
# Audit committed project plugin pins for drift vs canonical (read-only)
96+
# Audit committed project plugin pins for drift vs canonical over the whole ~/repos tree (read-only)
9797
[group: "plugins"]
98-
plugins-audit:
98+
plugins-audit: (_plugins-audit-roots (env_var('HOME') / "repos"))
99+
100+
# Rewrite one repo's committed enabledPlugins from canonical (shows diff first)
101+
[group: "plugins"]
102+
plugins-sync-repo repo:
103+
#!/usr/bin/env bash
104+
# Canonical = ~/.claude/settings.json. Shows the enabledPlugins diff, then
105+
# writes in place (jq normalizes JSON formatting). Reversible — review with
106+
# `git -C <repo> diff`.
107+
set -uo pipefail
108+
canonical="$HOME/.claude/settings.json"
109+
target="{{repo}}"
110+
case "$target" in */.claude/settings.json) : ;; *) target="${target%/}/.claude/settings.json" ;; esac
111+
if [ ! -f "$canonical" ]; then echo "{{YELLOW}}No $canonical{{NORMAL}}"; exit 1; fi
112+
if [ ! -f "$target" ]; then echo "{{YELLOW}}No $target{{NORMAL}}"; exit 1; fi
113+
canon_ep="$(jq '.enabledPlugins // {}' "$canonical")"
114+
new="$(jq --argjson ep "$canon_ep" '.enabledPlugins = $ep' "$target")"
115+
if [ "$new" = "$(cat "$target")" ]; then echo "{{GREEN}}Already canonical: $target{{NORMAL}}"; exit 0; fi
116+
echo "{{BLUE}}enabledPlugins changes for $target:{{NORMAL}}"
117+
diff <(jq -S '.enabledPlugins // {}' "$target") <(printf '%s' "$canon_ep" | jq -S '.') || true
118+
printf '%s\n' "$new" > "$target"
119+
echo "{{GREEN}}Wrote $target — review with: git -C \"$(dirname "$(dirname "$target")")\" diff{{NORMAL}}"
120+
121+
# ── Domain-scoped bulk install/check/re-eval across repos ─────────────────────
122+
# Orchestration layer over the per-repo judgment skills (configure-claude-plugins,
123+
# health-check --scope=stack, configure:repo). Enumerate a domain scope → fzf
124+
# --multi checkbox picker → sequential per-repo action → deterministic alias
125+
# post-pass. AI-free orchestration; per-repo judgment still runs `claude -p`.
126+
127+
# Enumerate guarded repo paths for a domain scope (fvh|laurigates|all)
128+
[private]
129+
_plugins-repos scope:
130+
#!/usr/bin/env bash
131+
set -uo pipefail
132+
case "{{scope}}" in
133+
fvh) dirs=("$HOME/repos/ForumViriumHelsinki") ;;
134+
laurigates) dirs=("$HOME/repos/laurigates") ;;
135+
all) dirs=("$HOME/repos/ForumViriumHelsinki" "$HOME/repos/laurigates") ;;
136+
*) echo "{{YELLOW}}Unknown scope: {{scope}} (use fvh|laurigates|all){{NORMAL}}" >&2; exit 2 ;;
137+
esac
138+
# Guard on `.git` (dir OR worktree file), NOT `git rev-parse` — ~/repos is
139+
# itself a git repo, so rev-parse falsely passes non-repos (archive/, tmp/, …).
140+
for d in "${dirs[@]}"; do
141+
[ -d "$d" ] || continue
142+
for e in "$d"/*/; do e="${e%/}"
143+
if [ -e "$e/.git" ]; then printf '%s\n' "$e"
144+
elif [ -f "$e/.claude/settings.json" ]; then
145+
printf '{{YELLOW}}FLAG{{NORMAL}} non-repo with .claude config, skipped: %s\n' "$e" >&2
146+
fi
147+
done
148+
done
149+
150+
# Annotate one repo's plugin-config state as `path<TAB>summary` (fzf row + preview + check report)
151+
[private]
152+
_plugins-annotate repo:
153+
#!/usr/bin/env bash
154+
set -uo pipefail
155+
repo="{{repo}}"; repo="${repo%/}"
156+
s="$repo/.claude/settings.json"
157+
if [ ! -f "$s" ]; then printf '%s\tsettings=MISSING\n' "$repo"; exit 0; fi
158+
# Alias allowlist for laurigates/claude-plugins (EXCLUDES claude-plugins-official,
159+
# a different marketplace). Alias = the segment after the last @ in each key.
160+
allow_re='^(laurigates-claude-plugins|claude-plugins|lgates-claude-plugins|laurigates-plugins)$'
161+
mapfile -t all_aliases < <(jq -r '(.enabledPlugins // {}) | keys[] | split("@") | .[-1]' "$s" 2>/dev/null)
162+
total=${#all_aliases[@]}
163+
lauri_n=0; declare -A seen
164+
for a in "${all_aliases[@]}"; do
165+
if [[ "$a" =~ $allow_re ]]; then lauri_n=$((lauri_n + 1)); seen["$a"]=1; fi
166+
done
167+
if [ "$lauri_n" -eq 0 ]; then status="none"
168+
else
169+
drift=0; for a in "${!seen[@]}"; do [ "$a" != "laurigates-claude-plugins" ] && drift=1; done
170+
if [ "$drift" -eq 1 ]; then status="{{YELLOW}}DRIFT{{NORMAL}}"; else status="ok"; fi
171+
fi
172+
mp_keys="$(jq -r '.extraKnownMarketplaces // {} | to_entries | map(select(.value.source.repo == "laurigates/claude-plugins")) | .[].key' "$s" 2>/dev/null)"
173+
wf=(); for f in claude.yml claude-review.yml claude-code-review.yml; do
174+
[ -f "$repo/.github/workflows/$f" ] && wf+=("$f")
175+
done
176+
track="tracked"
177+
git -C "$repo" check-ignore -q .claude/settings.json 2>/dev/null && track="gitignored"
178+
ann="enabled=${total} lauri=${lauri_n}"
179+
[ "${#seen[@]}" -gt 0 ] && ann="$ann aliases=$(printf '%s\n' "${!seen[@]}" | sort | paste -sd, -)"
180+
ann="$ann $status"
181+
[ -n "$mp_keys" ] && ann="$ann mp=$(printf '%s' "$mp_keys" | paste -sd, -)"
182+
[ "${#wf[@]}" -gt 0 ] && ann="$ann wf=$(IFS=,; echo "${wf[*]}")"
183+
ann="$ann $track"
184+
printf '%s\t%s\n' "$repo" "$ann"
185+
186+
# Normalize every laurigates alias to laurigates-claude-plugins (idempotent; edits local files even if gitignored)
187+
[group: "plugins"]
188+
plugins-normalize-alias repo:
189+
#!/usr/bin/env bash
190+
# Deterministic post-pass after any skill write (the skill may write the
191+
# `claude-plugins` alias; this reconciles it). Two parts: settings.json (jq,
192+
# keyed on source.repo — robust to whatever alias the skill wrote) and
193+
# workflows (perl). Shows a diff; re-run is a no-op ("already canonical").
194+
set -uo pipefail
195+
repo="{{repo}}"; repo="${repo%/}"
196+
canon="laurigates-claude-plugins"
197+
changed=0
198+
# --- settings.json -----------------------------------------------------------
199+
s="$repo/.claude/settings.json"
200+
if [ -f "$s" ]; then
201+
new="$(jq --arg canon "$canon" '
202+
["laurigates-claude-plugins","claude-plugins","lgates-claude-plugins","laurigates-plugins"] as $allow
203+
| (.extraKnownMarketplaces // {}) as $m
204+
| (if ($m | length) > 0 then
205+
.extraKnownMarketplaces = ($m | to_entries
206+
| map(if (.value.source.repo == "laurigates/claude-plugins") then .key = $canon else . end)
207+
| from_entries)
208+
else . end)
209+
| .enabledPlugins = ((.enabledPlugins // {}) | to_entries
210+
| map((.key | split("@")) as $p
211+
| if (($p | length) > 1) and ($allow | index($p[-1]))
212+
then .key = (($p[:-1] | join("@")) + "@" + $canon) else . end)
213+
| from_entries)
214+
' "$s" 2>/dev/null)"
215+
if [ -n "$new" ] && [ "$(printf '%s' "$new" | jq -S .)" != "$(jq -S . "$s")" ]; then
216+
echo "{{BLUE}}settings.json alias changes for $repo:{{NORMAL}}"
217+
diff <(jq -S '{extraKnownMarketplaces, enabledPlugins}' "$s") <(printf '%s' "$new" | jq -S '{extraKnownMarketplaces, enabledPlugins}') || true
218+
printf '%s\n' "$new" > "$s"
219+
changed=1
220+
if git -C "$repo" check-ignore -q .claude/settings.json 2>/dev/null; then
221+
echo "{{YELLOW}}note:{{NORMAL}} settings.json is gitignored — local-only, nothing to stage."
222+
fi
223+
fi
224+
fi
225+
# --- workflows ---------------------------------------------------------------
226+
# The (?![-\w]) lookahead is load-bearing: it protects @claude-plugins-official
227+
# and never rewrites the already-canonical @laurigates-claude-plugins.
228+
for f in claude.yml claude-review.yml claude-code-review.yml; do
229+
wfp="$repo/.github/workflows/$f"
230+
[ -f "$wfp" ] || continue
231+
new="$(perl -0pe 's/@(?:claude-plugins|lgates-claude-plugins|laurigates-plugins)(?![-\w])/@laurigates-claude-plugins/g' "$wfp")"
232+
if [ "$new" != "$(cat "$wfp")" ]; then
233+
echo "{{BLUE}}workflow $f alias changes for $repo:{{NORMAL}}"
234+
diff <(cat "$wfp") <(printf '%s' "$new") || true
235+
printf '%s' "$new" > "$wfp"
236+
changed=1
237+
fi
238+
done
239+
if [ "$changed" -eq 0 ]; then echo "{{GREEN}}Already canonical: $repo{{NORMAL}}"; fi
240+
241+
# Bulk plugin check/setup/re-eval across a domain scope via an fzf checkbox picker (never commits)
242+
[group: "plugins"]
243+
plugins-bulk scope action="check":
244+
#!/usr/bin/env bash
245+
# actions: check (local jq annotation, read-only) | setup (/configure-claude-plugins
246+
# --fix) | reeval (/health:check --scope=stack --fix) | full (/configure:repo --fix).
247+
# Sequential (sibling claude subprocesses are rate-limited & per-repo expensive);
248+
# every claude call gets </dev/null so the picker loop never eats its stdin.
249+
# NOTE: reeval's fix flow uses per-category AskUserQuestion; under `-p` there is
250+
# no operator, so it may no-op — best run interactively. setup (--fix) is reliably
251+
# non-interactive.
252+
set -uo pipefail
253+
case "{{action}}" in check|setup|reeval|full) : ;; *) echo "{{YELLOW}}Unknown action: {{action}} (use check|setup|reeval|full){{NORMAL}}" >&2; exit 2 ;; esac
254+
mapfile -t repos < <(just -g _plugins-repos {{scope}})
255+
if [ "${#repos[@]}" -eq 0 ]; then echo "{{YELLOW}}No repos for scope {{scope}}.{{NORMAL}}"; exit 0; fi
256+
list=""
257+
for r in "${repos[@]}"; do list="$list$(just -g _plugins-annotate "$r" || true)"$'\n'; done
258+
selected="$(printf '%s' "$list" | fzf --multi --ansi --delimiter='\t' --with-nth=1.. \
259+
--preview 'just -g _plugins-annotate {1}' --preview-window=up,3,wrap \
260+
--header 'TAB=mark ENTER=run ESC=abort — action={{action}} scope={{scope}}')" || exit 0
261+
[ -z "$selected" ] && { echo "Nothing selected."; exit 0; }
262+
mapfile -t picked <<< "$selected"
263+
ok=0; failed=0
264+
for line in "${picked[@]}"; do
265+
[ -z "$line" ] && continue
266+
repo="${line%%$'\t'*}"
267+
echo ""; echo "{{BLUE}}=== {{action}} :: $repo ==={{NORMAL}}"
268+
rc=0
269+
case "{{action}}" in
270+
check) just -g _plugins-annotate "$repo" ;;
271+
setup) CLAUDECODE= claude -p "/configure-claude-plugins --fix" --model {{claude_model}} --permission-mode auto </dev/null || rc=$? ;;
272+
reeval) CLAUDECODE= claude -p "/health:check --scope=stack --fix" --model {{claude_model}} --permission-mode auto </dev/null || rc=$? ;;
273+
full) CLAUDECODE= claude -p "/configure:repo --fix" --model {{claude_model}} --permission-mode auto </dev/null || rc=$? ;;
274+
esac
275+
if [ "$rc" -eq 0 ]; then echo "{{GREEN}}ok{{NORMAL}}"; ok=$((ok + 1)); else echo "{{YELLOW}}FAILED (rc=$rc){{NORMAL}}"; failed=$((failed + 1)); fi
276+
case "{{action}}" in
277+
setup|reeval|full)
278+
just -g plugins-normalize-alias "$repo"
279+
echo "Review: git -C \"$repo\" diff"
280+
;;
281+
esac
282+
done
283+
echo ""; echo "{{GREEN}}$ok ok, $failed failed.{{NORMAL}}"
284+
285+
# Shared drift-audit body over one-or-more root dirs (used by plugins-audit + plugins-audit-scope)
286+
[private]
287+
_plugins-audit-roots +roots:
99288
#!/usr/bin/env bash
100289
# Canonical = the @laurigates-claude-plugins keys in ~/.claude/settings.json.
101290
# Compares ONLY that marketplace's keys (official LSPs and other-marketplace
@@ -136,28 +325,20 @@ plugins-audit:
136325
[ -n "$missing" ] && { echo " missing (canonical, not pinned):"; printf ' - %s\n' $missing; }
137326
[ -n "$extra" ] && { echo " extra (pinned, not in canonical):"; printf ' + %s\n' $extra; }
138327
[ -n "$valdiff" ] && { echo " state drift (enabled differs from canonical):"; printf ' ~ %s\n' $valdiff; }
139-
done < <(find "$HOME/repos" -type f -path "*/.claude/settings.json" 2>/dev/null)
328+
done < <(find {{roots}} -type f -path "*/.claude/settings.json" 2>/dev/null)
140329
echo ""
141330
echo "{{GREEN}}Scanned $found exhaustive-pinned repo(s); $drifted drifted.{{NORMAL}}"
142331
[ "$drifted" -gt 0 ] && echo "Bring one to canonical: just plugins-sync-repo <repo-path>"
143332
144-
# Rewrite one repo's committed enabledPlugins from canonical (shows diff first)
333+
# Audit committed project plugin pins for drift vs canonical over one domain scope (read-only)
145334
[group: "plugins"]
146-
plugins-sync-repo repo:
335+
plugins-audit-scope scope:
147336
#!/usr/bin/env bash
148-
# Canonical = ~/.claude/settings.json. Shows the enabledPlugins diff, then
149-
# writes in place (jq normalizes JSON formatting). Reversible — review with
150-
# `git -C <repo> diff`.
151337
set -uo pipefail
152-
canonical="$HOME/.claude/settings.json"
153-
target="{{repo}}"
154-
case "$target" in */.claude/settings.json) : ;; *) target="${target%/}/.claude/settings.json" ;; esac
155-
if [ ! -f "$canonical" ]; then echo "{{YELLOW}}No $canonical{{NORMAL}}"; exit 1; fi
156-
if [ ! -f "$target" ]; then echo "{{YELLOW}}No $target{{NORMAL}}"; exit 1; fi
157-
canon_ep="$(jq '.enabledPlugins // {}' "$canonical")"
158-
new="$(jq --argjson ep "$canon_ep" '.enabledPlugins = $ep' "$target")"
159-
if [ "$new" = "$(cat "$target")" ]; then echo "{{GREEN}}Already canonical: $target{{NORMAL}}"; exit 0; fi
160-
echo "{{BLUE}}enabledPlugins changes for $target:{{NORMAL}}"
161-
diff <(jq -S '.enabledPlugins // {}' "$target") <(printf '%s' "$canon_ep" | jq -S '.') || true
162-
printf '%s\n' "$new" > "$target"
163-
echo "{{GREEN}}Wrote $target — review with: git -C \"$(dirname "$(dirname "$target")")\" diff{{NORMAL}}"
338+
case "{{scope}}" in
339+
fvh) roots=("$HOME/repos/ForumViriumHelsinki") ;;
340+
laurigates) roots=("$HOME/repos/laurigates") ;;
341+
all) roots=("$HOME/repos/ForumViriumHelsinki" "$HOME/repos/laurigates") ;;
342+
*) echo "{{YELLOW}}Unknown scope: {{scope}} (use fvh|laurigates|all){{NORMAL}}" >&2; exit 2 ;;
343+
esac
344+
just -g _plugins-audit-roots "${roots[@]}"

0 commit comments

Comments
 (0)