universal-plugin 0.7.0 → 0.9.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.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/bin/upx.mjs +12 -5
  5. package/dist/cli.mjs +5731 -243
  6. package/package.json +4 -2
  7. package/plugin.json +1 -1
  8. package/readme.md +12 -5
  9. package/schema/extension.schema.json +2 -6
  10. package/skills/adopt-upx/README.md +4 -4
  11. package/skills/adopt-upx/SKILL.md +4 -4
  12. package/skills/{doctor → doctor-universal-plugin}/README.md +4 -5
  13. package/skills/{doctor → doctor-universal-plugin}/SKILL.md +30 -10
  14. package/skills/{doctor → doctor-universal-plugin}/scripts/doctor.mjs +84 -38
  15. package/skills/{init → init-universal-plugin}/README.md +1 -1
  16. package/skills/{init → init-universal-plugin}/SKILL.md +3 -3
  17. package/skills/{init → init-universal-plugin}/references/standard.md +5 -1
  18. package/skills/{init → init-universal-plugin}/references/vendors/claude-code.md +1 -1
  19. package/skills/marketplace/SKILL.md +1 -1
  20. package/skills/migrate-plugin/SKILL.md +140 -26
  21. package/skills/migrate-plugin/evals/evals.json +6 -0
  22. package/skills/migrate-plugin/evals/trigger-queries.json +18 -0
  23. package/skills/publish-plugin/README.md +35 -0
  24. package/skills/publish-plugin/SKILL.md +118 -19
  25. package/skills/publish-plugin/evals/evals.json +12 -0
  26. package/skills/remove-plugin/README.md +1 -1
  27. package/skills/remove-plugin/SKILL.md +2 -2
  28. package/skills/version/SKILL.md +2 -2
  29. package/dist/run.mjs +0 -271
  30. /package/skills/{init → init-universal-plugin}/assets/templates/agent.md +0 -0
  31. /package/skills/{init → init-universal-plugin}/assets/templates/command.md +0 -0
  32. /package/skills/{init → init-universal-plugin}/assets/templates/hooks.json +0 -0
  33. /package/skills/{init → init-universal-plugin}/assets/templates/plugin.json +0 -0
  34. /package/skills/{init → init-universal-plugin}/assets/templates/setup-command.md +0 -0
  35. /package/skills/{init → init-universal-plugin}/assets/templates/skill.md +0 -0
  36. /package/skills/{init → init-universal-plugin}/references/adopt.md +0 -0
  37. /package/skills/{init → init-universal-plugin}/references/create.md +0 -0
  38. /package/skills/{init → init-universal-plugin}/references/detection.md +0 -0
  39. /package/skills/{init → init-universal-plugin}/references/frontmatter.md +0 -0
  40. /package/skills/{init → init-universal-plugin}/references/update.md +0 -0
  41. /package/skills/{init → init-universal-plugin}/references/vendors/codex.md +0 -0
  42. /package/skills/{init → init-universal-plugin}/references/vendors/copilot-cli.md +0 -0
  43. /package/skills/{init → init-universal-plugin}/references/vendors/cursor.md +0 -0
  44. /package/skills/{init → init-universal-plugin}/scripts/init.mjs +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Universal AI agent plugin build tool",
5
5
  "keywords": [
6
6
  "agent-plugin",
@@ -39,6 +39,7 @@
39
39
  "agents"
40
40
  ],
41
41
  "dependencies": {
42
+ "@repobuddy/upx": "^0.1.0",
42
43
  "@toon-format/toon": "^4.1.1",
43
44
  "commander": "^14.0.3",
44
45
  "semver": "^7.8.1"
@@ -62,12 +63,13 @@
62
63
  },
63
64
  "scripts": {
64
65
  "build": "tsdown",
66
+ "check:governances": "tsx src/cli.ts plugin build --check --root .",
65
67
  "dev": "tsx src/cli.ts",
66
68
  "knip": "knip",
67
69
  "lint": "biome check .",
68
70
  "test": "pnpm build && vitest run src",
69
71
  "test:watch": "vitest",
70
72
  "typecheck": "tsc --noEmit",
71
- "verify": "pnpm typecheck && pnpm lint && pnpm test"
73
+ "verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm check:governances"
72
74
  }
