universal-plugin 0.12.1 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/agents/agentskills-specialist.md +4 -16
- package/com.github.copilot/agents/agentskills-specialist.agent.md +4 -16
- package/dist/cli.mjs +5140 -4033
- package/package.json +3 -2
- 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/SKILL.md +20 -11
- package/skills/build-plugin/scripts/build.mjs +51 -10
- package/skills/doctor-universal-plugin/SKILL.md +43 -13
- package/skills/doctor-universal-plugin/scripts/doctor.mjs +249 -365
- package/skills/init-universal-plugin/SKILL.md +18 -7
- package/skills/init-universal-plugin/references/create.md +8 -5
- 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/SKILL.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 +79 -46
- package/skills/version/scripts/version.mjs +51 -10
|
@@ -1,425 +1,309 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import * as
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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,
|
|
62
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
63
|
} catch {
|
|
29
|
-
return null
|
|
64
|
+
return null;
|
|
30
65
|
}
|
|
31
|
-
}
|
|
32
|
-
const mtime = (file) =>
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 ===
|
|
139
|
-
if (typeof value ===
|
|
140
|
-
if (Array.isArray(value)) return value.filter((entry) => typeof entry ===
|
|
141
|
-
if (value && Array.isArray(value.paths)) return value.paths.filter((entry) => typeof entry ===
|
|
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
|
-
[
|
|
153
|
-
[
|
|
154
|
-
[
|
|
155
|
-
])
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
180
|
-
const
|
|
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 ??
|
|
187
|
-
if (/required when targeting codex/.test(stderr))
|
|
188
|
-
|
|
189
|
-
|
|
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 [
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
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
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
226
|
+
* directory outside any repository are all the same answer here: no history to read. */
|
|
351
227
|
function git(...args) {
|
|
352
|
-
const result = spawnSync(
|
|
353
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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(
|
|
361
|
-
if (log === null || log ===
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
381
|
-
|
|
257
|
+
* they only ever change because the canonical manifest did, and counting both would report one
|
|
258
|
+
* change twice. */
|
|
382
259
|
function shippedPaths() {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 : {
|
|
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 ?
|
|
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
|
-
|
|
415
|
-
|
|
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(
|
|
418
|
-
if (result.status !== 0) return
|
|
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 ===
|
|
302
|
+
const declared = JSON.parse(result.stdout);
|
|
303
|
+
return typeof declared === "string" || declared === null ? declared : void 0;
|
|
422
304
|
} catch {
|
|
423
|
-
return
|
|
305
|
+
return;
|
|
424
306
|
}
|
|
425
307
|
}
|
|
308
|
+
//#endregion
|
|
309
|
+
export {};
|