@dp-bohrium/widget-cli 0.1.0 → 0.2.0

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/dist/cli.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as resolveWidgetProjects, i as checkWidgetProject, n as scaffoldDshWidget, o as compileAuthorManifest, r as buildWidgetPackage, t as auditDshPlugin } from "./dsh-DgkNH7L-.mjs";
3
- import { createRequire } from "node:module";
2
+ import { a as normalizeArchivePath, c as resolveWidgetProjects, i as collectWidgetPackageFiles, l as compileAuthorManifest, n as scaffoldDshWidget, o as storedZip, s as checkWidgetProject, t as auditDshPlugin } from "./dsh-BzxDpEcZ.mjs";
4
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
5
4
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
6
- import { mkdir, writeFile } from "node:fs/promises";
5
+ import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
7
6
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
8
7
  import { fileURLToPath } from "node:url";
9
8
  import { spawn, spawnSync } from "node:child_process";
@@ -76,9 +75,9 @@ const RESOURCE_BUDGET = {
76
75
  },
77
76
  retry: {
78
77
  maxAttempts: 1,
79
- initialDelayMs: 0,
80
- maxDelayMs: 0,
81
- totalMs: 0
78
+ initialDelayMs: 250,
79
+ maxDelayMs: 1e3,
80
+ totalMs: 5e3
82
81
  },
83
82
  failure: {
84
83
  consecutiveFailures: 3,
@@ -172,11 +171,15 @@ function parseArgs(argv) {
172
171
  };
173
172
  }
174
173
  function createWidgetDevError(error, context = {}) {
174
+ const code = error?.cliCode ?? error?.code;
175
175
  return {
176
- code: typeof error?.code === "string" && error.code.startsWith("WIDGET_DEV_") ? error.code : "WIDGET_DEV_FAILED",
176
+ code: typeof code === "string" && /^(?:WIDGET_DEV_|WIDGET_CLI_)/u.test(code) ? code : "WIDGET_DEV_FAILED",
177
177
  message: error instanceof Error ? error.message : String(error),
178
- context,
179
- recovery: context.phase === "watch-build" ? "读取 status.lastBuildError,修复工程后重新保存源码或重新执行 build。" : "先读取 error.context 和 build 输出,修复工程或宿主连接后重试。"
178
+ context: {
179
+ ...context,
180
+ ...error?.context
181
+ },
182
+ recovery: error?.recovery ?? (context.phase === "watch-build" ? "读取 status.lastBuildError,修复工程后重新保存源码或重新执行 build。" : "先读取 error.context 和 build 输出,修复工程或宿主连接后重试。")
180
183
  };
181
184
  }
182
185
  function secureHeaders(contentType) {
@@ -187,7 +190,7 @@ function secureHeaders(contentType) {
187
190
  "x-content-type-options": "nosniff"
188
191
  };
189
192
  }
