@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.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +111 -2
- package/bin/capybara-plugin.mjs +12 -0
- package/dist/check/applied-migrations.d.ts +28 -0
- package/dist/check/applied-migrations.js +65 -0
- package/dist/check/cli.d.ts +16 -0
- package/dist/check/cli.js +69 -0
- package/dist/check/deps.d.ts +23 -0
- package/dist/check/deps.js +207 -0
- package/dist/check/index.d.ts +15 -0
- package/dist/check/index.js +14 -0
- package/dist/check/migrations.d.ts +11 -0
- package/dist/check/migrations.js +93 -0
- package/dist/check/plugin-pack.d.ts +17 -0
- package/dist/check/plugin-pack.js +56 -0
- package/dist/check/plugin-permissions.d.ts +22 -0
- package/dist/check/plugin-permissions.js +392 -0
- package/dist/check/plugin-state.d.ts +19 -0
- package/dist/check/plugin-state.js +181 -0
- package/dist/check/plugin-versions.d.ts +13 -0
- package/dist/check/plugin-versions.js +56 -0
- package/dist/check/release-metadata.d.ts +13 -0
- package/dist/check/release-metadata.js +80 -0
- package/dist/check/report.d.ts +8 -0
- package/dist/check/report.js +17 -0
- package/dist/check/run.d.ts +36 -0
- package/dist/check/run.js +60 -0
- package/dist/check/source.d.ts +53 -0
- package/dist/check/source.js +153 -0
- package/dist/check/sync-versions-cli.d.ts +10 -0
- package/dist/check/sync-versions-cli.js +44 -0
- package/dist/check/test-imports.d.ts +20 -0
- package/dist/check/test-imports.js +158 -0
- package/dist/check/tool-design.d.ts +13 -0
- package/dist/check/tool-design.js +227 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +48 -0
- package/dist/discover.d.ts +21 -0
- package/dist/discover.js +69 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/integrity.d.ts +2 -0
- package/dist/integrity.js +3 -0
- package/dist/marketplace/cli.d.ts +20 -0
- package/dist/marketplace/cli.js +80 -0
- package/dist/marketplace/file.d.ts +2 -0
- package/dist/marketplace/file.js +2 -0
- package/dist/marketplace/index.d.ts +2 -0
- package/dist/marketplace/index.js +2 -0
- package/dist/marketplace/marketplace.d.ts +57 -0
- package/dist/marketplace/marketplace.js +245 -0
- package/dist/pack/bootstrap.d.ts +25 -0
- package/dist/pack/bootstrap.js +70 -0
- package/dist/pack/cli.d.ts +20 -0
- package/dist/pack/cli.js +109 -0
- package/dist/pack/index.d.ts +4 -0
- package/dist/pack/index.js +4 -0
- package/dist/pack/npm.d.ts +66 -0
- package/dist/pack/npm.js +177 -0
- package/dist/pack/pack.d.ts +44 -0
- package/dist/pack/pack.js +518 -0
- package/dist/release/cli.d.ts +30 -0
- package/dist/release/cli.js +93 -0
- package/dist/release/index.d.ts +4 -0
- package/dist/release/index.js +4 -0
- package/dist/release/publish-plan.d.ts +68 -0
- package/dist/release/publish-plan.js +117 -0
- package/dist/release/publish.d.ts +91 -0
- package/dist/release/publish.js +274 -0
- package/dist/release/semver.d.ts +11 -0
- package/dist/release/semver.js +44 -0
- package/package.json +61 -3
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, symlink, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { builtinModules } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join, posix, relative, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { gzip } from "node:zlib";
|
|
10
|
+
import { AGENT_TEMPLATE_LIMITS, checkEntryImports, defineExtension, HOST_PROVIDED_PACKAGES, PLUGIN_MANIFEST_FILE, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, SDK_VERSION, staticManifestOf, validatePluginPackageJsonFor, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { init as lexerInit, parse as lexerParse } from "es-module-lexer";
|
|
12
|
+
import { build } from "esbuild";
|
|
13
|
+
import { pack as tarPack } from "tar-stream";
|
|
14
|
+
/** The extension entry the packer writes (relative to the package root). */
|
|
15
|
+
const EXTENSION_ENTRY = "dist/index.mjs";
|
|
16
|
+
/**
|
|
17
|
+
* 临时目录前缀。临时目录建在系统临时目录下(不在插件源码树里,免得源码扫描读到半成品);bundle 里对宿主包的
|
|
18
|
+
* import 经临时目录里指向插件解析结果的 `node_modules` 软链解析。
|
|
19
|
+
*/
|
|
20
|
+
export const STAGING_PREFIX = ".capybara-pack-";
|
|
21
|
+
/**
|
|
22
|
+
* 在临时目录下建 `node_modules/<宿主包>` 软链,指向从插件目录按 Node 规则向上找到的那份(找不到的跳过,
|
|
23
|
+
* 导入产物时照常报错)。软链在 `package/` 之外,不进包。
|
|
24
|
+
*/
|
|
25
|
+
async function linkHostPackages(dir, stage) {
|
|
26
|
+
for (const name of HOST_PROVIDED_PACKAGES) {
|
|
27
|
+
let found;
|
|
28
|
+
for (let d = dir;; d = dirname(d)) {
|
|
29
|
+
const candidate = join(d, "node_modules", name);
|
|
30
|
+
if (existsSync(candidate)) {
|
|
31
|
+
found = candidate;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (dirname(d) === d)
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (found === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const link = join(stage, "node_modules", name);
|
|
40
|
+
await mkdir(dirname(link), { recursive: true });
|
|
41
|
+
await symlink(await realpath(found), link, "dir");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** npm pack 使用的固定 mtime(让同样的输入产出字节相同的包)。 */
|
|
45
|
+
const FIXED_MTIME = new Date("1985-10-26T08:15:00Z");
|
|
46
|
+
/** 动态加载 bundle 的入口(单独成函数,便于测试与内核的加载方式保持一致)。 */
|
|
47
|
+
const importBundle = async (file) => (await import(pathToFileURL(file).href));
|
|
48
|
+
const BUILTINS = new Set(builtinModules.filter((m) => !m.startsWith("_")));
|
|
49
|
+
/**
|
|
50
|
+
* esbuild 插件:Node 内置模块(带不带 `node:` 都算)一律外置并改写成 `node:` 前缀,
|
|
51
|
+
* 这样产物的 import / require 只会出现 `node:*`,满足 C11 的白名单。
|
|
52
|
+
*/
|
|
53
|
+
const nodeBuiltins = {
|
|
54
|
+
name: "capybara-node-builtins",
|
|
55
|
+
setup(b) {
|
|
56
|
+
b.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
57
|
+
const bare = args.path.startsWith("node:") ? args.path.slice(5) : args.path;
|
|
58
|
+
const top = bare.split("/")[0];
|
|
59
|
+
if (args.path.startsWith("node:") || BUILTINS.has(bare) || BUILTINS.has(top)) {
|
|
60
|
+
return { path: `node:${bare}`, external: true };
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* 打进来的 CJS 依赖要用 `require` / `__dirname` / `__filename`,ESM 里都没有:`require` 用 `node:module` 的
|
|
68
|
+
* createRequire 补上(esbuild 的 `__require` 垫片按 `typeof require` 取它);后两者在横幅里以不会撞名的名字定义,
|
|
69
|
+
* 再经 `define` 把对全局 `__dirname` / `__filename` 的引用替换过去(指向包内的 `dist/`)。
|
|
70
|
+
*/
|
|
71
|
+
const BANNER = [
|
|
72
|
+
'import { createRequire as __capybaraCreateRequire } from "node:module";',
|
|
73
|
+
'import { fileURLToPath as __capybaraFileURLToPath } from "node:url";',
|
|
74
|
+
'import { dirname as __capybaraPathDirname } from "node:path";',
|
|
75
|
+
"const require = __capybaraCreateRequire(import.meta.url);",
|
|
76
|
+
"const __capybara_filename = __capybaraFileURLToPath(import.meta.url);",
|
|
77
|
+
"const __capybara_dirname = __capybaraPathDirname(__capybara_filename);",
|
|
78
|
+
].join("\n");
|
|
79
|
+
const DEFINE = { __dirname: "__capybara_dirname", __filename: "__capybara_filename" };
|
|
80
|
+
/** 用 es-module-lexer 列出模块的 import(与内核安装时的扫描一致),交给 SDK 的白名单核对。 */
|
|
81
|
+
async function importIssues(code) {
|
|
82
|
+
await lexerInit;
|
|
83
|
+
const [imports] = lexerParse(code);
|
|
84
|
+
return checkEntryImports(imports
|
|
85
|
+
.filter((i) => i.d !== -2)
|
|
86
|
+
.map((i) => ({ name: i.n ?? "", dynamic: i.d > -1, literal: i.n !== undefined })));
|
|
87
|
+
}
|
|
88
|
+
async function listFiles(dir, base = dir) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
91
|
+
const p = join(dir, e.name);
|
|
92
|
+
if (e.isDirectory())
|
|
93
|
+
out.push(...(await listFiles(p, base)));
|
|
94
|
+
else if (e.isFile())
|
|
95
|
+
out.push(relative(base, p).split("\\").join("/"));
|
|
96
|
+
else
|
|
97
|
+
throw new Error(`${p}: only regular files can be packed`);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The gzip header's OS byte (RFC 1952 offset 9). zlib fills it from the host it was built for (0x13
|
|
103
|
+
* on macOS, 0x03 on Unix, 0x0a on Windows), the only byte of the pack that depended on the host: it
|
|
104
|
+
* is pinned to Unix so the same source and lockfile give the same tgz on every platform. Readers
|
|
105
|
+
* ignore it.
|
|
106
|
+
*/
|
|
107
|
+
const GZIP_OS_BYTE = 9;
|
|
108
|
+
const GZIP_OS_UNIX = 0x03;
|
|
109
|
+
/**
|
|
110
|
+
* Gzips the tar: entries sorted by path, `package/` prefix, 0644, fixed mtime, no owner; the gzip
|
|
111
|
+
* header has mtime 0 and the Unix OS byte ({@link GZIP_OS_UNIX}).
|
|
112
|
+
*/
|
|
113
|
+
async function makeTgz(files) {
|
|
114
|
+
const p = tarPack();
|
|
115
|
+
const chunks = [];
|
|
116
|
+
const collected = (async () => {
|
|
117
|
+
for await (const c of p)
|
|
118
|
+
chunks.push(c);
|
|
119
|
+
})();
|
|
120
|
+
for (const path of [...files.keys()].sort()) {
|
|
121
|
+
const data = files.get(path);
|
|
122
|
+
await new Promise((res, rej) => {
|
|
123
|
+
p.entry({
|
|
124
|
+
name: `package/${path}`,
|
|
125
|
+
size: data.length,
|
|
126
|
+
mode: 0o644,
|
|
127
|
+
mtime: FIXED_MTIME,
|
|
128
|
+
uid: 0,
|
|
129
|
+
gid: 0,
|
|
130
|
+
uname: "",
|
|
131
|
+
gname: "",
|
|
132
|
+
type: "file",
|
|
133
|
+
}, data, (err) => (err ? rej(err) : res()));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
p.finalize();
|
|
137
|
+
await collected;
|
|
138
|
+
const gz = await promisify(gzip)(Buffer.concat(chunks), { level: 9 });
|
|
139
|
+
gz[GZIP_OS_BYTE] = GZIP_OS_UNIX;
|
|
140
|
+
return gz;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* npm metadata copied verbatim from the plugin's package.json when present (F4). They only describe
|
|
144
|
+
* the package on npm; `repository` (with `directory`) is required for npm provenance.
|
|
145
|
+
*/
|
|
146
|
+
export const METADATA_FIELDS = ["description", "repository", "homepage", "keywords"];
|
|
147
|
+
const pick = (o, keys) => Object.fromEntries(keys.filter((k) => o[k] !== undefined).map((k) => [k, o[k]]));
|
|
148
|
+
/** Third-party license notices of the bundled dependencies (written only when there are any). */
|
|
149
|
+
export const NOTICES_FILE = "THIRD-PARTY-NOTICES.txt";
|
|
150
|
+
const LICENSE_FILE_RE = /^(?:licen[cs]e|copying|notice)(?:[.-].*)?$/i;
|
|
151
|
+
/** Nearest `LICENSE` from the plugin dir up to the workspace root (the dir with pnpm-workspace.yaml). */
|
|
152
|
+
function findLicense(dir) {
|
|
153
|
+
for (let d = dir;; d = dirname(d)) {
|
|
154
|
+
const f = join(d, "LICENSE");
|
|
155
|
+
if (existsSync(f))
|
|
156
|
+
return f;
|
|
157
|
+
if (existsSync(join(d, "pnpm-workspace.yaml")) || dirname(d) === d)
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** `files` entries shipped (or deliberately not shipped) by other means than {@link extraFiles}. */
|
|
162
|
+
const SHIPPED_ELSEWHERE = [
|
|
163
|
+
"dist",
|
|
164
|
+
"migrations",
|
|
165
|
+
"ui",
|
|
166
|
+
"skills",
|
|
167
|
+
"connectors",
|
|
168
|
+
"agents",
|
|
169
|
+
".capybara-plugin",
|
|
170
|
+
"README.md",
|
|
171
|
+
"LICENSE",
|
|
172
|
+
"NOTICE",
|
|
173
|
+
];
|
|
174
|
+
/**
|
|
175
|
+
* Files shipped besides the bundle: `README.md` and `LICENSE` (the plugin's own, else the workspace's),
|
|
176
|
+
* the `NOTICE` next to that `LICENSE` if there is one (Apache-2.0 4(d) asks redistributions to carry it),
|
|
177
|
+
* plus every plain file named in the package.json `files` list at the plugin root (e.g. slack's
|
|
178
|
+
* `app-manifest.yaml`). `dist` is replaced by the bundle, `migrations` / `ui` are handled by the
|
|
179
|
+
* extension, `skills` / `connectors` / `agents` by {@link contentFiles} and `.capybara-plugin` is written by the
|
|
180
|
+
* packer; any other entry (a directory, a glob, a missing path) is reported in `dropped`.
|
|
181
|
+
*/
|
|
182
|
+
async function extraFiles(dir, listed) {
|
|
183
|
+
const files = new Map();
|
|
184
|
+
const dropped = [];
|
|
185
|
+
if (existsSync(join(dir, "README.md"))) {
|
|
186
|
+
files.set("README.md", await readFile(join(dir, "README.md")));
|
|
187
|
+
}
|
|
188
|
+
const license = findLicense(dir);
|
|
189
|
+
if (license !== undefined)
|
|
190
|
+
files.set("LICENSE", await readFile(license));
|
|
191
|
+
const notice = license === undefined ? undefined : join(dirname(license), "NOTICE");
|
|
192
|
+
if (notice !== undefined && existsSync(notice))
|
|
193
|
+
files.set("NOTICE", await readFile(notice));
|
|
194
|
+
for (const raw of Array.isArray(listed) ? listed : []) {
|
|
195
|
+
const entry = String(raw).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
196
|
+
if (SHIPPED_ELSEWHERE.includes(entry))
|
|
197
|
+
continue;
|
|
198
|
+
const p = join(dir, entry);
|
|
199
|
+
const plain = /^[\w.-]+$/.test(entry) && existsSync(p) && (await stat(p)).isFile();
|
|
200
|
+
if (plain)
|
|
201
|
+
files.set(entry, await readFile(p));
|
|
202
|
+
else
|
|
203
|
+
dropped.push(entry);
|
|
204
|
+
}
|
|
205
|
+
return { files, dropped };
|
|
206
|
+
}
|
|
207
|
+
/** Package root of a bundled input under node_modules (`.../node_modules/@scope/name` or `/name`). */
|
|
208
|
+
function packageRootOf(abs) {
|
|
209
|
+
const m = /^(.*[/\\]node_modules[/\\](?:@[^/\\]+[/\\])?[^/\\]+)/.exec(abs);
|
|
210
|
+
return m?.[1];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* THIRD-PARTY-NOTICES.txt for the npm packages esbuild bundled (from the metafile inputs): name,
|
|
214
|
+
* version, license and the package's license / notice files, sorted, so MIT / BSD notices that require
|
|
215
|
+
* reproduction survive bundling. Undefined when nothing third-party was bundled.
|
|
216
|
+
*/
|
|
217
|
+
async function thirdPartyNotices(dir, inputs) {
|
|
218
|
+
const roots = new Set();
|
|
219
|
+
for (const i of inputs) {
|
|
220
|
+
const r = packageRootOf(resolve(dir, i));
|
|
221
|
+
if (r !== undefined)
|
|
222
|
+
roots.add(r);
|
|
223
|
+
}
|
|
224
|
+
const entries = [];
|
|
225
|
+
for (const root of roots) {
|
|
226
|
+
const pj = join(root, "package.json");
|
|
227
|
+
if (!existsSync(pj))
|
|
228
|
+
continue;
|
|
229
|
+
const pkg = JSON.parse(await readFile(pj, "utf8"));
|
|
230
|
+
const id = `${String(pkg.name)}@${String(pkg.version)}`;
|
|
231
|
+
const license = typeof pkg.license === "string" ? pkg.license : "UNKNOWN";
|
|
232
|
+
const texts = [];
|
|
233
|
+
for (const f of (await readdir(root)).filter((n) => LICENSE_FILE_RE.test(n)).sort()) {
|
|
234
|
+
if ((await stat(join(root, f))).isFile())
|
|
235
|
+
texts.push((await readFile(join(root, f), "utf8")).trim());
|
|
236
|
+
}
|
|
237
|
+
const body = texts.length > 0
|
|
238
|
+
? texts.join("\n\n")
|
|
239
|
+
: `(no license file in the package; license: ${license})`;
|
|
240
|
+
entries.push({ id, text: `${id} (${license})\n\n${body}` });
|
|
241
|
+
}
|
|
242
|
+
if (entries.length === 0)
|
|
243
|
+
return undefined;
|
|
244
|
+
const unique = [...new Map(entries.map((e) => [e.id, e])).values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
245
|
+
const rule = "-".repeat(72);
|
|
246
|
+
return `This package bundles the third-party software listed below. Their license notices follow.\n\n${unique
|
|
247
|
+
.map((e) => `${rule}\n${e.text}\n`)
|
|
248
|
+
.join("\n")}`;
|
|
249
|
+
}
|
|
250
|
+
/** npm pack 的文件名:`@scope/name` → `scope-name-<version>.tgz`。 */
|
|
251
|
+
const tgzName = (name, version) => `${name.replace(/^@/, "").replace("/", "-")}-${version}.tgz`;
|
|
252
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
253
|
+
const stripDot = (p) => p.replace(/^\.\//, "");
|
|
254
|
+
/**
|
|
255
|
+
* Reads and validates the source `.capybara-plugin/plugin.json` (without `extension`, which the
|
|
256
|
+
* packer writes).
|
|
257
|
+
*/
|
|
258
|
+
async function readSourceManifest(dir, label) {
|
|
259
|
+
const file = join(dir, PLUGIN_MANIFEST_FILE);
|
|
260
|
+
if (!existsSync(file)) {
|
|
261
|
+
throw new Error(`${label}: missing ${PLUGIN_MANIFEST_FILE} (the plugin manifest)`);
|
|
262
|
+
}
|
|
263
|
+
const raw = await readFile(file, "utf8");
|
|
264
|
+
let data = raw;
|
|
265
|
+
try {
|
|
266
|
+
data = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// parsePluginManifest reports the JSON error.
|
|
270
|
+
}
|
|
271
|
+
if (isObj(data) && "extension" in data) {
|
|
272
|
+
throw new Error(`${label}: ${PLUGIN_MANIFEST_FILE}: extension is written by the packer; remove it from the source`);
|
|
273
|
+
}
|
|
274
|
+
const m = parsePluginManifest(data);
|
|
275
|
+
if ("errors" in m)
|
|
276
|
+
throw new Error(`${label}: ${PLUGIN_MANIFEST_FILE}: ${m.errors.join("; ")}`);
|
|
277
|
+
return m;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The skills, connector templates, agent templates and icons of a plugin: every file under
|
|
281
|
+
* `skills/`, every `connectors/*.json` and every `agents/*.md` when present, plus the paths
|
|
282
|
+
* `plugin.json` lists (`skills`, `connectorTemplates`, `agents`, `icon`) and the template icons.
|
|
283
|
+
* Listed paths must exist, connector template files must parse ({@link parseConnectorTemplateFile})
|
|
284
|
+
* and agent templates too ({@link parseAgentTemplate}), so a package that lies about its content
|
|
285
|
+
* fails here instead of at import. `skills` / `templates` / `agents` count what the kernel will see
|
|
286
|
+
* (the listed ones, else the scanned ones).
|
|
287
|
+
*/
|
|
288
|
+
async function contentFiles(dir, label, m) {
|
|
289
|
+
const files = new Map();
|
|
290
|
+
const addTree = async (rel) => {
|
|
291
|
+
for (const f of await listFiles(join(dir, rel))) {
|
|
292
|
+
files.set(posix.join(rel, f), await readFile(join(dir, rel, f)));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const addFile = async (rel, what) => {
|
|
296
|
+
const p = join(dir, rel);
|
|
297
|
+
if (!existsSync(p) || !(await stat(p)).isFile()) {
|
|
298
|
+
throw new Error(`${label}: ${what} ./${rel} does not exist`);
|
|
299
|
+
}
|
|
300
|
+
files.set(rel, await readFile(p));
|
|
301
|
+
};
|
|
302
|
+
let scannedSkills = 0;
|
|
303
|
+
if (existsSync(join(dir, "skills"))) {
|
|
304
|
+
await addTree("skills");
|
|
305
|
+
scannedSkills = [...files.keys()].filter((p) => /^skills\/[^/]+\/SKILL\.md$/.test(p)).length;
|
|
306
|
+
}
|
|
307
|
+
for (const s of m.skills ?? []) {
|
|
308
|
+
const rel = stripDot(s.path);
|
|
309
|
+
if (!existsSync(join(dir, rel, "SKILL.md"))) {
|
|
310
|
+
throw new Error(`${label}: skill ${s.path} has no SKILL.md`);
|
|
311
|
+
}
|
|
312
|
+
await addTree(rel);
|
|
313
|
+
}
|
|
314
|
+
const templates = [];
|
|
315
|
+
if (existsSync(join(dir, "connectors"))) {
|
|
316
|
+
for (const e of (await readdir(join(dir, "connectors"), { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
317
|
+
if (e.isFile() && e.name.endsWith(".json"))
|
|
318
|
+
templates.push(`connectors/${e.name}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const listedTemplates = (m.connectorTemplates ?? []).map((c) => stripDot(c.path));
|
|
322
|
+
for (const rel of new Set([...templates, ...listedTemplates])) {
|
|
323
|
+
await addFile(rel, "connector template");
|
|
324
|
+
const t = parseConnectorTemplateFile(files.get(rel).toString("utf8"));
|
|
325
|
+
if ("errors" in t)
|
|
326
|
+
throw new Error(`${label}: ${rel}: ${t.errors.join("; ")}`);
|
|
327
|
+
if (t.icon !== undefined)
|
|
328
|
+
await addFile(stripDot(t.icon), `${rel} icon`);
|
|
329
|
+
}
|
|
330
|
+
if (m.icon !== undefined)
|
|
331
|
+
await addFile(stripDot(m.icon), "icon");
|
|
332
|
+
// Agent templates: listed, else scanned from agents/*.md (sub-agents spec §16.2)
|
|
333
|
+
const agents = [];
|
|
334
|
+
if (m.agents === undefined && existsSync(join(dir, "agents"))) {
|
|
335
|
+
for (const e of (await readdir(join(dir, "agents"), { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
336
|
+
if (e.isFile() && e.name.endsWith(".md"))
|
|
337
|
+
agents.push(`agents/${e.name}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// the import's checks, here so a pack fails before import: the per-plugin limit for scanned
|
|
341
|
+
// files (a listed one is capped by the manifest schema) and unique names
|
|
342
|
+
if (agents.length > AGENT_TEMPLATE_LIMITS.perPlugin) {
|
|
343
|
+
throw new Error(`${label}: agents/ holds ${agents.length} agent templates; a plugin may have at most ${AGENT_TEMPLATE_LIMITS.perPlugin}`);
|
|
344
|
+
}
|
|
345
|
+
for (const a of m.agents ?? [])
|
|
346
|
+
agents.push(stripDot(a.path));
|
|
347
|
+
const agentNames = new Map();
|
|
348
|
+
for (const rel of agents) {
|
|
349
|
+
await addFile(rel, "agent template");
|
|
350
|
+
const t = parseAgentTemplate(files.get(rel).toString("utf8"));
|
|
351
|
+
if ("errors" in t)
|
|
352
|
+
throw new Error(`${label}: ${rel}: ${t.errors.join("; ")}`);
|
|
353
|
+
const other = agentNames.get(t.template.name);
|
|
354
|
+
if (other !== undefined) {
|
|
355
|
+
throw new Error(`${label}: ${rel}: agent template name "${t.template.name}" is also used by ${other}`);
|
|
356
|
+
}
|
|
357
|
+
agentNames.set(t.template.name, rel);
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
files,
|
|
361
|
+
skills: m.skills?.length ?? scannedSkills,
|
|
362
|
+
templates: m.connectorTemplates?.length ?? templates.length,
|
|
363
|
+
agents: agents.length,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Bundles the extension `src/index.ts`:
|
|
368
|
+
* 1. esbuild bundles it into the single-file ESM `dist/index.mjs` (platform node, target node24),
|
|
369
|
+
* with only {@link HOST_PROVIDED_PACKAGES} and Node builtins (rewritten to `node:*`) external;
|
|
370
|
+
* CJS dependencies work through the createRequire banner;
|
|
371
|
+
* 2. the bundle's imports are checked (allowlist, no non-literal dynamic import), the bundle is
|
|
372
|
+
* imported and its default export goes through defineExtension; `staticManifestOf` gives the
|
|
373
|
+
* static manifest, whose version must equal the package.json version;
|
|
374
|
+
* 3. `migrations/` (when the manifest declares migrations, which must resolve to the plugin's
|
|
375
|
+
* `migrations/`), `ui/` (when present) and THIRD-PARTY-NOTICES.txt join the files.
|
|
376
|
+
*
|
|
377
|
+
* Temporary files live in `.capybara-pack-*` under the system temp dir and are removed afterwards.
|
|
378
|
+
*/
|
|
379
|
+
async function bundleExtension(dir, label, pkgVersion) {
|
|
380
|
+
const stage = await realpath(await mkdtemp(join(tmpdir(), STAGING_PREFIX)));
|
|
381
|
+
try {
|
|
382
|
+
await linkHostPackages(dir, stage);
|
|
383
|
+
const entryOut = join(stage, "package", EXTENSION_ENTRY);
|
|
384
|
+
const result = await build({
|
|
385
|
+
absWorkingDir: dir,
|
|
386
|
+
entryPoints: [join(dir, "src/index.ts")],
|
|
387
|
+
outfile: entryOut,
|
|
388
|
+
bundle: true,
|
|
389
|
+
platform: "node",
|
|
390
|
+
format: "esm",
|
|
391
|
+
target: "node24",
|
|
392
|
+
external: [...HOST_PROVIDED_PACKAGES],
|
|
393
|
+
plugins: [nodeBuiltins],
|
|
394
|
+
banner: { js: BANNER },
|
|
395
|
+
define: DEFINE,
|
|
396
|
+
charset: "utf8",
|
|
397
|
+
logLevel: "silent",
|
|
398
|
+
metafile: true,
|
|
399
|
+
// Notices of bundled dependencies go to THIRD-PARTY-NOTICES.txt (built from the metafile).
|
|
400
|
+
legalComments: "none",
|
|
401
|
+
});
|
|
402
|
+
if (result.errors.length > 0) {
|
|
403
|
+
throw new Error(`${label}: esbuild failed: ${result.errors.map((e) => e.text).join("; ")}`);
|
|
404
|
+
}
|
|
405
|
+
const code = await readFile(entryOut);
|
|
406
|
+
const issues = await importIssues(code.toString("utf8"));
|
|
407
|
+
if (issues.length > 0)
|
|
408
|
+
throw new Error(`${label}: ${issues.join("; ")}`);
|
|
409
|
+
const mod = await importBundle(entryOut);
|
|
410
|
+
if (typeof mod.default !== "object" || mod.default === null) {
|
|
411
|
+
throw new Error(`${label}: the bundle has no default export object`);
|
|
412
|
+
}
|
|
413
|
+
const m = defineExtension(mod.default);
|
|
414
|
+
if (m.version !== pkgVersion) {
|
|
415
|
+
throw new Error(`${label}: manifest version ${m.version} does not match package.json version ${String(pkgVersion)}`);
|
|
416
|
+
}
|
|
417
|
+
const files = new Map([[EXTENSION_ENTRY, code]]);
|
|
418
|
+
const extension = {
|
|
419
|
+
entry: `./${EXTENSION_ENTRY}`,
|
|
420
|
+
manifest: staticManifestOf(m),
|
|
421
|
+
};
|
|
422
|
+
if (m.migrations !== undefined) {
|
|
423
|
+
const from = resolve(dir, "src", m.migrations);
|
|
424
|
+
if (from !== join(dir, "migrations")) {
|
|
425
|
+
throw new Error(`${label}: manifest migrations "${m.migrations}" must resolve to <plugin>/migrations`);
|
|
426
|
+
}
|
|
427
|
+
for (const f of await listFiles(from)) {
|
|
428
|
+
files.set(posix.join("migrations", f), await readFile(join(from, f)));
|
|
429
|
+
}
|
|
430
|
+
extension.migrations = "./migrations";
|
|
431
|
+
}
|
|
432
|
+
if (existsSync(join(dir, "ui"))) {
|
|
433
|
+
for (const f of await listFiles(join(dir, "ui"))) {
|
|
434
|
+
files.set(posix.join("ui", f), await readFile(join(dir, "ui", f)));
|
|
435
|
+
}
|
|
436
|
+
extension.ui = "./ui";
|
|
437
|
+
}
|
|
438
|
+
const notices = await thirdPartyNotices(dir, Object.keys(result.metafile?.inputs ?? {}));
|
|
439
|
+
if (notices !== undefined)
|
|
440
|
+
files.set(NOTICES_FILE, Buffer.from(notices));
|
|
441
|
+
return { files, extension };
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
await rm(stage, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Packs a plugin directory into a plugin package (spec §4.1):
|
|
449
|
+
* 1. reads and validates the source `.capybara-plugin/plugin.json` (it must not carry
|
|
450
|
+
* `extension`);
|
|
451
|
+
* 2. when `src/index.ts` exists, bundles the extension ({@link bundleExtension}) and sets
|
|
452
|
+
* `extension = { entry: "./dist/index.mjs", manifest, migrations?: "./migrations", ui?: "./ui" }`;
|
|
453
|
+
* a declarative plugin (no `src/index.ts`) runs no esbuild and creates no staging dir;
|
|
454
|
+
* 3. ships `skills/**`, `connectors/*.json`, `agents/*.md` and the listed paths ({@link contentFiles}), README,
|
|
455
|
+
* LICENSE, NOTICE and the plain files of package.json `files` ({@link extraFiles});
|
|
456
|
+
* 4. writes a package.json with only name / version / type / license / {@link METADATA_FIELDS}
|
|
457
|
+
* and, with an extension, `main` / `exports` (the entry) and peerDependencies (no `capybara`
|
|
458
|
+
* section), and the final
|
|
459
|
+
* `plugin.json`, both validated with the SDK (`parsePluginManifest`,
|
|
460
|
+
* `validatePluginPackageJsonFor`);
|
|
461
|
+
* 5. writes a reproducible tgz with the `package/` prefix to `outDir/<npm-style file name>`.
|
|
462
|
+
*/
|
|
463
|
+
export async function packPlugin(pluginDir, outDir) {
|
|
464
|
+
const dir = resolve(pluginDir);
|
|
465
|
+
const srcPkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
466
|
+
const label = typeof srcPkg.name === "string" ? srcPkg.name : dir;
|
|
467
|
+
const source = await readSourceManifest(dir, label);
|
|
468
|
+
const content = await contentFiles(dir, label, source);
|
|
469
|
+
const hasExtension = existsSync(join(dir, "src/index.ts"));
|
|
470
|
+
if (!hasExtension && content.skills === 0 && content.templates === 0 && content.agents === 0) {
|
|
471
|
+
throw new Error(`${label}: a plugin must contain an extension, a skill, a connector template or an agent template (src/index.ts, skills/<name>/SKILL.md, connectors/<id>.json or agents/<name>.md)`);
|
|
472
|
+
}
|
|
473
|
+
const ext = hasExtension ? await bundleExtension(dir, label, srcPkg.version) : undefined;
|
|
474
|
+
const files = new Map([...(ext?.files ?? []), ...content.files]);
|
|
475
|
+
const extra = await extraFiles(dir, srcPkg.files);
|
|
476
|
+
for (const [path, data] of extra.files)
|
|
477
|
+
files.set(path, data);
|
|
478
|
+
const manifest = ext === undefined ? source : { ...source, extension: ext.extension };
|
|
479
|
+
const parsed = parsePluginManifest(manifest);
|
|
480
|
+
if ("errors" in parsed) {
|
|
481
|
+
throw new Error(`${label}: packed ${PLUGIN_MANIFEST_FILE}: ${parsed.errors.join("; ")}`);
|
|
482
|
+
}
|
|
483
|
+
const deps = (srcPkg.dependencies ?? {});
|
|
484
|
+
const pkg = {
|
|
485
|
+
name: srcPkg.name,
|
|
486
|
+
version: srcPkg.version,
|
|
487
|
+
type: "module",
|
|
488
|
+
...(srcPkg.license === undefined ? {} : { license: srcPkg.license }),
|
|
489
|
+
...pick(srcPkg, METADATA_FIELDS),
|
|
490
|
+
...(ext === undefined
|
|
491
|
+
? {}
|
|
492
|
+
: {
|
|
493
|
+
// the entry fields make an installed plugin resolvable by package name (a plugin
|
|
494
|
+
// repository's tests load model-fake from npm with `createRequire(...).resolve(name)`)
|
|
495
|
+
main: ext.extension.entry,
|
|
496
|
+
exports: { ".": ext.extension.entry },
|
|
497
|
+
peerDependencies: {
|
|
498
|
+
"@capybara-hq/plugin-sdk": ext.extension.manifest.sdk,
|
|
499
|
+
zod: deps.zod ?? "^4.0.0",
|
|
500
|
+
},
|
|
501
|
+
}),
|
|
502
|
+
};
|
|
503
|
+
const pkgIssues = validatePluginPackageJsonFor(pkg, parsed, SDK_VERSION);
|
|
504
|
+
if (pkgIssues.length > 0)
|
|
505
|
+
throw new Error(`${label}: ${pkgIssues.join("; ")}`);
|
|
506
|
+
files.set("package.json", Buffer.from(`${JSON.stringify(pkg, null, 2)}\n`));
|
|
507
|
+
files.set(PLUGIN_MANIFEST_FILE, Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`));
|
|
508
|
+
const tgz = await makeTgz(files);
|
|
509
|
+
await mkdir(outDir, { recursive: true });
|
|
510
|
+
const file = resolve(outDir, tgzName(String(srcPkg.name), String(srcPkg.version)));
|
|
511
|
+
await writeFile(file, tgz);
|
|
512
|
+
return {
|
|
513
|
+
file,
|
|
514
|
+
sha256: createHash("sha256").update(tgz).digest("hex"),
|
|
515
|
+
manifest: parsed,
|
|
516
|
+
dropped: extra.dropped,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const RELEASE_USAGE: string;
|
|
2
|
+
/**
|
|
3
|
+
* `capybara-plugin release`: the npm release of a workspace.
|
|
4
|
+
*
|
|
5
|
+
* Publishes every non-private workspace package under `--root` (default: the current directory;
|
|
6
|
+
* the packages are the directories with a package.json in `packages/*`, `apps/*`, `plugins/*` and
|
|
7
|
+
* `tools/*`, and none found is an error) whose version is not on the registry yet (plugins, the packages under `plugins/`, as plugin tgz
|
|
8
|
+
* files; libraries through `pnpm pack`), then writes the plugin checksums to `<out>/plugins`.
|
|
9
|
+
* Default output: `<root>/artifacts/release`. Build the libraries first.
|
|
10
|
+
*
|
|
11
|
+
* `--auto` is the push-to-main mode: the checkout must be exactly GITHUB_SHA (when set) with no
|
|
12
|
+
* uncommitted package.json changes, and the first release (nothing public on the registry yet) is
|
|
13
|
+
* refused: that one is only ever published by a manual workflow run, and so is the promotion of a
|
|
14
|
+
* release candidate (a stable version whose highest version on npm is a prerelease of it); the
|
|
15
|
+
* whole run then publishes nothing. Otherwise it publishes
|
|
16
|
+
* whatever committed versions are missing from npm, so a run that replaced a cancelled one still
|
|
17
|
+
* publishes what the cancelled run would have.
|
|
18
|
+
*
|
|
19
|
+
* `--plugins <pattern>` (repeatable) limits the plugins to those whose directory relative to the
|
|
20
|
+
* root matches (`*` within one path segment; a pattern matching no plugin is an error; libraries
|
|
21
|
+
* are not affected): core passes `plugins/model-fake`. `--marketplace <file>` makes each plugin's
|
|
22
|
+
* pack match that version's npm entry in the repository's `marketplace.json` before it is
|
|
23
|
+
* published, else the run stops (`release.integrity_mismatch`); every plugin is checked before the
|
|
24
|
+
* first publish. Without it, `<root>/.capybara-plugin/marketplace.json` is used when it exists.
|
|
25
|
+
*
|
|
26
|
+
* Under GitHub Actions it appends `published=<count>` to $GITHUB_OUTPUT after every publish (the
|
|
27
|
+
* last line wins; always 0 in dry run). Relative paths resolve against `cwd` (the current
|
|
28
|
+
* directory). Returns the exit code.
|
|
29
|
+
*/
|
|
30
|
+
export declare function releaseCommand(args: string[], cwd: string): Promise<number>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { MARKETPLACE_FILE } from "../marketplace/file.js";
|
|
5
|
+
import { assertCommittedTree, runPublish } from "./publish.js";
|
|
6
|
+
export const RELEASE_USAGE = "capybara-plugin release [--dry-run] [--provenance] [--out <dir>] [--auto] [--root <dir>]\n" +
|
|
7
|
+
" [--plugins <pattern>]... [--marketplace <file>]";
|
|
8
|
+
/**
|
|
9
|
+
* `capybara-plugin release`: the npm release of a workspace.
|
|
10
|
+
*
|
|
11
|
+
* Publishes every non-private workspace package under `--root` (default: the current directory;
|
|
12
|
+
* the packages are the directories with a package.json in `packages/*`, `apps/*`, `plugins/*` and
|
|
13
|
+
* `tools/*`, and none found is an error) whose version is not on the registry yet (plugins, the packages under `plugins/`, as plugin tgz
|
|
14
|
+
* files; libraries through `pnpm pack`), then writes the plugin checksums to `<out>/plugins`.
|
|
15
|
+
* Default output: `<root>/artifacts/release`. Build the libraries first.
|
|
16
|
+
*
|
|
17
|
+
* `--auto` is the push-to-main mode: the checkout must be exactly GITHUB_SHA (when set) with no
|
|
18
|
+
* uncommitted package.json changes, and the first release (nothing public on the registry yet) is
|
|
19
|
+
* refused: that one is only ever published by a manual workflow run, and so is the promotion of a
|
|
20
|
+
* release candidate (a stable version whose highest version on npm is a prerelease of it); the
|
|
21
|
+
* whole run then publishes nothing. Otherwise it publishes
|
|
22
|
+
* whatever committed versions are missing from npm, so a run that replaced a cancelled one still
|
|
23
|
+
* publishes what the cancelled run would have.
|
|
24
|
+
*
|
|
25
|
+
* `--plugins <pattern>` (repeatable) limits the plugins to those whose directory relative to the
|
|
26
|
+
* root matches (`*` within one path segment; a pattern matching no plugin is an error; libraries
|
|
27
|
+
* are not affected): core passes `plugins/model-fake`. `--marketplace <file>` makes each plugin's
|
|
28
|
+
* pack match that version's npm entry in the repository's `marketplace.json` before it is
|
|
29
|
+
* published, else the run stops (`release.integrity_mismatch`); every plugin is checked before the
|
|
30
|
+
* first publish. Without it, `<root>/.capybara-plugin/marketplace.json` is used when it exists.
|
|
31
|
+
*
|
|
32
|
+
* Under GitHub Actions it appends `published=<count>` to $GITHUB_OUTPUT after every publish (the
|
|
33
|
+
* last line wins; always 0 in dry run). Relative paths resolve against `cwd` (the current
|
|
34
|
+
* directory). Returns the exit code.
|
|
35
|
+
*/
|
|
36
|
+
export async function releaseCommand(args, cwd) {
|
|
37
|
+
const { values } = parseArgs({
|
|
38
|
+
args,
|
|
39
|
+
options: {
|
|
40
|
+
help: { type: "boolean", short: "h", default: false },
|
|
41
|
+
"dry-run": { type: "boolean", default: false },
|
|
42
|
+
provenance: { type: "boolean", default: false },
|
|
43
|
+
auto: { type: "boolean", default: false },
|
|
44
|
+
out: { type: "string" },
|
|
45
|
+
root: { type: "string" },
|
|
46
|
+
plugins: { type: "string", multiple: true },
|
|
47
|
+
marketplace: { type: "string" },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (values.help) {
|
|
51
|
+
console.log(`usage: ${RELEASE_USAGE}`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const root = values.root === undefined ? cwd : resolve(cwd, values.root);
|
|
55
|
+
const out = values.out === undefined ? join(root, "artifacts/release") : resolve(cwd, values.out);
|
|
56
|
+
const dryRun = values["dry-run"];
|
|
57
|
+
// The repository's marketplace is the default, so no workflow can forget the integrity gate.
|
|
58
|
+
const own = join(root, MARKETPLACE_FILE);
|
|
59
|
+
const marketplace = values.marketplace !== undefined
|
|
60
|
+
? resolve(cwd, values.marketplace)
|
|
61
|
+
: existsSync(own)
|
|
62
|
+
? own
|
|
63
|
+
: undefined;
|
|
64
|
+
const output = (published) => {
|
|
65
|
+
const ghOut = process.env.GITHUB_OUTPUT;
|
|
66
|
+
if (ghOut !== undefined && ghOut !== "")
|
|
67
|
+
appendFileSync(ghOut, `published=${published}\n`);
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
if (values.auto)
|
|
71
|
+
assertCommittedTree({ root, expectedHead: process.env.GITHUB_SHA });
|
|
72
|
+
output(0);
|
|
73
|
+
const r = await runPublish({
|
|
74
|
+
dryRun,
|
|
75
|
+
provenance: values.provenance,
|
|
76
|
+
out,
|
|
77
|
+
root,
|
|
78
|
+
refuseFirstRelease: values.auto,
|
|
79
|
+
refusePromotion: values.auto,
|
|
80
|
+
...(values.plugins !== undefined && { plugins: values.plugins }),
|
|
81
|
+
...(marketplace !== undefined && { marketplace }),
|
|
82
|
+
// Written after every publish, so a run that fails half-way still reports what it published.
|
|
83
|
+
onPublished: (n) => output(dryRun ? 0 : n),
|
|
84
|
+
});
|
|
85
|
+
const verb = dryRun ? "would publish" : "published";
|
|
86
|
+
console.log(`\n${verb} ${r.published.length}, skipped ${r.skipped.length} (already published)`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error(`release failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { RELEASE_USAGE, releaseCommand } from "./cli.js";
|
|
2
|
+
export { assertCommittedTree, discoverWorkspace, type Exec, fetchPublishedVersions, integrityOf, ReleaseIntegrityError, type RunPublishOptions, registryUrl, runPublish, selectPlugins, } from "./publish.js";
|
|
3
|
+
export { distTagFor, isFirstRelease, isPromotion, LIBRARY_ORDER, type PublishStep, packedManifestIssues, planPublish, promotions, publishedVersionsOf, type WorkspacePackage, } from "./publish-plan.js";
|
|
4
|
+
export { compareSemver, isPrerelease, parseSemver, SEMVER_RE } from "./semver.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { RELEASE_USAGE, releaseCommand } from "./cli.js";
|
|
2
|
+
export { assertCommittedTree, discoverWorkspace, fetchPublishedVersions, integrityOf, ReleaseIntegrityError, registryUrl, runPublish, selectPlugins, } from "./publish.js";
|
|
3
|
+
export { distTagFor, isFirstRelease, isPromotion, LIBRARY_ORDER, packedManifestIssues, planPublish, promotions, publishedVersionsOf, } from "./publish-plan.js";
|
|
4
|
+
export { compareSemver, isPrerelease, parseSemver, SEMVER_RE } from "./semver.js";
|