bfi-cli 0.1.103 → 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.
package/README.md CHANGED
@@ -15,27 +15,33 @@ bfi package install @team/kit --agent claude-code
15
15
  ## 明确选择插件
16
16
 
17
17
  ```sh
18
- bfi skills package install @team/kit --as plugin --version 1.0.0
19
- bfi package install @team/kit --as plugin --agent claude-code --version 1.0.0 --plan
20
- bfi package install @team/kit --as plugin --agent claude-code --version 1.0.0 --export ./kit-1.0.0.zip
21
- bfi package install @team/kit --as plugin --agent claude-code --scope user --version 1.0.0 --prepare-native ./kit-1.0.0-native
22
- bfi package install ./kit.tgz --as plugin --agent codex --root bundle --version 1.0.0 --plan
18
+ # 下载并校验默认安装版本(= 最新发布版),不安装
19
+ bfi skills package download @team/kit --as plugin -o ./kit.zip
20
+ bfi skills package download @team/kit --as plugin --format tgz -o ./kit.tgz
21
+ bfi skills package download @team/kit --as plugin --format tar.gz -o ./kit.tar.gz
22
+
23
+ # 生成版本化原生目录并更新本地市场索引;由用户执行输出中的宿主命令
24
+ bfi skills package prepare @team/kit --as plugin --agent claude-code --dir ./kit-handoff
25
+
26
+ # 按默认版本重新下载/交接,不依赖安装记录
27
+ bfi skills package update @team/kit --as plugin --agent claude-code --dir ./kit-handoff
28
+
29
+ # 把命名空间注册为 Claude Code 市场(写入 ~/.claude/settings.json,headersHelper 使用 bfi 登录态)
30
+ bfi skills package marketplace add global
31
+
32
+ # 本地归档
33
+ bfi package prepare ./kit.tgz --as plugin --agent codex --root bundle --dir ./kit-handoff
23
34
  bfi package list --as plugin --scope project
24
35
  ```
25
36
 
26
- - Claude Code **2.1.251** 的已验证组件开放受控原生注册;仍须满足精确发布版本的服务端授权、宿主/adapter 版本和 LOAD 证据。没有这些证据时不会安装。Codex **0.136.0** 仅开放原生技能型插件的人工交接;ZCode、CodeBuddy、Qwen Code 未实测,只导出并明确提示待适配。详见 [插件验证矩阵](PLUGIN-VALIDATION.md)。
27
- - 通用安装命令不必指定 Agent 和范围,CLI 会在交互终端中询问;非交互调用必须显式选择,`--yes` 不会代选。不支持的宿主或组件仍明确停止,不降级成技能安装。
28
- - `--plan` 只给计划/阻断原因;`--export` 原样导出归档,不计作安装。目标文件必须不存在,扩展名与源格式一致。
29
- - `--prepare-native <新目录>` 生成完整原始插件目录及独立市场索引,输出真实宿主命令;不注册宿主、不执行 Hooks、不创建 bfi 安装归属。只在宿主版本、作用域及全部必要组件受支持时生成。审阅后由用户确认执行;安装后保留该目录。人工交接的更新/卸载由原生宿主管理。
30
- - `--plan` 返回 READY 后,去掉 `--plan` 并确认可原生注册;非交互需显式 `--yes`。结果 REGISTERED 仅表示注册和文件归属已核对,需重启宿主检查所有组件,不能视为运行功能验证通过。
31
- - 市场来源必须写完整名称 `@namespace/package`,不要求用户输入数据库 ID。未指定版本时只解析一次可见 stable;没有 stable 则要求明确版本,不猜 latest。
32
- - 本地支持 zip/tgz/tar.gz,多根必须用 `--root` 选择;保留全部原始文件、执行位和未知字段,不自动转换各宿主格式。缺原生版本时可用 `--version` 描述本地导出身份,但不会改源文件或绕过市场正式发布的版本校验。
33
- - 原生安装/卸载/交接要求明确范围:`--scope user`(或 `-g`)/ `--scope project`;交互终端省略时询问,非交互或 `--yes` 不能代选。只读计划/导出/列表省略时按项目范围查看。插件独立使用 `.bfi/plugin-lock.json`,不与旧技能锁混用。
34
- - 未知安装结果保留 PREPARED 并阻止盲目重试;更新/卸载必须证明精确来源、宿主、范围、版本及安装归属。
35
- - 同一已安装内容重复安装不重复写入;原生版本更新暂不自动替换,提示 NATIVE_UPDATE_REQUIRES_USER 并保留旧版。卸载保留数据、市场索引及原始目录;重装使用新的独立市场,不覆盖保留目录。明确的同名预检冲突记录 FAILED,可解决后重试;原生命令结果不明才保留 PREPARED。
36
- - 所有路径都先执行归档安全检查。疑似凭据、非法路径、缺失关键组件或不支持的格式会停止,不能为导出或安装绕过,也不会自动降级成技能安装。
37
-
38
- 服务器插件功能默认开启;显式设置 `PACKAGE_PLUGIN_ENABLED=false` 时仍可使用既有技能包,但插件市场操作不可用。本地原始包计划/导出不等于服务器发布。
37
+ - 版本默认取「默认安装版本」,即最近一次发布;无需 `--version`。没有默认版本时才要求显式版本,不猜 latest。
38
+ - Claude Code **2.1.251** 与 Codex **0.136.0** 已实现适配器,但原生自动注册仍受服务端能力与运行时 LOAD 证据门禁;不满足时使用 `download` / `prepare`,不会降级成技能安装。ZCode、CodeBuddy、Qwen Code 只导出/交接。
39
+ - `download` 不要求 `--agent`,只导出并校验归档;支持 `--format zip|tgz|tar.gz`(默认按输出扩展名或 zip)。zip 校验归档摘要,tar 格式校验 treeDigest 与逐文件摘要;目标文件必须不存在。
40
+ - `prepare` 生成 `versions/<version>/plugin` 与市场索引(`.claude-plugin/marketplace.json` 或 `.agents/plugins/marketplace.json`),更新时原子改写索引指向新版本;不注册宿主、不执行 Hooks、不创建安装归属。
41
+ - `update`、`download`、`prepare` 均不以安装记录为前提;记录只用于原生注册的归属校验与卸载。
42
+ - 市场来源必须写完整名称 `@namespace/package`;跨来源凭据不转发。
43
+ - 原生安装/卸载要求明确范围:`--scope user`(或 `-g`)/ `--scope project`;交互终端省略时询问,非交互或 `--yes` 不能代选。
44
+ - 未知安装结果保留 PREPARED 并阻止盲目重试;卸载保留数据与市场索引。
39
45
 
