universal-plugin 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,193 +1,82 @@
1
1
  ---
2
2
  name: marketplace
3
- description: Use this skill to let people install a plugin straight from its own repository — generate the local marketplace catalogs Claude Code, Codex, GitHub Copilot CLI, and Cursor read, and write the README install section that tells users what to type. Trigger on "set up a local marketplace", "let users install this from my repo", "generate marketplace catalogs", "add install instructions to the README", "how do people install this plugin", or "make this repo installable".
4
- argument-hint: '[--claude] [--codex] [--copilot] [--cursor] [--dry-run] [--force]'
3
+ description: Use this skill to run a repository's own plugin marketplace — generate the catalogs Claude Code, Codex, GitHub Copilot CLI, and Cursor read, list a plugin that lives elsewhere (an npm package, a GitHub repo, or `<plugin>@<marketplace>`), check every catalog against the schema its runtime loads, and write the README install section. Trigger on "set up a local marketplace", "let users install this from my repo", "generate marketplace catalogs", "add a plugin to the marketplace", "list this npm package in my marketplace", "make this repo installable", "add install instructions to the README", or "check my marketplace catalogs".
4
+ argument-hint: '[init|add|validate] [--claude] [--codex] [--copilot] [--cursor] [--dry-run] [--force]'
5
5
  ---
6
6
 
7
- # Local marketplace
7
+ # marketplace
8
8
 
9
9
  A repository can carry its own catalog, so a user adds the repository as a marketplace and installs
10
10
  from it. No service, no submission, no account.
11
11
 
12
- All four runtimes read such a catalog. Three of them let a user add it; Cursor's reaches users when
13
- an admin imports the repository as a team marketplace.
12
+ This skill is the front door to that catalog. It routes; the procedure lives in the reference the
13
+ route names. Read only the one you need.
14
14
 
15
- | Runtime | Catalog it reads | What the user types |
16
- | --- | --- | --- |
17
- | Claude Code | `.claude-plugin/marketplace.json` | `/plugin marketplace add`, then `/plugin install` |
18
- | Codex | `.agents/plugins/marketplace.json`, or the Claude path | `codex plugin marketplace add`, then `codex plugin add` |
19
- | GitHub Copilot CLI | `.github/plugin/marketplace.json`, or the Claude path | `copilot plugin marketplace add`, then `copilot plugin install` |
20
- | Cursor | `.cursor-plugin/marketplace.json` | nothing; an admin imports the repository as a team marketplace |
21
-
22
- Read `references/runtimes.md` before writing any command into a README, and treat that file as the
23
- only source of install commands. The trap that lives there: Codex installs with `plugin add` where
24
- Copilot CLI uses `plugin install`.
25
-
26
- ## Workflow
27
-
28
- ### 1. Find what there is to list
29
-
30
- ```bash
31
- ls plugins/*/plugin.json 2>/dev/null
32
- test -f plugin.json && cat plugin.json
33
- ```
34
-
35
- `plugin init --vendor <id>` already registers the plugin it scaffolds in these catalogs, so a
36
- repository that has run it carries an entry before this skill starts. Read what is there first: this
37
- command regenerates a catalog from what it discovers, which is the whole repository rather than one
38
- plugin.
39
-
40
- A catalog lists plugins found at `<scan-root>/<plugin-dir>/plugin.json`, which defaults to
41
- `plugins/`. Pass `--plugin-scan-dir <dir>` when the repository keeps them elsewhere. A repository
42
- whose only plugin sits at its root has nothing to discover; say so rather than generating an empty
43
- catalog.
44
-
45
- Discovery reads a plugin's `name` and nothing else. A missing or malformed `name` stops the command
46
- before any write.
47
-
48
- ### 2. Choose targets with the user
49
-
50
- Name the runtimes and what each one gets, using the table above. With no target flags the command
51
- selects all four.
52
-
53
- One catalog can serve two runtimes when the user wants fewer files: Codex reads the Claude catalog
54
- too, so `--claude` alone covers both. The reverse does not hold, because Claude Code rejects the
55
- Codex catalog for its missing `owner`. Generating both is the default, so each is idiomatic for its
56
- runtime.
57
-
58
- ### 3. Generate
59
-
60
- ```bash
61
- node scripts/marketplace.mjs --claude --copilot --dry-run
62
- ```
63
-
64
- Resolve that path against this skill's own directory; `npx universal-plugin marketplace init` is the
65
- fallback. Run `--dry-run` first and show the plan. The command never prompts.
66
-
67
- Then generate for real. Selected targets compose as a union, so name every target you want each run.
68
-
69
- | Status | Means |
70
- | --- | --- |
71
- | `generated` | written |
72
- | `unchanged` | already correct, byte differences in key order and whitespace ignored |
73
- | `planned` | `--dry-run` only |
74
- | `empty` | nothing discovered for this target |
75
-
76
- A selected artifact that differs from what would be generated stops the whole run. That is the
77
- command protecting a hand-edited catalog. Read the difference, then re-run with `--force` only once
78
- you know what it discards.
79
-
80
- ### 4. Validate every catalog
81
-
82
- A catalog the runtime rejects is worse than no catalog: it is found, read, and refused at install
83
- time, far from here. Never conclude this skill without running the check.
15
+ ## Routes
84
16
 
