universal-plugin 0.6.0 → 0.8.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 (50) 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/com.github.copilot/agents/agentskills-specialist.agent.md +132 -0
  6. package/dist/cli.mjs +5806 -261
  7. package/governances/plugin-design.md +14 -0
  8. package/package.json +4 -1
  9. package/plugin.json +1 -1
  10. package/readme.md +12 -5
  11. package/schema/README.md +10 -0
  12. package/schema/claude-code-marketplace.json +1939 -0
  13. package/schema/extension.schema.json +842 -0
  14. package/skills/adopt-upx/README.md +4 -4
  15. package/skills/adopt-upx/SKILL.md +4 -4
  16. package/skills/doctor/README.md +3 -4
  17. package/skills/doctor/SKILL.md +44 -14
  18. package/skills/doctor/scripts/doctor.mjs +145 -37
  19. package/skills/{init → init-universal-plugin}/README.md +5 -2
  20. package/skills/{init → init-universal-plugin}/SKILL.md +38 -13
  21. package/skills/init-universal-plugin/references/adopt.md +206 -0
  22. package/skills/{init → init-universal-plugin}/references/detection.md +8 -2
  23. package/skills/{init → init-universal-plugin}/references/standard.md +5 -1
  24. package/skills/{init → init-universal-plugin}/references/vendors/claude-code.md +1 -1
  25. package/skills/init-universal-plugin/references/vendors/copilot-cli.md +201 -0
  26. package/skills/marketplace/SKILL.md +1 -1
  27. package/skills/migrate-plugin/SKILL.md +140 -26
  28. package/skills/migrate-plugin/evals/evals.json +6 -0
  29. package/skills/migrate-plugin/evals/trigger-queries.json +18 -0
  30. package/skills/publish-plugin/README.md +35 -0
  31. package/skills/publish-plugin/SKILL.md +118 -19
  32. package/skills/publish-plugin/evals/evals.json +12 -0
  33. package/skills/remove-plugin/README.md +1 -1
  34. package/skills/remove-plugin/SKILL.md +2 -2
  35. package/skills/version/SKILL.md +2 -2
  36. package/dist/run.mjs +0 -271
  37. package/skills/init/references/adopt.md +0 -118
  38. package/skills/init/references/vendors/copilot-cli.md +0 -53
  39. /package/skills/{init → init-universal-plugin}/assets/templates/agent.md +0 -0
  40. /package/skills/{init → init-universal-plugin}/assets/templates/command.md +0 -0
  41. /package/skills/{init → init-universal-plugin}/assets/templates/hooks.json +0 -0
  42. /package/skills/{init → init-universal-plugin}/assets/templates/plugin.json +0 -0
  43. /package/skills/{init → init-universal-plugin}/assets/templates/setup-command.md +0 -0
  44. /package/skills/{init → init-universal-plugin}/assets/templates/skill.md +0 -0
  45. /package/skills/{init → init-universal-plugin}/references/create.md +0 -0
  46. /package/skills/{init → init-universal-plugin}/references/frontmatter.md +0 -0
  47. /package/skills/{init → init-universal-plugin}/references/update.md +0 -0
  48. /package/skills/{init → init-universal-plugin}/references/vendors/codex.md +0 -0
  49. /package/skills/{init → init-universal-plugin}/references/vendors/cursor.md +0 -0
  50. /package/skills/{init → init-universal-plugin}/scripts/init.mjs +0 -0
