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.
@@ -1,30 +1,14 @@
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";
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 (defaultComponents.has(root)) requirements.add(`component:${root}`);
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
- records.fail(identifier, "STAGING_FAILED");
282
- throw new Error(`PLUGIN_STAGING_FAILED: ${identifier}`);
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 registry = new PluginRegistryClient(config.registry, config.token);
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 artifactDirectory = join(storeRoot, ".bfi", "plugin-artifacts", `${pluginInstallKey(identity)}-${randomUUID()}`);
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: adapter ? await adapter.detect(runNative, cwd) : null
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
- 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");
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
- verified: true
1026
+ state: "REGISTERED",
1027
+ registrationVerified: true,
1028
+ loadStatus: "PENDING_HOST_RESTART",
1029
+ message: "原生注册和文件归属已核对;请重启宿主确认组件加载,不代表业务功能测试通过。"
593
1030
  }, null, 2));
594
1031
  }
595
1032
  export { runPluginPackage };