#!/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 # # 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 whose `source` is neither a local path string nor a # remote source object. Only those two shapes are classifiable: the walk below # takes the string ones, and the object ones are remote. Anything else -- absent # (`null`), a number, an array, a boolean -- is neither, so it silently drops out # of every marketplace-derived work list: this walk's and, through it, # sync-plugin-content.sh --all's. # # The check is deliberately typed as "not string AND not object" rather than # enumerating `.source == null`. Rejecting null specifically left every other # malformed value (`"source": 42`, `"source": []`) passing the assert, skipped by # the walk below, AND rescued by check-manifests.sh's disk -> marketplace name # axis -- i.e. exactly the defect the null case was fixed for, reached with a # different value. # # 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" root_type plugins_type unclassifiable 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 # `jq empty` passes on any valid JSON document, including `[]`, `"x"` and `123`. # The `.plugins` lookup on the next line then aborts with a raw # `jq: error: Cannot index array with string "plugins"` and rc=5, attributed to # nothing -- so assert the root shape here, where it can be named. root_type="$(jq -r 'type' "$marketplace")" if [[ "$root_type" != "object" ]]; then echo "Error: $marketplace is a JSON $root_type at its top level; expected an object with a \`plugins\` array. 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 unclassifiable="$(jq -r '[.plugins[]? | select((.source | type) as $t | $t != "string" and $t != "object") | "\(.name // "") (source: \(.source | type))"] | join(", ")' "$marketplace")" if [[ -n "$unclassifiable" ]]; then echo "Error: $marketplace has entries whose \`source\` is neither a local path string nor a remote source object: $unclassifiable. Such an entry is neither local nor remote, so it is skipped by every marketplace-derived check while still claiming its name. Fix it in root apm.yml's marketplace.packages[] and recompile." >&2 exit 1 fi } # list_marketplace_local_plugins # # Prints one "\t" 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 } # list_marketplace_remote_plugin_names # # Prints the `name` of every REMOTE (object `source:`) marketplace entry, one per # line -- the exact complement of list_marketplace_local_plugins. # # check-manifests.sh's disk -> marketplace pass uses it as its name axis: a plugin # vendored on disk but declared with the remote-object shape has no local entry to # path-match against, so without a name match it would be reported as unlisted when # its entry is in fact right there. # # The select is `(.source | type) == "object"`, an allowlist of the one shape that # axis is actually for -- NOT the denylist `(.source | type) != "string"` it used to # be. That denylist was true for `null` (and for numbers, arrays, booleans), so a # malformed entry marked its same-named directory "listed" while the local walk above # skipped it for lacking a string source: one bad entry disabled BOTH directions of # the check at once. assert_marketplace_manifest_usable rejects those shapes too, but # this function must be correct on its own -- it is called from a different script, # and a precondition that stops running is not a property of this select. list_marketplace_remote_plugin_names() { local marketplace="$1" jq -r '.plugins[]? | select((.source | type) == "object") | .name // empty' "$marketplace" }