bfi-cli 0.1.102 → 0.1.104

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.
@@ -2,10 +2,10 @@ import { isCancel } from "./libs/@clack/core.mjs";
2
2
  import { confirm, select } from "./libs/@clack/prompts.mjs";
3
3
  import { PluginInstallRecords, PluginRegistryClient, inspectPluginArchive, isPluginArchive, isPluginVersion, materializePluginTree, normalizeRegistry, parsePluginArguments, pluginArchiveLimits, pluginInstallKey, pluginSlotKey, pluginTargets, selectPluginRoot, sha256, treeDigest } from "./intent.mjs";
4
4
  import { loadGlobalConfig, loadProjectConfig, resolveConfig } from "./config.mjs";
5
- import { credentialForRegistry } from "./client.mjs";
5
+ import { credentialForRegistry } from "../cli.mjs";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
8
- import { lstat, mkdir, open, readFile, readdir, realpath, writeFile } from "node:fs/promises";
8
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, writeFile } from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
10
  import { z } from "zod";
11
11
  import { execFile } from "node:child_process";
@@ -139,9 +139,9 @@ function nativeRequirements(tree, target) {
139
139
  }
140
140
  return [...requirements].sort();
141
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)}`;
142
+ const nativeNamePattern$1 = /^[a-z][a-z0-9-]{0,63}$/u;
143
+ const cwdFor$1 = (identity) => identity.projectPath ?? homedir();
144
+ const marketName$1 = (identity, digest, directory) => `bfi-${pluginInstallKey(identity).slice(0, 20)}-${digest.slice(0, 12)}-${sha256(Buffer.from(directory)).slice(0, 12)}`;
145
145
  async function nativeDirectoryDigest(directory) {
146
146
  const files = [];
147
147
  let bytes = 0, entries = 0;
@@ -176,9 +176,9 @@ async function nativeList(runner, cwd, markets = false) {
176
176
  if (!Array.isArray(result) || result.some((item) => !item || typeof item !== "object" || Array.isArray(item))) throw new Error("NATIVE_LIST_INVALID");
177
177
  return result;
178
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);
179
+ function contextValues$1(context) {
180
+ if (!context.nativeName || !nativeNamePattern$1.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$1(context.identity, context.treeDigest, context.artifactDirectory);
182
182
  return {
183
183
  market,
184
184
  selector: `${context.nativeName}@${market}`,
@@ -197,7 +197,7 @@ function contextValues(context) {
197
197
  async function recordValues(record) {
198
198
  if (await nativeDirectoryDigest(record.artifactDirectory) !== record.treeDigest) throw new Error("NATIVE_CONTENT_CHANGED");
199
199
  const manifest = JSON.parse(await readFile(join(record.artifactDirectory, ".claude-plugin/plugin.json"), "utf8"));
200
- return contextValues({
200
+ return contextValues$1({
201
201
  ...record,
202
202
  nativeName: manifest.name
203
203
  });
@@ -233,7 +233,7 @@ const claudeAdapter = {
233
233
  }
234
234
  },
235
235
  plan(context) {
236
- const values = contextValues(context);
236
+ const values = contextValues$1(context);
237
237
  return [{
238
238
  command: "claude",
239
239
  args: [
@@ -257,21 +257,21 @@ const claudeAdapter = {
257
257
  },
258
258
  async preflight(plan, runner) {
259
259
  if (!plan.artifactDirectory) throw new Error("NATIVE_CONTEXT_INVALID");
260
- const values = contextValues({
260
+ const values = contextValues$1({
261
261
  ...plan,
262
262
  artifactDirectory: plan.artifactDirectory
263
263
  });
264
- const cwd = cwdFor(plan.identity);
264
+ const cwd = cwdFor$1(plan.identity);
265
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
266
  if ((await nativeList(runner, cwd, true)).some((item) => item.name === values.market)) throw new Error("NATIVE_MARKET_CONFLICT");
267
267
  },
268
268
  async install(plan, runner) {
269
269
  if (!plan.artifactDirectory) throw new Error("NATIVE_CONTEXT_INVALID");
270
- const values = contextValues({
270
+ const values = contextValues$1({
271
271
  ...plan,
272
272
  artifactDirectory: plan.artifactDirectory
273
273
  });
274
- const cwd = cwdFor(plan.identity);
274
+ const cwd = cwdFor$1(plan.identity);
275
275
  await this.preflight(plan, runner);
276
276
  if (await nativeDirectoryDigest(plan.artifactDirectory) !== plan.treeDigest) throw new Error("NATIVE_CONTENT_CHANGED");
277
277
  const nativeManifest = JSON.parse(await readFile(join(plan.artifactDirectory, ".claude-plugin/plugin.json"), "utf8"));
@@ -288,7 +288,7 @@ const claudeAdapter = {
288
288
  },
289
289
  async verify(record, runner) {
290
290
  try {
291
- const values = await recordValues(record), cwd = cwdFor(record.identity);
291
+ const values = await recordValues(record), cwd = cwdFor$1(record.identity);
292
292
  const index = JSON.parse(await readFile(join(values.directory, ".claude-plugin/marketplace.json"), "utf8"));
293
293
  if (JSON.stringify(index) !== JSON.stringify(values.manifest)) throw new Error("NATIVE_INDEX_CHANGED");
294
294
  const market = (await nativeList(runner, cwd, true)).filter((item) => item.name === values.market);
@@ -309,7 +309,7 @@ const claudeAdapter = {
309
309
  }
310
310
  },
311
311
  async uninstall(record, runner) {
312
- const values = await recordValues(record), cwd = cwdFor(record.identity);
312
+ const values = await recordValues(record), cwd = cwdFor$1(record.identity);
313
313
  await runner({
314
314
  command: "claude",
315
315
  args: [
@@ -324,7 +324,7 @@ const claudeAdapter = {
324
324
  },
325
325
  async verifyAbsent(record, runner) {
326
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));
327
+ return !(await nativeList(runner, cwdFor$1(record.identity))).some((item) => item.id === values.selector && item.scope === record.identity.scope && (record.identity.scope === "user" || item.projectPath === record.identity.projectPath));
328
328
  }
329
329
  };
330
330
  const codexRequirements = [
@@ -336,26 +336,75 @@ const codexRequirements = [
336
336
  "executable-assets"
337
337
  ];
338
338
  async function prepareNativeHandoff(input) {
339
- const { tree, target, scope, hostVersion, version } = input;
339
+ const { tree, target, scope, hostVersion, version, market } = input;
340
340
  if (target !== "claude-code" && target !== "codex") throw new Error("NATIVE_HOST_UNVERIFIED");
341
341
  if (hostVersion !== (target === "claude-code" ? "2.1.251" : "0.136.0")) throw new Error("HOST_VERSION_UNVERIFIED");
342
342
  if (target === "codex" && scope !== "user") throw new Error("SCOPE_UNSUPPORTED");
343
+ if (!/^bfi-[a-f0-9]{24}$/u.test(market)) throw new Error("MARKET_NAME_INVALID");
343
344
  const supported = target === "claude-code" ? claudeAdapter.capabilities : codexRequirements;
344
345
  const missing = nativeRequirements(tree, target).filter((requirement) => !supported.includes(requirement));
345
346
  if (missing.length) throw new Error(`NATIVE_COMPONENTS_UNSUPPORTED: ${missing.join(", ")}; original files retained; no skills fallback`);
346
347
  const manifest = tree.manifests.find((item) => item.root === "" && item.format === (target === "claude-code" ? "CLAUDE" : "CODEX"));
347
348
  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)}`;
