Files
holocron/scripts/lib/marketplace-plugins.sh
Defame1297 3f1ee47f1e fix(scripts): stop check-manifests passing on entries it cannot parse
A marketplace entry missing its source key disabled both directions of the
check at once. The helper required source to be a string, so a source-less
entry was skipped and its plugin.json existence check never ran; the name axis
selected on (.source | type) != "string", and null != "string" is true, so the
same entry also marked its on-disk directory as listed. Delete source from an
entry and delete its plugin.json and the script exited 0. Because
sync-plugin-content.sh --all derives its work list from the same helper, that
plugin silently dropped out of the content-mirror gate too.

Also in this pass:
- a wrongly typed skills value crashed the script mid-loop with a raw jq error
  and no "Manifest check failed:" line, leaving every later plugin unchecked.
  Note skills is legally string|string[] per both host schemas, so a string
  now resolves as a single path rather than erroring
- array- and object-valued pointer fields were reported missing even when they
  resolved, because the whole JSON value was pretty-printed into a path test
- an unparseable marketplace.json died inside a process substitution, so the
  run reported six "no entry in marketplace.json" errors that sent the reader
  to edit apm.yml when the real fault was a corrupt manifest
- a missing marketplace.json exited 0 even with plugin directories present

Tests: 14 -> 23 assertions. Every failure case asserts on message text, not
exit code alone, since exit 1 here is reachable by several causes that call
for opposite fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 11:03:57 +00:00

67 lines
3.4 KiB
Bash

#!/usr/bin/env bash
# Shared helper: enumerates the local (string-source) plugin entries declared in
# .claude-plugin/marketplace.json. Sourced by scripts/sync-plugin-content.sh
# (--all) and scripts/check-manifests.sh so a future marketplace.json schema
# change (e.g. a new source type) only has to be handled in one place instead
# of drifting between two hand-maintained copies of the same walk.
#
# Requires jq. Not meant to be executed directly -- source it.
# assert_marketplace_manifest_usable <marketplace_json_path>
#
# Checks the preconditions list_marketplace_local_plugins depends on but cannot
# report on. Both callers run the walk inside a process substitution
# (`done < <(list_marketplace_local_plugins ...)`), which is its own subshell: a
# jq abort in there kills only that subshell, so an unparseable manifest yields
# zero lines and reads exactly like "this marketplace declares no local plugins".
# The caller then blames whatever its empty-set branch blames -- for
# check-manifests.sh, every plugin directory on disk being unlisted.
#
# It also rejects an entry with no `source` at all. Such an entry is not a local
# plugin (the walk below requires a string `source`) and not a remote one either,
# so it silently drops out of every marketplace-derived work list -- this walk's
# and, through it, sync-plugin-content.sh --all's.
#
# Exits 1 with a specific message on any violation, so call it from the caller's
# own shell -- never inside `< <(...)`, which is the exact swallowing this guards.
assert_marketplace_manifest_usable() {
local marketplace="$1" plugins_type sourceless
if ! jq empty "$marketplace" >/dev/null 2>&1; then
echo "Error: $marketplace is not valid JSON -- every marketplace-derived check reads as \"no plugins declared\" until it parses. Fix it, or recompile it with \`apm pack\`." >&2
exit 1
fi
plugins_type="$(jq -r '.plugins | type' "$marketplace")"
if [[ "$plugins_type" != "array" && "$plugins_type" != "null" ]]; then
echo "Error: $marketplace has a \`plugins\` field of type $plugins_type; expected an array of plugin entries." >&2
exit 1
fi
sourceless="$(jq -r '[.plugins[]? | select(.source == null) | .name // "<unnamed>"] | join(", ")' "$marketplace")"
if [[ -n "$sourceless" ]]; then
echo "Error: $marketplace has entries with no \`source\` field: $sourceless. An entry without a \`source\` is neither local nor remote, so it is skipped by every marketplace-derived check while still claiming its name. Give it a \`source\` in root apm.yml's marketplace.packages[] and recompile." >&2
exit 1
fi
}
# list_marketplace_local_plugins <repo_root> <marketplace_json_path>
#
# Prints one "<name>\t<absolute_plugin_dir>" line per local (string `source:`)
# marketplace entry. Remote sources (github/git/npm objects) have no local
# directory to walk and are skipped, matching both callers' prior behavior.
list_marketplace_local_plugins() {
local repo_root="$1" marketplace="$2"
local plugin_count i name source_type source
plugin_count="$(jq '.plugins | length' "$marketplace")"
for ((i = 0; i < plugin_count; i++)); do
source_type="$(jq -r ".plugins[$i].source | type" "$marketplace")"
[[ "$source_type" == "string" ]] || continue
name="$(jq -r ".plugins[$i].name" "$marketplace")"
source="$(jq -r ".plugins[$i].source" "$marketplace")"
source="${source#./}"
printf '%s\t%s\n' "$name" "$repo_root/$source"
done
}