85
- ```bash
86
- node scripts/validate.mjs
87
- ```
88
-
89
- Resolve that path against this skill's own directory; `npx universal-plugin marketplace validate` is
90
- the fallback. It reads each catalog and checks it against the schema its runtime loads. Exit status is
91
- 1 when any selected catalog is invalid, and every issue names the key and the value to write instead,
92
- on stderr:
93
-
94
- ```
95
- error: catalog ".claude-plugin/marketplace.json" does not match the marketplace schema:
96
- owner must be an object with a name, not string — write { "name": "Ari Vance" }
97
- plugins[0].repository must be a string, not object — write "https://github.com/o/r.git"
98
- ```
99
-
100
- | Status | Means |
101
- | --- | --- |
102
- | `valid` | loads in that runtime |
103
- | `invalid` | the runtime would refuse it; the issues say why |
104
- | `missing` | no catalog at that path, which is only a failure under `--required` |
105
-
106
- Fix an issue in the source it comes from, then regenerate: an entry's fields are derived from the
107
- plugin's `plugin.json`, so an npm-style `repository` object belongs fixed there. The top-level `name`
108
- and `owner` live in the catalog itself, and `owner` must be an object — `{ "name": "…" }`, never the
109
- `"Name <email>"` string `package.json` uses.
110
-
111
- The two checks the schema cannot make: `--required` for a target the user asked for, and sources on
112
- disk. Every `./` source is checked for existence by `validate`; sources resolve against the directory
113
- containing `.claude-plugin/`, and they do not resolve at all for a user who adds the marketplace by
114
- direct URL to the JSON file.
115
-
116
- ### 5. Offer the README section
117
-
118
- Ask before writing. A README is the user's document, and this is an edit to it, not a new file.
119
-
120
- ```bash
121
- node scripts/install-docs.mjs
122
- ```
17
+ | Route | Use it when | Procedure |
18
+ | --- | --- | --- |
19
+ | `init` | The repository **holds** the plugins. Derive a catalog from what is on disk, then offer the README install section. | `references/init.md` |
20
+ | `add` | The plugin **lives elsewhere** — an npm package, a GitHub repository, or an entry another marketplace already publishes. | `references/add.md` |
21
+ | `validate` | Check the catalogs the repository already carries against the schema each runtime loads. | `references/validate.md` |
123
22
 
124
- Stdout is one JSON object: `targets`, `repo`, and `markdown`. Insert `markdown` verbatim. It carries
125
- a section per generated catalog, built from the marketplace name and plugin names actually on disk.
23
+ `init` and `add` are not alternatives. One repository can be both a plugin's home and a curated
24
+ list, and the two commands compose: a regeneration keeps the entries `add` wrote, because discovery
25
+ could never have produced them.
126
26
 