73
75
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "universal-plugin",
4
- "version": "0.7.0",
4
+ "version": "0.9.0",
5
5
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
6
6
  "author": {
7
7
  "name": "unional"
package/readme.md CHANGED
@@ -120,21 +120,28 @@ npx universal-plugin self-update <version> # update the version pin i
120
120
 
121
121
  ## upx, the fast package runner
122
122
 
123
- `npm i -g universal-plugin` puts a second bin, `upx`, on PATH.
123
+ `upx` now ships as its own package, [`@repobuddy/upx`](https://www.npmjs.com/package/@repobuddy/upx).
124
124
 
125
125
  `upx <pkg>@^<major>` looks for an already-installed version satisfying the range, checking local
126
126
  `node_modules` first and then global. It spawns that binary directly, skipping the resolve step that
127
- costs `npx` roughly 1s per call. When nothing installed matches, it falls back to `npx`.
127
+ costs `npx` a registry round-trip on every call. When nothing installed matches, it falls back to
128
+ `npx`. See [its measurements](https://repobuddy.github.io/upx/concepts/measurements/) for what that
129
+ is worth on current npm.
128
130
 
129
131
  ```sh
130
- npm i -g universal-plugin
132
+ npm i -g @repobuddy/upx
131
133
  upx cyber-skills@^2 audit validate
132
134
  ```
133
135
 
134
136
  Use a caret range on the major rather than an exact pin, so one global install serves every caller.
137
+ `plugin bundle --runner upx` emits references in this form.
135
138
 
136
- `upx` only works once `universal-plugin` is installed globally. `npx` ships with npm, so keep `npx`
137
- as the default anywhere you cannot guarantee that install.
139
+ `upx` only works once it is installed globally. `npx` ships with npm, so keep `npx` as the default
140
+ anywhere you cannot guarantee that install.
141
+
142
+ > **Deprecated:** `universal-plugin` still installs a `upx` bin that re-exports `@repobuddy/upx`, so
143
+ > existing global installs keep working. It will be removed in the next major — install
144
+ > `@repobuddy/upx` directly.
138
145
 
139
146
  ## Related
140
147
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "universal-plugin extension namespace",
4
- "description": "universal-plugin build configuration: the build-target list, per-harness manifest overrides, the plugins depended on, the npm package tracked, and the canonical component input paths the build derives per harness. This is the body of extensions[\"org.cyberuni.universal-plugin\"] in an Agent Plugins Specification v1.0.0 manifest, and nothing else — apply it to the namespace object, not to the manifest.",
5
- "$comment": "The manifest envelope around this object belongs to the Agent Plugins Specification and is validated by https://agent-plugins.org/schemas/1.0.0/plugin.schema.json. That specification assigns no semantics to a namespace object's contents (§8), so this schema supplies them for the one namespace universal-plugin owns. It carries no $id: the fields it describes version with the universal-plugin package that ships it.",
4
+ "description": "universal-plugin build configuration: the build-target list, per-harness manifest overrides, the plugins depended on, and the canonical component input paths the build derives per harness. This is the body of extensions[\"org.cyberuni.universal-plugin\"] in an Agent Plugins Specification v1.0.0 manifest, and nothing else — apply it to the namespace object, not to the manifest.",
5
+ "$comment": "The manifest envelope around this object belongs to the Agent Plugins Specification and is validated by https://agent-plugins.org/schemas/1.0.0/plugin.schema.json. That specification assigns no semantics to a namespace object's contents (§8), so this schema supplies them for the one namespace universal-plugin owns. packagePath is not here: it is repository release config, read from .agents/universal-plugin.json beside plugin.json and resolved from the plugin root (ADR-0010 §2). It carries no $id: the fields it describes version with the universal-plugin package that ships it.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
@@ -22,10 +22,6 @@
22
22
  "$ref": "#/$defs/dependencies",
23
23
  "description": "Plugins this plugin needs. Delivered to the harnesses that read a dependency and dropped with a build warning for the rest — only Claude Code reads one (ADR-0013)."
24
24
  },
25
- "packagePath": {
26
- "type": "string",
27
- "description": "Relative path (from repo root) to the npm package folder whose version this plugin tracks."
28
- },
29
25
  "skills": {
30
26
  "$ref": "#/$defs/pathValue",
31
27
  "description": "Skill directories. Default: ./skills/. Each subdirectory must contain SKILL.md."
@@ -6,13 +6,13 @@ Rewrites `npx <pkg>@<version>` references in `SKILL.md` files to a caret range o
6
6
 
7
7
  ## When to use
8
8
 
9
- When you want a project's skills to call CLIs via `upx` instead of `npx`, for the ~10× speed win
10
- on repeated invocations (local-first resolution vs. `npx`'s ~1s registry+spawn cost per call, even
9
+ When you want a project's skills to call CLIs via `upx` instead of `npx`, for the speed win
10
+ on repeated invocations (local-first resolution vs. `npx`'s registry+spawn cost per call, even
11
11
  cached).
12
12
 
13
13
  ## What it does
14
14
 
15
- 1. Confirms `upx` is installed (`npm i -g universal-plugin`) and on PATH.
15
+ 1. Confirms `upx` is installed (`npm i -g @repobuddy/upx`) and on PATH.
16
16
  2. Rewrites `npx <pkg>@<concrete-semver>` → `upx <pkg>@^<major>` (or `^0.<minor>` for a 0.x pin)
17
17
  across a chosen scope:
18
18
  - one specific skill (a path)
@@ -29,7 +29,7 @@ cached).
29
29
  ## Tradeoff
30
30
 
31
31
  A rewritten skill depends on `upx` being on PATH. `npx` ships with every npm install; `upx` only
32
- exists after `npm i -g universal-plugin`. This is an opt-in migration, not a safe default.
32
+ exists after `npm i -g @repobuddy/upx`. This is an opt-in migration, not a safe default.
33
33
 
34
34
  ## Install
35
35
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: adopt-upx
3
- description: Use this skill when the user wants to make their skills use upx — the fast local-first package runner shipped by universal-plugin. Trigger on phrases like "make my skills use upx", "adopt the upx runner", "speed up npx calls", "switch to upx", or "rewrite npx pins to upx". Rewrites `npx <pkg>@<version>` references to a caret range on `upx` (`^<major>`, or `^0.<minor>` for a 0.x pin) across one skill, a named set, or every skill in the project.
3
+ description: Use this skill when the user wants to make their skills use upx — the fast local-first package runner shipped as @repobuddy/upx. Trigger on phrases like "make my skills use upx", "adopt the upx runner", "speed up npx calls", "switch to upx", or "rewrite npx pins to upx". Rewrites `npx <pkg>@<version>` references to a caret range on `upx` (`^<major>`, or `^0.<minor>` for a 0.x pin) across one skill, a named set, or every skill in the project.
4
4
  ---
5
5
 
6
6
  # Adopt upx
@@ -11,7 +11,7 @@ fast local-first runner shipped by `universal-plugin` (see the package
11
11
 
12
12
  ## When to use
13
13
 
14
- The user wants their project's skills to shell out via `upx` instead of `npx`, for the ~10× speed
14
+ The user wants their project's skills to shell out via `upx` instead of `npx`, for the speed
15
15
  win on repeated calls. This is an opt-in migration, not a default — see Tradeoff below before
16
16
  running it broadly.
17
17
 
@@ -20,7 +20,7 @@ running it broadly.
20
20
  Install the runner:
21
21
 
22
22
  ```bash
23
- npm i -g universal-plugin
23
+ npm i -g @repobuddy/upx
24
24
  ```
25
25
 
26
26
  This puts the `upx` bin on PATH. Verify:
@@ -94,7 +94,7 @@ a no-op (there's no `npx` left to match), so it's safe to run again after adding
94
94
 
95
95
  A skill rewritten to `upx` now depends on the `upx` bin being on that environment's PATH.
96
96
  `npx` always ships with npm — every Node environment has it. `upx` does not — it only exists after
97
- `npm i -g universal-plugin`. So this is a deliberate opt-in for environments where
97
+ `npm i -g @repobuddy/upx`. So this is a deliberate opt-in for environments where
98
98
  `universal-plugin` is installed globally, not a safe-by-default swap.
99
99
 
100
100
  Mitigating factor: `upx` itself falls back to plain `npx` on a miss (no local/global install
@@ -1,4 +1,4 @@
1
- # doctor skill
1
+ # doctor-universal-plugin skill
2
2
 
3
3
  Diagnose a universal agent plugin: what the canonical `plugin.json` declares, and whether what is on
4
4
  disk still matches it for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
@@ -16,7 +16,7 @@ The skill supplies the judgment around it: which finding matters, and which skil
16
16
 
17
17
  ## It never repairs
18
18
 
19
- Every finding names the skill that fixes it — `init` for anything that rewrites the manifest,
19
+ Every finding names the skill that fixes it — `init-universal-plugin` for anything that rewrites the manifest,
20
20
  `version` for the release number, `remove-plugin` for artifacts. A repair can overwrite a manifest
21
21
  the user maintains, and that judgment belongs to the skill that owns the write.
22
22
 
@@ -29,9 +29,8 @@ The checks are deterministic: same tree, same findings. The one check that is no
29
29
  definitive staleness test — rebuild on a clean tree and read the diff — because it writes. The skill
30
30
  reports that one as a repair for the user to run.
31
31
 
32
- Manifest validation is chartered as a CLI capability (`plugin validate`, specified but not yet
33
- shipped). This script stays a thin composition on purpose, so it folds into that command rather than
34
- competing with it.
32
+ Manifest validation is a CLI capability (`plugin validate`). This script stays a thin composition
33
+ on purpose, so it folds into that command rather than competing with it.
35
34
 
36
35
  ## Boundaries
37
36
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: doctor
2
+ name: doctor-universal-plugin
3
3
  description: Use this skill to diagnose a universal agent plugin — when `plugin build` reports "built 0" or "nothing to build", when it warns "No vendors declared in harnesses", when a repository still carries `.plugin/plugin.json` or a top-level `vendorExtensions` block after upgrading universal-plugin across a major, when a released version never reached the vendor manifests, when a runtime loads none of the plugin's skills, when a vendor manifest is missing or looks out of date after a pull, or when checking whether what the canonical plugin.json declares still matches what is on disk for Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "is my plugin set up right", "why isn't my plugin loading", "the build says built 0", "why did nothing get built", "check the plugin", "are the vendor manifests current", or "what does this plugin declare".
4
4
  ---
5
5
 
@@ -21,6 +21,9 @@ node scripts/doctor.mjs
21
21
 
22
22
  Resolve that path against this skill's own directory. It runs the CLI that shipped beside it against
23
23
  the current working directory, so nothing is downloaded; add `--root <path>` to diagnose elsewhere.
24
+ Add `--marketplace-root <path>` (repeatable) to also check a **separately-cloned** shared marketplace
25
+ repository (e.g. a local clone of `cyberuni/marketplace`) — see
26
+ [Catalogs are checked at the repository root](#catalogs-are-checked-at-the-repository-root).
24
27
  It never prompts and never writes, so it is safe to run unattended.
25
28
 
26
29
  Stdout is one JSON object — that is the contract to read, not the CLI's own terminal output:
@@ -67,24 +70,27 @@ Each `code` below is what the script emits.
67
70
 
68
71
  | Finding | What it means | Repair |
69
72
  | --- | --- | --- |
70
- | `no-manifest` | no root `plugin.json` — this is not a plugin yet | `/universal-plugin:init` |
71
- | `legacy-manifest` | root `plugin.json` with neither `$schema` nor `extensions` — a single-vendor manifest on the canonical path | `/universal-plugin:init`, adopt route |
72
- | `vendor-only` | a vendor manifest with no canonical manifest above it | `/universal-plugin:init`, adopt route |
73
+ | `no-manifest` | no root `plugin.json` — this is not a plugin yet | `/universal-plugin:init-universal-plugin` |
74
+ | `legacy-manifest` | root `plugin.json` with neither `$schema` nor `extensions` — a single-vendor manifest on the canonical path | `/universal-plugin:init-universal-plugin`, adopt route |
75
+ | `vendor-only` | a vendor manifest with no canonical manifest above it | `/universal-plugin:init-universal-plugin`, adopt route |
73
76
  | `unbuilt` | a declared vendor whose output path holds no file — that runtime sees no plugin | `universal-plugin plugin build` |
74
77
  | `stale` | a derived manifest older than `plugin.json` | `universal-plugin plugin build` |
75
78
  | `hand-edited` | a derived manifest that `build` would rewrite — the edit is already lost, it just has not been overwritten yet | move the field to the canonical manifest or to `harnesses.<vendor>`, then rebuild |
76
79
  | `unknown-vendor` | a `vendors` entry no build target matches; reported as `skipped` plus a warning | fix the id in `plugin.json` |
77
- | `undeliverable-override` | `harnesses["copilot-cli"]` sets fields that reach nothing | `/universal-plugin:init`, update route — move them to a vendor that has a derived manifest, or drop them |
80
+ | `undeliverable-override` | `harnesses["copilot-cli"]` sets fields that reach nothing | `/universal-plugin:init-universal-plugin`, update route — move them to a vendor that has a derived manifest, or drop them |
78
81
  | `codex-fields-missing` | Codex is targeted without `version` or `description`; the build fails and writes **nothing at all**, including for the other vendors | add both to the canonical top level |
79
82
  | `version-drift` | the `packagePath` `package.json` and the canonical manifest carry different versions | `/universal-plugin:version` |
80
83
  | `unreleased-content` | shipped content was committed after the commit that set the current version — a consumer keyed on that version never re-extracts it | `/universal-plugin:version` |
81
84
  | `copilot-root-components` | agents, commands, rules, hooks, or LSP servers sit at the plugin root with no copy under `com.github.copilot/` — Copilot CLI reads them only from there in spec mode, so it loads none of them, silently | `universal-plugin plugin build` |
82
85
  | `stale-github-plugin` | a leftover `.github/plugin/plugin.json` from an older build — shadowed by root and no longer generated | `/universal-plugin:remove-plugin` |
83
86
  | `shadowing-manifest` | a `.plugin/plugin.json` exists — it outranks root in Copilot CLI's search order and silently shadows the canonical manifest | `/universal-plugin:remove-plugin` |
84
- | `no-vendors` | no vendor is declared, so the build writes nothing and no runtime reads the plugin. On a repository still on the pre-0.6 layout the build stops rather than reporting an empty result, and the detail says so — read it beside `legacy-manifest` and `shadowing-manifest`, which name the signals | `/universal-plugin:init`, adopt route on the pre-0.6 layout, else update route |
87
+ | `no-vendors` | no vendor is declared, so the build writes nothing and no runtime reads the plugin. On a repository still on the pre-0.6 layout the build stops rather than reporting an empty result, and the detail says so — read it beside `legacy-manifest` and `shadowing-manifest`, which name the signals | `/universal-plugin:init-universal-plugin`, adopt route on the pre-0.6 layout, else update route |
85
88
  | `package-path-missing` | `packagePath` names a directory with no readable `package.json` | fix `packagePath`, or create the package |
89
+ | `package-path-unknown` | the CLI could not report `packagePath` (a version too old to read it), so `version-drift` and `unreleased-content` were skipped rather than guessed | upgrade universal-plugin |
90
+ | `misplaced-package-path` | `plugin.json` declares `packagePath` under `extensions["org.cyberuni.universal-plugin"]`, where the CLI never reads it — the plugin is silently treated as not shipping to npm | move it to `.agents/universal-plugin.json`, relative to the plugin root |
86
91
  | `unparsable-manifest` | root `plugin.json` is not valid JSON | fix the syntax error |
87
- | `invalid-catalog` | a marketplace catalog at the repository root is not a shape its runtime loads — it is found, read, and refused at install time, in the user's terminal | `/universal-plugin:marketplace` |
92
+ | `invalid-catalog` | a marketplace catalog — at the repository root, or at a `--marketplace-root` clone — is not a shape its runtime loads — it is found, read, and refused at install time, in the user's terminal | `/universal-plugin:marketplace` |
93
+ | `marketplace-root-missing` | a `--marketplace-root` path does not exist, so it could not be checked at all | clone the marketplace repository, or fix the path |
88
94
 
89
95
  ## Catalogs are checked at the repository root
90
96
 
@@ -96,6 +102,20 @@ The detail names the key at fault, so hand it to `/universal-plugin:marketplace`
96
102
  entry's fields are derived from the plugin's `plugin.json`, and the catalog's own `name` and `owner`
97
103
  are authored in the catalog — which half is at fault decides where the repair goes.
98
104
 
105
+ A **shared** marketplace repository (e.g. `cyberuni/marketplace`) is a repository of its own — a bad
106
+ entry that reached it by another path (a hand-edited entry, a PR from a different tool, a curator
107
+ edit) is invisible to a `doctor` run inside any plugin's own repo, because that run never sees the
108
+ shared repository at all. Clone it separately and name the clone explicitly:
109
+
110
+ ```bash
111
+ node scripts/doctor.mjs --marketplace-root ../marketplace
112
+ ```
113
+
114
+ Pass `--marketplace-root` once per clone to check more than one. Each invalid entry it finds is still
115
+ reported as `invalid-catalog`, with the clone's path in the detail so it reads apart from the plugin
116
+ repo's own catalogs; a path that does not exist is `marketplace-root-missing` rather than a silent
117
+ skip.
118
+
99
119
  ## Checking staleness properly
100
120
 
101
121
  The `stale` finding is an mtime comparison, which catches the common case and nothing more. It cannot
@@ -116,8 +136,8 @@ user can run, or ask before running it yourself.
116
136
 
117
137
  ## Version drift
118
138
 
119
- Two files carry an authored version: the canonical `plugin.json`, and the `package.json` at
120
- `extensions["org.cyberuni.universal-plugin"].packagePath` when one is declared. The script compares
139
+ Two files carry an authored version: the canonical `plugin.json`, and the `package.json` at the
140
+ `packagePath` the CLI reports (`config get --key packagePath`), resolved from the plugin root. The script compares
121
141
  them and emits `version-drift`.
122
142
 
123
143
  They diverge when someone ran `npm version`, or when changesets released a number that never flowed
@@ -158,7 +178,7 @@ is meant to ship — content that is still being worked on is not a finding to a
158
178
 
159
179
  | Task | Skill |
160
180
  |------|-------|
161
- | Create, adopt, or change what the plugin declares | `init` |
181
+ | Create, adopt, or change what the plugin declares | `init-universal-plugin` |
162
182
  | Move the plugin's version | `version` |
163
183
  | Remove derived manifests, or the plugin itself | `remove-plugin` |
164
184
  | Generate the repository's own marketplace catalogs | `marketplace` |
@@ -15,6 +15,9 @@ const argv = process.argv.slice(2)
15
15
  const verbose = argv.includes('--verbose')
16
16
  const rootFlag = argv.indexOf('--root')
17
17
  const root = path.resolve(rootFlag === -1 ? process.cwd() : (argv[rootFlag + 1] ?? process.cwd()))
18
+ // A shared marketplace repository (e.g. cyberuni/marketplace) is cloned separately from any plugin
19
+ // repo, so it is named explicitly rather than discovered — repeatable, one clone per flag.
20
+ const marketplaceRoots = argv.flatMap((arg, i) => (arg === '--marketplace-root' ? [argv[i + 1]] : [])).filter(Boolean)
18
21
 
19
22
  const findings = []
20
23
  const add = (code, severity, detail, repair) => findings.push({ code, severity, detail, repair })
@@ -42,27 +45,56 @@ if (manifest === null) {
42
45
  'vendor-only',
43
46
  'high',
44
47
  `vendor manifests with no canonical manifest: ${orphans.join(', ')}`,
45
- '/universal-plugin:init, adopt route',
48
+ '/universal-plugin:init-universal-plugin, adopt route',
46
49
  )
47
50
  } else {
48
- add('no-manifest', 'high', 'no root plugin.json — this is not a plugin yet', '/universal-plugin:init')
51
+ add(
52
+ 'no-manifest',
53
+ 'high',
54
+ 'no root plugin.json — this is not a plugin yet',
55
+ '/universal-plugin:init-universal-plugin',
56
+ )
49
57
  }
50
58
  report({ vendors: [] })
51
59
  }
52
60
 
53
61
  const ext = manifest.extensions?.[UP_NAMESPACE] ?? null
54
62
 
55
- // `packagePath` is the CLI's own config, and the CLI reads it from `.agents/universal-plugin.json`
56
- // (src/version/fs.ts). It is read here from the same file, so a plugin the CLI treats as npm-shipping
57
- // is one this script treats the same way. The manifest extension is accepted as a fallback for a
58
- // repository that put it there.
63
+ // The shipped CLI, preferring the one this skill ships with. Every question the CLI can answer is asked
64
+ // of it rather than re-derived here, so this script cannot drift from the commands it diagnoses.
65
+ const bin = path.join(packageRoot, 'bin', 'universal-plugin.mjs')
66
+ const runCli = (...args) =>
67
+ fs.existsSync(bin)
68
+ ? spawnSync(process.execPath, [bin, ...args], { encoding: 'utf8' })
69
+ : spawnSync('npx', ['universal-plugin', ...args], { encoding: 'utf8' })
70
+
71
+ // `packagePath` is the CLI's own config. It is asked of the CLI (`config get --key packagePath`), the
72
+ // same reader `plugin version` and `publish sync-version` use, so a plugin the CLI treats as
73
+ // npm-shipping is one this script treats the same way (issue #79). `undefined` means the CLI could not
74
+ // answer — too old to read the key — and every check that depends on it is skipped rather than guessed.
59
75
  const packagePath = readPackagePath()
76
+ if (packagePath === undefined) {
77
+ add(
78
+ 'package-path-unknown',
79
+ 'low',
80
+ 'the CLI could not report packagePath, so version-drift and unreleased-content were not checked',
81
+ 'upgrade universal-plugin',
82
+ )
83
+ }
84
+ if (ext !== null && Object.hasOwn(ext, 'packagePath')) {
85
+ add(
86
+ 'misplaced-package-path',
87
+ 'high',
88
+ `plugin.json declares extensions["${UP_NAMESPACE}"].packagePath, which the CLI never reads${packagePath === null ? ' — this plugin is treated as not shipping to npm' : ''}`,
89
+ 'move packagePath to .agents/universal-plugin.json, relative to the plugin root',
90
+ )
91
+ }
60
92
  if (!manifest.$schema?.includes('agent-plugins.org') || ext === null) {
61
93
  add(
62
94
  'legacy-manifest',
63
95
  'high',
64
96
  'root plugin.json carries no $schema on agent-plugins.org or no extensions block',
65
- '/universal-plugin:init, adopt route',
97
+ '/universal-plugin:init-universal-plugin, adopt route',
66
98
  )
67
99
  }
68
100
 
@@ -136,14 +168,7 @@ if (declaredTargets.includes('copilot-cli')) {
136
168
  }
137
169
 
138
170
  // Ask the shipped CLI what it would write, without writing it.
139
- const bin = path.join(packageRoot, 'bin', 'universal-plugin.mjs')
140
- const cli = fs.existsSync(bin)
141
- ? spawnSync(process.execPath, [bin, 'plugin', 'build', '--dry-run', '--format', 'json', '--root', root], {
142
- encoding: 'utf8',
143
- })
144
- : spawnSync('npx', ['universal-plugin', 'plugin', 'build', '--dry-run', '--format', 'json', '--root', root], {
145
- encoding: 'utf8',
146
- })
171
+ const cli = runCli('plugin', 'build', '--dry-run', '--format', 'json', '--root', root)
147
172
 
148
173
  const vendors = []
149
174
  const build = readJson_stdout(cli.stdout)
@@ -166,7 +191,7 @@ if (build === null) {
166
191
  'no-vendors',
167
192
  'medium',
168
193
  'no vendor is declared — the project is on the pre-0.6 layout, so the build derives nothing and no runtime reads this plugin',
169
- '/universal-plugin:init, adopt route',
194
+ '/universal-plugin:init-universal-plugin, adopt route',
170
195
  )
171
196
  } else {
172
197
  add(
@@ -199,13 +224,13 @@ if (build === null) {
199
224
  }
200
225
  for (const warning of build.warnings ?? []) {
201
226
  if (/not delivered/.test(warning)) {
202
- add('undeliverable-override', 'medium', warning, '/universal-plugin:init, update route')
227
+ add('undeliverable-override', 'medium', warning, '/universal-plugin:init-universal-plugin, update route')
203
228
  } else if (/No vendors declared/.test(warning)) {
204
229
  add(
205
230
  'no-vendors',
206
231
  'medium',
207
232
  'no vendor is declared — the build writes nothing, so no runtime reads this plugin',
208
- '/universal-plugin:init, update route',
233
+ '/universal-plugin:init-universal-plugin, update route',
209
234
  )
210
235
  } else if (/^Unknown vendor/.test(warning)) {
211
236
  add('unknown-vendor', 'medium', warning, 'fix the vendor id in plugin.json')
@@ -216,7 +241,7 @@ if (build === null) {
216
241
  }
217
242
 
218
243
  // Version drift between the two authored numbers.
219
- if (packagePath !== null) {
244
+ if (typeof packagePath === 'string') {
220
245
  const pkgPath = path.join(root, packagePath, 'package.json')
221
246
  const pkg = readJson(pkgPath)
222
247
  if (pkg === null) {
@@ -264,31 +289,45 @@ if (manifest.version !== undefined && packagePath === null) {
264
289
  // The marketplace catalogs a user installs from. They sit at the *repository* root, above a plugin in
265
290
  // a monorepo, and each is read by its runtime at install time — a catalog whose shape that runtime
266
291
  // refuses fails in the user's terminal, not here. The shipped CLI owns the rules; this only asks.
267
- for (const row of invalidCatalogs()) {
292
+ // A shared marketplace repository (--marketplace-root) is a separate clone this plugin's repo root
293
+ // cannot see, so it is checked the same way but reported with the clone's path attached.
294
+ const missingMarketplaceRoots = marketplaceRoots.filter((r) => !fs.existsSync(path.resolve(r)))
295
+ for (const r of missingMarketplaceRoots) {
296
+ add(
297
+ 'marketplace-root-missing',
298
+ 'high',
299
+ `--marketplace-root ${r} does not exist`,
300
+ 'clone the marketplace repository, or fix the path',
301
+ )
302
+ }
303
+ const existingMarketplaceRoots = marketplaceRoots.filter((r) => fs.existsSync(path.resolve(r)))
304
+
305
+ for (const row of invalidCatalogs(existingMarketplaceRoots)) {
306
+ const label = row.own ? row.path : `${row.catalogRoot}/${row.path}`
268
307
  add(
269
308
  'invalid-catalog',
270
309
  'high',
271
- `${row.path} is not a shape its runtime loads: ${row.issues.map((issue) => `${issue.path} ${issue.message}`).join('; ')}`,
310
+ `${label} is not a shape its runtime loads: ${row.issues.map((issue) => `${issue.path} ${issue.message}`).join('; ')}`,
272
311
  '/universal-plugin:marketplace',
273
312
  )
274
313
  }
275
314
 
276
315
  report({ vendors })
277
316
 
278
- /** Every catalog the repository carries that its runtime would refuse. Empty when there is nothing to
279
- * read, when the CLI is too old to answer, or when every catalog is fine — a missing catalog is not a
280
- * fault, and this reports no opinion on which ones a repository ought to carry. */
281
- function invalidCatalogs() {
317
+ /** Every catalog the repository — and any explicitly named marketplace clone — carries that its
318
+ * runtime would refuse. Empty when there is nothing to read, when the CLI is too old to answer, or
319
+ * when every catalog is fine — a missing catalog is not a fault, and this reports no opinion on
320
+ * which ones a repository ought to carry. */
321
+ function invalidCatalogs(extraRoots) {
282
322
  const catalogRoot = git('rev-parse', '--show-toplevel') ?? root
283
- const result = fs.existsSync(bin)
284
- ? spawnSync(process.execPath, [bin, 'marketplace', 'validate', '--format', 'json', '--root', catalogRoot], {
285
- encoding: 'utf8',
286
- })
287
- : spawnSync('npx', ['universal-plugin', 'marketplace', 'validate', '--format', 'json', '--root', catalogRoot], {
288
- encoding: 'utf8',
289
- })
290
- const rows = readJson_stdout(result.stdout)
291
- return Array.isArray(rows) ? rows.filter((row) => row.status === 'invalid') : []
323
+ const targets = [{ catalogRoot, own: true }, ...extraRoots.map((r) => ({ catalogRoot: path.resolve(r), own: false }))]
324
+ return targets.flatMap(({ catalogRoot, own }) => {
325
+ const result = runCli('marketplace', 'validate', '--format', 'json', '--root', catalogRoot)
326
+ const rows = readJson_stdout(result.stdout)
327
+ return Array.isArray(rows)
328
+ ? rows.filter((row) => row.status === 'invalid').map((row) => ({ ...row, catalogRoot, own }))
329
+ : []
330
+ })
292
331
  }
293
332
 
294
333
  /** Runs git inside `root`, returning its stdout or `null` — a non-zero status, a missing git, and a
@@ -355,9 +394,16 @@ function report({ vendors }) {
355
394
  process.exit(0)
356
395
  }
357
396
 
358
- /** Where the npm package that ships this plugin lives, or `null` when the plugin ships to no
359
- * package. `null` is the author-picks release model of ADR-0010 §2. */
397
+ /** Where the npm package that ships this plugin lives, relative to the plugin root, as the CLI reads
398
+ * it; `null` when the plugin ships to no package (the author-picks release model of ADR-0010 §2);
399
+ * `undefined` when the CLI could not answer. */
360
400
  function readPackagePath() {
361
- const declared = readJson(path.join(root, '.agents', 'universal-plugin.json'))?.packagePath ?? ext?.packagePath
362
- return typeof declared === 'string' && declared.length > 0 ? declared : null
401
+ const result = runCli('config', 'get', '--key', 'packagePath', '--format', 'json', '--root', root)
402
+ if (result.status !== 0) return undefined
403
+ try {
404
+ const declared = JSON.parse(result.stdout)
405
+ return typeof declared === 'string' || declared === null ? declared : undefined
406
+ } catch {
407
+ return undefined
408
+ }
363
409
  }
@@ -1,4 +1,4 @@
1
- # init skill
1
+ # init-universal-plugin skill
2
2
 
3
3
  Give a project one canonical `plugin.json` on the [Agent Plugins
4
4
  Specification](https://agent-plugins.org), then derive the manifest each runtime expects — Claude
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: init
2
+ name: init-universal-plugin
3
3
  description: Use this skill to create or change a universal agent plugin — scaffold a new one, adopt an existing vendor-specific plugin or already-shipped skills onto the open Agent Plugins Specification, or add and remove vendors and components on the canonical plugin.json that drives Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "init a plugin here", "make my Claude Code plugin work in Cursor", "convert this to the open plugin standard", "turn these skills into a plugin", "add Codex support", or "add a hooks component".
4
4
  argument-hint: '[--name <name>] [--vendor <id>] [--scaffold] [--npm] [--no-marketplace] [--force]'
5
5
  ---
@@ -48,7 +48,7 @@ It is the authoritative source for which component to reach for and which anti-p
48
48
 
49
49
  ## Arguments
50
50
 
51
- An invocation may carry the CLI's own flags: `/universal-plugin:init --name my-plugin --scaffold --npm`.
51
+ An invocation may carry the CLI's own flags: `/universal-plugin:init-universal-plugin --name my-plugin --scaffold --npm`.
52
52
 
53
53
  Read them from the invocation itself rather than from a placeholder. Claude Code appends what the
54
54
  caller typed as `ARGUMENTS: <value>`, and Codex substitutes nothing at all, so on every runtime the
@@ -161,7 +161,7 @@ and read back any line it printed on stderr — a repository with no author, no
161
161
  remote gets no catalog, because every runtime requires an owner. `--no-marketplace` skips the step.
162
162
 
163
163
  The catalog is named after the repository, `<owner>-<repo>-local`, not after the plugin: it lists
164
- every plugin the repository develops. Re-running `init` folds the entry back in and leaves the
164
+ every plugin the repository develops. Re-running `init-universal-plugin` folds the entry back in and leaves the
165
165
  marketplace name, the owner, and every other entry alone, so it is safe over a catalog someone
166
166
  edited. Generating catalogs for a repository that already holds several plugins, and writing the
167
167
  README install section, is the `marketplace` skill's job.
@@ -36,10 +36,14 @@ lives under one namespaced key, `extensions["org.cyberuni.universal-plugin"]`:
36
36
  | --- | --- |
37
37
  | `vendors` | the build targets. When absent, the `harnesses` keys are the targets |
38
38
  | `harnesses` | per-vendor overrides, keyed by vendor id. `{}` opts in with no overrides |
39
- | `packagePath` | the npm package whose `package.json` carries the same version |
40
39
  | component paths (`skills`, `commands`, `agents`, `hooks`, …) | where each component lives |
41
40
  | `dependencies` | the plugins this plugin needs. Only Claude Code reads them; see [`vendors/claude-code.md`](./vendors/claude-code.md) |
42
41
 
42
+ `packagePath` does not belong here. The npm package whose `package.json` carries the same version is
43
+ named in `.agents/universal-plugin.json` beside `plugin.json`, as a path relative to the plugin root
44
+ (for example `"packagePath": "."`, or `"../../packages/<name>"` in a monorepo). It is release config
45
+ for the repository, not part of the manifest.
46
+
43
47
  `vendors` and `harnesses` are separate on purpose: `vendors` says what to build, `harnesses` says
44
48
  what each build gets. A vendor listed in `vendors` with no `harnesses` entry still builds; a
45
49
  `harnesses` entry with no `vendors` list builds only while `vendors` is absent.
@@ -10,7 +10,7 @@ npx universal-plugin plugin build --vendor claude-code
10
10
 
11
11
  The shared metadata from the canonical top level, plus the component paths, plus whatever
12
12
  `extensions["org.cyberuni.universal-plugin"].harnesses["claude-code"]` sets. `$schema`, `extensions`,
13
- `vendors`, `packagePath`, and `harnesses` are universal-plugin's own orchestration — they never
13
+ `vendors`, and `harnesses` are universal-plugin's own orchestration — they never
14
14
  appear in a vendor manifest.
15
15
 
16
16
  An empty `"claude-code": {}` is the normal case: it opts into the build with no overrides.
@@ -195,7 +195,7 @@ a version the plugin does not have.
195
195
 
196
196
  | Task | Skill |
197
197
  |------|-------|
198
- | Create or change the plugin being listed | `init` |
198
+ | Create or change the plugin being listed | `init-universal-plugin` |
199
199
  | Check that the plugin's own manifests are current | `doctor` |
200
200
  | Move the version users will install | `version` |
201
201
  | Submit to the shared marketplace repository instead | `publish-plugin` |