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.
- package/README.md +40 -0
- package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
- package/dist/_chunks/agents2.mjs +15 -1
- package/dist/_chunks/config.mjs +141 -0
- package/dist/_chunks/libs/@clack/core.mjs +63 -58
- package/dist/_chunks/libs/@clack/prompts.mjs +48 -45
- package/dist/_chunks/libs/@kwsites/file-exists.mjs +10 -9
- package/dist/_chunks/libs/simple-git.mjs +4 -2
- package/dist/_chunks/package-plugin.mjs +595 -0
- package/dist/_chunks/package-skill-identity.mjs +18 -0
- package/dist/_chunks/registry-client.mjs +535 -0
- package/dist/_chunks/rolldown-runtime.mjs +1 -1
- package/dist/_chunks/skills.mjs +6 -3
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +876 -1095
- package/dist/mcp-codesearch/main.d.mts +1 -1
- package/dist/mcp-codesearch/main.mjs +16 -23
- package/dist/mcp-db/main.d.mts +1 -1
- package/package.json +6 -2
- package/dist/THIRD-PARTY-LICENSES.md +0 -293
|
@@ -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));
|
package/dist/_chunks/skills.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {}
|