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.
Files changed (32) 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/dist/cli.mjs +5197 -4028
  5. package/package.json +2 -1
  6. package/plugin.json +30 -1
  7. package/readme.md +7 -6
  8. package/references/plugin-design.md +21 -9
  9. package/schema/extension.schema.json +31 -0
  10. package/skills/adopt-upx/SKILL.md +4 -2
  11. package/skills/adopt-upx/scripts/rewrite-upx.mjs +92 -129
  12. package/skills/build-plugin/README.md +1 -1
  13. package/skills/build-plugin/SKILL.md +17 -8
  14. package/skills/build-plugin/scripts/build.mjs +51 -10
  15. package/skills/doctor-universal-plugin/SKILL.md +53 -23
  16. package/skills/doctor-universal-plugin/scripts/doctor.mjs +249 -365
  17. package/skills/init-universal-plugin/SKILL.md +13 -2
  18. package/skills/init-universal-plugin/references/create.md +9 -6
  19. package/skills/init-universal-plugin/references/standard.md +1 -1
  20. package/skills/init-universal-plugin/references/vendors/copilot-cli.md +2 -2
  21. package/skills/init-universal-plugin/scripts/init.mjs +51 -10
  22. package/skills/marketplace/references/init.md +1 -1
  23. package/skills/marketplace/scripts/add.mjs +51 -10
  24. package/skills/marketplace/scripts/install-docs.mjs +100 -96
  25. package/skills/marketplace/scripts/marketplace.mjs +51 -10
  26. package/skills/marketplace/scripts/validate.mjs +51 -10
  27. package/skills/migrate-plugin/SKILL.md +1 -1
  28. package/skills/remove-plugin/SKILL.md +1 -1
  29. package/skills/upgrade-plugin/SKILL.md +2 -0
  30. package/skills/version/README.md +24 -18
  31. package/skills/version/SKILL.md +80 -47
  32. 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.12.2",
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.12.2",
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> # move the version across every file carrying one
43
- npx universal-plugin plugin bundle # pin skill npx references to workspace versions
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 sync-version # copy packagePath/package.json version into plugin.json, then rebuild
73
+ npx universal-plugin publish check-version # fail when any place carries a different version (read-only, for CI)
73
74
  ```
74
75
 
75
- This writes the canonical `plugin.json` only. Run `plugin build` afterwards, or the vendor manifests
76
- keep their previous version.
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
- `plugin bundle --runner upx` emits references in this form.
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 `build` stamps the version on:
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.** `build` 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 build directive, not part of any vendor's schema, so `build` strips it from every derived manifest and derived `mcp.json`. The authored files are left as written.
258
- - An already-pinned specifier is **overwritten** with the manifest version, and the build warns naming the version it replaced. Do not hand-pin a marked entry.
259
- - A marked entry the build cannot pin — the `command` is not `npx`/`upx`, the manifest declares no `version`, or `args` carry no package specifier — is warned about and left alone; the build stays green.
260
- - `copilot-cli` reads the canonical manifest directly, so it has no derived manifest to receive the pin; the build warns rather than pretending otherwise.
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
- **Decision tree for always-on guidance:**
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
- - Always-on, cross-agent → merge into **AGENTS.md**
284
- - Always-on, Cursor-only → `rules/` + `commands/setup.md`
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`](../../readme.md#upx--the-fast-package-runner) for the `upx` contract).
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 bundle` treats
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
- // Rewrites `npx <pkg>@<version>` references inside SKILL.md files to the fast local-first
3
- // `upx <pkg>@^<major>` form. See ../SKILL.md for the full contract; this is the mechanism.
4
- //
5
- // Usage:
6
- // node rewrite-upx.mjs --all # every SKILL.md under cwd
7
- // node rewrite-upx.mjs skills/my-skill # one skill (dir or SKILL.md path)
8
- // node rewrite-upx.mjs skills/a skills/b/SKILL.md skills/c-* # a named set (paths + globs)
9
- // node rewrite-upx.mjs --all --dry-run # report only, write nothing
10
- //
11
- // Rewrite rule: `npx <pkg>@<concrete-semver>` -> `upx <pkg>@^<major>` (caret on the major, so
12
- // one global `upx` install serves many pinned callers). A `0.x` pin instead becomes
13
- // `upx <pkg>@^0.<minor>` — under semver a `0.x` minor bump is breaking, so `^0` would be far too
14
- // loose. Left alone:
15
- // - `@<placeholder>` (non-semver, e.g. `@<version>`) — not a real pin
16
- // - dist-tags (e.g. `@next`, `@latest`) — upx can't range-match these, they go to npx anyway
17
- // - any occurrence already using `upx` — nothing to rewrite
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 === '0' ? `^0.${minor}` : `^${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
- const next = content.replace(NPX_REF_PATTERN, (whole, ws, pkg, version) => {
56
- if (!SEMVER_PATTERN.test(version)) return whole // placeholder or dist-tag — leave alone
57
- changed++
58
- return `upx${ws}${pkg}@${caretRange(version)}`
59
- })
60
- return { next, changed }
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('SKILL.md')) out.push(root)
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, 'SKILL.md')
47
+ const direct = join(root, "SKILL.md");
73
48
  if (statSync(direct, { throwIfNoEntry: false })?.isFile()) {
74
- out.push(direct)
75
- return out // a skill dir given directly — don't also recurse into it
49
+ out.push(direct);
50
+ return out;
76
51
  }
