@capybara-hq/plugin-tools 0.1.0-rc.1 → 0.1.0-rc.3
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/README.md +3 -9
- package/bin/capybara-plugin.mjs +1 -1
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/plugin-permissions.d.ts +17 -0
- package/dist/check/plugin-permissions.js +30 -3
- package/dist/check/plugin-tool-keys.d.ts +13 -0
- package/dist/check/plugin-tool-keys.js +38 -0
- package/dist/check/run.js +2 -0
- package/dist/check/tool-design.d.ts +1 -1
- package/dist/check/tool-design.js +3 -3
- package/dist/cli.js +0 -2
- package/dist/pack/cli.d.ts +3 -10
- package/dist/pack/cli.js +3 -40
- package/dist/pack/index.d.ts +0 -1
- package/dist/pack/index.js +0 -1
- package/package.json +4 -4
- package/dist/pack/bootstrap.d.ts +0 -25
- package/dist/pack/bootstrap.js +0 -70
package/README.md
CHANGED
|
@@ -14,8 +14,6 @@ npm install --save-dev @capybara-hq/plugin-tools
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
capybara-plugin pack [<dir>...] [--all <root>] [--out <dir>]
|
|
17
|
-
capybara-plugin pack --bootstrap <manifest> [--root <dir>] [--out <dir>] [--registry <url>]
|
|
18
|
-
[--npm-token <token>]
|
|
19
17
|
capybara-plugin check [--root <dir>] [--plugins <glob>] [--base <ref>]
|
|
20
18
|
capybara-plugin sync-versions [--root <dir>] [--plugins <glob>]
|
|
21
19
|
capybara-plugin marketplace --write|--check [--base <rev>] [--root <dir>] [--file <path>]
|
|
@@ -30,12 +28,8 @@ capybara-plugin release [--dry-run] [--provenance] [--out <dir>] [--auto] [--roo
|
|
|
30
28
|
manifest written into `plugin.json`, LICENSE / NOTICE and the bundled dependencies' notices
|
|
31
29
|
included. A plugin with an extension gets `"main"` and `"exports": { ".": "./dist/index.mjs" }`,
|
|
32
30
|
so once installed it resolves by package name (a test loads `@capybara-hq/plugin-model-fake`
|
|
33
|
-
from npm). `--all <root>` packs every plugin of a repository
|
|
34
|
-
a
|
|
35
|
-
`--registry`, default `CAPYBARA_PACKAGES__NPM_REGISTRY` or the public registry, with
|
|
36
|
-
`--npm-token` / `CAPYBARA_PACKAGES__NPM_TOKEN` sent to the registry origin only, tarballs capped
|
|
37
|
-
at 50 MiB) and checks them against their pinned sha512 and their `package.json` name and version.
|
|
38
|
-
Output: `artifacts/plugins`.
|
|
31
|
+
from npm). `--all <root>` packs every plugin of a repository. Output: `artifacts/plugins`. (The
|
|
32
|
+
bootstrap plugins a kernel pins are no longer packed here: a starting kernel downloads them.)
|
|
39
33
|
- **check**: every plugin check, on the plugins under `<root>/plugins/*` (or `--plugins <glob>`, or
|
|
40
34
|
the root itself when it is a plugin): the dependency boundary (a plugin depends only on
|
|
41
35
|
`@capybara-hq/plugin-sdk` and third-party packages), extension state under multiple replicas (no
|
|
@@ -101,7 +95,7 @@ and hides packaging bugs.
|
|
|
101
95
|
|
|
102
96
|
## Library
|
|
103
97
|
|
|
104
|
-
The same code as functions: `packPlugin`, `
|
|
98
|
+
The same code as functions: `packPlugin`, `downloadPinnedNpmPackage`, `pluginChecks` / `runChecks` and each
|
|
105
99
|
check (`checkDeps`, `checkPluginState`, `checkPluginPermissions`, `checkPluginPack`,
|
|
106
100
|
`checkToolDesign`, `checkMigrations`, `checkAppliedMigrations`, `checkPluginTestImports`,
|
|
107
101
|
`checkPluginReleaseMetadata`), `syncPluginVersions` (after `changeset version`),
|
package/bin/capybara-plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The `capybara-plugin` bin: runs the compiled CLI (dist/cli.js, built by `tsc -b`). In this
|
|
3
|
-
// repository, the root scripts (pnpm pack-plugin, pack-plugins,
|
|
3
|
+
// repository, the root scripts (pnpm pack-plugin, pack-plugins, release) run the
|
|
4
4
|
// TypeScript sources through tsx instead.
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
|
package/dist/check/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { checkMigrations, checkSql } from "./migrations.js";
|
|
|
5
5
|
export { checkPluginPack, PLUGIN_PACK_MAX_BYTES } from "./plugin-pack.js";
|
|
6
6
|
export { checkPluginPermissions } from "./plugin-permissions.js";
|
|
7
7
|
export { checkPluginState } from "./plugin-state.js";
|
|
8
|
+
export { checkPluginToolKeys, TOOL_KEY_MAX } from "./plugin-tool-keys.js";
|
|
8
9
|
export { syncPluginVersions } from "./plugin-versions.js";
|
|
9
10
|
export { checkPluginReleaseMetadata } from "./release-metadata.js";
|
|
10
11
|
export { formatFinding, printCheck } from "./report.js";
|
package/dist/check/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { checkMigrations, checkSql } from "./migrations.js";
|
|
|
5
5
|
export { checkPluginPack, PLUGIN_PACK_MAX_BYTES } from "./plugin-pack.js";
|
|
6
6
|
export { checkPluginPermissions } from "./plugin-permissions.js";
|
|
7
7
|
export { checkPluginState } from "./plugin-state.js";
|
|
8
|
+
export { checkPluginToolKeys, TOOL_KEY_MAX } from "./plugin-tool-keys.js";
|
|
8
9
|
export { syncPluginVersions } from "./plugin-versions.js";
|
|
9
10
|
export { checkPluginReleaseMetadata } from "./release-metadata.js";
|
|
10
11
|
export { formatFinding, printCheck } from "./report.js";
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { type Finding } from "./source.js";
|
|
2
2
|
export type { Finding };
|
|
3
|
+
/** A `ctx.tools.register({ name })` call found in an extension's setup. */
|
|
4
|
+
export interface ToolRegistration {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
/** The extension name of the enclosing defineExtension (undefined: not a literal). */
|
|
8
|
+
extension: string | undefined;
|
|
9
|
+
/** The tool name (undefined: not a literal or same-file const). */
|
|
10
|
+
tool: string | undefined;
|
|
11
|
+
/** The call carries a `capybara:allow-dynamic-permission <reason>` marker. */
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* 插件权限静态检查(AST):把 setup(ctx) 里对平台能力的访问与 defineExtension 清单比对。规则:
|
|
5
16
|
* - perm-secrets:用了 ctx.secrets 而 permissions.secrets 不是 true;
|
|
@@ -20,3 +31,9 @@ export type { Finding };
|
|
|
20
31
|
* 扫描范围:传入目录下的非测试源码。
|
|
21
32
|
*/
|
|
22
33
|
export declare function checkPluginPermissions(pluginDirs: string[]): Finding[];
|
|
34
|
+
/**
|
|
35
|
+
* Every `ctx.tools.register({ name })` of the non-test sources under `pluginDirs`, found by the
|
|
36
|
+
* same scanner as {@link checkPluginPermissions} (so it follows ctx aliases and same-file helpers).
|
|
37
|
+
* Unparseable files are skipped here; the permissions check reports them.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findToolRegistrations(pluginDirs: string[]): ToolRegistration[];
|
|
@@ -115,6 +115,7 @@ function readManifest(obj, decls) {
|
|
|
115
115
|
caps: new Set(),
|
|
116
116
|
storage: false,
|
|
117
117
|
models: false,
|
|
118
|
+
name: stringValue(prop(obj, "name")?.value ? resolve(prop(obj, "name")?.value, decls) : undefined),
|
|
118
119
|
setup: undefined,
|
|
119
120
|
};
|
|
120
121
|
for (const key of ["requires", "optional", "provides"]) {
|
|
@@ -165,7 +166,7 @@ function readManifest(obj, decls) {
|
|
|
165
166
|
}
|
|
166
167
|
return m;
|
|
167
168
|
}
|
|
168
|
-
function scanSetup(f, m, decls) {
|
|
169
|
+
function scanSetup(f, m, decls, onTool) {
|
|
169
170
|
const out = [];
|
|
170
171
|
/** 明确的违规:豁免注释不起作用。 */
|
|
171
172
|
const add = (at, rule, message) => {
|
|
@@ -253,6 +254,13 @@ function scanSetup(f, m, decls) {
|
|
|
253
254
|
const spec = args[0] ? resolve(args[0], decls) : undefined;
|
|
254
255
|
const nameProp = spec?.type === "ObjectExpression" ? prop(spec, "name") : undefined;
|
|
255
256
|
const name = nameProp ? stringValue(resolve(nameProp.value, decls)) : undefined;
|
|
257
|
+
onTool?.({
|
|
258
|
+
file: f.file,
|
|
259
|
+
line: f.lineOf(n.start),
|
|
260
|
+
extension: m.name,
|
|
261
|
+
tool: name,
|
|
262
|
+
allowed: !!findMarker(f, n)?.reason,
|
|
263
|
+
});
|
|
256
264
|
if (name === undefined)
|
|
257
265
|
return dynamic(n, "perm-tools", "the tool name");
|
|
258
266
|
if (!m.tools.has(name)) {
|
|
@@ -338,7 +346,7 @@ function scanSetup(f, m, decls) {
|
|
|
338
346
|
}
|
|
339
347
|
return out;
|
|
340
348
|
}
|
|
341
|
-
function scanFile(f) {
|
|
349
|
+
function scanFile(f, onTool) {
|
|
342
350
|
const decls = buildDecls(f);
|
|
343
351
|
const out = [];
|
|
344
352
|
walk(f.program, (n) => {
|
|
@@ -351,7 +359,7 @@ function scanFile(f) {
|
|
|
351
359
|
const obj = arg ? resolve(arg, decls) : undefined;
|
|
352
360
|
if (obj?.type !== "ObjectExpression")
|
|
353
361
|
return;
|
|
354
|
-
out.push(...scanSetup(f, readManifest(obj, decls), decls));
|
|
362
|
+
out.push(...scanSetup(f, readManifest(obj, decls), decls, onTool));
|
|
355
363
|
});
|
|
356
364
|
return out;
|
|
357
365
|
}
|
|
@@ -390,3 +398,22 @@ export function checkPluginPermissions(pluginDirs) {
|
|
|
390
398
|
}
|
|
391
399
|
return findings;
|
|
392
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Every `ctx.tools.register({ name })` of the non-test sources under `pluginDirs`, found by the
|
|
403
|
+
* same scanner as {@link checkPluginPermissions} (so it follows ctx aliases and same-file helpers).
|
|
404
|
+
* Unparseable files are skipped here; the permissions check reports them.
|
|
405
|
+
*/
|
|
406
|
+
export function findToolRegistrations(pluginDirs) {
|
|
407
|
+
const out = [];
|
|
408
|
+
for (const dir of pluginDirs) {
|
|
409
|
+
for (const { file, isTest } of listSourceFiles(dir)) {
|
|
410
|
+
if (isTest)
|
|
411
|
+
continue;
|
|
412
|
+
const parsed = parseFile(file);
|
|
413
|
+
if (isParseError(parsed))
|
|
414
|
+
continue;
|
|
415
|
+
scanFile(parsed, (r) => out.push(r));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
419
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Finding } from "./source.js";
|
|
2
|
+
export type { Finding };
|
|
3
|
+
/** The model-visible key of an extension tool is `plugin__<extension>__<tool>`; 64 characters at most. */
|
|
4
|
+
export declare const TOOL_KEY_PREFIX = "plugin__";
|
|
5
|
+
export declare const TOOL_KEY_MAX = 64;
|
|
6
|
+
/**
|
|
7
|
+
* plugin-tool-key-length: the key `plugin__<extension>__<tool>` of every `ctx.tools.register`
|
|
8
|
+
* must fit in {@link TOOL_KEY_MAX} characters (model APIs cap tool names at 64). The names must be
|
|
9
|
+
* string literals or same-file consts, as for `perm-tools`; a name that is not is reported as
|
|
10
|
+
* "cannot be confirmed statically" unless the call carries `// capybara:allow-dynamic-permission
|
|
11
|
+
* <reason>`. Scans the non-test sources under `pluginDirs`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkPluginToolKeys(pluginDirs: string[]): Finding[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { findToolRegistrations } from "./plugin-permissions.js";
|
|
2
|
+
/** The model-visible key of an extension tool is `plugin__<extension>__<tool>`; 64 characters at most. */
|
|
3
|
+
export const TOOL_KEY_PREFIX = "plugin__";
|
|
4
|
+
export const TOOL_KEY_MAX = 64;
|
|
5
|
+
/**
|
|
6
|
+
* plugin-tool-key-length: the key `plugin__<extension>__<tool>` of every `ctx.tools.register`
|
|
7
|
+
* must fit in {@link TOOL_KEY_MAX} characters (model APIs cap tool names at 64). The names must be
|
|
8
|
+
* string literals or same-file consts, as for `perm-tools`; a name that is not is reported as
|
|
9
|
+
* "cannot be confirmed statically" unless the call carries `// capybara:allow-dynamic-permission
|
|
10
|
+
* <reason>`. Scans the non-test sources under `pluginDirs`.
|
|
11
|
+
*/
|
|
12
|
+
export function checkPluginToolKeys(pluginDirs) {
|
|
13
|
+
const rule = "plugin-tool-key-length";
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const r of findToolRegistrations(pluginDirs)) {
|
|
16
|
+
if (r.extension === undefined || r.tool === undefined) {
|
|
17
|
+
if (r.allowed)
|
|
18
|
+
continue;
|
|
19
|
+
out.push({
|
|
20
|
+
file: r.file,
|
|
21
|
+
line: r.line,
|
|
22
|
+
rule,
|
|
23
|
+
message: `Cannot statically confirm the length of the tool key (${r.extension === undefined ? "extension name" : "tool name"} is not a literal). Use a string literal (or a same-file const), or add // capybara:allow-dynamic-permission <reason>`,
|
|
24
|
+
});
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const key = `${TOOL_KEY_PREFIX}${r.extension}__${r.tool}`;
|
|
28
|
+
if (key.length > TOOL_KEY_MAX) {
|
|
29
|
+
out.push({
|
|
30
|
+
file: r.file,
|
|
31
|
+
line: r.line,
|
|
32
|
+
rule,
|
|
33
|
+
message: `Tool key "${key}" is ${key.length} characters; the limit is ${TOOL_KEY_MAX} (plugin__<extension>__<tool>). Shorten the tool name to at most ${Math.max(0, TOOL_KEY_MAX - TOOL_KEY_PREFIX.length - r.extension.length - 2)} characters or the extension name`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
package/dist/check/run.js
CHANGED
|
@@ -4,6 +4,7 @@ import { checkMigrations } from "./migrations.js";
|
|
|
4
4
|
import { checkPluginPack } from "./plugin-pack.js";
|
|
5
5
|
import { checkPluginPermissions } from "./plugin-permissions.js";
|
|
6
6
|
import { checkPluginState } from "./plugin-state.js";
|
|
7
|
+
import { checkPluginToolKeys } from "./plugin-tool-keys.js";
|
|
7
8
|
import { checkPluginReleaseMetadata } from "./release-metadata.js";
|
|
8
9
|
import { printCheck } from "./report.js";
|
|
9
10
|
import { checkPluginTestImports } from "./test-imports.js";
|
|
@@ -30,6 +31,7 @@ export function pluginChecks(scope) {
|
|
|
30
31
|
name: "Tool design",
|
|
31
32
|
run: () => checkToolDesign([...(scope.toolDesignDirs ?? []), ...plugins]),
|
|
32
33
|
},
|
|
34
|
+
{ name: "Tool key length", run: () => checkPluginToolKeys(plugins) },
|
|
33
35
|
{ name: "Plugin test imports", run: () => checkPluginTestImports(plugins) },
|
|
34
36
|
{
|
|
35
37
|
name: "Plugin release metadata",
|
|
@@ -5,7 +5,7 @@ export type { Finding };
|
|
|
5
5
|
* - tool-description:`ctx.tools.register(spec, …)` 与 `BuiltinTool` 字面量(有 spec 与 run)里的 spec
|
|
6
6
|
* 必须有字符串字面量 description(可 `+` 拼接)、不少于 20 字符、不含中文(模型可见文案一律英文);
|
|
7
7
|
* - tool-error-actionable:`new ToolError(msg)` 的 msg 是字符串字面量 / 模板(或本文件内的常量、函数返回值),
|
|
8
|
-
* 且含建议动词 use / call / retry / check / ask / pass / provide 之一,让模型知道下一步做什么。
|
|
8
|
+
* 且含建议动词 use / call / retry / check / ask / pass / provide / read 之一,让模型知道下一步做什么。
|
|
9
9
|
* spec 可以直接写对象字面量,也可以引用本文件内的 const;其他写法(跨文件、动态构造)报 tool-description。
|
|
10
10
|
* 已知缺口:从别的文件导入的消息常量 / 函数不追踪;非 ToolError 类的错误不查;运行时拼出的 spec 不查。
|
|
11
11
|
* 扫描:传入目录下的非测试源码。
|
|
@@ -2,7 +2,7 @@ import { isParseError, listSourceFiles, parseFile, stringValue, walk, } from "./
|
|
|
2
2
|
const MIN_DESCRIPTION = 20;
|
|
3
3
|
const CJK = /[㐀-鿿豈- -〿-]/;
|
|
4
4
|
/** 可执行的建议动词:错误信息里至少出现其一(整词、不分大小写)。 */
|
|
5
|
-
const ACTION_VERB = /\b(?:use|call|retry|check|ask|pass|provide)\b/i;
|
|
5
|
+
const ACTION_VERB = /\b(?:use|call|retry|check|ask|pass|provide|read)\b/i;
|
|
6
6
|
const WRAPPERS = new Set([
|
|
7
7
|
"ParenthesizedExpression",
|
|
8
8
|
"TSAsExpression",
|
|
@@ -129,7 +129,7 @@ const MSG_DESC_MISSING = "工具缺少 description,或它不是字符串字面
|
|
|
129
129
|
const MSG_DESC_SHORT = `工具 description 至少 ${MIN_DESCRIPTION} 个字符:说清工具做什么、何时该用、何时不该用`;
|
|
130
130
|
const MSG_DESC_CJK = "工具 description 是模型可见文案,必须是英文,不得含中文字符";
|
|
131
131
|
const MSG_ERR_DYNAMIC = "ToolError 的消息必须是字符串字面量或模板(或指向本文件内的此类常量 / 函数),否则无法检查是否可执行";
|
|
132
|
-
const MSG_ERR_ACTION = "ToolError 的消息要让模型知道下一步怎么做:必须含建议动词之一(use / call / retry / check / ask / pass / provide)";
|
|
132
|
+
const MSG_ERR_ACTION = "ToolError 的消息要让模型知道下一步怎么做:必须含建议动词之一(use / call / retry / check / ask / pass / provide / read)";
|
|
133
133
|
/** 取一个 spec 表达式对应的对象字面量(直接写,或经本文件内的常量引用)。 */
|
|
134
134
|
function specObject(n, env) {
|
|
135
135
|
let e = unwrap(n);
|
|
@@ -204,7 +204,7 @@ function scanFile(f) {
|
|
|
204
204
|
* - tool-description:`ctx.tools.register(spec, …)` 与 `BuiltinTool` 字面量(有 spec 与 run)里的 spec
|
|
205
205
|
* 必须有字符串字面量 description(可 `+` 拼接)、不少于 20 字符、不含中文(模型可见文案一律英文);
|
|
206
206
|
* - tool-error-actionable:`new ToolError(msg)` 的 msg 是字符串字面量 / 模板(或本文件内的常量、函数返回值),
|
|
207
|
-
* 且含建议动词 use / call / retry / check / ask / pass / provide 之一,让模型知道下一步做什么。
|
|
207
|
+
* 且含建议动词 use / call / retry / check / ask / pass / provide / read 之一,让模型知道下一步做什么。
|
|
208
208
|
* spec 可以直接写对象字面量,也可以引用本文件内的 const;其他写法(跨文件、动态构造)报 tool-description。
|
|
209
209
|
* 已知缺口:从别的文件导入的消息常量 / 函数不追踪;非 ToolError 类的错误不查;运行时拼出的 spec 不查。
|
|
210
210
|
* 扫描:传入目录下的非测试源码。
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* `capybara-plugin`: pack, check, sync the versions of and release Capybara plugins.
|
|
3
3
|
*
|
|
4
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
5
|
* capybara-plugin check [--root <dir>] [--plugins <glob>] [--base <ref>]
|
|
8
6
|
* capybara-plugin sync-versions [--root <dir>] [--plugins <glob>]
|
|
9
7
|
* capybara-plugin marketplace --write|--check [--base <rev>] [--root <dir>] [--file <path>]
|
package/dist/pack/cli.d.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
export declare const PACK_USAGE
|
|
1
|
+
export declare const PACK_USAGE = "capybara-plugin pack [<dir>...] [--all <root>] [--out <dir>]";
|
|
2
2
|
/**
|
|
3
3
|
* `capybara-plugin pack`: packs plugin directories into plugin tgz files.
|
|
4
4
|
*
|
|
5
5
|
* - `<dir>...`: the plugin directories to pack;
|
|
6
6
|
* - `--all <root>`: also every plugin of the repository at `<root>` (`<root>/plugins/*`, or `<root>`
|
|
7
7
|
* itself when it is a plugin);
|
|
8
|
-
* - `--
|
|
9
|
-
*
|
|
10
|
-
* relative to `--root`, default the current directory; npm entries are downloaded from
|
|
11
|
-
* `--registry`, default `CAPYBARA_PACKAGES__NPM_REGISTRY` (the kernel's `packages.npmRegistry`)
|
|
12
|
-
* or else the public npm registry, and must match their pinned sha512; `--npm-token`, default
|
|
13
|
-
* `CAPYBARA_PACKAGES__NPM_TOKEN` (the kernel's `packages.npmToken`), is sent as a Bearer token to
|
|
14
|
-
* the registry origin only);
|
|
15
|
-
* - `--out <dir>`: output directory, default `artifacts/plugins` (`artifacts/bootstrap` with
|
|
16
|
-
* `--bootstrap`) under the current directory. The last `--out` wins.
|
|
8
|
+
* - `--out <dir>`: output directory, default `artifacts/plugins` under the current directory. The
|
|
9
|
+
* last `--out` wins.
|
|
17
10
|
*
|
|
18
11
|
* Relative paths resolve against `cwd` (the current directory). Returns the exit code.
|
|
19
12
|
*/
|
package/dist/pack/cli.js
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { discoverPlugins } from "../discover.js";
|
|
4
|
-
import { packBootstrap } from "./bootstrap.js";
|
|
5
|
-
import { DEFAULT_NPM_REGISTRY } from "./npm.js";
|
|
6
4
|
import { packPlugin } from "./pack.js";
|
|
7
|
-
export const PACK_USAGE = "capybara-plugin pack [<dir>...] [--all <root>] [--out <dir>]
|
|
8
|
-
" capybara-plugin pack --bootstrap <manifest> [--root <dir>] [--out <dir>] [--registry <url>]\n" +
|
|
9
|
-
" [--npm-token <token>]";
|
|
5
|
+
export const PACK_USAGE = "capybara-plugin pack [<dir>...] [--all <root>] [--out <dir>]";
|
|
10
6
|
/**
|
|
11
7
|
* `capybara-plugin pack`: packs plugin directories into plugin tgz files.
|
|
12
8
|
*
|
|
13
9
|
* - `<dir>...`: the plugin directories to pack;
|
|
14
10
|
* - `--all <root>`: also every plugin of the repository at `<root>` (`<root>/plugins/*`, or `<root>`
|
|
15
11
|
* itself when it is a plugin);
|
|
16
|
-
* - `--
|
|
17
|
-
*
|
|
18
|
-
* relative to `--root`, default the current directory; npm entries are downloaded from
|
|
19
|
-
* `--registry`, default `CAPYBARA_PACKAGES__NPM_REGISTRY` (the kernel's `packages.npmRegistry`)
|
|
20
|
-
* or else the public npm registry, and must match their pinned sha512; `--npm-token`, default
|
|
21
|
-
* `CAPYBARA_PACKAGES__NPM_TOKEN` (the kernel's `packages.npmToken`), is sent as a Bearer token to
|
|
22
|
-
* the registry origin only);
|
|
23
|
-
* - `--out <dir>`: output directory, default `artifacts/plugins` (`artifacts/bootstrap` with
|
|
24
|
-
* `--bootstrap`) under the current directory. The last `--out` wins.
|
|
12
|
+
* - `--out <dir>`: output directory, default `artifacts/plugins` under the current directory. The
|
|
13
|
+
* last `--out` wins.
|
|
25
14
|
*
|
|
26
15
|
* Relative paths resolve against `cwd` (the current directory). Returns the exit code.
|
|
27
16
|
*/
|
|
@@ -33,10 +22,6 @@ export async function packCommand(args, cwd) {
|
|
|
33
22
|
help: { type: "boolean", short: "h", default: false },
|
|
34
23
|
all: { type: "string" },
|
|
35
24
|
out: { type: "string" },
|
|
36
|
-
bootstrap: { type: "string" },
|
|
37
|
-
root: { type: "string" },
|
|
38
|
-
registry: { type: "string" },
|
|
39
|
-
"npm-token": { type: "string" },
|
|
40
25
|
},
|
|
41
26
|
});
|
|
42
27
|
if (values.help) {
|
|
@@ -48,28 +33,6 @@ export async function packCommand(args, cwd) {
|
|
|
48
33
|
const rel = relative(cwd, p);
|
|
49
34
|
return rel === "" ? "." : rel.startsWith("..") ? p : rel;
|
|
50
35
|
};
|
|
51
|
-
if (values.bootstrap !== undefined) {
|
|
52
|
-
if (values.all !== undefined || positionals.length > 0) {
|
|
53
|
-
return usage("--bootstrap takes no plugin directory");
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
const root = values.root === undefined ? cwd : resolve(cwd, values.root);
|
|
57
|
-
const manifest = resolve(cwd, values.bootstrap);
|
|
58
|
-
const registry = values.registry ?? (process.env.CAPYBARA_PACKAGES__NPM_REGISTRY || DEFAULT_NPM_REGISTRY);
|
|
59
|
-
const token = values["npm-token"] ?? process.env.CAPYBARA_PACKAGES__NPM_TOKEN;
|
|
60
|
-
for (const p of await packBootstrap(manifest, root, outOf("artifacts/bootstrap"), {
|
|
61
|
-
registry,
|
|
62
|
-
...(token !== undefined && token !== "" && { token }),
|
|
63
|
-
})) {
|
|
64
|
-
console.log(`${shown(p.file)} ${p.integrity}`);
|
|
65
|
-
}
|
|
66
|
-
return 0;
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
console.error(e instanceof Error ? e.message : String(e));
|
|
70
|
-
return 1;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
36
|
const plugins = positionals.map((d) => resolve(cwd, d));
|
|
74
37
|
if (values.all !== undefined) {
|
|
75
38
|
const root = resolve(cwd, values.all);
|
package/dist/pack/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { type BootstrapPack, packBootstrap } from "./bootstrap.js";
|
|
2
1
|
export { PACK_USAGE, packCommand } from "./cli.js";
|
|
3
2
|
export { assertNpmTarballLabel, DEFAULT_NPM_REGISTRY, downloadPinnedNpmPackage, fetchNpmTarball, NPM_TARBALL_MAX_BYTES, type NpmFetchOptions, NpmPackageError, type NpmPackageErrorKind, } from "./npm.js";
|
|
4
3
|
export { METADATA_FIELDS, NOTICES_FILE, type PackResult, packPlugin, STAGING_PREFIX, } from "./pack.js";
|
package/dist/pack/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { packBootstrap } from "./bootstrap.js";
|
|
2
1
|
export { PACK_USAGE, packCommand } from "./cli.js";
|
|
3
2
|
export { assertNpmTarballLabel, DEFAULT_NPM_REGISTRY, downloadPinnedNpmPackage, fetchNpmTarball, NPM_TARBALL_MAX_BYTES, NpmPackageError, } from "./npm.js";
|
|
4
3
|
export { METADATA_FIELDS, NOTICES_FILE, packPlugin, STAGING_PREFIX, } from "./pack.js";
|
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.3",
|
|
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.3",
|
|
55
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.1"
|
|
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.3"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc -b"
|
package/dist/pack/bootstrap.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type NpmFetchOptions } from "./npm.js";
|
|
2
|
-
/** One package {@link packBootstrap} wrote. */
|
|
3
|
-
export interface BootstrapPack {
|
|
4
|
-
name: string;
|
|
5
|
-
version: string;
|
|
6
|
-
file: string;
|
|
7
|
-
integrity: string;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Produces the bootstrap directory the image bakes (package ecosystem spec §7): every entry of the
|
|
11
|
-
* bootstrap manifest (`packages/kernel/bootstrap.json`) as `<outDir>/<name>-<version>.tgz`. A
|
|
12
|
-
* `source: "workspace"` entry is packed from `<repoRoot>/<path>` with {@link packPlugin}
|
|
13
|
-
* (byte-reproducible) and must equal the entry: the plugin name, the version and the pinned sha512,
|
|
14
|
-
* otherwise this throws naming the plugin and writes nothing for it (the image build fails rather
|
|
15
|
-
* than baking a package `capybara bootstrap` would refuse). A `source: "npm"` entry is downloaded
|
|
16
|
-
* from `opts.registry` (default the public npm registry, the kernel's `packages.npmRegistry`
|
|
17
|
-
* default; `opts.token` goes to the registry origin only) and written only when its bytes match
|
|
18
|
-
* the pinned sha512 and its package.json names the entry's package and version; a tampered,
|
|
19
|
-
* mislabelled or oversized (`bootstrap.too_large`) tarball or an unpublished package or version
|
|
20
|
-
* throws naming the plugin. So npm entries need the network at
|
|
21
|
-
* build time only: init installs from the baked directory and never downloads.
|
|
22
|
-
*/
|
|
23
|
-
export declare function packBootstrap(manifestFile: string, repoRoot: string, outDir: string, opts?: {
|
|
24
|
-
registry?: string;
|
|
25
|
-
} & NpmFetchOptions): Promise<BootstrapPack[]>;
|
package/dist/pack/bootstrap.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { bootstrapFileName, parseBootstrapManifest } from "@capybara-hq/plugin-sdk";
|
|
5
|
-
import { integrityOf } from "../integrity.js";
|
|
6
|
-
import { DEFAULT_NPM_REGISTRY, downloadPinnedNpmPackage } from "./npm.js";
|
|
7
|
-
import { packPlugin } from "./pack.js";
|
|
8
|
-
/**
|
|
9
|
-
* Produces the bootstrap directory the image bakes (package ecosystem spec §7): every entry of the
|
|
10
|
-
* bootstrap manifest (`packages/kernel/bootstrap.json`) as `<outDir>/<name>-<version>.tgz`. A
|
|
11
|
-
* `source: "workspace"` entry is packed from `<repoRoot>/<path>` with {@link packPlugin}
|
|
12
|
-
* (byte-reproducible) and must equal the entry: the plugin name, the version and the pinned sha512,
|
|
13
|
-
* otherwise this throws naming the plugin and writes nothing for it (the image build fails rather
|
|
14
|
-
* than baking a package `capybara bootstrap` would refuse). A `source: "npm"` entry is downloaded
|
|
15
|
-
* from `opts.registry` (default the public npm registry, the kernel's `packages.npmRegistry`
|
|
16
|
-
* default; `opts.token` goes to the registry origin only) and written only when its bytes match
|
|
17
|
-
* the pinned sha512 and its package.json names the entry's package and version; a tampered,
|
|
18
|
-
* mislabelled or oversized (`bootstrap.too_large`) tarball or an unpublished package or version
|
|
19
|
-
* throws naming the plugin. So npm entries need the network at
|
|
20
|
-
* build time only: init installs from the baked directory and never downloads.
|
|
21
|
-
*/
|
|
22
|
-
export async function packBootstrap(manifestFile, repoRoot, outDir, opts = {}) {
|
|
23
|
-
const parsed = parseBootstrapManifest(JSON.parse(await readFile(manifestFile, "utf8")));
|
|
24
|
-
if ("errors" in parsed) {
|
|
25
|
-
throw new Error(`invalid bootstrap manifest ${manifestFile}: ${parsed.errors.join("; ")}`);
|
|
26
|
-
}
|
|
27
|
-
await mkdir(outDir, { recursive: true });
|
|
28
|
-
const staging = await mkdtemp(join(tmpdir(), "pack-bootstrap-"));
|
|
29
|
-
const out = [];
|
|
30
|
-
try {
|
|
31
|
-
for (const e of parsed.plugins) {
|
|
32
|
-
if (e.source === "npm") {
|
|
33
|
-
let bytes;
|
|
34
|
-
try {
|
|
35
|
-
bytes = await downloadPinnedNpmPackage(e, opts.registry ?? DEFAULT_NPM_REGISTRY, {
|
|
36
|
-
...(opts.token !== undefined && { token: opts.token }),
|
|
37
|
-
...(opts.maxBytes !== undefined && { maxBytes: opts.maxBytes }),
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
throw new Error(`${e.name}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
42
|
-
cause: err,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
const file = join(outDir, bootstrapFileName(e));
|
|
46
|
-
await writeFile(file, bytes);
|
|
47
|
-
out.push({ name: e.name, version: e.version, file, integrity: e.integrity });
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const r = await packPlugin(join(repoRoot, e.path), staging);
|
|
51
|
-
const integrity = integrityOf(new Uint8Array(await readFile(r.file)));
|
|
52
|
-
const problems = [
|
|
53
|
-
r.manifest.name !== e.name && `${e.path} packs the plugin "${r.manifest.name}"`,
|
|
54
|
-
r.manifest.version !== e.version &&
|
|
55
|
-
`version is ${e.version} but ${e.path} packs ${r.manifest.version}`,
|
|
56
|
-
integrity !== e.integrity && `integrity is ${e.integrity} but ${e.path} packs ${integrity}`,
|
|
57
|
-
].filter((p) => typeof p === "string");
|
|
58
|
-
if (problems.length > 0) {
|
|
59
|
-
throw new Error(`${e.name}: ${problems.join("; ")}; the bootstrap manifest is stale (pnpm check:bootstrap --write)`);
|
|
60
|
-
}
|
|
61
|
-
const file = join(outDir, bootstrapFileName(e));
|
|
62
|
-
await copyFile(r.file, file);
|
|
63
|
-
out.push({ name: e.name, version: e.version, file, integrity });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
await rm(staging, { recursive: true, force: true });
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|