127
- Check `repoResolved` first. When it is `false` the repository slug could not be found and the
128
- snippet contains `<owner>/<repo>`; ask the user for the slug and re-run with `--repo <owner>/<repo>`
129
- rather than leaving a placeholder in their README.
27
+ Both routes end in `validate`. A catalog the runtime rejects is worse than no catalog — it is found,
28
+ read, and refused at install time, in someone else's terminal.
130
29
 
131
- If the README already has an install section, show the difference and let the user choose. Do not
132
- append a second one.
30
+ ## Intake
133
31
 
134
- ### 6. Verify
32
+ **Name the route and go.** "Set up a local marketplace", "make this repo installable", "generate the
33
+ catalogs" → `init`. "Add repobuddy to the marketplace", "list this npm package" → `add`. "Check the
34
+ catalogs" → `validate`. Load that reference and follow it; do not ask.
135
35
 
136
- Re-run the generator and confirm every selected target reports `unchanged`. Confirm each catalog
137
- path exists. State plainly that nothing was published: these files sit in the repository until a
138
- user adds it as a marketplace.
36
+ **Ask only when the invocation is bare.** One question: does the repository hold the plugins it is
37
+ listing, or is it listing plugins that live somewhere else? Offer `init`, `add`, and `validate`.
139
38
 
140
- A local path is the cheapest end-to-end proof:
39
+ Discovery settles the question faster than the user can:
141
40
 
142
41
  ```bash
143
- codex plugin marketplace add "$(pwd)"
144
- codex plugin list
145
- codex plugin marketplace remove <marketplace-name>
42
+ ls plugins/*/plugin.json 2>/dev/null
43
+ ls .claude-plugin/marketplace.json .agents/plugins/marketplace.json 2>/dev/null
146
44
  ```
147
45
 
148
- `codex plugin list` prints the manifest path it read, which is what tells you the catalog was found
149
- rather than merely present. Offer this check rather than running it unasked: it writes to the user's
150
- Codex config. Remove what you added.
151
-
152
- ## Local development against Codex
46
+ Plugins under `plugins/` and no catalog yet is `init`. A catalog already there and nothing new on
47
+ disk is usually `add`.
153
48
 
154
- Codex installs a **copy** of the plugin at
155
- `~/.codex/plugins/cache/<marketplace>/<plugin>/<version>`, where the version is the one the plugin's
156
- own manifest carries. Editing the plugin's files does not reach that copy.
49
+ ## Where each runtime looks
157
50
 
158
- After changing packaged files, install again and start a new session:
159
-
160
- ```bash
161
- codex plugin add <plugin>@<marketplace>
162
- ```
163
-
164
- That one command is the whole refresh: re-running it at the same version overwrites the cached copy
165
- with the current source, so neither `codex plugin remove` nor a version bump is needed. Codex reads
166
- the cache when a session starts, so the session you are in keeps the old copy — start a new one.
51
+ | Runtime | Catalog it reads | What the user types |
52
+ | --- | --- | --- |
53
+ | Claude Code | `.claude-plugin/marketplace.json` | `/plugin marketplace add`, then `/plugin install` |
54
+ | Codex | `.agents/plugins/marketplace.json`, or the Claude path | `codex plugin marketplace add`, then `codex plugin add` |
55
+ | GitHub Copilot CLI | `.github/plugin/marketplace.json`, or the Claude path | `copilot plugin marketplace add`, then `copilot plugin install` |
56
+ | Cursor | `.cursor-plugin/marketplace.json` | nothing; an admin imports the repository as a team marketplace |
167
57
 
168
- The catalog entry's version is derived from the canonical manifest, so a version move updates both
169
- (`/universal-plugin:version` owns that). Codex itself does
170
- not read the entry's version; keeping it true is this project's policy, so the catalog never states
171
- a version the plugin does not have.
58
+ Three of the four read `.claude-plugin/marketplace.json`, so one file covers Claude Code, Codex, and
59
+ Copilot CLI. The reverse does not hold: Claude Code rejects the Codex catalog for its missing
60
+ `owner`.
172
61
 
173
- ## Rules
62
+ ## Rules for every route
174
63
 
