bfi-cli 0.1.89 → 0.1.92
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 +875 -1097
- package/dist/mcp-codesearch/main.d.mts +1 -1
- package/dist/mcp-codesearch/main.mjs +17 -24
- package/dist/mcp-db/main.d.mts +1 -1
- package/dist/mcp-db/main.mjs +1 -1
- package/package.json +6 -2
- package/dist/THIRD-PARTY-LICENSES.md +0 -293
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { PluginInstallRecords, PluginRegistryClient, inspectPluginArchive, materializePluginTree, normalizeRegistry, pluginArchiveLimits, pluginInstallKey, pluginSlotKey, selectPluginRoot, sha256, treeDigest } from "./registry-client.mjs";
|
|
2
|
+
import { resolveConfig } from "./config.mjs";
|
|
3
|
+
import { isPluginArchive, isPluginVersion, parsePluginArguments } from "../cli.mjs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { lstat, mkdir, open, realpath, writeFile } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
const adapters = Object.freeze({});
|
|
11
|
+
const reasons = {
|
|
12
|
+
"claude-code": ["PUBLIC_INSTALL_VERIFIED", "EXACT_RUNTIME_LOAD_AND_OWNERSHIP_UNVERIFIED"],
|
|
13
|
+
codex: ["PUBLIC_INSTALL_VERIFIED", "EXACT_RUNTIME_LOAD_AND_OWNERSHIP_UNVERIFIED"],
|
|
14
|
+
zcode: ["HOST_BINARY_UNAVAILABLE", "NATIVE_ADAPTER_UNVERIFIED"],
|
|
15
|
+
codebuddy: ["OFFICIAL_FIXTURE_UNVERIFIED", "HOST_BINARY_UNAVAILABLE"],
|
|
16
|
+
"qwen-code": ["HOST_BINARY_UNAVAILABLE", "NATIVE_ADAPTER_UNVERIFIED"]
|
|
17
|
+
};
|
|
18
|
+
function nativeAdapterFor(target) {
|
|
19
|
+
return adapters[target] ?? null;
|
|
20
|
+
}
|
|
21
|
+
function nativeSupportFor(target) {
|
|
22
|
+
return {
|
|
23
|
+
targetAgent: target,
|
|
24
|
+
status: "EXPORT_ONLY",
|
|
25
|
+
reasons: [...reasons[target]]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const nativeManifestFormats = {
|
|
29
|
+
"claude-code": ["CLAUDE"],
|
|
30
|
+
codex: [
|
|
31
|
+
"AGENT_PLUGINS_V1",
|
|
32
|
+
"CODEX",
|
|
33
|
+
"CLAUDE",
|
|
34
|
+
"CURSOR"
|
|
35
|
+
],
|
|
36
|
+
zcode: [
|
|
37
|
+
"ZCODE",
|
|
38
|
+
"CLAUDE",
|
|
39
|
+
"CODEX"
|
|
40
|
+
],
|
|
41
|
+
codebuddy: ["CODEBUDDY"],
|
|
42
|
+
"qwen-code": ["QWEN"]
|
|
43
|
+
};
|
|
44
|
+
const metadataFields = /* @__PURE__ */ new Set([
|
|
45
|
+
"$schema",
|
|
46
|
+
"name",
|
|
47
|
+
"version",
|
|
48
|
+
"description",
|
|
49
|
+
"author",
|
|
50
|
+
"homepage",
|
|
51
|
+
"repository",
|
|
52
|
+
"license",
|
|
53
|
+
"keywords",
|
|
54
|
+
"displayName",
|
|
55
|
+
"metadata",
|
|
56
|
+
"interface"
|
|
57
|
+
]);
|
|
58
|
+
const defaultComponents = /* @__PURE__ */ new Set([
|
|
59
|
+
"skills",
|
|
60
|
+
"commands",
|
|
61
|
+
"agents",
|
|
62
|
+
"hooks",
|
|
63
|
+
"output-styles",
|
|
64
|
+
"workflows",
|
|
65
|
+
"themes",
|
|
66
|
+
"monitors",
|
|
67
|
+
"bin"
|
|
68
|
+
]);
|
|
69
|
+
const object = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
70
|
+
function nativeRequirements(tree, target) {
|
|
71
|
+
const requirements = /* @__PURE__ */ new Set(["native-plugin"]);
|
|
72
|
+
const manifest = nativeManifestFormats[target].map((format) => tree.manifests.find((item) => item.root === "" && item.format === format)).find(Boolean);
|
|
73
|
+
if (!manifest || manifest.invalid || !manifest.name?.trim()) requirements.add("manifest:missing-or-invalid");
|
|
74
|
+
else requirements.add(`manifest:${manifest.format}`);
|
|
75
|
+
const files = new Map(tree.files.map((file) => [file.path, file]));
|
|
76
|
+
for (const file of tree.files) {
|
|
77
|
+
const root = file.path.split("/")[0];
|
|
78
|
+
if (defaultComponents.has(root)) requirements.add(`component:${root}`);
|
|
79
|
+
if (file.executable) requirements.add("executable-assets");
|
|
80
|
+
}
|
|
81
|
+
const configuration = (value, kind) => {
|
|
82
|
+
if (!object(value)) {
|
|
83
|
+
requirements.add(`configuration:${kind}:unknown`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (kind === "hooks") {
|
|
87
|
+
const events = object(value.hooks) ? value.hooks : value;
|
|
88
|
+
for (const [event, groups] of Object.entries(events)) {
|
|
89
|
+
if (event === "description") continue;
|
|
90
|
+
requirements.add(`hook-event:${event}`);
|
|
91
|
+
if (!Array.isArray(groups)) {
|
|
92
|
+
requirements.add("hook-handler:unknown");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const group of groups) {
|
|
96
|
+
if (!object(group) || !Array.isArray(group.hooks)) {
|
|
97
|
+
requirements.add("hook-handler:unknown");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const handler of group.hooks) requirements.add(`hook-handler:${object(handler) && typeof handler.type === "string" ? handler.type : "unknown"}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
const servers = object(value.mcpServers) ? value.mcpServers : value;
|
|
105
|
+
for (const server of Object.values(servers)) {
|
|
106
|
+
const transport = object(server) ? typeof server.type === "string" ? server.type : typeof server.command === "string" ? "stdio" : "unknown" : "unknown";
|
|
107
|
+
requirements.add(`mcp-transport:${transport}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const visited = /* @__PURE__ */ new Set();
|
|
112
|
+
const inspect = (value, kind) => {
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
value.forEach((item) => inspect(item, kind));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value !== "string") {
|
|
118
|
+
configuration(value, kind);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const path = value.replace(/^\.\//u, "");
|
|
122
|
+
if (visited.has(`${kind}:${path}`)) return;
|
|
123
|
+
visited.add(`${kind}:${path}`);
|
|
124
|
+
const file = files.get(path);
|
|
125
|
+
if (!file) {
|
|
126
|
+
requirements.add(`configuration:${kind}:external-or-missing`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
configuration(JSON.parse(file.content.toString("utf8")), kind);
|
|
131
|
+
} catch {
|
|
132
|
+
requirements.add(`configuration:${kind}:invalid`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
for (const [path, component] of [
|
|
136
|
+
[".mcp.json", "mcp"],
|
|
137
|
+
[".lsp.json", "lsp"],
|
|
138
|
+
["settings.json", "settings"],
|
|
139
|
+
[".app.json", "apps"]
|
|
140
|
+
]) if (files.has(path)) requirements.add(`component:${component}`);
|
|
141
|
+
if (files.has(".mcp.json")) inspect(".mcp.json", "mcp");
|
|
142
|
+
if (files.has("hooks/hooks.json")) inspect("hooks/hooks.json", "hooks");
|
|
143
|
+
if (manifest && files.has(manifest.path)) try {
|
|
144
|
+
const data = JSON.parse(files.get(manifest.path).content.toString("utf8"));
|
|
145
|
+
if (!object(data)) requirements.add("manifest:missing-or-invalid");
|
|
146
|
+
else {
|
|
147
|
+
for (const field of Object.keys(data)) if (!metadataFields.has(field)) requirements.add(`manifest-field:${field}`);
|
|
148
|
+
if (data.hooks !== void 0) inspect(data.hooks, "hooks");
|
|
149
|
+
if (data.mcpServers !== void 0) inspect(data.mcpServers, "mcp");
|
|
150
|
+
if (data.mcp_servers !== void 0) inspect(data.mcp_servers, "mcp");
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
requirements.add("manifest:missing-or-invalid");
|
|
154
|
+
}
|
|
155
|
+
return [...requirements].sort();
|
|
156
|
+
}
|
|
157
|
+
function meetsMinimum(version, minimum) {
|
|
158
|
+
const expression = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
159
|
+
const current = expression.exec(version), required = expression.exec(minimum);
|
|
160
|
+
if (!current || !required) return false;
|
|
161
|
+
for (const index of [
|
|
162
|
+
1,
|
|
163
|
+
2,
|
|
164
|
+
3
|
|
165
|
+
]) {
|
|
166
|
+
const difference = BigInt(current[index]) - BigInt(required[index]);
|
|
167
|
+
if (difference !== 0n) return difference > 0n;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
function createPluginInstallPlan(input) {
|
|
172
|
+
const { identity, release, adapter, host } = input;
|
|
173
|
+
const plan = {
|
|
174
|
+
identity: structuredClone(identity),
|
|
175
|
+
treeDigest: release.treeDigest,
|
|
176
|
+
artifactSha256: release.artifactSha256,
|
|
177
|
+
adapterVersion: adapter?.version ?? "",
|
|
178
|
+
hostVersion: host.version ?? "",
|
|
179
|
+
status: "BLOCKED",
|
|
180
|
+
reasons: [],
|
|
181
|
+
invocations: []
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
pluginInstallKey(identity);
|
|
185
|
+
} catch {
|
|
186
|
+
plan.reasons.push("INVALID_IDENTITY");
|
|
187
|
+
}
|
|
188
|
+
if (release.version !== identity.version) plan.reasons.push("VERSION_IDENTITY_MISMATCH");
|
|
189
|
+
if (release.state !== "PUBLISHED") plan.reasons.push("RELEASE_WITHDRAWN");
|
|
190
|
+
if (release.quarantined) plan.reasons.push("RELEASE_QUARANTINED");
|
|
191
|
+
if (!/^[a-f0-9]{64}$/u.test(release.treeDigest) || input.actualTreeDigest !== release.treeDigest) plan.reasons.push("TREE_DIGEST_MISMATCH");
|
|
192
|
+
if (!/^[a-f0-9]{64}$/u.test(release.artifactSha256) || input.actualArtifactSha256 !== release.artifactSha256) plan.reasons.push("ARCHIVE_DIGEST_MISMATCH");
|
|
193
|
+
if (!isAbsolute(input.artifactDirectory) || /[\u0000-\u001f\u007f]/u.test(input.artifactDirectory)) plan.reasons.push("INVALID_ARTIFACT_DIRECTORY");
|
|
194
|
+
if (plan.reasons.length) return plan;
|
|
195
|
+
if (!adapter) return {
|
|
196
|
+
...plan,
|
|
197
|
+
status: "EXPORT_ONLY",
|
|
198
|
+
reasons: ["NATIVE_ADAPTER_UNVERIFIED"]
|
|
199
|
+
};
|
|
200
|
+
if (adapter.targetAgent !== identity.agent || host.agent !== identity.agent) plan.reasons.push("HOST_IDENTITY_MISMATCH");
|
|
201
|
+
if (!host.version || !adapter.hostVersions.includes(host.version)) plan.reasons.push("HOST_VERSION_UNVERIFIED");
|
|
202
|
+
if (!adapter.scopes.includes(identity.scope)) plan.reasons.push("SCOPE_UNSUPPORTED");
|
|
203
|
+
for (const capability of new Set(input.requiredCapabilities)) if (!adapter.capabilities.includes(capability)) plan.reasons.push(`UNSUPPORTED_CAPABILITY:${capability}`);
|
|
204
|
+
const server = input.serverCapability;
|
|
205
|
+
if (server?.minCliVersion && !meetsMinimum(input.cliVersion, server.minCliVersion)) plan.reasons.push("CLI_UPGRADE_REQUIRED");
|
|
206
|
+
const matching = input.evidence.filter((item) => item.targetAgent === identity.agent && item.treeDigest === release.treeDigest && item.adapterVersion === adapter.version && item.hostVersion === host.version);
|
|
207
|
+
if (matching.some((item) => item.result === "FAIL" || item.result === "UNSUPPORTED")) plan.reasons.push("COMPATIBILITY_CHECK_FAILED");
|
|
208
|
+
if (plan.reasons.length) return plan;
|
|
209
|
+
if (identity.source.kind === "registry" && (!server?.installable || !server.minCliVersion || server.adapterVersion !== adapter.version)) return {
|
|
210
|
+
...plan,
|
|
211
|
+
status: "EXPORT_ONLY",
|
|
212
|
+
reasons: ["SERVER_NATIVE_CAPABILITY_UNVERIFIED"]
|
|
213
|
+
};
|
|
214
|
+
if (!matching.some((item) => ["LOAD", "FUNCTIONAL"].includes(item.level) && item.result === "PASS")) return {
|
|
215
|
+
...plan,
|
|
216
|
+
status: "EXPORT_ONLY",
|
|
217
|
+
reasons: ["EXACT_RUNTIME_EVIDENCE_REQUIRED"]
|
|
218
|
+
};
|
|
219
|
+
const invocations = adapter.plan({
|
|
220
|
+
identity: structuredClone(identity),
|
|
221
|
+
artifactDirectory: input.artifactDirectory
|
|
222
|
+
});
|
|
223
|
+
if (!invocations.length || invocations.some((invocation) => !invocation.command || [invocation.command, ...invocation.args].some((value) => typeof value !== "string" || value.includes("\0")))) return {
|
|
224
|
+
...plan,
|
|
225
|
+
reasons: ["INVALID_NATIVE_PLAN"]
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
...plan,
|
|
229
|
+
status: "READY",
|
|
230
|
+
invocations: structuredClone(invocations)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const runNative = (invocation, options) => new Promise((resolve, reject) => {
|
|
234
|
+
execFile(invocation.command, invocation.args, {
|
|
235
|
+
cwd: options.cwd,
|
|
236
|
+
shell: false,
|
|
237
|
+
timeout: 6e4,
|
|
238
|
+
maxBuffer: 1024 ** 2,
|
|
239
|
+
signal: options.signal,
|
|
240
|
+
windowsHide: true
|
|
241
|
+
}, (error, stdout, stderr) => {
|
|
242
|
+
if (error) reject(/* @__PURE__ */ new Error(error.name === "AbortError" ? "NATIVE_CANCELLED" : "NATIVE_COMMAND_FAILED"));
|
|
243
|
+
else resolve({
|
|
244
|
+
stdout,
|
|
245
|
+
stderr
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
async function installNativePlugin(request) {
|
|
250
|
+
const { input, archive, tree, records, adapter, runner } = request;
|
|
251
|
+
if (sha256(archive) !== input.release.artifactSha256 || archive.length !== input.release.artifactSize || treeDigest(tree.files) !== input.release.treeDigest) throw new Error("Plugin integrity changed before installation");
|
|
252
|
+
const plan = createPluginInstallPlan({
|
|
253
|
+
...input,
|
|
254
|
+
adapter,
|
|
255
|
+
actualArtifactSha256: sha256(archive),
|
|
256
|
+
actualTreeDigest: treeDigest(tree.files)
|
|
257
|
+
});
|
|
258
|
+
if (plan.status !== "READY") throw new Error(`${plan.status}: ${plan.reasons.join(", ")}`);
|
|
259
|
+
const previous = records.read(), activeId = previous.active[pluginSlotKey(plan.identity)];
|
|
260
|
+
if (activeId) {
|
|
261
|
+
const active = previous.operations[activeId];
|
|
262
|
+
if (active.adapterVersion !== adapter.version) throw new Error("Existing plugin requires its original adapter for safe update");
|
|
263
|
+
const verification = await adapter.verify(active, runner);
|
|
264
|
+
if (!verification.verified || verification.treeDigest !== active.treeDigest || verification.installationId !== active.installationId) throw new Error("Existing plugin ownership changed; refusing destructive update");
|
|
265
|
+
}
|
|
266
|
+
const identifier = records.prepare(plan.identity, plan.artifactSha256, adapter.version, {
|
|
267
|
+
treeDigest: plan.treeDigest,
|
|
268
|
+
artifactDirectory: input.artifactDirectory
|
|
269
|
+
});
|
|
270
|
+
let invoked = false;
|
|
271
|
+
try {
|
|
272
|
+
await materializePluginTree(tree, input.artifactDirectory);
|
|
273
|
+
invoked = true;
|
|
274
|
+
await adapter.install(plan, runner);
|
|
275
|
+
const verification = await adapter.verify(records.read().operations[identifier], runner);
|
|
276
|
+
if (!verification.verified || verification.treeDigest !== plan.treeDigest || !verification.installationId) throw new Error("Native verification failed");
|
|
277
|
+
records.complete(identifier, verification);
|
|
278
|
+
return identifier;
|
|
279
|
+
} catch {
|
|
280
|
+
if (!invoked) {
|
|
281
|
+
records.fail(identifier, "STAGING_FAILED");
|
|
282
|
+
throw new Error(`PLUGIN_STAGING_FAILED: ${identifier}`);
|
|
283
|
+
}
|
|
284
|
+
throw new Error(`NATIVE_OUTCOME_UNKNOWN: ${identifier}; PREPARED retained, inspect the native host before recovery`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function uninstallNativePlugin(request) {
|
|
288
|
+
const { operationId, expectedIdentity, records, adapter, runner } = request;
|
|
289
|
+
const record = records.read().operations[operationId];
|
|
290
|
+
if (!record || pluginInstallKey(record.identity) !== pluginInstallKey(expectedIdentity) || record.adapterVersion !== adapter.version || record.identity.agent !== adapter.targetAgent) throw new Error("Uninstall identity or adapter mismatch");
|
|
291
|
+
const removalId = records.prepareRemoval(operationId);
|
|
292
|
+
let invoked = false;
|
|
293
|
+
try {
|
|
294
|
+
const verification = await adapter.verify(record, runner);
|
|
295
|
+
if (!verification.verified || verification.installationId !== record.installationId || verification.treeDigest !== record.treeDigest) throw new Error("Plugin was modified outside bfi");
|
|
296
|
+
invoked = true;
|
|
297
|
+
await adapter.uninstall(record, runner);
|
|
298
|
+
if (!await adapter.verifyAbsent(record, runner)) throw new Error("Native removal is not verified");
|
|
299
|
+
records.removed(removalId);
|
|
300
|
+
} catch {
|
|
301
|
+
if (!invoked) {
|
|
302
|
+
records.fail(removalId, "OWNERSHIP_CHANGED");
|
|
303
|
+
throw new Error("Plugin ownership changed; user content retained");
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`NATIVE_OUTCOME_UNKNOWN: ${removalId}; active installation retained until native removal is verified`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const exactId = z.string().regex(/^[1-9][0-9]*$/u);
|
|
309
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
310
|
+
const releaseSchema = z.object({
|
|
311
|
+
id: exactId,
|
|
312
|
+
snapshotId: exactId,
|
|
313
|
+
version: z.string().refine(isPluginVersion),
|
|
314
|
+
treeDigest: digest,
|
|
315
|
+
artifactSha256: digest,
|
|
316
|
+
artifactSize: z.number().int().min(0).max(pluginArchiveLimits.compressedBytes),
|
|
317
|
+
state: z.enum(["PUBLISHED", "YANKED"]),
|
|
318
|
+
quarantined: z.boolean(),
|
|
319
|
+
distributionScope: z.enum(["PUBLIC", "PACKAGE_MEMBERS"]),
|
|
320
|
+
lifecycleRevision: z.string().regex(/^[0-9]+$/u)
|
|
321
|
+
});
|
|
322
|
+
const detailSchema = z.object({
|
|
323
|
+
release: releaseSchema,
|
|
324
|
+
files: z.array(z.object({
|
|
325
|
+
path: z.string(),
|
|
326
|
+
sha256: digest,
|
|
327
|
+
size: z.number().int().nonnegative(),
|
|
328
|
+
executable: z.boolean(),
|
|
329
|
+
mediaType: z.string()
|
|
330
|
+
})).max(pluginArchiveLimits.entries),
|
|
331
|
+
evidence: z.array(z.object({
|
|
332
|
+
treeDigest: digest,
|
|
333
|
+
targetAgent: z.string(),
|
|
334
|
+
hostVersion: z.string().nullable(),
|
|
335
|
+
adapterVersion: z.string(),
|
|
336
|
+
level: z.enum([
|
|
337
|
+
"UNVERIFIED",
|
|
338
|
+
"STATIC",
|
|
339
|
+
"LOAD",
|
|
340
|
+
"FUNCTIONAL"
|
|
341
|
+
]),
|
|
342
|
+
result: z.enum([
|
|
343
|
+
"PASS",
|
|
344
|
+
"FAIL",
|
|
345
|
+
"UNKNOWN",
|
|
346
|
+
"UNSUPPORTED"
|
|
347
|
+
])
|
|
348
|
+
}))
|
|
349
|
+
});
|
|
350
|
+
function checkedDetail(value) {
|
|
351
|
+
const result = detailSchema.safeParse(value);
|
|
352
|
+
if (!result.success) throw new Error("Invalid immutable plugin release response");
|
|
353
|
+
return {
|
|
354
|
+
...result.data,
|
|
355
|
+
evidence: result.data.evidence.map((item) => ({
|
|
356
|
+
...item,
|
|
357
|
+
hostVersion: item.hostVersion ?? "",
|
|
358
|
+
targetAgent: item.targetAgent
|
|
359
|
+
}))
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
async function readArchive(path) {
|
|
363
|
+
const descriptor = await open(path, "r");
|
|
364
|
+
try {
|
|
365
|
+
const info = await descriptor.stat();
|
|
366
|
+
if (!info.isFile() || info.size > pluginArchiveLimits.compressedBytes) throw new Error("Local plugin archive exceeds the file limit or is not a regular file");
|
|
367
|
+
const bytes = Buffer.alloc(Math.min(info.size + 1, pluginArchiveLimits.compressedBytes + 1));
|
|
368
|
+
let length = 0;
|
|
369
|
+
while (length < bytes.length) {
|
|
370
|
+
const result = await descriptor.read(bytes, length, bytes.length - length, null);
|
|
371
|
+
if (!result.bytesRead) break;
|
|
372
|
+
length += result.bytesRead;
|
|
373
|
+
}
|
|
374
|
+
if (length !== info.size) throw new Error("Local archive changed while reading");
|
|
375
|
+
return bytes.subarray(0, length);
|
|
376
|
+
} finally {
|
|
377
|
+
await descriptor.close();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function checkFiles(detail, tree) {
|
|
381
|
+
const actual = new Map(tree.files.map((file) => [file.path, file]));
|
|
382
|
+
if (detail.files.length !== actual.size || new Set(detail.files.map((file) => file.path)).size !== actual.size) throw new Error("Plugin file inventory integrity mismatch");
|
|
383
|
+
for (const expected of detail.files) {
|
|
384
|
+
const file = actual.get(expected.path);
|
|
385
|
+
if (!file || file.executable !== expected.executable || file.content.length !== expected.size || sha256(file.content) !== expected.sha256) throw new Error("Plugin file integrity mismatch");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function artifactParent(root) {
|
|
389
|
+
let current = root;
|
|
390
|
+
for (const part of [".bfi", "plugin-artifacts"]) {
|
|
391
|
+
current = join(current, part);
|
|
392
|
+
try {
|
|
393
|
+
await mkdir(current, { mode: 448 });
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error.code !== "EEXIST") throw error;
|
|
396
|
+
}
|
|
397
|
+
const info = await lstat(current);
|
|
398
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Plugin artifact store must not follow symbolic links");
|
|
399
|
+
}
|
|
400
|
+
return current;
|
|
401
|
+
}
|
|
402
|
+
function matchingOperations(records, options, root, registry) {
|
|
403
|
+
const lock = records.read();
|
|
404
|
+
return Object.values(lock.active).map((identifier) => ({
|
|
405
|
+
identifier,
|
|
406
|
+
record: lock.operations[identifier]
|
|
407
|
+
})).filter(({ identifier, record }) => {
|
|
408
|
+
const identity = record.identity;
|
|
409
|
+
if (identity.agent !== options.agent || identity.scope !== options.scope || identity.scope === "project" && identity.projectPath !== root || options.version && identity.version !== options.version) return false;
|
|
410
|
+
if (options.operationId && options.operationId !== identifier) return false;
|
|
411
|
+
if (!options.source) return Boolean(options.operationId);
|
|
412
|
+
return identity.source.kind === "registry" ? identity.source.registry === normalizeRegistry(registry) && identity.source.packageRef === options.source : identity.source.sourcePath === resolve(root, options.source);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async function runPluginPackage(args, globalOpts) {
|
|
416
|
+
const options = parsePluginArguments(args), cwd = await realpath(String(globalOpts.workdir ?? process.cwd()));
|
|
417
|
+
const storeRoot = options.scope === "user" ? await realpath(homedir()) : cwd;
|
|
418
|
+
const records = new PluginInstallRecords(storeRoot), config = resolveConfig(globalOpts);
|
|
419
|
+
const registry = new PluginRegistryClient(config.registry, config.token);
|
|
420
|
+
if (options.command === "list") {
|
|
421
|
+
const lock = records.read();
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
version: 1,
|
|
424
|
+
scope: options.scope,
|
|
425
|
+
active: lock.active,
|
|
426
|
+
operations: Object.fromEntries(Object.entries(lock.operations).filter(([, record]) => !options.agent || record.identity.agent === options.agent))
|
|
427
|
+
}, null, 2));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const agent = options.agent, adapter = nativeAdapterFor(agent);
|
|
431
|
+
if (options.command === "uninstall") {
|
|
432
|
+
const matches = matchingOperations(records, options, cwd, config.registry);
|
|
433
|
+
if (matches.length !== 1) throw new Error("No unique active plugin installation matches; use list --as plugin and --operation with exact scope/agent");
|
|
434
|
+
const selected = matches[0];
|
|
435
|
+
if (options.planOnly) {
|
|
436
|
+
console.log(JSON.stringify({
|
|
437
|
+
operationId: selected.identifier,
|
|
438
|
+
identity: selected.record.identity,
|
|
439
|
+
status: adapter ? "VERIFY_REQUIRED" : "EXPORT_ONLY",
|
|
440
|
+
reason: adapter ? "Native ownership verification required before removal" : "Original native adapter is unavailable; no files were removed"
|
|
441
|
+
}, null, 2));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!adapter || !adapter.hostVersions.includes(await adapter.detect(runNative, cwd) ?? "")) throw new Error("Original native adapter/host version unavailable; no files were removed");
|
|
445
|
+
await uninstallNativePlugin({
|
|
446
|
+
operationId: selected.identifier,
|
|
447
|
+
expectedIdentity: selected.record.identity,
|
|
448
|
+
records,
|
|
449
|
+
adapter,
|
|
450
|
+
runner: runNative
|
|
451
|
+
});
|
|
452
|
+
console.log(JSON.stringify({
|
|
453
|
+
removed: selected.identifier,
|
|
454
|
+
identity: selected.record.identity
|
|
455
|
+
}));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
let archive, tree, identity, detail;
|
|
459
|
+
let serverCapability, archiveName = "plugin.zip", root = "";
|
|
460
|
+
const source = options.source;
|
|
461
|
+
if (isPluginArchive(source) && !source.startsWith("@")) {
|
|
462
|
+
const sourcePath = await realpath(resolve(cwd, source));
|
|
463
|
+
archive = await readArchive(sourcePath);
|
|
464
|
+
archiveName = sourcePath;
|
|
465
|
+
const inspected = await inspectPluginArchive(archive, archiveName);
|
|
466
|
+
if (options.root === void 0 && inspected.roots.length !== 1) throw new Error(`Multiple plugin roots; choose --root explicitly: ${JSON.stringify(inspected.roots)}`);
|
|
467
|
+
root = options.root ?? inspected.roots[0];
|
|
468
|
+
tree = selectPluginRoot(inspected, root);
|
|
469
|
+
const manifest = nativeManifestFormats[agent].map((format) => tree.manifests.find((item) => item.root === "" && item.format === format)).find(Boolean);
|
|
470
|
+
if (manifest?.invalid) throw new Error("Invalid native plugin manifest");
|
|
471
|
+
const version = options.version ?? manifest?.version;
|
|
472
|
+
if (!version || !isPluginVersion(version)) throw new Error("Local plugin needs an exact manifest version or explicit --version; source files are never rewritten");
|
|
473
|
+
if (manifest?.version && manifest.version !== version) throw new Error("Local manifest version conflicts with --version");
|
|
474
|
+
const hash = sha256(archive);
|
|
475
|
+
identity = {
|
|
476
|
+
source: {
|
|
477
|
+
kind: "local",
|
|
478
|
+
sourcePath,
|
|
479
|
+
sourceDigest: hash
|
|
480
|
+
},
|
|
481
|
+
version,
|
|
482
|
+
agent,
|
|
483
|
+
scope: options.scope,
|
|
484
|
+
projectPath: options.scope === "project" ? cwd : null
|
|
485
|
+
};
|
|
486
|
+
detail = {
|
|
487
|
+
release: {
|
|
488
|
+
id: `local:${hash}`,
|
|
489
|
+
snapshotId: `local:${tree.treeDigest}`,
|
|
490
|
+
version,
|
|
491
|
+
treeDigest: tree.treeDigest,
|
|
492
|
+
artifactSha256: hash,
|
|
493
|
+
artifactSize: archive.length,
|
|
494
|
+
state: "PUBLISHED",
|
|
495
|
+
quarantined: false,
|
|
496
|
+
distributionScope: "PUBLIC",
|
|
497
|
+
lifecycleRevision: "0"
|
|
498
|
+
},
|
|
499
|
+
files: [],
|
|
500
|
+
evidence: []
|
|
501
|
+
};
|
|
502
|
+
} else {
|
|
503
|
+
const [namespace, slug] = source.slice(1).split("/");
|
|
504
|
+
const packageId = await registry.packageId(namespace, slug), summary = await registry.summary(namespace, slug);
|
|
505
|
+
if (!summary?.enabled) throw new Error("Plugin capability is not enabled for this package");
|
|
506
|
+
const version = options.version ?? summary.stable?.version;
|
|
507
|
+
if (!version || !isPluginVersion(version)) throw new Error("No visible stable plugin version; specify an exact --version, never latest");
|
|
508
|
+
detail = checkedDetail(await registry.release(namespace, slug, version));
|
|
509
|
+
if (detail.release.version !== version || detail.release.state !== "PUBLISHED" || detail.release.quarantined) throw new Error("Requested plugin release is not distributable");
|
|
510
|
+
identity = {
|
|
511
|
+
source: {
|
|
512
|
+
kind: "registry",
|
|
513
|
+
registry: registry.registry,
|
|
514
|
+
packageId,
|
|
515
|
+
packageRef: source
|
|
516
|
+
},
|
|
517
|
+
version,
|
|
518
|
+
agent,
|
|
519
|
+
scope: options.scope,
|
|
520
|
+
projectPath: options.scope === "project" ? cwd : null
|
|
521
|
+
};
|
|
522
|
+
serverCapability = summary.installCapabilities?.version === "1" ? summary.installCapabilities.targets.find((item) => item.targetAgent === agent && item.path === "native") : void 0;
|
|
523
|
+
archive = await registry.download(namespace, slug, version);
|
|
524
|
+
if (sha256(archive) !== detail.release.artifactSha256 || archive.length !== detail.release.artifactSize) throw new Error("Plugin archive digest/size integrity mismatch");
|
|
525
|
+
tree = await inspectPluginArchive(archive, archiveName);
|
|
526
|
+
if (tree.treeDigest !== detail.release.treeDigest) throw new Error("Plugin tree digest integrity mismatch");
|
|
527
|
+
checkFiles(detail, tree);
|
|
528
|
+
}
|
|
529
|
+
if (options.command === "update" && !records.read().active[pluginSlotKey(identity)]) throw new Error("This exact source/agent/scope is not installed; local changed-content archives are separate identities");
|
|
530
|
+
const artifactDirectory = join(storeRoot, ".bfi", "plugin-artifacts", `${pluginInstallKey(identity)}-${randomUUID()}`);
|
|
531
|
+
const input = {
|
|
532
|
+
identity,
|
|
533
|
+
release: detail.release,
|
|
534
|
+
actualTreeDigest: tree.treeDigest,
|
|
535
|
+
actualArtifactSha256: sha256(archive),
|
|
536
|
+
requiredCapabilities: nativeRequirements(tree, agent),
|
|
537
|
+
evidence: detail.evidence,
|
|
538
|
+
host: {
|
|
539
|
+
agent,
|
|
540
|
+
version: adapter ? await adapter.detect(runNative, cwd) : null
|
|
541
|
+
},
|
|
542
|
+
cliVersion: String(globalOpts.version ?? ""),
|
|
543
|
+
serverCapability,
|
|
544
|
+
artifactDirectory,
|
|
545
|
+
adapter
|
|
546
|
+
};
|
|
547
|
+
const plan = createPluginInstallPlan(input);
|
|
548
|
+
if (options.planOnly) {
|
|
549
|
+
console.log(JSON.stringify({
|
|
550
|
+
...plan,
|
|
551
|
+
archiveRoot: root,
|
|
552
|
+
requiredCapabilities: input.requiredCapabilities,
|
|
553
|
+
nativeSupport: nativeSupportFor(agent)
|
|
554
|
+
}, null, 2));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (options.exportPath) {
|
|
558
|
+
const target = resolve(cwd, options.exportPath);
|
|
559
|
+
if (/\.zip$/iu.test(archiveName) !== /\.zip$/iu.test(target) || !isPluginArchive(target)) throw new Error("Export extension must match original archive format");
|
|
560
|
+
await writeFile(target, archive, {
|
|
561
|
+
flag: "wx",
|
|
562
|
+
mode: 384
|
|
563
|
+
});
|
|
564
|
+
console.log(JSON.stringify({
|
|
565
|
+
exported: target,
|
|
566
|
+
archiveSha256: sha256(archive),
|
|
567
|
+
treeDigest: tree.treeDigest,
|
|
568
|
+
archiveRoot: root,
|
|
569
|
+
originalArchive: true,
|
|
570
|
+
installed: false
|
|
571
|
+
}, null, 2));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (plan.status !== "READY" || !adapter) throw new Error(`${plan.status}: ${plan.reasons.join(", ")}; use --plan or --export <archive>, no skills-only fallback occurred`);
|
|
575
|
+
if (identity.source.kind === "registry") {
|
|
576
|
+
const [namespace, slug] = identity.source.packageRef.slice(1).split("/");
|
|
577
|
+
const current = checkedDetail(await registry.release(namespace, slug, identity.version));
|
|
578
|
+
if (await registry.packageId(namespace, slug) !== identity.source.packageId || current.release.id !== detail.release.id || current.release.treeDigest !== tree.treeDigest || current.release.state !== "PUBLISHED" || current.release.quarantined) throw new Error("Plugin distribution identity or authority changed before installation");
|
|
579
|
+
}
|
|
580
|
+
await artifactParent(storeRoot);
|
|
581
|
+
const operationId = await installNativePlugin({
|
|
582
|
+
input,
|
|
583
|
+
archive,
|
|
584
|
+
tree,
|
|
585
|
+
records,
|
|
586
|
+
adapter,
|
|
587
|
+
runner: runNative
|
|
588
|
+
});
|
|
589
|
+
console.log(JSON.stringify({
|
|
590
|
+
operationId,
|
|
591
|
+
identity,
|
|
592
|
+
verified: true
|
|
593
|
+
}, null, 2));
|
|
594
|
+
}
|
|
595
|
+
export { runPluginPackage };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { __exportAll } from "./rolldown-runtime.mjs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
var package_skill_identity_exports = /* @__PURE__ */ __exportAll({
|
|
4
|
+
packageSkillName: () => packageSkillName,
|
|
5
|
+
scopePackageSkill: () => scopePackageSkill
|
|
6
|
+
});
|
|
7
|
+
function packageSkillName(namespace, slug) {
|
|
8
|
+
if (!namespace || !slug) throw new Error("Namespace and slug are required");
|
|
9
|
+
return `bfi-skill-${createHash("sha256").update(JSON.stringify([namespace, slug])).digest("hex")}`;
|
|
10
|
+
}
|
|
11
|
+
function scopePackageSkill(skills, namespace, slug) {
|
|
12
|
+
if (skills.length !== 1) throw new Error("A package reference must resolve to exactly one skill");
|
|
13
|
+
return [{
|
|
14
|
+
...skills[0],
|
|
15
|
+
name: packageSkillName(namespace, slug)
|
|
16
|
+
}];
|
|
17
|
+
}
|
|
18
|
+
export { packageSkillName, package_skill_identity_exports };
|