universal-plugin 0.12.2 → 0.13.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.
@@ -1,425 +1,309 @@
1
1
  #!/usr/bin/env node
2
- // Read-only diagnosis of a universal plugin. Emits one JSON object on stdout; never writes.
3
- // Derivation itself is not re-implemented here — the shipped CLI's own build resolver is the source
4
- // of truth for what each vendor gets, and this script only adds the filesystem facts it cannot see.
5
- import { spawnSync } from 'node:child_process'
6
- import * as fs from 'node:fs'
7
- import * as path from 'node:path'
8
- import { fileURLToPath } from 'node:url'
9
-
10
- const UP_NAMESPACE = 'org.cyberuni.universal-plugin'
11
- // <package>/skills/<skill>/scripts/doctor.mjs: four levels up is the package root.
12
- const packageRoot = path.dirname(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))))
13
-
14
- const argv = process.argv.slice(2)
15
- const verbose = argv.includes('--verbose')
16
- const rootFlag = argv.indexOf('--root')
17
- const root = path.resolve(rootFlag === -1 ? process.cwd() : (argv[rootFlag + 1] ?? process.cwd()))
18
- // A shared marketplace repository (e.g. cyberuni/marketplace) is cloned separately from any plugin
19
- // repo, so it is named explicitly rather than discovered — repeatable, one clone per flag.
20
- const marketplaceRoots = argv.flatMap((arg, i) => (arg === '--marketplace-root' ? [argv[i + 1]] : [])).filter(Boolean)
21
-
22
- const findings = []
23
- const add = (code, severity, detail, repair) => findings.push({ code, severity, detail, repair })
24
-
2
+ // Compiled by `universal-plugin plugin build` from src/skill-scripts/doctor.ts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.13.0","entry":"skills/doctor-universal-plugin/scripts/doctor.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
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ //#region src/compile/scripts.ts
8
+ /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
9
+ * library it inlines, reads it; universal-plugin depends on neither. */
10
+ const SCRIPT_GLOBAL = "__SKILL_SCRIPT__";
11
+ //#endregion
12
+ //#region src/skill-scripts/cli-runner.ts
13
+ /** The data `plugin build` embedded in this compiled script. */
14
+ function skillScript() {
15
+ const data = globalThis[SCRIPT_GLOBAL];
16
+ if (data === void 0) throw new Error("this script runs only as compiled by `plugin build`");
17
+ return data;
18
+ }
19
+ /** Runs the universal-plugin CLI. A skill may be copied away from its package, so it never reaches
20
+ * for a CLI beside it: the project's installed CLI first, then the one pinned to this script's
21
+ * version, which `plugin version` stamps. */
22
+ function runCli$1(args, options = {}) {
23
+ const { bin, package: pkg, version } = skillScript();
24
+ const name = bin ?? "universal-plugin";
25
+ if (spawnSync("pnpm", [
26
+ "exec",
27
+ name,
28
+ "--version"
29
+ ], { encoding: "utf8" }).status === 0) return spawnSync("pnpm", [
30
+ "exec",
31
+ name,
32
+ ...args
33
+ ], {
34
+ encoding: "utf8",
35
+ ...options
36
+ });
37
+ const pinned = `${pkg ?? "universal-plugin"}@${version ?? "latest"}`;
38
+ return spawnSync("npx", [pinned, ...args], {
39
+ encoding: "utf8",
40
+ ...options
41
+ });
42
+ }
43
+ //#endregion
44
+ //#region src/skill-scripts/doctor.ts
45
+ const UP_NAMESPACE = "org.cyberuni.universal-plugin";
46
+ const argv = process.argv.slice(2);
47
+ const verbose = argv.includes("--verbose");
48
+ const rootFlag = argv.indexOf("--root");
49
+ const root = path.resolve(rootFlag === -1 ? process.cwd() : argv[rootFlag + 1] ?? process.cwd());
50
+ const marketplaceRoots = argv.flatMap((arg, i) => arg === "--marketplace-root" ? [argv[i + 1]] : []).filter(Boolean);
51
+ const findings = [];
52
+ /** semantic-release's version placeholder until its publish run stamps the real number. */
53
+ const UNSTAMPED_VERSION = "0.0.0-development";
54
+ const add = (code, severity, detail, repair) => findings.push({
55
+ code,
56
+ severity,
57
+ detail,
58
+ repair
59
+ });
25
60
  const readJson = (file) => {
26
61
  try {
27
- return JSON.parse(fs.readFileSync(file, 'utf8'))
62
+ return JSON.parse(fs.readFileSync(file, "utf8"));
28
63
  } catch {
29
- return null
64
+ return null;
30
65
  }
31
- }
32
- const mtime = (file) => (fs.existsSync(file) ? fs.statSync(file).mtimeMs : null)
33
- // A derived directory (copilot-cli's com.github.copilot/) is as fresh as the newest file in it. The
34
- // directory's own mtime moves only when an entry is added, removed or renamed, so a rebuild that
35
- // rewrites its files in place leaves it as old as the first build (issue #144).
66
+ };
67
+ const mtime = (file) => fs.existsSync(file) ? fs.statSync(file).mtimeMs : null;
36
68
  const contentMtime = (abs) => {
37
- const stat = fs.statSync(abs)
38
- if (!stat.isDirectory()) return stat.mtimeMs
39
- const files = fs.readdirSync(abs).map((entry) => contentMtime(path.join(abs, entry)))
40
- return files.length === 0 ? stat.mtimeMs : Math.max(...files)
41
- }
42
-
43
- const VENDOR_MANIFESTS = ['.claude-plugin/plugin.json', '.cursor-plugin/plugin.json', '.codex-plugin/plugin.json']
44
-
45
- const manifestPath = path.join(root, 'plugin.json')
46
- const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : null
47
-
69
+ const stat = fs.statSync(abs);
70
+ if (!stat.isDirectory()) return stat.mtimeMs;
71
+ const files = fs.readdirSync(abs).map((entry) => contentMtime(path.join(abs, entry)));
72
+ return files.length === 0 ? stat.mtimeMs : Math.max(...files);
73
+ };
74
+ const VENDOR_MANIFESTS = [
75
+ ".claude-plugin/plugin.json",
76
+ ".cursor-plugin/plugin.json",
77
+ ".codex-plugin/plugin.json"
78
+ ];
79
+ const manifestPath = path.join(root, "plugin.json");
80
+ const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : null;
48
81
  if (manifest === null) {
49
- const orphans = VENDOR_MANIFESTS.filter((rel) => fs.existsSync(path.join(root, rel)))
50
- if (fs.existsSync(manifestPath)) {
51
- add('unparsable-manifest', 'critical', 'plugin.json is not valid JSON', 'fix the syntax error')
52
- } else if (orphans.length > 0) {
53
- add(
54
- 'vendor-only',
55
- 'high',
56
- `vendor manifests with no canonical manifest: ${orphans.join(', ')}`,
57
- '/universal-plugin:init-universal-plugin, adopt route',
58
- )
59
- } else {
60
- add(
61
- 'no-manifest',
62
- 'high',
63
- 'no root plugin.json — this is not a plugin yet',
64
- '/universal-plugin:init-universal-plugin',
65
- )
66
- }
67
- report({ vendors: [] })
68
- }
69
-
70
- const ext = manifest.extensions?.[UP_NAMESPACE] ?? null
71
-
72
- // The shipped CLI, preferring the one this skill ships with. Every question the CLI can answer is asked
73
- // of it rather than re-derived here, so this script cannot drift from the commands it diagnoses.
74
- const bin = path.join(packageRoot, 'bin', 'universal-plugin.mjs')
75
- const runCli = (...args) =>
76
- fs.existsSync(bin)
77
- ? spawnSync(process.execPath, [bin, ...args], { encoding: 'utf8' })
78
- : spawnSync('npx', ['universal-plugin', ...args], { encoding: 'utf8' })
79
-
80
- // `packagePath` is the CLI's own config. It is asked of the CLI (`config get --key packagePath`), the
81
- // same reader `plugin version` and `publish sync-version` use, so a plugin the CLI treats as
82
- // npm-shipping is one this script treats the same way (issue #79). `undefined` means the CLI could not
83
- // answer — too old to read the key — and every check that depends on it is skipped rather than guessed.
84
- const packagePath = readPackagePath()
85
- if (packagePath === undefined) {
86
- add(
87
- 'package-path-unknown',
88
- 'low',
89
- 'the CLI could not report packagePath, so version-drift and unreleased-content were not checked',
90
- 'upgrade universal-plugin',
91
- )
92
- }
93
- if (ext !== null && Object.hasOwn(ext, 'packagePath')) {
94
- add(
95
- 'misplaced-package-path',
96
- 'high',
97
- `plugin.json declares extensions["${UP_NAMESPACE}"].packagePath, which the CLI never reads${packagePath === null ? ' — this plugin is treated as not shipping to npm' : ''}`,
98
- 'move packagePath to .agents/universal-plugin.json, relative to the plugin root',
99
- )
100
- }
101
- if (!manifest.$schema?.includes('agent-plugins.org') || ext === null) {
102
- add(
103
- 'legacy-manifest',
104
- 'high',
105
- 'root plugin.json carries no $schema on agent-plugins.org or no extensions block',
106
- '/universal-plugin:init-universal-plugin, adopt route',
107
- )
82
+ const orphans = VENDOR_MANIFESTS.filter((rel) => fs.existsSync(path.join(root, rel)));
83
+ if (fs.existsSync(manifestPath)) add("unparsable-manifest", "critical", "plugin.json is not valid JSON", "fix the syntax error");
84
+ else if (orphans.length > 0) add("vendor-only", "high", `vendor manifests with no canonical manifest: ${orphans.join(", ")}`, "/universal-plugin:init-universal-plugin, adopt route");
85
+ else add("no-manifest", "high", "no root plugin.json — this is not a plugin yet", "/universal-plugin:init-universal-plugin");
86
+ report({ vendors: [] });
108
87
  }
