gstack skills hardcode ~/.claude/skills/gstack/<path> for 83 shared assets (bin, scripts/jargon-list.json, ETHOS.md, */sections, review/specialists, make-pdf/dist, lib/diagram-render/dist, freeze/bin...) but only bin and browse/dist were linked: make-pdf and diagram failed on every run, cso and plan-*-review could not read their sections, the freeze hook exited 127. lib/gstack-links.sh links every top-level entry except SKILL.md, skips non-skill dirs holding a nested SKILL.md (browser-skills, openclaw, node_modules), removes the global symlink gstack ./setup plants and refuses a destination inside the submodule. link.sh, install-plugins.sh and update-all.sh all call it (three hand-copied blocks gone). doctor.sh counted 34 skills (find without -L) and zero chars for block scalar descriptions; lib/doctor-skills.sh reuses the census parser and counts through the symlinks. Plugin constants re-based on measured values; install-plugins.sh notes why frontend-design@claude-plugins-official and brightdata-plugin@synced stay off and describes security-guidance truthfully.
63 lines
2.4 KiB
Bash
63 lines
2.4 KiB
Bash
#!/usr/bin/env bash
|
|
# ============================================================
|
|
# lib/doctor-skills.sh — doctor.sh's skill-catalog stats
|
|
#
|
|
# `skill_catalog_stats <skills_dir>` counts every skill reachable
|
|
# through <skills_dir>/*/SKILL.md (symlinks included — Python's
|
|
# glob.glob follows them, verified: a symlinked skill dir matches the
|
|
# pattern the same as a real one) and sums their description length,
|
|
# reusing lib/skill-routing-census.py's extract_description() (handles
|
|
# a plain scalar AND a `|`/`>` YAML block scalar) instead of doctor.sh's
|
|
# old `grep '^description:' | head -1` (0 chars on every block-scalar
|
|
# description) and its `find -maxdepth 2` skill count (missed
|
|
# symlinked skill dirs without `-L`).
|
|
#
|
|
# Prints "<count> <desc_chars>" on stdout and ALWAYS exits 0 — an
|
|
# absent <skills_dir> naturally globs to nothing (0 0, no error); a
|
|
# python failure also prints "0 0" so doctor.sh (which runs under
|
|
# `set -euo pipefail`) never aborts on this check, PLUS one warn line on
|
|
# stderr so a real failure still shows instead of reading as a healthy
|
|
# empty catalog. The warn goes to stderr explicitly (not just via the
|
|
# caller's own warn() convention) because the caller reads this
|
|
# function's stdout with `read -r … < <(skill_catalog_stats …)` — any
|
|
# extra stdout line would corrupt that capture.
|
|
#
|
|
# No `set -euo pipefail` here (mirrors lib/vendor-skills.sh): a sourced
|
|
# lib must not change the caller's shell options.
|
|
# ============================================================
|
|
|
|
DOCTOR_SKILLS_LIB_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
if ! declare -F warn >/dev/null 2>&1; then
|
|
YELLOW='\033[1;33m'; NC='\033[0m'
|
|
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
|
fi
|
|
|
|
# skill_catalog_stats <skills_dir> — see file header.
|
|
skill_catalog_stats() {
|
|
local dir="$1" census="$DOCTOR_SKILLS_LIB_DIR/skill-routing-census.py"
|
|
local out rc
|
|
out=$(python3 - "$dir" "$census" <<'PY'
|
|
import glob, importlib.util, sys
|
|
|
|
skills_dir, census_path = sys.argv[1], sys.argv[2]
|
|
spec = importlib.util.spec_from_file_location(
|
|
"skill_routing_census", census_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
paths = glob.glob(skills_dir + "/*/SKILL.md")
|
|
chars = sum(len(module.extract_description(p) or "") for p in paths)
|
|
print(len(paths), chars)
|
|
PY
|
|
)
|
|
rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
echo "$out"
|
|
return 0
|
|
fi
|
|
warn "skill_catalog_stats: python failed (rc=$rc) — showing 0 0" >&2
|
|
echo "0 0"
|
|
return 0
|
|
}
|