@capybara-hq/plugin-tools 0.0.0-stage → 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +111 -2
  4. package/bin/capybara-plugin.mjs +12 -0
  5. package/dist/check/applied-migrations.d.ts +28 -0
  6. package/dist/check/applied-migrations.js +65 -0
  7. package/dist/check/cli.d.ts +16 -0
  8. package/dist/check/cli.js +69 -0
  9. package/dist/check/deps.d.ts +23 -0
  10. package/dist/check/deps.js +207 -0
  11. package/dist/check/index.d.ts +15 -0
  12. package/dist/check/index.js +14 -0
  13. package/dist/check/migrations.d.ts +11 -0
  14. package/dist/check/migrations.js +93 -0
  15. package/dist/check/plugin-pack.d.ts +17 -0
  16. package/dist/check/plugin-pack.js +56 -0
  17. package/dist/check/plugin-permissions.d.ts +22 -0
  18. package/dist/check/plugin-permissions.js +392 -0
  19. package/dist/check/plugin-state.d.ts +19 -0
  20. package/dist/check/plugin-state.js +181 -0
  21. package/dist/check/plugin-versions.d.ts +13 -0
  22. package/dist/check/plugin-versions.js +56 -0
  23. package/dist/check/release-metadata.d.ts +13 -0
  24. package/dist/check/release-metadata.js +80 -0
  25. package/dist/check/report.d.ts +8 -0
  26. package/dist/check/report.js +17 -0
  27. package/dist/check/run.d.ts +36 -0
  28. package/dist/check/run.js +60 -0
  29. package/dist/check/source.d.ts +53 -0
  30. package/dist/check/source.js +153 -0
  31. package/dist/check/sync-versions-cli.d.ts +10 -0
  32. package/dist/check/sync-versions-cli.js +44 -0
  33. package/dist/check/test-imports.d.ts +20 -0
  34. package/dist/check/test-imports.js +158 -0
  35. package/dist/check/tool-design.d.ts +13 -0
  36. package/dist/check/tool-design.js +227 -0
  37. package/dist/cli.d.ts +1 -0
  38. package/dist/cli.js +48 -0
  39. package/dist/discover.d.ts +21 -0
  40. package/dist/discover.js +69 -0
  41. package/dist/index.d.ts +10 -0
  42. package/dist/index.js +10 -0
  43. package/dist/integrity.d.ts +2 -0
  44. package/dist/integrity.js +3 -0
  45. package/dist/marketplace/cli.d.ts +20 -0
  46. package/dist/marketplace/cli.js +80 -0
  47. package/dist/marketplace/file.d.ts +2 -0
  48. package/dist/marketplace/file.js +2 -0
  49. package/dist/marketplace/index.d.ts +2 -0
  50. package/dist/marketplace/index.js +2 -0
  51. package/dist/marketplace/marketplace.d.ts +57 -0
  52. package/dist/marketplace/marketplace.js +245 -0
  53. package/dist/pack/bootstrap.d.ts +25 -0
  54. package/dist/pack/bootstrap.js +70 -0
  55. package/dist/pack/cli.d.ts +20 -0
  56. package/dist/pack/cli.js +109 -0
  57. package/dist/pack/index.d.ts +4 -0
  58. package/dist/pack/index.js +4 -0
  59. package/dist/pack/npm.d.ts +66 -0
  60. package/dist/pack/npm.js +177 -0
  61. package/dist/pack/pack.d.ts +44 -0
  62. package/dist/pack/pack.js +518 -0
  63. package/dist/release/cli.d.ts +30 -0
  64. package/dist/release/cli.js +93 -0
  65. package/dist/release/index.d.ts +4 -0
  66. package/dist/release/index.js +4 -0
  67. package/dist/release/publish-plan.d.ts +68 -0
  68. package/dist/release/publish-plan.js +117 -0
  69. package/dist/release/publish.d.ts +91 -0
  70. package/dist/release/publish.js +274 -0
  71. package/dist/release/semver.d.ts +11 -0
  72. package/dist/release/semver.js +44 -0
  73. package/package.json +61 -3
