@capybara-hq/plugin-tools 0.1.0-rc.7 → 0.1.0-rc.8
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.
|
@@ -16,7 +16,8 @@ export declare const MARKETPLACE_USAGE: string;
|
|
|
16
16
|
* `--check` exits 1 on any finding that is not a warning (`marketplace-missing`,
|
|
17
17
|
* `marketplace-display-stale`, `marketplace-integrity`, `marketplace-version-decrease`,
|
|
18
18
|
* `marketplace-invalid`); `--write` writes each plugin's entry (replacing its other versions,
|
|
19
|
-
* refreshing a listed entry's summary and
|
|
19
|
+
* refreshing a listed entry's summary, displayName and icon) and exits 1 only on
|
|
20
|
+
* `marketplace-invalid`.
|
|
20
21
|
* Relative paths resolve against `cwd`. Returns the exit code.
|
|
21
22
|
*/
|
|
22
23
|
export declare function marketplaceCommand(args: string[], cwd: string): Promise<number>;
|
package/dist/marketplace/cli.js
CHANGED
|
@@ -22,7 +22,8 @@ export const MARKETPLACE_USAGE = "capybara-plugin marketplace --write|--check [-
|
|
|
22
22
|
* `--check` exits 1 on any finding that is not a warning (`marketplace-missing`,
|
|
23
23
|
* `marketplace-display-stale`, `marketplace-integrity`, `marketplace-version-decrease`,
|
|
24
24
|
* `marketplace-invalid`); `--write` writes each plugin's entry (replacing its other versions,
|
|
25
|
-
* refreshing a listed entry's summary and
|
|
25
|
+
* refreshing a listed entry's summary, displayName and icon) and exits 1 only on
|
|
26
|
+
* `marketplace-invalid`.
|
|
26
27
|
* Relative paths resolve against `cwd`. Returns the exit code.
|
|
27
28
|
*/
|
|
28
29
|
export async function marketplaceCommand(args, cwd) {
|
|
@@ -28,7 +28,7 @@ export interface SyncMarketplaceResult {
|
|
|
28
28
|
/** `name@version` of the other entries of the tree's plugins (dropped with `write`). */
|
|
29
29
|
removed: string[];
|
|
30
30
|
/**
|
|
31
|
-
* `name@version` of listed entries whose `summary` / `displayName` differ from plugin.json
|
|
31
|
+
* `name@version` of listed entries whose `summary` / `displayName` / `icon` differ from plugin.json
|
|
32
32
|
* (rewritten with `write`; source and integrity never change).
|
|
33
33
|
*/
|
|
34
34
|
refreshed: string[];
|
|
@@ -37,25 +37,33 @@ export interface SyncMarketplaceResult {
|
|
|
37
37
|
* `marketplace-integrity` and `marketplace-version-decrease` (with a base, see
|
|
38
38
|
* {@link SyncMarketplaceOptions.base}) and, without `write`, `marketplace-missing` (one per
|
|
39
39
|
* plugin whose current version is not listed, or not as its only entry) and
|
|
40
|
-
* `marketplace-display-stale` (a listed entry's summary / displayName differ from plugin.json).
|
|
40
|
+
* `marketplace-display-stale` (a listed entry's summary / displayName / icon differ from plugin.json).
|
|
41
41
|
*/
|
|
42
42
|
findings: Finding[];
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The repository path of a plugin's icon: the plugin.json `icon` (relative to the plugin's
|
|
46
|
+
* directory `dir`) as `./<path from the root>` (an entry's `icon`, read at the marketplace's
|
|
47
|
+
* commit, so it follows every release without naming a tag).
|
|
48
|
+
*/
|
|
49
|
+
export declare function repoIconPath(root: string, dir: string, icon: string): string;
|
|
44
50
|
/**
|
|
45
51
|
* `capybara-plugin marketplace`: keeps a plugin repository's `marketplace.json` listing one entry
|
|
46
52
|
* per public plugin, its current version (plugin component management spec §13.2, replacing the
|
|
47
53
|
* one-entry-per-version rule of the package ecosystem spec §5.2). Each public plugin (not
|
|
48
54
|
* `"private": true`) not listed at its version is packed reproducibly with {@link packPlugin} and
|
|
49
|
-
* gets an entry `{ name, version, summary, displayName?, homepage?, permissions, source: {
|
|
50
|
-
* package, version, integrity } } }` (`summary` from the plugin.json `description`, a string
|
|
51
|
-
* `en`; `displayName` copied from plugin.json; `
|
|
55
|
+
* gets an entry `{ name, version, summary, displayName?, icon?, homepage?, permissions, source: {
|
|
56
|
+
* npm: { package, version, integrity } } }` (`summary` from the plugin.json `description`, a string
|
|
57
|
+
* being `en`; `displayName` copied from plugin.json; `icon` the plugin.json `icon` as a path in the
|
|
58
|
+
* repository, `./<plugin dir>/<icon>`, which a git marketplace reads at its refreshed commit;
|
|
59
|
+
* `integrity` the pack's sha512, which the release
|
|
52
60
|
* then publishes byte for byte; `permissions` display only). The new entry takes the place of the
|
|
53
61
|
* plugin's first entry (a version that moved), the plugin's other entries are dropped (also those
|
|
54
62
|
* of a file written in the older one-entry-per-version form), and a plugin not listed yet is
|
|
55
63
|
* appended. Entries of plugins not in the tree are kept. A version already listed is not packed or
|
|
56
64
|
* compared: once published, its bytes are the release gate's business (`release --marketplace`),
|
|
57
65
|
* so a later source or toolchain change at the same version is not a finding; only its display
|
|
58
|
-
* fields (`summary`, `displayName`) are refreshed from the source plugin.json. Every other field
|
|
66
|
+
* fields (`summary`, `displayName`, `icon`) are refreshed from the source plugin.json. Every other field
|
|
59
67
|
* of a kept entry stays as written, among them `deprecated`, which the maintainer sets by hand on
|
|
60
68
|
* a published version's entry (not in plugin.json: that would change the packed bytes); a new
|
|
61
69
|
* version's entry has none, and no rule compares it with the base. With `base`, see
|
|
@@ -36,39 +36,60 @@ async function permissionsOf(bytes, hasExtension) {
|
|
|
36
36
|
}
|
|
37
37
|
/** An entry's version: `version`, else its npm source's. */
|
|
38
38
|
const versionOf = (e) => e.version ?? ("npm" in e.source ? e.source.npm.version : undefined);
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The repository path of a plugin's icon: the plugin.json `icon` (relative to the plugin's
|
|
41
|
+
* directory `dir`) as `./<path from the root>` (an entry's `icon`, read at the marketplace's
|
|
42
|
+
* commit, so it follows every release without naming a tag).
|
|
43
|
+
*/
|
|
44
|
+
export function repoIconPath(root, dir, icon) {
|
|
45
|
+
const rel = relative(root, dir).split("\\").join("/");
|
|
46
|
+
const file = icon.replace(/^\.\//, "");
|
|
47
|
+
return `./${rel === "" ? file : `${rel}/${file}`}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The display fields of a plugin's source plugin.json (`dir` its directory under `root`); undefined
|
|
51
|
+
* when it does not parse.
|
|
52
|
+
*/
|
|
53
|
+
function displayOf(json, root, dir) {
|
|
41
54
|
const m = parsePluginManifest(json);
|
|
42
55
|
if ("errors" in m)
|
|
43
56
|
return undefined;
|
|
44
57
|
return {
|
|
45
58
|
summary: summaryOf(m.description),
|
|
46
59
|
...(m.displayName !== undefined && { displayName: m.displayName }),
|
|
60
|
+
...(m.icon !== undefined && { icon: repoIconPath(root, dir, m.icon) }),
|
|
47
61
|
};
|
|
48
62
|
}
|
|
63
|
+
/** The display fields after `summary`, in this order. */
|
|
64
|
+
const AFTER_SUMMARY = ["displayName", "icon"];
|
|
49
65
|
/**
|
|
50
|
-
* `raw` with its `summary` and `
|
|
51
|
-
* `
|
|
66
|
+
* `raw` with its `summary`, `displayName` and `icon` set to `d` (in place; a new `displayName` /
|
|
67
|
+
* `icon` right after `summary`, removed when plugin.json has none); undefined when they already
|
|
68
|
+
* equal.
|
|
52
69
|
*/
|
|
53
70
|
function refreshDisplay(raw, d) {
|
|
54
71
|
if (!isObj(raw))
|
|
55
72
|
return undefined;
|
|
56
73
|
if (canonical(raw.summary) === canonical(d.summary) &&
|
|
57
|
-
canonical(raw
|
|
74
|
+
AFTER_SUMMARY.every((k) => canonical(raw[k]) === canonical(d[k]))) {
|
|
58
75
|
return undefined;
|
|
59
76
|
}
|
|
77
|
+
const after = (out) => {
|
|
78
|
+
for (const k of AFTER_SUMMARY)
|
|
79
|
+
if (d[k] !== undefined)
|
|
80
|
+
out[k] = d[k];
|
|
81
|
+
};
|
|
60
82
|
const out = {};
|
|
61
83
|
for (const [k, v] of Object.entries(raw)) {
|
|
62
|
-
if (k
|
|
84
|
+
if (AFTER_SUMMARY.includes(k))
|
|
63
85
|
continue;
|
|
64
86
|
out[k] = k === "summary" ? d.summary : v;
|
|
65
|
-
if (k === "summary"
|
|
66
|
-
out
|
|
87
|
+
if (k === "summary")
|
|
88
|
+
after(out);
|
|
67
89
|
}
|
|
68
90
|
if (!("summary" in raw)) {
|
|
69
91
|
out.summary = d.summary;
|
|
70
|
-
|
|
71
|
-
out.displayName = d.displayName;
|
|
92
|
+
after(out);
|
|
72
93
|
}
|
|
73
94
|
return out;
|
|
74
95
|
}
|
|
@@ -77,16 +98,18 @@ function refreshDisplay(raw, d) {
|
|
|
77
98
|
* per public plugin, its current version (plugin component management spec §13.2, replacing the
|
|
78
99
|
* one-entry-per-version rule of the package ecosystem spec §5.2). Each public plugin (not
|
|
79
100
|
* `"private": true`) not listed at its version is packed reproducibly with {@link packPlugin} and
|
|
80
|
-
* gets an entry `{ name, version, summary, displayName?, homepage?, permissions, source: {
|
|
81
|
-
* package, version, integrity } } }` (`summary` from the plugin.json `description`, a string
|
|
82
|
-
* `en`; `displayName` copied from plugin.json; `
|
|
101
|
+
* gets an entry `{ name, version, summary, displayName?, icon?, homepage?, permissions, source: {
|
|
102
|
+
* npm: { package, version, integrity } } }` (`summary` from the plugin.json `description`, a string
|
|
103
|
+
* being `en`; `displayName` copied from plugin.json; `icon` the plugin.json `icon` as a path in the
|
|
104
|
+
* repository, `./<plugin dir>/<icon>`, which a git marketplace reads at its refreshed commit;
|
|
105
|
+
* `integrity` the pack's sha512, which the release
|
|
83
106
|
* then publishes byte for byte; `permissions` display only). The new entry takes the place of the
|
|
84
107
|
* plugin's first entry (a version that moved), the plugin's other entries are dropped (also those
|
|
85
108
|
* of a file written in the older one-entry-per-version form), and a plugin not listed yet is
|
|
86
109
|
* appended. Entries of plugins not in the tree are kept. A version already listed is not packed or
|
|
87
110
|
* compared: once published, its bytes are the release gate's business (`release --marketplace`),
|
|
88
111
|
* so a later source or toolchain change at the same version is not a finding; only its display
|
|
89
|
-
* fields (`summary`, `displayName`) are refreshed from the source plugin.json. Every other field
|
|
112
|
+
* fields (`summary`, `displayName`, `icon`) are refreshed from the source plugin.json. Every other field
|
|
90
113
|
* of a kept entry stays as written, among them `deprecated`, which the maintainer sets by hand on
|
|
91
114
|
* a published version's entry (not in plugin.json: that would change the packed bytes); a new
|
|
92
115
|
* version's entry has none, and no rule compares it with the base. With `base`, see
|
|
@@ -150,7 +173,7 @@ export async function syncMarketplace(opts) {
|
|
|
150
173
|
const kept = listed[0];
|
|
151
174
|
/** Keeps the listed entry: drops the other versions, refreshes its display fields. */
|
|
152
175
|
const keep = (raw) => {
|
|
153
|
-
const display = displayOf(source);
|
|
176
|
+
const display = displayOf(source, opts.root, dir);
|
|
154
177
|
const fresh = display === undefined ? undefined : refreshDisplay(raw, display);
|
|
155
178
|
if (dropped.length > 0 || fresh !== undefined) {
|
|
156
179
|
const entry = fresh ?? raw;
|
|
@@ -188,6 +211,9 @@ export async function syncMarketplace(opts) {
|
|
|
188
211
|
version: npm.version,
|
|
189
212
|
summary: summaryOf(r.manifest.description),
|
|
190
213
|
...(r.manifest.displayName !== undefined && { displayName: r.manifest.displayName }),
|
|
214
|
+
...(r.manifest.icon !== undefined && {
|
|
215
|
+
icon: repoIconPath(opts.root, dir, r.manifest.icon),
|
|
216
|
+
}),
|
|
191
217
|
...(r.manifest.homepage !== undefined && { homepage: r.manifest.homepage }),
|
|
192
218
|
permissions: await permissionsOf(bytes, r.manifest.extension !== undefined),
|
|
193
219
|
source: { npm },
|
package/dist/pack/pack.js
CHANGED
|
@@ -7,7 +7,7 @@ import { dirname, join, posix, relative, resolve } from "node:path";
|
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { gzip } from "node:zlib";
|
|
10
|
-
import { AGENT_TEMPLATE_LIMITS, checkEntryImports, defineExtension, HOST_PROVIDED_PACKAGES, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_LIMITS, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, SDK_VERSION, staticManifestOf, validatePluginPackageJsonFor, } from "@capybara-hq/plugin-sdk";
|
|
10
|
+
import { AGENT_TEMPLATE_LIMITS, checkEntryImports, defineExtension, HOST_PROVIDED_PACKAGES, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_LIMITS, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, SDK_VERSION, setupGuideFileProblem, staticManifestOf, unknownSetupGuidePlaceholders, validatePluginPackageJsonFor, } from "@capybara-hq/plugin-sdk";
|
|
11
11
|
import { init as lexerInit, parse as lexerParse } from "es-module-lexer";
|
|
12
12
|
import { build } from "esbuild";
|
|
13
13
|
import { pack as tarPack } from "tar-stream";
|
|
@@ -167,6 +167,8 @@ const SHIPPED_ELSEWHERE = [
|
|
|
167
167
|
"skills",
|
|
168
168
|
"connectors",
|
|
169
169
|
"agents",
|
|
170
|
+
// setup guide templates: the listed files are shipped by contentFiles
|
|
171
|
+
"guides",
|
|
170
172
|
".capybara-plugin",
|
|
171
173
|
"README.md",
|
|
172
174
|
"LICENSE",
|
|
@@ -357,6 +359,32 @@ async function contentFiles(dir, label, m) {
|
|
|
357
359
|
}
|
|
358
360
|
agentNames.set(t.template.name, rel);
|
|
359
361
|
}
|
|
362
|
+
// Setup guides (spec 2026-11-08-plugin-setup-guides-design.md §2): the listed template files,
|
|
363
|
+
// text within the size limit, and only placeholders the place defines (the kernel leaves an
|
|
364
|
+
// unknown one visible; the toolchain refuses it so a typo never ships)
|
|
365
|
+
for (const g of m.setupGuides ?? []) {
|
|
366
|
+
const at = `${label}: setupGuides ${g.id}`;
|
|
367
|
+
const unknown = (text, where) => {
|
|
368
|
+
const names = unknownSetupGuidePlaceholders(text, g.place);
|
|
369
|
+
if (names.length > 0) {
|
|
370
|
+
throw new Error(`${at}: ${where} uses ${names.map((n) => `{{${n}}}`).join(", ")}, which place ${g.place} does not define`);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
g.steps?.forEach((step, i) => {
|
|
374
|
+
unknown(step.en, `step ${i + 1}`);
|
|
375
|
+
if (step.zh !== undefined)
|
|
376
|
+
unknown(step.zh, `step ${i + 1} (zh)`);
|
|
377
|
+
});
|
|
378
|
+
for (const f of g.files ?? []) {
|
|
379
|
+
const rel = stripDot(f.path);
|
|
380
|
+
await addFile(rel, `setup guide ${g.id} file`);
|
|
381
|
+
const data = files.get(rel);
|
|
382
|
+
const problem = setupGuideFileProblem(f.path, new Uint8Array(data));
|
|
383
|
+
if (problem !== undefined)
|
|
384
|
+
throw new Error(`${at}: ${problem}`);
|
|
385
|
+
unknown(data.toString("utf8"), f.path);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
360
388
|
return {
|
|
361
389
|
files,
|
|
362
390
|
skills: m.skills?.length ?? scannedSkills,
|
|
@@ -505,6 +533,9 @@ export async function packPlugin(pluginDir, outDir) {
|
|
|
505
533
|
throw new Error(`${label}: a plugin must contain an extension, a skill, a connector template or an agent template (src/index.ts, skills/<name>/SKILL.md, connectors/<id>.json or agents/<name>.md)`);
|
|
506
534
|
}
|
|
507
535
|
const ext = hasExtension ? await bundleExtension(dir, label, srcPkg.version) : undefined;
|
|
536
|
+
if (ext === undefined && source.setupGuides?.some((g) => g.place === "channel-connection")) {
|
|
537
|
+
throw new Error(`${label}: a channel-connection setup guide needs the plugin's extension to provide channel@1 (src/index.ts)`);
|
|
538
|
+
}
|
|
508
539
|
const files = new Map([...(ext?.files ?? []), ...content.files]);
|
|
509
540
|
const extra = await extraFiles(dir, srcPkg.files);
|
|
510
541
|
for (const [path, data] of extra.files)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/plugin-tools",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.8",
|
|
4
4
|
"description": "Capybara plugin toolchain: the capybara-plugin CLI to pack, check and release Capybara plugins",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"esbuild": "0.28.2",
|
|
52
52
|
"oxc-parser": "^0.154.0",
|
|
53
53
|
"tar-stream": "3.2.2",
|
|
54
|
-
"@capybara-hq/
|
|
55
|
-
"@capybara-hq/
|
|
54
|
+
"@capybara-hq/kernel": "^0.1.0-rc.10",
|
|
55
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.7"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"zod": "^4.6.7",
|
|
59
|
-
"@capybara-hq/testing": "0.1.0-rc.
|
|
59
|
+
"@capybara-hq/testing": "0.1.0-rc.10"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc -b"
|