40
46
  ## 独立 MCP 安装与测试
41
47
 
@@ -4,9 +4,8 @@ import { inspectPluginArchive } from "./intent.mjs";
4
4
  import { loadGlobalConfig, loadProjectConfig, resolveConfig } from "./config.mjs";
5
5
  import { agents, detectInstalledAgents, setAgents } from "./agents.mjs";
6
6
  import { builtinCommands, normalizeConfig, variables } from "./model.mjs";
7
- import { SkillHubAPI, checkPath, getAgentBaseDir, planInstall, readConfig, readDocument, serversIn, targetFor, writeConfig } from "../cli.mjs";
8
7
  import { canonical, hash, stableManifest, verifyPlan } from "./digest.mjs";
9
- import { InstallationClient, credentialForRegistry, registryUrl } from "./client.mjs";
8
+ import { InstallationClient, SkillHubAPI, checkPath, credentialForRegistry, getAgentBaseDir, planInstall, readConfig, readDocument, registryUrl, serversIn, targetFor, writeConfig } from "../cli.mjs";
10
9
  import { resolveAgents } from "./agents2.mjs";
11
10
  import { assertWritable, atomicPrivate, checkpoint, fingerprint, operationRoot, ownershipDigest, pathKey, readReceipt, receipts, safeDirectory, treeFingerprint, withInstallationLock } from "./receipt.mjs";
12
11
  import { randomUUID } from "node:crypto";
@@ -572,8 +572,9 @@ var PluginRegistryClient = class {
572
572
  async release(namespace, slug, version) {
573
573
  return this.json(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}`);
574
574
  }
575
- async download(namespace, slug, version, maximum = pluginArchiveLimits.compressedBytes) {
576
- return this.bytes(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}/download`, maximum);
575
+ async download(namespace, slug, version, format = "zip", maximum = pluginArchiveLimits.compressedBytes) {
576
+ const suffix = format === "zip" ? "" : `?format=${encodeURIComponent(format)}`;
577
+ return this.bytes(`${this.base(namespace, slug)}/plugin/releases/${segment(version)}/download${suffix}`, maximum);
577
578
  }
578
579
  };
