From af80650d5370687acf392d529f1aed83a6abba7c Mon Sep 17 00:00:00 2001 From: Lauri Gates Date: Tue, 28 Jul 2026 12:20:14 +0300 Subject: [PATCH] fix(just): bind empty assoc array; add plugins-reconcile-keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to the canonical plugins recipes. 1. `_plugins-annotate` aborted with "seen: unbound variable" for any repo with zero laurigates plugins. Under `set -u`, bash 5.3 treats a `declare -A seen` that is never assigned as UNBOUND, so `${#seen[@]}` fails — and it failed on exactly the repos an audit most needs to report (OLMap, kicad-mcp: a claude.yml present with nothing enabled). `declare -A seen=()` binds it. 2. New `plugins-reconcile-keys` — the safe half of `plugins-sync-repo`, for unattended use. It reconciles the key SET of an exhaustive pin against canonical (adds omitted canonical plugins, drops pins for plugins gone from the marketplace) while leaving each repo's own enable/disable VALUES alone. The distinction is load-bearing. A missing key is a real functional bug: enabledPlugins is a whole-map replacement, so a plugin omitted from an exhaustive pin is silently disabled in web/remote sessions. A differing value is not a bug — it is stack-specific and drifts in both directions on purpose (`infrastructure` keeps terraform-plugin and kubernetes-plugin ON where canonical has them OFF). Blanket `plugins-sync-repo` would disable exactly the plugins a repo needs, so it stays a deliberate, interactive operation. Refuses to run if the marketplace fetch fails, rather than treating an empty response as "every plugin was removed". Consumed by the new weekly ~/repos/.routines/plugins-audit.sh sweep. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012zrh72rhT9VtdDYKnjRyVh --- private_dot_config/just/plugins.just | 53 +++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/private_dot_config/just/plugins.just b/private_dot_config/just/plugins.just index 4f973fa..69952eb 100644 --- a/private_dot_config/just/plugins.just +++ b/private_dot_config/just/plugins.just @@ -118,6 +118,53 @@ plugins-sync-repo repo: printf '%s\n' "$new" > "$target" echo "{{GREEN}}Wrote $target — review with: git -C \"$(dirname "$(dirname "$target")")\" diff{{NORMAL}}" +# Reconcile one repo's exhaustive plugin key SET against canonical, preserving its own enable values +[group: "plugins"] +plugins-reconcile-keys repo: + #!/usr/bin/env bash + # The safe half of plugins-sync-repo, for unattended use (the weekly routine). + # Membership only: + # + add canonical @laurigates-claude-plugins keys the repo is MISSING, using + # canonical's value. This is the real functional bug: enabledPlugins is a + # WHOLE-MAP replacement, so a plugin omitted from an exhaustive pin is + # silently DISABLED in web/remote sessions. + # - drop keys pinned here that no longer exist in the live marketplace + # (renamed/removed upstream) — dead pins that can never resolve. + # + # Deliberately does NOT touch the value of a key present in both. Enable state + # is stack-specific and drifts in BOTH directions on purpose: `infrastructure` + # keeps terraform-plugin/kubernetes-plugin ON where canonical has them OFF. + # Auto-"fixing" that would disable exactly the plugins a repo needs. Value + # drift is reported by plugins-audit and resolved by a human. + # + # Applies only to EXHAUSTIVE pins (>= floor keys, same floor as plugins-audit); + # a deliberate narrow subset is left untouched. Idempotent. + set -uo pipefail + repo="{{repo}}"; repo="${repo%/}" + s="$repo/.claude/settings.json" + canonical="$HOME/.claude/settings.json" + mp="laurigates-claude-plugins" + floor=30 + if [ ! -f "$s" ]; then echo "{{YELLOW}}No $s{{NORMAL}}"; exit 1; fi + if [ ! -f "$canonical" ]; then echo "{{YELLOW}}No $canonical{{NORMAL}}"; exit 1; fi + n="$(jq -r --arg m "$mp" '[(.enabledPlugins // {}) | keys[] | select(endswith("@"+$m))] | length' "$s" 2>/dev/null)" + if [ -z "$n" ]; then echo "{{YELLOW}}Unreadable enabledPlugins: $s{{NORMAL}}"; exit 1; fi + if [ "$n" -lt "$floor" ]; then echo "{{GREEN}}Deliberate subset ($n < $floor) — untouched: $repo{{NORMAL}}"; exit 0; fi + live="$(curl -fsS "{{marketplace_url}}" | jq -c '[.plugins[].name]')" || live="" + if [ -z "$live" ] || [ "$live" = "[]" ]; then echo "{{YELLOW}}Marketplace fetch failed — refusing to reconcile (would look like every plugin was removed){{NORMAL}}"; exit 1; fi + new="$(jq --arg m "$mp" --argjson live "$live" --slurpfile c "$canonical" ' + ($live | map(. + "@" + $m)) as $liveKeys + | (($c[0].enabledPlugins // {}) | with_entries(select((.key | endswith("@"+$m)) and (.key | IN($liveKeys[]))))) as $Canon + | ((.enabledPlugins // {}) | with_entries(select((.key | endswith("@"+$m) | not) or (.key | IN($liveKeys[]))))) as $Kept + | .enabledPlugins = ($Canon + $Kept) + ' "$s" 2>/dev/null)" + if [ -z "$new" ]; then echo "{{YELLOW}}jq reconcile failed: $s{{NORMAL}}"; exit 1; fi + if [ "$(printf '%s' "$new" | jq -S .)" = "$(jq -S . "$s")" ]; then echo "{{GREEN}}Key set already canonical: $repo{{NORMAL}}"; exit 0; fi + echo "{{BLUE}}enabledPlugins key-set changes for $repo:{{NORMAL}}" + diff <(jq -S '.enabledPlugins // {}' "$s") <(printf '%s' "$new" | jq -S '.enabledPlugins') || true + printf '%s\n' "$new" > "$s" + echo "{{GREEN}}Wrote $s — review with: git -C \"$repo\" diff{{NORMAL}}" + # ── Domain-scoped bulk install/check/re-eval across repos ───────────────────── # Orchestration layer over the per-repo judgment skills (configure-claude-plugins, # health-check --scope=stack, configure:repo). Enumerate a domain scope → fzf @@ -160,7 +207,11 @@ _plugins-annotate repo: allow_re='^(laurigates-claude-plugins|claude-plugins|lgates-claude-plugins|laurigates-plugins)$' mapfile -t all_aliases < <(jq -r '(.enabledPlugins // {}) | keys[] | split("@") | .[-1]' "$s" 2>/dev/null) total=${#all_aliases[@]} - lauri_n=0; declare -A seen + # The `=()` is load-bearing: under `set -u`, bash treats a declared-but-never- + # assigned associative array as UNBOUND, so `${#seen[@]}` below aborts the + # recipe ("seen: unbound variable") for any repo with zero laurigates plugins + # — exactly the repos an audit most needs to report (OLMap, kicad-mcp). + lauri_n=0; declare -A seen=() for a in "${all_aliases[@]}"; do if [[ "$a" =~ $allow_re ]]; then lauri_n=$((lauri_n + 1)); seen["$a"]=1; fi done