175
64
  - **Never publish a command that is not in `references/runtimes.md`.** An install command that fails
176
- is worse than no install section. Widely-copied README snippets are not sources.
65
+ is worse than no install section, and that file is the only source of them. Widely-copied README
66
+ snippets are not sources. The trap that lives there: Codex installs with `plugin add` where
67
+ Copilot CLI uses `plugin install`.
68
+ - **Never hand-author or hand-patch a catalog.** Generate it, then validate it. A catalog written by
69
+ copying fields out of `package.json` carries `owner` as a string and `repository` as an object,
70
+ and Claude Code refuses it for either one.
177
71
  - **Name every catalog `marketplace.json`.** Codex discovers a catalog by that filename inside a
178
72
  supported directory. A file named anything else is invisible to it, whatever directory holds it.
179
- - **Do not write a Cursor install command.** Cursor has no command that adds a repository catalog;
180
- a developer tests through `~/.cursor/plugins/local/<name>` and users get the plugin through a team
181
- marketplace an admin imports.
182
- - **Never hand-author or hand-patch a catalog.** Generate it, then validate it. A catalog written by
183
- copying fields out of `package.json` carries `owner` as a string and `repository` as an object, and
184
- Claude Code refuses it for either one.
185
- - **Report the validation result, not just the generation result.** A run that generated four files
186
- and validated none has not been verified.
187
- - **Ask before editing the README**, and before `--force` replaces a catalog the user may have
188
- hand-edited.
189
- - This command publishes nothing and registers nothing. Say so in the report; a user who believes
190
- they have published will not understand why nobody can install.
73
+ - **Do not write a Cursor install command.** Cursor has no command that adds a repository catalog.
74
+ - **Report the validation result, not just the write.** A run that wrote four files and validated
75
+ none has not been verified.
76
+ - **Nothing here publishes or registers anything.** Say so in the report; a user who believes they
77
+ have published will not understand why nobody can install.
78
+ - **Ask before editing the README**, and before `--force` replaces anything a user may have written
79
+ by hand.
191
80
  - Listing a plugin in the shared `cyberuni/marketplace` repository is a different job: use
192
81
  `publish-plugin`.
193
82
 
@@ -202,8 +91,8 @@ a version the plugin does not have.
202
91
 
203
92
  ## References
204
93
 
94
+ - `references/init.md` — derive the catalogs from the plugins this repository holds
95
+ - `references/add.md` — list a plugin that lives elsewhere
96
+ - `references/validate.md` — check the catalogs against the schema each runtime loads
205
97
  - `references/runtimes.md` — per-runtime install commands and their sources