579
580
  const pluginTargets = [
@@ -600,10 +601,13 @@ function parsePackageIntent(args) {
600
601
  "--plan",
601
602
  "--export",
602
603
  "--prepare-native",
604
+ "--dir",
605
+ "--output",
606
+ "--format",
603
607
  "--scope",
604
608
  "--root",
605
609
  "--operation"
606
- ].includes(value) || /^(--plan|--export|--prepare-native|--scope|--root|--operation)=/u.test(value)) || isPluginArchive(output[1] ?? ""))) throw new Error("Plugin archives and plugin options require explicit --as plugin; no skills were installed");
610
+ ].includes(value) || /^(--plan|--export|--prepare-native|--dir|--output|--format|--scope|--root|--operation)=/u.test(value)) || isPluginArchive(output[1] ?? ""))) throw new Error("Plugin archives and plugin options require explicit --as plugin; no skills were installed");
607
611
  return {
608
612
  mode,
609
613
  args: output
@@ -615,12 +619,17 @@ function parsePluginArguments(args) {
615
619
  i: "install",
616
620
  update: "update",
617
621
  up: "update",
622
+ download: "download",
623
+ dl: "download",
624
+ export: "download",
625
+ prepare: "prepare",
626
+ handoff: "prepare",
618
627
  uninstall: "uninstall",
619
628
  rm: "uninstall",
620
629
  list: "list",
621
630
  ls: "list"
622
631
  }[args[0] ?? ""];
623
- if (!command) throw new Error("Plugin intent supports install, update, uninstall and list");
632
+ if (!command) throw new Error("Plugin intent supports install, update, download, prepare, uninstall and list");
624
633
  const result = {
625
634
  command,
626
635
  scope: "project",
@@ -634,7 +643,10 @@ function parsePluginArguments(args) {
634
643
  "--scope",
635
644
  "--root",
636
645
  "--export",
646
+ "--output",
647
+ "--format",
637
648
  "--prepare-native",
649
+ "--dir",
638
650
  "--operation",
639
651
  "--registry",
640
652
  "--token",
@@ -663,9 +675,16 @@ function parsePluginArguments(args) {
663
675
  if (!["project", "user"].includes(value)) throw new Error("Plugin scope must be project or user");
664
676
  result.scope = value;
665
677
  } else if (flag === "--root") result.root = value;
666
- else if (flag === "--export") result.exportPath = value;
667
- else if (flag === "--prepare-native") result.preparePath = value;
668
- else if (flag === "--operation") result.operationId = value;
678
+ else if (flag === "--export" || flag === "--output") result.exportPath = value;
679
+ else if (flag === "--prepare-native" || flag === "--dir") result.preparePath = value;
680
+ else if (flag === "--format") {
681
+ if (![
682
+ "zip",
683
+ "tgz",
684
+ "tar.gz"
685
+ ].includes(value)) throw new Error("Plugin archive format must be zip, tgz or tar.gz");
686
+ result.format = value;
687
+ } else if (flag === "--operation") result.operationId = value;
669
688
  } else if (flag === "--global") result.scope = "user";
670
689
  else if (flag === "--plan") result.planOnly = true;
671
690
  else if (flag === "--json") result.json = true;
@@ -677,9 +696,21 @@ function parsePluginArguments(args) {
677
696
  if (command !== "list" && !result.source && !(command === "uninstall" && result.operationId)) throw new Error("Specify @namespace/package or a local zip/tgz path");
678
697
  if (result.source && !/^@[^/\s]+\/[^/\s]+$/u.test(result.source) && !isPluginArchive(result.source)) throw new Error("Use a namespace-qualified @namespace/package reference or local archive");
679
698
  if (result.planOnly && result.exportPath) throw new Error("Plan and export are separate operations");
680
- if (result.preparePath && (result.planOnly || result.exportPath || command !== "install")) throw new Error("Native handoff, plan and export are separate install operations");
681
- if (result.root !== void 0 && !isPluginArchive(result.source ?? "") || result.operationId && command !== "uninstall" || (result.exportPath || result.root !== void 0) && command !== "install") throw new Error("Plugin option is invalid for this operation");
682
- if (command === "list" && (result.source || result.version || result.planOnly || result.exportPath || result.operationId)) throw new Error("Invalid plugin list options");
699
+ if (result.planOnly && command !== "install") throw new Error("--plan is only available for plugin install");
700
+ if (result.preparePath && result.exportPath) throw new Error("Download and native handoff are separate operations");
701
+ if (command === "download" && (result.planOnly || result.preparePath)) throw new Error("Download only supports --output; use prepare for native handoff");
702
+ if (command === "prepare" && !result.agent) throw new Error("AGENT_REQUIRED: native handoff needs --agent claude-code or --agent codex");
703
+ if ((result.exportPath || result.preparePath) && ![
704
+ "install",
705
+ "update",
706
+ "download",
707
+ "prepare"
708
+ ].includes(command)) throw new Error("Plugin option is invalid for this operation");
709
+ if (result.root !== void 0 && !isPluginArchive(result.source ?? "")) throw new Error("--root only applies to local plugin archives");
710
+ if (result.operationId && command !== "uninstall") throw new Error("--operation is only valid for uninstall");
711
+ if (command === "list" && (result.source || result.version || result.planOnly || result.exportPath || result.preparePath || result.operationId)) throw new Error("Invalid plugin list options");
712
+ if (command === "prepare" && result.planOnly) throw new Error("Plan and native handoff are separate operations");
713
+ if (result.format && !(command === "download" || result.exportPath)) throw new Error("--format requires download or --export");
683
714
  return result;
684
715
  }
685
716
  export { PluginInstallRecords, PluginRegistryClient, inspectPluginArchive, isPluginArchive, isPluginVersion, materializePluginTree, normalizeRegistry, parsePackageIntent, parsePluginArguments, pluginArchiveLimits, pluginInstallKey, pluginSlotKey, pluginTargets, selectPluginRoot, sha256, treeDigest };
@@ -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 "../cli.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";
package/dist/cli.mjs CHANGED
@@ -4,12 +4,12 @@ import { isCancel } from "./_chunks/libs/@clack/core.mjs";
4
4
  import { cancel, confirm, groupMultiselect, intro, log, multiselect as multiselect$1, note, outro, password, select, spinner } from "./_chunks/libs/@clack/prompts.mjs";
5
5
  import { require_picocolors } from "./_chunks/libs/picocolors.mjs";
6
6
  import { addSkillToLocalLock, addSkillToLock, computeSkillFolderHash, discoverSkills, dismissPrompt, filterSkills, getAllLockedSkills, getGitHubToken, getLastSelectedAgents, getSkillDisplayName, getSkillFromLock, isPromptDismissed, parseFrontmatter, parseSkillMd, readLocalLock, readSkillLock, removeSkillFromLock, sanitizeMetadata, saveSelectedAgents, stripTerminalEscapes } from "./_chunks/skills.mjs";
7
- import { PluginRegistryClient, parsePackageIntent } from "./_chunks/intent.mjs";
7
+ import { PluginRegistryClient, normalizeRegistry, parsePackageIntent } from "./_chunks/intent.mjs";
8
8
  import { loadGlobalConfig, loadLockfile, loadProjectConfig, resolveConfig, saveGlobalConfig, saveLockfile, setConfigValue } from "./_chunks/config.mjs";
9
9
  import { esm_default } from "./_chunks/libs/simple-git.mjs";
10
10
  import { EVE_SUBAGENTS_DIR, agents, detectInstalledAgents, getEveSubagents, getNonUniversalAgents, getUniversalAgents, getVisibleUniversalAgents, isUniversalAgent } from "./_chunks/agents.mjs";
11
- import { packageSkillName } from "./_chunks/package-skill-identity.mjs";
12
11
  import { entriesFromJson, mapStrings, normalizeConfig, object, resolveVariables, validName, variablePattern, variables } from "./_chunks/model.mjs";
12
+ import { hash, verifyPlan } from "./_chunks/digest.mjs";
13
13
  import { createRequire } from "node:module";
14
14
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
15
15
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
@@ -23,7 +23,7 @@ import * as readline from "readline";
23
23
  import { Writable } from "stream";
24
24
  import https from "node:https";
25
25
  import http from "node:http";
26
- import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
26
+ import { lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
27
27
  import zlib, { gunzipSync, inflateRawSync } from "node:zlib";
28
28
  import os$1, { homedir as homedir$1 } from "node:os";
29
29
  import { promisify } from "util";
@@ -3560,7 +3560,7 @@ async function installFromSkillHub(parsed, options, spinner) {
3560
3560
  process.exit(1);
3561
3561
  }
3562
3562
  if (options.packageSkill) {
3563
- const { scopePackageSkill } = await import("./_chunks/package-skill-identity.mjs").then((n) => n.package_skill_identity_exports);
3563
+ const { scopePackageSkill } = await Promise.resolve().then(() => package_skill_identity_exports);
3564
3564
  try {
3565
3565
  return {
3566
3566
  skills: scopePackageSkill(skills, namespace, slug),
@@ -5514,7 +5514,20 @@ async function runUpdate(args = []) {
5514
5514
  totalFail += failCount;
5515
5515
  totalFound += foundCount;
5516
5516
  }
5517
- if (options.skills && totalFound === 0) console.log(`${DIM$9}未找到匹配的已安装技能:${options.skills.join(", ")}${RESET$9}`);
5517
+ if (options.skills && totalFound === 0) {
5518
+ console.log(`${DIM$9}未找到安装记录,按首次安装重新获取:${options.skills.join(", ")}${RESET$9}`);
5519
+ const { runAdd } = await Promise.resolve().then(() => add_exports);
5520
+ for (const skill of options.skills) try {
5521
+ await runAdd([skill], {
5522
+ yes: true,
5523
+ global: options.global || scope === "global"
5524
+ });
5525
+ totalSuccess += 1;
5526
+ } catch (error) {
5527
+ totalFail += 1;
5528
+ console.log(`${DIM$9}✗ ${skill}:${error instanceof Error ? error.message : "安装失败"}${RESET$9}`);
5529
+ }
5530
+ }
5518
5531
  console.log();
5519
5532
  if (totalSuccess > 0) console.log(`${TEXT$7}✓ 已更新 ${totalSuccess} 个技能${RESET$9}`);
5520
5533
  if (totalFail > 0) {
@@ -6984,6 +6997,162 @@ function maybePrintAnalyticsNotice(env = process.env) {
6984
6997
  console.log();
6985
6998
  } catch {}
6986
6999
  }
7000
+ function registryUrl(value) {
7001
+ const url = new URL(value);
7002
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("无效的 registry 地址。");
7003
+ return url.toString().replace(/\/$/, "");
7004
+ }
7005
+ function credentialForRegistry(registry, config) {
7006
+ return config?.token && registryUrl(registry) === registryUrl(config.registry ?? "https://bfi.yyuap.com:811") ? config.token : void 0;
7007
+ }
7008
+ async function readResponse(response, limit) {
7009
+ const chunks = [];
7010
+ let size = 0;
7011
+ if (!response.body) throw new Error("服务返回空响应。");
7012
+ const reader = response.body.getReader();
7013
+ try {
7014
+ while (true) {
7015
+ const chunk = await reader.read();
7016
+ if (chunk.done) break;
7017
+ size += chunk.value.length;
7018
+ if (size > limit) throw new Error("响应超过安全大小上限。");
7019
+ chunks.push(chunk.value);
7020
+ }
7021
+ } finally {
7022
+ await reader.cancel().catch(() => {});
7023
+ }
7024
+ return Buffer.concat(chunks);
7025
+ }
7026
+ var InstallationClient = class {
7027
+ registry;
7028
+ token;
7029
+ fetcher;
7030
+ constructor(registry, token, fetcher = fetch) {
7031
+ this.registry = registryUrl(registry);
7032
+ this.token = token;
7033
+ this.fetcher = fetcher;
7034
+ }
7035
+ async request(path, method = "GET", body) {
7036
+ const headers = { Accept: "application/json" };
7037
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
7038
+ if (method === "POST") {
7039
+ headers["Content-Type"] = "application/json";
7040
+ if (!this.token) {
7041
+ const initial = await this.request("/api/web/mcps?size=1");
7042
+ const cookie = initial.headers.get("set-cookie")?.match(/(?:^|,\s*)XSRF-TOKEN=([^;]+)/)?.[1];
7043
+ await initial.body?.cancel();
7044
+ if (!cookie) throw new Error("无法获取匿名 CSRF 凭据,请检查服务配置。");
7045
+ headers.Cookie = `XSRF-TOKEN=${cookie}`;
7046
+ headers["X-XSRF-TOKEN"] = decodeURIComponent(cookie);
7047
+ }
7048
+ }
7049
+ let response;
7050
+ try {
7051
+ response = await this.fetcher(`${this.registry}${path}`, {
7052
+ method,
7053
+ headers,
7054
+ body: body === void 0 ? void 0 : JSON.stringify(body),
7055
+ redirect: "error",
7056
+ signal: AbortSignal.timeout(12e4)
7057
+ });
7058
+ } catch {
7059
+ throw new Error("网络连接失败或服务重定向;未转发登录凭据。");
7060
+ }
7061
+ if (response.status === 401) throw new Error("登录无效或需要登录,请运行 bfi auth login 后重试。");
7062
+ if (response.status === 403) throw new Error("权限不足或 CSRF 校验失败;不会覆盖现有登录。");
7063
+ if (response.status >= 300 && response.status < 400) throw new Error("不允许服务重定向。");
7064
+ if (!response.ok) throw new Error(`安装服务请求失败(HTTP ${response.status}),请检查权限、计划有效期和服务版本。`);
7065
+ return response;
7066
+ }
7067
+ async plan(response, kind, ref, id) {
7068
+ const envelope = JSON.parse((await readResponse(response, 12 * 1024 ** 2)).toString("utf8"));
7069
+ if (envelope.code !== 0) throw new Error("安装服务拒绝请求。");
7070
+ const plan = verifyPlan(envelope.data);
7071
+ if (registryUrl(plan.registry) !== this.registry || plan.kind !== kind || plan.ref !== ref || id && plan.id !== id) throw new Error("计划与请求来源或资源不匹配。");
7072
+ if (Date.parse(plan.expiresAt) <= Date.now()) throw new Error("内容计划已过期,请重新确认最新内容。");
7073
+ return plan;
7074
+ }
7075
+ async resolve(kind, ref) {
7076
+ return this.plan(await this.request("/api/v1/installation-content-plans", "POST", {
7077
+ kind,
7078
+ ref
7079
+ }), kind, ref);
7080
+ }
7081
+ async get(id, kind, ref) {
7082
+ return this.plan(await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(id)}`), kind, ref, id);
7083
+ }
7084
+ async artifact(plan) {
7085
+ const response = await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(plan.id)}/artifact`);
7086
+ const bytes = await readResponse(response, 510 * 1024 ** 2);
7087
+ if (response.headers.get("X-Archive-SHA256") !== hash(bytes)) throw new Error("下载归档摘要不匹配。");
7088
+ return bytes;
7089
+ }
7090
+ async verifyAccount() {
7091
+ if (!this.token) throw new Error("BFI MCP 需要本机登录态,请先运行 bfi auth login;不会把 Token 写进 MCP 配置。");
7092
+ const response = await this.request("/api/v1/whoami");
7093
+ let envelope;
7094
+ try {
7095
+ envelope = JSON.parse((await readResponse(response, 1048576)).toString("utf8"));
7096
+ } catch {
7097
+ throw new Error("本机 BFI 登录响应无效;未覆盖现有登录。");
7098
+ }
7099
+ if (!envelope || typeof envelope !== "object") throw new Error("本机 BFI 登录响应无效。");
7100
+ if (envelope.code !== void 0 && envelope.code !== 0) throw new Error("本机 BFI 登录态验证失败。");
7101
+ const data = envelope.data ?? envelope;
7102
+ if (!data?.id && !data?.user?.id) throw new Error("本机 BFI 登录响应无效。");
7103
+ }
7104
+ };
7105
+ function printArchiveHeaders(globalOpts) {
7106
+ const { registry } = resolveConfig(globalOpts);
7107
+ const token = typeof globalOpts.token === "string" ? globalOpts.token : credentialForRegistry(registry, loadGlobalConfig());
7108
+ if (!token) throw new Error("BFI_TOKEN_REQUIRED: run bfi auth login first");
7109
+ console.log(JSON.stringify({ Authorization: `Bearer ${token}` }));
7110
+ }
7111
+ async function addClaudeMarketplace(namespace, globalOpts) {
7112
+ const { registry } = resolveConfig(globalOpts);
7113
+ const base = normalizeRegistry(String(registry));
7114
+ const url = `${base}/api/web/namespaces/${encodeURIComponent(namespace)}/plugin-marketplace.json`;
7115
+ const market = `skillhub-${namespace}`;
7116
+ const path = join$1(homedir$1(), ".claude", "settings.json");
7117
+ const existing = await readFile(path, "utf8").then(JSON.parse).catch(() => ({}));
7118
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) throw new Error("CLAUDE_SETTINGS_INVALID");
7119
+ const known = { ...existing.extraKnownMarketplaces };
7120
+ known[market] = { source: {
7121
+ source: "url",
7122
+ url,
7123
+ headersHelper: `bfi skills package archive-headers --registry ${base}`
7124
+ } };
7125
+ existing.extraKnownMarketplaces = known;
7126
+ await mkdir(dirname$1(path), {
7127
+ recursive: true,
7128
+ mode: 448
7129
+ });
7130
+ const temporary = `${path}.${process.pid}.tmp`;
7131
+ await writeFile(temporary, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
7132
+ await rename(temporary, path);
7133
+ return {
7134
+ market,
7135
+ url,
7136
+ settings: path,
7137
+ next: `claude plugin install <plugin>@${market} --scope user`,
7138
+ message: "Claude Code 将通过 headersHelper 使用 bfi 当前登录态读取市场和归档;安装后可用 claude plugin update 更新。"
7139
+ };
7140
+ }
7141
+ var package_skill_identity_exports = /* @__PURE__ */ __exportAll({
7142
+ packageSkillName: () => packageSkillName,
7143
+ scopePackageSkill: () => scopePackageSkill
7144
+ });
7145
+ function packageSkillName(namespace, slug) {
7146
+ if (!namespace || !slug) throw new Error("Namespace and slug are required");
7147
+ return `bfi-skill-${createHash("sha256").update(JSON.stringify([namespace, slug])).digest("hex")}`;
7148
+ }
7149
+ function scopePackageSkill(skills, namespace, slug) {
7150
+ if (skills.length !== 1) throw new Error("A package reference must resolve to exactly one skill");
7151
+ return [{
7152
+ ...skills[0],
7153
+ name: packageSkillName(namespace, slug)
7154
+ }];
7155
+ }
6987
7156
  function checkNode(node) {
6988
7157
  if (node.type === "object") {
6989
7158
  const keys = (node.children ?? []).map((child) => String(child.children?.[0]?.value));
@@ -7536,6 +7705,12 @@ async function dispatchPackage(args, globalOpts) {
7536
7705
  case "up":
7537
7706
  await handleUpdate(args.slice(1), api, globalOpts);
7538
7707
  break;
7708
+ case "marketplace":
7709
+ await handleMarketplace(args.slice(1), globalOpts);
7710
+ break;
7711
+ case "archive-headers":
7712
+ printArchiveHeaders(globalOpts);
7713
+ break;
7539
7714
  case "search":
7540
7715
  await handleSearch(args.slice(1), api, globalOpts);
7541
7716
  break;
@@ -7788,11 +7963,11 @@ async function handleInstall(args, api, globalOpts) {
7788
7963
  console.log(`${DIM$3}范围:${installGlobally ? "全局" : "项目"} | 方式:${installMode === "symlink" ? "符号链接" : "复制"}${RESET$3}`);
7789
7964
  console.log();
7790
7965
  } else {
7791
- const spinner$2 = spinner();
7792
- spinner$2.start("正在加载 Agent...");
7966
+ const spinner$1 = spinner();
7967
+ spinner$1.start("正在加载 Agent...");
7793
7968
  const installedAgents = await detectInstalledAgents();
7794
7969
  const totalAgents = Object.keys(agents).length;
7795
- spinner$2.stop(`${totalAgents} agents`);
7970
+ spinner$1.stop(`${totalAgents} agents`);
7796
7971
  const skillNames = skills.map((s) => s.displayName || s.slug);
7797
7972
  const skillSubject = skillNames.length <= 3 ? skillNames.map((n) => import_picocolors.default.cyan(n)).join(", ") : `${skills.length} 个技能`;
7798
7973
  if (installedAgents.includes("eve")) {
@@ -7975,12 +8150,12 @@ async function handleInstall(args, api, globalOpts) {
7975
8150
  try {
7976
8151
  const archive = join(work, "package.zip");
7977
8152
  const unpack = join(work, "content");
7978
- const spinner$1 = spinner();
7979
- spinner$1.start("正在下载组合包...");
8153
+ const spinner$2 = spinner();
8154
+ spinner$2.start("正在下载组合包...");
7980
8155
  await api.downloadPackage(namespace, slug, archive);
7981
- spinner$1.message("正在解压组合包...");
8156
+ spinner$2.message("正在解压组合包...");
7982
8157
  await extractZip(archive, unpack);
7983
- spinner$1.stop("组合包已就绪,开始安装技能");
8158
+ spinner$2.stop("组合包已就绪,开始安装技能");
7984
8159
  const entries = await readdir(unpack, { withFileTypes: true });
7985
8160
  if (entries.some((entry) => entry.isFile() && entry.name === "mcp.json")) mcpFile = join(unpack, "mcp.json");
7986
8161
  const directories = entries.filter((entry) => entry.isDirectory());
@@ -8158,10 +8333,7 @@ async function handleUpdate(args, api, globalOpts) {
8158
8333
  const pkgs = lock.packages || {};
8159
8334
  if (pkgRef) {
8160
8335
  const { namespace, slug } = parseSkillRef(pkgRef, globalOpts.namespace);
8161
- if (!pkgs[`@${namespace}/${slug}`]) {
8162
- console.log(`${YELLOW$1}包未安装:@${namespace}/${slug}${RESET$3}`);
8163
- return;
8164
- }
8336
+ if (!pkgs[`@${namespace}/${slug}`]) console.log(`${DIM$3}未找到安装记录,按首次安装重新执行:@${namespace}/${slug}${RESET$3}`);
8165
8337
  packagesToUpdate.push({
8166
8338
  namespace,
8167
8339
  slug
@@ -8196,6 +8368,12 @@ async function handleUpdate(args, api, globalOpts) {
8196
8368
  }
8197
8369
  console.log(`${GREEN$2}✓ 包更新完成${RESET$3}`);
8198
8370
  }
8371
+ async function handleMarketplace(args, globalOpts) {
8372
+ const [action, namespace] = args;
8373
+ if (action !== "add" || !namespace) throw new Error("用法:bfi skills package marketplace add <namespace>");
8374
+ const result = await addClaudeMarketplace(namespace, globalOpts);
8375
+ console.log(JSON.stringify(result, null, 2));
8376
+ }
8199
8377
  async function handleSearch(args, api, globalOpts) {
8200
8378
  const query = args.filter((a) => !a.startsWith("--")).join(" ") || void 0;
8201
8379
  const json = args.includes("--json");
@@ -10488,6 +10666,11 @@ ${BOLD}━━━ 技能包管理 ━━━${RESET}
10488
10666
  --prepare-native <dir> 准备完整原生目录和市场索引,审阅后交给宿主安装;不注册、不转技能
10489
10667
  --agent <a> 目标 agent(如 claude-code、codex、zcode)
10490
10668
  --global 安装到主目录(跨项目可用)
10669
+ skills package download <ref> --as plugin [--output <file>] [--format zip|tgz|tar.gz] 下载并校验插件归档(不安装、不需要 --agent)
10670
+ skills package prepare <ref> --as plugin --agent <a> [--dir <dir>] 生成版本化原生目录并更新市场索引
10671
+ skills package update <ref> --as plugin [--agent <a>] 按默认安装版本重新下载/交接,不依赖安装记录
10672
+ skills package marketplace add <namespace> 将命名空间注册为 Claude Code 市场(settings.json)
10673
+ skills package archive-headers 输出 marketplace/归档所需的认证头(供 headersHelper 调用)
10491
10674
 
10492
10675
  ${BOLD}━━━ 文档 ━━━${RESET}
10493
10676
  docs upload <file> 上传文档
@@ -10954,4 +11137,4 @@ main().catch((e) => {
10954
11137
  console.error(`✗ ${e?.message || e}`);
10955
11138
  process.exit(1);
10956
11139
  });
10957
- export { SkillHubAPI, agentIds, applyPlan, checkPath, decodeServer, getAgentBaseDir, loadTarget, planInstall, planRemove, readConfig, readDocument, serversIn, targetFor, writeConfig };
11140
+ export { InstallationClient, SkillHubAPI, agentIds, applyPlan, checkPath, credentialForRegistry, decodeServer, getAgentBaseDir, loadTarget, planInstall, planRemove, readConfig, readDocument, registryUrl, serversIn, targetFor, writeConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.103",
3
+ "version": "0.1.104",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,107 +0,0 @@
1
- import { hash, verifyPlan } from "./digest.mjs";
2
- function registryUrl(value) {
3
- const url = new URL(value);
4
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("无效的 registry 地址。");
5
- return url.toString().replace(/\/$/, "");
6
- }
7
- function credentialForRegistry(registry, config) {
8
- return config?.token && registryUrl(registry) === registryUrl(config.registry ?? "https://bfi.yyuap.com:811") ? config.token : void 0;
9
- }
10
- async function readResponse(response, limit) {
11
- const chunks = [];
12
- let size = 0;
13
- if (!response.body) throw new Error("服务返回空响应。");
14
- const reader = response.body.getReader();
15
- try {
16
- while (true) {
17
- const chunk = await reader.read();
18
- if (chunk.done) break;
19
- size += chunk.value.length;
20
- if (size > limit) throw new Error("响应超过安全大小上限。");
21
- chunks.push(chunk.value);
22
- }
23
- } finally {
24
- await reader.cancel().catch(() => {});
25
- }
26
- return Buffer.concat(chunks);
27
- }
28
- var InstallationClient = class {
29
- registry;
30
- token;
31
- fetcher;
32
- constructor(registry, token, fetcher = fetch) {
33
- this.registry = registryUrl(registry);
34
- this.token = token;
35
- this.fetcher = fetcher;
36
- }
37
- async request(path, method = "GET", body) {
38
- const headers = { Accept: "application/json" };
39
- if (this.token) headers.Authorization = `Bearer ${this.token}`;
40
- if (method === "POST") {
41
- headers["Content-Type"] = "application/json";
42
- if (!this.token) {
43
- const initial = await this.request("/api/web/mcps?size=1");
44
- const cookie = initial.headers.get("set-cookie")?.match(/(?:^|,\s*)XSRF-TOKEN=([^;]+)/)?.[1];
45
- await initial.body?.cancel();
46
- if (!cookie) throw new Error("无法获取匿名 CSRF 凭据,请检查服务配置。");
47
- headers.Cookie = `XSRF-TOKEN=${cookie}`;
48
- headers["X-XSRF-TOKEN"] = decodeURIComponent(cookie);
49
- }
50
- }
51
- let response;
52
- try {
53
- response = await this.fetcher(`${this.registry}${path}`, {
54
- method,
55
- headers,
56
- body: body === void 0 ? void 0 : JSON.stringify(body),
57
- redirect: "error",
58
- signal: AbortSignal.timeout(12e4)
59
- });
60
- } catch {
61
- throw new Error("网络连接失败或服务重定向;未转发登录凭据。");
62
- }
63
- if (response.status === 401) throw new Error("登录无效或需要登录,请运行 bfi auth login 后重试。");
64
- if (response.status === 403) throw new Error("权限不足或 CSRF 校验失败;不会覆盖现有登录。");
65
- if (response.status >= 300 && response.status < 400) throw new Error("不允许服务重定向。");
66
- if (!response.ok) throw new Error(`安装服务请求失败(HTTP ${response.status}),请检查权限、计划有效期和服务版本。`);
67
- return response;
68
- }
69
- async plan(response, kind, ref, id) {
70
- const envelope = JSON.parse((await readResponse(response, 12 * 1024 ** 2)).toString("utf8"));
71
- if (envelope.code !== 0) throw new Error("安装服务拒绝请求。");
72
- const plan = verifyPlan(envelope.data);
73
- if (registryUrl(plan.registry) !== this.registry || plan.kind !== kind || plan.ref !== ref || id && plan.id !== id) throw new Error("计划与请求来源或资源不匹配。");
74
- if (Date.parse(plan.expiresAt) <= Date.now()) throw new Error("内容计划已过期,请重新确认最新内容。");
75
- return plan;
76
- }
77
- async resolve(kind, ref) {
78
- return this.plan(await this.request("/api/v1/installation-content-plans", "POST", {
79
- kind,
80
- ref
81
- }), kind, ref);
82
- }
83
- async get(id, kind, ref) {
84
- return this.plan(await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(id)}`), kind, ref, id);
85
- }
86
- async artifact(plan) {
87
- const response = await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(plan.id)}/artifact`);
88
- const bytes = await readResponse(response, 510 * 1024 ** 2);
89
- if (response.headers.get("X-Archive-SHA256") !== hash(bytes)) throw new Error("下载归档摘要不匹配。");
90
- return bytes;
91
- }
92
- async verifyAccount() {
93
- if (!this.token) throw new Error("BFI MCP 需要本机登录态,请先运行 bfi auth login;不会把 Token 写进 MCP 配置。");
94
- const response = await this.request("/api/v1/whoami");
95
- let envelope;
96
- try {
97
- envelope = JSON.parse((await readResponse(response, 1048576)).toString("utf8"));
98
- } catch {
99
- throw new Error("本机 BFI 登录响应无效;未覆盖现有登录。");
100
- }
101
- if (!envelope || typeof envelope !== "object") throw new Error("本机 BFI 登录响应无效。");
102
- if (envelope.code !== void 0 && envelope.code !== 0) throw new Error("本机 BFI 登录态验证失败。");
103
- const data = envelope.data ?? envelope;
104
- if (!data?.id && !data?.user?.id) throw new Error("本机 BFI 登录响应无效。");
105
- }
106
- };
107
- export { InstallationClient, credentialForRegistry, registryUrl };
@@ -1,18 +0,0 @@
1
- import { __exportAll } from "./rolldown-runtime.mjs";
2
- import { createHash } from "node:crypto";
3
- var package_skill_identity_exports = /* @__PURE__ */ __exportAll({
4
- packageSkillName: () => packageSkillName,
5
- scopePackageSkill: () => scopePackageSkill
6
- });
7
- function packageSkillName(namespace, slug) {
8
- if (!namespace || !slug) throw new Error("Namespace and slug are required");
9
- return `bfi-skill-${createHash("sha256").update(JSON.stringify([namespace, slug])).digest("hex")}`;
10
- }
11
- function scopePackageSkill(skills, namespace, slug) {
12
- if (skills.length !== 1) throw new Error("A package reference must resolve to exactly one skill");
13
- return [{
14
- ...skills[0],
15
- name: packageSkillName(namespace, slug)
16
- }];
17
- }
18
- export { packageSkillName, package_skill_identity_exports };