109
-
110
- // Shadowing and leftovers. `.plugin/plugin.json` outranks root in Copilot CLI's search order.
111
- if (fs.existsSync(path.join(root, '.plugin/plugin.json'))) {
112
- add(
113
- 'shadowing-manifest',
114
- 'high',
115
- '.plugin/plugin.json outranks root and is read instead of the canonical manifest',
116
- '/universal-plugin:remove-plugin',
117
- )
118
- }
119
- if (fs.existsSync(path.join(root, '.github/plugin/plugin.json'))) {
120
- add(
121
- 'stale-github-plugin',
122
- 'low',
123
- '.github/plugin/plugin.json is a leftover from an older build and is no longer generated',
124
- '/universal-plugin:remove-plugin',
125
- )
88
+ /** The canonical $schema is hosted on agent-plugins.org; a URL that merely mentions the name is not. */
89
+ function isAgentPluginsSchema(schema) {
90
+ if (typeof schema !== "string") return false;
91
+ try {
92
+ return new URL(schema).hostname === "agent-plugins.org";
93
+ } catch {
94
+ return false;
95
+ }
126
96
  }
127
-
128
- // Copilot CLI's spec mode reads its native components ONLY under com.github.copilot/ — declaring the
129
- // canonical $schema moves them out of the plugin root (ADR-0015). A plugin that ships them at the
130
- // root and has no namespace copy loads none of them on Copilot CLI, and nothing at runtime says so.
131
- // Checked from the filesystem rather than from the build result, so it still fires on a repository
132
- // whose build cannot run.
133
- const COPILOT_NAMESPACE = 'com.github.copilot'
134
- const declaredTargets = ext?.vendors ?? Object.keys(ext?.harnesses ?? {})
135
- if (declaredTargets.includes('copilot-cli')) {
136
- const nsDir = path.join(root, COPILOT_NAMESPACE)
97
+ const ext = manifest.extensions?.[UP_NAMESPACE] ?? null;
98
+ const runCli = (...args) => runCli$1(args);
99
+ const packagePath = readPackagePath();
100
+ if (packagePath === void 0) add("package-path-unknown", "low", "the CLI could not report packagePath, so version-drift and unreleased-content were not checked", "upgrade universal-plugin");
101
+ if (ext !== null && Object.hasOwn(ext, "packagePath")) add("misplaced-package-path", "high", `plugin.json declares extensions["${UP_NAMESPACE}"].packagePath, which the CLI never reads${packagePath === null ? " — this plugin is treated as not shipping to npm" : ""}`, "move packagePath to .agents/universal-plugin.json, relative to the plugin root");
102
+ if (!isAgentPluginsSchema(manifest.$schema) || ext === null) add("legacy-manifest", "high", "root plugin.json carries no $schema on agent-plugins.org or no extensions block", "/universal-plugin:init-universal-plugin, adopt route");
103
+ if (fs.existsSync(path.join(root, ".plugin/plugin.json"))) add("shadowing-manifest", "high", ".plugin/plugin.json outranks root and is read instead of the canonical manifest", "/universal-plugin:remove-plugin");
104
+ if (fs.existsSync(path.join(root, ".github/plugin/plugin.json"))) add("stale-github-plugin", "low", ".github/plugin/plugin.json is a leftover from an older build and is no longer generated", "/universal-plugin:remove-plugin");
105
+ const COPILOT_NAMESPACE = "com.github.copilot";
106
+ if ((ext?.vendors ?? Object.keys(ext?.harnesses ?? {})).includes("copilot-cli")) {
107
+ const nsDir = path.join(root, COPILOT_NAMESPACE);
137
108
  const declaredPaths = (value, fallback) => {
138
- if (value === undefined) return fallback === null ? [] : [fallback]
139
- if (typeof value === 'string') return [value]
140
- if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'string')
141
- if (value && Array.isArray(value.paths)) return value.paths.filter((entry) => typeof entry === 'string')
142
- return []
143
- }
109
+ if (value === void 0) return fallback === null ? [] : [fallback];
110
+ if (typeof value === "string") return [value];
111
+ if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string");
112
+ if (value && Array.isArray(value.paths)) return value.paths.filter((entry) => typeof entry === "string");
113
+ return [];
114
+ };
144
115
  const hasContent = (rel) => {
145
- const abs = path.join(root, rel)
146
- return fs.existsSync(abs) && fs.statSync(abs).isDirectory() && fs.readdirSync(abs).length > 0
147
- }
148
- const missing = []
149
- // The same defaults the build derives from. Passing null here would make the check fire only for a
150
- // manifest that names the path — and the manifest that names none is the common one.
116
+ const abs = path.join(root, rel);
117
+ return fs.existsSync(abs) && fs.statSync(abs).isDirectory() && fs.readdirSync(abs).length > 0;
118
+ };
119
+ const missing = [];
151
120
  for (const [kind, defaultPath] of [
152
- ['agents', './agents/'],
153
- ['commands', './commands/'],
154
- ['rules', './rules/'],
155
- ]) {
156
- const roots = declaredPaths(ext?.[kind], defaultPath)
157
- if (roots.some(hasContent) && !hasContent(path.join(COPILOT_NAMESPACE, kind))) missing.push(kind)
158
- }
159
- // hooks is a single file, declared as a path or inline in the manifest, and carries a default the
160
- // manifest need not declare at all.
161
- const inlineHooks = ext?.hooks !== null && typeof ext?.hooks === 'object' && 'hooks' in ext.hooks
162
- const hookSources = inlineHooks ? [] : declaredPaths(ext?.hooks, './hooks/hooks.json')
163
- const hasHooks = inlineHooks || hookSources.some((rel) => fs.existsSync(path.join(root, rel)))
164
- if (hasHooks && !fs.existsSync(path.join(nsDir, 'hooks', 'hooks.json'))) missing.push('hooks')
165
- const lspSources = declaredPaths(ext?.lspServers, null)
166
- if (lspSources.some((rel) => fs.existsSync(path.join(root, rel))) && !fs.existsSync(path.join(nsDir, 'lsp.json'))) {
167
- missing.push('lspServers')
168
- }
169
- if (missing.length > 0) {
170
- add(
171
- 'copilot-root-components',
172
- 'high',
173
- `${missing.join(', ')} sit at the plugin root with no copy under ${COPILOT_NAMESPACE}/ — Copilot CLI reads them only from there once the canonical $schema is declared, so it loads none of them`,
174
- 'universal-plugin plugin build',
175
- )
176
- }
121
+ ["agents", "./agents/"],
122
+ ["commands", "./commands/"],
123
+ ["rules", "./rules/"]
124
+ ]) if (declaredPaths(ext?.[kind], defaultPath).some(hasContent) && !hasContent(path.join(COPILOT_NAMESPACE, kind))) missing.push(kind);
125
+ const inlineHooks = ext?.hooks !== null && typeof ext?.hooks === "object" && "hooks" in ext.hooks;
126
+ const hookSources = inlineHooks ? [] : declaredPaths(ext?.hooks, "./hooks/hooks.json");
127
+ if ((inlineHooks || hookSources.some((rel) => fs.existsSync(path.join(root, rel)))) && !fs.existsSync(path.join(nsDir, "hooks", "hooks.json"))) missing.push("hooks");
128
+ if (declaredPaths(ext?.lspServers, null).some((rel) => fs.existsSync(path.join(root, rel))) && !fs.existsSync(path.join(nsDir, "lsp.json"))) missing.push("lspServers");
129
+ if (missing.length > 0) add("copilot-root-components", "high", `${missing.join(", ")} sit at the plugin root with no copy under ${COPILOT_NAMESPACE}/ — Copilot CLI reads them only from there once the canonical $schema is declared, so it loads none of them`, "universal-plugin plugin build");
177
130
  }
