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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/bin/upx.mjs +12 -5
- package/com.github.copilot/agents/agentskills-specialist.agent.md +132 -0
- package/dist/cli.mjs +5806 -261
- package/governances/plugin-design.md +14 -0
- package/package.json +4 -1
- package/plugin.json +1 -1
- package/readme.md +12 -5
- package/schema/README.md +10 -0
- package/schema/claude-code-marketplace.json +1939 -0
- package/schema/extension.schema.json +842 -0
- package/skills/adopt-upx/README.md +4 -4
- package/skills/adopt-upx/SKILL.md +4 -4
- package/skills/doctor/README.md +3 -4
- package/skills/doctor/SKILL.md +44 -14
- package/skills/doctor/scripts/doctor.mjs +145 -37
- package/skills/{init → init-universal-plugin}/README.md +5 -2
- package/skills/{init → init-universal-plugin}/SKILL.md +38 -13
- package/skills/init-universal-plugin/references/adopt.md +206 -0
- package/skills/{init → init-universal-plugin}/references/detection.md +8 -2
- package/skills/{init → init-universal-plugin}/references/standard.md +5 -1
- package/skills/{init → init-universal-plugin}/references/vendors/claude-code.md +1 -1
- package/skills/init-universal-plugin/references/vendors/copilot-cli.md +201 -0
- package/skills/marketplace/SKILL.md +1 -1
- package/skills/migrate-plugin/SKILL.md +140 -26
- package/skills/migrate-plugin/evals/evals.json +6 -0
- package/skills/migrate-plugin/evals/trigger-queries.json +18 -0
- package/skills/publish-plugin/README.md +35 -0
- package/skills/publish-plugin/SKILL.md +118 -19
- package/skills/publish-plugin/evals/evals.json +12 -0
- package/skills/remove-plugin/README.md +1 -1
- package/skills/remove-plugin/SKILL.md +2 -2
- package/skills/version/SKILL.md +2 -2
- package/dist/run.mjs +0 -271
- package/skills/init/references/adopt.md +0 -118
- package/skills/init/references/vendors/copilot-cli.md +0 -53
- /package/skills/{init → init-universal-plugin}/assets/templates/agent.md +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/command.md +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/hooks.json +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/plugin.json +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/setup-command.md +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/skill.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/create.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/frontmatter.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/update.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/vendors/codex.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/vendors/cursor.md +0 -0
- /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).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|