@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 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; `--bootstrap` packs the entries of
34
- a bootstrap manifest (workspace entries from the repository, npm entries downloaded from
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`, `packBootstrap`, `pluginChecks` / `runChecks` and each
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`),
@@ -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, pack-bootstrap, release) run the
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
 
@@ -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";
@@ -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>]
@@ -1,19 +1,12 @@
1
- export declare const PACK_USAGE: string;
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
- * - `--bootstrap <manifest>`: the bootstrap directory an image bakes: every entry of the bootstrap
9
- * manifest as `<name>-<version>.tgz`, checked against its pins (workspace entries' `path` is
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>]\n" +
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
- * - `--bootstrap <manifest>`: the bootstrap directory an image bakes: every entry of the bootstrap
17
- * manifest as `<name>-<version>.tgz`, checked against its pins (workspace entries' `path` is
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);
@@ -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";
@@ -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.1",
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/plugin-sdk": "^0.1.0-rc.0",
55
- "@capybara-hq/kernel": "^0.1.0-rc.0"
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.1"
59
+ "@capybara-hq/testing": "0.1.0-rc.3"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc -b"
@@ -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[]>;
@@ -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
- }