bfi-cli 0.1.89 → 0.1.91

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.
@@ -0,0 +1,535 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import https from "node:https";
5
+ import http from "node:http";
6
+ import { mkdir, writeFile } from "node:fs/promises";
7
+ import { gunzipSync } from "node:zlib";
8
+ import yauzl from "yauzl";
9
+ import tar from "tar-stream";
10
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
11
+ const digestPattern = /^[a-f0-9]{64}$/u;
12
+ const targets = /* @__PURE__ */ new Set([
13
+ "claude-code",
14
+ "codex",
15
+ "zcode",
16
+ "codebuddy",
17
+ "qwen-code"
18
+ ]);
19
+ function normalizeRegistry(registry) {
20
+ const url = new URL(registry);
21
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("Registry must be an HTTP(S) base URL without credentials, query or fragment");
22
+ return url.toString().replace(/\/+$/u, "");
23
+ }
24
+ function canonicalIdentity(identity) {
25
+ if (!identity || !identity.version || !targets.has(identity.agent)) throw new Error("Invalid plugin identity");
26
+ if (identity.scope !== "user" && identity.scope !== "project") throw new Error("Invalid plugin scope");
27
+ if (identity.scope === "project" && (!identity.projectPath || !isAbsolute(identity.projectPath))) throw new Error("Project scope requires a canonical absolute project path");
28
+ if (identity.scope === "user" && identity.projectPath !== null) throw new Error("User scope cannot contain a project path");
29
+ const source = identity.source;
30
+ let sourceKey;
31
+ if (source.kind === "registry") {
32
+ if (!/^[1-9][0-9]*$/u.test(source.packageId) || !/^@[^/\s]+\/[^/\s]+$/u.test(source.packageRef)) throw new Error("Registry source needs an exact package ID and namespace-qualified reference");
33
+ sourceKey = [
34
+ "registry",
35
+ normalizeRegistry(source.registry),
36
+ source.packageId,
37
+ source.packageRef
38
+ ];
39
+ } else if (source.kind === "local") {
40
+ if (!isAbsolute(source.sourcePath) || !digestPattern.test(source.sourceDigest)) throw new Error("Local source needs an absolute path and content digest");
41
+ sourceKey = [
42
+ "local",
43
+ resolve(source.sourcePath),
44
+ source.sourceDigest
45
+ ];
46
+ } else throw new Error("Unknown plugin source");
47
+ return [
48
+ sourceKey,
49
+ identity.agent,
50
+ identity.scope,
51
+ identity.projectPath ? resolve(identity.projectPath) : null
52
+ ];
53
+ }
54
+ function pluginSlotKey(identity) {
55
+ return hash(JSON.stringify(canonicalIdentity(identity)));
56
+ }
57
+ function pluginInstallKey(identity) {
58
+ return hash(JSON.stringify([...canonicalIdentity(identity), identity.version]));
59
+ }
60
+ var PluginInstallRecords = class {
61
+ root;
62
+ directory;
63
+ file;
64
+ constructor(root) {
65
+ this.root = realpathSync(root);
66
+ this.directory = join(this.root, ".bfi");
67
+ this.file = join(this.directory, "plugin-lock.json");
68
+ }
69
+ checkPaths() {
70
+ for (const candidate of [this.directory, this.file]) {
71
+ let info;
72
+ try {
73
+ info = lstatSync(candidate);
74
+ } catch (error) {
75
+ if (error.code === "ENOENT") continue;
76
+ throw error;
77
+ }
78
+ if (info.isSymbolicLink() || (candidate === this.directory ? !info.isDirectory() : !info.isFile())) throw new Error("Plugin store must not follow links or special files");
79
+ }
80
+ }
81
+ read() {
82
+ this.checkPaths();
83
+ if (!existsSync(this.file)) return {
84
+ version: 1,
85
+ active: {},
86
+ operations: {}
87
+ };
88
+ const value = JSON.parse(readFileSync(this.file, "utf8"));
89
+ if (value?.version !== 1 || !value.active || !value.operations || typeof value.active !== "object" || typeof value.operations !== "object" || Array.isArray(value.active) || Array.isArray(value.operations)) throw new Error("Invalid plugin lock; refusing to overwrite it");
90
+ for (const record of Object.values(value.operations)) {
91
+ pluginInstallKey(record.identity);
92
+ if (!digestPattern.test(record.artifactSha256) || !digestPattern.test(record.treeDigest) || !isAbsolute(record.artifactDirectory) || !["INSTALL", "UNINSTALL"].includes(record.action) || ![
93
+ "PREPARED",
94
+ "COMPLETED",
95
+ "FAILED",
96
+ "REMOVED"
97
+ ].includes(record.state)) throw new Error("Invalid plugin record");
98
+ }
99
+ for (const [slot, operationId] of Object.entries(value.active)) {
100
+ const record = value.operations[operationId];
101
+ if (!record || record.state !== "COMPLETED" || pluginSlotKey(record.identity) !== slot) throw new Error("Invalid active plugin record");
102
+ }
103
+ return value;
104
+ }
105
+ update(change) {
106
+ this.checkPaths();
107
+ mkdirSync(this.directory, {
108
+ recursive: true,
109
+ mode: 448
110
+ });
111
+ const guard = join(this.directory, "plugin-lock.guard");
112
+ try {
113
+ mkdirSync(guard, { mode: 448 });
114
+ } catch {
115
+ throw new Error("Plugin records are locked by another operation; inspect an interrupted operation before recovering its lock");
116
+ }
117
+ const temporary = join(this.directory, `plugin-lock.${randomUUID()}.tmp`);
118
+ try {
119
+ const lock = this.read();
120
+ const result = change(lock);
121
+ const descriptor = openSync(temporary, "wx", 384);
122
+ try {
123
+ writeFileSync(descriptor, JSON.stringify(lock, null, 2) + "\n");
124
+ fsyncSync(descriptor);
125
+ } finally {
126
+ closeSync(descriptor);
127
+ }
128
+ this.checkPaths();
129
+ renameSync(temporary, this.file);
130
+ const directoryDescriptor = openSync(this.directory, "r");
131
+ try {
132
+ fsyncSync(directoryDescriptor);
133
+ } finally {
134
+ closeSync(directoryDescriptor);
135
+ }
136
+ return result;
137
+ } finally {
138
+ if (existsSync(temporary)) unlinkSync(temporary);
139
+ rmdirSync(guard);
140
+ }
141
+ }
142
+ prepare(identity, artifactSha256, adapterVersion, artifact) {
143
+ pluginInstallKey(identity);
144
+ if (identity.scope === "project" && realpathSync(identity.projectPath) !== this.root) throw new Error("Plugin record belongs to a different project");
145
+ if (!digestPattern.test(artifactSha256) || !adapterVersion || !digestPattern.test(artifact.treeDigest) || !isAbsolute(artifact.artifactDirectory)) throw new Error("Verified artifact and adapter identity required");
146
+ return this.update((lock) => {
147
+ const slot = pluginSlotKey(identity);
148
+ if (Object.values(lock.operations).some((record) => record.state === "PREPARED" && pluginSlotKey(record.identity) === slot)) throw new Error("A prepared operation already owns this plugin slot; verify or recover it first");
149
+ const identifier = randomUUID();
150
+ const now = (/* @__PURE__ */ new Date()).toISOString();
151
+ lock.operations[identifier] = {
152
+ identity: structuredClone(identity),
153
+ artifactSha256,
154
+ adapterVersion,
155
+ ...artifact,
156
+ action: "INSTALL",
157
+ state: "PREPARED",
158
+ createdAt: now,
159
+ updatedAt: now
160
+ };
161
+ return identifier;
162
+ });
163
+ }
164
+ complete(operationId, receipt) {
165
+ if (!receipt.verified || !receipt.installationId) throw new Error("Native host verification is required");
166
+ this.update((lock) => {
167
+ const record = lock.operations[operationId];
168
+ if (!record || record.state !== "PREPARED" || record.action !== "INSTALL") throw new Error("Install operation is not prepared");
169
+ record.state = "COMPLETED";
170
+ record.installationId = receipt.installationId;
171
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
172
+ lock.active[pluginSlotKey(record.identity)] = operationId;
173
+ });
174
+ }
175
+ fail(operationId, errorCode) {
176
+ if (!/^[A-Z_]{1,80}$/u.test(errorCode)) throw new Error("Store only a safe failure code, not host output");
177
+ this.update((lock) => {
178
+ const record = lock.operations[operationId];
179
+ if (!record || record.state !== "PREPARED") throw new Error("Operation is not prepared");
180
+ record.state = "FAILED";
181
+ record.errorCode = errorCode;
182
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
183
+ });
184
+ }
185
+ prepareRemoval(operationId) {
186
+ return this.update((lock) => {
187
+ const record = lock.operations[operationId];
188
+ if (!record || record.state !== "COMPLETED" || lock.active[pluginSlotKey(record.identity)] !== operationId) throw new Error("Only the exact active installation can be removed");
189
+ const slot = pluginSlotKey(record.identity);
190
+ if (Object.values(lock.operations).some((item) => item.state === "PREPARED" && pluginSlotKey(item.identity) === slot)) throw new Error("A prepared operation already owns this plugin slot");
191
+ const identifier = randomUUID(), now = (/* @__PURE__ */ new Date()).toISOString();
192
+ lock.operations[identifier] = {
193
+ ...structuredClone(record),
194
+ action: "UNINSTALL",
195
+ targetOperationId: operationId,
196
+ state: "PREPARED",
197
+ createdAt: now,
198
+ updatedAt: now
199
+ };
200
+ return identifier;
201
+ });
202
+ }
203
+ removed(removalId) {
204
+ this.update((lock) => {
205
+ const removal = lock.operations[removalId];
206
+ if (!removal || removal.action !== "UNINSTALL" || removal.state !== "PREPARED" || !removal.targetOperationId) throw new Error("Removal is not prepared");
207
+ const record = lock.operations[removal.targetOperationId];
208
+ if (!record || lock.active[pluginSlotKey(record.identity)] !== removal.targetOperationId) throw new Error("Only the exact active installation can be removed");
209
+ record.state = "REMOVED";
210
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
211
+ removal.state = "REMOVED";
212
+ removal.updatedAt = record.updatedAt;
213
+ delete lock.active[pluginSlotKey(record.identity)];
214
+ });
215
+ }
216
+ };
217
+ const pluginArchiveLimits = {
218
+ compressedBytes: 50 * 1024 ** 2,
219
+ expandedBytes: 200 * 1024 ** 2,
220
+ fileBytes: 20 * 1024 ** 2,
221
+ entries: 1e4,
222
+ pathBytes: 1024
223
+ };
224
+ const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
225
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
226
+ const manifestPaths = {
227
+ ".claude-plugin/plugin.json": "CLAUDE",
228
+ ".codex-plugin/plugin.json": "CODEX",
229
+ ".zcode-plugin/plugin.json": "ZCODE",
230
+ ".cursor-plugin/plugin.json": "CURSOR",
231
+ ".codebuddy-plugin/plugin.json": "CODEBUDDY",
232
+ "qwen-extension.json": "QWEN"
233
+ };
234
+ function safeArchivePath(input, directory = false, maximum = pluginArchiveLimits.pathBytes) {
235
+ if (!input || input.startsWith("/") || input.includes("\\") || /[\u0000-\u001f\u007f-\u009f]/u.test(input)) throw new Error("Unsafe archive path");
236
+ const name = input.replace(/^(?:\.\/)+/u, "").replace(directory ? /\/$/u : /$^/u, "");
237
+ if (!name || Buffer.byteLength(name) > maximum || name.split("/").some((segment) => !segment || segment === "." || segment === ".." || segment.includes(":"))) throw new Error("Unsafe archive segment or path limit");
238
+ return name;
239
+ }
240
+ function checkCredentials(file) {
241
+ const lower = file.path.toLowerCase(), basename = lower.split("/").pop();
242
+ if (basename === ".env" || basename.startsWith(".env.") && ![
243
+ ".env.example",
244
+ ".env.sample",
245
+ ".env.template"
246
+ ].includes(basename) || [
247
+ "id_rsa",
248
+ "id_ed25519",
249
+ ".credentials.json",
250
+ "credentials"
251
+ ].includes(basename) || lower.startsWith(".git/") || lower.includes("/.git/")) throw new Error("Archive contains a credential or repository-private file");
252
+ const text = file.content.toString("utf8");
253
+ checkCredentialText(text);
254
+ if (lower.endsWith(".json")) {
255
+ let decoded;
256
+ try {
257
+ decoded = JSON.parse(text);
258
+ } catch {
259
+ return;
260
+ }
261
+ checkCredentialText(JSON.stringify(decoded));
262
+ }
263
+ }
264
+ function checkCredentialText(text) {
265
+ if (/AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/u.test(text)) throw new Error("Archive contains a credential literal");
266
+ for (const match of text.matchAll(/(?:api[_-]?key|access[_-]?key|secret(?:[_-]?key)?|password|token|authorization|x-pm-[as]k)["']?\s*[:=]\s*["']?(?:Bearer\s+)?([A-Za-z0-9_/-]{12,})/giu)) if (![
267
+ "YOUR_API_KEY",
268
+ "YOUR_ACCESS_TOKEN",
269
+ "YOUR_PASSWORD",
270
+ "REPLACE_ME",
271
+ "placeholder"
272
+ ].includes(match[1])) throw new Error("Archive contains a possible credential literal");
273
+ }
274
+ function treeDigest(files) {
275
+ const digest = createHash("sha256");
276
+ for (const file of [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) {
277
+ const path = Buffer.from(file.path), length = Buffer.alloc(4), size = Buffer.alloc(8);
278
+ length.writeUInt32BE(path.length);
279
+ size.writeBigUInt64BE(BigInt(file.content.length));
280
+ digest.update(length).update(path).update(Buffer.from(sha256(file.content), "hex")).update(size).update(Buffer.from([file.executable ? 1 : 0]));
281
+ }
282
+ return digest.digest("hex");
283
+ }
284
+ function describe(files) {
285
+ files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
286
+ const manifests = [];
287
+ for (const file of files) {
288
+ if (file.path !== "plugin.json" && !file.path.endsWith("/plugin.json") && file.path !== "qwen-extension.json" && !file.path.endsWith("/qwen-extension.json")) continue;
289
+ const nativePath = Object.keys(manifestPaths).find((candidate) => file.path === candidate || file.path.endsWith("/" + candidate));
290
+ const manifest = {
291
+ path: file.path,
292
+ root: file.path.slice(0, -(nativePath ?? "plugin.json").length).replace(/\/$/u, ""),
293
+ format: nativePath ? manifestPaths[nativePath] : "UNKNOWN",
294
+ invalid: false
295
+ };
296
+ try {
297
+ if (file.content.length > 1024 ** 2) throw new Error();
298
+ const parsed = JSON.parse(utf8.decode(file.content));
299
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
300
+ if (manifest.format === "UNKNOWN" && parsed.$schema === "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json") manifest.format = "AGENT_PLUGINS_V1";
301
+ manifest.name = typeof parsed.name === "string" ? parsed.name : void 0;
302
+ manifest.version = typeof parsed.version === "string" ? parsed.version : void 0;
303
+ } catch {
304
+ manifest.invalid = true;
305
+ }
306
+ manifests.push(manifest);
307
+ }
308
+ return {
309
+ files,
310
+ treeDigest: treeDigest(files),
311
+ manifests,
312
+ roots: manifests.length ? [...new Set(manifests.map((item) => item.root))].sort() : [""]
313
+ };
314
+ }
315
+ async function limitedRead(stream, maximum) {
316
+ const chunks = [];
317
+ let size = 0;
318
+ for await (const chunk of stream) {
319
+ if (!Buffer.isBuffer(chunk)) throw new Error("Invalid archive stream");
320
+ size += chunk.length;
321
+ if (size > maximum) throw new Error("Archive file limit exceeded");
322
+ chunks.push(chunk);
323
+ }
324
+ return Buffer.concat(chunks);
325
+ }
326
+ function crc32(bytes) {
327
+ let crc = 4294967295;
328
+ for (const byte of bytes) {
329
+ crc ^= byte;
330
+ for (let bit = 0; bit < 8; bit++) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
331
+ }
332
+ return (crc ^ 4294967295) >>> 0;
333
+ }
334
+ async function inspectPluginArchive(archive, filename, overrides = {}) {
335
+ const limits = {
336
+ ...pluginArchiveLimits,
337
+ ...overrides
338
+ };
339
+ if (Object.values(limits).some((value) => !Number.isSafeInteger(value) || value <= 0)) throw new Error("Invalid archive limits");
340
+ if (archive.length > limits.compressedBytes) throw new Error("Compressed archive limit exceeded");
341
+ const files = [], paths = /* @__PURE__ */ new Map();
342
+ let count = 0, expanded = 0;
343
+ const register = (raw, directory) => {
344
+ if (++count > limits.entries) throw new Error("Archive entry count exceeded");
345
+ const path = safeArchivePath(raw, directory, limits.pathBytes), key = path.normalize("NFC").toLowerCase();
346
+ if (paths.has(key)) throw new Error("Duplicate or case/unicode path collision");
347
+ for (const [known, knownDirectory] of paths) if (!knownDirectory && key.startsWith(known + "/") || !directory && known.startsWith(key + "/")) throw new Error("File/directory collision");
348
+ paths.set(key, directory);
349
+ return path;
350
+ };
351
+ const append = (file) => {
352
+ expanded += file.content.length;
353
+ if (expanded > limits.expandedBytes) throw new Error("Expanded archive limit exceeded");
354
+ checkCredentials(file);
355
+ files.push(file);
356
+ };
357
+ if (/\.zip$/iu.test(filename)) {
358
+ const zip = await new Promise((resolve, reject) => yauzl.fromBuffer(archive, {
359
+ lazyEntries: true,
360
+ decodeStrings: true,
361
+ strictFileNames: true,
362
+ validateEntrySizes: true
363
+ }, (error, result) => error ? reject(error) : resolve(result)));
364
+ try {
365
+ if (zip.entryCount > limits.entries) throw new Error("Archive entry count exceeded");
366
+ await new Promise((resolve, reject) => {
367
+ zip.on("error", reject).on("end", resolve).on("entry", (entry) => {
368
+ (async () => {
369
+ const raw = utf8.decode(entry.fileNameRaw), directory = entry.fileName.endsWith("/");
370
+ safeArchivePath(raw, directory, limits.pathBytes);
371
+ const path = register(entry.fileName, directory), mode = entry.externalFileAttributes >>> 16, kind = mode & 61440;
372
+ if (![
373
+ 0,
374
+ 32768,
375
+ 16384
376
+ ].includes(kind) || kind === 16384 && !directory || entry.isEncrypted() || ![0, 8].includes(entry.compressionMethod)) throw new Error("Unsupported or special ZIP entry");
377
+ if (!(await new Promise((resolveHeader, rejectHeader) => zip.readLocalFileHeader(entry, (error, value) => error ? rejectHeader(error) : resolveHeader(value)))).fileName.equals(entry.fileNameRaw)) throw new Error("Conflicting ZIP local path");
378
+ if (directory) {
379
+ if (entry.uncompressedSize) throw new Error("Nonempty ZIP directory");
380
+ zip.readEntry();
381
+ return;
382
+ }
383
+ if (entry.uncompressedSize > limits.fileBytes) throw new Error("Archive file limit exceeded");
384
+ const content = await limitedRead(await new Promise((resolveStream, rejectStream) => zip.openReadStream(entry, (error, value) => error ? rejectStream(error) : resolveStream(value))), Math.min(limits.fileBytes, limits.expandedBytes - expanded));
385
+ if (content.length !== entry.uncompressedSize || crc32(content) !== entry.crc32) throw new Error("ZIP size or checksum mismatch");
386
+ append({
387
+ path,
388
+ content,
389
+ executable: (mode & 73) !== 0
390
+ });
391
+ zip.readEntry();
392
+ })().catch(reject);
393
+ });
394
+ zip.readEntry();
395
+ });
396
+ } finally {
397
+ zip.close();
398
+ }
399
+ } else if (/\.(tgz|tar\.gz)$/iu.test(filename)) {
400
+ let bytes;
401
+ try {
402
+ bytes = gunzipSync(archive, { maxOutputLength: limits.expandedBytes });
403
+ } catch {
404
+ throw new Error("Invalid gzip or expanded archive limit exceeded");
405
+ }
406
+ const extractor = tar.extract();
407
+ await new Promise((resolve, reject) => {
408
+ extractor.on("error", reject).on("finish", resolve).on("entry", (header, stream, next) => {
409
+ (async () => {
410
+ const directory = header.type === "directory";
411
+ if (directory && [".", "./"].includes(header.name)) {
412
+ stream.resume();
413
+ next();
414
+ return;
415
+ }
416
+ if (!directory && header.type !== "file") throw new Error("Unsupported or special TAR entry");
417
+ if (header.pax && Object.keys(header.pax).some((key) => /sparse/iu.test(key))) throw new Error("Sparse TAR is unsupported");
418
+ const path = register(header.name, directory);
419
+ if ((header.size ?? 0) > limits.fileBytes) throw new Error("Archive file limit exceeded");
420
+ const content = await limitedRead(stream, limits.fileBytes);
421
+ if (content.length !== (header.size ?? 0) || directory && content.length) throw new Error("Invalid TAR size");
422
+ if (!directory) append({
423
+ path,
424
+ content,
425
+ executable: ((header.mode ?? 0) & 73) !== 0
426
+ });
427
+ next();
428
+ })().catch((error) => extractor.destroy(error));
429
+ });
430
+ extractor.end(bytes);
431
+ });
432
+ } else throw new Error("Expected zip or tgz plugin archive");
433
+ return describe(files);
434
+ }
435
+ function selectPluginRoot(tree, root) {
436
+ if (!tree.roots.includes(root)) throw new Error("Unknown plugin root");
437
+ return root ? describe(tree.files.filter((file) => file.path.startsWith(root + "/")).map((file) => ({
438
+ ...file,
439
+ path: file.path.slice(root.length + 1)
440
+ }))) : tree;
441
+ }
442
+ async function materializePluginTree(tree, freshDirectory) {
443
+ if (tree.treeDigest !== treeDigest(tree.files)) throw new Error("Plugin tree changed after inspection");
444
+ await mkdir(freshDirectory, { mode: 448 });
445
+ for (const file of tree.files) {
446
+ const path = join(freshDirectory, safeArchivePath(file.path));
447
+ await mkdir(dirname(path), {
448
+ recursive: true,
449
+ mode: 448
450
+ });
451
+ await writeFile(path, file.content, {
452
+ flag: "wx",
453
+ mode: file.executable ? 448 : 384
454
+ });
455
+ }
456
+ }
457
+ function segment(value) {
458
+ if (!value || value === "." || value === ".." || /[\s/\\\u0000-\u001f\u007f]/u.test(value)) throw new Error("Invalid package path segment");
459
+ return encodeURIComponent(value);
460
+ }
461
+ var PluginRegistryClient = class {
462
+ registry;
463
+ token;
464
+ constructor(registry, token = null) {
465
+ this.registry = normalizeRegistry(registry);
466
+ this.token = token;
467
+ }
468
+ base(namespace, slug) {
469
+ return `/namespaces/${segment(namespace)}/packages/${segment(slug)}`;
470
+ }
471
+ async bytes(path, maximum) {
472
+ if (!Number.isSafeInteger(maximum) || maximum <= 0) throw new Error("Invalid response limit");
473
+ const url = new URL(`${this.registry}/api/v1${path}`);
474
+ return new Promise((resolve, reject) => {
475
+ const request = (url.protocol === "https:" ? https : http).get(url, {
476
+ headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
477
+ rejectUnauthorized: true
478
+ }, (response) => {
479
+ if (response.statusCode !== 200) {
480
+ reject(/* @__PURE__ */ new Error(`Plugin registry HTTP ${response.statusCode ?? 0}`));
481
+ response.destroy();
482
+ return;
483
+ }
484
+ if (Number(response.headers["content-length"] ?? 0) > maximum) {
485
+ reject(/* @__PURE__ */ new Error("Plugin response limit exceeded"));
486
+ response.destroy();
487
+ return;
488
+ }
489
+ let size = 0;
490
+ const chunks = [];
491
+ response.on("data", (chunk) => {
492
+ size += chunk.length;
493
+ if (size > maximum) {
494
+ reject(/* @__PURE__ */ new Error("Plugin response limit exceeded"));
495
+ response.destroy();
496
+ return;
497
+ }
498
+ chunks.push(chunk);
499
+ });
500
+ response.on("end", () => resolve(Buffer.concat(chunks)));
501
+ response.on("error", reject);
502
+ response.on("aborted", () => reject(/* @__PURE__ */ new Error("Plugin response interrupted")));
503
+ });
504
+ const timeout = setTimeout(() => request.destroy(/* @__PURE__ */ new Error("Plugin request timed out")), 3e4);
505
+ request.on("close", () => clearTimeout(timeout));
506
+ request.on("error", reject);
507
+ });
508
+ }
509
+ async json(path) {
510
+ const bytes = await this.bytes(path, 8 * 1024 ** 2);
511
+ try {
512
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
513
+ if (!parsed || typeof parsed !== "object" || !Object.hasOwn(parsed, "data") || parsed.data == null) throw new Error();
514
+ return parsed.data;
515
+ } catch {
516
+ throw new Error("Invalid plugin registry response");
517
+ }
518
+ }
519
+ async packageId(namespace, slug) {
520
+ const value = await this.json(this.base(namespace, slug));
521
+ const identifier = typeof value.id === "string" ? value.id : Number.isSafeInteger(value.id) ? String(value.id) : "";
522
+ if (!/^[1-9][0-9]*$/u.test(identifier)) throw new Error("Package identity is not an exact ID");
523
+ return identifier;
524
+ }
525
+ async summary(namespace, slug) {
526
+ return this.json(`${this.base(namespace, slug)}/plugin`);
527
+ }
528
+ async release(namespace, slug, version) {
529
+ return this.json(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}`);
530
+ }
531
+ async download(namespace, slug, version, maximum = pluginArchiveLimits.compressedBytes) {
532
+ return this.bytes(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}/download`, maximum);
533
+ }
534
+ };
535
+ export { PluginInstallRecords, PluginRegistryClient, inspectPluginArchive, materializePluginTree, normalizeRegistry, pluginArchiveLimits, pluginInstallKey, pluginSlotKey, selectPluginRoot, sha256, treeDigest };
@@ -25,7 +25,7 @@ var __copyProps = (to, from, except, desc) => {
25
25
  }
26
26
  return to;
27
27
  };
