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

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 +105 -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,245 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, relative } from "node:path";
6
+ import { isSensitivePermissionKey, PACKAGES_MAX_BYTES_DEFAULT, permissionKeys, permissionSetOfPackage, readExtensionPackage, WARNINGS_KEY_PREFIX, } from "@capybara-hq/kernel";
7
+ import { MARKETPLACE_LIMITS, PLUGIN_MANIFEST_FILE, parseMarketplaceManifest, SDK_VERSION, } from "@capybara-hq/plugin-sdk";
8
+ import { discoverPlugins } from "../discover.js";
9
+ import { integrityOf } from "../integrity.js";
10
+ import { packPlugin } from "../pack/pack.js";
11
+ export { MARKETPLACE_FILE } from "./file.js";
12
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
13
+ /** The marketplace summary of a plugin: its plugin.json description, cut to the summary limit. */
14
+ function summaryOf(description) {
15
+ const max = MARKETPLACE_LIMITS.summaryChars;
16
+ return description.length <= max ? description : `${description.slice(0, max - 1)}…`;
17
+ }
18
+ /** The permission summary of a packed plugin, as the kernel computes it at import (display only). */
19
+ async function permissionsOf(bytes, hasExtension) {
20
+ if (!hasExtension)
21
+ return { keys: [], sensitive: [] };
22
+ const ext = await readExtensionPackage(bytes, {
23
+ maxBytes: PACKAGES_MAX_BYTES_DEFAULT,
24
+ hostSdkVersion: SDK_VERSION,
25
+ });
26
+ const keys = permissionKeys(permissionSetOfPackage(ext))
27
+ .filter((k) => !k.startsWith(WARNINGS_KEY_PREFIX))
28
+ .sort();
29
+ return { keys, sensitive: keys.filter(isSensitivePermissionKey) };
30
+ }
31
+ /** An entry's version: `version`, else its npm source's. */
32
+ const versionOf = (e) => e.version ?? ("npm" in e.source ? e.source.npm.version : undefined);
33
+ /**
34
+ * `capybara-plugin marketplace`: keeps a plugin repository's `marketplace.json` listing every
35
+ * published version of its public plugins (package ecosystem spec §5.2; one entry per
36
+ * `(name, version)`, Decision 7 of phase A3b). Each public plugin (not `"private": true`) is packed
37
+ * reproducibly with {@link packPlugin}; when the file has no entry for its name and version, an
38
+ * entry `{ name, version, summary, homepage?, permissions, source: { npm: { package, version,
39
+ * integrity } } }` is added (`integrity` the pack's sha512, which the release then publishes
40
+ * byte for byte; `permissions` display only). A version already listed is not packed or compared:
41
+ * once published, its bytes are the release gate's business (`release --marketplace`), so a later
42
+ * source or toolchain change at the same version is not a finding. With `base`, published entries
43
+ * must be unchanged and first in order, and entries added since must match the tree's packs.
44
+ * Entries of versions no longer in the tree are kept, so a pinned commit lists what it pins and
45
+ * older versions stay importable. The file is written formatted as Biome formats it
46
+ * ({@link formatMarketplaceJson}).
47
+ * With `write`, the new entries are appended in plugin order (nothing is written when there is any
48
+ * other finding).
49
+ */
50
+ export async function syncMarketplace(opts) {
51
+ const findings = [];
52
+ const at = (rule, message) => findings.push({ file: opts.file, line: 1, rule, message });
53
+ if (!existsSync(opts.file)) {
54
+ at("marketplace-invalid", `${opts.file} does not exist: create it with schemaVersion, name, owner and "plugins": []`);
55
+ return { added: [], findings };
56
+ }
57
+ let raw;
58
+ try {
59
+ raw = JSON.parse(readFileSync(opts.file, "utf8"));
60
+ }
61
+ catch (e) {
62
+ at("marketplace-invalid", `not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
63
+ return { added: [], findings };
64
+ }
65
+ const parsed = parseMarketplaceManifest(raw);
66
+ if ("errors" in parsed) {
67
+ at("marketplace-invalid", parsed.errors.join("; "));
68
+ return { added: [], findings };
69
+ }
70
+ const rawEntries = isObj(raw) && Array.isArray(raw.plugins) ? raw.plugins : [];
71
+ /** `name@version` of the entries added since `opts.base` (all of them without a base). */
72
+ let addedSinceBase;
73
+ if (opts.base !== undefined) {
74
+ const baseEntries = entriesAt(opts.root, opts.file, opts.base);
75
+ findings.push(...appendOnly(baseEntries, rawEntries, opts.file));
76
+ addedSinceBase = new Set(rawEntries.slice(baseEntries.length).map((e) => keyOfRaw(e)));
77
+ }
78
+ const added = [];
79
+ const out = await mkdtemp(join(tmpdir(), "capybara-marketplace-"));
80
+ try {
81
+ for (const dir of discoverPlugins(opts.root, opts.plugins)) {
82
+ const pkgFile = join(dir, "package.json");
83
+ const manifestFile = join(dir, PLUGIN_MANIFEST_FILE);
84
+ if (!existsSync(pkgFile) || !existsSync(manifestFile))
85
+ continue;
86
+ const pkg = JSON.parse(await readFile(pkgFile, "utf8"));
87
+ if (pkg.private === true)
88
+ continue;
89
+ const source = JSON.parse(await readFile(manifestFile, "utf8"));
90
+ const name = String(source.name);
91
+ const version = String(pkg.version);
92
+ const listed = parsed.plugins.filter((e) => e.name === name && versionOf(e) === version);
93
+ // A version already listed is published (or about to be): its bytes are the release gate's
94
+ // business, so a later source or toolchain change at the same version is not a finding here.
95
+ // Only an entry added since the base is compared with the tree's pack.
96
+ const toCompare = listed.filter((e) => addedSinceBase?.has(`${e.name}@${versionOf(e)}`));
97
+ if (listed.length > 0 && toCompare.length === 0)
98
+ continue;
99
+ const r = await packPlugin(dir, out);
100
+ const bytes = new Uint8Array(await readFile(r.file));
101
+ const npm = {
102
+ package: String(pkg.name),
103
+ version: r.manifest.version,
104
+ integrity: integrityOf(bytes),
105
+ };
106
+ for (const e of toCompare) {
107
+ const src = "npm" in e.source ? e.source.npm : undefined;
108
+ if (src?.package !== npm.package || src.integrity !== npm.integrity) {
109
+ const was = src === undefined
110
+ ? `a ${Object.keys(e.source)[0]} source`
111
+ : `npm ${src.package} with integrity ${src.integrity}`;
112
+ at("marketplace-integrity", `${name}@${npm.version} is added as ${was}, but ${dir} packs npm ${npm.package} ` +
113
+ `with integrity ${npm.integrity}: run capybara-plugin marketplace --write`);
114
+ }
115
+ }
116
+ if (listed.length > 0)
117
+ continue;
118
+ added.push({
119
+ name: r.manifest.name,
120
+ version: npm.version,
121
+ summary: { en: summaryOf(r.manifest.description) },
122
+ ...(r.manifest.homepage !== undefined && { homepage: r.manifest.homepage }),
123
+ permissions: await permissionsOf(bytes, r.manifest.extension !== undefined),
124
+ source: { npm },
125
+ });
126
+ }
127
+ }
128
+ finally {
129
+ await rm(out, { recursive: true, force: true });
130
+ }
131
+ if (!opts.write) {
132
+ for (const e of added) {
133
+ at("marketplace-missing", `${e.name}@${e.version} is not listed: run capybara-plugin marketplace --write`);
134
+ }
135
+ return { added, findings };
136
+ }
137
+ if (findings.length > 0 || added.length === 0)
138
+ return { added, findings };
139
+ const next = isObj(raw) ? { ...raw } : {};
140
+ next.plugins = [...(Array.isArray(next.plugins) ? next.plugins : []), ...added];
141
+ const check = parseMarketplaceManifest(next);
142
+ if ("errors" in check) {
143
+ at("marketplace-invalid", `the new entries do not parse: ${check.errors.join("; ")}`);
144
+ return { added, findings };
145
+ }
146
+ await writeFile(opts.file, formatMarketplaceJson(next));
147
+ return { added, findings };
148
+ }
149
+ /** `name@version` of a raw entry (`?` for what is missing). */
150
+ function keyOfRaw(e) {
151
+ if (!isObj(e))
152
+ return "?@?";
153
+ const npm = isObj(e.source) && isObj(e.source.npm) ? e.source.npm : {};
154
+ return `${String(e.name ?? "?")}@${String(e.version ?? npm.version ?? "?")}`;
155
+ }
156
+ /** JSON with object keys sorted (deep equality of entries regardless of key order). */
157
+ function canonical(v) {
158
+ if (Array.isArray(v))
159
+ return `[${v.map(canonical).join(",")}]`;
160
+ if (isObj(v)) {
161
+ return `{${Object.keys(v)
162
+ .sort()
163
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
164
+ .join(",")}}`;
165
+ }
166
+ return JSON.stringify(v);
167
+ }
168
+ /** The raw `plugins` of the marketplace file at git revision `base` (`[]` when it has no file). */
169
+ function entriesAt(root, file, base) {
170
+ let text;
171
+ try {
172
+ text = execFileSync("git", ["show", `${base}:./${relative(root, file).split("\\").join("/")}`], {
173
+ cwd: root,
174
+ encoding: "utf8",
175
+ stdio: ["ignore", "pipe", "pipe"],
176
+ });
177
+ }
178
+ catch (e) {
179
+ const stderr = String(e.stderr ?? "");
180
+ if (/does not exist|exists on disk, but not in/.test(stderr))
181
+ return [];
182
+ throw new Error(`cannot read ${file} at ${base}: ${stderr.trim() || String(e)}`);
183
+ }
184
+ const raw = JSON.parse(text);
185
+ return isObj(raw) && Array.isArray(raw.plugins) ? raw.plugins : [];
186
+ }
187
+ /**
188
+ * `marketplace-append-only`: the base's entries are published, so the current list must start with
189
+ * exactly them, in the same order (an entry is never edited, removed or moved; new ones go last).
190
+ */
191
+ function appendOnly(base, current, file) {
192
+ const out = [];
193
+ base.forEach((b, i) => {
194
+ const c = current[i];
195
+ if (c !== undefined && canonical(c) === canonical(b))
196
+ return;
197
+ const key = keyOfRaw(b);
198
+ const moved = current.some((x) => canonical(x) === canonical(b));
199
+ out.push({
200
+ file,
201
+ line: 1,
202
+ rule: "marketplace-append-only",
203
+ message: `${key} is published at the base and ${moved ? "moved" : c !== undefined && keyOfRaw(c) === key ? "was edited" : "went away"}: published entries never change and new ones are appended at the end`,
204
+ });
205
+ });
206
+ return out;
207
+ }
208
+ const INDENT = " ";
209
+ /** The formatter's line width (Biome, as the Capybara repositories configure it). */
210
+ const LINE_WIDTH = 100;
211
+ const isScalar = (v) => v === null || typeof v !== "object";
212
+ /**
213
+ * `marketplace.json` as Biome formats it with a 2-space indent and a 100-column line width: a
214
+ * non-empty object puts each member on its own line; an array of scalars stays on one line when
215
+ * it fits (the trailing comma counted), otherwise one item per line; empty ones are `[]` / `{}`.
216
+ */
217
+ export function formatMarketplaceJson(value) {
218
+ const render = (v, depth, lead, tail) => {
219
+ const pad = INDENT.repeat(depth + 1);
220
+ const close = INDENT.repeat(depth);
221
+ if (Array.isArray(v)) {
222
+ if (v.length === 0)
223
+ return "[]";
224
+ if (v.every(isScalar)) {
225
+ const flat = `[${v.map((x) => JSON.stringify(x)).join(", ")}]`;
226
+ if (lead + flat.length + tail <= LINE_WIDTH)
227
+ return flat;
228
+ }
229
+ const items = v.map((x, i) => `${pad}${render(x, depth + 1, pad.length, i < v.length - 1 ? 1 : 0)}`);
230
+ return `[\n${items.join(",\n")}\n${close}]`;
231
+ }
232
+ if (isObj(v)) {
233
+ const keys = Object.keys(v);
234
+ if (keys.length === 0)
235
+ return "{}";
236
+ const items = keys.map((k, i) => {
237
+ const head = `${pad}${JSON.stringify(k)}: `;
238
+ return `${head}${render(v[k], depth + 1, head.length, i < keys.length - 1 ? 1 : 0)}`;
239
+ });
240
+ return `{\n${items.join(",\n")}\n${close}}`;
241
+ }
242
+ return JSON.stringify(v);
243
+ };
244
+ return `${render(value, 0, 0, 0)}\n`;
245
+ }
@@ -0,0 +1,25 @@
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[]>;
@@ -0,0 +1,70 @@
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
+ }
@@ -0,0 +1,20 @@
1
+ export declare const PACK_USAGE: string;
2
+ /**
3
+ * `capybara-plugin pack`: packs plugin directories into plugin tgz files.
4
+ *
5
+ * - `<dir>...`: the plugin directories to pack;
6
+ * - `--all <root>`: also every plugin of the repository at `<root>` (`<root>/plugins/*`, or `<root>`
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.
17
+ *
18
+ * Relative paths resolve against `cwd` (the current directory). Returns the exit code.
19
+ */
20
+ export declare function packCommand(args: string[], cwd: string): Promise<number>;
@@ -0,0 +1,109 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { parseArgs } from "node:util";
3
+ import { discoverPlugins } from "../discover.js";
4
+ import { packBootstrap } from "./bootstrap.js";
5
+ import { DEFAULT_NPM_REGISTRY } from "./npm.js";
6
+ 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>]";
10
+ /**
11
+ * `capybara-plugin pack`: packs plugin directories into plugin tgz files.
12
+ *
13
+ * - `<dir>...`: the plugin directories to pack;
14
+ * - `--all <root>`: also every plugin of the repository at `<root>` (`<root>/plugins/*`, or `<root>`
15
+ * 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.
25
+ *
26
+ * Relative paths resolve against `cwd` (the current directory). Returns the exit code.
27
+ */
28
+ export async function packCommand(args, cwd) {
29
+ const { values, positionals } = parseArgs({
30
+ args,
31
+ allowPositionals: true,
32
+ options: {
33
+ help: { type: "boolean", short: "h", default: false },
34
+ all: { type: "string" },
35
+ out: { type: "string" },
36
+ bootstrap: { type: "string" },
37
+ root: { type: "string" },
38
+ registry: { type: "string" },
39
+ "npm-token": { type: "string" },
40
+ },
41
+ });
42
+ if (values.help) {
43
+ console.log(`usage: ${PACK_USAGE}`);
44
+ return 0;
45
+ }
46
+ const outOf = (fallback) => resolve(cwd, values.out ?? fallback);
47
+ const shown = (p) => {
48
+ const rel = relative(cwd, p);
49
+ return rel === "" ? "." : rel.startsWith("..") ? p : rel;
50
+ };
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
+ const plugins = positionals.map((d) => resolve(cwd, d));
74
+ if (values.all !== undefined) {
75
+ const root = resolve(cwd, values.all);
76
+ const found = discoverPlugins(root);
77
+ if (found.length === 0) {
78
+ console.error(`${shown(root)}: no plugin found (no plugins/* and no .capybara-plugin/)`);
79
+ return 1;
80
+ }
81
+ plugins.push(...found.filter((d) => !plugins.includes(d)));
82
+ }
83
+ if (plugins.length === 0)
84
+ return usage("no plugin directory given");
85
+ const out = outOf("artifacts/plugins");
86
+ let failed = 0;
87
+ for (const dir of plugins) {
88
+ try {
89
+ const r = await packPlugin(dir, out);
90
+ console.log(`${shown(r.file)} sha256=${r.sha256}`);
91
+ for (const d of r.dropped) {
92
+ console.warn(`${shown(dir)}: warning: "files" entry "${d}" is not in the package`);
93
+ }
94
+ }
95
+ catch (e) {
96
+ failed++;
97
+ console.error(`${shown(dir)}: ${e instanceof Error ? e.message : String(e)}`);
98
+ }
99
+ }
100
+ if (failed > 0) {
101
+ console.error(`\n${failed} plugin(s) failed to pack`);
102
+ return 1;
103
+ }
104
+ return 0;
105
+ }
106
+ function usage(msg) {
107
+ console.error(`${msg}\nusage: ${PACK_USAGE}`);
108
+ return 2;
109
+ }
@@ -0,0 +1,4 @@
1
+ export { type BootstrapPack, packBootstrap } from "./bootstrap.js";
2
+ export { PACK_USAGE, packCommand } from "./cli.js";
3
+ export { assertNpmTarballLabel, DEFAULT_NPM_REGISTRY, downloadPinnedNpmPackage, fetchNpmTarball, NPM_TARBALL_MAX_BYTES, type NpmFetchOptions, NpmPackageError, type NpmPackageErrorKind, } from "./npm.js";
4
+ export { METADATA_FIELDS, NOTICES_FILE, type PackResult, packPlugin, STAGING_PREFIX, } from "./pack.js";
@@ -0,0 +1,4 @@
1
+ export { packBootstrap } from "./bootstrap.js";
2
+ export { PACK_USAGE, packCommand } from "./cli.js";
3
+ export { assertNpmTarballLabel, DEFAULT_NPM_REGISTRY, downloadPinnedNpmPackage, fetchNpmTarball, NPM_TARBALL_MAX_BYTES, NpmPackageError, } from "./npm.js";
4
+ export { METADATA_FIELDS, NOTICES_FILE, packPlugin, STAGING_PREFIX, } from "./pack.js";
@@ -0,0 +1,66 @@
1
+ /** The npm registry the kernel uses by default (`packages.npmRegistry`). */
2
+ export declare const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
3
+ /**
4
+ * The largest tarball downloaded: the kernel's `packages.maxBytes` default
5
+ * (`PACKAGES_MAX_BYTES_DEFAULT`, 50 MiB; this subpath does not import the kernel, a test keeps the
6
+ * two equal). A bigger plugin could not be installed by a kernel with the default limit anyway.
7
+ */
8
+ export declare const NPM_TARBALL_MAX_BYTES = 52428800;
9
+ /** Why {@link fetchNpmTarball} or {@link downloadPinnedNpmPackage} failed. */
10
+ export type NpmPackageErrorKind =
11
+ /** The package or that version is not on the registry. */
12
+ "unpublished"
13
+ /** The registry's metadata or bytes do not match the pinned sha512. */
14
+ | "integrity"
15
+ /** Network error, HTTP status, or a packument that cannot be read. */
16
+ | "fetch"
17
+ /** The tarball is larger than the cap ({@link NPM_TARBALL_MAX_BYTES}): `bootstrap.too_large`. */
18
+ | "too_large"
19
+ /** The tarball's `package/package.json` names another package or version than was asked for. */
20
+ | "mislabelled";
21
+ /** Options of {@link fetchNpmTarball} and {@link downloadPinnedNpmPackage}. */
22
+ export interface NpmFetchOptions {
23
+ /**
24
+ * Bearer token for a private registry (`CAPYBARA_PACKAGES__NPM_TOKEN`, the kernel's
25
+ * `packages.npmToken`): sent only to the registry's origin, never to a tarball URL elsewhere.
26
+ */
27
+ token?: string;
28
+ /** Tarball size cap in bytes; default {@link NPM_TARBALL_MAX_BYTES}. */
29
+ maxBytes?: number;
30
+ }
31
+ /** A pinned npm package could not be fetched or does not match its pin. English message. */
32
+ export declare class NpmPackageError extends Error {
33
+ readonly kind: NpmPackageErrorKind;
34
+ constructor(kind: NpmPackageErrorKind, message: string);
35
+ }
36
+ /**
37
+ * `<pkg>@<version>` from an npm registry: its tarball bytes and the packument's `dist.integrity`
38
+ * (whatever the registry claims; callers compare it with their pin). The packument is asked for in
39
+ * npm's abbreviated form; `opts.token` goes to the registry origin only; the tarball is read up to
40
+ * `opts.maxBytes`. Throws {@link NpmPackageError}: `unpublished` (404 or no such version),
41
+ * `too_large`, `fetch` (anything else). The bytes are not judged here: see
42
+ * {@link downloadPinnedNpmPackage} and {@link assertNpmTarballLabel}.
43
+ */
44
+ export declare function fetchNpmTarball(pkg: string, version: string, registry?: string, opts?: NpmFetchOptions): Promise<{
45
+ bytes: Uint8Array;
46
+ distIntegrity: string | undefined;
47
+ tarball: string;
48
+ }>;
49
+ /**
50
+ * Downloads `<pkg>@<version>` and checks its bytes against the pinned sha512 `integrity`; returns the
51
+ * bytes only when they match (a tampered tarball, or a registry serving other bytes than the pin,
52
+ * throws {@link NpmPackageError} `integrity`), then checks its label
53
+ * ({@link assertNpmTarballLabel}). The registry's own `dist.integrity` plays no part: only the
54
+ * bytes are judged.
55
+ */
56
+ export declare function downloadPinnedNpmPackage(pin: {
57
+ npm: string;
58
+ version: string;
59
+ integrity: string;
60
+ }, registry?: string, opts?: NpmFetchOptions): Promise<Uint8Array>;
61
+ /**
62
+ * The tarball's `package/package.json` must name `pkg` at `version` (a pin taken from another
63
+ * package's tarball, or a registry serving one under the wrong name); throws
64
+ * {@link NpmPackageError} `mislabelled` otherwise (`fetch` when the bytes are no tgz).
65
+ */
66
+ export declare function assertNpmTarballLabel(bytes: Uint8Array, pkg: string, version: string): Promise<void>;