@@ -0,0 +1,13 @@
1
+ import { type Finding } from "./source.js";
2
+ export type { Finding };
3
+ /**
4
+ * 工具设计检查(AST)。规则:
5
+ * - tool-description:`ctx.tools.register(spec, …)` 与 `BuiltinTool` 字面量(有 spec 与 run)里的 spec
6
+ * 必须有字符串字面量 description(可 `+` 拼接)、不少于 20 字符、不含中文(模型可见文案一律英文);
7
+ * - tool-error-actionable:`new ToolError(msg)` 的 msg 是字符串字面量 / 模板(或本文件内的常量、函数返回值),
8
+ * 且含建议动词 use / call / retry / check / ask / pass / provide 之一,让模型知道下一步做什么。
9
+ * spec 可以直接写对象字面量,也可以引用本文件内的 const;其他写法(跨文件、动态构造)报 tool-description。
10
+ * 已知缺口:从别的文件导入的消息常量 / 函数不追踪;非 ToolError 类的错误不查;运行时拼出的 spec 不查。
11
+ * 扫描:传入目录下的非测试源码。
12
+ */
13
+ export declare function checkToolDesign(dirs: string[]): Finding[];
@@ -0,0 +1,227 @@
1
+ import { isParseError, listSourceFiles, parseFile, stringValue, walk, } from "./source.js";
2
+ const MIN_DESCRIPTION = 20;
3
+ const CJK = /[㐀-鿿豈-﫿 -〿＀-￯]/;
4
+ /** 可执行的建议动词:错误信息里至少出现其一(整词、不分大小写)。 */
5
+ const ACTION_VERB = /\b(?:use|call|retry|check|ask|pass|provide)\b/i;
6
+ const WRAPPERS = new Set([
7
+ "ParenthesizedExpression",
8
+ "TSAsExpression",
9
+ "TSSatisfiesExpression",
10
+ "TSNonNullExpression",
11
+ "TSTypeAssertion",
12
+ ]);
13
+ function unwrap(n) {
14
+ let cur = n;
15
+ while (WRAPPERS.has(cur.type))
16
+ cur = cur.expression;
17
+ return cur;
18
+ }
19
+ const propName = (p) => {
20
+ if (p.type !== "Property" || p.computed)
21
+ return undefined;
22
+ const k = p.key;
23
+ if (k.type === "Identifier")
24
+ return k.name;
25
+ if (k.type === "Literal" && typeof k.value === "string")
26
+ return k.value;
27
+ return undefined;
28
+ };
29
+ function prop(obj, name) {
30
+ for (const p of obj.properties)
31
+ if (propName(p) === name)
32
+ return p;
33
+ return undefined;
34
+ }
35
+ function buildEnv(f) {
36
+ const decls = new Map();
37
+ const imported = new Set();
38
+ walk(f.program, (n) => {
39
+ if (n.type === "VariableDeclarator") {
40
+ const id = n.id;
41
+ if (id.type === "Identifier" && n.init)
42
+ decls.set(id.name, n.init);
43
+ }
44
+ else if (n.type === "FunctionDeclaration") {
45
+ const id = n.id;
46
+ if (id)
47
+ decls.set(id.name, n);
48
+ }
49
+ else if (n.type === "ImportDeclaration") {
50
+ for (const s of n.specifiers)
51
+ imported.add(s.local.name ?? "");
52
+ }
53
+ });
54
+ return { decls, imported };
55
+ }
56
+ /** 把消息表达式尽量还原成静态文本:字面量、模板、`+` 拼接、本地常量、本地函数的返回值。 */
57
+ function resolveText(n, env, depth = 0) {
58
+ if (depth > 5)
59
+ return { kind: "dynamic" };
60
+ const e = unwrap(n);
61
+ const lit = stringValue(e);
62
+ if (lit !== undefined)
63
+ return { kind: "text", text: lit };
64
+ if (e.type === "TemplateLiteral") {
65
+ const quasis = e.quasis;
66
+ return { kind: "text", text: quasis.map((q) => q.value.cooked ?? "").join(" ") };
67
+ }
68
+ if (e.type === "BinaryExpression" && e.operator === "+") {
69
+ const parts = [
70
+ resolveText(e.left, env, depth + 1),
71
+ resolveText(e.right, env, depth + 1),
72
+ ];
73
+ if (parts.some((p) => p.kind === "dynamic")) {
74
+ // 拼接里夹着动态片段:静态部分仍可检查,片段本身不算
75
+ return {
76
+ kind: "text",
77
+ text: parts.map((p) => (p.kind === "text" ? p.text : "")).join(" "),
78
+ };
79
+ }
80
+ if (parts.some((p) => p.kind === "imported"))
81
+ return { kind: "imported" };
82
+ return { kind: "text", text: parts.map((p) => (p.kind === "text" ? p.text : "")).join(" ") };
83
+ }
84
+ if (e.type === "Identifier") {
85
+ const name = e.name;
86
+ if (env.imported.has(name))
87
+ return { kind: "imported" };
88
+ const init = env.decls.get(name);
89
+ return init ? resolveText(init, env, depth + 1) : { kind: "dynamic" };
90
+ }
91
+ if (e.type === "CallExpression") {
92
+ const callee = unwrap(e.callee);
93
+ if (callee.type !== "Identifier")
94
+ return { kind: "dynamic" };
95
+ const name = callee.name;
96
+ if (env.imported.has(name))
97
+ return { kind: "imported" };
98
+ const fn = env.decls.get(name);
99
+ if (!fn)
100
+ return { kind: "dynamic" };
101
+ return resolveFnReturn(fn, env, depth + 1);
102
+ }
103
+ return { kind: "dynamic" };
104
+ }
105
+ function resolveFnReturn(fn, env, depth) {
106
+ const f = unwrap(fn);
107
+ if (f.type !== "ArrowFunctionExpression" &&
108
+ f.type !== "FunctionExpression" &&
109
+ f.type !== "FunctionDeclaration") {
110
+ return { kind: "dynamic" };
111
+ }
112
+ const body = f.body;
113
+ if (body.type !== "BlockStatement")
114
+ return resolveText(body, env, depth);
115
+ const texts = [];
116
+ let bad = false;
117
+ walk(body, (n) => {
118
+ if (n.type !== "ReturnStatement" || !n.argument)
119
+ return;
120
+ const r = resolveText(n.argument, env, depth + 1);
121
+ if (r.kind === "text")
122
+ texts.push(r.text);
123
+ else
124
+ bad = true;
125
+ });
126
+ return bad || texts.length === 0 ? { kind: "dynamic" } : { kind: "text", text: texts.join(" ") };
127
+ }
128
+ const MSG_DESC_MISSING = "工具缺少 description,或它不是字符串字面量。给模型的描述必须写成可静态检查的英文字符串字面量(可用 + 拼接)";
129
+ const MSG_DESC_SHORT = `工具 description 至少 ${MIN_DESCRIPTION} 个字符:说清工具做什么、何时该用、何时不该用`;
130
+ const MSG_DESC_CJK = "工具 description 是模型可见文案,必须是英文,不得含中文字符";
131
+ const MSG_ERR_DYNAMIC = "ToolError 的消息必须是字符串字面量或模板(或指向本文件内的此类常量 / 函数),否则无法检查是否可执行";
132
+ const MSG_ERR_ACTION = "ToolError 的消息要让模型知道下一步怎么做:必须含建议动词之一(use / call / retry / check / ask / pass / provide)";
133
+ /** 取一个 spec 表达式对应的对象字面量(直接写,或经本文件内的常量引用)。 */
134
+ function specObject(n, env) {
135
+ let e = unwrap(n);
136
+ for (let i = 0; i < 5 && e.type === "Identifier"; i++) {
137
+ const init = env.decls.get(e.name);
138
+ if (!init)
139
+ return undefined;
140
+ e = unwrap(init);
141
+ }
142
+ return e.type === "ObjectExpression" ? e : undefined;
143
+ }
144
+ function checkSpec(f, at, spec, out) {
145
+ const add = (node, message) => {
146
+ out.push({ file: f.file, line: f.lineOf(node.start), rule: "tool-description", message });
147
+ };
148
+ const p = spec ? prop(spec, "description") : undefined;
149
+ const value = p ? stringValue(unwrap(p.value)) : undefined;
150
+ if (value === undefined)
151
+ add(at, MSG_DESC_MISSING);
152
+ else if (value.trim().length < MIN_DESCRIPTION)
153
+ add(at, MSG_DESC_SHORT);
154
+ else if (CJK.test(value))
155
+ add(at, MSG_DESC_CJK);
156
+ }
157
+ function scanFile(f) {
158
+ const out = [];
159
+ const env = buildEnv(f);
160
+ walk(f.program, (n) => {
161
+ // ctx.tools.register(spec, handler)
162
+ if (n.type === "CallExpression") {
163
+ const c = unwrap(n.callee);
164
+ if (c.type === "MemberExpression" &&
165
+ !c.computed &&
166
+ c.property.name === "register") {
167
+ const o = unwrap(c.object);
168
+ const isTools = (o.type === "MemberExpression" && !o.computed && o.property.name === "tools") ||
169
+ (o.type === "Identifier" && o.name === "tools");
170
+ if (isTools) {
171
+ const first = n.arguments[0];
172
+ checkSpec(f, n, first ? specObject(first, env) : undefined, out);
173
+ }
174
+ }
175
+ }
176
+ // BuiltinTool 对象字面量:同时有 spec 与 run
177
+ if (n.type === "ObjectExpression" && prop(n, "spec") && prop(n, "run")) {
178
+ const sp = prop(n, "spec");
179
+ checkSpec(f, sp, specObject(sp.value, env), out);
180
+ }
181
+ // throw new ToolError(...),也包括 return new ToolError(...)
182
+ if (n.type === "NewExpression") {
183
+ const callee = unwrap(n.callee);
184
+ if (callee.type !== "Identifier" || callee.name !== "ToolError")
185
+ return;
186
+ const arg = n.arguments[0];
187
+ const line = f.lineOf(n.start);
188
+ const add = (message) => out.push({ file: f.file, line, rule: "tool-error-actionable", message });
189
+ if (!arg)
190
+ return add(MSG_ERR_ACTION);
191
+ const r = resolveText(arg, env);
192
+ if (r.kind === "imported")
193
+ return; // 定义在别处,不跨文件追踪(已知缺口)
194
+ if (r.kind === "dynamic")
195
+ return add(MSG_ERR_DYNAMIC);
196
+ if (!ACTION_VERB.test(r.text))
197
+ add(MSG_ERR_ACTION);
198
+ }
199
+ });
200
+ return out;
201
+ }
202
+ /**
203
+ * 工具设计检查(AST)。规则:
204
+ * - tool-description:`ctx.tools.register(spec, …)` 与 `BuiltinTool` 字面量(有 spec 与 run)里的 spec
205
+ * 必须有字符串字面量 description(可 `+` 拼接)、不少于 20 字符、不含中文(模型可见文案一律英文);
206
+ * - tool-error-actionable:`new ToolError(msg)` 的 msg 是字符串字面量 / 模板(或本文件内的常量、函数返回值),
207
+ * 且含建议动词 use / call / retry / check / ask / pass / provide 之一,让模型知道下一步做什么。
208
+ * spec 可以直接写对象字面量,也可以引用本文件内的 const;其他写法(跨文件、动态构造)报 tool-description。
209
+ * 已知缺口:从别的文件导入的消息常量 / 函数不追踪;非 ToolError 类的错误不查;运行时拼出的 spec 不查。
210
+ * 扫描:传入目录下的非测试源码。
211
+ */
212
+ export function checkToolDesign(dirs) {
213
+ const findings = [];
214
+ for (const dir of dirs) {
215
+ for (const { file, isTest } of listSourceFiles(dir)) {
216
+ if (isTest)
217
+ continue;
218
+ const parsed = parseFile(file);
219
+ if (isParseError(parsed)) {
220
+ findings.push(parsed);
221
+ continue;
222
+ }
223
+ findings.push(...scanFile(parsed).sort((a, b) => a.line - b.line));
224
+ }
225
+ }
226
+ return findings;
227
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `capybara-plugin`: pack, check, sync the versions of and release Capybara plugins.
3
+ *
4
+ * capybara-plugin pack [<dir>...] [--all <root>] [--out <dir>]
5
+ * capybara-plugin pack --bootstrap <manifest> [--root <dir>] [--out <dir>] [--registry <url>]
6
+ * [--npm-token <token>]
7
+ * capybara-plugin check [--root <dir>] [--plugins <glob>] [--base <ref>]
8
+ * capybara-plugin sync-versions [--root <dir>] [--plugins <glob>]
9
+ * capybara-plugin marketplace --write|--check [--base <rev>] [--root <dir>] [--file <path>]
10
+ * [--plugins <glob>]
11
+ * capybara-plugin release [--dry-run] [--provenance] [--out <dir>] [--auto] [--root <dir>]
12
+ * [--plugins <pattern>]... [--marketplace <file>]
13
+ *
14
+ * Relative paths resolve against the current directory of the process (`capybara-plugin pack .`
15
+ * in a package script packs that package). `<command> --help` prints one command's usage.
16
+ */
17
+ import { CHECK_USAGE, checkCommand } from "./check/cli.js";
18
+ import { SYNC_VERSIONS_USAGE, syncVersionsCommand } from "./check/sync-versions-cli.js";
19
+ import { MARKETPLACE_USAGE, marketplaceCommand } from "./marketplace/cli.js";
20
+ import { PACK_USAGE, packCommand } from "./pack/cli.js";
21
+ import { RELEASE_USAGE, releaseCommand } from "./release/cli.js";
22
+ const COMMANDS = {
23
+ pack: packCommand,
24
+ check: checkCommand,
25
+ "sync-versions": syncVersionsCommand,
26
+ marketplace: marketplaceCommand,
27
+ release: releaseCommand,
28
+ };
29
+ const USAGE = `usage: ${[PACK_USAGE, CHECK_USAGE, SYNC_VERSIONS_USAGE, MARKETPLACE_USAGE, RELEASE_USAGE].join("\n ")}`;
30
+ const [command, ...args] = process.argv.slice(2);
31
+ const run = command === undefined ? undefined : COMMANDS[command];
32
+ if (command === "--help" || command === "-h" || command === "help") {
33
+ console.log(USAGE);
34
+ }
35
+ else if (run === undefined) {
36
+ console.error(`${command === undefined ? "no command" : `unknown command ${command}`}\n${USAGE}`);
37
+ process.exitCode = 2;
38
+ }
39
+ else {
40
+ try {
41
+ process.exitCode = await run(args, process.cwd());
42
+ }
43
+ catch (e) {
44
+ // parseArgs rejects unknown options and missing values.
45
+ console.error(`${e instanceof Error ? e.message : String(e)}\n${USAGE}`);
46
+ process.exitCode = 2;
47
+ }
48
+ }
@@ -0,0 +1,21 @@
1
+ /** The default plugin glob of a repository with a `plugins/` directory. */
2
+ export declare const DEFAULT_PLUGINS_GLOB = "plugins/*";
3
+ /**
4
+ * A POSIX path glob (`*` and `?` within one segment; `**` is not supported) as a regular expression
5
+ * that matches the whole path.
6
+ */
7
+ export declare function globToRegExp(glob: string): RegExp;
8
+ /** Directories under `root` matching `glob` (sorted, absolute); `node_modules` never matches. */
9
+ export declare function expandGlob(root: string, glob: string): string[];
10
+ /**
11
+ * The plugin directories of a repository, as `capybara-plugin pack --all` and `check` find them:
12
+ * with `glob`, the matching directories under `root` that hold a `package.json` or a
13
+ * `.capybara-plugin/plugin.json`; without, `<root>/plugins/*` when `<root>/plugins` exists, else
14
+ * `root` itself when it is a plugin (has `.capybara-plugin/plugin.json`). Sorted, absolute.
15
+ */
16
+ export declare function discoverPlugins(root: string, glob?: string): string[];
17
+ /**
18
+ * The glob (relative to `root`) that {@link discoverPlugins} uses: `glob` itself, `plugins/*`, or
19
+ * `.` for a single-plugin repository.
20
+ */
21
+ export declare function pluginsGlobOf(root: string, glob?: string): string;
@@ -0,0 +1,69 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { PLUGIN_MANIFEST_FILE } from "@capybara-hq/plugin-sdk";
4
+ /** The default plugin glob of a repository with a `plugins/` directory. */
5
+ export const DEFAULT_PLUGINS_GLOB = "plugins/*";
6
+ const escapeRe = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
7
+ /** One glob segment (`*` and `?`, never across `/`) as a regular expression source. */
8
+ const segmentRe = (seg) => escapeRe(seg).replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
9
+ /**
10
+ * A POSIX path glob (`*` and `?` within one segment; `**` is not supported) as a regular expression
11
+ * that matches the whole path.
12
+ */
13
+ export function globToRegExp(glob) {
14
+ const segs = normalizeGlob(glob);
15
+ return new RegExp(`^${segs.map(segmentRe).join("/")}$`);
16
+ }
17
+ function normalizeGlob(glob) {
18
+ const segs = glob
19
+ .replace(/\\/g, "/")
20
+ .split("/")
21
+ .filter((s) => s !== "" && s !== ".");
22
+ if (segs.some((s) => s.includes("**") || s === "..")) {
23
+ throw new Error(`unsupported plugin glob ${glob}: use * within one path segment, no ** or ..`);
24
+ }
25
+ return segs;
26
+ }
27
+ /** Directories under `root` matching `glob` (sorted, absolute); `node_modules` never matches. */
28
+ export function expandGlob(root, glob) {
29
+ let dirs = [root];
30
+ for (const seg of normalizeGlob(glob)) {
31
+ const re = new RegExp(`^${segmentRe(seg)}$`);
32
+ const next = [];
33
+ for (const d of dirs) {
34
+ if (!existsSync(d))
35
+ continue;
36
+ for (const e of readdirSync(d, { withFileTypes: true })) {
37
+ if (e.isDirectory() && e.name !== "node_modules" && re.test(e.name)) {
38
+ next.push(join(d, e.name));
39
+ }
40
+ }
41
+ }
42
+ dirs = next;
43
+ }
44
+ return dirs.sort();
45
+ }
46
+ const isPluginDir = (d) => existsSync(join(d, "package.json")) || existsSync(join(d, PLUGIN_MANIFEST_FILE));
47
+ /**
48
+ * The plugin directories of a repository, as `capybara-plugin pack --all` and `check` find them:
49
+ * with `glob`, the matching directories under `root` that hold a `package.json` or a
50
+ * `.capybara-plugin/plugin.json`; without, `<root>/plugins/*` when `<root>/plugins` exists, else
51
+ * `root` itself when it is a plugin (has `.capybara-plugin/plugin.json`). Sorted, absolute.
52
+ */
53
+ export function discoverPlugins(root, glob) {
54
+ if (glob !== undefined)
55
+ return expandGlob(root, glob).filter(isPluginDir);
56
+ if (existsSync(join(root, "plugins"))) {
57
+ return expandGlob(root, DEFAULT_PLUGINS_GLOB).filter(isPluginDir);
58
+ }
59
+ return existsSync(join(root, PLUGIN_MANIFEST_FILE)) ? [root] : [];
60
+ }
61
+ /**
62
+ * The glob (relative to `root`) that {@link discoverPlugins} uses: `glob` itself, `plugins/*`, or
63
+ * `.` for a single-plugin repository.
64
+ */
65
+ export function pluginsGlobOf(root, glob) {
66
+ if (glob !== undefined)
67
+ return glob;
68
+ return existsSync(join(root, "plugins")) ? DEFAULT_PLUGINS_GLOB : ".";
69
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@capybara-hq/plugin-tools`: the toolchain behind the `capybara-plugin` CLI (pack, check, release)
3
+ * as a library. Subpaths without the kernel: `./pack`, `./release`, `./source` (the source scanner
4
+ * the checks are built on).
5
+ */
6
+ export * from "./check/index.js";
7
+ export { DEFAULT_PLUGINS_GLOB, discoverPlugins, expandGlob, globToRegExp, pluginsGlobOf, } from "./discover.js";
8
+ export * from "./marketplace/index.js";
9
+ export * from "./pack/index.js";
10
+ export * from "./release/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@capybara-hq/plugin-tools`: the toolchain behind the `capybara-plugin` CLI (pack, check, release)
3
+ * as a library. Subpaths without the kernel: `./pack`, `./release`, `./source` (the source scanner
4
+ * the checks are built on).
5
+ */
6
+ export * from "./check/index.js";
7
+ export { DEFAULT_PLUGINS_GLOB, discoverPlugins, expandGlob, globToRegExp, pluginsGlobOf, } from "./discover.js";
8
+ export * from "./marketplace/index.js";
9
+ export * from "./pack/index.js";
10
+ export * from "./release/index.js";
@@ -0,0 +1,2 @@
1
+ /** sha512 SRI (`sha512-<base64>`), the form marketplaces, bootstrap pins and npm pin tgz files with. */
2
+ export declare const integrityOf: (bytes: Uint8Array) => string;
@@ -0,0 +1,3 @@
1
+ import { createHash } from "node:crypto";
2
+ /** sha512 SRI (`sha512-<base64>`), the form marketplaces, bootstrap pins and npm pin tgz files with. */
3
+ export const integrityOf = (bytes) => `sha512-${createHash("sha512").update(bytes).digest("base64")}`;
@@ -0,0 +1,20 @@
1
+ export declare const MARKETPLACE_USAGE: string;
2
+ /**
3
+ * `capybara-plugin marketplace`: writes (`--write`) or checks (`--check`) the repository's
4
+ * marketplace manifest against reproducible packs of its public plugins ({@link syncMarketplace}).
5
+ *
6
+ * - `--root <dir>`: the repository (default: the current directory);
7
+ * - `--file <path>`: the manifest, relative to the root (default `.capybara-plugin/marketplace.json`);
8
+ * - `--plugins <glob>`: the plugin directories, relative to the root (default `plugins/*`, or the
9
+ * root itself when it is a plugin).
10
+ *
11
+ * - `--base <rev>` (with `--check`, on pull requests): the entries the file had at `<rev>` must be
12
+ * unchanged and first, in order (`marketplace-append-only`), and the entries added since must
13
+ * match the tree's packs (`marketplace-integrity`).
14
+ *
15
+ * `--check` exits 1 on any finding (`marketplace-missing`, `marketplace-integrity`,
16
+ * `marketplace-append-only`, `marketplace-invalid`); `--write` appends the missing entries and
17
+ * exits 1 only on `marketplace-invalid`.
18
+ * Relative paths resolve against `cwd`. Returns the exit code.
19
+ */
20
+ export declare function marketplaceCommand(args: string[], cwd: string): Promise<number>;
@@ -0,0 +1,80 @@
1
+ import { resolve } from "node:path";
2
+ import { parseArgs } from "node:util";
3
+ import { formatFinding } from "../check/report.js";
4
+ import { MARKETPLACE_FILE, syncMarketplace } from "./marketplace.js";
5
+ export const MARKETPLACE_USAGE = "capybara-plugin marketplace --write|--check [--base <rev>] [--root <dir>] [--file <path>]\n" +
6
+ " [--plugins <glob>]";
7
+ /**
8
+ * `capybara-plugin marketplace`: writes (`--write`) or checks (`--check`) the repository's
9
+ * marketplace manifest against reproducible packs of its public plugins ({@link syncMarketplace}).
10
+ *
11
+ * - `--root <dir>`: the repository (default: the current directory);
12
+ * - `--file <path>`: the manifest, relative to the root (default `.capybara-plugin/marketplace.json`);
13
+ * - `--plugins <glob>`: the plugin directories, relative to the root (default `plugins/*`, or the
14
+ * root itself when it is a plugin).
15
+ *
16
+ * - `--base <rev>` (with `--check`, on pull requests): the entries the file had at `<rev>` must be
17
+ * unchanged and first, in order (`marketplace-append-only`), and the entries added since must
18
+ * match the tree's packs (`marketplace-integrity`).
19
+ *
20
+ * `--check` exits 1 on any finding (`marketplace-missing`, `marketplace-integrity`,
21
+ * `marketplace-append-only`, `marketplace-invalid`); `--write` appends the missing entries and
22
+ * exits 1 only on `marketplace-invalid`.
23
+ * Relative paths resolve against `cwd`. Returns the exit code.
24
+ */
25
+ export async function marketplaceCommand(args, cwd) {
26
+ const { values } = parseArgs({
27
+ args,
28
+ options: {
29
+ help: { type: "boolean", short: "h", default: false },
30
+ write: { type: "boolean", default: false },
31
+ check: { type: "boolean", default: false },
32
+ root: { type: "string" },
33
+ file: { type: "string" },
34
+ plugins: { type: "string" },
35
+ base: { type: "string" },
36
+ },
37
+ });
38
+ if (values.help) {
39
+ console.log(`usage: ${MARKETPLACE_USAGE}`);
40
+ return 0;
41
+ }
42
+ if (values.write === values.check) {
43
+ console.error(`give exactly one of --write and --check\nusage: ${MARKETPLACE_USAGE}`);
44
+ return 2;
45
+ }
46
+ if (values.base !== undefined && values.write) {
47
+ console.error(`--base goes with --check\nusage: ${MARKETPLACE_USAGE}`);
48
+ return 2;
49
+ }
50
+ const root = values.root === undefined ? cwd : resolve(cwd, values.root);
51
+ const file = resolve(root, values.file ?? MARKETPLACE_FILE);
52
+ try {
53
+ const r = await syncMarketplace({
54
+ root,
55
+ file,
56
+ write: values.write,
57
+ ...(values.plugins !== undefined && { plugins: values.plugins }),
58
+ ...(values.base !== undefined && { base: values.base }),
59
+ });
60
+ for (const f of r.findings)
61
+ console.error(formatFinding(f, root));
62
+ if (r.findings.length > 0) {
63
+ console.error(`\n${r.findings.length} marketplace finding(s)`);
64
+ return 1;
65
+ }
66
+ if (values.write) {
67
+ for (const e of r.added)
68
+ console.log(`added ${e.name}@${e.version}`);
69
+ console.log(r.added.length === 0 ? "marketplace is up to date" : `wrote ${file}`);
70
+ }
71
+ else {
72
+ console.log("marketplace is up to date");
73
+ }
74
+ return 0;
75
+ }
76
+ catch (e) {
77
+ console.error(e instanceof Error ? e.message : String(e));
78
+ return 1;
79
+ }
80
+ }
@@ -0,0 +1,2 @@
1
+ /** Where a plugin repository keeps its marketplace manifest (relative to the root). */
2
+ export declare const MARKETPLACE_FILE = ".capybara-plugin/marketplace.json";
@@ -0,0 +1,2 @@
1
+ /** Where a plugin repository keeps its marketplace manifest (relative to the root). */
2
+ export const MARKETPLACE_FILE = ".capybara-plugin/marketplace.json";
@@ -0,0 +1,2 @@
1
+ export { MARKETPLACE_USAGE, marketplaceCommand } from "./cli.js";
2
+ export { formatMarketplaceJson, MARKETPLACE_FILE, type SyncMarketplaceOptions, type SyncMarketplaceResult, syncMarketplace, } from "./marketplace.js";
@@ -0,0 +1,2 @@
1
+ export { MARKETPLACE_USAGE, marketplaceCommand } from "./cli.js";
2
+ export { formatMarketplaceJson, MARKETPLACE_FILE, syncMarketplace, } from "./marketplace.js";
@@ -0,0 +1,57 @@
1
+ import { type MarketplaceEntry } from "@capybara-hq/plugin-sdk";
2
+ import type { Finding } from "../check/source.js";
3
+ export { MARKETPLACE_FILE } from "./file.js";
4
+ /** Options of {@link syncMarketplace}. */
5
+ export interface SyncMarketplaceOptions {
6
+ /** The repository root (its plugins: `plugins/*`, or the root when it is a plugin). */
7
+ root: string;
8
+ /** The marketplace manifest (absolute). It must exist: its name and owner are the maintainers'. */
9
+ file: string;
10
+ /** Plugin directories relative to `root` (`*` within one segment); default as `discoverPlugins`. */
11
+ plugins?: string;
12
+ /** Append the missing entries to `file` (`--write`); otherwise only report them (`--check`). */
13
+ write: boolean;
14
+ /**
15
+ * A git revision (`--check --base <rev>`, pull requests): the entries the file had there are
16
+ * published and must still be there, unchanged and in the same order (only appending is
17
+ * allowed; `marketplace-append-only`), and every entry added since must match the tree's pack
18
+ * (`marketplace-integrity`). A file the base does not have constrains nothing.
19
+ */
20
+ base?: string;
21
+ }
22
+ /** The result of {@link syncMarketplace}. */
23
+ export interface SyncMarketplaceResult {
24
+ /** Entries for `(name, version)` pairs the file does not list yet (appended with `write`). */
25
+ added: MarketplaceEntry[];
26
+ /**
27
+ * `marketplace-invalid` (the file does not parse, or would not after appending),
28
+ * `marketplace-integrity` (with a base: an entry added since names another package or integrity
29
+ * than the tree's pack), `marketplace-append-only` (with a base: a published entry changed, moved
30
+ * or went away) and, without `write`, `marketplace-missing` (one per added entry).
31
+ */
32
+ findings: Finding[];
33
+ }
34
+ /**
35
+ * `capybara-plugin marketplace`: keeps a plugin repository's `marketplace.json` listing every
36
+ * published version of its public plugins (package ecosystem spec §5.2; one entry per
37
+ * `(name, version)`, Decision 7 of phase A3b). Each public plugin (not `"private": true`) is packed
38
+ * reproducibly with {@link packPlugin}; when the file has no entry for its name and version, an
39
+ * entry `{ name, version, summary, homepage?, permissions, source: { npm: { package, version,
40
+ * integrity } } }` is added (`integrity` the pack's sha512, which the release then publishes
41
+ * byte for byte; `permissions` display only). A version already listed is not packed or compared:
42
+ * once published, its bytes are the release gate's business (`release --marketplace`), so a later
43
+ * source or toolchain change at the same version is not a finding. With `base`, published entries
44
+ * must be unchanged and first in order, and entries added since must match the tree's packs.
45
+ * Entries of versions no longer in the tree are kept, so a pinned commit lists what it pins and
46
+ * older versions stay importable. The file is written formatted as Biome formats it
47
+ * ({@link formatMarketplaceJson}).
48
+ * With `write`, the new entries are appended in plugin order (nothing is written when there is any
49
+ * other finding).
50
+ */
51
+ export declare function syncMarketplace(opts: SyncMarketplaceOptions): Promise<SyncMarketplaceResult>;
52
+ /**
53
+ * `marketplace.json` as Biome formats it with a 2-space indent and a 100-column line width: a
54
+ * non-empty object puts each member on its own line; an array of scalars stays on one line when
55
+ * it fits (the trailing comma counted), otherwise one item per line; empty ones are `[]` / `{}`.
56
+ */
57
+ export declare function formatMarketplaceJson(value: unknown): string;