77
- for (const entry of readdirSync(root, { withFileTypes: true })) {
78
- if (entry.isDirectory()) {
79
- if (SKIP_DIRS.has(entry.name)) continue
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('*')) return null
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('^' + seg.split('*').map(escapeRegExp).join('.*') + '$')
99
- const next = []
68
+ const re = new RegExp("^" + seg.split("*").map(escapeRegExp).join(".*") + "$");
69
+ const next = [];
100
70
  for (const b of bases) {
101
- const st = statSync(b, { throwIfNoEntry: false })
102
- if (!st?.isDirectory()) continue
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('--all')
118
- const dryRun = args.includes('--dry-run')
119
- const positional = args.filter((a) => a !== '--all' && a !== '--dry-run')
120
-
121
- if (all) return { files: findSkillFiles('.'), dryRun }
122
-
123
- const files = []
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
- for (const g of globbed) files.push(...findSkillFiles(g))
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 { files: [...new Set(files)], dryRun }
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('usage: rewrite-upx.mjs (--all | <path-or-glob>...) [--dry-run]')
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('no SKILL.md files matched — nothing to do')
145
- process.exit(0)
108
+ console.error("no SKILL.md files matched — nothing to do");
109
+ process.exit(0);
146
110
  }
147
-
148
- let touched = 0
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, 'utf8')
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 ? '[dry-run] ' : ''}rewrite (${changed}): ${file}`)
162
- if (!dryRun) writeFileSync(abs, next, 'utf8')
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
- main()
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 the launcher shipped beside this skill:
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 that shipped beside
48
- it, so nothing is downloaded and the build cannot resolve a different version from the one installed.
49
- `npx universal-plugin plugin build` is the fallback. `--root` defaults to the working directory and
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. Each warning names something that will not reach a runtime. Tell the user each
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 refreshed catalog. A diff outside those paths did not come from the build.
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.** That is the release-time `plugin bundle`, not
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
- // Runs `universal-plugin plugin build` from the CLI that ships beside this skill, so the manifests a
3
- // plugin ships are derived by the version the author installed, never by one `npx` resolves.
4
- import { dirname, join } from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
-
7
- // <package>/skills/<skill>/scripts/build.mjs: four levels up is the package root.
8
- const packageRoot = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
9
-
10
- process.argv.splice(2, 0, 'plugin', 'build')
11
- await import(join(packageRoot, 'bin', 'universal-plugin.mjs'))
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 {};