feat(marketplace): make the skill a gateway and add marketplace add - #89
Merged
Merged
Conversation
A catalog can list plugins that live outside the repository — an npm package, a GitHub repo. Discovery walks directories, so it produced none of them, and a regeneration that trusted discovery alone reported every one as a deletion, which `--force` then carried out. Two things now survive a re-derivation. An entry whose source is not local stays even though discovery never saw it, and a discovered plugin whose entry names a non-local source keeps that source with only its derived metadata refreshed. What discovery owns it still owns: a local-path entry it no longer finds is dropped, so the catalog keeps mirroring the repository for the part of it discovery describes. The same rule applies at the other two places an entry is re-derived, `plugin build`'s refresh and `plugin init`'s registration, which is the first half of #86 — a plugin shipped through npm, whose repository path holds only gitignored build output, was rewritten to that path on every build. This is what lets `marketplace add` (#88) coexist with `marketplace init` on one repository. Refs #88, #86 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…where `marketplace init` derives a catalog from the plugins a repository holds. A repository that curates plugins living somewhere else had no command at all, so the entry had to be hand-written into a catalog — which the marketplace skill forbids, because a catalog assembled by hand carries the shapes every runtime refuses. `marketplace add <spec>` writes that entry. One positional names where the plugin lives, disambiguated by shape with a flag to override every guess: ./plugins/alpha a path in this repository owner/repo a GitHub repository https://... , git@... a git URL npm:pkg , @scope/pkg an npm package plugin@marketplace an entry another marketplace already publishes The last form has no source type in the schema, so it is resolved and copied: the marketplaces the runtime has already added are read from `~/.claude/plugins`, with `--from` for one that is not installed. Nothing is fetched. Metadata comes from the plugin manifest at a local path, from an installed `node_modules` copy for a package, from the entry being copied, and otherwise from the metadata flags. An entry only reaches a catalog whose runtime can resolve it: npm goes to Claude Code and Codex, and Copilot CLI and Cursor are reported skipped with the reason rather than written a source they refuse. Replacing an entry already listed differently needs `--force`. Closes #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skill documented one job: derive a catalog from the plugins this
repository holds. `marketplace add` is a second job with different
inputs, a different failure mode, and almost no steps in common — a
single document would have had to hedge every instruction with "unless
the plugin is not in this repository".
SKILL.md now classifies and routes, holding no procedure of its own. It
declares the routes in `argument-hint`, carries the intake, the runtime
table, and the rules that apply to every route, and names one reference
per route:
init references/init.md (today's steps, moved)
add references/add.md (new)
validate references/validate.md (was the last step of init; both
writing routes end there, so it
has one home rather than two)
One user-invocable skill with routes, rather than splitting into
marketplace-init and marketplace-add skills that would compete to
trigger on the same phrases.
scripts/add.mjs runs the new command from the CLI shipped beside the
skill, matching the other three wrappers.
Refs #88
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the `add/` behavioral node — spec reading, the per-runtime source support that decides what is skipped, metadata with no fetch, resolving `<plugin>@<marketplace>`, and the write guards — plus its frozen scenario suite and the group index entry. Records the preservation rule in `init/`: which entries a regeneration keeps, why folding happens after the containment check, and that a result row names what the catalog ends up listing rather than only what discovery contributed. Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The local-marketplace page described two commands that write catalogs. `marketplace add` is a third, and the page had no account of listing a plugin the repository does not hold. Adds its section — the spec forms and the two collisions that need a flag, the per-runtime source support and what a skip means, where metadata comes from now that nothing is fetched, and how `<plugin>@<marketplace>` resolves — and notes on the `init` and build sections that a non-local source survives a regeneration. Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
knip flagged both as dead: claudePluginsHome has one caller in its own file, and SOURCE_KINDS was never read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…igin
Copying an entry out of another marketplace refused a `./` source. The
reasoning was sound — that path resolves against a root this repository
does not share — but the conclusion was wrong. It still names a location
inside a repository whose URL is known, which is what `git-subdir`
states, so the path can be rewritten instead of rejected.
./plugins/aced in cyberuni/cyberplace
-> git-subdir https://github.com/cyberuni/cyberplace.git plugins/aced
./ in unional/skills
-> github unional/skills
A plugin at the marketplace root needs no subdirectory, so it takes the
plainer `github` form, or `url` where the origin names no owner/repo.
The origin is what the runtime recorded for that marketplace — a github
repo slug or a git clone URL — falling back to the clone's own origin
remote, which is also how a `--from` directory supplies one. Only a
marketplace with neither still stops the run, and it now says why.
Both rewritten forms are Claude Code source types, so a subdirectory
plugin reports Codex, Copilot CLI, and Cursor as skipped.
Refs #88
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the local-source refusal with the rewrite: which form a subdirectory and a root take, where the origin comes from, and the one case that still stops the command. Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skill's add reference and the CLI page both described the refusal. Both now carry the rewrite table and note that a subdirectory plugin reaches Claude Code only. Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`owner/repo` and a git URL name a whole repository, and neither the
`github` nor the `url` source form has a field for a directory inside
it. A monorepo publishing several plugins could therefore only be listed
at its root, which installs the wrong thing rather than failing.
`--subdir` states it, turning the source into `git-subdir` and naming
the entry for the directory rather than the repository:
add cyberuni/cyber-sdd --subdir plugins/aced
-> aced git-subdir .../cyber-sdd.git plugins/aced
`--ref` and `--sha` pin a git source to a branch, tag, or commit. All
three apply to a git-backed source only — an npm package is pinned by
version and a path is whatever is on disk — and each says so rather than
being ignored. A `--sha` is checked against the 40-character hash the
schema requires, at input rather than against the catalog schema later.
Refs #88
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skill's add reference gains a monorepo section, including when to ask the user for --subdir: a repository that publishes more than one plugin installs the wrong one without it. Refs #88 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 49ffa79 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
unional
pushed a commit
that referenced
this pull request
Sep 18, 2026
The package readme and the web docs overview page listed `marketplace init`/`validate` but never `marketplace add`, added in #89. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWLkFTgecKnDFyJjkiCBHJ
unional
added a commit
that referenced
this pull request
Sep 21, 2026
Closes #86. ## What #86 asked, and where it already stands - **Keep an npm source across a rebuild.** Fixed in 5811bd6: `plugin build`'s refresh, `plugin init`, and `marketplace init` all keep an entry whose source is not local and re-derive only its fields. - **Generate an npm source.** `marketplace add npm:<pkg> --force` (#89) writes `{ "source": "npm", "package": … }` to the Claude and Codex catalogs and skips Copilot CLI and Cursor, which document only local paths. I checked this against a fixture shaped like repobuddy: a monorepo with `packages/buddy`, npm entries in the Claude and Codex catalogs, and a local entry in the Copilot catalog. `plugin build` bumped each entry's version and kept the npm sources and the Copilot path. `marketplace add npm:repobuddy --force` turned a local Claude entry into an npm one, and the next build kept it. repobuddy's `sync-plugin-manifests.mjs` workaround is no longer needed. ## What this PR adds The build spec never stated this rule, and no build-level test held it. This PR adds: - a decision in `spec/plugin/build/README.md`: a refresh keeps where the plugin is distributed from, and never chooses a non-local source itself - a `build.feature` scenario - a `buildPlugin` test covering both the Claude and Codex catalogs ## Not done: an automatic npm source whenever `packagePath` is set This repository has `packagePath: "."`, is published, and keeps a local-path catalog on purpose so it can be developed locally. A default that switched to npm whenever `packagePath` is set would break that catalog. So the npm source stays opt-in, done once with `marketplace add npm:<pkg> --force`, and every later build and regeneration keeps it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #88. Covers the first half of #86.
Why
marketplace initderives a catalog from the plugins a repository holds. A repository thatcurates plugins living somewhere else — the way
cyberuni/marketplacedoes — had no command atall, so the entry had to be hand-written into a catalog, which the skill forbids precisely because
a hand-assembled catalog carries the shapes every runtime refuses.
marketplace add <spec>One positional names where the plugin lives, disambiguated by shape:
./plugins/alpha"./plugins/alpha"cyberuni/universal-plugin{ "source": "github", "repo": … }https://…,git@…{ "source": "url", "url": … }npm:repobuddy,@cyberuni/upx{ "source": "npm", "package": … }repobuddy@cyberplaceTwo shapes collide and each has a flag that settles it.
plugins/alphareads as a GitHub repository—
owner/repoand a relative path are the same string, and consulting the filesystem would make itmean different things in different working directories. A leading
@is a scope, so@cyberuni/upxis a package while
upx@cyberplaceis a marketplace entry.--path,--npm,--github,--url,and
--from-marketplaceforce the reading.Monorepos. Neither
githubnorurlhas a field for a directory inside the repository, so--subdirturns the source intogit-subdirand names the entry for that directory:marketplace add cyberuni/cyber-sdd --subdir plugins/aced # aced -> {"source":"git-subdir","url":"https://github.com/cyberuni/cyber-sdd.git","path":"plugins/aced"}--refand--shapin a git source. All three apply to a git-backed source only and say so ratherthan being ignored; a
--shais checked against the 40-character hash the schema requires, at inputrather than against the catalog schema at install time.
Copying from another marketplace. The schema has no "from another marketplace" source, so the
entry is resolved and copied. A source relative to that marketplace is rewritten, not refused —
it names a location inside a repository whose URL is known:
./plugins/cyberspaceincyberuni/cyberplace{"source":"git-subdir","url":"https://github.com/cyberuni/cyberplace.git","path":"plugins/cyberspace"}./inunional/skills{"source":"github","repo":"unional/skills"}The origin comes from what the runtime recorded in
known_marketplaces.json, falling back to theclone's own git remote — which is also how
--from <dir>resolves one. Only a marketplace withneither stops the run.
Per-runtime source support. An entry only reaches a catalog whose runtime can resolve it. Claude
Code takes the schema's full tagged set, Codex takes npm, Copilot CLI and Cursor take local paths
only. The rest are reported
skippedwith the reason rather than written a source they refuse:A prerequisite: regeneration stopped discarding entries
marketplace initrebuilt catalogs from discovery alone, so an added entry was a conflict and then adeletion under
--force.init,plugin build, andplugin initnow keep an entry whose source isnot a local path, and keep a non-local source on a plugin they do discover while refreshing its
derived metadata. That is the loss in #86 — a plugin shipped through npm, whose repository path holds
gitignored build output, was rewritten to that path on every build. What discovery owns it still
owns: a local-path entry it no longer finds is dropped.
The skill is now a gateway
SKILL.mdholds no procedure. It declares the routes inargument-hint, carries the intake, theruntime table, and the shared rules, and names one reference per route:
references/init.md(today'ssteps, moved),
references/add.md(new), andreferences/validate.md— which was the last step ofinitand is now one home rather than two, since both writing routes end there.One user-invocable skill with routes, rather than
marketplace-initandmarketplace-addskillscompeting to trigger on the same phrases.
Decisions worth a reviewer's eye
plugin.json, an installednode_modulescopy,the entry being copied, or the metadata flags. The marketplace spec's non-goals name "calling a
marketplace API" and nothing in
src/does network I/O. A--fetchflag stays open.--authoris not supported. It would have meant changing the shared serializer, which wouldhave added
authorto discovered entries too.version/registryare not exposed.--versionis already the entry's metadata version;overloading it would be confusing.
Verification
731 tests, 726 passing, and
plugin build --checkis clean.End to end in a scratch repository:
init→add npm:repobuddy→add <plugin>@cyberplace→init --forcekeeps all three entries, and all four catalogs validate. The<plugin>@<marketplace>path was exercised against the real installed
cyberplaceandunionalclones, both thesubdirectory and root cases, not only against fixtures.
CI will be red on 5 tests that are already red on
main— four insrc/build/build.test.tsandone in
src/init/init.test.ts, all about catalog refresh. I verified this in a cleanorigin/mainworktree at
e76bd25, where they fail identically with none of this branch's code present. They arenot diagnosed here: reading
src/build/is blocked by local permission settings.🤖 Generated with Claude Code