@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,177 @@
1
+ import { gunzipSync } from "node:zlib";
2
+ import { extract } from "tar-stream";
3
+ import { integrityOf } from "../integrity.js";
4
+ /** The npm registry the kernel uses by default (`packages.npmRegistry`). */
5
+ export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
6
+ /** Per request (packument or tarball). */
7
+ const NPM_TIMEOUT_MS = 120_000;
8
+ /**
9
+ * The largest tarball downloaded: the kernel's `packages.maxBytes` default
10
+ * (`PACKAGES_MAX_BYTES_DEFAULT`, 50 MiB; this subpath does not import the kernel, a test keeps the
11
+ * two equal). A bigger plugin could not be installed by a kernel with the default limit anyway.
12
+ */
13
+ export const NPM_TARBALL_MAX_BYTES = 52_428_800;
14
+ /**
15
+ * The packument media type npm itself asks for: the abbreviated metadata (versions with `dist`,
16
+ * no readmes), much smaller than the full document; registries that do not know it send JSON.
17
+ */
18
+ const PACKUMENT_ACCEPT = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
19
+ /** A pinned npm package could not be fetched or does not match its pin. English message. */
20
+ export class NpmPackageError extends Error {
21
+ kind;
22
+ constructor(kind, message) {
23
+ super(message);
24
+ this.kind = kind;
25
+ this.name = "NpmPackageError";
26
+ }
27
+ }
28
+ /** The packument path of a package: a scoped name is written `@scope%2fname`, as npm does. */
29
+ const packumentUrl = (registry, pkg) => `${registry.replace(/\/+$/, "")}/${pkg.replace("/", "%2f")}`;
30
+ async function get(url, what, accept, registry, token) {
31
+ const headers = { accept };
32
+ if (token !== undefined && token !== "" && new URL(url).origin === new URL(registry).origin) {
33
+ headers.authorization = `Bearer ${token}`;
34
+ }
35
+ try {
36
+ return await fetch(url, { headers, signal: AbortSignal.timeout(NPM_TIMEOUT_MS) });
37
+ }
38
+ catch (e) {
39
+ throw new NpmPackageError("fetch", `cannot fetch ${what} (${url}): ${e instanceof Error ? e.message : String(e)}`);
40
+ }
41
+ }
42
+ /**
43
+ * `<pkg>@<version>` from an npm registry: its tarball bytes and the packument's `dist.integrity`
44
+ * (whatever the registry claims; callers compare it with their pin). The packument is asked for in
45
+ * npm's abbreviated form; `opts.token` goes to the registry origin only; the tarball is read up to
46
+ * `opts.maxBytes`. Throws {@link NpmPackageError}: `unpublished` (404 or no such version),
47
+ * `too_large`, `fetch` (anything else). The bytes are not judged here: see
48
+ * {@link downloadPinnedNpmPackage} and {@link assertNpmTarballLabel}.
49
+ */
50
+ export async function fetchNpmTarball(pkg, version, registry = DEFAULT_NPM_REGISTRY, opts = {}) {
51
+ const spec = `${pkg}@${version}`;
52
+ const url = packumentUrl(registry, pkg);
53
+ const res = await get(url, `the packument of ${pkg}`, PACKUMENT_ACCEPT, registry, opts.token);
54
+ if (res.status === 404) {
55
+ throw new NpmPackageError("unpublished", `${pkg} is not published on ${registry}`);
56
+ }
57
+ if (!res.ok) {
58
+ throw new NpmPackageError("fetch", `${url} answered ${res.status} for the packument of ${pkg}`);
59
+ }
60
+ let doc;
61
+ try {
62
+ doc = (await res.json());
63
+ }
64
+ catch {
65
+ throw new NpmPackageError("fetch", `the packument of ${pkg} at ${url} is not valid JSON`);
66
+ }
67
+ const dist = doc.versions?.[version]?.dist;
68
+ if (dist === undefined) {
69
+ throw new NpmPackageError("unpublished", `${spec} is not published on ${registry}`);
70
+ }
71
+ if (typeof dist.tarball !== "string" || !/^https?:\/\//.test(dist.tarball)) {
72
+ throw new NpmPackageError("fetch", `the packument of ${spec} has no tarball URL`);
73
+ }
74
+ const tarball = dist.tarball;
75
+ const t = await get(tarball, `the tarball of ${spec}`, "application/octet-stream", registry, opts.token);
76
+ if (!t.ok) {
77
+ throw new NpmPackageError(t.status === 404 ? "unpublished" : "fetch", `${tarball} answered ${t.status} for the tarball of ${spec}`);
78
+ }
79
+ const bytes = await readCapped(t, spec, opts.maxBytes ?? NPM_TARBALL_MAX_BYTES);
80
+ return {
81
+ bytes,
82
+ distIntegrity: typeof dist.integrity === "string" ? dist.integrity : undefined,
83
+ tarball,
84
+ };
85
+ }
86
+ /**
87
+ * Downloads `<pkg>@<version>` and checks its bytes against the pinned sha512 `integrity`; returns the
88
+ * bytes only when they match (a tampered tarball, or a registry serving other bytes than the pin,
89
+ * throws {@link NpmPackageError} `integrity`), then checks its label
90
+ * ({@link assertNpmTarballLabel}). The registry's own `dist.integrity` plays no part: only the
91
+ * bytes are judged.
92
+ */
93
+ export async function downloadPinnedNpmPackage(pin, registry = DEFAULT_NPM_REGISTRY, opts = {}) {
94
+ const r = await fetchNpmTarball(pin.npm, pin.version, registry, opts);
95
+ const actual = integrityOf(r.bytes);
96
+ if (actual !== pin.integrity) {
97
+ throw new NpmPackageError("integrity", `${pin.npm}@${pin.version} from ${r.tarball} has sha512 ${actual}, not the pinned ${pin.integrity}`);
98
+ }
99
+ await assertNpmTarballLabel(r.bytes, pin.npm, pin.version);
100
+ return r.bytes;
101
+ }
102
+ /**
103
+ * The tarball's `package/package.json` must name `pkg` at `version` (a pin taken from another
104
+ * package's tarball, or a registry serving one under the wrong name); throws
105
+ * {@link NpmPackageError} `mislabelled` otherwise (`fetch` when the bytes are no tgz).
106
+ */
107
+ export async function assertNpmTarballLabel(bytes, pkg, version) {
108
+ const label = await packageJsonOf(bytes, `${pkg}@${version}`);
109
+ if (label.name !== pkg || label.version !== version) {
110
+ throw new NpmPackageError("mislabelled", `the tarball of ${pkg}@${version} is labelled ${String(label.name)}@${String(label.version)} in its package.json`);
111
+ }
112
+ }
113
+ /** The response body, refused past `max` bytes (by its content-length, then while reading). */
114
+ async function readCapped(res, spec, max) {
115
+ const tooLarge = () => new NpmPackageError("too_large", `bootstrap.too_large: the tarball of ${spec} is larger than ${max} bytes`);
116
+ const declared = Number(res.headers.get("content-length") ?? Number.NaN);
117
+ if (Number.isFinite(declared) && declared > max) {
118
+ await res.body?.cancel();
119
+ throw tooLarge();
120
+ }
121
+ const chunks = [];
122
+ let size = 0;
123
+ if (res.body !== null) {
124
+ for await (const chunk of res.body) {
125
+ size += chunk.byteLength;
126
+ if (size > max) {
127
+ await res.body.cancel().catch(() => { });
128
+ throw tooLarge();
129
+ }
130
+ chunks.push(chunk);
131
+ }
132
+ }
133
+ const out = new Uint8Array(size);
134
+ let at = 0;
135
+ for (const c of chunks) {
136
+ out.set(c, at);
137
+ at += c.byteLength;
138
+ }
139
+ return out;
140
+ }
141
+ /** `package/package.json` of an npm tarball (`{}` fields when it has none or it does not parse). */
142
+ async function packageJsonOf(bytes, spec) {
143
+ let tar;
144
+ try {
145
+ tar = gunzipSync(bytes, { maxOutputLength: NPM_TARBALL_MAX_BYTES * 8 });
146
+ }
147
+ catch (e) {
148
+ throw new NpmPackageError("fetch", `the tarball of ${spec} is not a gzip file: ${e instanceof Error ? e.message : String(e)}`);
149
+ }
150
+ const ex = extract();
151
+ let found;
152
+ const done = (async () => {
153
+ for await (const entry of ex) {
154
+ const chunks = [];
155
+ for await (const c of entry)
156
+ chunks.push(c);
157
+ if (found === undefined && entry.header.name === "package/package.json") {
158
+ found = Buffer.concat(chunks);
159
+ }
160
+ }
161
+ })();
162
+ ex.end(tar);
163
+ try {
164
+ await done;
165
+ }
166
+ catch (e) {
167
+ throw new NpmPackageError("fetch", `the tarball of ${spec} is not a tar file: ${e instanceof Error ? e.message : String(e)}`);
168
+ }
169
+ if (found === undefined)
170
+ return {};
171
+ try {
172
+ return JSON.parse(found.toString("utf8"));
173
+ }
174
+ catch {
175
+ return {};
176
+ }
177
+ }
@@ -0,0 +1,44 @@
1
+ import { type PluginManifest } from "@capybara-hq/plugin-sdk";
2
+ /**
3
+ * The result of packing: the tgz path, its hex sha256 and the `.capybara-plugin/plugin.json`
4
+ * written into it (the extension's static manifest is `manifest.extension?.manifest`).
5
+ */
6
+ export interface PackResult {
7
+ file: string;
8
+ sha256: string;
9
+ manifest: PluginManifest;
10
+ /**
11
+ * Entries of the plugin's package.json `files` that are not in the package (directories other than
12
+ * `dist` / `migrations` / `ui` / `skills` / `connectors` / `.capybara-plugin`, globs, missing paths). Callers print them; `check:plugin-pack` fails.
13
+ */
14
+ dropped: string[];
15
+ }
16
+ /**
17
+ * 临时目录前缀。临时目录建在系统临时目录下(不在插件源码树里,免得源码扫描读到半成品);bundle 里对宿主包的
18
+ * import 经临时目录里指向插件解析结果的 `node_modules` 软链解析。
19
+ */
20
+ export declare const STAGING_PREFIX = ".capybara-pack-";
21
+ /**
22
+ * npm metadata copied verbatim from the plugin's package.json when present (F4). They only describe
23
+ * the package on npm; `repository` (with `directory`) is required for npm provenance.
24
+ */
25
+ export declare const METADATA_FIELDS: readonly ["description", "repository", "homepage", "keywords"];
26
+ /** Third-party license notices of the bundled dependencies (written only when there are any). */
27
+ export declare const NOTICES_FILE = "THIRD-PARTY-NOTICES.txt";
28
+ /**
29
+ * Packs a plugin directory into a plugin package (spec §4.1):
30
+ * 1. reads and validates the source `.capybara-plugin/plugin.json` (it must not carry
31
+ * `extension`);
32
+ * 2. when `src/index.ts` exists, bundles the extension ({@link bundleExtension}) and sets
33
+ * `extension = { entry: "./dist/index.mjs", manifest, migrations?: "./migrations", ui?: "./ui" }`;
34
+ * a declarative plugin (no `src/index.ts`) runs no esbuild and creates no staging dir;
35
+ * 3. ships `skills/**`, `connectors/*.json`, `agents/*.md` and the listed paths ({@link contentFiles}), README,
36
+ * LICENSE, NOTICE and the plain files of package.json `files` ({@link extraFiles});
37
+ * 4. writes a package.json with only name / version / type / license / {@link METADATA_FIELDS}
38
+ * and, with an extension, `main` / `exports` (the entry) and peerDependencies (no `capybara`
39
+ * section), and the final
40
+ * `plugin.json`, both validated with the SDK (`parsePluginManifest`,
41
+ * `validatePluginPackageJsonFor`);
42
+ * 5. writes a reproducible tgz with the `package/` prefix to `outDir/<npm-style file name>`.
43
+ */
44
+ export declare function packPlugin(pluginDir: string, outDir: string): Promise<PackResult>;