package/dist/run.mjs DELETED
@@ -1,271 +0,0 @@
1
- #!/usr/bin/env node
2
- import * as fsNode from "node:fs";
3
- import * as path from "node:path";
4
- import * as semver$1 from "semver";
5
- import { spawnSync } from "node:child_process";
6
- //#region src/run/fs.ts
7
- function readInstall(dir) {
8
- try {
9
- const raw = fsNode.readFileSync(path.join(dir, "package.json"), "utf8");
10
- const pkg = JSON.parse(raw);
11
- if (typeof pkg.version !== "string") return void 0;
12
- return {
13
- dir,
14
- version: pkg.version,
15
- bin: pkg.bin
16
- };
17
- } catch {
18
- return;
19
- }
20
- }
21
- function packageDirIn(nodeModulesDir, pkg) {
22
- return path.join(nodeModulesDir, ...pkg.split("/"));
23
- }
24
- /** Every ancestor `node_modules` directory from `startDir` up to the filesystem root, nearest
25
- * first. */
26
- function ancestorNodeModulesDirs(startDir) {
27
- const dirs = [];
28
- let dir = startDir;
29
- for (;;) {
30
- dirs.push(path.join(dir, "node_modules"));
31
- const parent = path.dirname(dir);
32
- if (parent === dir) break;
33
- dir = parent;
34
- }
35
- return dirs;
36
- }
37
- /** Walks `node_modules` from `cwd` up through its ancestors, nearest first, collecting every
38
- * install of `pkg` found along the way (an install missing a readable `package.json` version is
39
- * skipped, not a match). */
40
- function findLocalInstalls(pkg, cwd) {
41
- const installs = [];
42
- for (const nodeModulesDir of ancestorNodeModulesDirs(cwd)) {
43
- const install = readInstall(packageDirIn(nodeModulesDir, pkg));
44
- if (install) installs.push(install);
45
- }
46
- return installs;
47
- }
48
- function globalRoot() {
49
- try {
50
- const out = spawnSync("npm", ["root", "-g"], { encoding: "utf8" }).stdout?.trim();
51
- return out ? out : void 0;
52
- } catch {
53
- return;
54
- }
55
- }
56
- function findGlobalInstall(pkg) {
57
- const root = globalRoot();
58
- if (!root) return void 0;
59
- return readInstall(packageDirIn(root, pkg));
60
- }
61
- function spawnBin(binPath, args) {
62
- const result = spawnSync(binPath, args, { stdio: "inherit" });
63
- if (result.error) throw result.error;
64
- return result.status ?? 1;
65
- }
66
- function spawnNpx(args) {
67
- const result = spawnSync("npx", args, { stdio: "inherit" });
68
- if (result.error) throw result.error;
69
- return result.status ?? 1;
70
- }
71
- function realRunFs() {
72
- return {
73
- findLocalInstalls: (pkg) => findLocalInstalls(pkg, process.cwd()),
74
- findGlobalInstall,
75
- spawnBin,
76
- spawnNpx
77
- };
78
- }
79
- //#endregion
80
- //#region src/run/run.ts
81
- const NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
82
- /** A loose approximation of npm's package-name rules: lowercase, alphanumeric/`.`/`_`/`-`, an
83
- * optional `@scope/` prefix, non-empty. Good enough to reject the obviously malformed without
84
- * chasing every edge npm itself enforces. */
85
- function isValidPackageName(name) {
86
- if (!name || name.length > 214) return false;
87
- return NPM_NAME_PATTERN.test(name);
88
- }
89
- /** Splits `<pkg>@<range>` on the **last** `@` at index > 0, so a scoped name's leading `@` survives.
90
- * No `@` after index 0 — or a trailing `@` with an empty range — is a bare package (range `*`). A
91
- * package name that is empty or violates npm's naming rules is a fail-loud error. */
92
- function parseSpec(spec) {
93
- if (!spec) return {
94
- ok: false,
95
- error: "error: no package spec given"
96
- };
97
- const lastAt = spec.lastIndexOf("@");
98
- let pkg;
99
- let rangeRaw;
100
- if (lastAt <= 0) {
101
- pkg = spec;
102
- rangeRaw = "";
103
- } else {
104
- pkg = spec.slice(0, lastAt);
105
- rangeRaw = spec.slice(lastAt + 1);
106
- }
107
- if (!isValidPackageName(pkg)) return {
108
- ok: false,
109
- error: `error: unparseable package spec "${spec}"`
110
- };
111
- const bare = rangeRaw === "";
112
- return {
113
- ok: true,
114
- spec: {
115
- pkg,
116
- range: bare ? "*" : rangeRaw,
117
- bare
118
- }
119
- };
120
- }
121
- /** A valid semver range drives local-first matching; a non-empty non-semver spec (`next`,
122
- * `latest`) is a dist-tag that can't be matched against an installed `package.json` version. */
123
- function isSemverRange(range) {
124
- return semver$1.validRange(range) !== null;
125
- }
126
- /** Nearest-local → global, first install whose version satisfies `range`. `locals` must already be
127
- * nearest-first. */
128
- function selectInstall(range, locals, globalInstall) {
129
- for (const install of locals) if (semver$1.satisfies(install.version, range)) return install;
130
- if (globalInstall && semver$1.satisfies(globalInstall.version, range)) return globalInstall;
131
- }
132
- /** Resolves the executable from a `package.json` `bin` field: a string bin, an object entry keyed
133
- * by the package's unscoped name (even among several bins), or a single-entry object. A
134
- * multi-entry object with no name match, or a missing `bin` field entirely, fails loud — `upx`
135
- * never guesses which bin to run. */
136
- function resolveBinPath(pkgName, bin) {
137
- if (bin === void 0) return {
138
- ok: false,
139
- error: `error: package "${pkgName}" declares no bin field`
140
- };
141
- if (typeof bin === "string") return {
142
- ok: true,
143
- bin
144
- };
145
- const entries = Object.entries(bin);
146
- if (entries.length === 0) return {
147
- ok: false,
148
- error: `error: package "${pkgName}" declares no bin field`
149
- };
150
- if (entries.length === 1) return {
151
- ok: true,
152
- bin: entries[0][1]
153
- };
154
- const match = bin[pkgName.includes("/") ? pkgName.slice(pkgName.lastIndexOf("/") + 1) : pkgName] ?? bin[pkgName];
155
- if (match) return {
156
- ok: true,
157
- bin: match
158
- };
159
- return {
160
- ok: false,
161
- error: `error: package "${pkgName}" declares multiple bins and none matches its name — upx never guesses which to run`
162
- };
163
- }
164
- const KNOWN_LEADING_FLAGS = new Set(["--help", "-h"]);
165
- /** A flag is a token starting with `-`; `upx`'s own flags are recognized only before the first
166
- * non-flag token (the package spec) — everything from the spec onward belongs to the child. An
167
- * unknown flag before the spec fails loud. */
168
- function parseArgv(argv) {
169
- if (argv.length === 0) return {
170
- ok: false,
171
- error: "error: no package spec given"
172
- };
173
- const first = argv[0];
174
- if (first.startsWith("-")) {
175
- if (KNOWN_LEADING_FLAGS.has(first)) return {
176
- ok: true,
177
- args: {
178
- help: true,
179
- spec: void 0,
180
- childArgs: []
181
- }
182
- };
183
- return {
184
- ok: false,
185
- error: `error: unknown flag "${first}"`
186
- };
187
- }
188
- return {
189
- ok: true,
190
- args: {
191
- help: false,
192
- spec: first,
193
- childArgs: argv.slice(1)
194
- }
195
- };
196
- }
197
- function fallbackNotice(pkg, range) {
198
- return `upx: no installed ${pkg} satisfies "${range}", using npx`;
199
- }
200
- /** A dist-tag (`next`, `latest`) is not a version range, so the miss notice must not claim the
201
- * installed versions failed to "satisfy" it — it never could. Keeps the fixed
202
- * `upx: no installed <pkg>` prefix shared with {@link fallbackNotice}. */
203
- function distTagNotice(pkg, tag) {
204
- return `upx: no installed ${pkg}; "${tag}" is a dist-tag, using npx`;
205
- }
206
- const HELP_TEXT = `Usage: upx <pkg>@<range> [args…]
207
-
208
- Runs a package's CLI from a local or global install matching <range>, falling
209
- back to npx when nothing installed satisfies it.
210
-
211
- Example:
212
- $ upx cyberplace@^1.0.0 build
213
- `;
214
- /** Resolves `argv` (everything after `upx`) to a running child, per the Resolution algorithm in
215
- * the spec: parse → classify the range → search local-then-global → resolve the bin → spawn, or
216
- * fall back to npx with the spec exactly as given. */
217
- function runUpx(argv, fs) {
218
- const parsedArgv = parseArgv(argv);
219
- if (!parsedArgv.ok) return {
220
- kind: "error",
221
- message: parsedArgv.error
222
- };
223
- if (parsedArgv.args.help) return {
224
- kind: "help",
225
- text: HELP_TEXT
226
- };
227
- const specResult = parseSpec(parsedArgv.args.spec);
228
- if (!specResult.ok) return {
229
- kind: "error",
230
- message: specResult.error
231
- };
232
- const { pkg, range, bare } = specResult.spec;
233
- const childArgs = parsedArgv.args.childArgs;
234
- const semverRange = isSemverRange(range);
235
- if (semverRange) {
236
- const install = selectInstall(range, fs.findLocalInstalls(pkg), fs.findGlobalInstall(pkg));
237
- if (install) {
238
- const binResult = resolveBinPath(pkg, install.bin);
239
- if (!binResult.ok) return {
240
- kind: "error",
241
- message: binResult.error
242
- };
243
- const binPath = path.join(install.dir, binResult.bin);
244
- return {
245
- kind: "exit",
246
- code: fs.spawnBin(binPath, childArgs)
247
- };
248
- }
249
- }
250
- const npxSpec = bare ? pkg : `${pkg}@${range}`;
251
- return {
252
- kind: "exit",
253
- code: fs.spawnNpx([npxSpec, ...childArgs]),
254
- notice: semverRange ? fallbackNotice(pkg, range) : distTagNotice(pkg, range)
255
- };
256
- }
257
- //#endregion
258
- //#region src/run/cli.ts
259
- const outcome = runUpx(process.argv.slice(2), realRunFs());
260
- if (outcome.kind === "help") {
261
- process.stdout.write(outcome.text);
262
- process.exit(0);
263
- }
264
- if (outcome.kind === "error") {
265
- process.stderr.write(`${outcome.message}\n`);
266
- process.exit(1);
267
- }
268
- if (outcome.notice) process.stderr.write(`${outcome.notice}\n`);
269
- process.exit(outcome.code);
270
- //#endregion
271
- export {};
@@ -1,118 +0,0 @@
1
- # Adopt the open standard
2
-
3
- Convert something that is *already* a plugin — or already ships skills — onto the canonical
4
- Agent Plugins Specification manifest, without changing what it does.
5
-
6
- Two starting shapes land here:
7
-
8
- - **A vendor-specific plugin** — it has one or more hand-written vendor manifests
9
- (`.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, …) and no canonical root
10
- `plugin.json`.
11
- - **Bare public skills** — it ships `skills/<name>/SKILL.md` to users but has no plugin manifest of
12
- any kind.
13
-
14
- Adoption is **lossless by contract**: every vendor that worked before must still work after. Step 6
15
- is the check that proves it — do not skip it.
16
-
17
- ## Step 0 — Confirm the user wants this
18
-
19
- This is the skill's Phase 3 gate, and adoption always needs it: adoption rewrites the project's
20
- manifest layout and turns hand-written vendor manifests into generated artifacts. Say that plainly
21
- and get agreement before touching files. If the user declines,
22
- route back to whatever they originally asked for.
23
-
24
- Also confirm the working tree is clean (`git status`). The Step 6 diff is worthless if uncommitted
25
- changes are mixed in.
26
-
27
- ## Step 1 — Inventory what exists
28
-
29
- ```bash
30
- ls -d .claude-plugin .cursor-plugin .codex-plugin .github/plugin .plugin 2>/dev/null
31
- test -f plugin.json && cat plugin.json
32
- find . -name SKILL.md -not -path '*/node_modules/*' -not -path './.git/*'
33
- ls .mcp.json .lsp.json hooks/ commands/ agents/ rules/ output-styles/ 2>/dev/null
34
- ```
35
-
36
- Record, for each vendor manifest found: its path, and every field it sets. You are about to
37
- reproduce all of it.
38
-
39
- **If a root `plugin.json` already exists**, read it before assuming anything. It is either the
40
- canonical manifest (has `$schema` pointing at `agent-plugins.org` and an `extensions` object — in
41
- which case there is nothing to adopt; route to `update.md`, or hand off to `doctor`, instead), or a legacy
42
- Copilot CLI manifest that now collides with the canonical path and must be folded in.
43
-
44
- ## Step 2 — Sort every field into shared vs vendor-specific
45
-
46
- Build two buckets from the manifests you inventoried:
47
-
48
- - **Shared metadata** — `name`, `version`, `description`, `author`, `homepage`, `repository`,
49
- `license`, `keywords`, and the component paths. These go at the canonical top level.
50
- - **Vendor-specific** — anything only one runtime understands (Cursor's `publisher`/`category`/
51
- `tags`, Codex's `interface`, Copilot's `category`/`tags`). These go under
52
- `extensions["org.cyberuni.universal-plugin"].harnesses.<vendor>`.
53
-
54
- Where two vendor manifests disagree on a shared field, **ask the user** which value is canonical
55
- rather than picking one. A silent choice here is a silent behavior change for one of their runtimes.
56
-
57
- ## Step 3 — Write the canonical manifest
58
-
59
- Scaffold it, naming exactly the vendors you found in Step 1:
60
-
61
- ```bash
62
- node scripts/init.mjs --name <name> --vendor claude-code --vendor cursor
63
- ```
64
-
65
- Resolve `scripts/init.mjs` against this skill's directory; `npx universal-plugin plugin init` is the
66
- fallback.
67
-
68
- > `plugin init` writes a **minimal** manifest — `$schema`, `name`, and the `vendors` list. It does
69
- > not read your existing vendor manifests. Carry the Step 2 buckets in by hand afterwards.
70
-
71
- Then fill in the shared metadata and `harnesses` as laid out in
72
- [`create.md`](./create.md) Step 5. Point the component paths at the directories that already exist —
73
- adoption must not move files.
74
-
75
- For the bare-public-skills case there is no metadata to carry over; supply `name`, `description`,
76
- and `version`, set `"skills": "./skills/"`, and choose vendors with the user (see
77
- [`create.md`](./create.md) Step 2).
78
-
79
- ## Step 4 — Decide what happens to the old manifests
80
-
81
- The vendor manifests are now **build outputs**. They stay at the same paths, but they are
82
- regenerated rather than edited.
83
-
84
- - Commit them as-is first, so Step 6 has a baseline to diff against.
85
- - Tell the user they are generated from here on, and that hand-edits will be overwritten by
86
- `plugin build`.
87
- - If the project has a legacy root `plugin.json` for Copilot CLI, that path is now the canonical
88
- manifest — its derived Copilot output moves elsewhere. Check the vendor output table in
89
- [`create.md`](./create.md) Step 2 for the current path.
90
-
91
- ## Step 5 — Build
92
-
93
- ```bash
94
- npx universal-plugin plugin build
95
- ```
96
-
97
- ## Step 6 — Prove it was lossless
98
-
99
- This is the point of the whole procedure.
100
-
101
- ```bash
102
- git diff -- .claude-plugin .cursor-plugin .codex-plugin .github/plugin
103
- ```
104
-
105
- Read every line of that diff. Expect only formatting and key-order churn.
106
-
107
- **Any field that disappeared is a regression**, not a cleanup. Trace it back: either it belongs in
108
- the shared metadata, or it belongs in that vendor's `harnesses` entry, or it is a field the build
109
- does not yet support — in which case stop and tell the user rather than shipping a quiet
110
- capability loss.
111
-
112
- Then confirm the plugin still loads. See [`create.md`](./create.md) Step 8 for local install.
113
-
114
- ## Step 7 — Hand off
115
-
116
- - Audit the skills: [`create.md`](./create.md) Step 6.
117
- - Shipping it on npm? → `migrate-plugin`.
118
- - Listing it in the marketplace? → `publish-plugin`.
@@ -1,53 +0,0 @@
1
- # GitHub Copilot CLI
2
-
3
- Reads the **canonical root `plugin.json` directly**. The build derives nothing for it and writes no
4
- file — `plugin build` reports it with status `canonical`, which is success, not a skipped target.
5
-
6
- ## Why nothing is derived
7
-
8
- Copilot CLI searches four paths and takes the first match:
9
-
10
- ```
11
- .plugin/plugin.json → plugin.json → .github/plugin/plugin.json → .claude-plugin/plugin.json
12
- ```
13
-
14
- Root `plugin.json` — the canonical manifest — is second, so it always shadows the two below it. It
15
- has consumed Open Plugin Spec v1 manifests since v1.0.74, so it already serves the canonical manifest
16
- as-is.
17
-
18
- Earlier builds wrote `.github/plugin/plugin.json`. That path loses to root by construction and was
19
- never read; a leftover copy is stale and safe to delete.
20
-
21
- ## Vendor-specific fields cannot be delivered
22
-
23
- A `harnesses["copilot-cli"]` entry has nowhere to go. The canonical schema is closed
24
- (`additionalProperties: false`), so a Copilot-only field cannot ride along in root, and there is no
25
- derived file to put it in. The build warns:
26
-
27
- ```
28
- harnesses.copilot-cli sets category, tags, but copilot-cli reads the canonical plugin.json
29
- directly — these fields are not delivered
30
- ```
31
-
32
- Treat that warning as a decision to make, not noise: either the field belongs to a vendor that has a
33
- derived manifest, or it does not ship. Do not invent a path for it.
34
-
35
- ## Do not
36
-
37
- - **Do not delete root `plugin.json` to "clean up" a Copilot target.** It is the source of truth and
38
- the Copilot manifest at once.
39
- - Do not write `.plugin/plugin.json`. It outranks root, so it would silently shadow the canonical
40
- manifest with a copy nothing regenerates.
41
-
42
- ## Hooks
43
-
44
- Copilot CLI accepts **either casing**, and the casing selects the payload format: PascalCase gets the
45
- Claude-compatible format, so the canonical file reaches Copilot CLI unchanged. Because Copilot CLI
46
- reads that file directly, the build derives nothing for it — an `agent` handler is reported as ignored
47
- at runtime rather than dropped. See [`claude-code.md`](./claude-code.md).
48
-
49
- ## Dependencies
50
-
51
- Copilot CLI reads no plugin dependency. Because it reads the canonical manifest directly, there is no
52
- derived file to leave the declaration out of — it sits under `extensions`, which Copilot CLI ignores,
53
- and the build reports it as ignored at runtime. See [`claude-code.md`](./claude-code.md).