@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 displayName) and exits 1 only on `marketplace-invalid`.
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>;
@@ -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 displayName) and exits 1 only on `marketplace-invalid`.
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: { npm: {
50
- * package, version, integrity } } }` (`summary` from the plugin.json `description`, a string being
51
- * `en`; `displayName` copied from plugin.json; `integrity` the pack's sha512, which the release
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
- /** The display fields of a plugin's source plugin.json; undefined when it does not parse. */
40
- function displayOf(json) {
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 `displayName` set to `d` (in place, `displayName` right after
51
- * `summary` when it is new, removed when plugin.json has none); undefined when they already equal.
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.displayName) === canonical(d.displayName)) {
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 === "displayName")
84
+ if (AFTER_SUMMARY.includes(k))
63
85
  continue;
64
86
  out[k] = k === "summary" ? d.summary : v;
65
- if (k === "summary" && d.displayName !== undefined)
66
- out.displayName = d.displayName;
87
+ if (k === "summary")
88
+ after(out);
67
89
  }
68
90
  if (!("summary" in raw)) {
69
91
  out.summary = d.summary;
70
- if (d.displayName !== undefined)
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: { npm: {
81
- * package, version, integrity } } }` (`summary` from the plugin.json `description`, a string being
82
- * `en`; `displayName` copied from plugin.json; `integrity` the pack's sha512, which the release
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.7",
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/plugin-sdk": "^0.1.0-rc.6",
55
- "@capybara-hq/kernel": "^0.1.0-rc.9"
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.9"
59
+ "@capybara-hq/testing": "0.1.0-rc.10"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc -b"