349
+ const requested = resolve(input.directory);
350
+ await mkdir(dirname(requested), {
351
+ recursive: true,
352
+ mode: 448
353
+ });
354
+ const root = join(await realpath(dirname(requested)), requested.slice(dirname(requested).length + 1));
355
+ await mkdir(root, {
356
+ recursive: true,
357
+ mode: 448
358
+ });
359
+ const pluginDirectory = join(root, "versions", version, "plugin");
360
+ const existing = await nativeDirectoryDigest(pluginDirectory).catch(() => null);
361
+ if (existing === null) {
362
+ await mkdir(join(root, "versions", version), {
363
+ recursive: true,
364
+ mode: 448
365
+ });
366
+ await materializePluginTree(tree, pluginDirectory);
367
+ } else if (existing !== tree.treeDigest) throw new Error("NATIVE_VERSION_CONTENT_CONFLICT: this version directory already contains different content; choose another --dir");
368
+ const indexDirectory = join(root, target === "claude-code" ? ".claude-plugin" : ".agents/plugins");
369
+ await mkdir(indexDirectory, {
370
+ recursive: true,
371
+ mode: 448
372
+ });
373
+ const indexFile = join(indexDirectory, "marketplace.json");
374
+ const source = `./versions/${version}/plugin`;
375
+ const entry = target === "claude-code" ? {
376
+ name: manifest.name,
377
+ version,
378
+ source
379
+ } : {
380
+ name: manifest.name,
381
+ version,
382
+ source: {
383
+ source: "local",
384
+ path: source
385
+ },
386
+ policy: { installation: "AVAILABLE" }
387
+ };
388
+ const previous = await readFile(indexFile, "utf8").then(JSON.parse).catch(() => null);
389
+ if (previous && previous.name !== market) throw new Error("MARKET_DIRECTORY_CONFLICT: this directory holds a different marketplace");
390
+ const plugins = Array.isArray(previous?.plugins) ? previous.plugins : [];
391
+ const index = {
392
+ ...previous ?? {},
393
+ name: market,
394
+ owner: { name: "bfi" },
395
+ plugins: [...plugins.filter((item) => item?.name !== manifest.name), entry]
396
+ };
397
+ const temporary = `${indexFile}.${process.pid}.tmp`;
398
+ await writeFile(temporary, JSON.stringify(index, null, 2) + "\n", { mode: 384 });
399
+ await rename(temporary, indexFile);
351
400
  const selector = `${manifest.name}@${market}`;