206
- - The official Claude Code marketplace schema, which `validate` checks against:
207
- <https://json.schemastore.org/claude-code-marketplace.json>
208
98
  - [Research conclusion](https://github.com/cyberuni/universal-plugin/blob/main/.research/local-marketplaces/conclusion.md)
209
- - [`marketplace init` spec](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/marketplace/init/README.md)
@@ -0,0 +1,138 @@
1
+ # Route: add
2
+
3
+ List a plugin that lives somewhere else — an npm package, a GitHub repository, or an entry another
4
+ marketplace already publishes. This is what makes a repository a curated marketplace rather than
5
+ only its own plugins' home.
6
+
7
+ `init` covers the other case and the two compose; see `references/init.md`.
8
+
9
+ ## 1. Work out what the user is naming
10
+
11
+ One positional argument says where the plugin lives. The command disambiguates by shape:
12
+
13
+ | Form | Example | Source written |
14
+ | --- | --- | --- |
15
+ | path in this repository | `./plugins/alpha` | `"./plugins/alpha"` |
16
+ | GitHub repository | `cyberuni/universal-plugin` | `{ "source": "github", "repo": "…" }` |
17
+ | git or https URL | `https://example.com/o/r.git` | `{ "source": "url", "url": "…" }` |
18
+ | npm package | `npm:repobuddy`, `@cyberuni/upx` | `{ "source": "npm", "package": "…" }` |
19
+ | another marketplace | `repobuddy@cyberplace` | whatever source that marketplace publishes |
20
+
21
+ Two shapes collide, and both have a flag that settles them:
22
+
23
+ - **`plugins/alpha` reads as a GitHub repository**, not a directory — `owner/repo` and a relative
24
+ path are the same string. Write `./plugins/alpha`, or pass `--path`.
25
+ - **A leading `@` is a scope, not a marketplace.** `@cyberuni/upx` is a package; `upx@cyberplace` is
26
+ a marketplace entry.
27
+
28
+ `--path`, `--npm`, `--github`, `--url`, and `--from-marketplace` each force the reading. Pass one.
29
+
30
+ ### One plugin out of a monorepo
31
+
32
+ `owner/repo` and a git URL both name a whole repository, and neither source form can say which
33
+ directory inside it is the plugin. `--subdir` says it, which turns the source into `git-subdir`:
34
+
35
+ ```bash
36
+ node scripts/add.mjs cyberuni/cyber-sdd --subdir plugins/aced
37
+ # { "source": "git-subdir", "url": "https://github.com/cyberuni/cyber-sdd.git", "path": "plugins/aced" }
38
+ ```
39
+
40
+ The entry is named for that directory — `aced` — not for the repository. Ask for `--subdir` whenever
41
+ the user names a repository you know publishes more than one plugin; without it the entry points at
42
+ the repository root, which installs the wrong thing rather than failing.
43
+
44
+ `--ref <branch-or-tag>` and `--sha <commit>` pin a git source. Both apply to `url`, `github`, and
45
+ `git-subdir` only: an npm package is pinned by version and a path is whatever is on disk. A `--sha`
46
+ must be the full 40-character hash, which the schema requires.
47
+
48
+ ## 2. Choose targets, and know what will be skipped
49
+
50
+ Not every runtime installs from every source. Only a repository path reaches all four:
51
+
52
+ | Source | Claude Code | Codex | Copilot CLI | Cursor |
53
+ | --- | --- | --- | --- | --- |
54
+ | path | yes | yes | yes | yes |
55
+ | npm | yes | yes | — | — |
56
+ | github, url, git-subdir | yes | — | — | — |
57
+
58
+ A target that cannot resolve the source is reported `skipped` with the reason, and no file is
59
+ written for it. That is deliberate: a source a runtime refuses is an install failure in someone
60
+ else's terminal. Say which targets were skipped and why — do not report "added" and leave the user
61
+ to discover that two catalogs have no entry.
62
+
63
+ With no target flags all four are selected, so the skips are the normal case for an npm package.
64
+
65
+ ## 3. Supply the metadata
66
+
67
+ Nothing is fetched. The command reads what is already on this machine and takes the rest from flags:
68
+
69
+ - a **path** source reads the plugin's own `plugin.json`
70
+ - an **npm** source reads `node_modules/<pkg>/package.json` when the package happens to be installed
71
+ - a **marketplace** entry brings the metadata that marketplace already publishes
72
+
73
+ Flags fill or override the gaps: `--description`, `--version`, `--homepage`, `--repository`,
74
+ `--license`, `--keywords` (comma-separated). Ask the user for a description at minimum; an entry
75
+ with only a name tells a browsing user nothing.
76
+
77
+ `--name` overrides the entry name, which otherwise comes from the spec: the package name without its
78
+ scope, the repository name, or the directory name.
79
+
80
+ ## 4. Preview, then write
81
+
82
+ ```bash
83
+ node scripts/add.mjs npm:repobuddy --description "Repo automation" --dry-run
84
+ ```
85
+
86
+ Resolve that path against this skill's own directory; `npx universal-plugin marketplace add` is the
87
+ fallback. Run `--dry-run` first and show the plan, including the skips. The command never prompts.
88
+
89
+ | Status | Means |
90
+ | --- | --- |
91
+ | `added` | the entry was not listed; it is now |
92
+ | `updated` | an entry of that name was listed and has been replaced |
93
+ | `unchanged` | already says exactly this |
94
+ | `planned` | `--dry-run` only |
95
+ | `skipped` | that runtime cannot resolve this source; the reason says which |
96
+
97
+ An entry already listed **differently** stops the run and asks for `--force`. Read what would change
98
+ before passing it: someone wrote that entry, and `--force` is what discards their version.
99
+
100
+ The catalog is created if the repository has none, named and owned the same way `init` names it —
101
+ from the root `plugin.json` author, with `--marketplace-name` and `--owner` to override. A
102
+ repository curating other people's plugins usually has no root manifest, so `--owner` is the flag it
103
+ needs.
104
+
105
+ ## 5. Resolving `<plugin>@<marketplace>`
106
+
107
+ The catalog schema has no "from another marketplace" source, so the entry is **resolved and copied**
108
+ rather than referenced. The marketplace has to be readable:
109
+
110
+ - installed in the runtime already — read from `~/.claude/plugins`, no network
111
+ - otherwise, `--from <dir>` naming a checkout of it
112
+
113
+ A marketplace that is not installed stops the run and says so. Ask the user to add it in their
114
+ runtime, or to point `--from` at a clone; do not guess a URL.
115
+
116
+ An entry whose source is a `./` path is **rewritten**, not copied. That path resolves against the
117
+ other marketplace's root, which this repository is not — but it is a location inside a repository
118
+ whose URL is known, so it becomes an absolute source:
119
+
120
+ | Entry in the other marketplace | Written here |
121
+ | --- | --- |
122
+ | `./plugins/aced` in `cyberuni/cyberplace` | `{ "source": "git-subdir", "url": "https://github.com/cyberuni/cyberplace.git", "path": "plugins/aced" }` |
123
+ | `./` in `unional/skills` | `{ "source": "github", "repo": "unional/skills" }` |
124
+
125
+ The origin comes from what the runtime recorded for that marketplace, falling back to the clone's
126
+ own git remote — which is also how `--from` resolves one. A marketplace with neither stops the run:
127
+ there is no URL to rewrite against, and a bare path would name a directory this repository does not
128
+ have.
129
+
130
+ Both rewritten forms are Claude Code source types, so expect Codex, Copilot CLI, and Cursor to be
131
+ skipped for a subdirectory plugin.
132
+
133
+ ## 6. Validate
134
+
135
+ Follow `references/validate.md`. The entry has to load in every runtime that got one.
136
+
137
+ Then state plainly that nothing was published: the entry sits in the repository until a user adds it
138
+ as a marketplace.
@@ -0,0 +1,131 @@
1
+ # Route: init
2
+
3
+ Derive a catalog from the plugins this repository holds, then offer the README section that tells
4
+ users what to type.
5
+
6
+ For a plugin that lives elsewhere, see `references/add.md`. The two compose on one repository.
7
+
8
+ ## 1. Find what there is to list
9
+
10
+ ```bash
11
+ ls plugins/*/plugin.json 2>/dev/null
12
+ test -f plugin.json && cat plugin.json
13
+ ```
14
+
15
+ `plugin init --vendor <id>` already registers the plugin it scaffolds in these catalogs, so a
16
+ repository that has run it carries an entry before this route starts. Read what is there first: this
17
+ command regenerates a catalog from what it discovers, which is the whole repository rather than one
18
+ plugin.
19
+
20
+ A catalog lists plugins found at `<scan-root>/<plugin-dir>/plugin.json`, which defaults to
21
+ `plugins/`. Pass `--plugin-scan-dir <dir>` when the repository keeps them elsewhere. A repository
22
+ whose only plugin sits at its root has nothing to discover; say so rather than generating an empty
23
+ catalog.
24
+
25
+ Discovery reads a plugin's `name` and nothing else. A missing or malformed `name` stops the command
26
+ before any write.
27
+
28
+ ## 2. Choose targets with the user
29
+
30
+ Name the runtimes and what each one gets, using the table in `SKILL.md`. With no target flags the
31
+ command selects all four.
32
+
33
+ One catalog can serve two runtimes when the user wants fewer files: Codex reads the Claude catalog
34
+ too, so `--claude` alone covers both. The reverse does not hold, because Claude Code rejects the
35
+ Codex catalog for its missing `owner`. Generating both is the default, so each is idiomatic for its
36
+ runtime.
37
+
38
+ ## 3. Generate
39
+
40
+ ```bash
41
+ node scripts/marketplace.mjs --claude --copilot --dry-run
42
+ ```
43
+
44
+ Resolve that path against this skill's own directory; `npx universal-plugin marketplace init` is the
45
+ fallback. Run `--dry-run` first and show the plan. The command never prompts.
46
+
47
+ Then generate for real. Selected targets compose as a union, so name every target you want each run.
48
+
49
+ | Status | Means |
50
+ | --- | --- |
51
+ | `generated` | written |
52
+ | `unchanged` | already correct, byte differences in key order and whitespace ignored |
53
+ | `planned` | `--dry-run` only |
54
+ | `empty` | nothing discovered for this target |
55
+
56
+ A selected artifact that differs from what would be generated stops the whole run. That is the
57
+ command protecting a hand-edited catalog. Read the difference, then re-run with `--force` only once
58
+ you know what it discards.
59
+
60
+ ### What a regeneration keeps
61
+
62
+ Discovery walks directories, so it can only speak for plugins that are in them. Two things survive a
63
+ regeneration untouched:
64
+
65
+ - an entry whose source is **not** a local path — an npm package, a GitHub repository — which only
66
+ `add` puts there
67
+ - the source of a **discovered** plugin whose entry names a non-local source, with its derived
68
+ metadata refreshed around it
69
+
70
+ So a plugin shipped through npm, whose repository path holds gitignored build output, keeps pointing
71
+ at npm. What discovery owns it still owns: a local-path entry it no longer finds is dropped.
72
+
73
+ ## 4. Validate every catalog
74
+
75
+ Follow `references/validate.md`. Never conclude without it.
76
+
77
+ ## 5. Offer the README section
78
+
79
+ Ask before writing. A README is the user's document, and this is an edit to it, not a new file.
80
+
81
+ ```bash
82
+ node scripts/install-docs.mjs
83
+ ```
84
+
85
+ Stdout is one JSON object: `targets`, `repo`, and `markdown`. Insert `markdown` verbatim. It carries
86
+ a section per generated catalog, built from the marketplace name and plugin names actually on disk.
87
+
88
+ Check `repoResolved` first. When it is `false` the repository slug could not be found and the
89
+ snippet contains `<owner>/<repo>`; ask the user for the slug and re-run with `--repo <owner>/<repo>`
90
+ rather than leaving a placeholder in their README.
91
+
92
+ If the README already has an install section, show the difference and let the user choose. Do not
93
+ append a second one.
94
+
95
+ ## 6. Verify
96
+
97
+ Re-run the generator and confirm every selected target reports `unchanged`. Confirm each catalog
98
+ path exists. State plainly that nothing was published: these files sit in the repository until a
99
+ user adds it as a marketplace.
100
+
101
+ A local path is the cheapest end-to-end proof:
102
+
103
+ ```bash
104
+ codex plugin marketplace add "$(pwd)"
105
+ codex plugin list
106
+ codex plugin marketplace remove <marketplace-name>
107
+ ```
108
+
109
+ `codex plugin list` prints the manifest path it read, which is what tells you the catalog was found
110
+ rather than merely present. Offer this check rather than running it unasked: it writes to the user's
111
+ Codex config. Remove what you added.
112
+
113
+ ## Local development against Codex
114
+
115
+ Codex installs a **copy** of the plugin at
116
+ `~/.codex/plugins/cache/<marketplace>/<plugin>/<version>`, where the version is the one the plugin's
117
+ own manifest carries. Editing the plugin's files does not reach that copy.
118
+
119
+ After changing packaged files, install again and start a new session:
120
+
121
+ ```bash
122
+ codex plugin add <plugin>@<marketplace>
123
+ ```
124
+
125
+ That one command is the whole refresh: re-running it at the same version overwrites the cached copy
126
+ with the current source, so neither `codex plugin remove` nor a version bump is needed. Codex reads
127
+ the cache when a session starts, so the session you are in keeps the old copy — start a new one.
128
+
129
+ The catalog entry's version is derived from the canonical manifest, so a version move updates both
130
+ (`/universal-plugin:version` owns that). Codex itself does not read the entry's version; keeping it
131
+ true is this project's policy, so the catalog never states a version the plugin does not have.
@@ -0,0 +1,46 @@
1
+ # Route: validate
2
+
3
+ Check the catalogs the repository carries against the schema each runtime loads.
4
+
5
+ Every route ends here. A catalog the runtime rejects is worse than no catalog: it is found, read,
6
+ and refused at install time, far from the repository that wrote it. Never conclude the skill without
7
+ running this.
8
+
9
+ ```bash
10
+ node scripts/validate.mjs
11
+ ```
12
+
13
+ Resolve that path against this skill's own directory; `npx universal-plugin marketplace validate` is
14
+ the fallback. Exit status is 1 when any selected catalog is invalid, and every issue names the key
15
+ and the value to write instead, on stderr:
16
+
17
+ ```
18
+ error: catalog ".claude-plugin/marketplace.json" does not match the marketplace schema:
19
+ owner must be an object with a name, not string — write { "name": "Ari Vance" }
20
+ plugins[0].repository must be a string, not object — write "https://github.com/o/r.git"
21
+ ```
22
+
23
+ | Status | Means |
24
+ | --- | --- |
25
+ | `valid` | loads in that runtime |
26
+ | `invalid` | the runtime would refuse it; the issues say why |
27
+ | `missing` | no catalog at that path, which is only a failure under `--required` |
28
+
29
+ ## Fixing what it reports
30
+
31
+ Fix an issue in the source it comes from, then regenerate. An entry's fields are derived, so an
32
+ npm-style `repository` object belongs fixed in the plugin's `plugin.json` (for a discovered plugin)
33
+ or passed as `--repository <url>` (for an added one).
34
+
35
+ The top-level `name` and `owner` live in the catalog itself, and `owner` must be an object —
36
+ `{ "name": "…" }`, never the `"Name <email>"` string `package.json` uses.
37
+
38
+ Nothing is repaired automatically. A catalog edited by hand is the user's to correct.
39
+
40
+ ## The two checks the schema cannot make
41
+
42
+ - `--required`, for a target the user asked for and did not get.
43
+ - Sources on disk. Every `./` source is checked for existence. They resolve against the directory
44
+ holding `.claude-plugin/`, and they do not resolve at all for a user who adds the marketplace by
45
+ direct URL to the JSON file — which is the argument for a non-local source on a plugin
46
+ distributed that way (`references/add.md`).
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Runs `universal-plugin marketplace add` from the CLI that ships beside this skill, so listing a
3
+ // plugin never depends on a network fetch or on which version `npx` happens to resolve.
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ // <package>/skills/<skill>/scripts/add.mjs: four levels up is the package root.
8
+ const packageRoot = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
9
+
10
+ process.argv.splice(2, 0, 'marketplace', 'add')
11
+ await import(join(packageRoot, 'bin', 'universal-plugin.mjs'))
@@ -175,9 +175,10 @@ Point every version and build step at the new plugin root:
175
175
 
176
176
  - Set the manifest extension's `packagePath` to `"."`.
177
177
  - `universal-plugin publish sync-version --root <pkg>` reads `packagePath` from
178
- `<pkg>/.agents/universal-plugin.json`, not from the manifest extension. To use
179
- it, create that file with `{ "packagePath": "." }`. If the repository instead
180
- runs its own sync script, repoint that script's manifest path and leave it.
178
+ `<pkg>/.agents/universal-plugin.json`, not from the manifest extension. With
179
+ no `packagePath` it reads `<pkg>/package.json`, so a package that holds its
180
+ own plugin needs no config file for it. If the repository instead runs its
181
+ own sync script, repoint that script's manifest path and leave it.
181
182
  - Change every `universal-plugin plugin build --root <old>` in `package.json`
182
183
  scripts to the package directory, then run it and confirm it reports the
183
184
  vendor manifests as built and the catalog as unchanged or refolded.
File without changes