bfi-cli 0.1.99 → 0.1.101
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 +117 -5
- package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
- package/dist/_chunks/agents.mjs +1 -1
- package/dist/_chunks/agents2.mjs +4 -1
- package/dist/_chunks/client.mjs +107 -0
- package/dist/_chunks/command.mjs +697 -0
- package/dist/_chunks/digest.mjs +132 -0
- package/dist/_chunks/files.mjs +89 -0
- package/dist/_chunks/{registry-client.mjs → intent.mjs} +163 -13
- package/dist/_chunks/libs/@clack/core.mjs +63 -58
- package/dist/_chunks/libs/@clack/prompts.mjs +46 -45
- package/dist/_chunks/libs/@kwsites/file-exists.mjs +10 -9
- package/dist/_chunks/libs/simple-git.mjs +4 -2
- package/dist/_chunks/manager.mjs +436 -0
- package/dist/_chunks/mcp.mjs +559 -0
- package/dist/_chunks/model.mjs +124 -0
- package/dist/_chunks/ownership.mjs +7 -0
- package/dist/_chunks/package-plugin.mjs +473 -36
- package/dist/_chunks/receipt.mjs +166 -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 +254 -221
- package/dist/mcp-codesearch/main.d.mts +1 -1
- package/dist/mcp-db/main.d.mts +1 -1
- package/dist/mcp-weknora/main.d.mts +1 -1
- package/package.json +4 -2
- package/dist/THIRD-PARTY-LICENSES.md +0 -293
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isPluginArchive, isPluginVersion, parsePluginArguments } from "
|
|
1
|
+
import { isCancel } from "./libs/@clack/core.mjs";
|
|
2
|
+
import { confirm, select } from "./libs/@clack/prompts.mjs";
|
|
3
|
+
import { PluginInstallRecords, PluginRegistryClient, inspectPluginArchive, isPluginArchive, isPluginVersion, materializePluginTree, normalizeRegistry, parsePluginArguments, pluginArchiveLimits, pluginInstallKey, pluginSlotKey, pluginTargets, selectPluginRoot, sha256, treeDigest } from "./intent.mjs";
|
|
4
|
+
import { loadGlobalConfig, loadProjectConfig, resolveConfig } from "./config.mjs";
|
|
5
|
+
import { credentialForRegistry } from "./client.mjs";
|
|
4
6
|
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 { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, writeFile } from "node:fs/promises";
|
|
7
9
|
import { homedir } from "node:os";
|
|
8
10
|
import { z } from "zod";
|
|
9
11
|
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
12
|
const nativeManifestFormats = {
|
|
29
13
|
"claude-code": ["CLAUDE"],
|
|
30
14
|
codex: [
|
|
@@ -75,7 +59,8 @@ function nativeRequirements(tree, target) {
|
|
|
75
59
|
const files = new Map(tree.files.map((file) => [file.path, file]));
|
|
76
60
|
for (const file of tree.files) {
|
|
77
61
|
const root = file.path.split("/")[0];
|
|
78
|
-
if (
|
|
62
|
+
if (target === "codex" && file.path === "agents/openai.yaml") requirements.add("component:agent-metadata");
|
|
63
|
+
else if (defaultComponents.has(root)) requirements.add(`component:${root}`);
|
|
79
64
|
if (file.executable) requirements.add("executable-assets");
|
|
80
65
|
}
|
|
81
66
|
const configuration = (value, kind) => {
|
|
@@ -154,6 +139,367 @@ function nativeRequirements(tree, target) {
|
|
|
154
139
|
}
|
|
155
140
|
return [...requirements].sort();
|
|
156
141
|
}
|
|
142
|
+
const nativeNamePattern = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
143
|
+
const cwdFor = (identity) => identity.projectPath ?? homedir();
|
|
144
|
+
const marketName = (identity, digest, directory) => `bfi-${pluginInstallKey(identity).slice(0, 20)}-${digest.slice(0, 12)}-${sha256(Buffer.from(directory)).slice(0, 12)}`;
|
|
145
|
+
async function nativeDirectoryDigest(directory) {
|
|
146
|
+
const files = [];
|
|
147
|
+
let bytes = 0, entries = 0;
|
|
148
|
+
const walk = async (relative) => {
|
|
149
|
+
if (++entries > pluginArchiveLimits.entries) throw new Error("NATIVE_TREE_LIMIT");
|
|
150
|
+
const path = join(directory, relative), info = await lstat(path);
|
|
151
|
+
if (info.isSymbolicLink()) throw new Error("NATIVE_TREE_LINK");
|
|
152
|
+
if (info.isDirectory()) for (const name of await readdir(path)) await walk(relative ? `${relative}/${name}` : name);
|
|
153
|
+
else {
|
|
154
|
+
if (!info.isFile() || info.size > pluginArchiveLimits.fileBytes || (bytes += info.size) > pluginArchiveLimits.expandedBytes) throw new Error("NATIVE_TREE_LIMIT");
|
|
155
|
+
files.push({
|
|
156
|
+
path: relative,
|
|
157
|
+
content: await readFile(path),
|
|
158
|
+
executable: (info.mode & 73) !== 0
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
await walk("");
|
|
163
|
+
return treeDigest(files);
|
|
164
|
+
}
|
|
165
|
+
async function nativeList(runner, cwd, markets = false) {
|
|
166
|
+
const { stdout } = await runner({
|
|
167
|
+
command: "claude",
|
|
168
|
+
args: [
|
|
169
|
+
"plugin",
|
|
170
|
+
...markets ? ["marketplace"] : [],
|
|
171
|
+
"list",
|
|
172
|
+
"--json"
|
|
173
|
+
]
|
|
174
|
+
}, { cwd });
|
|
175
|
+
const result = JSON.parse(stdout);
|
|
176
|
+
if (!Array.isArray(result) || result.some((item) => !item || typeof item !== "object" || Array.isArray(item))) throw new Error("NATIVE_LIST_INVALID");
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function contextValues(context) {
|
|
180
|
+
if (!context.nativeName || !nativeNamePattern.test(context.nativeName) || !/^[a-f0-9]{64}$/u.test(context.treeDigest ?? "") || basename(context.artifactDirectory) !== "plugin") throw new Error("NATIVE_CONTEXT_INVALID");
|
|
181
|
+
const market = marketName(context.identity, context.treeDigest, context.artifactDirectory);
|
|
182
|
+
return {
|
|
183
|
+
market,
|
|
184
|
+
selector: `${context.nativeName}@${market}`,
|
|
185
|
+
directory: dirname(context.artifactDirectory),
|
|
186
|
+
manifest: {
|
|
187
|
+
name: market,
|
|
188
|
+
owner: { name: "bfi" },
|
|
189
|
+
plugins: [{
|
|
190
|
+
name: context.nativeName,
|
|
191
|
+
version: context.identity.version,
|
|
192
|
+
source: "./plugin"
|
|
193
|
+
}]
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function recordValues(record) {
|
|
198
|
+
if (await nativeDirectoryDigest(record.artifactDirectory) !== record.treeDigest) throw new Error("NATIVE_CONTENT_CHANGED");
|
|
199
|
+
const manifest = JSON.parse(await readFile(join(record.artifactDirectory, ".claude-plugin/plugin.json"), "utf8"));
|
|
200
|
+
return contextValues({
|
|
201
|
+
...record,
|
|
202
|
+
nativeName: manifest.name
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const claudeAdapter = {
|
|
206
|
+
targetAgent: "claude-code",
|
|
207
|
+
version: "claude-public-v1",
|
|
208
|
+
hostVersions: ["2.1.251"],
|
|
209
|
+
scopes: ["user", "project"],
|
|
210
|
+
capabilities: [
|
|
211
|
+
"native-plugin",
|
|
212
|
+
"manifest:CLAUDE",
|
|
213
|
+
"component:skills",
|
|
214
|
+
"component:commands",
|
|
215
|
+
"component:agents",
|
|
216
|
+
"component:hooks",
|
|
217
|
+
"executable-assets",
|
|
218
|
+
"manifest-field:hooks",
|
|
219
|
+
"hook-handler:command",
|
|
220
|
+
"hook-event:PreToolUse",
|
|
221
|
+
"hook-event:PostToolUse",
|
|
222
|
+
"hook-event:Stop",
|
|
223
|
+
"hook-event:UserPromptSubmit"
|
|
224
|
+
],
|
|
225
|
+
async detect(runner, cwd) {
|
|
226
|
+
try {
|
|
227
|
+
return /^(\d+\.\d+\.\d+) \(Claude Code\)\s*$/u.exec((await runner({
|
|
228
|
+
command: "claude",
|
|
229
|
+
args: ["--version"]
|
|
230
|
+
}, { cwd })).stdout)?.[1] ?? null;
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
plan(context) {
|
|
236
|
+
const values = contextValues(context);
|
|
237
|
+
return [{
|
|
238
|
+
command: "claude",
|
|
239
|
+
args: [
|
|
240
|
+
"plugin",
|
|
241
|
+
"marketplace",
|
|
242
|
+
"add",
|
|
243
|
+
values.directory,
|
|
244
|
+
"--scope",
|
|
245
|
+
context.identity.scope
|
|
246
|
+
]
|
|
247
|
+
}, {
|
|
248
|
+
command: "claude",
|
|
249
|
+
args: [
|
|
250
|
+
"plugin",
|
|
251
|
+
"install",
|
|
252
|
+
values.selector,
|
|
253
|
+
"--scope",
|
|
254
|
+
context.identity.scope
|
|
255
|
+
]
|
|
256
|
+
}];
|
|
257
|
+
},
|
|
258
|
+
async preflight(plan, runner) {
|
|
259
|
+
if (!plan.artifactDirectory) throw new Error("NATIVE_CONTEXT_INVALID");
|
|
260
|
+
const values = contextValues({
|
|
261
|
+
...plan,
|
|
262
|
+
artifactDirectory: plan.artifactDirectory
|
|
263
|
+
});
|
|
264
|
+
const cwd = cwdFor(plan.identity);
|
|
265
|
+
if ((await nativeList(runner, cwd)).some((item) => typeof item.id !== "string" || item.id.split("@")[0] === plan.nativeName)) throw new Error("NATIVE_NAME_CONFLICT");
|
|
266
|
+
if ((await nativeList(runner, cwd, true)).some((item) => item.name === values.market)) throw new Error("NATIVE_MARKET_CONFLICT");
|
|
267
|
+
},
|
|
268
|
+
async install(plan, runner) {
|
|
269
|
+
if (!plan.artifactDirectory) throw new Error("NATIVE_CONTEXT_INVALID");
|
|
270
|
+
const values = contextValues({
|
|
271
|
+
...plan,
|
|
272
|
+
artifactDirectory: plan.artifactDirectory
|
|
273
|
+
});
|
|
274
|
+
const cwd = cwdFor(plan.identity);
|
|
275
|
+
await this.preflight(plan, runner);
|
|
276
|
+
if (await nativeDirectoryDigest(plan.artifactDirectory) !== plan.treeDigest) throw new Error("NATIVE_CONTENT_CHANGED");
|
|
277
|
+
const nativeManifest = JSON.parse(await readFile(join(plan.artifactDirectory, ".claude-plugin/plugin.json"), "utf8"));
|
|
278
|
+
if (nativeManifest.name !== plan.nativeName || nativeManifest.version && nativeManifest.version !== plan.identity.version) throw new Error("NATIVE_IDENTITY_CHANGED");
|
|
279
|
+
await mkdir(join(values.directory, ".claude-plugin"), { mode: 448 });
|
|
280
|
+
await writeFile(join(values.directory, ".claude-plugin/marketplace.json"), JSON.stringify(values.manifest, null, 2) + "\n", {
|
|
281
|
+
flag: "wx",
|
|
282
|
+
mode: 384
|
|
283
|
+
});
|
|
284
|
+
for (const invocation of this.plan({
|
|
285
|
+
...plan,
|
|
286
|
+
artifactDirectory: plan.artifactDirectory
|
|
287
|
+
})) await runner(invocation, { cwd });
|
|
288
|
+
},
|
|
289
|
+
async verify(record, runner) {
|
|
290
|
+
try {
|
|
291
|
+
const values = await recordValues(record), cwd = cwdFor(record.identity);
|
|
292
|
+
const index = JSON.parse(await readFile(join(values.directory, ".claude-plugin/marketplace.json"), "utf8"));
|
|
293
|
+
if (JSON.stringify(index) !== JSON.stringify(values.manifest)) throw new Error("NATIVE_INDEX_CHANGED");
|
|
294
|
+
const market = (await nativeList(runner, cwd, true)).filter((item) => item.name === values.market);
|
|
295
|
+
if (market.length !== 1 || market[0].source !== "directory" || typeof market[0].path !== "string" || await realpath(market[0].path) !== await realpath(values.directory)) throw new Error("NATIVE_MARKET_CHANGED");
|
|
296
|
+
const entries = (await nativeList(runner, cwd)).filter((item) => item.id === values.selector && item.scope === record.identity.scope && (record.identity.scope === "user" || item.projectPath === record.identity.projectPath));
|
|
297
|
+
const entry = entries[0];
|
|
298
|
+
return {
|
|
299
|
+
verified: entries.length === 1 && entry?.enabled === true && entry.version === record.identity.version && typeof entry.installPath === "string" && await nativeDirectoryDigest(entry.installPath) === record.treeDigest,
|
|
300
|
+
installationId: values.selector,
|
|
301
|
+
treeDigest: record.treeDigest
|
|
302
|
+
};
|
|
303
|
+
} catch {
|
|
304
|
+
return {
|
|
305
|
+
verified: false,
|
|
306
|
+
installationId: "",
|
|
307
|
+
treeDigest: ""
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
async uninstall(record, runner) {
|
|
312
|
+
const values = await recordValues(record), cwd = cwdFor(record.identity);
|
|
313
|
+
await runner({
|
|
314
|
+
command: "claude",
|
|
315
|
+
args: [
|
|
316
|
+
"plugin",
|
|
317
|
+
"uninstall",
|
|
318
|
+
values.selector,
|
|
319
|
+
"--scope",
|
|
320
|
+
record.identity.scope,
|
|
321
|
+
"--keep-data"
|
|
322
|
+
]
|
|
323
|
+
}, { cwd });
|
|
324
|
+
},
|
|
325
|
+
async verifyAbsent(record, runner) {
|
|
326
|
+
const values = await recordValues(record);
|
|
327
|
+
return !(await nativeList(runner, cwdFor(record.identity))).some((item) => item.id === values.selector && item.scope === record.identity.scope && (record.identity.scope === "user" || item.projectPath === record.identity.projectPath));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const codexRequirements = [
|
|
331
|
+
"native-plugin",
|
|
332
|
+
"manifest:CODEX",
|
|
333
|
+
"manifest-field:skills",
|
|
334
|
+
"component:skills",
|
|
335
|
+
"component:agent-metadata",
|
|
336
|
+
"executable-assets"
|
|
337
|
+
];
|
|
338
|
+
async function prepareNativeHandoff(input) {
|
|
339
|
+
const { tree, target, scope, hostVersion, version } = input;
|
|
340
|
+
if (target !== "claude-code" && target !== "codex") throw new Error("NATIVE_HOST_UNVERIFIED");
|
|
341
|
+
if (hostVersion !== (target === "claude-code" ? "2.1.251" : "0.136.0")) throw new Error("HOST_VERSION_UNVERIFIED");
|
|
342
|
+
if (target === "codex" && scope !== "user") throw new Error("SCOPE_UNSUPPORTED");
|
|
343
|
+
const supported = target === "claude-code" ? claudeAdapter.capabilities : codexRequirements;
|
|
344
|
+
const missing = nativeRequirements(tree, target).filter((requirement) => !supported.includes(requirement));
|
|
345
|
+
if (missing.length) throw new Error(`NATIVE_COMPONENTS_UNSUPPORTED: ${missing.join(", ")}; original files retained; no skills fallback`);
|
|
346
|
+
const manifest = tree.manifests.find((item) => item.root === "" && item.format === (target === "claude-code" ? "CLAUDE" : "CODEX"));
|
|
347
|
+
if (!manifest?.name || !/^[a-z][a-z0-9-]{0,63}$/u.test(manifest.name) || !isPluginVersion(version) || manifest.version && manifest.version !== version) throw new Error("NATIVE_MANIFEST_UNSUPPORTED");
|
|
348
|
+
const destination = resolve(input.directory);
|
|
349
|
+
const directory = join(await realpath(dirname(destination)), destination.slice(dirname(destination).length + 1));
|
|
350
|
+
const market = `bfi-${target}-${tree.treeDigest.slice(0, 24)}`;
|
|
351
|
+
const selector = `${manifest.name}@${market}`;
|
|
352
|
+
const commands = target === "claude-code" ? [{
|
|
353
|
+
command: "claude",
|
|
354
|
+
args: [
|
|
355
|
+
"plugin",
|
|
356
|
+
"marketplace",
|
|
357
|
+
"add",
|
|
358
|
+
directory,
|
|
359
|
+
"--scope",
|
|
360
|
+
scope
|
|
361
|
+
]
|
|
362
|
+
}, {
|
|
363
|
+
command: "claude",
|
|
364
|
+
args: [
|
|
365
|
+
"plugin",
|
|
366
|
+
"install",
|
|
367
|
+
selector,
|
|
368
|
+
"--scope",
|
|
369
|
+
scope
|
|
370
|
+
]
|
|
371
|
+
}] : [{
|
|
372
|
+
command: "codex",
|
|
373
|
+
args: [
|
|
374
|
+
"plugin",
|
|
375
|
+
"marketplace",
|
|
376
|
+
"add",
|
|
377
|
+
directory
|
|
378
|
+
]
|
|
379
|
+
}, {
|
|
380
|
+
command: "codex",
|
|
381
|
+
args: [
|
|
382
|
+
"plugin",
|
|
383
|
+
"add",
|
|
384
|
+
selector
|
|
385
|
+
]
|
|
386
|
+
}];
|
|
387
|
+
await mkdir(directory, { mode: 448 });
|
|
388
|
+
await materializePluginTree(tree, join(directory, "plugin"));
|
|
389
|
+
const indexDirectory = join(directory, target === "claude-code" ? ".claude-plugin" : ".agents/plugins");
|
|
390
|
+
await mkdir(indexDirectory, {
|
|
391
|
+
recursive: true,
|
|
392
|
+
mode: 448
|
|
393
|
+
});
|
|
394
|
+
const index = target === "claude-code" ? {
|
|
395
|
+
name: market,
|
|
396
|
+
owner: { name: "bfi" },
|
|
397
|
+
plugins: [{
|
|
398
|
+
name: manifest.name,
|
|
399
|
+
version,
|
|
400
|
+
source: "./plugin"
|
|
401
|
+
}]
|
|
402
|
+
} : {
|
|
403
|
+
name: market,
|
|
404
|
+
plugins: [{
|
|
405
|
+
name: manifest.name,
|
|
406
|
+
source: {
|
|
407
|
+
source: "local",
|
|
408
|
+
path: "./plugin"
|
|
409
|
+
}
|
|
410
|
+
}]
|
|
411
|
+
};
|
|
412
|
+
await writeFile(join(indexDirectory, "marketplace.json"), JSON.stringify(index, null, 2) + "\n", {
|
|
413
|
+
flag: "wx",
|
|
414
|
+
mode: 384
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
state: "PREPARED_FOR_REVIEW",
|
|
418
|
+
directory,
|
|
419
|
+
treeDigest: tree.treeDigest,
|
|
420
|
+
registered: false,
|
|
421
|
+
loaded: false,
|
|
422
|
+
commands,
|
|
423
|
+
instructions: "Review all plugin code and native-name conflicts first. Execute from the intended project only after user confirmation. Do not bypass trust. Keep this directory while installed. List/registration success is not runtime LOAD; restart the host and check all components. Use the native host for updates/removal; this handoff does not create bfi installation ownership."
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
var targets = [
|
|
427
|
+
{
|
|
428
|
+
"id": "claude-code",
|
|
429
|
+
"name": "Claude Code",
|
|
430
|
+
"status": "NATIVE_GATED",
|
|
431
|
+
"versions": ["2.1.251"],
|
|
432
|
+
"scopes": ["user", "project"],
|
|
433
|
+
"manifest": ".claude-plugin/plugin.json",
|
|
434
|
+
"docs": "https://code.claude.com/docs/en/plugins/cli-reference",
|
|
435
|
+
"reason": "仅开放已验证的技能、命令、Agents 和四类 command Hooks;每个发布版本仍需匹配加载证据与服务端授权。MCP、userConfig 及其他扩展尚未通过完整运行验证。",
|
|
436
|
+
"manual": "先用 --prepare-native 生成完整插件目录及独立市场索引,再审阅其输出的 claude plugin marketplace add / install 命令。需要信任审批时由用户操作,重启后检查 /plugin 与组件是否加载。"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"id": "codex",
|
|
440
|
+
"name": "Codex",
|
|
441
|
+
"status": "EXPORT_ONLY",
|
|
442
|
+
"versions": ["0.136.0"],
|
|
443
|
+
"scopes": ["user"],
|
|
444
|
+
"manifest": ".codex-plugin/plugin.json",
|
|
445
|
+
"docs": "https://learn.chatgpt.com/docs/plugins",
|
|
446
|
+
"reason": "已验证公开市场注册、安装和卸载接口;尚未满足自动安装的完整加载及归属验证门禁。项目级原生插件安装未验证。",
|
|
447
|
+
"manual": "仅原生 Codex 技能型插件可用 --prepare-native 生成完整目录和独立市场索引,再按输出执行 codex plugin marketplace add / plugin add。在 /plugins 检查实际启用;有 MCP、Hooks、Apps 或转换需求时停止,不把不支持的组件丢弃。"
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
"id": "zcode",
|
|
451
|
+
"name": "ZCode",
|
|
452
|
+
"status": "EXPORT_ONLY",
|
|
453
|
+
"versions": [],
|
|
454
|
+
"scopes": [],
|
|
455
|
+
"manifest": ".zcode-plugin/plugin.json",
|
|
456
|
+
"docs": "https://zcode.z.ai/cn/docs/plugin",
|
|
457
|
+
"reason": "本机无 ZCode,原生安装、加载和作用域未实测。",
|
|
458
|
+
"manual": "官方入口为打开工作区后进入 设置 → 插件 → 个人 → 创建,添加插件市场。此处尚未验证 SkillHub 归档的完整导入链路,只导出,不生成或执行猜测的安装命令。"
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"id": "codebuddy",
|
|
462
|
+
"name": "CodeBuddy",
|
|
463
|
+
"status": "EXPORT_ONLY",
|
|
464
|
+
"versions": [],
|
|
465
|
+
"scopes": [],
|
|
466
|
+
"manifest": ".codebuddy-plugin/plugin.json",
|
|
467
|
+
"docs": "https://www.codebuddy.ai/docs/cli/plugins-reference",
|
|
468
|
+
"reason": "本机无 CodeBuddy,官方样例及完整加载未实测。",
|
|
469
|
+
"manual": "官方提供 codebuddy plugin marketplace / install 入口;未完成当前格式和宿主版本验证,只导出并提示待适配,不借用 Claude 的测试结论。"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"id": "qwen-code",
|
|
473
|
+
"name": "Qwen Code",
|
|
474
|
+
"status": "EXPORT_ONLY",
|
|
475
|
+
"versions": [],
|
|
476
|
+
"scopes": [],
|
|
477
|
+
"manifest": "qwen-extension.json",
|
|
478
|
+
"docs": "https://qwenlm.github.io/qwen-code-docs/en/users/extension/introduction/",
|
|
479
|
+
"reason": "本机无 Qwen Code;Claude 插件导入会进行格式转换,不等于保真兼容。",
|
|
480
|
+
"manual": "原生入口为 qwen extensions install;此处未验证隔离安装及组件保真,只导出。禁止自动把 Claude 插件转换成扩展或拆成技能。"
|
|
481
|
+
}
|
|
482
|
+
];
|
|
483
|
+
const adapters = Object.freeze({ "claude-code": claudeAdapter });
|
|
484
|
+
const reasons = {
|
|
485
|
+
"claude-code": ["EXACT_RELEASE_LOAD_EVIDENCE_REQUIRED", "SERVER_NATIVE_CAPABILITY_REQUIRED"],
|
|
486
|
+
codex: ["PUBLIC_INSTALL_VERIFIED", "EXACT_RUNTIME_LOAD_AND_OWNERSHIP_UNVERIFIED"],
|
|
487
|
+
zcode: ["HOST_BINARY_UNAVAILABLE", "NATIVE_ADAPTER_UNVERIFIED"],
|
|
488
|
+
codebuddy: ["OFFICIAL_FIXTURE_UNVERIFIED", "HOST_BINARY_UNAVAILABLE"],
|
|
489
|
+
"qwen-code": ["HOST_BINARY_UNAVAILABLE", "NATIVE_ADAPTER_UNVERIFIED"]
|
|
490
|
+
};
|
|
491
|
+
function nativeAdapterFor(target) {
|
|
492
|
+
return adapters[target] ?? null;
|
|
493
|
+
}
|
|
494
|
+
function nativeSupportFor(target) {
|
|
495
|
+
return {
|
|
496
|
+
...targets.find((item) => item.id === target),
|
|
497
|
+
targetAgent: target,
|
|
498
|
+
status: adapters[target] ? "NATIVE_GATED" : "EXPORT_ONLY",
|
|
499
|
+
reasons: [...reasons[target]],
|
|
500
|
+
adapterVersion: adapters[target]?.version
|
|
501
|
+
};
|
|
502
|
+
}
|
|
157
503
|
function meetsMinimum(version, minimum) {
|
|
158
504
|
const expression = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
159
505
|
const current = expression.exec(version), required = expression.exec(minimum);
|
|
@@ -172,6 +518,8 @@ function createPluginInstallPlan(input) {
|
|
|
172
518
|
const { identity, release, adapter, host } = input;
|
|
173
519
|
const plan = {
|
|
174
520
|
identity: structuredClone(identity),
|
|
521
|
+
artifactDirectory: input.artifactDirectory,
|
|
522
|
+
nativeName: input.nativeName,
|
|
175
523
|
treeDigest: release.treeDigest,
|
|
176
524
|
artifactSha256: release.artifactSha256,
|
|
177
525
|
adapterVersion: adapter?.version ?? "",
|
|
@@ -218,7 +566,9 @@ function createPluginInstallPlan(input) {
|
|
|
218
566
|
};
|
|
219
567
|
const invocations = adapter.plan({
|
|
220
568
|
identity: structuredClone(identity),
|
|
221
|
-
artifactDirectory: input.artifactDirectory
|
|
569
|
+
artifactDirectory: input.artifactDirectory,
|
|
570
|
+
nativeName: input.nativeName,
|
|
571
|
+
treeDigest: release.treeDigest
|
|
222
572
|
});
|
|
223
573
|
if (!invocations.length || invocations.some((invocation) => !invocation.command || [invocation.command, ...invocation.args].some((value) => typeof value !== "string" || value.includes("\0")))) return {
|
|
224
574
|
...plan,
|
|
@@ -262,24 +612,30 @@ async function installNativePlugin(request) {
|
|
|
262
612
|
if (active.adapterVersion !== adapter.version) throw new Error("Existing plugin requires its original adapter for safe update");
|
|
263
613
|
const verification = await adapter.verify(active, runner);
|
|
264
614
|
if (!verification.verified || verification.treeDigest !== active.treeDigest || verification.installationId !== active.installationId) throw new Error("Existing plugin ownership changed; refusing destructive update");
|
|
615
|
+
if (pluginInstallKey(active.identity) === pluginInstallKey(plan.identity) && active.artifactSha256 === plan.artifactSha256 && active.treeDigest === plan.treeDigest) return activeId;
|
|
616
|
+
throw new Error("NATIVE_UPDATE_REQUIRES_USER: existing verified version retained; export the new version and review the native update procedure");
|
|
265
617
|
}
|
|
266
618
|
const identifier = records.prepare(plan.identity, plan.artifactSha256, adapter.version, {
|
|
267
619
|
treeDigest: plan.treeDigest,
|
|
268
620
|
artifactDirectory: input.artifactDirectory
|
|
269
621
|
});
|
|
270
|
-
let invoked = false;
|
|
622
|
+
let invoked = false, staged = false;
|
|
271
623
|
try {
|
|
272
624
|
await materializePluginTree(tree, input.artifactDirectory);
|
|
625
|
+
staged = true;
|
|
626
|
+
await adapter.preflight?.(plan, runner);
|
|
273
627
|
invoked = true;
|
|
274
628
|
await adapter.install(plan, runner);
|
|
275
629
|
const verification = await adapter.verify(records.read().operations[identifier], runner);
|
|
276
630
|
if (!verification.verified || verification.treeDigest !== plan.treeDigest || !verification.installationId) throw new Error("Native verification failed");
|
|
277
631
|
records.complete(identifier, verification);
|
|
278
632
|
return identifier;
|
|
279
|
-
} catch {
|
|
633
|
+
} catch (error) {
|
|
280
634
|
if (!invoked) {
|
|
281
|
-
|
|
282
|
-
|
|
635
|
+
const code = staged ? "NATIVE_PREFLIGHT_FAILED" : "PLUGIN_STAGING_FAILED";
|
|
636
|
+
records.fail(identifier, code);
|
|
637
|
+
const reason = error instanceof Error && /^[A-Z_]+$/u.test(error.message) ? `: ${error.message}` : "";
|
|
638
|
+
throw new Error(`${code}: ${identifier}${reason}; no native mutation occurred`);
|
|
283
639
|
}
|
|
284
640
|
throw new Error(`NATIVE_OUTCOME_UNKNOWN: ${identifier}; PREPARED retained, inspect the native host before recovery`);
|
|
285
641
|
}
|
|
@@ -414,9 +770,40 @@ function matchingOperations(records, options, root, registry) {
|
|
|
414
770
|
}
|
|
415
771
|
async function runPluginPackage(args, globalOpts) {
|
|
416
772
|
const options = parsePluginArguments(args), cwd = await realpath(String(globalOpts.workdir ?? process.cwd()));
|
|
773
|
+
const chooseAgent = !options.agent && options.command !== "list";
|
|
774
|
+
if (chooseAgent) {
|
|
775
|
+
if (options.json || options.confirmed || !process.stdin.isTTY || !process.stdout.isTTY) throw new Error("AGENT_REQUIRED: specify --agent; interactive installation will ask you to choose");
|
|
776
|
+
const agent = await select({
|
|
777
|
+
message: "选择插件目标 Agent",
|
|
778
|
+
options: pluginTargets.map((value) => ({
|
|
779
|
+
value,
|
|
780
|
+
label: value
|
|
781
|
+
}))
|
|
782
|
+
});
|
|
783
|
+
if (isCancel(agent) || !pluginTargets.includes(agent)) throw new Error("NATIVE_CANCELLED");
|
|
784
|
+
options.agent = agent;
|
|
785
|
+
}
|
|
786
|
+
if (!options.scopeExplicit && options.command !== "list" && (chooseAgent || !options.planOnly && !options.exportPath)) {
|
|
787
|
+
if (options.json || options.confirmed || !process.stdin.isTTY || !process.stdout.isTTY) throw new Error("SCOPE_REQUIRED: specify --scope user or --scope project; --yes does not choose a scope");
|
|
788
|
+
const scope = await select({
|
|
789
|
+
message: "选择插件安装范围",
|
|
790
|
+
options: [{
|
|
791
|
+
value: "user",
|
|
792
|
+
label: "用户级"
|
|
793
|
+
}, {
|
|
794
|
+
value: "project",
|
|
795
|
+
label: `当前项目:${cwd}`
|
|
796
|
+
}]
|
|
797
|
+
});
|
|
798
|
+
if (isCancel(scope) || scope !== "user" && scope !== "project") throw new Error("NATIVE_CANCELLED");
|
|
799
|
+
options.scope = scope;
|
|
800
|
+
}
|
|
417
801
|
const storeRoot = options.scope === "user" ? await realpath(homedir()) : cwd;
|
|
418
802
|
const records = new PluginInstallRecords(storeRoot), config = resolveConfig(globalOpts);
|
|
419
|
-
const
|
|
803
|
+
const projectConfig = loadProjectConfig(cwd);
|
|
804
|
+
const projectToken = projectConfig?.registry && normalizeRegistry(projectConfig.registry) === normalizeRegistry(config.registry) ? projectConfig.token : void 0;
|
|
805
|
+
const token = typeof globalOpts.token === "string" ? globalOpts.token : projectToken ?? credentialForRegistry(config.registry, loadGlobalConfig());
|
|
806
|
+
const registry = new PluginRegistryClient(config.registry, token ?? null);
|
|
420
807
|
if (options.command === "list") {
|
|
421
808
|
const lock = records.read();
|
|
422
809
|
console.log(JSON.stringify({
|
|
@@ -527,17 +914,29 @@ async function runPluginPackage(args, globalOpts) {
|
|
|
527
914
|
checkFiles(detail, tree);
|
|
528
915
|
}
|
|
529
916
|
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
|
|
917
|
+
const artifactContainer = join(storeRoot, ".bfi", "plugin-artifacts", `${pluginInstallKey(identity)}-${randomUUID()}`);
|
|
918
|
+
const artifactDirectory = join(artifactContainer, "plugin");
|
|
919
|
+
const manifest = nativeManifestFormats[agent].map((format) => tree.manifests.find((item) => item.root === "" && item.format === format)).find(Boolean);
|
|
920
|
+
let hostVersion = adapter ? await adapter.detect(runNative, cwd) : null;
|
|
921
|
+
if (options.preparePath && agent === "codex") try {
|
|
922
|
+
hostVersion = /^codex-cli (\d+\.\d+\.\d+)\s*$/u.exec((await runNative({
|
|
923
|
+
command: "codex",
|
|
924
|
+
args: ["--version"]
|
|
925
|
+
}, { cwd })).stdout)?.[1] ?? null;
|
|
926
|
+
} catch {
|
|
927
|
+
hostVersion = null;
|
|
928
|
+
}
|
|
531
929
|
const input = {
|
|
532
930
|
identity,
|
|
533
931
|
release: detail.release,
|
|
534
932
|
actualTreeDigest: tree.treeDigest,
|
|
535
933
|
actualArtifactSha256: sha256(archive),
|
|
536
934
|
requiredCapabilities: nativeRequirements(tree, agent),
|
|
935
|
+
nativeName: manifest?.name,
|
|
537
936
|
evidence: detail.evidence,
|
|
538
937
|
host: {
|
|
539
938
|
agent,
|
|
540
|
-
version:
|
|
939
|
+
version: hostVersion
|
|
541
940
|
},
|
|
542
941
|
cliVersion: String(globalOpts.version ?? ""),
|
|
543
942
|
serverCapability,
|
|
@@ -550,7 +949,10 @@ async function runPluginPackage(args, globalOpts) {
|
|
|
550
949
|
...plan,
|
|
551
950
|
archiveRoot: root,
|
|
552
951
|
requiredCapabilities: input.requiredCapabilities,
|
|
553
|
-
nativeSupport: nativeSupportFor(agent)
|
|
952
|
+
nativeSupport: nativeSupportFor(agent),
|
|
953
|
+
diagnostics: input.requiredCapabilities.includes("manifest:missing-or-invalid") ? ["没有目标 Agent 可识别的原生插件清单;旧版 provides/pi 格式只允许原样导出,不生成替代清单。"] : [],
|
|
954
|
+
registered: false,
|
|
955
|
+
loaded: false
|
|
554
956
|
}, null, 2));
|
|
555
957
|
return;
|
|
556
958
|
}
|
|
@@ -562,22 +964,54 @@ async function runPluginPackage(args, globalOpts) {
|
|
|
562
964
|
mode: 384
|
|
563
965
|
});
|
|
564
966
|
console.log(JSON.stringify({
|
|
967
|
+
state: "EXPORTED",
|
|
565
968
|
exported: target,
|
|
566
969
|
archiveSha256: sha256(archive),
|
|
567
970
|
treeDigest: tree.treeDigest,
|
|
568
971
|
archiveRoot: root,
|
|
569
972
|
originalArchive: true,
|
|
570
|
-
installed: false
|
|
973
|
+
installed: false,
|
|
974
|
+
registered: false,
|
|
975
|
+
loaded: false
|
|
571
976
|
}, null, 2));
|
|
572
977
|
return;
|
|
573
978
|
}
|
|
979
|
+
if (options.preparePath) {
|
|
980
|
+
if (detail.evidence.some((item) => item.targetAgent === agent && item.treeDigest === tree.treeDigest && (!item.hostVersion || item.hostVersion === hostVersion) && (item.result === "FAIL" || item.result === "UNSUPPORTED"))) throw new Error("COMPATIBILITY_CHECK_FAILED: native handoff blocked by recorded compatibility failure; complete export remains available");
|
|
981
|
+
if (plan.reasons.includes("CLI_UPGRADE_REQUIRED")) throw new Error("CLI_UPGRADE_REQUIRED");
|
|
982
|
+
console.log(JSON.stringify(await prepareNativeHandoff({
|
|
983
|
+
tree,
|
|
984
|
+
target: agent,
|
|
985
|
+
scope: options.scope,
|
|
986
|
+
version: identity.version,
|
|
987
|
+
hostVersion,
|
|
988
|
+
directory: resolve(cwd, options.preparePath)
|
|
989
|
+
}), null, 2));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
574
992
|
if (plan.status !== "READY" || !adapter) throw new Error(`${plan.status}: ${plan.reasons.join(", ")}; use --plan or --export <archive>, no skills-only fallback occurred`);
|
|
993
|
+
if (!options.confirmed) {
|
|
994
|
+
if (options.json || !process.stdin.isTTY || !process.stdout.isTTY) throw new Error("NATIVE_CONFIRMATION_REQUIRED: review --plan first, then explicitly confirm with --yes");
|
|
995
|
+
const answer = await confirm({ message: `原生注册 ${identity.version} 到 ${agent} (${identity.scope})。插件代码、Hooks 可在宿主加载时执行;是否已审阅完整内容并继续?` });
|
|
996
|
+
if (isCancel(answer) || !answer) throw new Error("NATIVE_CANCELLED");
|
|
997
|
+
}
|
|
575
998
|
if (identity.source.kind === "registry") {
|
|
576
999
|
const [namespace, slug] = identity.source.packageRef.slice(1).split("/");
|
|
577
1000
|
const current = checkedDetail(await registry.release(namespace, slug, identity.version));
|
|
578
|
-
|
|
1001
|
+
const summary = await registry.summary(namespace, slug);
|
|
1002
|
+
const capability = summary?.installCapabilities?.version === "1" ? summary.installCapabilities.targets.find((item) => item.targetAgent === agent && item.path === "native") : void 0;
|
|
1003
|
+
const fresh = createPluginInstallPlan({
|
|
1004
|
+
...input,
|
|
1005
|
+
release: current.release,
|
|
1006
|
+
evidence: current.evidence,
|
|
1007
|
+
serverCapability: capability
|
|
1008
|
+
});
|
|
1009
|
+
if (!summary?.enabled || await registry.packageId(namespace, slug) !== identity.source.packageId || current.release.id !== detail.release.id || current.release.snapshotId !== detail.release.snapshotId || current.release.artifactSize !== archive.length || current.release.lifecycleRevision !== detail.release.lifecycleRevision || fresh.status !== "READY") throw new Error("Plugin distribution identity or authority changed before installation");
|
|
1010
|
+
checkFiles(current, tree);
|
|
579
1011
|
}
|
|
1012
|
+
if (await adapter.detect(runNative, cwd) !== hostVersion) throw new Error("HOST_VERSION_CHANGED");
|
|
580
1013
|
await artifactParent(storeRoot);
|
|
1014
|
+
await mkdir(artifactContainer, { mode: 448 });
|
|
581
1015
|
const operationId = await installNativePlugin({
|
|
582
1016
|
input,
|
|
583
1017
|
archive,
|
|
@@ -589,7 +1023,10 @@ async function runPluginPackage(args, globalOpts) {
|
|
|
589
1023
|
console.log(JSON.stringify({
|
|
590
1024
|
operationId,
|
|
591
1025
|
identity,
|
|
592
|
-
|
|
1026
|
+
state: "REGISTERED",
|
|
1027
|
+
registrationVerified: true,
|
|
1028
|
+
loadStatus: "PENDING_HOST_RESTART",
|
|
1029
|
+
message: "原生注册和文件归属已核对;请重启宿主确认组件加载,不代表业务功能测试通过。"
|
|
593
1030
|
}, null, 2));
|
|
594
1031
|
}
|
|
595
1032
|
export { runPluginPackage };
|