28
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
29
29
  value: mod,
30
30
  enumerable: true
31
31
  }) : target, mod));
@@ -127,7 +127,8 @@ async function readSkillLock() {
127
127
  async function writeSkillLock(lock) {
128
128
  const lockPath = getSkillLockPath();
129
129
  await mkdir(dirname(lockPath), { recursive: true });
130
- await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
130
+ const content = JSON.stringify(lock, null, 2);
131
+ await writeFile(lockPath, content, "utf-8");
131
132
  }
132
133
  let _ghWarningShown = false;
133
134
  function getGitHubToken() {
@@ -220,7 +221,8 @@ async function writeLocalLock(lock, cwd) {
220
221
  version: lock.version,
221
222
  skills: sortedSkills
222
223
  };
223
- await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
224
+ const content = JSON.stringify(sorted, null, 2) + "\n";
225
+ await writeFile(lockPath, content, "utf-8");
224
226
  }
225
227
  async function computeSkillFolderHash(skillDir) {
226
228
  const files = [];
@@ -308,7 +310,8 @@ function shouldInstallInternalSkills() {
308
310
  }
309
311
  async function hasSkillMd(dir) {
310
312
  try {
311
- return (await stat(join(dir, "SKILL.md"))).isFile();
313
+ const skillPath = join(dir, "SKILL.md");
314
+ return (await stat(skillPath)).isFile();
312
315
  } catch {
313
316
  return false;
314
317
  }
package/dist/cli.d.mts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}