178
-
179
- // Ask the shipped CLI what it would write, without writing it.
180
- const cli = runCli('plugin', 'build', '--dry-run', '--format', 'json', '--root', root)
181
-
182
- const vendors = []
183
- const build = readJson_stdout(cli.stdout)
184
-
131
+ const cli = runCli("plugin", "build", "--dry-run", "--format", "json", "--root", root);
132
+ const vendors = [];
133
+ const build = readJson_stdout(cli.stdout);
185
134
  if (build === null) {
186
- const stderr = (cli.stderr ?? '').trim()
187
- if (/required when targeting codex/.test(stderr)) {
188
- add(
189
- 'codex-fields-missing',
190
- 'high',
191
- 'codex is targeted without version or description — the build writes nothing at all, for any vendor',
192
- 'add both to the canonical top level',
193
- )
194
- } else if (findings.some((f) => f.code === 'legacy-manifest' || f.code === 'shadowing-manifest')) {
195
- // The build stops rather than reporting a definitive empty state when the layout explains the
196
- // empty result (issue #61). That is still the no-vendors diagnosis — the layout findings above
197
- // name the signals, and this names the consequence. Keyed on those findings rather than on the
198
- // build's message, so the two never have to agree on wording.
199
- add(
200
- 'no-vendors',
201
- 'medium',
202
- 'no vendor is declared — the project is on the pre-0.6 layout, so the build derives nothing and no runtime reads this plugin',
203
- '/universal-plugin:init-universal-plugin, adopt route',
204
- )
205
- } else {
206
- add(
207
- 'build-failed',
208
- 'high',
209
- stderr.split('\n')[0] || 'plugin build could not resolve the manifest',
210
- 'run plugin build to see the full error',
211
- )
212
- }
135
+ const stderr = (cli.stderr ?? "").trim();
136
+ if (/required when targeting codex/.test(stderr)) add("codex-fields-missing", "high", "codex is targeted without version or description — the build writes nothing at all, for any vendor", "add both to the canonical top level");
137
+ else if (findings.some((f) => f.code === "legacy-manifest" || f.code === "shadowing-manifest")) add("no-vendors", "medium", "no vendor is declared — the project is on the pre-0.6 layout, so the build derives nothing and no runtime reads this plugin", "/universal-plugin:init-universal-plugin, adopt route");
138
+ else add("build-failed", "high", stderr.split("\n")[0] || "plugin build could not resolve the manifest", "run plugin build to see the full error");
213
139
  } else {
214
- const manifestMtime = mtime(manifestPath)
215
- for (const row of [...build.built, ...build.canonical, ...build.skipped, ...build.failed]) {
216
- const abs = path.join(root, row.path)
217
- const exists = fs.existsSync(abs)
218
- // `canonical` means the vendor reads root plugin.json; no derived file is expected.
219
- const stale = row.status === 'built' && exists && manifestMtime !== null && contentMtime(abs) < manifestMtime
220
- vendors.push({ vendor: row.vendor, path: row.path, status: row.status, exists, stale })
221
-
222
- if (row.status === 'built' && !exists) {
223
- add(
224
- 'unbuilt',
225
- 'high',
226
- `${row.vendor} is declared but ${row.path} does not exist — that runtime sees no plugin`,
227
- 'universal-plugin plugin build',
228
- )
229
- }
230
- if (stale) {
231
- add('stale', 'medium', `${row.path} is older than plugin.json`, 'universal-plugin plugin build')
232
- }
140
+ const manifestMtime = mtime(manifestPath);
141
+ for (const row of [
142
+ ...build.built,
143
+ ...build.canonical,
144
+ ...build.skipped,
145
+ ...build.failed
146
+ ]) {
147
+ const abs = path.join(root, row.path);
148
+ const exists = fs.existsSync(abs);
149
+ const stale = row.status === "built" && exists && manifestMtime !== null && contentMtime(abs) < manifestMtime;
150
+ vendors.push({
151
+ vendor: row.vendor,
152
+ path: row.path,
153
+ status: row.status,
154
+ exists,
155
+ stale
156
+ });
157
+ if (row.status === "built" && !exists) add("unbuilt", "high", `${row.vendor} is declared but ${row.path} does not exist — that runtime sees no plugin`, "universal-plugin plugin build");
158
+ if (stale) add("stale", "medium", `${row.path} is older than plugin.json`, "universal-plugin plugin build");
233
159
  }
234
- for (const warning of build.warnings ?? []) {
235
- if (/not delivered/.test(warning)) {
236
- add('undeliverable-override', 'medium', warning, '/universal-plugin:init-universal-plugin, update route')
237
- } else if (/No vendors declared/.test(warning)) {
238
- add(
239
- 'no-vendors',
240
- 'medium',
241
- 'no vendor is declared — the build writes nothing, so no runtime reads this plugin',
242
- '/universal-plugin:init-universal-plugin, update route',
243
- )
244
- } else if (/has no "[^"]+" component/.test(warning)) {
245
- add(
246
- 'unsupported-component',
247
- 'low',
248
- warning,
249
- 'declare the path under harnesses.<vendor> for the vendors that read it, or leave it — the build already drops it',
250
- )
251
- } else if (/^Unknown vendor/.test(warning)) {
252
- add('unknown-vendor', 'medium', warning, 'fix the vendor id in plugin.json')
253
- } else {
254
- add('build-warning', 'low', warning, 'read the warning and decide')
255
- }
160
+ for (const row of build.scripts ?? []) {
161
+ if (row.status !== "stale" && row.status !== "missing") continue;
162
+ add("stale-script", "medium", row.status === "missing" ? `${row.path} is declared but not compiled from ${row.source}` : `${row.path} is behind its source ${row.source}`, "universal-plugin plugin build, then commit the compiled script");
163
+ }
164
+ for (const warning of build.warnings ?? []) if (/^Failed to compile /.test(warning)) add("script-compile-failed", "high", warning, "fix the entry so every import resolves, then plugin build");
165
+ else if (/budget for a compiled skill script/.test(warning)) add("script-over-budget", "low", warning, "have the skill call the installed CLI instead of compiling it in");
166
+ else if (/not delivered/.test(warning)) add("undeliverable-override", "medium", warning, "/universal-plugin:init-universal-plugin, update route");
167
+ else if (/No vendors declared/.test(warning)) add("no-vendors", "medium", "no vendor is declared — the build writes nothing, so no runtime reads this plugin", "/universal-plugin:init-universal-plugin, update route");
168
+ else if (/has no "[^"]+" component/.test(warning)) add("unsupported-component", "low", warning, "declare the path under harnesses.<vendor> for the vendors that read it, or leave it — the build already drops it");
169
+ else if (/outside the skill folder/.test(warning)) add("skill-link-escapes", "medium", warning, "copy the linked file into the skill and link it relatively, or link it by URL");
170
+ else if (/^Unknown vendor/.test(warning)) add("unknown-vendor", "medium", warning, "fix the vendor id in plugin.json");
171
+ else add("build-warning", "low", warning, "read the warning and decide");
172
+ }
173
+ let packageMissing = false;
174
+ if (typeof packagePath === "string") {
175
+ if (readJson(path.join(root, packagePath, "package.json")) === null) {
176
+ packageMissing = true;
177
+ add("package-path-missing", "medium", `packagePath names ${packagePath}, which holds no readable package.json`, "fix packagePath, or create the package");
256
178
  }
257
179
  }
258
-
259
- // Version drift between the two authored numbers.
260
- if (typeof packagePath === 'string') {
261
- const pkgPath = path.join(root, packagePath, 'package.json')
262
- const pkg = readJson(pkgPath)
263
- if (pkg === null) {
264
- add(
265
- 'package-path-missing',
266
- 'medium',
267
- `packagePath names ${packagePath}, which holds no readable package.json`,
268
- 'fix packagePath, or create the package',
269
- )
270
- } else if (manifest.version !== undefined && pkg.version !== manifest.version) {
271
- add(
272
- 'version-drift',
273
- 'high',
274
- `plugin.json is ${manifest.version}, ${packagePath}/package.json is ${pkg.version}`,
275
- '/universal-plugin:version',
276
- )
180
+ if (packagePath !== void 0 && !packageMissing) {
181
+ const check = readJson_stdout(runCli("publish", "check-version", "--format", "json", "--root", root).stdout);
182
+ if (check !== null && Array.isArray(check.drifted)) {
183
+ for (const file of check.drifted) add("version-drift", "high", `${file.path} is ${file.version ?? "(no version)"}, ${check.source} is ${check.expected}`, "/universal-plugin:version");
184
+ for (const file of check.unstamped ?? []) add("unstamped", "low", `${file.path} holds ${UNSTAMPED_VERSION}, which the release replaces`, "semantic-release: run `plugin version ${nextRelease.version}` in prepareCmd and commit the stamped files back");
185
+ } else if (typeof packagePath === "string") {
186
+ const pkg = readJson(path.join(root, packagePath, "package.json"));
187
+ if (manifest.version !== void 0 && pkg.version !== UNSTAMPED_VERSION && pkg.version !== manifest.version) add("version-drift", "high", `plugin.json is ${manifest.version}, ${packagePath}/package.json is ${pkg.version}`, "/universal-plugin:version");
277
188
  }
278
189
  }
279
-
280
- // Content shipped since the version last moved (ADR-0010 §6). A runtime keys its plugin cache on the
281
- // version, so anything committed after the commit that set the current one is invisible to a consumer
282
- // who already installed the plugin. Read-only: the comparison is git's, and it is skipped wherever
283
- // git cannot answer.
284
- //
285
- // Not run where the release picks the number. ADR-0010 §2 makes `packagePath` the switch: a plugin
286
- // that ships to npm gets its version from the release, so content sitting ahead of the last released
287
- // one is the normal state there, not a defect. Only the author-picks model can forget the bump.
288
- if (manifest.version !== undefined && packagePath === null) {
289
- const introduced = commitThatSetVersion(manifest.version)
190
+ if (manifest.version !== void 0 && packagePath === null) {
191
+ const introduced = commitThatSetVersion(manifest.version);
290
192
  if (introduced !== null) {
291
- const changed = git('diff', '--name-only', `${introduced}..HEAD`, '--', ...shippedPaths())
292
- const files = (changed ?? '').split('\n').filter(Boolean)
193
+ const files = (git("diff", "--name-only", `${introduced}..HEAD`, "--", ...shippedPaths()) ?? "").split("\n").filter(Boolean);
293
194
  if (files.length > 0) {
294
- const sample = files.slice(0, 3).join(', ')
295
- add(
296
- 'unreleased-content',
297
- 'medium',
298
- `${files.length} shipped file(s) changed since ${manifest.version} was set (${sample}${files.length > 3 ? ', …' : ''}) — a consumer keyed on that version never re-extracts them`,
299
- '/universal-plugin:version',
300
- )
195
+ const sample = files.slice(0, 3).join(", ");
196
+ add("unreleased-content", "medium", `${files.length} shipped file(s) changed since ${manifest.version} was set (${sample}${files.length > 3 ? ", …" : ""}) — a consumer keyed on that version never re-extracts them`, "/universal-plugin:version");
301
197
  }
302
198
  }
303
199
  }
304
-
305
- // The marketplace catalogs a user installs from. They sit at the *repository* root, above a plugin in
306
- // a monorepo, and each is read by its runtime at install time — a catalog whose shape that runtime
307
- // refuses fails in the user's terminal, not here. The shipped CLI owns the rules; this only asks.
308
- // A shared marketplace repository (--marketplace-root) is a separate clone this plugin's repo root
309
- // cannot see, so it is checked the same way but reported with the clone's path attached.
310
- const missingMarketplaceRoots = marketplaceRoots.filter((r) => !fs.existsSync(path.resolve(r)))
311
- for (const r of missingMarketplaceRoots) {
312
- add(
313
- 'marketplace-root-missing',
314
- 'high',
315
- `--marketplace-root ${r} does not exist`,
316
- 'clone the marketplace repository, or fix the path',
317
- )
318
- }
319
- const existingMarketplaceRoots = marketplaceRoots.filter((r) => fs.existsSync(path.resolve(r)))
320
-
321
- for (const row of invalidCatalogs(existingMarketplaceRoots)) {
322
- const label = row.own ? row.path : `${row.catalogRoot}/${row.path}`
323
- add(
324
- 'invalid-catalog',
325
- 'high',
326
- `${label} is not a shape its runtime loads: ${row.issues.map((issue) => `${issue.path} ${issue.message}`).join('; ')}`,
327
- '/universal-plugin:marketplace',
328
- )
329
- }
330
-
331
- report({ vendors })
332
-
200
+ const missingMarketplaceRoots = marketplaceRoots.filter((r) => !fs.existsSync(path.resolve(r)));
201
+ for (const r of missingMarketplaceRoots) add("marketplace-root-missing", "high", `--marketplace-root ${r} does not exist`, "clone the marketplace repository, or fix the path");
202
+ const existingMarketplaceRoots = marketplaceRoots.filter((r) => fs.existsSync(path.resolve(r)));
203
+ for (const row of invalidCatalogs(existingMarketplaceRoots)) add("invalid-catalog", "high", `${row.own ? row.path : `${row.catalogRoot}/${row.path}`} is not a shape its runtime loads: ${row.issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`, "/universal-plugin:marketplace");
204
+ report({ vendors });
333
205
  /** Every catalog the repository — and any explicitly named marketplace clone — carries that its
334
- * runtime would refuse. Empty when there is nothing to read, when the CLI is too old to answer, or
335
- * when every catalog is fine — a missing catalog is not a fault, and this reports no opinion on
336
- * which ones a repository ought to carry. */
206
+ * runtime would refuse. Empty when there is nothing to read, when the CLI is too old to answer, or
207
+ * when every catalog is fine — a missing catalog is not a fault, and this reports no opinion on
208
+ * which ones a repository ought to carry. */
337
209
  function invalidCatalogs(extraRoots) {
338
- const catalogRoot = git('rev-parse', '--show-toplevel') ?? root
339
- const targets = [{ catalogRoot, own: true }, ...extraRoots.map((r) => ({ catalogRoot: path.resolve(r), own: false }))]
340
- return targets.flatMap(({ catalogRoot, own }) => {
341
- const result = runCli('marketplace', 'validate', '--format', 'json', '--root', catalogRoot)
342
- const rows = readJson_stdout(result.stdout)
343
- return Array.isArray(rows)
344
- ? rows.filter((row) => row.status === 'invalid').map((row) => ({ ...row, catalogRoot, own }))
345
- : []
346
- })
210
+ return [{
211
+ catalogRoot: git("rev-parse", "--show-toplevel") ?? root,
212
+ own: true
213
+ }, ...extraRoots.map((r) => ({
214
+ catalogRoot: path.resolve(r),
215
+ own: false
216
+ }))].flatMap(({ catalogRoot, own }) => {
217
+ const rows = readJson_stdout(runCli("marketplace", "validate", "--format", "json", "--root", catalogRoot).stdout);
218
+ return Array.isArray(rows) ? rows.filter((row) => row.status === "invalid").map((row) => ({
219
+ ...row,
220
+ catalogRoot,
221
+ own
222
+ })) : [];
223
+ });
347
224
  }
348
-
349
225
  /** Runs git inside `root`, returning its stdout or `null` — a non-zero status, a missing git, and a
350
- * directory outside any repository are all the same answer here: no history to read. */
226
+ * directory outside any repository are all the same answer here: no history to read. */
351
227
  function git(...args) {
352
- const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
353
- return result.status === 0 ? result.stdout.trim() : null
228
+ const result = spawnSync("git", [
229
+ "-C",
230
+ root,
231
+ ...args
232
+ ], { encoding: "utf8" });
233
+ return result.status === 0 ? result.stdout.trim() : null;
354
234
  }
355
-
356
235
  /** The commit that introduced the version the manifest carries now, walking `plugin.json`'s history
357
- * newest-first until the version changes. `null` when there is no history, or when the newest
358
- * committed manifest already disagrees — that version is uncommitted, so nothing shipped under it. */
236
+ * newest-first until the version changes. `null` when there is no history, or when the newest
237
+ * committed manifest already disagrees — that version is uncommitted, so nothing shipped under it. */
359
238
  function commitThatSetVersion(current) {
360
- const log = git('log', '--format=%H', '-100', '--', 'plugin.json')
361
- if (log === null || log === '') return null
362
-
363
- let introduced = null
364
- for (const sha of log.split('\n').filter(Boolean)) {
365
- const blob = git('show', `${sha}:./plugin.json`)
366
- if (blob === null) break
367
- let version
239
+ const log = git("log", "--format=%H", "-100", "--", "plugin.json");
240
+ if (log === null || log === "") return null;
241
+ let introduced = null;
242
+ for (const sha of log.split("\n").filter(Boolean)) {
243
+ const blob = git("show", `${sha}:./plugin.json`);
244
+ if (blob === null) break;
245
+ let version;
368
246
  try {
369
- version = JSON.parse(blob).version
247
+ version = JSON.parse(blob).version;
370
248
  } catch {
371
- break
249
+ break;
372
250
  }
373
- if (version !== current) break
374
- introduced = sha
251
+ if (version !== current) break;
252
+ introduced = sha;
375
253
  }
376
- return introduced
254
+ return introduced;
377
255
  }
378
-
379
256
  /** What a consumer installs, as pathspecs. The derived vendor manifests are deliberately absent —
380
- * they only ever change because the canonical manifest did, and counting both would report one
381
- * change twice. */
257
+ * they only ever change because the canonical manifest did, and counting both would report one
258
+ * change twice. */
382
259
  function shippedPaths() {
383
- const skills = typeof ext?.skills === 'string' ? ext.skills : './skills/'
384
- const paths = ['plugin.json', skills, 'agents', 'governances', 'references', 'mcp.json']
385
- return paths.filter((rel) => fs.existsSync(path.join(root, rel)))
260
+ return [
261
+ "plugin.json",
262
+ typeof ext?.skills === "string" ? ext.skills : "./skills/",
263
+ "agents",
264
+ "governances",
265
+ "references",
266
+ "mcp.json"
267
+ ].filter((rel) => fs.existsSync(path.join(root, rel)));
386
268
  }
387
-
388
269
  function readJson_stdout(stdout) {
389
- if (!stdout) return null
270
+ if (!stdout) return null;
390
271
  try {
391
- return JSON.parse(stdout)
272
+ return JSON.parse(stdout);
392
273
  } catch {
393
- return null
274
+ return null;
394
275
  }
395
276
  }
396
-
397
277
  function report({ vendors }) {
398
278
  const result = {
399
279
  root,
400
- manifest: manifest === null ? null : { name: manifest.name ?? null, version: manifest.version ?? null },
280
+ manifest: manifest === null ? null : {
281
+ name: manifest.name ?? null,
282
+ version: manifest.version ?? null
283
+ },
401
284
  vendors,
402
285
  findings,
403
- ok: findings.length === 0,
404
- }
405
- process.stdout.write(`${JSON.stringify(result)}\n`)
286
+ ok: findings.length === 0
287
+ };
288
+ process.stdout.write(`${JSON.stringify(result)}\n`);
406
289
  if (verbose) {
407
- process.stderr.write(result.ok ? 'no findings\n' : `${findings.length} finding(s)\n`)
408
- for (const f of findings) process.stderr.write(` [${f.severity}] ${f.code}: ${f.detail}\n`)
290
+ process.stderr.write(result.ok ? "no findings\n" : `${findings.length} finding(s)\n`);
291
+ for (const f of findings) process.stderr.write(` [${f.severity}] ${f.code}: ${f.detail}\n`);
409
292
  }
410
- process.exit(0)
293
+ process.exit(0);
411
294
  }
412
-
413
295
  /** Where the npm package that ships this plugin lives, relative to the plugin root, as the CLI reads
414
- * it; `null` when the plugin ships to no package (the author-picks release model of ADR-0010 §2);
415
- * `undefined` when the CLI could not answer. */
296
+ * it; `null` when the plugin ships to no package (the author-picks release model of ADR-0010 §2);
297
+ * `undefined` when the CLI could not answer. */
416
298
  function readPackagePath() {
417
- const result = runCli('config', 'get', '--key', 'packagePath', '--format', 'json', '--root', root)
418
- if (result.status !== 0) return undefined
299
+ const result = runCli("config", "get", "--key", "packagePath", "--format", "json", "--root", root);
300
+ if (result.status !== 0) return void 0;
419
301
  try {
420
- const declared = JSON.parse(result.stdout)
421
- return typeof declared === 'string' || declared === null ? declared : undefined
302
+ const declared = JSON.parse(result.stdout);
303
+ return typeof declared === "string" || declared === null ? declared : void 0;
422
304
  } catch {
423
- return undefined
305
+ return;
424
306
  }
425
307
  }
308
+ //#endregion
309
+ export {};