352
- const commands = target === "claude-code" ? [{
401
+ const installCommands = target === "claude-code" ? [{
353
402
  command: "claude",
354
403
  args: [
355
404
  "plugin",
356
405
  "marketplace",
357
406
  "add",
358
- directory,
407
+ root,
359
408
  "--scope",
360
409
  scope
361
410
  ]
@@ -374,7 +423,40 @@ async function prepareNativeHandoff(input) {
374
423
  "plugin",
375
424
  "marketplace",
376
425
  "add",
377
- directory
426
+ root
427
+ ]
428
+ }, {
429
+ command: "codex",
430
+ args: [
431
+ "plugin",
432
+ "add",
433
+ selector
434
+ ]
435
+ }];
436
+ const updateCommands = target === "claude-code" ? [{
437
+ command: "claude",
438
+ args: [
439
+ "plugin",
440
+ "marketplace",
441
+ "update",
442
+ market
443
+ ]
444
+ }, {
445
+ command: "claude",
446
+ args: [
447
+ "plugin",
448
+ "update",
449
+ selector,
450
+ "--scope",
451
+ scope
452
+ ]
453
+ }] : [{
454
+ command: "codex",
455
+ args: [
456
+ "plugin",
457
+ "marketplace",
458
+ "add",
459
+ root
378
460
  ]
379
461
  }, {
380
462
  command: "codex",
@@ -384,45 +466,208 @@ async function prepareNativeHandoff(input) {
384
466
  selector
385
467
  ]
386
468
  }];
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
469
  return {
417
470
  state: "PREPARED_FOR_REVIEW",
418
- directory,
471
+ directory: root,
472
+ market,
473
+ selector,
474
+ version,
475
+ pluginDirectory,
419
476
  treeDigest: tree.treeDigest,
420
477
  registered: false,
421
478
  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."
479
+ commands: input.update ? updateCommands : installCommands,
480
+ installCommands,
481
+ updateCommands,
482
+ 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."
424
483
  };
425
484
  }
485
+ const nativeNamePattern = /^[a-z][a-z0-9-]{0,63}$/u;
486
+ const scope = "user";
487
+ const cwdFor = (identity) => identity.projectPath ?? homedir();
488
+ const codexHome = () => process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
489
+ const marketName = (identity, digest, directory) => `bfi-${pluginInstallKey(identity).slice(0, 20)}-${digest.slice(0, 12)}-${sha256(Buffer.from(directory)).slice(0, 12)}`;
490
+ async function installedSelector(record) {
491
+ return record.installationId ?? null;
492
+ }
493
+ async function listed(runner, cwd) {
494
+ return (await runner({
495
+ command: "codex",
496
+ args: ["plugin", "list"]
497
+ }, { cwd })).stdout;
498
+ }
499
+ function contextValues(context) {
500
+ if (!context.nativeName || !nativeNamePattern.test(context.nativeName) || !/^[a-f0-9]{64}$/u.test(context.treeDigest ?? "")) throw new Error("NATIVE_CONTEXT_INVALID");
501
+ const market = marketName(context.identity, context.treeDigest, context.artifactDirectory);
502
+ return {
503
+ market,
504
+ directory: dirname(context.artifactDirectory),
505
+ selector: `${context.nativeName}@${market}`,
506
+ manifest: {
507
+ name: market,
508
+ interface: { displayName: `bfi ${context.nativeName}` },
509
+ plugins: [{
510
+ name: context.nativeName,
511
+ version: context.identity.version,
512
+ source: {
513
+ source: "local",
514
+ path: "./plugin"
515
+ },
516
+ policy: { installation: "AVAILABLE" }
517
+ }]
518
+ }
519
+ };
520
+ }
521
+ const codexAdapter = {
522
+ targetAgent: "codex",
523
+ version: "codex-public-v1",
524
+ hostVersions: ["0.136.0"],
525
+ scopes: [scope],
526
+ capabilities: [
527
+ "native-plugin",
528
+ "manifest:CODEX",
529
+ "manifest-field:skills",
530
+ "component:skills",
531
+ "component:agent-metadata",
532
+ "executable-assets"
533
+ ],
534
+ async detect(runner, cwd) {
535
+ try {
536
+ return /^codex(?:-cli)? (\d+\.\d+\.\d+)\s*$/u.exec((await runner({
537
+ command: "codex",
538
+ args: ["--version"]
539
+ }, { cwd })).stdout)?.[1] ?? null;
540
+ } catch {
541
+ return null;
542
+ }
543
+ },
544
+ plan(context) {
545
+ const values = contextValues(context);
546
+ return [{
547
+ command: "codex",
548
+ args: [
549
+ "plugin",
550
+ "marketplace",
551
+ "add",
552
+ values.directory
553
+ ]
554
+ }, {
555
+ command: "codex",
556
+ args: [
557
+ "plugin",
558
+ "add",
559
+ values.selector
560
+ ]
561
+ }];
562
+ },
563
+ async preflight(plan, runner) {
564
+ if (!plan.artifactDirectory) throw new Error("NATIVE_CONTEXT_INVALID");
565
+ const values = contextValues({
566
+ ...plan,
567
+ artifactDirectory: plan.artifactDirectory
568
+ });
569
+ const cwd = cwdFor(plan.identity);
570
+ if ((await runner({
571
+ command: "codex",
572
+ args: [
573
+ "plugin",
574
+ "marketplace",
575
+ "list"
576
+ ]
577
+ }, { cwd })).stdout.split("\n").some((line) => line.trim().startsWith(values.market))) throw new Error("NATIVE_MARKET_CONFLICT");
578
+ if ((await listed(runner, cwd)).split("\n").some((line) => line.trim().startsWith(`${plan.nativeName}@`))) throw new Error("NATIVE_NAME_CONFLICT");
579
+ },
580
+ async install(plan, runner) {
581
+ if (!plan.artifactDirectory) throw new Error("NATIVE_CONTEXT_INVALID");
582
+ const values = contextValues({
583
+ ...plan,
584
+ artifactDirectory: plan.artifactDirectory
585
+ });
586
+ const cwd = cwdFor(plan.identity);
587
+ await this.preflight(plan, runner);
588
+ if (await nativeDirectoryDigest(plan.artifactDirectory) !== plan.treeDigest) throw new Error("NATIVE_CONTENT_CHANGED");
589
+ const indexDirectory = join(values.directory, ".agents", "plugins");
590
+ await mkdir(indexDirectory, {
591
+ recursive: true,
592
+ mode: 448
593
+ });
594
+ await writeFile(join(indexDirectory, "marketplace.json"), JSON.stringify(values.manifest, null, 2) + "\n", {
595
+ flag: "wx",
596
+ mode: 384
597
+ });
598
+ for (const invocation of this.plan({
599
+ ...plan,
600
+ artifactDirectory: plan.artifactDirectory
601
+ })) await runner(invocation, { cwd });
602
+ },
603
+ async verify(record, runner) {
604
+ try {
605
+ const selector = await installedSelector(record);
606
+ if (!selector) return {
607
+ verified: false,
608
+ installationId: "",
609
+ treeDigest: ""
610
+ };
611
+ const [nativeName, market] = selector.split("@");
612
+ if (!nativeName || !market) return {
613
+ verified: false,
614
+ installationId: "",
615
+ treeDigest: ""
616
+ };
617
+ if (!enabled(await readFile(join(codexHome(), "config.toml"), "utf8"), selector)) return {
618
+ verified: false,
619
+ installationId: "",
620
+ treeDigest: ""
621
+ };
622
+ const entry = (await listed(runner, cwdFor(record.identity))).split("\n").find((line) => line.trim().startsWith(selector));
623
+ if (!entry || !/installed(?:,\s*enabled)?/u.test(entry)) return {
624
+ verified: false,
625
+ installationId: "",
626
+ treeDigest: ""
627
+ };
628
+ const digest = await nativeDirectoryDigest(join(codexHome(), "plugins", "cache", market, nativeName, record.identity.version));
629
+ return {
630
+ verified: digest === record.treeDigest,
631
+ installationId: selector,
632
+ treeDigest: digest
633
+ };
634
+ } catch {
635
+ return {
636
+ verified: false,
637
+ installationId: "",
638
+ treeDigest: ""
639
+ };
640
+ }
641
+ },
642
+ async uninstall(record, runner) {
643
+ const selector = await installedSelector(record);
644
+ if (!selector) throw new Error("NATIVE_CONTEXT_INVALID");
645
+ await runner({
646
+ command: "codex",
647
+ args: [
648
+ "plugin",
649
+ "remove",
650
+ selector
651
+ ]
652
+ }, { cwd: cwdFor(record.identity) });
653
+ },
654
+ async verifyAbsent(record, runner) {
655
+ const selector = await installedSelector(record);
656
+ if (!selector) return false;
657
+ return !(await listed(runner, cwdFor(record.identity)).catch(() => "")).split("\n").some((line) => line.trim().startsWith(selector));
658
+ }
659
+ };
660
+ function enabled(config, selector) {
661
+ const lines = config.split("\n");
662
+ const header = `[plugins."${selector}"]`;
663
+ const start = lines.findIndex((line) => line.trim() === header);
664
+ if (start < 0) return false;
665
+ for (const line of lines.slice(start + 1)) {
666
+ if (line.trim().startsWith("[")) break;
667
+ if (/^\s*enabled\s*=\s*true\s*$/u.test(line)) return true;
668
+ }
669
+ return false;
670
+ }
426
671
  var targets = [
427
672
  {
428
673
  "id": "claude-code",
@@ -438,12 +683,12 @@ var targets = [
438
683
  {
439
684
  "id": "codex",
440
685
  "name": "Codex",
441
- "status": "EXPORT_ONLY",
686
+ "status": "NATIVE_GATED",
442
687
  "versions": ["0.136.0"],
443
688
  "scopes": ["user"],
444
689
  "manifest": ".codex-plugin/plugin.json",
445
690
  "docs": "https://learn.chatgpt.com/docs/plugins",
446
- "reason": "已验证公开市场注册、安装和卸载接口;尚未满足自动安装的完整加载及归属验证门禁。项目级原生插件安装未验证。",
691
+ "reason": "适配器已按 0.136.0 的本地市场与 plugin add/remove/list 实现;仍需每个发布版本的运行时加载证据与服务端授权,项目级安装未验证。",
447
692
  "manual": "仅原生 Codex 技能型插件可用 --prepare-native 生成完整目录和独立市场索引,再按输出执行 codex plugin marketplace add / plugin add。在 /plugins 检查实际启用;有 MCP、Hooks、Apps 或转换需求时停止,不把不支持的组件丢弃。"
448
693
  },
449
694
  {
@@ -480,7 +725,10 @@ var targets = [
480
725
  "manual": "原生入口为 qwen extensions install;此处未验证隔离安装及组件保真,只导出。禁止自动把 Claude 插件转换成扩展或拆成技能。"
481
726
  }
482
727
  ];
483
- const adapters = Object.freeze({ "claude-code": claudeAdapter });
728
+ const adapters = Object.freeze({
729
+ "claude-code": claudeAdapter,
730
+ codex: codexAdapter
731
+ });
484
732
  const reasons = {
485
733
  "claude-code": ["EXACT_RELEASE_LOAD_EVIDENCE_REQUIRED", "SERVER_NATIVE_CAPABILITY_REQUIRED"],
486
734
  codex: ["PUBLIC_INSTALL_VERIFIED", "EXACT_RUNTIME_LOAD_AND_OWNERSHIP_UNVERIFIED"],
@@ -663,6 +911,24 @@ async function uninstallNativePlugin(request) {
663
911
  }
664
912
  const exactId = z.string().regex(/^[1-9][0-9]*$/u);
665
913
  const digest = z.string().regex(/^[a-f0-9]{64}$/u);
914
+ function baseName(source) {
915
+ if (source.startsWith("@")) return source.split("/")[1] ?? "plugin";
916
+ return basename(source).replace(/\.(?:zip|tgz|tar\.gz)$/iu, "");
917
+ }
918
+ function archiveFormat(path) {
919
+ if (/\.zip$/iu.test(path)) return "zip";
920
+ if (/\.tar\.gz$/iu.test(path)) return "tar.gz";
921
+ if (/\.tgz$/iu.test(path)) return "tgz";
922
+ }
923
+ function handoffMarket(identity) {
924
+ const source = identity.source.kind === "registry" ? `${identity.source.registry}|${identity.source.packageRef}` : identity.source.sourcePath;
925
+ return `bfi-${sha256(Buffer.from(JSON.stringify([
926
+ source,
927
+ identity.agent,
928
+ identity.scope,
929
+ identity.projectPath
930
+ ]))).slice(0, 24)}`;
931
+ }
666
932
  const releaseSchema = z.object({
667
933
  id: exactId,
668
934
  snapshotId: exactId,
@@ -770,7 +1036,7 @@ function matchingOperations(records, options, root, registry) {
770
1036
  }
771
1037
  async function runPluginPackage(args, globalOpts) {
772
1038
  const options = parsePluginArguments(args), cwd = await realpath(String(globalOpts.workdir ?? process.cwd()));
773
- const chooseAgent = !options.agent && options.command !== "list";
1039
+ const chooseAgent = !options.agent && options.command !== "list" && options.command !== "download" && !options.exportPath;
774
1040
  if (chooseAgent) {
775
1041
  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
1042
  const agent = await select({
@@ -783,7 +1049,8 @@ async function runPluginPackage(args, globalOpts) {
783
1049
  if (isCancel(agent) || !pluginTargets.includes(agent)) throw new Error("NATIVE_CANCELLED");
784
1050
  options.agent = agent;
785
1051
  }
786
- if (!options.scopeExplicit && options.command !== "list" && (chooseAgent || !options.planOnly && !options.exportPath)) {
1052
+ if (!options.agent && (options.command === "download" || options.exportPath)) options.agent = "claude-code";
1053
+ if (!options.scopeExplicit && options.command !== "list" && options.command !== "download" && (chooseAgent || !options.planOnly && !options.exportPath)) {
787
1054
  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
1055
  const scope = await select({
789
1056
  message: "选择插件安装范围",
@@ -844,8 +1111,14 @@ async function runPluginPackage(args, globalOpts) {
844
1111
  }
845
1112
  let archive, tree, identity, detail;
846
1113
  let serverCapability, archiveName = "plugin.zip", root = "";
847
- const source = options.source;
848
- if (isPluginArchive(source) && !source.startsWith("@")) {
1114
+ const source = options.source, local = isPluginArchive(source) && !source.startsWith("@");
1115
+ const exportRequested = options.command === "download" || Boolean(options.exportPath);
1116
+ const localFormat = local ? archiveFormat(source) : void 0;
1117
+ if (local && options.format && options.format !== localFormat) throw new Error("--format must match the local archive format");
1118
+ if (options.format && !exportRequested) throw new Error("--format requires download or --export");
1119
+ const format = exportRequested ? options.format ?? localFormat ?? archiveFormat(options.exportPath ?? "") ?? "zip" : "zip";
1120
+ if (!local) archiveName = format === "zip" ? "plugin.zip" : format === "tar.gz" ? "plugin.tar.gz" : "plugin.tgz";
1121
+ if (local) {
849
1122
  const sourcePath = await realpath(resolve(cwd, source));
850
1123
  archive = await readArchive(sourcePath);
851
1124
  archiveName = sourcePath;
@@ -907,13 +1180,12 @@ async function runPluginPackage(args, globalOpts) {
907
1180
  projectPath: options.scope === "project" ? cwd : null
908
1181
  };
909
1182
  serverCapability = summary.installCapabilities?.version === "1" ? summary.installCapabilities.targets.find((item) => item.targetAgent === agent && item.path === "native") : void 0;
910
- archive = await registry.download(namespace, slug, version);
911
- if (sha256(archive) !== detail.release.artifactSha256 || archive.length !== detail.release.artifactSize) throw new Error("Plugin archive digest/size integrity mismatch");
1183
+ archive = await registry.download(namespace, slug, version, format);
1184
+ if (format === "zip" && (sha256(archive) !== detail.release.artifactSha256 || archive.length !== detail.release.artifactSize)) throw new Error("Plugin archive digest/size integrity mismatch");
912
1185
  tree = await inspectPluginArchive(archive, archiveName);
913
1186
  if (tree.treeDigest !== detail.release.treeDigest) throw new Error("Plugin tree digest integrity mismatch");
914
1187
  checkFiles(detail, tree);
915
1188
  }
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");
917
1189
  const artifactContainer = join(storeRoot, ".bfi", "plugin-artifacts", `${pluginInstallKey(identity)}-${randomUUID()}`);
918
1190
  const artifactDirectory = join(artifactContainer, "plugin");
919
1191
  const manifest = nativeManifestFormats[agent].map((format) => tree.manifests.find((item) => item.root === "" && item.format === format)).find(Boolean);
@@ -956,9 +1228,11 @@ async function runPluginPackage(args, globalOpts) {
956
1228
  }, null, 2));
957
1229
  return;
958
1230
  }
959
- if (options.exportPath) {
960
- const target = resolve(cwd, options.exportPath);
961
- if (/\.zip$/iu.test(archiveName) !== /\.zip$/iu.test(target) || !isPluginArchive(target)) throw new Error("Export extension must match original archive format");
1231
+ if (options.exportPath || options.command === "download") {
1232
+ const suffix = format === "zip" ? ".zip" : format === "tar.gz" ? ".tar.gz" : ".tgz";
1233
+ const target = resolve(cwd, options.exportPath ?? `${baseName(options.source)}-${identity.version}${suffix}`);
1234
+ const targetFormat = archiveFormat(target);
1235
+ if (!targetFormat || targetFormat === "zip" !== (format === "zip")) throw new Error("Export extension must match --format");
962
1236
  await writeFile(target, archive, {
963
1237
  flag: "wx",
964
1238
  mode: 384
@@ -966,17 +1240,18 @@ async function runPluginPackage(args, globalOpts) {
966
1240
  console.log(JSON.stringify({
967
1241
  state: "EXPORTED",
968
1242
  exported: target,
1243
+ format,
1244
+ version: identity.version,
969
1245
  archiveSha256: sha256(archive),
970
1246
  treeDigest: tree.treeDigest,
971
1247
  archiveRoot: root,
972
- originalArchive: true,
973
1248
  installed: false,
974
1249
  registered: false,
975
1250
  loaded: false
976
1251
  }, null, 2));
977
1252
  return;
978
1253
  }
979
- if (options.preparePath) {
1254
+ if (options.preparePath || options.command === "prepare") {
980
1255
  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
1256
  if (plan.reasons.includes("CLI_UPGRADE_REQUIRED")) throw new Error("CLI_UPGRADE_REQUIRED");
982
1257
  console.log(JSON.stringify(await prepareNativeHandoff({
@@ -985,7 +1260,9 @@ async function runPluginPackage(args, globalOpts) {
985
1260
  scope: options.scope,
986
1261
  version: identity.version,
987
1262
  hostVersion,
988
- directory: resolve(cwd, options.preparePath)
1263
+ directory: resolve(cwd, options.preparePath ?? `${baseName(options.source)}-handoff`),
1264
+ market: handoffMarket(identity),
1265
+ update: options.command === "update"
989
1266
  }), null, 2));
990
1267
  return;
991
1268
  }
@@ -1,5 +1,5 @@
1
- import { checkPath, readConfig } from "./files.mjs";
2
1
  import { canonical, hash } from "./digest.mjs";
2
+ import { checkPath, readConfig } from "../cli.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { constants, realpathSync } from "node:fs";
5
5
  import { dirname, join, parse, resolve } from "node:path";