universal-plugin 0.12.2 → 0.14.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/dist/cli.mjs +5197 -4028
- package/package.json +2 -1
- package/plugin.json +30 -1
- package/readme.md +7 -6
- package/references/plugin-design.md +21 -9
- package/schema/extension.schema.json +31 -0
- package/skills/adopt-upx/SKILL.md +4 -2
- package/skills/adopt-upx/scripts/rewrite-upx.mjs +92 -129
- package/skills/build-plugin/README.md +1 -1
- package/skills/build-plugin/SKILL.md +17 -8
- package/skills/build-plugin/scripts/build.mjs +51 -10
- package/skills/doctor-universal-plugin/SKILL.md +53 -23
- package/skills/doctor-universal-plugin/scripts/doctor.mjs +249 -365
- package/skills/init-universal-plugin/SKILL.md +13 -2
- package/skills/init-universal-plugin/references/create.md +9 -6
- package/skills/init-universal-plugin/references/standard.md +1 -1
- package/skills/init-universal-plugin/references/vendors/copilot-cli.md +2 -2
- package/skills/init-universal-plugin/scripts/init.mjs +51 -10
- package/skills/marketplace/references/init.md +1 -1
- package/skills/marketplace/scripts/add.mjs +51 -10
- package/skills/marketplace/scripts/install-docs.mjs +100 -96
- package/skills/marketplace/scripts/marketplace.mjs +51 -10
- package/skills/marketplace/scripts/validate.mjs +51 -10
- package/skills/migrate-plugin/SKILL.md +1 -1
- package/skills/remove-plugin/SKILL.md +1 -1
- package/skills/upgrade-plugin/SKILL.md +2 -0
- package/skills/version/README.md +24 -18
- package/skills/version/SKILL.md +80 -47
- package/skills/version/scripts/version.mjs +51 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Universal AI agent plugin build tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-plugin",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@repobuddy/upx": "^0.2.0",
|
|
44
44
|
"@toon-format/toon": "^4.1.1",
|
|
45
45
|
"commander": "^15.0.0",
|
|
46
|
+
"rolldown": "^1.2.11",
|
|
46
47
|
"semver": "^7.8.1"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
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.
|
|
4
|
+
"version": "0.14.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"
|
|
@@ -19,6 +19,35 @@
|
|
|
19
19
|
"cursor": {},
|
|
20
20
|
"codex": {},
|
|
21
21
|
"copilot-cli": {}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"skills/adopt-upx/scripts/rewrite-upx.mjs": "src/skill-scripts/rewrite-upx.ts",
|
|
25
|
+
"skills/doctor-universal-plugin/scripts/doctor.mjs": "src/skill-scripts/doctor.ts",
|
|
26
|
+
"skills/marketplace/scripts/install-docs.mjs": "src/skill-scripts/install-docs.ts",
|
|
27
|
+
"skills/build-plugin/scripts/build.mjs": {
|
|
28
|
+
"source": "src/skill-scripts/run-cli.ts",
|
|
29
|
+
"command": "plugin build"
|
|
30
|
+
},
|
|
31
|
+
"skills/init-universal-plugin/scripts/init.mjs": {
|
|
32
|
+
"source": "src/skill-scripts/run-cli.ts",
|
|
33
|
+
"command": "plugin init"
|
|
34
|
+
},
|
|
35
|
+
"skills/version/scripts/version.mjs": {
|
|
36
|
+
"source": "src/skill-scripts/run-cli.ts",
|
|
37
|
+
"command": "plugin version"
|
|
38
|
+
},
|
|
39
|
+
"skills/marketplace/scripts/add.mjs": {
|
|
40
|
+
"source": "src/skill-scripts/run-cli.ts",
|
|
41
|
+
"command": "marketplace add"
|
|
42
|
+
},
|
|
43
|
+
"skills/marketplace/scripts/marketplace.mjs": {
|
|
44
|
+
"source": "src/skill-scripts/run-cli.ts",
|
|
45
|
+
"command": "marketplace init"
|
|
46
|
+
},
|
|
47
|
+
"skills/marketplace/scripts/validate.mjs": {
|
|
48
|
+
"source": "src/skill-scripts/run-cli.ts",
|
|
49
|
+
"command": "marketplace validate"
|
|
50
|
+
}
|
|
22
51
|
}
|
|
23
52
|
}
|
|
24
53
|
}
|
package/readme.md
CHANGED
|
@@ -39,8 +39,9 @@ npx universal-plugin plugin init --npm # also wire an npm package to s
|
|
|
39
39
|
npx universal-plugin plugin build # generate vendor manifests
|
|
40
40
|
npx universal-plugin plugin install # install the working copy into the runtimes it targets
|
|
41
41
|
npx universal-plugin plugin uninstall # take it back out
|
|
42
|
-
npx universal-plugin plugin version <bump> #
|
|
43
|
-
npx universal-plugin plugin
|
|
42
|
+
npx universal-plugin plugin version <bump> # stamp the version into every place it lives (author bump)
|
|
43
|
+
npx universal-plugin plugin version 1.4.0 # explicit number (semantic-release prepareCmd)
|
|
44
|
+
npx universal-plugin plugin version --from-package # take package.json's version (changesets, release-please)
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
`build` writes `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, and
|
|
@@ -69,11 +70,11 @@ npx universal-plugin sync apply <action-id>
|
|
|
69
70
|
### publish
|
|
70
71
|
|
|
71
72
|
```sh
|
|
72
|
-
npx universal-plugin publish
|
|
73
|
+
npx universal-plugin publish check-version # fail when any place carries a different version (read-only, for CI)
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
`publish sync-version` and `plugin bundle` are deprecated aliases of `plugin version --from-package`
|
|
77
|
+
and `plugin version`. They print a warning and are removed in the next minor release.
|
|
77
78
|
|
|
78
79
|
### marketplace
|
|
79
80
|
|
|
@@ -136,7 +137,7 @@ upx cyber-skills@^2 audit validate
|
|
|
136
137
|
```
|
|
137
138
|
|
|
138
139
|
Use a caret range on the major rather than an exact pin, so one global install serves every caller.
|
|
139
|
-
`
|
|
140
|
+
The `adopt-upx` skill rewrites references to this form.
|
|
140
141
|
|
|
141
142
|
`upx` only works once it is installed globally. `npx` ships with npm, so keep `npx` as the default
|
|
142
143
|
anywhere you cannot guarantee that install.
|
|
@@ -247,17 +247,18 @@ If the repo needs explicit symlink tracking: `mcp.json symlink` in `.gitattribut
|
|
|
247
247
|
|
|
248
248
|
### Pinning a self-published server to the plugin version
|
|
249
249
|
|
|
250
|
-
A plugin whose MCP server is its own npm package writes an unpinned invocation — `{"command": "npx", "args": ["-y", "my-server", "mcp"]}` — and a consumer on plugin 1.4.0 then gets 1.4.0's skills alongside whatever `npx` resolves as latest for the server. Mark the entry and `
|
|
250
|
+
A plugin whose MCP server is its own npm package writes an unpinned invocation — `{"command": "npx", "args": ["-y", "my-server", "mcp"]}` — and a consumer on plugin 1.4.0 then gets 1.4.0's skills alongside whatever `npx` resolves as latest for the server. Mark the entry and `plugin version` stamps the version on at release:
|
|
251
251
|
|
|
252
252
|
```json
|
|
253
253
|
{ "command": "npx", "args": ["-y", "my-server", "mcp"], "pinToPluginVersion": true }
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
- **The marker is required.**
|
|
257
|
-
- **The marker never reaches a runtime.** It is a
|
|
258
|
-
-
|
|
259
|
-
- A marked entry
|
|
260
|
-
- `
|
|
256
|
+
- **The marker is required.** Stamping never name-matches — a plugin may publish its server under a package name that is not the plugin's, and an unrelated `npx -y widget-cli` must never be stamped with this plugin's version.
|
|
257
|
+
- **The marker never reaches a runtime.** It is a directive, not part of any vendor's schema, so `build` strips it from every derived manifest and derived `mcp.json` and copies the entry otherwise as authored. `build` never decides a version.
|
|
258
|
+
- **`plugin version` writes the pin** into the authored entry, inline or in the mcp file, and keeps the marker. An already-pinned specifier is overwritten with the new version. Do not hand-pin a marked entry.
|
|
259
|
+
- A marked entry that cannot be pinned — the `command` is not `npx`/`upx`, or `args` carry no package specifier — is warned about and left alone.
|
|
260
|
+
- `publish check-version` and `doctor` compare marked entries too, so a stale pin is reported as drift.
|
|
261
|
+
- `copilot-cli` reads the canonical manifest directly, so the stamped authored entry is what it sees.
|
|
261
262
|
|
|
262
263
|
## Environment Variable Mapping
|
|
263
264
|
|
|
@@ -278,10 +279,20 @@ A plugin whose MCP server is its own npm package writes an unpinned invocation
|
|
|
278
279
|
|
|
279
280
|
**Rules (Cursor-only):** `.mdc` files in `rules/`. Required frontmatter: `description`. Optional: `alwaysApply` (bool), `globs` (file-pattern array). Bundle `commands/setup.md` to merge rule content into project's `AGENTS.md` — after that merge, `.mdc` files are redundant.
|
|
280
281
|
|
|
281
|
-
|
|
282
|
+
## Rules vs References
|
|
283
|
+
|
|
284
|
+
"Always-on" assumes one agent per session. In an orchestrated system, a team of subagents each with its own focus, always-on content is injected into every subagent whatever its role. Anything that is not true for every role costs context and dilutes focus.
|
|
285
|
+
|
|
286
|
+
- **Always-on content is the HCF (highest common factor) of all agents.** It holds only the constraints every agent in every role must follow, such as "never commit secrets" or "use Conventional Commits". That set is small.
|
|
287
|
+
- **`rules/` is for the HCF only.** It stays for backward compatibility and for simple single-agent setups where no orchestration is involved. Its scope is narrow on purpose.
|
|
288
|
+
- **Shared guidance is a reference, not a rule.** Content that cuts across components but applies only to some roles or tasks (a design standard, a review checklist, a workflow discipline) belongs in `references/<name>.md`. Whichever agent, skill, command, hook, or MCP server needs it loads it by name on demand, through the `reference` skill in the `cyber-agent-harness` plugin.
|
|
289
|
+
|
|
290
|
+
**Decision tree for guidance:**
|
|
291
|
+
- Every agent, every role (the HCF) → merge into **AGENTS.md**. For Cursor-only delivery, use `rules/` + `commands/setup.md`.
|
|
292
|
+
- Cross-cutting, scoped to a role or task → `references/<name>.md`, loaded by name
|
|
282
293
|
- Situation-triggered → **skill** (all agents)
|
|
283
|
-
-
|
|
284
|
-
-
|
|
294
|
+
- Agent-specific → the agent definition file
|
|
295
|
+
- Skill-specific → the `SKILL.md` body
|
|
285
296
|
|
|
286
297
|
## Namespacing
|
|
287
298
|
|
|
@@ -312,6 +323,7 @@ Default scope: **team**.
|
|
|
312
323
|
- Relying on extended component types (commands, rules, agents, hooks) for core functionality — silently ignored on non-supporting hosts
|
|
313
324
|
- Duplicating SKILL.md content in `plugin.json` — the manifest is a path index, never a content mirror
|
|
314
325
|
- Using `rules/` for cross-agent always-on guidance — use AGENTS.md instead
|
|
326
|
+
- Putting role- or task-scoped guidance in always-on context (`rules/` or AGENTS.md) — make it a reference loaded on demand
|
|
315
327
|
- Putting install-time metadata in `plugin.json` instead of `skill.json`
|
|
316
328
|
|
|
317
329
|
## Cross-Platform Portability
|
|
@@ -53,6 +53,37 @@
|
|
|
53
53
|
"outputStyles": {
|
|
54
54
|
"$ref": "#/$defs/pathValue",
|
|
55
55
|
"description": "Output style resources. Default: ./output-styles/."
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": "Skill script entries 'plugin build' compiles into self-contained .mjs files. Keyed by the output path, relative to the plugin root (e.g. skills/doctor/scripts/doctor.mjs); each value is the source path, relative to the plugin root, or { source, command } where command is the CLI command path the entry runs. Never written to a derived manifest.",
|
|
60
|
+
"propertyNames": {
|
|
61
|
+
"pattern": "\\.mjs$"
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": {
|
|
64
|
+
"oneOf": [
|
|
65
|
+
{
|
|
66
|
+
"type": "string",
|
|
67
|
+
"minLength": 1
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "object",
|
|
71
|
+
"required": ["source"],
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"properties": {
|
|
74
|
+
"source": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"minLength": 1
|
|
77
|
+
},
|
|
78
|
+
"command": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"minLength": 1,
|
|
81
|
+
"description": "The CLI command path this entry runs (e.g. 'plugin build'). Every compiled script carries the map of these to their entries."
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
56
87
|
}
|
|
57
88
|
},
|
|
58
89
|
"$defs": {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: adopt-upx
|
|
3
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
|
+
metadata:
|
|
5
|
+
pin-exempt: true
|
|
4
6
|
---
|
|
5
7
|
|
|
6
8
|
# Adopt upx
|
|
7
9
|
|
|
8
10
|
Rewrites `npx <pkg>@<version>` references inside `SKILL.md` files to a caret range on `upx` — the
|
|
9
11
|
fast local-first runner shipped by `universal-plugin` (see the package
|
|
10
|
-
[`readme.md`](
|
|
12
|
+
[`readme.md`](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/readme.md#upx-the-fast-package-runner) for the `upx` contract).
|
|
11
13
|
|
|
12
14
|
## When to use
|
|
13
15
|
|
|
@@ -51,7 +53,7 @@ Left alone (never rewritten):
|
|
|
51
53
|
installed version; these already go straight to `npx` inside `upx` itself on a miss)
|
|
52
54
|
- **Already-`upx` references** — nothing to do
|
|
53
55
|
- **Any skill marked `pin-exempt: true`** in its frontmatter — its version strings are
|
|
54
|
-
documentation/illustration, not real invocations. This mirrors how `plugin
|
|
56
|
+
documentation/illustration, not real invocations. This mirrors how `plugin version` treats
|
|
55
57
|
pin-exempt skills; `upgrade-plugin` is a live example.
|
|
56
58
|
|
|
57
59
|
## Choose scope
|
|
@@ -1,168 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// - any SKILL.md whose frontmatter declares `pin-exempt: true` — its versions are illustration
|
|
19
|
-
// (the same convention the plugin bundler uses)
|
|
20
|
-
//
|
|
21
|
-
// Idempotent: a second run finds no more `npx <pkg>@<semver>` occurrences to rewrite.
|
|
22
|
-
|
|
23
|
-
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
24
|
-
import { join, resolve } from 'node:path'
|
|
25
|
-
|
|
26
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.turbo'])
|
|
27
|
-
|
|
28
|
-
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
29
|
-
const PIN_EXEMPT_PATTERN = /(^|\n)\s*pin-exempt:\s*true\s*(\n|$)/
|
|
30
|
-
|
|
31
|
-
// Matches `npx <pkg>@<version>` — mirrors the pattern `plugin bundle` uses for pin rewriting,
|
|
32
|
-
// but only for the `npx` runner word (upx refs are already fast, nothing to do). The package
|
|
33
|
-
// class includes `@` and `/` so a scoped package (`@acme/cli@1.2.3`) matches; greedy matching
|
|
34
|
-
// backtracks to the LAST `@`, which separates the version.
|
|
35
|
-
const NPX_REF_PATTERN = /\bnpx(\s+(?:--yes\s+|-y\s+)?)([@a-z0-9/._-]+)@([^\s`'")]+)/g
|
|
36
|
-
|
|
37
|
-
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
38
|
-
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/skill-scripts/rewrite-upx.ts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.14.0","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
//#region src/skill-scripts/rewrite-upx.ts
|
|
7
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
".turbo"
|
|
13
|
+
]);
|
|
14
|
+
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
15
|
+
const PIN_EXEMPT_PATTERN = /(^|\n)\s*pin-exempt:\s*true\s*(\n|$)/;
|
|
16
|
+
const NPX_REF_PATTERN = /\bnpx(\s+(?:--yes\s+|-y\s+)?)([@a-z0-9/._-]+)@([^\s`'")]+)/g;
|
|
17
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
39
18
|
function isPinExempt(content) {
|
|
40
|
-
const match = FRONTMATTER_PATTERN.exec(content)
|
|
41
|
-
if (!match) return false
|
|
42
|
-
return PIN_EXEMPT_PATTERN.test(match[1])
|
|
19
|
+
const match = FRONTMATTER_PATTERN.exec(content);
|
|
20
|
+
if (!match) return false;
|
|
21
|
+
return PIN_EXEMPT_PATTERN.test(match[1]);
|
|
43
22
|
}
|
|
44
|
-
|
|
45
|
-
// `>=1.0.0` → `^<major>` (any release in that major). A `0.x` version is special: under semver a
|
|
46
|
-
// `0.x` minor bump is a breaking change, so `^0` (= `>=0.0.0 <1.0.0`) is far too loose — pin the
|
|
47
|
-
// minor instead with `^0.<minor>` (= that `0.<minor>.x` line only).
|
|
48
23
|
function caretRange(version) {
|
|
49
|
-
const [major, minor] = version.split(
|
|
50
|
-
return major ===
|
|
24
|
+
const [major, minor] = version.split(".");
|
|
25
|
+
return major === "0" ? `^0.${minor}` : `^${major}`;
|
|
51
26
|
}
|
|
52
|
-
|
|
53
27
|
function rewriteContent(content) {
|
|
54
|
-
let changed = 0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
28
|
+
let changed = 0;
|
|
29
|
+
return {
|
|
30
|
+
next: content.replace(NPX_REF_PATTERN, (whole, ws, pkg, version) => {
|
|
31
|
+
if (!SEMVER_PATTERN.test(version)) return whole;
|
|
32
|
+
changed++;
|
|
33
|
+
return `upx${ws}${pkg}@${caretRange(version)}`;
|
|
34
|
+
}),
|
|
35
|
+
changed
|
|
36
|
+
};
|
|
61
37
|
}
|
|
62
|
-
|
|
63
38
|
function findSkillFiles(root) {
|
|
64
|
-
const out = []
|
|
65
|
-
const st = statSync(root, { throwIfNoEntry: false })
|
|
66
|
-
if (!st) return out
|
|
39
|
+
const out = [];
|
|
40
|
+
const st = statSync(root, { throwIfNoEntry: false });
|
|
41
|
+
if (!st) return out;
|
|
67
42
|
if (st.isFile()) {
|
|
68
|
-
if (root.endsWith(
|
|
69
|
-
return out
|
|
43
|
+
if (root.endsWith("SKILL.md")) out.push(root);
|
|
44
|
+
return out;
|
|
70
45
|
}
|
|
71
46
|
if (st.isDirectory()) {
|
|
72
|
-
const direct = join(root,
|
|
47
|
+
const direct = join(root, "SKILL.md");
|
|
73
48
|
if (statSync(direct, { throwIfNoEntry: false })?.isFile()) {
|
|
74
|
-
out.push(direct)
|
|
75
|
-
return out
|
|
49
|
+
out.push(direct);
|
|
50
|
+
return out;
|
|
76
51
|
}
|
|
77
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
78
|
-
if (entry.
|
|
79
|
-
|
|
80
|
-
out.push(...findSkillFiles(join(root, entry.name)))
|
|
81
|
-
}
|
|
52
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) if (entry.isDirectory()) {
|
|
53
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
54
|
+
out.push(...findSkillFiles(join(root, entry.name)));
|
|
82
55
|
}
|
|
83
56
|
}
|
|
84
|
-
return out
|
|
57
|
+
return out;
|
|
85
58
|
}
|
|
86
|
-
|
|
87
|
-
// Minimal glob: only `*` within a single path segment (no `**`). Enough for named-set patterns
|
|
88
|
-
// like `skills/foo-*` or `skills/*/SKILL.md`.
|
|
89
59
|
function expandGlob(pattern) {
|
|
90
|
-
if (!pattern.includes(
|
|
91
|
-
const segments = pattern.split(
|
|
92
|
-
let bases = [
|
|
60
|
+
if (!pattern.includes("*")) return null;
|
|
61
|
+
const segments = pattern.split("/");
|
|
62
|
+
let bases = ["."];
|
|
93
63
|
for (const seg of segments) {
|
|
94
|
-
if (!seg.includes(
|
|
95
|
-
bases = bases.map((b) => join(b, seg))
|
|
96
|
-
continue
|
|
64
|
+
if (!seg.includes("*")) {
|
|
65
|
+
bases = bases.map((b) => join(b, seg));
|
|
66
|
+
continue;
|
|
97
67
|
}
|
|
98
|
-
const re = new RegExp(
|
|
99
|
-
const next = []
|
|
68
|
+
const re = new RegExp("^" + seg.split("*").map(escapeRegExp).join(".*") + "$");
|
|
69
|
+
const next = [];
|
|
100
70
|
for (const b of bases) {
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
for (const entry of readdirSync(b)) {
|
|
104
|
-
if (re.test(entry)) next.push(join(b, entry))
|
|
105
|
-
}
|
|
71
|
+
if (!statSync(b, { throwIfNoEntry: false })?.isDirectory()) continue;
|
|
72
|
+
for (const entry of readdirSync(b)) if (re.test(entry)) next.push(join(b, entry));
|
|
106
73
|
}
|
|
107
|
-
bases = next
|
|
74
|
+
bases = next;
|
|
108
75
|
}
|
|
109
|
-
return bases
|
|
76
|
+
return bases;
|
|
110
77
|
}
|
|
111
|
-
|
|
112
78
|
function escapeRegExp(value) {
|
|
113
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g,
|
|
79
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
80
|
}
|
|
115
|
-
|
|
116
81
|
function resolveTargets(args) {
|
|
117
|
-
const all = args.includes(
|
|
118
|
-
const dryRun = args.includes(
|
|
119
|
-
const positional = args.filter((a) => a !==
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
82
|
+
const all = args.includes("--all");
|
|
83
|
+
const dryRun = args.includes("--dry-run");
|
|
84
|
+
const positional = args.filter((a) => a !== "--all" && a !== "--dry-run");
|
|
85
|
+
if (all) return {
|
|
86
|
+
files: findSkillFiles("."),
|
|
87
|
+
dryRun
|
|
88
|
+
};
|
|
89
|
+
const files = [];
|
|
124
90
|
for (const arg of positional) {
|
|
125
|
-
const globbed = expandGlob(arg)
|
|
126
|
-
if (globbed)
|
|
127
|
-
|
|
128
|
-
} else {
|
|
129
|
-
files.push(...findSkillFiles(arg))
|
|
130
|
-
}
|
|
91
|
+
const globbed = expandGlob(arg);
|
|
92
|
+
if (globbed) for (const g of globbed) files.push(...findSkillFiles(g));
|
|
93
|
+
else files.push(...findSkillFiles(arg));
|
|
131
94
|
}
|
|
132
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
files: [...new Set(files)],
|
|
97
|
+
dryRun
|
|
98
|
+
};
|
|
133
99
|
}
|
|
134
|
-
|
|
135
100
|
function main() {
|
|
136
|
-
const args = process.argv.slice(2)
|
|
101
|
+
const args = process.argv.slice(2);
|
|
137
102
|
if (args.length === 0) {
|
|
138
|
-
console.error(
|
|
139
|
-
process.exit(1)
|
|
103
|
+
console.error("usage: rewrite-upx.mjs (--all | <path-or-glob>...) [--dry-run]");
|
|
104
|
+
process.exit(1);
|
|
140
105
|
}
|
|
141
|
-
|
|
142
|
-
const { files, dryRun } = resolveTargets(args)
|
|
106
|
+
const { files, dryRun } = resolveTargets(args);
|
|
143
107
|
if (files.length === 0) {
|
|
144
|
-
console.error(
|
|
145
|
-
process.exit(0)
|
|
108
|
+
console.error("no SKILL.md files matched — nothing to do");
|
|
109
|
+
process.exit(0);
|
|
146
110
|
}
|
|
147
|
-
|
|
148
|
-
let
|
|
149
|
-
let skippedExempt = 0
|
|
111
|
+
let touched = 0;
|
|
112
|
+
let skippedExempt = 0;
|
|
150
113
|
for (const file of files) {
|
|
151
|
-
const abs = resolve(file)
|
|
152
|
-
const content = readFileSync(abs,
|
|
114
|
+
const abs = resolve(file);
|
|
115
|
+
const content = readFileSync(abs, "utf8");
|
|
153
116
|
if (isPinExempt(content)) {
|
|
154
|
-
skippedExempt
|
|
155
|
-
console.log(`skip (pin-exempt): ${file}`)
|
|
156
|
-
continue
|
|
117
|
+
skippedExempt++;
|
|
118
|
+
console.log(`skip (pin-exempt): ${file}`);
|
|
119
|
+
continue;
|
|
157
120
|
}
|
|
158
|
-
const { next, changed } = rewriteContent(content)
|
|
159
|
-
if (changed === 0) continue
|
|
160
|
-
touched
|
|
161
|
-
console.log(`${dryRun ?
|
|
162
|
-
if (!dryRun) writeFileSync(abs, next,
|
|
121
|
+
const { next, changed } = rewriteContent(content);
|
|
122
|
+
if (changed === 0) continue;
|
|
123
|
+
touched++;
|
|
124
|
+
console.log(`${dryRun ? "[dry-run] " : ""}rewrite (${changed}): ${file}`);
|
|
125
|
+
if (!dryRun) writeFileSync(abs, next, "utf8");
|
|
163
126
|
}
|
|
164
|
-
|
|
165
|
-
console.log(`\n${touched} file(s) rewritten, ${skippedExempt} skipped (pin-exempt), ${files.length} scanned.`)
|
|
127
|
+
console.log(`\n${touched} file(s) rewritten, ${skippedExempt} skipped (pin-exempt), ${files.length} scanned.`);
|
|
166
128
|
}
|
|
167
|
-
|
|
168
|
-
|
|
129
|
+
main();
|
|
130
|
+
//#endregion
|
|
131
|
+
export {};
|
|
@@ -22,7 +22,7 @@ rather than reported as success.
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
24
|
Ships with the `universal-plugin` plugin. It is invoked in Claude Code as
|
|
25
|
-
`/universal-plugin:build-plugin
|
|
25
|
+
`/universal-plugin:build-plugin` (pnpm install: `pnpm-universal-plugin-build-plugin`).
|
|
26
26
|
|
|
27
27
|
## References
|
|
28
28
|
|
|
@@ -15,6 +15,7 @@ beside it is **derived**, and this build is the one step that writes all of it:
|
|
|
15
15
|
| `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json` | Claude Code, Cursor, Codex |
|
|
16
16
|
| `com.github.copilot/` (agents, commands, rules, hooks, LSP) | Copilot CLI in spec mode; the manifest itself is root `plugin.json` |
|
|
17
17
|
| this plugin's entry in each marketplace catalog the repository already carries | anyone installing from the repository |
|
|
18
|
+
| each declared `scripts` entry, compiled into one self-contained `.mjs` in its skill | the skill, wherever it was linked or copied to |
|
|
18
19
|
|
|
19
20
|
So a change to the plugin is not done until it has been built. A runtime reads the derived files,
|
|
20
21
|
not the canonical manifest, and a derived file that predates the change still ships the old plugin.
|
|
@@ -38,15 +39,15 @@ grep -n "plugin build" package.json
|
|
|
38
39
|
|
|
39
40
|
If a script already runs `plugin build`, run that script (for example `pnpm plugin:build`). It
|
|
40
41
|
carries the project's own `--root` and whatever it chains after the build, such as a formatter over
|
|
41
|
-
the written JSON. Otherwise run
|
|
42
|
+
the written JSON. Otherwise run this skill's launcher:
|
|
42
43
|
|
|
43
44
|
```bash
|
|
44
45
|
node scripts/build.mjs --root <plugin-root>
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
Resolve `scripts/build.mjs` against this skill's own directory. It runs the CLI
|
|
48
|
-
|
|
49
|
-
`npx universal-plugin
|
|
48
|
+
Resolve `scripts/build.mjs` against this skill's own directory. It runs the CLI the project installed
|
|
49
|
+
(`pnpm exec universal-plugin`), else the CLI pinned to the version this skill shipped with
|
|
50
|
+
(`npx universal-plugin@0.14.0`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
|
|
50
51
|
must name the directory that holds the canonical `plugin.json`.
|
|
51
52
|
|
|
52
53
|
The command never prompts, so it is safe to run unattended.
|
|
@@ -73,6 +74,12 @@ Default stdout is TOON: one row per vendor (`vendor, path, status`), one row per
|
|
|
73
74
|
| `canonical` | Copilot CLI with nothing to derive: root `plugin.json` serves it directly |
|
|
74
75
|
| `skipped` / `failed` | the row says why; fix that and rebuild |
|
|
75
76
|
|
|
77
|
+
A script row (`path, status`) reads `compiled` or `unchanged`. On `--dry-run` it reads `unchanged`,
|
|
78
|
+
`missing`, `stale` (its source moved on), or `version` (only its embedded version did, which
|
|
79
|
+
`publish check-version` reports). A `failed` row is an entry that could not be made self-contained,
|
|
80
|
+
usually an import that does not resolve; the build exits 1 and the warning names it. A script over
|
|
81
|
+
150 KB warns: the skill should call the installed CLI instead of compiling it in.
|
|
82
|
+
|
|
76
83
|
A catalog row reads `updated`, `unchanged`, or `planned` on `--dry-run`. A build **never creates** a
|
|
77
84
|
catalog, and it keeps an entry's non-local source, such as an npm package, while it re-derives the
|
|
78
85
|
version. To add a catalog or change a source, use `marketplace`.
|
|
@@ -84,7 +91,8 @@ declares a `source` is listed in the catalog by the build itself.
|
|
|
84
91
|
|
|
85
92
|
Read stderr too. The build drops what a vendor cannot represent and warns instead of failing: a hook
|
|
86
93
|
handler type a vendor cannot run, a `harnesses.copilot-cli` override with no delivery path, an
|
|
87
|
-
invalid catalog, a dependency the catalog cannot resolve
|
|
94
|
+
invalid catalog, a dependency the catalog cannot resolve, a skill link that climbs out of the skill
|
|
95
|
+
folder. Each warning names something that will not reach a runtime. Tell the user each
|
|
88
96
|
one; do not bury it in a summary.
|
|
89
97
|
|
|
90
98
|
`built 0` with "nothing to build" is not success. The manifest declares no vendor, so hand it to
|
|
@@ -100,7 +108,7 @@ derived files together:
|
|
|
100
108
|
git status --short
|
|
101
109
|
```
|
|
102
110
|
|
|
103
|
-
Expect the vendor manifests, `com.github.copilot/`, and any
|
|
111
|
+
Expect the vendor manifests, `com.github.copilot/`, any refreshed catalog, and any compiled script. A diff outside those paths did not come from the build.
|
|
104
112
|
|
|
105
113
|
## Guards
|
|
106
114
|
|
|
@@ -120,8 +128,8 @@ Validation runs before the first write, so a failed build leaves the tree untouc
|
|
|
120
128
|
- **Edit `plugin.json` to get a different derived manifest.** A per-vendor field belongs under
|
|
121
129
|
`extensions["org.cyberuni.universal-plugin"].harnesses.<vendor>`. That is a declaration, and
|
|
122
130
|
`init-universal-plugin` owns it.
|
|
123
|
-
- **Expect a build to pin skill `npx` references
|
|
124
|
-
this step.
|
|
131
|
+
- **Expect a build to pin skill `npx` references or decide a version.** Build derives and copies
|
|
132
|
+
whatever `plugin.json` carries. Stamping is the release-time `plugin version`, not this step.
|
|
125
133
|
- **Expect a build to copy governances into skills.** A skill loads a reference through the
|
|
126
134
|
`reference` skill in the `cyber-agent-harness` plugin; the build does not copy them and has no
|
|
127
135
|
`--check` flag.
|
|
@@ -141,3 +149,4 @@ Validation runs before the first write, so a failed build leaves the tree untouc
|
|
|
141
149
|
- Spec: [`plugin/build/`](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/plugin/build/README.md)
|
|
142
150
|
- [ADR-0014](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0014-build-refreshes-catalogs.md): the build refreshes catalogs it finds
|
|
143
151
|
- [ADR-0017](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0017-retire-governance-for-reference.md): the build no longer copies governances into skills
|
|
152
|
+
- [ADR-0019](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0019-build-compiles-skill-scripts.md): the build compiles skill script entries into committed `.mjs`
|
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.14.0","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
//#region src/compile/scripts.ts
|
|
6
|
+
/** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
|
|
7
|
+
* library it inlines, reads it; universal-plugin depends on neither. */
|
|
8
|
+
const SCRIPT_GLOBAL = "__SKILL_SCRIPT__";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/skill-scripts/cli-runner.ts
|
|
11
|
+
/** The data `plugin build` embedded in this compiled script. */
|
|
12
|
+
function skillScript() {
|
|
13
|
+
const data = globalThis[SCRIPT_GLOBAL];
|
|
14
|
+
if (data === void 0) throw new Error("this script runs only as compiled by `plugin build`");
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
17
|
+
/** Runs the universal-plugin CLI. A skill may be copied away from its package, so it never reaches
|
|
18
|
+
* for a CLI beside it: the project's installed CLI first, then the one pinned to this script's
|
|
19
|
+
* version, which `plugin version` stamps. */
|
|
20
|
+
function runCli(args, options = {}) {
|
|
21
|
+
const { bin, package: pkg, version } = skillScript();
|
|
22
|
+
const name = bin ?? "universal-plugin";
|
|
23
|
+
if (spawnSync("pnpm", [
|
|
24
|
+
"exec",
|
|
25
|
+
name,
|
|
26
|
+
"--version"
|
|
27
|
+
], { encoding: "utf8" }).status === 0) return spawnSync("pnpm", [
|
|
28
|
+
"exec",
|
|
29
|
+
name,
|
|
30
|
+
...args
|
|
31
|
+
], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
...options
|
|
34
|
+
});
|
|
35
|
+
const pinned = `${pkg ?? "universal-plugin"}@${version ?? "latest"}`;
|
|
36
|
+
return spawnSync("npx", [pinned, ...args], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
...options
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/skill-scripts/run-cli.ts
|
|
43
|
+
const script = skillScript();
|
|
44
|
+
const command = Object.entries(script.commands).find(([, entry]) => entry === script.entry)?.[0];
|
|
45
|
+
if (command === void 0) {
|
|
46
|
+
process.stderr.write(`${script.entry} declares no command in the scripts map\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const result = runCli([...command.split(" "), ...process.argv.slice(2)], { stdio: "inherit" });
|
|
50
|
+
process.exit(result.status ?? 1);
|
|
51
|
+
//#endregion
|
|
52
|
+
export {};
|