190
- function json(response, status, value, extraHeaders = {}) {
193
+ function json$1(response, status, value, extraHeaders = {}) {
191
194
  const body = JSON.stringify(value);
192
195
  response.writeHead(status, {
193
196
  ...secureHeaders("application/json; charset=utf-8"),
@@ -406,9 +409,9 @@ function panelDevelopmentDescriptor(generation, profileUrl, statusUrl) {
406
409
  presentationRequests
407
410
  };
408
411
  }
409
- function readProject(projectRoot) {
412
+ function readProject(projectRoot, manifestOptions = {}) {
410
413
  const sourceManifest = JSON.parse(readFileSync(join(projectRoot, "manifest.json"), "utf8"));
411
- const manifest = compileAuthorManifest(sourceManifest);
414
+ const manifest = compileAuthorManifest(sourceManifest, manifestOptions);
412
415
  const entryPath = manifest.widgetConfig.entry;
413
416
  const entryFile = join(projectRoot, entryPath);
414
417
  if (!existsSync(entryFile) || statSync(entryFile).size === 0) throw new Error(`entry 不存在或为空:${entryPath};先运行 build,或移除 --no-build`);
@@ -458,8 +461,8 @@ function collectGenerationAssets(projectRoot, entryPath, limits) {
458
461
  };
459
462
  }).sort((a, b) => a.path.localeCompare(b.path));
460
463
  }
461
- function createDevelopmentGeneration(projectRoot, number = 0, limits = GENERATION_LIMITS) {
462
- const { manifest, entryPath } = readProject(projectRoot);
464
+ function createDevelopmentGeneration(projectRoot, number = 0, limits = GENERATION_LIMITS, manifestOptions = {}) {
465
+ const { manifest, entryPath } = readProject(projectRoot, manifestOptions);
463
466
  const collected = collectGenerationAssets(projectRoot, entryPath, limits);
464
467
  if (!collected.some(({ path }) => path === entryPath)) throw new Error(`entry 未进入 generation:${entryPath}`);
465
468
  const hash = createHash("sha256");
@@ -720,9 +723,28 @@ function runBuild(projectRoot, { capture = false } = {}) {
720
723
  });
721
724
  }
722
725
  async function startWidgetDevelopment(projectRoot, options) {
726
+ const buildProject = options.buildProject ?? runBuild;
723
727
  const emit = options.emit ?? ((line, error = false) => error ? console.error(line) : console.log(line));
724
- if (options.build) await runBuild(projectRoot, { capture: options.json });
725
- let successfulGeneration = createDevelopmentGeneration(projectRoot, 0);
728
+ const validateProject = async () => {
729
+ if (!options.checkProject) return;
730
+ const check = await options.checkProject(projectRoot, {
731
+ ...options.manifestOptions,
732
+ requireBuiltEntry: true
733
+ });
734
+ if (!check.ok) {
735
+ const error = /* @__PURE__ */ new Error(`Widget Dev L0 check 失败:${check.errors.join("; ")}`);
736
+ error.code = "WIDGET_DEV_PROJECT_INVALID";
737
+ throw error;
738
+ }
739
+ };
740
+ await options.beforeBuild?.({
741
+ projectRoot,
742
+ initial: true,
743
+ build: options.build
744
+ });
745
+ if (options.build) await buildProject(projectRoot, { capture: options.json });
746
+ await validateProject();
747
+ let successfulGeneration = createDevelopmentGeneration(projectRoot, 0, GENERATION_LIMITS, options.manifestOptions);
726
748
  const retainedGenerations = /* @__PURE__ */ new Map([[successfulGeneration.guestCapability, successfulGeneration]]);
727
749
  const initial = {
728
750
  manifest: successfulGeneration.manifest,
@@ -754,7 +776,7 @@ async function startWidgetDevelopment(projectRoot, options) {
754
776
  for (const response of eventClients.keys()) response.write(frame);
755
777
  if (options.json) emit(formatDevelopmentEvent(type, data), type === "generation-failed");
756
778
  };
757
- const unauthorized = (response) => json(response, 401, {
779
+ const unauthorized = (response) => json$1(response, 401, {
758
780
  ok: false,
759
781
  error: {
760
782
  code: "WIDGET_DEV_UNAUTHORIZED",
@@ -798,14 +820,14 @@ async function startWidgetDevelopment(projectRoot, options) {
798
820
  return;
799
821
  }
800
822
  if (url.pathname === DESCRIPTOR_PATH && request.method === "GET") {
801
- json(response, 200, session.descriptor);
823
+ json$1(response, 200, session.descriptor);
802
824
  return;
803
825
  }
804
826
  if (url.pathname === CLAIM_PATH && request.method === "POST") {
805
827
  try {
806
- json(response, 200, session.claim(bearer(request.headers.authorization)));
828
+ json$1(response, 200, session.claim(bearer(request.headers.authorization)));
807
829
  } catch (error) {
808
- json(response, 401, {
830
+ json$1(response, 401, {
809
831
  ok: false,
810
832
  error: {
811
833
  code: "WIDGET_DEV_CLAIM_REJECTED",
@@ -820,7 +842,7 @@ async function startWidgetDevelopment(projectRoot, options) {
820
842
  return;
821
843
  }
822
844
  if (url.pathname === V1_STATUS_PATH && request.method === "GET") {
823
- json(response, 200, developmentSnapshot());
845
+ json$1(response, 200, developmentSnapshot());
824
846
  return;
825
847
  }
826
848
  if (url.pathname === EVENTS_PATH && request.method === "GET") {
@@ -861,13 +883,13 @@ async function startWidgetDevelopment(projectRoot, options) {
861
883
  sendEvent("disconnected", { reason: "host-request" });
862
884
  session.disconnect();
863
885
  } else sendEvent("reload-requested", { generation: state.generation });
864
- json(response, 200, {
886
+ json$1(response, 200, {
865
887
  ok: true,
866
888
  protocolVersion: 1,
867
889
  action: control.action
868
890
  });
869
891
  } catch (error) {
870
- json(response, 400, {
892
+ json$1(response, 400, {
871
893
  ok: false,
872
894
  error: { message: String(error) }
873
895
  });
@@ -881,7 +903,7 @@ async function startWidgetDevelopment(projectRoot, options) {
881
903
  const panelId = url.searchParams.get("id");
882
904
  const manifestContribution = successfulGeneration.manifest.widgetConfig.contribution;
883
905
  if (!(!contribution || contribution === manifestContribution.type && (contribution !== "panel" || !panelId || panelId === manifestContribution.id))) {
884
- json(response, 404, {
906
+ json$1(response, 404, {
885
907
  ok: false,
886
908
  error: {
887
909
  code: "WIDGET_DEV_PROFILE_NOT_FOUND",
@@ -896,7 +918,7 @@ async function startWidgetDevelopment(projectRoot, options) {
896
918
  }
897
919
  const origin = `http://127.0.0.1:${server.address().port}`;
898
920
  const launchUrl = `${origin}${guestGenerationRoot(successfulGeneration)}/launch.html`;
899
- json(response, 200, createDevelopmentConfig({
921
+ json$1(response, 200, createDevelopmentConfig({
900
922
  manifest: successfulGeneration.manifest,
901
923
  runId,
902
924
  origin,
@@ -908,7 +930,7 @@ async function startWidgetDevelopment(projectRoot, options) {
908
930
  if (url.pathname === PROFILE_PATH) {
909
931
  const runId = url.searchParams.get("runId") || "widget-dev-run";
910
932
  const origin = `http://127.0.0.1:${server.address().port}`;
911
- json(response, 200, createDevelopmentConfig({
933
+ json$1(response, 200, createDevelopmentConfig({
912
934
  manifest: successfulGeneration.manifest,
913
935
  runId,
914
936
  origin,
@@ -918,7 +940,7 @@ async function startWidgetDevelopment(projectRoot, options) {
918
940
  return;
919
941
  }
920
942
  if (url.pathname === STATUS_PATH) {
921
- json(response, 200, {
943
+ json$1(response, 200, {
922
944
  ok: state.phase === "ready",
923
945
  command: "dev",
924
946
  profile: PROFILE_PATH,
@@ -1021,8 +1043,14 @@ async function startWidgetDevelopment(projectRoot, options) {
1021
1043
  }
1022
1044
  building = true;
1023
1045
  try {
1024
- const build = await runBuild(projectRoot, { capture: options.json });
1025
- const candidate = createDevelopmentGeneration(projectRoot, successfulGeneration.number + 1);
1046
+ await options.beforeBuild?.({
1047
+ projectRoot,
1048
+ initial: false,
1049
+ build: true
1050
+ });
1051
+ const build = await buildProject(projectRoot, { capture: options.json });
1052
+ await validateProject();
1053
+ const candidate = createDevelopmentGeneration(projectRoot, successfulGeneration.number + 1, GENERATION_LIMITS, options.manifestOptions);
1026
1054
  successfulGeneration = candidate;
1027
1055
  retainedGenerations.set(candidate.guestCapability, candidate);
1028
1056
  while (retainedGenerations.size > RETAINED_GENERATIONS) retainedGenerations.delete(retainedGenerations.keys().next().value);
@@ -1103,15 +1131,22 @@ function emitCliEnvelope(type, data, error = false) {
1103
1131
  if (error) console.error(line);
1104
1132
  else console.log(line);
1105
1133
  }
1106
- async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
1134
+ async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides = {}) {
1107
1135
  try {
1108
- const options = parseArgs(argv);
1136
+ const options = {
1137
+ ...parseArgs(argv),
1138
+ ...overrides
1139
+ };
1109
1140
  if (options.help) {
1110
1141
  console.log(usage$1());
1111
1142
  return 0;
1112
1143
  }
1113
1144
  const [projectRoot] = resolveWidgetProjects(options.project);
1114
- const check = await checkWidgetProject(projectRoot, { requireBuiltEntry: false });
1145
+ const checkProject = options.checkProject ?? checkWidgetProject;
1146
+ const check = await checkProject(projectRoot, {
1147
+ ...options.manifestOptions,
1148
+ requireBuiltEntry: false
1149
+ });
1115
1150
  if (!check.ok) {
1116
1151
  if (options.json) emitCliEnvelope("error", {
1117
1152
  ok: false,
@@ -1130,7 +1165,10 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
1130
1165
  }
1131
1166
  return 1;
1132
1167
  }
1133
- await startWidgetDevelopment(projectRoot, options);
1168
+ await startWidgetDevelopment(projectRoot, {
1169
+ ...options,
1170
+ checkProject
1171
+ });
1134
1172
  return 0;
1135
1173
  } catch (error) {
1136
1174
  if (argv.includes("--json")) emitCliEnvelope("error", {
@@ -1143,6 +1181,334 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
1143
1181
  }
1144
1182
  }
1145
1183
  //#endregion
1184
+ //#region src/help.mjs
1185
+ const COMMANDS = [
1186
+ "create",
1187
+ "check",
1188
+ "validate",
1189
+ "build",
1190
+ "dev",
1191
+ "pack",
1192
+ "doctor",
1193
+ "schema",
1194
+ "migrate",
1195
+ "version",
1196
+ "integration",
1197
+ "adopt",
1198
+ "help"
1199
+ ];
1200
+ const HELP_OPTION = "-h, --help 显示当前命令帮助并退出";
1201
+ const JSON_OPTION = "--json 输出单个 JSON 文档(默认输出文本)";
1202
+ const HOST_OPTION = "--host-compat <声明文件> 核对目标宿主的声明;默认不提供,兼容状态为 unknown";
1203
+ function commandTopics(context) {
1204
+ const check = (name) => ({
1205
+ description: name === "validate" ? "check 的别名;校验工程的清单、已构建入口、samples 和预览夹具。" : "校验作者清单、编译后的宿主清单、已构建入口、samples 和预览夹具。",
1206
+ usage: [`bohr-widget ${name} <目录|manifest.json|all> [选项]`],
1207
+ arguments: ["目录|manifest.json|all 必填;检查单个工程,或从当前目录发现示例工程"],
1208
+ options: [HOST_OPTION, "--json 单工程输出检查结果;all 输出单个汇总 JSON 文档"],
1209
+ examples: [`bohr-widget ${name} ./my-widget --json`, `bohr-widget ${name} all --json`],
1210
+ notes: [
1211
+ "不执行构建;入口缺失时先运行 bohr-widget build <目录>。",
1212
+ "all 从当前目录查找 examples/sdk 和 examples/integration;未发现时尝试当前目录的 manifest.json。",
1213
+ "没有找到工程或任一工程校验失败时,退出码为 1。",
1214
+ "校验通过只证明本地结构和声明符合要求;实际渲染与能力授权仍需在 Host 中验证。"
1215
+ ]
1216
+ });
1217
+ const migrationGroup = {
1218
+ description: "审计 DeepSeek Harness(DSH)插件,并按已审查的来源配方生成 Widget 工程。",
1219
+ usage: ["bohr-widget migrate dsh audit <plugin-dir> [选项]", "bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [选项]"],
1220
+ arguments: [
1221
+ "dsh 当前唯一支持的迁移来源",
1222
+ "audit 只读审计来源、指纹和能力缺口",
1223
+ "scaffold 使用匹配的已审查配方创建新工程"
1224
+ ],
1225
+ options: [],
1226
+ examples: ["bohr-widget migrate dsh audit ./dsh-plugin --json", "bohr-widget migrate dsh scaffold ./dsh-plugin --out ./migrated-widget --json"],
1227
+ notes: [
1228
+ "先 audit,再读取报告决定是否 scaffold;源指纹变化或无匹配配方时不能直接生成。",
1229
+ "不执行源插件安装脚本或 Cordis patch;Host adapter 工作仍需单独完成。",
1230
+ "使用 bohr-widget migrate dsh <audit|scaffold> --help 查看详细参数。"
1231
+ ]
1232
+ };
1233
+ return {
1234
+ create: {
1235
+ description: "创建可独立安装、构建和打包的 Widget 工程,生成源码、样例和工具链锁定文件。",
1236
+ usage: ["bohr-widget create <目录> [选项]"],
1237
+ arguments: ["目录 必填;目标目录必须不存在"],
1238
+ options: [
1239
+ "--template <模板> panel 或 file-preview(默认 panel)",
1240
+ "--ui <模式> vanilla 或 react(默认 vanilla)",
1241
+ "--panel-id <id> 仅用于 panel;默认取规范化后的工程名",
1242
+ HOST_OPTION,
1243
+ JSON_OPTION
1244
+ ],
1245
+ examples: ["bohr-widget create workspace-browser --ui react --panel-id workspace-files", "bohr-widget create text-preview --template file-preview --ui vanilla --json"],
1246
+ notes: [
1247
+ `默认组合为 panel + vanilla;生成工程的构建环境要求 Node.js ${context.generatedProjectNode}。`,
1248
+ "不会自动安装依赖;创建后进入目录,运行 pnpm install 或 npm install。",
1249
+ "workspace-files 会补齐 files.list、files.metadata、files.read 和目录引用样例;文件树逻辑与夹具需自行实现。",
1250
+ "其他 Panel id 需要 Host 预先注册和绑定;更改 id 不会自动接管宿主界面。",
1251
+ "将 widget-toolchain.json 与包管理器 lockfile 一起保存。"
1252
+ ],
1253
+ quickStart: true
1254
+ },
1255
+ check: check("check"),
1256
+ validate: check("validate"),
1257
+ build: {
1258
+ description: "执行工程自己的 build 脚本,核对依赖与构建结果,并记录可复用的构建 receipt。",
1259
+ usage: ["bohr-widget build <目录> [选项]"],
1260
+ arguments: ["目录 必填;已安装依赖的 Widget 工程目录"],
1261
+ options: [HOST_OPTION, JSON_OPTION],
1262
+ examples: ["bohr-widget build ./my-widget", "bohr-widget build . --host-compat ./host.json --json"],
1263
+ notes: [
1264
+ "先安装工程依赖;使用 packageManager 指定的包管理器,其次按 lockfile 检测,最后回退 npm。",
1265
+ "成功后写入 .widget-build-receipt.json;pack --no-build 会检查这份记录。",
1266
+ "本命令不代替完整的 Manifest、samples 和 fixtures 校验;构建后继续运行 check。",
1267
+ "失败时读取 error、stdout 和 stderr;构建器退出 0 但仍报告未解析依赖也会失败。"
1268
+ ]
1269
+ },
1270
+ dev: {
1271
+ description: "启动本地 Profile 与资源服务,供支持 Widget 开发会话的 Host 加载和调试。",
1272
+ usage: ["bohr-widget dev <目录|manifest.json> [选项]"],
1273
+ arguments: ["目录|manifest.json 必填;要运行的 Widget 工程"],
1274
+ options: [
1275
+ "--watch 监听源码与清单变化并重建(默认关闭)",
1276
+ "--open 连接或唤起 Widget 开发宿主(默认关闭)",
1277
+ "--no-build 跳过首次构建,使用现有入口(默认先构建)",
1278
+ "--port <端口> 本地服务端口,1024–65535(默认 4300)",
1279
+ "--scheme <scheme> 宿主 deep link scheme(默认 widget-host)",
1280
+ "--bridge <url> 指定 loopback HTTP(S) bridge;默认探测标准端口",
1281
+ HOST_OPTION,
1282
+ "--json 持续输出 NDJSON 事件流(默认输出文本)"
1283
+ ],
1284
+ examples: ["bohr-widget dev . --watch --open", "bohr-widget dev ./my-widget --no-build --port 4301 --json"],
1285
+ notes: [
1286
+ "--open 打开的是目标 Host,不是浏览器预览;先连接 bridge,必要时尝试宿主 scheme。",
1287
+ "不传 --open 时可使用输出的 Host URL;服务常驻,按 Ctrl+C 结束。",
1288
+ "--no-build 不要求构建 receipt,但仍校验工具链与现有产物;搭配 --watch 时后续变化仍会构建。",
1289
+ "--bridge 须带 1024–65535 的显式端口,且不能包含认证信息、query 或 hash。",
1290
+ "--bridge 仅在使用 --open 时参与宿主连接。",
1291
+ "dev --json 是逐行事件流;session-ready 含 profileUrl、statusUrl、hostUrl、digest 和连接结果。",
1292
+ "当前本地开发文件能力只读;Manifest 申请或样例 grant 不代表目标 Host 已开放写入。",
1293
+ "watch 构建失败时保留上一轮可用产物;从 statusUrl 查看 lastBuildError。"
1294
+ ],
1295
+ quickStart: true
1296
+ },
1297
+ pack: {
1298
+ description: "默认先构建和校验工程,再生成宿主可加载的 Widget ZIP 与 SHA-256 摘要。",
1299
+ usage: ["bohr-widget pack <目录> [选项]"],
1300
+ arguments: ["目录 必填;要打包的 Widget 工程目录"],
1301
+ options: [
1302
+ "--out <zip> 输出路径;默认 <工程目录>/<manifest.name>.zip",
1303
+ "--no-build 复用匹配的构建 receipt(默认重新构建)",
1304
+ "--include-preview-fixtures 包含 preview/fixtures.json(默认不包含)",
1305
+ HOST_OPTION,
1306
+ JSON_OPTION
1307
+ ],
1308
+ examples: ["bohr-widget pack . --out ./artifacts/widget.zip --json", "bohr-widget pack ./my-widget --no-build --include-preview-fixtures --out ./repro.zip"],
1309
+ notes: [
1310
+ "--out 相对调用时的当前目录解析,自动创建父目录;目标 ZIP 已存在时会覆盖。",
1311
+ "--no-build 要求 .widget-build-receipt.json 与当前 CLI/profile、工具链摘要、实际依赖、输入和产物一致。",
1312
+ "单独执行工程 build 脚本不生成 CLI receipt;需先运行 bohr-widget build <目录>。",
1313
+ "携带预览夹具的 ZIP 用于本地复现,不作为上传 ZIP;上传包使用默认不含夹具的形式。",
1314
+ "入口构建目录中的 chunk、CSS、WASM 等部署资源一并保留;不要将源码或凭据放入该目录。",
1315
+ "--json 返回 path、bytes、sha256、toolchain、manifest 和 fixtures.included;后续交付使用同一份 ZIP 与摘要。",
1316
+ "本命令不上传、不发布、不绑定 Agent;成功打包也不代表 Host 已实际渲染。"
1317
+ ]
1318
+ },
1319
+ doctor: {
1320
+ description: "诊断 Node、清单、精确依赖、工具链锁和构建入口,解释工程尚未就绪的原因。",
1321
+ usage: ["bohr-widget doctor [目录] [选项]"],
1322
+ arguments: ["目录 可选;默认当前目录(.)"],
1323
+ options: [HOST_OPTION, JSON_OPTION],
1324
+ examples: ["bohr-widget doctor --json", "bohr-widget doctor ./existing-widget --host-compat ./host.json --json"],
1325
+ notes: [
1326
+ "只检查状态,不安装依赖、不构建,也不写入工具链锁。",
1327
+ "ok 表示环境、清单与候选 profile 检查通过;ready 还要求工具链锁、已安装依赖和构建入口。",
1328
+ "尚未安装或构建时可能 ok=true、ready=false;先按 warnings 修复再继续。",
1329
+ "ready 不保证 pack --no-build 的 receipt 可复用;doctor 只检查 CLI 的 Node 要求,不验证构建器环境。",
1330
+ "旧工程缺少 widget-toolchain.json 时,先核对精确依赖并安装,再运行 adopt。"
1331
+ ]
1332
+ },
1333
+ integration: {
1334
+ description: "发现固定工具链、执行器位置、Node 要求与文档入口,供 Agent 或其他 CLI 集成。",
1335
+ usage: ["bohr-widget integration [选项]"],
1336
+ options: [
1337
+ "--project <目录> 可选;同时核对指定工程的锁定 profile 与依赖声明",
1338
+ HOST_OPTION,
1339
+ JSON_OPTION
1340
+ ],
1341
+ examples: ["bohr-widget integration --json", "bohr-widget integration --project ./my-widget --host-compat ./host.json --json"],
1342
+ notes: [
1343
+ "不传 --project 时只发现 CLI 自带工具链,不检查当前目录是否为 Widget 工程。",
1344
+ "传入工程时要求工具链锁;尚未安装依赖时先核对声明,已安装时同时核对实际版本。",
1345
+ "JSON 使用 interfaceVersion=2、toolchain.formatVersion=2;返回 execution、profile、requirements 和 documentation。",
1346
+ "调用方应使用 execution.executable 与 execution.entry 启动同一份 CLI,避免 PATH 指向另一版本。",
1347
+ "宿主声明由调用方提供;unknown、snapshot、confirmed 不代表上传或发布许可。"
1348
+ ],
1349
+ agentDocs: true
1350
+ },
1351
+ adopt: {
1352
+ description: "核验旧工程与当前固定工具链匹配后,显式写入 widget-toolchain.json。",
1353
+ usage: ["bohr-widget adopt <目录> [选项]"],
1354
+ arguments: ["目录 必填;待锁定的已有 Widget 工程"],
1355
+ options: [HOST_OPTION, JSON_OPTION],
1356
+ examples: ["bohr-widget adopt ./existing-widget --json", "bohr-widget adopt . --host-compat ./host.json --json"],
1357
+ notes: [
1358
+ "先运行 doctor,恢复当前 profile 的精确 SDK/contracts 依赖并安装。",
1359
+ "核对实际依赖、Manifest 和 samples 后才写锁;不要求已构建入口。",
1360
+ "不会自动升级依赖、迁移源码或修正协议字段;检查不通过时先修复工程。"
1361
+ ]
1362
+ },
1363
+ schema: {
1364
+ description: "打印本 CLI 自带的作者插件 Manifest Schema 的本地绝对路径。",
1365
+ usage: ["bohr-widget schema"],
1366
+ options: [],
1367
+ examples: ["bohr-widget schema", "bohr-widget schema > schema-path.txt"],
1368
+ notes: [
1369
+ "输出 widget-plugin-manifest.schema.json 的路径,不打印 Schema 正文。",
1370
+ "不读取工程;即使传 --json,仍输出纯文本路径。",
1371
+ "使用配套 contracts Schema 进行结构检查;通过 Schema 不代表 Host 授权。"
1372
+ ]
1373
+ },
1374
+ version: {
1375
+ description: "查看当前 CLI 及其固定的 SDK/contracts 版本。",
1376
+ usage: ["bohr-widget version [--json]", "bohr-widget --version [--json]"],
1377
+ options: [JSON_OPTION],
1378
+ examples: ["bohr-widget version --json", "bohr-widget -v"],
1379
+ notes: [
1380
+ `当前 CLI ${context.cli},SDK ${context.sdk},contracts ${context.contracts}。`,
1381
+ "文本输出 CLI 与 SDK 版本;--json 同时包含 contracts,不访问 npm registry。",
1382
+ "全局 -v / --version 等同版本查询,不执行工程操作。"
1383
+ ]
1384
+ },
1385
+ migrate: migrationGroup,
1386
+ "migrate dsh": {
1387
+ ...migrationGroup,
1388
+ arguments: migrationGroup.arguments.slice(1)
1389
+ },
1390
+ "migrate dsh audit": {
1391
+ description: "只读审计 DSH 插件的来源指纹、已知配方与能力迁移缺口。",
1392
+ usage: ["bohr-widget migrate dsh audit <plugin-dir> [选项]"],
1393
+ arguments: ["plugin-dir 必填;DSH 插件源目录"],
1394
+ options: [JSON_OPTION],
1395
+ examples: ["bohr-widget migrate dsh audit ./dsh-plugin", "bohr-widget migrate dsh audit ./dsh-plugin --json"],
1396
+ notes: [
1397
+ "不创建目标工程,不执行 Cordis patch 或源插件安装脚本。",
1398
+ "JSON 的 report 包含来源指纹、配方兼容状态和能力分类;读取结论后再决定是否 scaffold。",
1399
+ "审计命令执行成功不等于插件可自动迁移;未知来源或变化后的指纹需要人工审查。"
1400
+ ]
1401
+ },
1402
+ "migrate dsh scaffold": {
1403
+ description: "按已审查且来源指纹一致的 DSH 配方创建 Widget 工程及迁移报告。",
1404
+ usage: ["bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [选项]"],
1405
+ arguments: ["plugin-dir 必填;已完成审计的 DSH 插件源目录"],
1406
+ options: [
1407
+ "--out <widget-dir> 必填;要创建的新工程目录,不能已存在",
1408
+ HOST_OPTION,
1409
+ JSON_OPTION
1410
+ ],
1411
+ examples: ["bohr-widget migrate dsh scaffold ./dsh-plugin --out ./migrated-widget", "bohr-widget migrate dsh scaffold ./dsh-plugin --out ./migrated-widget --host-compat ./host.json --json"],
1412
+ notes: [
1413
+ "先运行 audit;无已审查配方或迁移期间指纹变化时停止,不覆盖已有工程;目标的父目录须已存在。",
1414
+ "输出 widget-toolchain.json 与 migration-report.json;随后安装依赖、build、check 和 pack。",
1415
+ "工具、Skill、system prompt、文件和网络等 Host adapter 工作不会自动迁入 Widget。",
1416
+ "生成成功只表示创建了工程,仍需目标 Host 验收。"
1417
+ ]
1418
+ },
1419
+ help: {
1420
+ description: "查看全局命令概览,或查看某个命令及迁移子命令的详细帮助。",
1421
+ usage: ["bohr-widget help [命令 [子命令...]]", "bohr-widget <命令> --help"],
1422
+ arguments: ["命令 [子命令...] 可选;省略时显示全局帮助"],
1423
+ options: [],
1424
+ examples: ["bohr-widget help create", "bohr-widget migrate dsh scaffold --help"],
1425
+ notes: ["不传参数、-h 和 --help 都可查看全局帮助;子命令后的 -h / --help 显示对应主题。", "帮助始终输出文本,不执行创建、构建或迁移操作。"]
1426
+ }
1427
+ };
1428
+ }
1429
+ function documentationLines(documentation, topic = {}) {
1430
+ return [
1431
+ ...topic.quickStart ? [`快速开始:${documentation.quickStart}`] : [],
1432
+ `命令参考:${documentation.cli}`,
1433
+ ...topic.agentDocs ? [
1434
+ `文档站:${documentation.url}`,
1435
+ `Agent 索引:${documentation.llms}`,
1436
+ `完整文档:${documentation.llmsFull}`
1437
+ ] : []
1438
+ ];
1439
+ }
1440
+ function section(title, lines) {
1441
+ return lines.length ? `\n${title}:\n${lines.map((line) => ` ${line}`).join("\n")}\n` : "";
1442
+ }
1443
+ function globalHelp(context) {
1444
+ const { cli, sdk, contracts, generatedProjectNode, documentation } = context;
1445
+ return `bohr-widget ${cli} — Bohrium Widget 插件工程工具
1446
+ SDK ${sdk} / contracts ${contracts}
1447
+ 创建、校验、构建、开发调试与打包;上传、发布和 Agent 绑定由 Bohr CLI 或平台执行。
1448
+
1449
+ 用法:
1450
+ bohr-widget <命令> [参数] [选项]
1451
+ bohr-widget help [命令 [子命令...]]
1452
+
1453
+ 命令:
1454
+ create <目录> 创建 Widget 工程(默认 panel + vanilla)
1455
+ check <目录|manifest.json|all> 校验清单、已构建入口、samples 和 fixtures
1456
+ validate <目录|manifest.json|all> check 的别名
1457
+ build <目录> 执行工程构建并记录构建 receipt
1458
+ dev <目录|manifest.json> 启动本地开发服务,供 Widget Host 加载
1459
+ pack <目录> 默认先构建、校验,再生成 Widget ZIP
1460
+ doctor [目录] 诊断环境和工程状态(默认当前目录)
1461
+ integration 发现工具链、执行器和文档入口
1462
+ adopt <目录> 核验旧工程并写入工具链锁
1463
+ schema 打印作者 Manifest Schema 的本地路径
1464
+ migrate dsh audit|scaffold 审计 DSH 来源或按已审查配方生成工程
1465
+ version 查看 CLI 与固定的 SDK/contracts 版本
1466
+ help [命令...] 查看全局或指定主题帮助
1467
+
1468
+ 全局选项:
1469
+ -h, --help 显示全局或指定命令帮助并退出
1470
+ -v, --version 显示版本并退出;可搭配 --json
1471
+ --json 多数命令输出单个 JSON;dev 输出 NDJSON 事件流
1472
+ check all 输出汇总;help 与 schema 始终输出文本
1473
+
1474
+ 调用方式:
1475
+ 工程内:pnpm exec bohr-widget ... 或 npx --no-install bohr-widget ...
1476
+ 未安装:pnpm dlx @dp-bohrium/widget-cli <命令> ...
1477
+ 下方示例中的 bohr-widget 可使用上述工程内调用前缀。
1478
+ CLI 要求 Node.js >=20;默认生成工程要求 Node.js ${generatedProjectNode}。
1479
+
1480
+ 典型开发流程:
1481
+ pnpm dlx @dp-bohrium/widget-cli create my-widget --ui react --panel-id workspace-files
1482
+ cd my-widget
1483
+ pnpm install
1484
+ pnpm exec bohr-widget build .
1485
+ pnpm exec bohr-widget check .
1486
+ pnpm exec bohr-widget dev . --watch --open
1487
+ pnpm run widget:pack
1488
+
1489
+ check 不执行构建;dev 按 Ctrl+C 结束后可继续打包。
1490
+ Widget ZIP 使用 widget:pack 脚本;pnpm pack 是制作 npm .tgz 包的内置命令。
1491
+ 工程命令可用 --host-compat <声明文件> 核对宿主;未提供时兼容状态为 unknown。
1492
+
1493
+ 文档:
1494
+ 文档站:${documentation.url}
1495
+ 快速开始:${documentation.quickStart}
1496
+ 命令参考:${documentation.cli}
1497
+ Agent 索引:${documentation.llms}
1498
+ 完整文档:${documentation.llmsFull}
1499
+
1500
+ 查看具体命令的参数、默认值和示例:bohr-widget <命令> --help
1501
+ `;
1502
+ }
1503
+ function renderHelp(path = [], context) {
1504
+ if (path.length === 0) return globalHelp(context);
1505
+ const key = path.join(" ");
1506
+ const topics = commandTopics(context);
1507
+ if (!Object.hasOwn(topics, key)) throw new Error(`未知帮助主题:${key}`);
1508
+ const topic = topics[key];
1509
+ return `bohr-widget ${key} — ${topic.description}\n` + section("用法", topic.usage) + section("参数", topic.arguments ?? []) + section("选项", [...topic.options, HELP_OPTION]) + section("示例", topic.examples) + section("说明", topic.notes) + section("文档", documentationLines(context.documentation, topic));
1510
+ }
1511
+ //#endregion
1146
1512
  //#region src/manifest-lint.mjs
1147
1513
  /**
1148
1514
  * 作者清单(VS Code 风格 `contributes` 清单)的本地静态检查。
@@ -1233,8 +1599,371 @@ function lintAuthorManifest(manifest, options = {}) {
1233
1599
  };
1234
1600
  }
1235
1601
  //#endregion
1602
+ //#region src/project-executor.mjs
1603
+ const PROJECT_TOOLCHAIN_FILE = "widget-toolchain.json";
1604
+ const BUILD_RECEIPT_FILE = ".widget-build-receipt.json";
1605
+ const PACKAGES = Object.freeze({
1606
+ sdk: "@dp-bohrium/widget-sdk",
1607
+ contracts: "@dp-bohrium/widget-contracts"
1608
+ });
1609
+ const EXACT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
1610
+ function failure(code, message, context, recovery) {
1611
+ return Object.assign(new Error(message), {
1612
+ cliCode: `WIDGET_CLI_${code}`,
1613
+ context,
1614
+ recovery
1615
+ });
1616
+ }
1617
+ function record(value) {
1618
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1619
+ }
1620
+ function text(value) {
1621
+ return typeof value === "string" && value.trim().length > 0;
1622
+ }
1623
+ async function json(path, code, recovery) {
1624
+ try {
1625
+ return JSON.parse(await readFile(path, "utf8"));
1626
+ } catch (error) {
1627
+ throw failure(code, `无法读取有效 JSON:${path}`, {
1628
+ path,
1629
+ cause: error.message
1630
+ }, recovery);
1631
+ }
1632
+ }
1633
+ function date(value) {
1634
+ return typeof value === "string" && /T.*(?:Z|[+-]\d{2}:\d{2})$/u.test(value) ? Date.parse(value) : NaN;
1635
+ }
1636
+ /** A caller-provided observation is evidence, not a platform publication permit. */
1637
+ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.now(), expectedHost, expectedEnvironment } = {}) {
1638
+ const profileId = toolchain.profile.id;
1639
+ if (!hostCompatPath) return {
1640
+ status: "unknown",
1641
+ profileId
1642
+ };
1643
+ const invalid = (reason) => {
1644
+ throw failure("HOST_COMPAT_INVALID", `宿主兼容声明无效:${reason}`, {
1645
+ path: resolve(hostCompatPath),
1646
+ expectedHost,
1647
+ expectedEnvironment
1648
+ }, ["重新获取本次目标环境的宿主兼容声明,再通过 --host-compat 传入。"]);
1649
+ };
1650
+ const value = await json(hostCompatPath, "HOST_COMPAT_INVALID", ["重新获取本次目标环境的宿主兼容声明。"]);
1651
+ if (!record(value) || !record(value.statement)) invalid("缺少 statement");
1652
+ const { statement, observation } = value;
1653
+ if (statement.formatVersion !== 1 || !text(statement.hostId) || !text(statement.environment) || !text(statement.revision) || !Array.isArray(statement.acceptedProfiles) || !statement.acceptedProfiles.every(text) || new Set(statement.acceptedProfiles).size !== statement.acceptedProfiles.length) invalid("statement 字段不符合 formatVersion=1");
1654
+ if (expectedHost && statement.hostId !== expectedHost || expectedEnvironment && statement.environment !== expectedEnvironment) invalid("hostId/environment 与目标环境不符");
1655
+ const issuedAt = date(statement.issuedAt);
1656
+ const expiresAt = date(statement.expiresAt);
1657
+ const observedNow = now instanceof Date ? now.getTime() : Number(now);
1658
+ if (!Number.isFinite(observedNow) || !Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt || issuedAt > observedNow) invalid("声明时间无效或尚未生效");
1659
+ if (!record(observation) || !["online", "snapshot"].includes(observation.source) || !Number.isFinite(date(observation.fetchedAt)) || date(observation.fetchedAt) < issuedAt || date(observation.fetchedAt) > observedNow) invalid("observation 必须包含有效 source 和 fetchedAt");
1660
+ if (!statement.acceptedProfiles.includes(profileId)) throw failure("HOST_PROFILE_UNSUPPORTED", `宿主 ${statement.hostId}/${statement.environment} 未接受当前工具链 ${profileId}`, {
1661
+ profileId,
1662
+ hostId: statement.hostId,
1663
+ environment: statement.environment,
1664
+ revision: statement.revision,
1665
+ acceptedProfiles: statement.acceptedProfiles
1666
+ }, ["使用经该宿主验收的 Widget CLI,或完成宿主平台升级;不要修改 SDK 版本字符串绕过校验。"]);
1667
+ const fresh = expiresAt > observedNow;
1668
+ return {
1669
+ status: observation.source === "online" && fresh ? "confirmed" : "snapshot",
1670
+ profileId,
1671
+ hostId: statement.hostId,
1672
+ environment: statement.environment,
1673
+ revision: statement.revision,
1674
+ issuedAt: statement.issuedAt,
1675
+ expiresAt: statement.expiresAt,
1676
+ observation,
1677
+ ...!fresh ? { reason: "expired" } : {}
1678
+ };
1679
+ }
1680
+ function assertMatchingLock(lock, toolchain, root) {
1681
+ const { profile } = toolchain;
1682
+ if (!record(lock) || lock.formatVersion !== 1 || lock.profileId !== profile.id) throw failure("PROJECT_PROFILE_MISMATCH", "工程锁定的 profile 与当前 Widget CLI 不一致", {
1683
+ projectRoot: root,
1684
+ expectedProfile: profile.id,
1685
+ actualProfile: lock?.profileId
1686
+ }, ["使用携带工程 profile 的 Widget CLI;跨 profile 迁移需显式修改和验收工程。"]);
1687
+ if (lock.sdk !== profile.sdk || lock.contracts !== profile.contracts) throw failure("PROJECT_SDK_MISMATCH", "工程锁定的 SDK/contracts 与当前 profile 不一致", {
1688
+ projectRoot: root,
1689
+ expected: profile,
1690
+ actual: lock
1691
+ }, ["恢复正确的 widget-toolchain.json 和精确依赖,再重新安装工程依赖。"]);
1692
+ return lock;
1693
+ }
1694
+ async function readLock(root, toolchain, required) {
1695
+ const path = join(root, PROJECT_TOOLCHAIN_FILE);
1696
+ let value;
1697
+ try {
1698
+ value = await readFile(path, "utf8");
1699
+ } catch (error) {
1700
+ if (error.code !== "ENOENT") throw error;
1701
+ if (!required) return null;
1702
+ throw failure("PROJECT_TOOLCHAIN_REQUIRED", "工程缺少 widget-toolchain.json,需先显式确认工具链", {
1703
+ projectRoot: root,
1704
+ path
1705
+ }, ["先执行 bohr-widget doctor,再执行 bohr-widget adopt <工程目录>。"]);
1706
+ }
1707
+ let lock;
1708
+ try {
1709
+ lock = JSON.parse(value);
1710
+ } catch {
1711
+ throw failure("PROJECT_PROFILE_MISMATCH", "widget-toolchain.json 不是有效 JSON", {
1712
+ projectRoot: root,
1713
+ path
1714
+ }, ["从版本库恢复有效的工程锁定信息,不要直接改 profile 绕过校验。"]);
1715
+ }
1716
+ return assertMatchingLock(lock, toolchain, root);
1717
+ }
1718
+ /** Resolve from this project's node_modules, never from an ancestor or the CLI. */
1719
+ async function validateProjectToolchain(projectRoot, toolchain, { requireInstalled = true, requireLock = true } = {}) {
1720
+ const root = resolve(projectRoot);
1721
+ const lock = await readLock(root, toolchain, requireLock);
1722
+ const pkg = await json(join(root, "package.json"), "PROJECT_SDK_MISMATCH", ["恢复工程 package.json 中的精确 SDK/contracts 依赖。"]);
1723
+ if (!record(pkg)) throw failure("PROJECT_SDK_MISMATCH", "工程 package.json 必须是对象", { projectRoot: root }, ["恢复工程 package.json 中的精确 SDK/contracts 依赖。"]);
1724
+ const declared = {};
1725
+ const installed = {};
1726
+ for (const [key, name] of Object.entries(PACKAGES)) {
1727
+ const declarations = [
1728
+ "dependencies",
1729
+ "devDependencies",
1730
+ "optionalDependencies"
1731
+ ].map((section) => pkg[section]?.[name]).filter((version) => version !== void 0);
1732
+ const expected = toolchain.profile[key];
1733
+ if (declarations.length === 0 || declarations.some((version) => typeof version !== "string" || !EXACT_VERSION.test(version) || version !== expected)) throw failure("PROJECT_SDK_MISMATCH", `${name} 必须精确声明为 ${expected}`, {
1734
+ projectRoot: root,
1735
+ package: name,
1736
+ expected,
1737
+ declared: declarations
1738
+ }, ["按 widget-toolchain.json 恢复精确版本,再重新安装工程依赖;不要使用 ^、~、workspace 或 latest。"]);
1739
+ declared[key] = expected;
1740
+ if (!requireInstalled) continue;
1741
+ const actual = await json(join(root, "node_modules", name, "package.json"), "PROJECT_SDK_MISMATCH", ["在该 Widget 工程目录安装依赖,再重试;父目录或 CLI 的安装不能替代工程安装。"]);
1742
+ if (!record(actual) || actual.name !== name || actual.version !== expected) throw failure("PROJECT_SDK_MISMATCH", `${name} 实际安装版本与工程工具链不一致`, {
1743
+ projectRoot: root,
1744
+ package: name,
1745
+ expected,
1746
+ installed: actual?.version
1747
+ }, ["按工程锁定信息恢复依赖并重新安装,再重新构建。"]);
1748
+ installed[key] = actual.version;
1749
+ }
1750
+ return {
1751
+ projectRoot: root,
1752
+ profile: { ...toolchain.profile },
1753
+ declared,
1754
+ installed,
1755
+ dependenciesVerified: requireInstalled,
1756
+ lock
1757
+ };
1758
+ }
1759
+ async function writeProjectToolchain(projectRoot, toolchain) {
1760
+ const root = resolve(projectRoot);
1761
+ const existing = await readLock(root, toolchain, false);
1762
+ if (existing) return existing;
1763
+ const value = {
1764
+ formatVersion: 1,
1765
+ profileId: toolchain.profile.id,
1766
+ sdk: toolchain.profile.sdk,
1767
+ contracts: toolchain.profile.contracts,
1768
+ createdWithCli: toolchain.cli
1769
+ };
1770
+ await mkdir(root, { recursive: true });
1771
+ try {
1772
+ await writeFile(join(root, PROJECT_TOOLCHAIN_FILE), `${JSON.stringify(value, null, 2)}\n`, { flag: "wx" });
1773
+ } catch (error) {
1774
+ if (error.code !== "EEXIST") throw error;
1775
+ return readLock(root, toolchain, true);
1776
+ }
1777
+ return value;
1778
+ }
1779
+ async function adoptProjectToolchain(projectRoot, toolchain) {
1780
+ const result = await validateProjectToolchain(projectRoot, toolchain, { requireLock: false });
1781
+ const check = await checkWidgetProject(result.projectRoot, {
1782
+ requireBuiltEntry: false,
1783
+ toolchain: { sdkVersion: toolchain.profile.sdk }
1784
+ });
1785
+ const lint = looksLikeAuthorManifest(check.sourceManifest) ? lintAuthorManifest(check.sourceManifest) : {
1786
+ ok: true,
1787
+ errors: []
1788
+ };
1789
+ if (!lint.ok || !check.ok || check.manifest?.widgetConfig?.widgetSdkVersion !== toolchain.profile.sdk) throw failure("PROJECT_PROFILE_MISMATCH", "工程 Manifest 或 samples 无法通过当前 profile 校验,不能直接 adopt", {
1790
+ projectRoot: result.projectRoot,
1791
+ profileId: toolchain.profile.id,
1792
+ errors: [...lint.errors, ...check.errors],
1793
+ manifestSdkVersion: check.manifest?.widgetConfig?.widgetSdkVersion
1794
+ }, ["先按当前工具链修复 Manifest 和 samples;adopt 不会迁移 SDK、源码或协议字段。"]);
1795
+ const lock = await writeProjectToolchain(result.projectRoot, toolchain);
1796
+ return {
1797
+ ...result,
1798
+ lock
1799
+ };
1800
+ }
1801
+ function digest(value) {
1802
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
1803
+ }
1804
+ function receiptToolchain(toolchain) {
1805
+ return {
1806
+ cli: toolchain.cli,
1807
+ profile: toolchain.profile,
1808
+ digests: toolchain.digests
1809
+ };
1810
+ }
1811
+ const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1812
+ "node_modules",
1813
+ ".git",
1814
+ ".widget-build",
1815
+ ".widget-dev"
1816
+ ]);
1817
+ async function fingerprint(root, { outputs = false } = {}) {
1818
+ const files = [];
1819
+ const walk = async (directory, prefix, ancestors) => {
1820
+ const actualDirectory = await realpath(directory);
1821
+ if (ancestors.has(actualDirectory)) throw new Error(`文件目录存在符号链接循环:${prefix}`);
1822
+ const nextAncestors = /* @__PURE__ */ new Set([...ancestors, actualDirectory]);
1823
+ const entries = await readdir(directory, { withFileTypes: true });
1824
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name, "en"))) {
1825
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
1826
+ if (IGNORED_DIRECTORIES.has(entry.name) || !outputs && !prefix && entry.name === "dist" || entry.name === "widget-toolchain.json" || entry.name === ".widget-build-receipt.json" || /\.zip$/iu.test(entry.name)) continue;
1827
+ const path = join(directory, entry.name);
1828
+ const metadata = await stat(path);
1829
+ if (metadata.isDirectory()) await walk(path, relativePath, nextAncestors);
1830
+ else if (metadata.isFile()) files.push({
1831
+ path: relativePath,
1832
+ sha256: digest(await readFile(path))
1833
+ });
1834
+ }
1835
+ };
1836
+ await walk(outputs ? join(root, "dist") : root, "", /* @__PURE__ */ new Set());
1837
+ return {
1838
+ digest: digest(JSON.stringify(files)),
1839
+ files
1840
+ };
1841
+ }
1842
+ async function captureBuildInputs(projectRoot) {
1843
+ return fingerprint(resolve(projectRoot));
1844
+ }
1845
+ async function writeBuildReceipt(projectRoot, toolchain, { inputsBefore } = {}) {
1846
+ const result = await validateProjectToolchain(projectRoot, toolchain);
1847
+ const inputs = await fingerprint(result.projectRoot);
1848
+ if (inputsBefore && inputsBefore.digest !== inputs.digest) throw failure("BUILD_RECEIPT_MISMATCH", "构建期间源输入发生变化,不能确认产物对应当前工程", { projectRoot: result.projectRoot }, ["保存工程文件后重新执行 bohr-widget build;构建结束前不要继续修改输入文件。"]);
1849
+ const outputs = await fingerprint(result.projectRoot, { outputs: true });
1850
+ if (outputs.files.length === 0) throw failure("BUILD_RECEIPT_MISMATCH", "构建目录 dist 没有可记录的产物", { projectRoot: result.projectRoot }, ["执行标准 build,确认 dist 产物存在后再打包。"]);
1851
+ const receipt = {
1852
+ formatVersion: 1,
1853
+ toolchain: receiptToolchain(toolchain),
1854
+ dependencies: result.installed,
1855
+ inputs,
1856
+ outputs
1857
+ };
1858
+ await writeFile(join(result.projectRoot, BUILD_RECEIPT_FILE), `${JSON.stringify(receipt, null, 2)}\n`);
1859
+ return receipt;
1860
+ }
1861
+ async function assertBuildReceipt(projectRoot, toolchain) {
1862
+ const root = resolve(projectRoot);
1863
+ const mismatch = (reason, context = {}) => {
1864
+ throw failure("BUILD_RECEIPT_MISMATCH", `不能复用构建产物:${reason}`, {
1865
+ projectRoot: root,
1866
+ ...context
1867
+ }, ["重新执行 bohr-widget build 或去掉 pack 的 --no-build。"]);
1868
+ };
1869
+ const receipt = await json(join(root, BUILD_RECEIPT_FILE), "BUILD_RECEIPT_MISMATCH", ["先执行 bohr-widget build,或去掉 pack 的 --no-build。"]);
1870
+ const project = await validateProjectToolchain(root, toolchain);
1871
+ if (!record(receipt) || receipt.formatVersion !== 1 || JSON.stringify(receipt.toolchain) !== JSON.stringify(receiptToolchain(toolchain)) || JSON.stringify(receipt.dependencies) !== JSON.stringify(project.installed)) mismatch("receipt 工具链或实际依赖与本次执行不一致");
1872
+ let inputs;
1873
+ let outputs;
1874
+ try {
1875
+ inputs = await fingerprint(root);
1876
+ outputs = await fingerprint(root, { outputs: true });
1877
+ } catch (error) {
1878
+ mismatch("无法读取构建输入或 dist", { cause: error.message });
1879
+ }
1880
+ if (receipt.inputs?.digest !== inputs.digest || receipt.outputs?.digest !== outputs.digest || outputs.files.length === 0) mismatch("源文件或 dist 内容已变化");
1881
+ return receipt;
1882
+ }
1883
+ //#endregion
1236
1884
  //#region src/templates.mjs
1885
+ const GENERATED_PROJECT_NODE = "^22.18.0 || >=24.11.0";
1886
+ function projectPackage({ name, ui, cliVersion, sdkVersion, contractsVersion }) {
1887
+ return {
1888
+ name: `@dp-bohrium/widget-example-${name}`,
1889
+ private: true,
1890
+ type: "module",
1891
+ engines: { node: GENERATED_PROJECT_NODE },
1892
+ scripts: {
1893
+ build: "tsc --noEmit && tsdown && node scripts/rename-iife.mjs",
1894
+ check: "bohr-widget check .",
1895
+ "widget:pack": "bohr-widget pack ."
1896
+ },
1897
+ dependencies: {
1898
+ "@dp-bohrium/widget-sdk": sdkVersion,
1899
+ ...ui === "react" ? {
1900
+ react: "^19.0.0",
1901
+ "react-dom": "^19.0.0"
1902
+ } : {}
1903
+ },
1904
+ devDependencies: {
1905
+ "@dp-bohrium/widget-cli": cliVersion,
1906
+ "@dp-bohrium/widget-contracts": contractsVersion,
1907
+ tsdown: "0.22.14",
1908
+ typescript: "5.9.3",
1909
+ ...ui === "react" ? {
1910
+ "@types/react": "^19.0.0",
1911
+ "@types/react-dom": "^19.0.0"
1912
+ } : {}
1913
+ }
1914
+ };
1915
+ }
1916
+ function projectReadme({ name, template, ui, sourceFile, hostFile }) {
1917
+ return `# ${name}
1918
+
1919
+ Bohrium Widget ${template} 插件工程(${ui})。
1920
+
1921
+ 默认构建器要求 Node.js \`${GENERATED_PROJECT_NODE}\`,高于 CLI 自身的 Node.js 20 要求。
1922
+ 工程已经精确依赖创建它的 CLI 版本;在本目录安装后使用本地命令,无需全局安装或源码仓库。
1923
+
1924
+ \`\`\`bash
1925
+ pnpm install
1926
+ pnpm exec bohr-widget build . --json
1927
+ pnpm exec bohr-widget check . --json
1928
+ pnpm run widget:pack --json
1929
+ \`\`\`
1930
+
1931
+ 使用 npm 时,安装后可执行 \`npm run build\`、\`npm run check\`、\`npm run widget:pack\`;需要构建 receipt 时执行 \`npx --no-install bohr-widget build .\`。
1932
+ Widget ZIP 使用 \`pnpm run widget:pack\` 或 \`npm run widget:pack\`;\`pnpm pack\` 是制作 npm \`.tgz\` 包的内置命令。
1933
+
1934
+ - 源码入口:${sourceFile},由 ${hostFile} 调用 SDK 的 runWidgetHost 启动。
1935
+ - 高度链、布局与滚动策略直接写在 ${sourceFile} 中,样式随 activate 安装并在释放时清理;JSON 展示元素单独处理换行。
1936
+ - manifest.json 是作者清单,打包时由 CLI 编译为宿主清单。
1937
+ - 提交 widget-toolchain.json 与依赖 lockfile;dist/widget.js 与生成的 ZIP 不提交。
1938
+ - check 只验证结构、声明和样例,不执行 JavaScript;pack 成功不代表首帧渲染、真实授权或业务验收成功。
1939
+ - 上传前至少运行首帧、一次事件后再 render、空数据、缺授权和尺寸/主题更新,并检查滚动区域实际可见。
1940
+ - 本地开发连接使用 \`pnpm exec bohr-widget dev . --open --watch\`,需要已安装且支持开发接入的目标 Host。
1941
+
1942
+ 通过 \`pnpm exec bohr-widget integration --project . --json\` 查看工具链和公开文档地址。
1943
+ 本地创建、构建、校验与打包无需平台登录;平台上传和绑定由目标平台提供。
1944
+ `;
1945
+ }
1946
+ function layoutSource() {
1947
+ return ` // 在作者源码中维护高度链与滚动策略,随本次 activate 一起释放。
1948
+ const style = document.createElement("style");
1949
+ style.textContent = \`
1950
+ * { box-sizing: border-box; }
1951
+ html, body { height: 100%; min-height: 0; margin: 0; }
1952
+ body { font-family: system-ui, sans-serif; overflow: hidden; }
1953
+ .widget-root { height: 100%; min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
1954
+ .widget-json { flex: 1; min-height: 0; min-width: 0; margin: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
1955
+ \`;
1956
+ document.head.append(style);
1957
+ context.subscriptions.add(() => style.remove());
1958
+ `;
1959
+ }
1237
1960
  function reactSource({ reportSize = false } = {}) {
1961
+ const resizeState = reportSize ? "\n let resized = false;" : "";
1962
+ const resizeCall = reportSize ? `
1963
+ if (!resized) {
1964
+ context.resize(640, 360);
1965
+ resized = true;
1966
+ }` : "";
1238
1967
  return `import { createRoot } from "react-dom/client";
1239
1968
  import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1240
1969
  import type { RenderRequest } from "@dp-bohrium/widget-contracts";
@@ -1242,9 +1971,9 @@ import type { RenderRequest } from "@dp-bohrium/widget-contracts";
1242
1971
  function WidgetView({ request }: { request?: RenderRequest }) {
1243
1972
  const data = request?.content ?? { state: "等待 Host 提供数据" };
1244
1973
  return (
1245
- <main style={{ fontFamily: "system-ui", padding: 20 }}>
1246
- <h1>Widget 面板</h1>
1247
- <pre style={{ whiteSpace: "pre-wrap" }}>
1974
+ <main style={{ flex: 1, minHeight: 0, minWidth: 0, display: "flex", flexDirection: "column", padding: 20 }}>
1975
+ <h1 style={{ flexShrink: 0 }}>Widget 面板</h1>
1976
+ <pre className="widget-json">
1248
1977
  {JSON.stringify(data, null, 2)}
1249
1978
  </pre>
1250
1979
  </main>
@@ -1252,21 +1981,23 @@ function WidgetView({ request }: { request?: RenderRequest }) {
1252
1981
  }
1253
1982
 
1254
1983
  function activate(context: WidgetContext) {
1255
- const host = document.createElement("main");
1984
+ ${layoutSource()} const host = document.createElement("div");
1985
+ host.className = "widget-root";
1256
1986
  document.body.replaceChildren(host);
1257
1987
  const root = createRoot(host);
1258
- let renderRequest: RenderRequest | undefined;${reportSize ? "\n let resized = false;" : ""}
1988
+ let renderRequest: RenderRequest | undefined;${resizeState}
1259
1989
  const render = () => root.render(<WidgetView request={renderRequest} />);
1260
1990
  render();
1261
1991
  context.render((request) => {
1992
+ document.body.style.background = request.context.theme.tokens.background;
1993
+ document.body.style.color = request.context.theme.tokens.foreground;
1262
1994
  renderRequest = request;
1263
- render();${reportSize ? `
1264
- if (!resized) {
1265
- context.resize(640, 360);
1266
- resized = true;
1267
- }` : ""}
1995
+ render();${resizeCall}
1996
+ });
1997
+ context.subscriptions.add(() => {
1998
+ root.unmount();
1999
+ host.remove();
1268
2000
  });
1269
- context.subscriptions.add(() => root.unmount());
1270
2001
  }
1271
2002
 
1272
2003
  export { activate };
@@ -1276,7 +2007,13 @@ function hostSource({ sourceFile }) {
1276
2007
  return `import { runWidgetHost } from "@dp-bohrium/widget-sdk";
1277
2008
  import * as widget from "./${sourceFile.split("/").at(-1)}";
1278
2009
 
1279
- runWidgetHost(widget);
2010
+ runWidgetHost(widget, {
2011
+ events: { onError: (error) => console.error("Widget 协议错误", error) },
2012
+ onStartupError: (error) => {
2013
+ document.body.textContent = "Widget 初始化失败:" + error.message;
2014
+ console.error("Widget 初始化失败", error);
2015
+ },
2016
+ });
1280
2017
  `;
1281
2018
  }
1282
2019
  const TEMPLATES = Object.freeze({
@@ -1307,12 +2044,20 @@ const TEMPLATES = Object.freeze({
1307
2044
  source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1308
2045
 
1309
2046
  function activate(context: WidgetContext) {
1310
- const root = document.createElement("main");
1311
- root.textContent = "等待 Host 提供面板数据";
2047
+ ${layoutSource()} const root = document.createElement("main");
2048
+ root.className = "widget-root";
2049
+ root.style.padding = "20px";
2050
+ const display = document.createElement("pre");
2051
+ display.className = "widget-json";
2052
+ display.textContent = "等待 Host 提供面板数据";
2053
+ root.append(display);
1312
2054
  document.body.replaceChildren(root);
2055
+ context.subscriptions.add(() => root.remove());
1313
2056
  context.render((request) => {
2057
+ document.body.style.background = request.context.theme.tokens.background;
2058
+ document.body.style.color = request.context.theme.tokens.foreground;
1314
2059
  const content = request.content;
1315
- root.textContent = JSON.stringify(
2060
+ display.textContent = JSON.stringify(
1316
2061
  "data" in content ? content.data : content,
1317
2062
  null,
1318
2063
  2,
@@ -1405,11 +2150,19 @@ export { activate };
1405
2150
  source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1406
2151
 
1407
2152
  function activate(context: WidgetContext) {
1408
- const root = document.createElement("main");
2153
+ ${layoutSource()} const root = document.createElement("main");
2154
+ root.className = "widget-root";
2155
+ root.style.padding = "20px";
2156
+ const display = document.createElement("pre");
2157
+ display.className = "widget-json";
2158
+ root.append(display);
1409
2159
  document.body.replaceChildren(root);
2160
+ context.subscriptions.add(() => root.remove());
1410
2161
  let resized = false;
1411
2162
  context.render((request) => {
1412
- root.textContent = JSON.stringify(request.content ?? {}, null, 2);
2163
+ document.body.style.background = request.context.theme.tokens.background;
2164
+ document.body.style.color = request.context.theme.tokens.foreground;
2165
+ display.textContent = JSON.stringify(request.content ?? {}, null, 2);
1413
2166
  if (!resized) {
1414
2167
  context.resize(640, 360);
1415
2168
  resized = true;
@@ -1466,43 +2219,58 @@ export { activate };
1466
2219
  }
1467
2220
  });
1468
2221
  //#endregion
2222
+ //#region src/toolchain.mjs
2223
+ /** Both src/ and the published dist/ locate resources in this package. */
2224
+ function getAssetRoot() {
2225
+ return fileURLToPath(new URL("../assets/", import.meta.url));
2226
+ }
2227
+ function getToolchain() {
2228
+ const path = join(getAssetRoot(), "toolchain.json");
2229
+ if (!existsSync(path)) throw new Error("缺少 Widget CLI 工具链资产;源码运行前请执行 pnpm --filter @dp-bohrium/widget-cli build:assets,发布包请重新安装。");
2230
+ return JSON.parse(readFileSync(path, "utf8"));
2231
+ }
2232
+ function getDocumentation() {
2233
+ return getToolchain().documentation;
2234
+ }
2235
+ function getSchemaPath(name = "widget-manifest.schema.json") {
2236
+ if (name !== basename(name) || !name.endsWith(".schema.json")) throw new Error(`不支持的 Schema 名称:${name}`);
2237
+ const path = join(getAssetRoot(), "schemas", name);
2238
+ if (!existsSync(path)) throw new Error(`Widget CLI Schema 不存在:${name}`);
2239
+ return path;
2240
+ }
2241
+ //#endregion
1469
2242
  //#region src/cli.mjs
1470
- const require = createRequire(import.meta.url);
1471
- const packageJson = require("../package.json");
1472
- const sdkPackage = require("@dp-bohrium/widget-sdk/package.json");
1473
- const contractsPackage = require("@dp-bohrium/widget-contracts/package.json");
1474
- const COMMANDS = [
1475
- "create",
1476
- "check",
1477
- "validate",
1478
- "build",
1479
- "dev",
1480
- "pack",
1481
- "doctor",
1482
- "schema",
1483
- "migrate",
1484
- "version",
1485
- "help"
1486
- ];
1487
- function usage() {
1488
- return `bohr-widget — Bohrium Widget 插件工具
1489
-
1490
- 用法:
1491
- bohr-widget create <目录> [--template panel|file-preview] [--ui vanilla|react] [--panel-id <id>]
1492
- bohr-widget check <目录|manifest.json|all> [--json]
1493
- bohr-widget validate <目录|manifest.json|all> [--json]
1494
- bohr-widget build <目录> [--json]
1495
- bohr-widget dev <目录> [--open] [--watch] [--no-build] [--json] [--port <端口>] [--scheme <scheme>] [--bridge <url>]
1496
- bohr-widget pack <目录> [--out <zip>] [--include-preview-fixtures] [--no-build] [--json]
1497
- bohr-widget doctor [目录] [--json]
1498
- bohr-widget schema
1499
- bohr-widget migrate dsh audit <plugin-dir> [--json]
1500
- bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [--json]
1501
- bohr-widget version [--json]
1502
- bohr-widget help | -h | --help
1503
-
1504
- --json 只输出一个 JSON 文档;check all --json 输出 checked/passed/failed/results[] 汇总。
1505
- `;
2243
+ const toolchain = getToolchain();
2244
+ const packageJson = { version: toolchain.cli };
2245
+ const sdkPackage = { version: toolchain.profile.sdk };
2246
+ const contractsPackage = { version: toolchain.profile.contracts };
2247
+ function usage(path = []) {
2248
+ return renderHelp(path, {
2249
+ cli: packageJson.version,
2250
+ sdk: sdkPackage.version,
2251
+ contracts: contractsPackage.version,
2252
+ documentation: getDocumentation(),
2253
+ generatedProjectNode: GENERATED_PROJECT_NODE
2254
+ });
2255
+ }
2256
+ function requestedHelpPath(argv) {
2257
+ const hasHelpFlag = argv.includes("--help") || argv.includes("-h");
2258
+ if (argv.length > 0 && argv[0] !== "help" && !hasHelpFlag) return null;
2259
+ const args = argv.filter((arg) => arg !== "--help" && arg !== "-h");
2260
+ if (args[0] === "help") {
2261
+ if (args.length === 1 && hasHelpFlag) return ["help"];
2262
+ args.shift();
2263
+ }
2264
+ const command = args[0];
2265
+ if (!command || command.startsWith("-")) return [];
2266
+ if (command !== "migrate") return [command];
2267
+ let positional;
2268
+ try {
2269
+ positional = parseOptions(args.slice(1)).positional;
2270
+ } catch {
2271
+ positional = args.slice(1).filter((arg) => !arg.startsWith("-"));
2272
+ }
2273
+ return [command, ...positional.slice(0, 2)];
1506
2274
  }
1507
2275
  function parseOptions(argv) {
1508
2276
  const positional = [];
@@ -1518,7 +2286,9 @@ function parseOptions(argv) {
1518
2286
  port: 4300,
1519
2287
  scheme: "widget-host",
1520
2288
  bridge: null,
1521
- panelId: void 0
2289
+ panelId: void 0,
2290
+ hostCompat: void 0,
2291
+ project: void 0
1522
2292
  };
1523
2293
  for (let index = 0; index < argv.length; index += 1) {
1524
2294
  const argument = argv[index];
@@ -1537,7 +2307,12 @@ function parseOptions(argv) {
1537
2307
  options.output = argv[++index];
1538
2308
  if (!options.output) throw new Error("--out 需要输出路径");
1539
2309
  } else if (argument === "--open") options.open = true;
1540
- else if (argument === "--watch") options.watch = true;
2310
+ else if (argument === "--host-compat" || argument === "--project") {
2311
+ const value = argv[++index];
2312
+ if (!value || value.startsWith("--")) throw new Error(`${argument} 需要路径`);
2313
+ if (argument === "--host-compat") options.hostCompat = value;
2314
+ else options.project = value;
2315
+ } else if (argument === "--watch") options.watch = true;
1541
2316
  else if (argument === "--no-build") options.build = false;
1542
2317
  else if (argument === "--port") options.port = Number(argv[++index]);
1543
2318
  else if (argument === "--scheme") options.scheme = argv[++index];
@@ -1585,10 +2360,65 @@ function commandError(command, error, context = {}) {
1585
2360
  return structuredError({
1586
2361
  code: typeof error?.cliCode === "string" ? error.cliCode : typeof error?.code === "string" && error.code.startsWith("WIDGET_") ? error.code : `WIDGET_CLI_${commandCode(command)}_FAILED`,
1587
2362
  message: error instanceof Error ? error.message : String(error),
1588
- context,
1589
- recovery: commandRecovery(command, error)
2363
+ context: {
2364
+ ...context,
2365
+ ...error?.context
2366
+ },
2367
+ recovery: error?.recovery ?? commandRecovery(command, error)
1590
2368
  });
1591
2369
  }
2370
+ async function projectPreflight(projectRoot, options = {}) {
2371
+ const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
2372
+ const project = await validateProjectToolchain(projectRoot, toolchain);
2373
+ return {
2374
+ profile: toolchain.profile,
2375
+ hostCompatibility,
2376
+ project
2377
+ };
2378
+ }
2379
+ async function commandIntegration(options) {
2380
+ const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
2381
+ const project = options.project ? await validateProjectToolchain(resolve(options.project), toolchain, { requireInstalled: existsSync(join(resolve(options.project), "node_modules")) }) : void 0;
2382
+ const result = {
2383
+ ok: true,
2384
+ command: "integration",
2385
+ interfaceVersion: 2,
2386
+ requirements: {
2387
+ node: ">=20",
2388
+ generatedProjectNode: GENERATED_PROJECT_NODE
2389
+ },
2390
+ execution: {
2391
+ executable: process.execPath,
2392
+ entry: fileURLToPath(import.meta.url)
2393
+ },
2394
+ profile: toolchain.profile,
2395
+ toolchain,
2396
+ assetRoot: getAssetRoot(),
2397
+ documentation: getDocumentation(),
2398
+ hostCompatibility,
2399
+ ...project ? { project } : {}
2400
+ };
2401
+ if (options.json) console.log(JSON.stringify(result, null, 2));
2402
+ else console.log(`${toolchain.profile.id}\n${result.documentation.url}`);
2403
+ return result;
2404
+ }
2405
+ async function commandAdopt(directory, options) {
2406
+ if (!directory) throw new Error("adopt 需要工程目录");
2407
+ const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
2408
+ const projectRoot = resolve(directory);
2409
+ const project = await adoptProjectToolchain(projectRoot, toolchain);
2410
+ const result = {
2411
+ ok: true,
2412
+ command: "adopt",
2413
+ projectRoot,
2414
+ profile: toolchain.profile,
2415
+ project,
2416
+ hostCompatibility
2417
+ };
2418
+ if (options.json) console.log(JSON.stringify(result, null, 2));
2419
+ else console.log(`已锁定工程工具链:${toolchain.profile.id}`);
2420
+ return result;
2421
+ }
1592
2422
  /**
1593
2423
  * 目录名 → Widget name(同时用作 panel id)。中文或纯符号目录名必须回退成合法标识符,
1594
2424
  * 不能净化成 "----" 这类空壳,否则 manifest 的 name/id 会直接违反作者清单契约。
@@ -1599,6 +2429,7 @@ function deriveWidgetName(directoryName) {
1599
2429
  return slug.slice(0, 64).replace(/-+$/gu, "");
1600
2430
  }
1601
2431
  async function commandCreate(directory, options) {
2432
+ const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
1602
2433
  if (!directory) throw new Error("create 需要目标目录");
1603
2434
  const template = TEMPLATES[options.template];
1604
2435
  if (!template) throw new Error(`未知模板:${options.template}`);
@@ -1632,22 +2463,14 @@ async function commandCreate(directory, options) {
1632
2463
  await writeFile(join(root, sourceFile), source);
1633
2464
  await writeFile(join(root, hostFile), template.hostSource({ sourceFile }));
1634
2465
  await writeFile(join(root, "scripts/rename-iife.mjs"), `import { rename } from "node:fs/promises";\n\nawait rename("dist/widget.iife.js", "dist/widget.js");\n`);
1635
- await writeFile(join(root, ".gitignore"), "node_modules/\ndist/\n.artifacts/\n*.zip\n");
1636
- await writeFile(join(root, "README.md"), `# ${name}
1637
-
1638
- Bohrium Widget ${options.template} 插件工程(${options.ui})。
1639
-
1640
- \`\`\`bash
1641
- pnpm install
1642
- pnpm build
1643
- bohr-widget check .
1644
- bohr-widget pack .
1645
- \`\`\`
1646
-
1647
- - 源码入口:${sourceFile},由 ${hostFile} 调用 SDK 的 runWidgetHost 启动
1648
- - manifest.json 是作者清单,打包时由 CLI 编译为宿主清单
1649
- - 构建产物 dist/widget.js 不提交到版本库
1650
- `);
2466
+ await writeFile(join(root, ".gitignore"), "node_modules/\ndist/\n.artifacts/\n.widget-build-receipt.json\n*.zip\n");
2467
+ await writeFile(join(root, "README.md"), projectReadme({
2468
+ name,
2469
+ template: options.template,
2470
+ ui: options.ui,
2471
+ sourceFile,
2472
+ hostFile
2473
+ }));
1651
2474
  await writeFile(join(root, "tsconfig.json"), `${JSON.stringify({
1652
2475
  compilerOptions: {
1653
2476
  target: "ES2022",
@@ -1663,29 +2486,15 @@ bohr-widget pack .
1663
2486
  include: ["src"]
1664
2487
  }, null, 2)}\n`);
1665
2488
  await writeFile(join(root, "tsdown.config.ts"), `import { defineConfig } from "tsdown";\n\nexport default defineConfig({\n entry: { widget: "${hostFile}" },\n format: ["iife"],\n platform: "browser",\n target: "es2022",\n ${options.ui === "react" ? "jsx: \"automatic\"," : ""}\n dts: false,\n outDir: "dist",\n outExtensions: () => ({ js: ".js" }),\n clean: true,\n sourcemap: false,\n minify: true,\n deps: {\n alwaysBundle: ["@dp-bohrium/widget-sdk"${options.ui === "react" ? ", \"react\", \"react-dom\", \"react-dom/client\", \"react/jsx-runtime\"" : ""}],\n onlyBundle: false,\n },\n});\n`);
1666
- await writeFile(join(root, "package.json"), `${JSON.stringify({
1667
- name: `@dp-bohrium/widget-example-${name}`,
1668
- private: true,
1669
- type: "module",
1670
- scripts: { build: "tsc --noEmit && tsdown && node scripts/rename-iife.mjs" },
1671
- dependencies: {
1672
- "@dp-bohrium/widget-sdk": sdkPackage.version,
1673
- ...options.ui === "react" ? {
1674
- react: "^19.0.0",
1675
- "react-dom": "^19.0.0"
1676
- } : {}
1677
- },
1678
- devDependencies: {
1679
- "@dp-bohrium/widget-contracts": contractsPackage.version,
1680
- tsdown: "0.22.14",
1681
- typescript: "5.9.3",
1682
- ...options.ui === "react" ? {
1683
- "@types/react": "^19.0.0",
1684
- "@types/react-dom": "^19.0.0"
1685
- } : {}
1686
- }
1687
- }, null, 2)}\n`);
2489
+ await writeFile(join(root, "package.json"), `${JSON.stringify(projectPackage({
2490
+ name,
2491
+ ui: options.ui,
2492
+ cliVersion: packageJson.version,
2493
+ sdkVersion: sdkPackage.version,
2494
+ contractsVersion: contractsPackage.version
2495
+ }), null, 2)}\n`);
1688
2496
  const warnings = [];
2497
+ await writeProjectToolchain(root, toolchain);
1689
2498
  if (name !== directoryName) warnings.push(`目录名 "${directoryName}" 不是合法标识符,manifest 的 name 已取 "${name}";需要其他取值请直接编辑 manifest.json。`);
1690
2499
  if (options.template === "panel" && panelId !== "workspace-files") warnings.push(`Panel id "${panelId}" 需要 Host 预先注册并绑定;工作区文件面板请使用 --panel-id workspace-files。`);
1691
2500
  const result = {
@@ -1699,13 +2508,15 @@ bohr-widget pack .
1699
2508
  ...panelId ? { panelId } : {},
1700
2509
  manifestPath: join(root, "manifest.json"),
1701
2510
  packagePath: join(root, "package.json"),
2511
+ profile: toolchain.profile,
2512
+ hostCompatibility,
1702
2513
  ...warnings.length === 0 ? {} : { warnings }
1703
2514
  };
1704
2515
  if (options.json) console.log(JSON.stringify(result, null, 2));
1705
2516
  else {
1706
2517
  console.log(`已创建 Widget 工程:${root}(manifest name: ${name})`);
1707
2518
  for (const warning of warnings) console.warn(`提示:${warning}`);
1708
- console.log("下一步:pnpm install && pnpm build && bohr-widget check .");
2519
+ console.log(`下一步:cd ${JSON.stringify(root)},执行 pnpm install,再执行 pnpm exec bohr-widget build . --json 和 pnpm exec bohr-widget check . --json。`);
1709
2520
  }
1710
2521
  return result;
1711
2522
  }
@@ -1841,7 +2652,28 @@ async function commandCheck(input, options, commandName = "check") {
1841
2652
  projectRoot: project
1842
2653
  };
1843
2654
  else {
1844
- result = applyCliProjectPolicies(await checkWidgetProject(project, { label }));
2655
+ try {
2656
+ const compatibility = await projectPreflight(project, options);
2657
+ result = {
2658
+ ...applyCliProjectPolicies(await checkWidgetProject(project, {
2659
+ label,
2660
+ toolchain: { sdkVersion: toolchain.profile.sdk }
2661
+ })),
2662
+ ...compatibility
2663
+ };
2664
+ } catch (error) {
2665
+ result = {
2666
+ ok: false,
2667
+ label,
2668
+ projectRoot: project,
2669
+ checks: [{
2670
+ name: "工程工具链与宿主兼容检查",
2671
+ ok: false
2672
+ }],
2673
+ errors: [error.message],
2674
+ compatibilityError: commandError(commandName, error, { projectRoot: project })
2675
+ };
2676
+ }
1845
2677
  if (lint?.warnings.length) result.warnings = [...lint.warnings, ...result.warnings ?? []];
1846
2678
  if (!result.ok && result.manifestKind === "host" && looksLikeAuthorManifest(result.sourceManifest)) result.errors = [...result.errors, "manifest.json 含作者字段 contributes,却被按宿主清单校验:请补齐 version,并确认 contributes.widget.type 为 panel 或 file-preview。"];
1847
2679
  }
@@ -1850,7 +2682,7 @@ async function commandCheck(input, options, commandName = "check") {
1850
2682
  if (input !== "all") printResult(options.json ? {
1851
2683
  command: commandName,
1852
2684
  ...result,
1853
- ...result.ok ? {} : { error: structuredError({
2685
+ ...result.ok ? {} : { error: result.compatibilityError ?? structuredError({
1854
2686
  code: result.lintErrors ? "WIDGET_CLI_MANIFEST_INVALID" : "WIDGET_CLI_VALIDATION_FAILED",
1855
2687
  message: "Widget 工程校验失败。",
1856
2688
  context: {
@@ -1914,9 +2746,9 @@ function detectPackageManager(root, projectPackage) {
1914
2746
  for (const [lockfile, manager] of LOCKFILE_MANAGERS) if (existsSync(join(root, lockfile))) return manager;
1915
2747
  return "npm";
1916
2748
  }
1917
- function firstFailureLine(output) {
1918
- const lines = String(output ?? "").split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
1919
- const line = lines.find((item) => !/^(?:npm|pnpm|yarn)\s+(?:warn|warning)\b/iu.test(item)) ?? lines[0];
2749
+ function firstFailureLine(stderr, stdout) {
2750
+ const lines = [stderr, stdout].flatMap((output) => String(output ?? "").split("\n")).map((item) => item.trim()).filter((item) => item.length > 0);
2751
+ const line = lines.find((item) => !/^(?:(?:npm|pnpm|yarn)\s+(?:warn|warning)\b|>)/iu.test(item)) ?? lines[0];
1920
2752
  return line ? line.slice(0, 300) : void 0;
1921
2753
  }
1922
2754
  const UNRESOLVED_BUILD_PATTERNS = [
@@ -1969,7 +2801,7 @@ async function runProjectBuild(root, { capture = false } = {}) {
1969
2801
  Object.assign(error, result, { cliCode: "WIDGET_CLI_BUILD_UNRESOLVED_IMPORT" });
1970
2802
  reject(error);
1971
2803
  } else {
1972
- const detail = firstFailureLine(stderr) ?? firstFailureLine(stdout);
2804
+ const detail = firstFailureLine(stderr, stdout);
1973
2805
  const error = /* @__PURE__ */ new Error(`构建失败(exit ${code ?? signal ?? "unknown"})${detail ? `:${detail}` : ""}`);
1974
2806
  Object.assign(error, result);
1975
2807
  reject(error);
@@ -1981,13 +2813,21 @@ async function commandBuild(directory, options = {}) {
1981
2813
  if (!directory) throw new Error("build 需要工程目录");
1982
2814
  const root = resolve(directory);
1983
2815
  try {
2816
+ const compatibility = await projectPreflight(root, options);
2817
+ await rm(join(root, BUILD_RECEIPT_FILE), { force: true });
2818
+ const inputsBefore = await captureBuildInputs(root);
1984
2819
  const build = await runProjectBuild(root, { capture: true });
2820
+ await validateProjectToolchain(root, toolchain);
2821
+ const receipt = await writeBuildReceipt(root, toolchain, { inputsBefore });
1985
2822
  const result = {
1986
2823
  ok: true,
1987
2824
  command: "build",
1988
2825
  projectRoot: root,
1989
2826
  stdout: build.stdout,
1990
- stderr: build.stderr
2827
+ stderr: build.stderr,
2828
+ ...compatibility,
2829
+ hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
2830
+ receipt
1991
2831
  };
1992
2832
  if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
1993
2833
  else if (options.emit !== false) {
@@ -2017,8 +2857,23 @@ async function commandBuild(directory, options = {}) {
2017
2857
  return result;
2018
2858
  }
2019
2859
  }
2860
+ const RUNTIME_MAX_ASSET_BYTES = 1048576;
2861
+ function oversizedAssetWarnings(files) {
2862
+ const oversized = /* @__PURE__ */ new Map();
2863
+ for (const [path, bytes] of files) if (bytes.byteLength > RUNTIME_MAX_ASSET_BYTES) oversized.set(normalizeArchivePath(path), bytes.byteLength);
2864
+ return [...oversized].map(([path, size]) => `${path} 为 ${(size / 1048576).toFixed(2)} MiB(${size} 字节),超过 Widget Runtime 单资产 1 MiB 上限:上传 Shrimp 会被拒绝,云端也无法加载。请确认 tsdown.config.ts 已开启 minify: true,或拆分、移除体积较大的依赖。`);
2865
+ }
2020
2866
  async function commandPack(directory, options) {
2021
2867
  if (!directory) throw new Error("pack 需要工程目录");
2868
+ const root = resolve(directory);
2869
+ const compatibility = await projectPreflight(root, options);
2870
+ const lint = lintProjectManifest(root);
2871
+ if (lint && !lint.ok) {
2872
+ const error = new Error(lint.errors.join("\n"));
2873
+ error.cliCode = "WIDGET_CLI_MANIFEST_INVALID";
2874
+ throw error;
2875
+ }
2876
+ if (!options.build) await assertBuildReceipt(root, toolchain);
2022
2877
  if (options.build) {
2023
2878
  const build = await commandBuild(directory, {
2024
2879
  ...options,
@@ -2038,7 +2893,10 @@ async function commandPack(directory, options) {
2038
2893
  return build;
2039
2894
  }
2040
2895
  }
2041
- const result = applyCliProjectPolicies(await checkWidgetProject(resolve(directory), { requireBuiltEntry: true }));
2896
+ const result = applyCliProjectPolicies(await checkWidgetProject(resolve(directory), {
2897
+ requireBuiltEntry: true,
2898
+ toolchain: { sdkVersion: toolchain.profile.sdk }
2899
+ }));
2042
2900
  if (!options.json) printResult(result, false);
2043
2901
  if (!result.ok) {
2044
2902
  if (options.json) console.log(JSON.stringify({
@@ -2071,7 +2929,9 @@ async function commandPack(directory, options) {
2071
2929
  const warnings = [];
2072
2930
  if (options.includePreviewFixtures && !hasFixtures) warnings.push("使用了 --include-preview-fixtures,但 preview/fixtures.json 不存在,ZIP 未包含任何 fixtures。");
2073
2931
  else if (hasFixtures && !options.includePreviewFixtures) warnings.push("工程包含 preview/fixtures.json,未使用 --include-preview-fixtures,ZIP 未包含 fixtures。");
2074
- const bundle = buildWidgetPackage(result, { includePreviewFixtures: fixturesIncluded });
2932
+ const files = collectWidgetPackageFiles(result, { includePreviewFixtures: fixturesIncluded });
2933
+ warnings.push(...oversizedAssetWarnings(files));
2934
+ const bundle = storedZip(files);
2075
2935
  await mkdir(dirname(destination), { recursive: true });
2076
2936
  await writeFile(destination, bundle);
2077
2937
  const artifact = {
@@ -2086,9 +2946,11 @@ async function commandPack(directory, options) {
2086
2946
  sdk: sdkPackage.version,
2087
2947
  contracts: contractsPackage.version
2088
2948
  },
2949
+ ...compatibility,
2950
+ hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
2089
2951
  manifest: {
2090
2952
  name: result.manifest.name,
2091
- entry: result.manifest.widgetConfig.entry,
2953
+ entry: normalizeArchivePath(result.manifest.widgetConfig.entry),
2092
2954
  contribution: result.manifest.widgetConfig.contribution,
2093
2955
  sampleIds: (result.manifest.widgetConfig.samples ?? []).map((sample) => sample.id)
2094
2956
  },
@@ -2124,10 +2986,14 @@ async function commandMigrate(positional, options) {
2124
2986
  }
2125
2987
  if (action === "scaffold") {
2126
2988
  if (!options.output) throw new Error("migrate dsh scaffold 需要 --out <widget-dir>");
2989
+ const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
2127
2990
  const result = await scaffoldDshWidget(input, options.output, {
2128
2991
  sdkVersion: sdkPackage.version,
2129
2992
  contractsVersion: contractsPackage.version
2130
2993
  });
2994
+ await writeProjectToolchain(result.projectRoot, toolchain);
2995
+ result.profile = toolchain.profile;
2996
+ result.hostCompatibility = hostCompatibility;
2131
2997
  if (options.json) console.log(JSON.stringify(result, null, 2));
2132
2998
  else console.log(`已生成迁移 Widget:${result.projectRoot}`);
2133
2999
  return result;
@@ -2139,6 +3005,7 @@ async function commandDoctor(directory, options) {
2139
3005
  const packagePath = join(root, "package.json");
2140
3006
  const manifestPath = join(root, "manifest.json");
2141
3007
  const report = {
3008
+ command: "doctor",
2142
3009
  cli: packageJson.version,
2143
3010
  node: process.versions.node,
2144
3011
  sdk: sdkPackage.version,
@@ -2164,6 +3031,36 @@ async function commandDoctor(directory, options) {
2164
3031
  report.entry = existsSync(join(root, report.entryPath));
2165
3032
  report.ok = report.nodeSupported && report.packageJson && report.manifest && report.manifestValid;
2166
3033
  report.ready = report.ok && report.dependenciesInstalled && report.entry;
3034
+ report.profile = toolchain.profile;
3035
+ report.toolchainLocked = existsSync(join(root, "widget-toolchain.json"));
3036
+ report.hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
3037
+ if (!report.toolchainLocked) {
3038
+ report.ready = false;
3039
+ report.warnings.push("工程尚未锁定工具链:安装依赖后执行 bohr-widget adopt .。");
3040
+ }
3041
+ if (report.packageJson) try {
3042
+ report.projectToolchain = await validateProjectToolchain(root, toolchain, {
3043
+ requireInstalled: report.dependenciesInstalled,
3044
+ requireLock: false
3045
+ });
3046
+ const lint = lintProjectManifest(root);
3047
+ const candidate = await checkWidgetProject(root, {
3048
+ requireBuiltEntry: false,
3049
+ toolchain: { sdkVersion: toolchain.profile.sdk }
3050
+ });
3051
+ if (lint && !lint.ok) candidate.errors.push(...lint.errors);
3052
+ report.candidateProfile = candidate.ok && (!lint || lint.ok) ? toolchain.profile.id : null;
3053
+ if (!report.candidateProfile) {
3054
+ report.ok = false;
3055
+ report.ready = false;
3056
+ report.warnings.push(...candidate.errors);
3057
+ }
3058
+ } catch (error) {
3059
+ report.ok = false;
3060
+ report.ready = false;
3061
+ report.error = commandError("doctor", error, { projectRoot: root });
3062
+ report.warnings.push(error.message);
3063
+ }
2167
3064
  if (report.manifest && !report.manifestValid) report.warnings.push("manifest.json 不是有效 JSON:修复语法后再执行 doctor、check 或 pack。");
2168
3065
  if (!report.dependenciesInstalled) report.warnings.push("未找到 node_modules:先执行包管理器 install,build/pack 需要工程依赖。");
2169
3066
  if (!report.entry) report.warnings.push(`未找到构建入口 ${report.entryPath}:先执行 bohr-widget build . 或工程 build 脚本。`);
@@ -2176,7 +3073,7 @@ async function commandDoctor(directory, options) {
2176
3073
  if (!report.ok) process.exitCode = 1;
2177
3074
  }
2178
3075
  function commandSchema() {
2179
- console.log(require.resolve("@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json"));
3076
+ console.log(getSchemaPath("widget-plugin-manifest.schema.json"));
2180
3077
  }
2181
3078
  function commandVersion(options) {
2182
3079
  const result = {
@@ -2191,17 +3088,43 @@ function commandVersion(options) {
2191
3088
  return result;
2192
3089
  }
2193
3090
  async function main(argv) {
2194
- if (argv.includes("--help") || argv.includes("-h")) return console.log(usage());
3091
+ const helpPath = requestedHelpPath(argv);
3092
+ if (helpPath !== null) return console.log(usage(helpPath));
3093
+ if (argv.includes("--version") || argv.includes("-v")) return commandVersion({ json: argv.includes("--json") });
2195
3094
  const command = argv[0] ?? "help";
2196
3095
  if (command === "dev") {
2197
- const exitCode = await runWidgetDevelopmentCli(argv.slice(1));
3096
+ const devArgs = [];
3097
+ let hostCompat;
3098
+ for (let index = 1; index < argv.length; index += 1) if (argv[index] === "--host-compat") {
3099
+ hostCompat = argv[++index];
3100
+ if (!hostCompat || hostCompat.startsWith("--")) throw new Error("--host-compat 需要声明文件路径");
3101
+ } else devArgs.push(argv[index]);
3102
+ const exitCode = await runWidgetDevelopmentCli(devArgs, {
3103
+ beforeBuild: ({ projectRoot }) => projectPreflight(projectRoot, { hostCompat }),
3104
+ buildProject: runProjectBuild,
3105
+ checkProject: async (projectRoot, checkOptions) => {
3106
+ await projectPreflight(projectRoot, { hostCompat });
3107
+ const lint = lintProjectManifest(projectRoot);
3108
+ if (lint && !lint.ok) {
3109
+ const error = new Error(lint.errors.join("\n"));
3110
+ error.cliCode = "WIDGET_CLI_MANIFEST_INVALID";
3111
+ throw error;
3112
+ }
3113
+ return applyCliProjectPolicies(await checkWidgetProject(projectRoot, {
3114
+ ...checkOptions,
3115
+ toolchain: { sdkVersion: toolchain.profile.sdk }
3116
+ }));
3117
+ },
3118
+ manifestOptions: { sdkVersion: toolchain.profile.sdk }
3119
+ });
2198
3120
  if (exitCode !== 0) process.exitCode = exitCode;
2199
3121
  return exitCode;
2200
3122
  }
2201
3123
  const { positional, options } = parseOptions(argv.slice(1));
2202
3124
  if (!COMMANDS.includes(command)) throw new Error(`未知命令:${command}`);
2203
- if (command === "help") return console.log(usage());
2204
3125
  if (command === "version") return commandVersion(options);
3126
+ if (command === "integration") return commandIntegration(options);
3127
+ if (command === "adopt") return commandAdopt(positional[0], options);
2205
3128
  if (command === "create") return commandCreate(positional[0], options);
2206
3129
  if (command === "check" || command === "validate") return commandCheck(positional[0], options, command);
2207
3130
  if (command === "build") return commandBuild(positional[0], options);