@dp-bohrium/widget-cli 0.3.0 → 0.5.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,12 +1,13 @@
1
1
  #!/usr/bin/env node
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-DbOeoKFJ.mjs";
2
+ import { a as normalizeArchivePath, c as compileAuthorManifest, i as prepareWidgetPackage, n as scaffoldDshWidget, o as checkWidgetProject, s as resolveWidgetProjects, t as auditDshPlugin } from "./dsh-BkPMTfZ_.mjs";
3
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
5
- import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
6
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
+ import { constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
5
+ import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { spawn, spawnSync } from "node:child_process";
9
9
  import { createServer } from "node:http";
10
+ import { isDeepStrictEqual } from "node:util";
10
11
  //#region ../../scripts/widget/widget-dev.mjs
11
12
  const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
12
13
  const DEFAULT_PORT = 4300;
@@ -56,7 +57,6 @@ const RESOURCE_BUDGET = {
56
57
  },
57
58
  files: {
58
59
  readBytesPerRequest: 4194304,
59
- readBytesPerRun: 16777216,
60
60
  callsPerMinute: 120,
61
61
  listPageSize: 100
62
62
  },
@@ -285,7 +285,6 @@ function requestedGrants(requested = {}) {
285
285
  name: "files.read",
286
286
  limits: {
287
287
  bytesPerRequest: 4194304,
288
- bytesPerRun: 16777216,
289
288
  callsPerMinute: 120
290
289
  }
291
290
  });
@@ -383,7 +382,7 @@ function panelDevelopmentDescriptor(generation, profileUrl, statusUrl) {
383
382
  widgetId,
384
383
  widgetVersionId,
385
384
  snapshot: {
386
- id,
385
+ panelType: id,
387
386
  title: generation.manifest.name,
388
387
  state: "complete",
389
388
  data: {}
@@ -1126,9 +1125,10 @@ async function startWidgetDevelopment(projectRoot, options) {
1126
1125
  session
1127
1126
  };
1128
1127
  }
1129
- function emitCliEnvelope(type, data, error = false) {
1128
+ function emitCliEnvelope(type, data, error = false, emit) {
1130
1129
  const line = formatDevelopmentEvent(type, data);
1131
- if (error) console.error(line);
1130
+ if (emit) emit(line, error);
1131
+ else if (error) console.error(line);
1132
1132
  else console.log(line);
1133
1133
  }
1134
1134
  async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides = {}) {
@@ -1156,7 +1156,7 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides =
1156
1156
  projectRoot,
1157
1157
  errors: check.errors
1158
1158
  })
1159
- });
1159
+ }, false, overrides.emit);
1160
1160
  else {
1161
1161
  console.error("Widget Dev 启动前 L0 check 失败:");
1162
1162
  check.errors.forEach((error) => {
@@ -1175,7 +1175,7 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides =
1175
1175
  ok: false,
1176
1176
  command: "dev",
1177
1177
  error: createWidgetDevError(error, { input: argv[0] ?? null })
1178
- });
1178
+ }, false, overrides.emit);
1179
1179
  else console.error(`${String(error)}\n\n${usage$1()}`);
1180
1180
  return 1;
1181
1181
  }
@@ -1215,10 +1215,16 @@ function commandTopics(context) {
1215
1215
  ]
1216
1216
  });
1217
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> [选项]"],
1218
+ description: "只读比较来源与 Agent 准备的当前工程候选,再安全复制到新目录;另提供 DSH 来源审计与脚手架。",
1219
+ usage: [
1220
+ "bohr-widget migrate project plan <source> [--candidate <候选目录>] --json",
1221
+ "bohr-widget migrate project apply <source> --plan <plan.json> --out <新目录> --json",
1222
+ "bohr-widget migrate dsh audit <plugin-dir> [选项]",
1223
+ "bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [选项]"
1224
+ ],
1220
1225
  arguments: [
1221
- "dsh 当前唯一支持的迁移来源",
1226
+ "project 比较已准备候选,再按摘要绑定的计划创建新工程",
1227
+ "dsh 已审查来源的审计与脚手架",
1222
1228
  "audit 只读审计来源、指纹和能力缺口",
1223
1229
  "scaffold 使用匹配的已审查配方创建新工程"
1224
1230
  ],
@@ -1230,7 +1236,32 @@ function commandTopics(context) {
1230
1236
  "使用 bohr-widget migrate dsh <audit|scaffold> --help 查看详细参数。"
1231
1237
  ]
1232
1238
  };
1239
+ const projectMigration = (action) => ({
1240
+ description: action === "apply" ? "重新核对已审阅计划、来源、候选和目标摘要,在新目录复制已准备候选。" : "只读比较完整来源和独立准备的候选,列出全部文件变更、阻断与人工步骤。",
1241
+ usage: [action === "apply" ? `bohr-widget migrate project apply <source> --plan <plan.json> --out <新目录> [选项]` : `bohr-widget migrate project plan <source> [选项]`],
1242
+ arguments: ["source 必填;完整作者源码目录;只作为数据,不选择或执行旧工具链"],
1243
+ options: [...action === "apply" ? [
1244
+ "--plan <plan.json> 必填;plan --json 保存的完整对象",
1245
+ "--out <新目录> 必填;与来源和候选目录树分离,不覆盖已有目录",
1246
+ HOST_OPTION
1247
+ ] : ["--candidate <候选目录> Agent 或开发者依据当前 SDK 独立准备的当前工程;不提供时 canApply=false"], JSON_OPTION],
1248
+ examples: [action === "apply" ? `bohr-widget migrate project apply ./old-widget --plan ./plan.json --out ./new-widget --json` : `bohr-widget migrate project plan ./source --candidate ./prepared-current-project --json > plan.json`],
1249
+ notes: [
1250
+ "plan 的 ok 表示报告生成成功;只有 canApply=true 且 blockers 为空才能 apply。",
1251
+ "没有自动版本转换或源码 codemod;Agent 先依据 SDK 模板、示例和文档准备并审阅独立候选。",
1252
+ "不提供候选时只给准备步骤且 canApply=false;ZIP/编译产物需要先提供完整作者源码。",
1253
+ "apply 只从计划读取候选路径,不接受 --candidate 覆盖;重算完整计划并核验来源/候选/目标摘要。",
1254
+ "不执行任何来源或候选脚本;原字节复制候选源码、配置和依赖锁,校验仅覆盖结构与依赖声明。",
1255
+ "复制结果仍须安装依赖、build、check、pack 与真实 Host 验收;不复用安装、构建产物或旧 receipt。"
1256
+ ]
1257
+ });
1233
1258
  return {
1259
+ "migrate project": {
1260
+ ...projectMigration("plan"),
1261
+ usage: ["bohr-widget migrate project plan|apply <source> [选项]"]
1262
+ },
1263
+ "migrate project plan": projectMigration("plan"),
1264
+ "migrate project apply": projectMigration("apply"),
1234
1265
  create: {
1235
1266
  description: "创建可独立安装、构建和打包的 Widget 工程,生成源码、样例和工具链锁定文件。",
1236
1267
  usage: ["bohr-widget create <目录> [选项]"],
@@ -1239,10 +1270,16 @@ function commandTopics(context) {
1239
1270
  "--template <模板> panel 或 file-preview(默认 panel)",
1240
1271
  "--ui <模式> vanilla 或 react(默认 vanilla)",
1241
1272
  "--panel-id <id> 仅用于 panel;默认取规范化后的工程名",
1273
+ "--extensions <列表> 仅用于 file-preview;逗号分隔扩展名(默认 txt)",
1274
+ "--tool <名称> 按指定工具成功结果激活,不注册工具卡片",
1242
1275
  HOST_OPTION,
1243
1276
  JSON_OPTION
1244
1277
  ],
1245
- examples: ["bohr-widget create workspace-browser --ui react --panel-id workspace-files", "bohr-widget create text-preview --template file-preview --ui vanilla --json"],
1278
+ examples: [
1279
+ "bohr-widget create workspace-browser --ui react --panel-id workspace-files",
1280
+ "bohr-widget create text-preview --template file-preview --extensions txt,csv --ui vanilla --json",
1281
+ "bohr-widget create search-panel --tool ScholarSearch --json"
1282
+ ],
1246
1283
  notes: [
1247
1284
  `默认组合为 panel + vanilla;生成工程的构建环境要求 Node.js ${context.generatedProjectNode}。`,
1248
1285
  "不会自动安装依赖;创建后进入目录,运行 pnpm install 或 npm install。",
@@ -1327,6 +1364,7 @@ function commandTopics(context) {
1327
1364
  "ok 表示环境、清单与候选 profile 检查通过;ready 还要求工具链锁、已安装依赖和构建入口。",
1328
1365
  "尚未安装或构建时可能 ok=true、ready=false;先按 warnings 修复再继续。",
1329
1366
  "ready 不保证 pack --no-build 的 receipt 可复用;doctor 只检查 CLI 的 Node 要求,不验证构建器环境。",
1367
+ "deprecations 列出已废弃的作者用法;直接 contracts 依赖将在统一入口切换发布 R1 前移除,当前使用 Agent 引导的候选准备、plan 对比与 apply 复制流程,不自动转换旧源码。",
1330
1368
  "旧工程缺少 widget-toolchain.json 时,先核对精确依赖并安装,再运行 adopt。"
1331
1369
  ]
1332
1370
  },
@@ -1342,7 +1380,7 @@ function commandTopics(context) {
1342
1380
  notes: [
1343
1381
  "不传 --project 时只发现 CLI 自带工具链,不检查当前目录是否为 Widget 工程。",
1344
1382
  "传入工程时要求工具链锁;尚未安装依赖时先核对声明,已安装时同时核对实际版本。",
1345
- "JSON 使用 interfaceVersion=2、toolchain.formatVersion=2;返回 execution、profile、requirements 和 documentation。",
1383
+ "JSON 使用 interfaceVersion=3、toolchain.formatVersion=3;返回 execution、profile、requirements 和 documentation。",
1346
1384
  "调用方应使用 execution.executable 与 execution.entry 启动同一份 CLI,避免 PATH 指向另一版本。",
1347
1385
  "宿主声明由调用方提供;unknown、snapshot、confirmed 不代表上传或发布许可。"
1348
1386
  ],
@@ -1355,7 +1393,7 @@ function commandTopics(context) {
1355
1393
  options: [HOST_OPTION, JSON_OPTION],
1356
1394
  examples: ["bohr-widget adopt ./existing-widget --json", "bohr-widget adopt . --host-compat ./host.json --json"],
1357
1395
  notes: [
1358
- "先运行 doctor,恢复当前 profile 的精确 SDK/contracts 依赖并安装。",
1396
+ "先运行 doctor,恢复当前 profile 的精确 SDK 依赖并安装。",
1359
1397
  "核对实际依赖、Manifest 和 samples 后才写锁;不要求已构建入口。",
1360
1398
  "不会自动升级依赖、迁移源码或修正协议字段;检查不通过时先修复工程。"
1361
1399
  ]
@@ -1372,7 +1410,7 @@ function commandTopics(context) {
1372
1410
  ]
1373
1411
  },
1374
1412
  version: {
1375
- description: "查看当前 CLI 及其固定的 SDK/contracts 版本。",
1413
+ description: "查看当前 SDK 执行器及内置协议摘要。",
1376
1414
  usage: ["bohr-widget version [--json]", "bohr-widget --version [--json]"],
1377
1415
  options: [JSON_OPTION],
1378
1416
  examples: ["bohr-widget version --json", "bohr-widget -v"],
@@ -1385,7 +1423,9 @@ function commandTopics(context) {
1385
1423
  migrate: migrationGroup,
1386
1424
  "migrate dsh": {
1387
1425
  ...migrationGroup,
1388
- arguments: migrationGroup.arguments.slice(1)
1426
+ description: "审计 DSH 插件,并按已审查来源配方生成 Widget 工程。",
1427
+ usage: migrationGroup.usage.filter((line) => line.includes("migrate dsh")),
1428
+ arguments: migrationGroup.arguments.slice(2)
1389
1429
  },
1390
1430
  "migrate dsh audit": {
1391
1431
  description: "只读审计 DSH 插件的来源指纹、已知配方与能力迁移缺口。",
@@ -1441,9 +1481,9 @@ function section(title, lines) {
1441
1481
  return lines.length ? `\n${title}:\n${lines.map((line) => ` ${line}`).join("\n")}\n` : "";
1442
1482
  }
1443
1483
  function globalHelp(context) {
1444
- const { cli, sdk, contracts, generatedProjectNode, documentation } = context;
1484
+ const { cli, sdk, packageName = "@dp-bohrium/widget-cli", generatedProjectNode, documentation } = context;
1445
1485
  return `bohr-widget ${cli} — Bohrium Widget 插件工程工具
1446
- SDK ${sdk} / contracts ${contracts}
1486
+ SDK ${sdk}(协议与执行工具链均由 SDK 内置)
1447
1487
  创建、校验、构建、开发调试与打包;上传、发布和 Agent 绑定由 Bohr CLI 或平台执行。
1448
1488
 
1449
1489
  用法:
@@ -1461,8 +1501,9 @@ SDK ${sdk} / contracts ${contracts}
1461
1501
  integration 发现工具链、执行器和文档入口
1462
1502
  adopt <目录> 核验旧工程并写入工具链锁
1463
1503
  schema 打印作者 Manifest Schema 的本地路径
1504
+ migrate project plan|apply 比较 Agent 准备的候选并安全复制到新目录
1464
1505
  migrate dsh audit|scaffold 审计 DSH 来源或按已审查配方生成工程
1465
- version 查看 CLI 与固定的 SDK/contracts 版本
1506
+ version 查看 SDK 执行器与内置协议版本
1466
1507
  help [命令...] 查看全局或指定主题帮助
1467
1508
 
1468
1509
  全局选项:
@@ -1473,12 +1514,12 @@ SDK ${sdk} / contracts ${contracts}
1473
1514
 
1474
1515
  调用方式:
1475
1516
  工程内:pnpm exec bohr-widget ... 或 npx --no-install bohr-widget ...
1476
- 未安装:pnpm dlx @dp-bohrium/widget-cli <命令> ...
1517
+ 未安装:pnpm dlx ${packageName} <命令> ...
1477
1518
  下方示例中的 bohr-widget 可使用上述工程内调用前缀。
1478
1519
  CLI 要求 Node.js >=20;默认生成工程要求 Node.js ${generatedProjectNode}。
1479
1520
 
1480
1521
  典型开发流程:
1481
- pnpm dlx @dp-bohrium/widget-cli create my-widget --ui react --panel-id workspace-files
1522
+ pnpm dlx ${packageName} create my-widget --ui react --panel-id workspace-files
1482
1523
  cd my-widget
1483
1524
  pnpm install
1484
1525
  pnpm exec bohr-widget build .
@@ -1536,7 +1577,7 @@ const AUTHOR_MANIFEST_KEYS = Object.freeze([
1536
1577
  "runtime",
1537
1578
  "samples"
1538
1579
  ]);
1539
- const AUTHOR_MANIFEST_SCHEMA_ID = "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json";
1580
+ const AUTHOR_MANIFEST_SCHEMA_ID = "@dp-bohrium/widget-sdk/schemas/widget-plugin-manifest.schema.json";
1540
1581
  function isPlainObject(value) {
1541
1582
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1542
1583
  }
@@ -1602,10 +1643,7 @@ function lintAuthorManifest(manifest, options = {}) {
1602
1643
  //#region src/project-executor.mjs
1603
1644
  const PROJECT_TOOLCHAIN_FILE = "widget-toolchain.json";
1604
1645
  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
- });
1646
+ const PACKAGES = Object.freeze({ sdk: "@dp-bohrium/widget-sdk" });
1609
1647
  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
1648
  function failure(code, message, context, recovery) {
1611
1649
  return Object.assign(new Error(message), {
@@ -1614,7 +1652,7 @@ function failure(code, message, context, recovery) {
1614
1652
  recovery
1615
1653
  });
1616
1654
  }
1617
- function record(value) {
1655
+ function record$1(value) {
1618
1656
  return value !== null && typeof value === "object" && !Array.isArray(value);
1619
1657
  }
1620
1658
  function text(value) {
@@ -1648,7 +1686,7 @@ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.n
1648
1686
  }, ["重新获取本次目标环境的宿主兼容声明,再通过 --host-compat 传入。"]);
1649
1687
  };
1650
1688
  const value = await json(hostCompatPath, "HOST_COMPAT_INVALID", ["重新获取本次目标环境的宿主兼容声明。"]);
1651
- if (!record(value) || !record(value.statement)) invalid("缺少 statement");
1689
+ if (!record$1(value) || !record$1(value.statement)) invalid("缺少 statement");
1652
1690
  const { statement, observation } = value;
1653
1691
  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
1692
  if (expectedHost && statement.hostId !== expectedHost || expectedEnvironment && statement.environment !== expectedEnvironment) invalid("hostId/environment 与目标环境不符");
@@ -1656,7 +1694,7 @@ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.n
1656
1694
  const expiresAt = date(statement.expiresAt);
1657
1695
  const observedNow = now instanceof Date ? now.getTime() : Number(now);
1658
1696
  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");
1697
+ if (!record$1(observation) || !["online", "snapshot"].includes(observation.source) || !Number.isFinite(date(observation.fetchedAt)) || date(observation.fetchedAt) < issuedAt || date(observation.fetchedAt) > observedNow) invalid("observation 必须包含有效 source 和 fetchedAt");
1660
1698
  if (!statement.acceptedProfiles.includes(profileId)) throw failure("HOST_PROFILE_UNSUPPORTED", `宿主 ${statement.hostId}/${statement.environment} 未接受当前工具链 ${profileId}`, {
1661
1699
  profileId,
1662
1700
  hostId: statement.hostId,
@@ -1679,12 +1717,12 @@ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.n
1679
1717
  }
1680
1718
  function assertMatchingLock(lock, toolchain, root) {
1681
1719
  const { profile } = toolchain;
1682
- if (!record(lock) || lock.formatVersion !== 1 || lock.profileId !== profile.id) throw failure("PROJECT_PROFILE_MISMATCH", "工程锁定的 profile 与当前 Widget CLI 不一致", {
1720
+ if (!record$1(lock) || lock.formatVersion !== 2 || lock.profileId !== profile.id) throw failure("PROJECT_PROFILE_MISMATCH", "工程锁定格式或 profile 与当前 SDK 不一致;旧工程需准备当前源码候选后迁移,不能复用历史执行器", {
1683
1721
  projectRoot: root,
1684
1722
  expectedProfile: profile.id,
1685
1723
  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 不一致", {
1724
+ }, ["使用当前受支持的 CLI,并在新目录显式迁移工程与验收;不要回选旧 CLI 绕过 profile 差异。"]);
1725
+ if (lock.sdk !== profile.sdk || !isDeepStrictEqual(lock.protocol, toolchain.protocol)) throw failure("PROJECT_SDK_MISMATCH", "工程锁定的 SDK 或内置协议与当前 profile 不一致", {
1688
1726
  projectRoot: root,
1689
1727
  expected: profile,
1690
1728
  actual: lock
@@ -1715,12 +1753,66 @@ async function readLock(root, toolchain, required) {
1715
1753
  }
1716
1754
  return assertMatchingLock(lock, toolchain, root);
1717
1755
  }
1718
- /** Resolve from this project's node_modules, never from an ancestor or the CLI. */
1756
+ function assertInstalledPackage(actual, name, expected, root) {
1757
+ if (!record$1(actual) || actual.name !== name || actual.version !== expected) throw failure("PROJECT_SDK_MISMATCH", `${name} 实际安装版本与工程工具链不一致`, {
1758
+ projectRoot: root,
1759
+ package: name,
1760
+ expected,
1761
+ installed: actual?.version
1762
+ }, ["按工程锁定信息恢复依赖并重新安装,再重新构建。"]);
1763
+ }
1764
+ /** 名称和字节同时参与摘要;禁止通过协议资产链接借用其他安装。 */
1765
+ async function schemaDigest(directory) {
1766
+ const hash = createHash("sha256");
1767
+ const paths = [];
1768
+ async function visit(path) {
1769
+ const entry = await lstat(path);
1770
+ if (entry.isSymbolicLink()) throw new Error("SDK 协议资产不允许符号链接");
1771
+ if (entry.isDirectory()) for (const name of (await readdir(path)).sort()) await visit(join(path, name));
1772
+ else if (entry.isFile()) paths.push(path);
1773
+ else throw new Error("SDK 协议资产必须是普通文件");
1774
+ }
1775
+ await visit(directory);
1776
+ if (!paths.length) throw new Error("SDK 协议资产为空");
1777
+ for (const path of paths.sort()) {
1778
+ const bytes = await readFile(path);
1779
+ hash.update(relative(directory, path).split(sep).join("/"));
1780
+ hash.update("\0");
1781
+ hash.update(String(bytes.length));
1782
+ hash.update("\0");
1783
+ hash.update(bytes);
1784
+ }
1785
+ return `sha256:${hash.digest("hex")}`;
1786
+ }
1787
+ async function assertEmbeddedProtocol(sdkPath, toolchain, root) {
1788
+ const sdkRoot = dirname(await realpath(sdkPath));
1789
+ try {
1790
+ const embedded = JSON.parse(await readFile(join(sdkRoot, "assets/toolchain.json"), "utf8"));
1791
+ if (embedded.formatVersion !== 3 || embedded.packageName !== PACKAGES.sdk || embedded.cli !== toolchain.profile.sdk || !isDeepStrictEqual(embedded.profile, toolchain.profile) || !isDeepStrictEqual(embedded.protocol, toolchain.protocol) || !isDeepStrictEqual(embedded.digests, toolchain.digests) || embedded.protocol?.schemaVersion !== 1 || embedded.protocol?.hostApiVersion !== 1 || embedded.protocol?.digest !== embedded.digests?.schemas) throw new Error("SDK 内置协议或执行工具链元数据与当前执行器不一致");
1792
+ for (const directory of ["schemas", "assets/schemas"]) if (await schemaDigest(join(sdkRoot, directory)) !== embedded.protocol.digest) throw new Error("SDK 内置协议资产已漂移");
1793
+ } catch (error) {
1794
+ throw failure("PROJECT_SDK_MISMATCH", "工程 SDK 缺少有效的自包含协议资产或摘要不一致", {
1795
+ projectRoot: root,
1796
+ package: PACKAGES.sdk,
1797
+ cause: error.message
1798
+ }, ["在当前工程安装正确的 SDK 完整包,再重新构建;不能借用祖先目录或旧 contracts 包。"]);
1799
+ }
1800
+ }
1801
+ /** SDK 必须由工程直接安装;协议只能来自该 SDK 自己的冻结资产。 */
1719
1802
  async function validateProjectToolchain(projectRoot, toolchain, { requireInstalled = true, requireLock = true } = {}) {
1720
1803
  const root = resolve(projectRoot);
1721
1804
  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 依赖。"]);
1805
+ const pkg = await json(join(root, "package.json"), "PROJECT_SDK_MISMATCH", ["恢复工程 package.json 中的精确 SDK 依赖。"]);
1806
+ if (!record$1(pkg)) throw failure("PROJECT_SDK_MISMATCH", "工程 package.json 必须是对象", { projectRoot: root }, ["恢复工程 package.json 中的精确 SDK 依赖。"]);
1807
+ for (const section of [
1808
+ "dependencies",
1809
+ "devDependencies",
1810
+ "optionalDependencies",
1811
+ "peerDependencies"
1812
+ ]) for (const name of ["@dp-bohrium/widget-contracts", "@dp-bohrium/widget-cli"]) if (pkg[section]?.[name] !== void 0) throw failure("PROJECT_SDK_MISMATCH", "作者工程仍直接声明 contracts 或 CLI,需统一使用 SDK 入口", {
1813
+ projectRoot: root,
1814
+ package: name
1815
+ }, ["作者工程统一依赖 SDK;独立 CLI 请在工具目录或通过 npx 使用,Contracts 独立包供 Host 等消费者使用。保留作者源码,在独立候选中移除重复依赖、重新安装和构建。"]);
1724
1816
  const declared = {};
1725
1817
  const installed = {};
1726
1818
  for (const [key, name] of Object.entries(PACKAGES)) {
@@ -1730,28 +1822,35 @@ async function validateProjectToolchain(projectRoot, toolchain, { requireInstall
1730
1822
  "optionalDependencies"
1731
1823
  ].map((section) => pkg[section]?.[name]).filter((version) => version !== void 0);
1732
1824
  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}`, {
1825
+ if (key === "sdk" && declarations.length === 0 || declarations.some((version) => typeof version !== "string" || !EXACT_VERSION.test(version) || version !== expected)) throw failure("PROJECT_SDK_MISMATCH", `${name} 必须精确声明为 ${expected}`, {
1734
1826
  projectRoot: root,
1735
1827
  package: name,
1736
1828
  expected,
1737
1829
  declared: declarations
1738
1830
  }, ["按 widget-toolchain.json 恢复精确版本,再重新安装工程依赖;不要使用 ^、~、workspace 或 latest。"]);
1831
+ if (declarations.length === 0) continue;
1739
1832
  declared[key] = expected;
1740
1833
  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
- }, ["按工程锁定信息恢复依赖并重新安装,再重新构建。"]);
1834
+ const path = join(root, "node_modules", name, "package.json");
1835
+ const actual = await json(path, "PROJECT_SDK_MISMATCH", ["在该 Widget 工程目录安装依赖,再重试;父目录或 CLI 的安装不能替代工程安装。"]);
1836
+ assertInstalledPackage(actual, name, expected, root);
1748
1837
  installed[key] = actual.version;
1838
+ for (const name of ["@dp-bohrium/widget-contracts", "@dp-bohrium/widget-cli"]) if ([
1839
+ "dependencies",
1840
+ "optionalDependencies",
1841
+ "peerDependencies"
1842
+ ].some((section) => actual[section]?.[name] !== void 0)) throw failure("PROJECT_SDK_MISMATCH", "工程 SDK 仍包含独立协议或 CLI 运行依赖,不符合当前自包含发布契约", {
1843
+ projectRoot: root,
1844
+ package: name
1845
+ }, ["安装当前自包含 SDK 包,再重新构建。"]);
1846
+ await assertEmbeddedProtocol(path, toolchain, root);
1749
1847
  }
1750
1848
  return {
1751
1849
  projectRoot: root,
1752
1850
  profile: { ...toolchain.profile },
1753
1851
  declared,
1754
1852
  installed,
1853
+ deprecations: [],
1755
1854
  dependenciesVerified: requireInstalled,
1756
1855
  lock
1757
1856
  };
@@ -1761,10 +1860,10 @@ async function writeProjectToolchain(projectRoot, toolchain) {
1761
1860
  const existing = await readLock(root, toolchain, false);
1762
1861
  if (existing) return existing;
1763
1862
  const value = {
1764
- formatVersion: 1,
1863
+ formatVersion: 2,
1765
1864
  profileId: toolchain.profile.id,
1766
1865
  sdk: toolchain.profile.sdk,
1767
- contracts: toolchain.profile.contracts,
1866
+ protocol: toolchain.protocol,
1768
1867
  createdWithCli: toolchain.cli
1769
1868
  };
1770
1869
  await mkdir(root, { recursive: true });
@@ -1805,6 +1904,7 @@ function receiptToolchain(toolchain) {
1805
1904
  return {
1806
1905
  cli: toolchain.cli,
1807
1906
  profile: toolchain.profile,
1907
+ protocol: toolchain.protocol,
1808
1908
  digests: toolchain.digests
1809
1909
  };
1810
1910
  }
@@ -1849,7 +1949,7 @@ async function writeBuildReceipt(projectRoot, toolchain, { inputsBefore } = {})
1849
1949
  const outputs = await fingerprint(result.projectRoot, { outputs: true });
1850
1950
  if (outputs.files.length === 0) throw failure("BUILD_RECEIPT_MISMATCH", "构建目录 dist 没有可记录的产物", { projectRoot: result.projectRoot }, ["执行标准 build,确认 dist 产物存在后再打包。"]);
1851
1951
  const receipt = {
1852
- formatVersion: 1,
1952
+ formatVersion: 2,
1853
1953
  toolchain: receiptToolchain(toolchain),
1854
1954
  dependencies: result.installed,
1855
1955
  inputs,
@@ -1868,7 +1968,7 @@ async function assertBuildReceipt(projectRoot, toolchain) {
1868
1968
  };
1869
1969
  const receipt = await json(join(root, BUILD_RECEIPT_FILE), "BUILD_RECEIPT_MISMATCH", ["先执行 bohr-widget build,或去掉 pack 的 --no-build。"]);
1870
1970
  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 工具链或实际依赖与本次执行不一致");
1971
+ if (!record$1(receipt) || receipt.formatVersion !== 2 || JSON.stringify(receipt.toolchain) !== JSON.stringify(receiptToolchain(toolchain)) || JSON.stringify(receipt.dependencies) !== JSON.stringify(project.installed)) mismatch("receipt 工具链或实际依赖与本次执行不一致");
1872
1972
  let inputs;
1873
1973
  let outputs;
1874
1974
  try {
@@ -1881,9 +1981,411 @@ async function assertBuildReceipt(projectRoot, toolchain) {
1881
1981
  return receipt;
1882
1982
  }
1883
1983
  //#endregion
1984
+ //#region src/migration/source.mjs
1985
+ const MAX_FILES = 2e3;
1986
+ const MAX_FILE_BYTES = 8388608;
1987
+ const MAX_TOTAL_BYTES = 67108864;
1988
+ const DERIVED = /* @__PURE__ */ new Set([
1989
+ ".git",
1990
+ "node_modules",
1991
+ "dist",
1992
+ "build",
1993
+ "coverage",
1994
+ ".artifacts",
1995
+ ".widget-build-receipt.json",
1996
+ "widget-migration.json",
1997
+ ".npmrc"
1998
+ ]);
1999
+ function migrationError(code, message) {
2000
+ return Object.assign(new Error(message), {
2001
+ cliCode: `WIDGET_CLI_MIGRATION_${code}`,
2002
+ recovery: "保留源目录与候选,依据当前 SDK 文档准备并审阅候选;重新生成 plan 后使用新的 --out 目录。"
2003
+ });
2004
+ }
2005
+ function canonical(value) {
2006
+ if (Array.isArray(value)) return value.map(canonical);
2007
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
2008
+ return value;
2009
+ }
2010
+ function sha256(value) {
2011
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
2012
+ }
2013
+ function objectSha256(value) {
2014
+ return sha256(JSON.stringify(canonical(value)));
2015
+ }
2016
+ function inside(root, path) {
2017
+ const child = relative(root, path);
2018
+ return child === "" || !isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`);
2019
+ }
2020
+ /** 不跟随最终符号链接,读取前后核对同一普通文件。 */
2021
+ async function sourceFile(path, maxBytes = MAX_FILE_BYTES) {
2022
+ const before = await lstat(path);
2023
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) throw migrationError("SOURCE_UNSAFE", "来源必须是预算内的普通文件,不接受符号链接。");
2024
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2025
+ try {
2026
+ const info = await handle.stat();
2027
+ if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) throw migrationError("SOURCE_CHANGED", "读取期间来源文件身份发生变化。");
2028
+ const bytes = await handle.readFile();
2029
+ const after = await handle.stat();
2030
+ if (bytes.length > maxBytes || before.size !== after.size || before.mtimeMs !== after.mtimeMs) throw migrationError("SOURCE_CHANGED", "读取期间来源文件发生变化。");
2031
+ return bytes;
2032
+ } finally {
2033
+ await handle.close();
2034
+ }
2035
+ }
2036
+ /** 捕获作者文件和依赖锁;安装、构建、本地环境及旧 receipt 明确排除。 */
2037
+ async function sourceSnapshot(input) {
2038
+ const requested = resolve(typeof input === "string" ? input : fileURLToPath(input));
2039
+ const info = await lstat(requested);
2040
+ if (info.isSymbolicLink()) throw migrationError("SOURCE_UNSAFE", "来源不能是符号链接。");
2041
+ const root = await realpath(requested);
2042
+ if (!info.isDirectory()) throw migrationError("AUTHOR_SOURCE_REQUIRED", "迁移需要完整作者源码目录;ZIP 或编译产物不能还原源码,请先提供源码。");
2043
+ const files = /* @__PURE__ */ new Map();
2044
+ const modes = /* @__PURE__ */ new Map();
2045
+ const excluded = [];
2046
+ let total = 0;
2047
+ async function visit(directory, prefix = "") {
2048
+ for (const name of (await readdir(directory)).sort()) {
2049
+ const path = prefix ? `${prefix}/${name}` : name;
2050
+ if (DERIVED.has(name) && (!prefix || [
2051
+ ".git",
2052
+ "node_modules",
2053
+ ".npmrc"
2054
+ ].includes(name)) || /^\.env(?:\.|$)/u.test(name)) {
2055
+ excluded.push(path);
2056
+ continue;
2057
+ }
2058
+ if (normalizeArchivePath(path) !== path) throw migrationError("SOURCE_UNSAFE", "来源含非规范路径。");
2059
+ const full = join(root, path);
2060
+ const entry = await lstat(full);
2061
+ if (entry.isSymbolicLink()) throw migrationError("SOURCE_UNSAFE", `来源含符号链接:${path}`);
2062
+ if (entry.isDirectory()) await visit(full, path);
2063
+ else if (entry.isFile()) {
2064
+ if (files.size >= MAX_FILES || total + entry.size > MAX_TOTAL_BYTES) throw migrationError("SOURCE_LIMIT", "来源超出扫描预算,请人工拆分工程。");
2065
+ const bytes = await sourceFile(full);
2066
+ total += bytes.length;
2067
+ files.set(path, bytes);
2068
+ modes.set(path, entry.mode & 73);
2069
+ } else throw migrationError("SOURCE_UNSAFE", `来源含非普通文件:${path}`);
2070
+ }
2071
+ }
2072
+ await visit(root);
2073
+ return {
2074
+ root,
2075
+ kind: "directory",
2076
+ files,
2077
+ excluded,
2078
+ sha256: filesSha256(files, modes),
2079
+ modes
2080
+ };
2081
+ }
2082
+ function filesSha256(files, modes = /* @__PURE__ */ new Map()) {
2083
+ return objectSha256([...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([path, bytes]) => [
2084
+ path,
2085
+ sha256(bytes),
2086
+ modes.get(path) ?? 0
2087
+ ]));
2088
+ }
2089
+ function jsonFile(files, path) {
2090
+ if (!files.has(path)) return null;
2091
+ try {
2092
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(files.get(path)));
2093
+ } catch {
2094
+ throw migrationError("SOURCE_JSON", `来源不是有效 UTF-8 JSON:${path}`);
2095
+ }
2096
+ }
2097
+ function jsonBytes(value) {
2098
+ return Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
2099
+ }
2100
+ async function sourceRulesSha256() {
2101
+ return sha256(await sourceFile(fileURLToPath(import.meta.url), MAX_TOTAL_BYTES));
2102
+ }
2103
+ //#endregion
2104
+ //#region src/migration/project.mjs
2105
+ const RECEIPT = "widget-migration.json";
2106
+ const HASH_SCOPE = "作者文件与依赖锁的字节、执行权限;排除安装、根构建、本地环境、Git 和旧 receipt";
2107
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value);
2108
+ function authorSource(snapshot) {
2109
+ const manifest = jsonFile(snapshot.files, "manifest.json");
2110
+ const pkg = jsonFile(snapshot.files, "package.json");
2111
+ const hasSource = [...snapshot.files.keys()].some((path) => /\.[cm]?[jt]sx?$/u.test(path) && (path.startsWith("src/") || path === manifest?.entry));
2112
+ if (!record(manifest) || !record(pkg) || !hasSource) throw migrationError("AUTHOR_SOURCE_REQUIRED", "需要包含 package.json、manifest.json 和作者源码的完整目录;编译产物不能代替源码。");
2113
+ return manifest;
2114
+ }
2115
+ function snapshotDescription(snapshot, kind) {
2116
+ return {
2117
+ kind,
2118
+ path: snapshot.root,
2119
+ sha256: snapshot.sha256,
2120
+ hashScope: HASH_SCOPE,
2121
+ excluded: snapshot.excluded
2122
+ };
2123
+ }
2124
+ function fileState(snapshot, path) {
2125
+ const bytes = snapshot.files.get(path);
2126
+ return bytes ? {
2127
+ sha256: sha256(bytes),
2128
+ executable: snapshot.modes.get(path) ?? 0
2129
+ } : null;
2130
+ }
2131
+ function changesBetween(source, candidate) {
2132
+ return [.../* @__PURE__ */ new Set([...source.files.keys(), ...candidate.files.keys()])].sort().flatMap((path) => {
2133
+ const before = fileState(source, path);
2134
+ const after = fileState(candidate, path);
2135
+ if (JSON.stringify(before) === JSON.stringify(after)) return [];
2136
+ return [{
2137
+ path,
2138
+ action: before ? after ? "update" : "delete" : "create",
2139
+ before,
2140
+ after
2141
+ }];
2142
+ });
2143
+ }
2144
+ function pendingValidation(reason) {
2145
+ return {
2146
+ status: "not-run",
2147
+ ok: false,
2148
+ scope: "structure-and-declarations",
2149
+ installation: "not-run",
2150
+ build: "not-run",
2151
+ host: "not-run",
2152
+ checks: [],
2153
+ errors: [],
2154
+ reason
2155
+ };
2156
+ }
2157
+ /** 只检查已准备候选的当前声明和内容结构,不执行候选代码或安装脚本。 */
2158
+ async function candidateValidation(snapshot, toolchain) {
2159
+ const validation = {
2160
+ ...pendingValidation("候选尚未校验"),
2161
+ status: "completed"
2162
+ };
2163
+ delete validation.reason;
2164
+ let manifest;
2165
+ try {
2166
+ manifest = authorSource(snapshot);
2167
+ const lint = lintAuthorManifest(manifest);
2168
+ if (!manifest.contributes || !lint.ok) {
2169
+ validation.errors.push(...lint.errors.length ? lint.errors : ["候选必须使用当前 contributes 作者清单。"]);
2170
+ return validation;
2171
+ }
2172
+ if (typeof manifest.entry !== "string" || normalizeArchivePath(manifest.entry) !== manifest.entry || !snapshot.files.has(manifest.entry)) {
2173
+ validation.errors.push("候选 entry 必须指向安全快照中的作者源码。");
2174
+ return validation;
2175
+ }
2176
+ const compiled = compileAuthorManifest(manifest, { sdkVersion: toolchain.profile.sdk });
2177
+ for (const sample of compiled.widgetConfig?.samples ?? []) if (typeof sample.request !== "string" || normalizeArchivePath(sample.request) !== sample.request || !snapshot.files.has(sample.request)) {
2178
+ validation.errors.push("候选 sample 不在安全源码快照中。");
2179
+ return validation;
2180
+ }
2181
+ let component = snapshot.root;
2182
+ const hostEntry = compiled.widgetConfig?.entry;
2183
+ if (typeof hostEntry !== "string" || normalizeArchivePath(hostEntry) !== hostEntry) {
2184
+ validation.errors.push("候选的编译入口不是规范相对路径。");
2185
+ return validation;
2186
+ }
2187
+ for (const segment of hostEntry.split("/")) {
2188
+ component = join(component, segment);
2189
+ const entry = await lstat(component).catch((error) => {
2190
+ if (error.code === "ENOENT") return null;
2191
+ throw error;
2192
+ });
2193
+ if (!entry) break;
2194
+ if (entry.isSymbolicLink()) throw migrationError("SOURCE_UNSAFE", "校验入口不能穿过符号链接。");
2195
+ }
2196
+ const project = await validateProjectToolchain(snapshot.root, toolchain, { requireInstalled: false });
2197
+ const pkg = jsonFile(snapshot.files, "package.json");
2198
+ const cliVersions = [
2199
+ "dependencies",
2200
+ "devDependencies",
2201
+ "optionalDependencies",
2202
+ "peerDependencies"
2203
+ ].flatMap((section) => pkg[section]?.["@dp-bohrium/widget-cli"] === void 0 ? [] : [pkg[section]["@dp-bohrium/widget-cli"]]);
2204
+ if (cliVersions.length > 1 || cliVersions.some((version) => version !== toolchain.cli)) {
2205
+ validation.errors.push("候选声明的 Widget CLI 必须是本次执行器的单一精确版本。");
2206
+ return validation;
2207
+ }
2208
+ validation.checks.push({
2209
+ name: "候选精确工具链锁与依赖声明",
2210
+ ok: true
2211
+ });
2212
+ validation.toolchain = {
2213
+ profile: project.profile,
2214
+ declared: project.declared,
2215
+ dependenciesVerified: false
2216
+ };
2217
+ const checked = await checkWidgetProject(snapshot.root, {
2218
+ requireBuiltEntry: false,
2219
+ sdkVersion: toolchain.profile.sdk
2220
+ });
2221
+ validation.checks.push(...checked.checks.map(({ name, ok }) => ({
2222
+ name,
2223
+ ok
2224
+ })));
2225
+ validation.errors.push(...checked.errors);
2226
+ validation.ok = checked.ok;
2227
+ return validation;
2228
+ } catch (error) {
2229
+ if (error.cliCode === "WIDGET_CLI_MIGRATION_SOURCE_UNSAFE") throw error;
2230
+ validation.errors.push(error.message);
2231
+ return validation;
2232
+ }
2233
+ }
2234
+ async function prepare(source, candidatePath, toolchain) {
2235
+ const snapshot = await sourceSnapshot(source);
2236
+ authorSource(snapshot);
2237
+ const report = {
2238
+ ok: true,
2239
+ command: "migrate project plan",
2240
+ formatVersion: 2,
2241
+ mode: "agent-guided",
2242
+ canApply: false,
2243
+ source: snapshotDescription(snapshot, "author-source"),
2244
+ candidate: null,
2245
+ target: {
2246
+ cli: toolchain.cli,
2247
+ profile: toolchain.profile,
2248
+ execution: toolchain.digests,
2249
+ migration: objectSha256([await sourceRulesSha256(), sha256(await sourceFile(fileURLToPath(import.meta.url), 67108864))])
2250
+ },
2251
+ changes: [],
2252
+ blockers: [],
2253
+ manualSteps: [
2254
+ "依据当前 SDK 模板、示例和文档,在来源目录树之外独立准备并审阅候选;源码、依赖和锁由 Agent 或开发者明确修改,不自动转换旧格式。",
2255
+ "使用 plan --candidate <候选目录> 保存并审阅来源/候选摘要和全部文件变更,再 apply 到与两者分离的新目录。",
2256
+ "apply 不安装或运行来源与候选脚本;随后审查安装脚本、安装依赖、build、check、pack,并在真实 Host 验收。"
2257
+ ],
2258
+ validation: pendingValidation("尚未提供已准备的当前工程候选。")
2259
+ };
2260
+ let candidate = null;
2261
+ if (!candidatePath) report.blockers.push({
2262
+ code: "WIDGET_CLI_MIGRATION_CANDIDATE_REQUIRED",
2263
+ message: "没有自动源码迁移规则;请由 Agent 或开发者准备候选后用 --candidate 重新 plan。"
2264
+ });
2265
+ else {
2266
+ candidate = await sourceSnapshot(candidatePath);
2267
+ if (inside(snapshot.root, candidate.root) || inside(candidate.root, snapshot.root)) throw migrationError("CANDIDATE_UNSAFE", "候选必须独立于来源目录树,不能使用来源、子目录或祖先。");
2268
+ report.candidate = snapshotDescription(candidate, "prepared-current-project");
2269
+ report.changes = changesBetween(snapshot, candidate);
2270
+ report.validation = await candidateValidation(candidate, toolchain);
2271
+ if (!report.validation.ok) report.blockers.push({
2272
+ code: "WIDGET_CLI_MIGRATION_CANDIDATE_INVALID",
2273
+ message: "候选未通过当前工程锁、依赖声明或 Manifest/sample 结构校验;请修正候选后重新 plan。"
2274
+ });
2275
+ if ((await sourceSnapshot(candidate.root)).sha256 !== candidate.sha256) throw migrationError("CANDIDATE_CHANGED", "校验期间候选发生变化,请重新 plan。");
2276
+ }
2277
+ if ((await sourceSnapshot(snapshot.root)).sha256 !== snapshot.sha256) throw migrationError("SOURCE_CHANGED", "规划期间来源发生变化,请重新 plan。");
2278
+ report.canApply = report.blockers.length === 0;
2279
+ report.planSha256 = objectSha256(report);
2280
+ return {
2281
+ report,
2282
+ snapshot,
2283
+ candidate
2284
+ };
2285
+ }
2286
+ async function planMigration(source, toolchain, candidate) {
2287
+ return (await prepare(source, candidate, toolchain)).report;
2288
+ }
2289
+ function receiptFor(plan) {
2290
+ return {
2291
+ formatVersion: 2,
2292
+ mode: "agent-guided",
2293
+ planSha256: plan.planSha256,
2294
+ source: plan.source,
2295
+ candidate: plan.candidate,
2296
+ target: plan.target,
2297
+ outputSha256: plan.candidate.sha256
2298
+ };
2299
+ }
2300
+ async function existingResult(out, plan) {
2301
+ const info = await lstat(out);
2302
+ if (!info.isDirectory() || info.isSymbolicLink()) throw migrationError("OUT_EXISTS", "目标已存在且不是可核验的同计划迁移目录。");
2303
+ const current = await sourceSnapshot(out);
2304
+ let receipt;
2305
+ try {
2306
+ receipt = JSON.parse((await sourceFile(join(out, RECEIPT))).toString("utf8"));
2307
+ } catch {
2308
+ throw migrationError("OUT_EXISTS", "目标已有内容但缺少有效迁移记录,拒绝覆盖。");
2309
+ }
2310
+ if (JSON.stringify(canonical(receipt)) !== JSON.stringify(canonical(receiptFor(plan))) || current.sha256 !== plan.candidate.sha256) throw migrationError("OUT_EXISTS", "目标已存在或迁移结果已改变,拒绝覆盖;请使用新的 --out。");
2311
+ return true;
2312
+ }
2313
+ /** 候选路径只来自已审阅计划;全部写入私有阶段目录后排他发布,不执行代码。 */
2314
+ async function applyMigration(source, planPath, output, toolchain, hostCompat) {
2315
+ if (!planPath || !output) throw migrationError("ARGUMENT", "apply 需要 --plan <plan.json> 和 --out <新目录>。");
2316
+ const plan = JSON.parse((await sourceFile(resolve(planPath))).toString("utf8"));
2317
+ if (plan.formatVersion !== 2 || plan.mode !== "agent-guided") throw migrationError("PLAN_MISMATCH", "需要当前 agent-guided 格式 v2 的完整计划,请重新 plan。");
2318
+ const prepared = await prepare(source, plan.candidate?.path, toolchain);
2319
+ if (JSON.stringify(canonical(plan)) !== JSON.stringify(canonical(prepared.report))) throw migrationError("PLAN_MISMATCH", "计划、来源、候选或目标执行器摘要已变化;请重新 plan 并审阅,不能修改计划绕过检查。");
2320
+ if (!plan.canApply) throw migrationError("BLOCKED", "计划含 blockers,不能 apply;请先独立准备并校验候选。");
2321
+ const hostCompatibility = await assertHostCompatibility(hostCompat, toolchain);
2322
+ const requested = resolve(output);
2323
+ const parent = await realpath(dirname(requested));
2324
+ const out = join(parent, basename(requested));
2325
+ for (const input of [prepared.snapshot.root, prepared.candidate.root]) if (inside(input, out) || inside(out, input)) throw migrationError("OUT_UNSAFE", "目标必须位于来源和候选目录树之外,不能覆盖任一输入、子目录或祖先。");
2326
+ const stage = await mkdtemp(join(parent, ".widget-migration-"));
2327
+ let reservation;
2328
+ try {
2329
+ for (const [path, bytes] of prepared.candidate.files) {
2330
+ await mkdir(dirname(join(stage, path)), { recursive: true });
2331
+ await writeFile(join(stage, path), bytes, { flag: "wx" });
2332
+ await chmod(join(stage, path), 420 | (prepared.candidate.modes.get(path) ?? 0));
2333
+ }
2334
+ const checked = await candidateValidation({
2335
+ ...prepared.candidate,
2336
+ root: stage
2337
+ }, toolchain);
2338
+ if (!checked.ok) throw migrationError("TARGET_INVALID", `复制后的候选未通过结构校验:${checked.errors.join(";")}`);
2339
+ if ((await sourceSnapshot(stage)).sha256 !== prepared.candidate.sha256) throw migrationError("TARGET_CHANGED", "复制结果与已审阅候选不同,未发布结果。");
2340
+ await writeFile(join(stage, RECEIPT), jsonBytes(receiptFor(plan)), { flag: "wx" });
2341
+ if ((await sourceSnapshot(prepared.snapshot.root)).sha256 !== plan.source.sha256) throw migrationError("SOURCE_CHANGED", "生成期间来源发生变化;未发布结果,请重新 plan。");
2342
+ if ((await sourceSnapshot(prepared.candidate.root)).sha256 !== plan.candidate.sha256) throw migrationError("CANDIDATE_CHANGED", "生成期间候选发生变化;未发布结果,请重新 plan。");
2343
+ let reused = false;
2344
+ try {
2345
+ await mkdir(out);
2346
+ reservation = await lstat(out);
2347
+ } catch (error) {
2348
+ if (error.code !== "EEXIST") throw error;
2349
+ reused = await existingResult(out, plan);
2350
+ }
2351
+ if (!reused) {
2352
+ const current = await lstat(out);
2353
+ if (current.ino !== reservation.ino || current.dev !== reservation.dev || current.isSymbolicLink()) throw migrationError("OUT_CHANGED", "目标占位目录已被替换,拒绝写入。");
2354
+ await rename(stage, out);
2355
+ reservation = null;
2356
+ }
2357
+ return {
2358
+ ok: true,
2359
+ command: "migrate project apply",
2360
+ formatVersion: 2,
2361
+ mode: "agent-guided",
2362
+ projectRoot: out,
2363
+ planSha256: plan.planSha256,
2364
+ source: plan.source,
2365
+ candidate: plan.candidate,
2366
+ target: plan.target,
2367
+ outputSha256: plan.candidate.sha256,
2368
+ reused,
2369
+ hostCompatibility,
2370
+ validation: checked,
2371
+ nextSteps: plan.manualSteps
2372
+ };
2373
+ } finally {
2374
+ await rm(stage, {
2375
+ recursive: true,
2376
+ force: true
2377
+ });
2378
+ if (reservation) {
2379
+ const current = await lstat(out).catch(() => null);
2380
+ if (current?.ino === reservation.ino && current?.dev === reservation.dev) await rmdir(out).catch(() => {});
2381
+ }
2382
+ }
2383
+ }
2384
+ //#endregion
1884
2385
  //#region src/templates.mjs
1885
2386
  const GENERATED_PROJECT_NODE = "^22.18.0 || >=24.11.0";
1886
- function projectPackage({ name, ui, cliVersion, sdkVersion, contractsVersion }) {
2387
+ const PROJECT_EXECUTOR_PACKAGE = "@dp-bohrium/widget-sdk";
2388
+ function projectPackage({ name, ui, sdkVersion }) {
1887
2389
  return {
1888
2390
  name: `@dp-bohrium/widget-example-${name}`,
1889
2391
  private: true,
@@ -1902,8 +2404,6 @@ function projectPackage({ name, ui, cliVersion, sdkVersion, contractsVersion })
1902
2404
  } : {}
1903
2405
  },
1904
2406
  devDependencies: {
1905
- "@dp-bohrium/widget-cli": cliVersion,
1906
- "@dp-bohrium/widget-contracts": contractsVersion,
1907
2407
  tsdown: "0.22.14",
1908
2408
  typescript: "5.9.3",
1909
2409
  ...ui === "react" ? {
@@ -1918,8 +2418,8 @@ function projectReadme({ name, template, ui, sourceFile, hostFile }) {
1918
2418
 
1919
2419
  Bohrium Widget ${template} 插件工程(${ui})。
1920
2420
 
1921
- 默认构建器要求 Node.js \`${GENERATED_PROJECT_NODE}\`,高于 CLI 自身的 Node.js 20 要求。
1922
- 工程已经精确依赖创建它的 CLI 版本;在本目录安装后使用本地命令,无需全局安装或源码仓库。
2421
+ 默认构建器要求 Node.js \`${GENERATED_PROJECT_NODE}\`,高于执行器自身的 Node.js 20 要求。
2422
+ 工程已经精确依赖提供命令的 \`${PROJECT_EXECUTOR_PACKAGE}\`;在本目录安装后使用本地命令,无需全局安装或源码仓库。
1923
2423
 
1924
2424
  \`\`\`bash
1925
2425
  pnpm install
@@ -1965,8 +2465,7 @@ function reactSource({ reportSize = false } = {}) {
1965
2465
  resized = true;
1966
2466
  }` : "";
1967
2467
  return `import { createRoot } from "react-dom/client";
1968
- import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1969
- import type { RenderRequest } from "@dp-bohrium/widget-contracts";
2468
+ import type { RenderRequest, WidgetContext } from "@dp-bohrium/widget-sdk";
1970
2469
 
1971
2470
  function WidgetView({ request }: { request?: RenderRequest }) {
1972
2471
  const data = request?.content ?? { state: "等待 Host 提供数据" };
@@ -2018,13 +2517,13 @@ runWidgetHost(widget, {
2018
2517
  }
2019
2518
  const TEMPLATES = Object.freeze({
2020
2519
  panel: {
2021
- manifest: ({ name, panelId = name, ui = "vanilla" }) => ({
2022
- $schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
2520
+ manifest: ({ name, panelId = name, ui = "vanilla", tool }) => ({
2521
+ $schema: "@dp-bohrium/widget-sdk/schemas/widget-plugin-manifest.schema.json",
2023
2522
  name,
2024
2523
  version: "0.1.0",
2025
2524
  displayName: name,
2026
2525
  entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
2027
- activationEvents: ["session.start"],
2526
+ activationEvents: [tool ? `tool-call-result:${tool}` : "session.start"],
2028
2527
  contributes: { widget: {
2029
2528
  type: "panel",
2030
2529
  id: panelId,
@@ -2120,7 +2619,6 @@ export { activate };
2120
2619
  name: "files.read",
2121
2620
  limits: {
2122
2621
  bytesPerRequest: 65536,
2123
- bytesPerRun: 262144,
2124
2622
  callsPerMinute: 60
2125
2623
  }
2126
2624
  }
@@ -2130,16 +2628,16 @@ export { activate };
2130
2628
  })
2131
2629
  },
2132
2630
  "file-preview": {
2133
- manifest: ({ name, ui = "vanilla" }) => ({
2134
- $schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
2631
+ manifest: ({ name, ui = "vanilla", extensions = ["txt"], tool }) => ({
2632
+ $schema: "@dp-bohrium/widget-sdk/schemas/widget-plugin-manifest.schema.json",
2135
2633
  name,
2136
2634
  version: "0.1.0",
2137
2635
  displayName: name,
2138
2636
  entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
2139
- activationEvents: ["workspace-file-opened"],
2637
+ activationEvents: [tool ? `tool-call-result:${tool}` : "workspace-file-opened"],
2140
2638
  contributes: { widget: {
2141
2639
  type: "file-preview",
2142
- extensions: ["txt"]
2640
+ extensions
2143
2641
  } },
2144
2642
  samples: [{
2145
2643
  id: "default",
@@ -2177,7 +2675,7 @@ export { activate };
2177
2675
  reportSize: true
2178
2676
  }),
2179
2677
  hostSource,
2180
- sample: () => ({
2678
+ sample: ({ extensions = ["txt"] } = {}) => ({
2181
2679
  schemaVersion: 1,
2182
2680
  requestId: "sample-file-preview",
2183
2681
  instanceId: "sample-file-preview",
@@ -2188,8 +2686,8 @@ export { activate };
2188
2686
  contribution: "file-preview",
2189
2687
  file: {
2190
2688
  id: "sample",
2191
- name: "README.txt",
2192
- extension: "txt",
2689
+ name: `README.${extensions[0]}`,
2690
+ extension: extensions[0],
2193
2691
  kind: "file"
2194
2692
  },
2195
2693
  preview: { format: "text" }
@@ -2226,7 +2724,7 @@ function getAssetRoot() {
2226
2724
  }
2227
2725
  function getToolchain() {
2228
2726
  const path = join(getAssetRoot(), "toolchain.json");
2229
- if (!existsSync(path)) throw new Error("缺少 Widget CLI 工具链资产;源码运行前请执行 pnpm --filter @dp-bohrium/widget-cli build:assets,发布包请重新安装。");
2727
+ if (!existsSync(path)) throw new Error("缺少 Widget 工具链资产;源码运行前请构建当前分发包的 CLI assets,发布包请重新安装。");
2230
2728
  return JSON.parse(readFileSync(path, "utf8"));
2231
2729
  }
2232
2730
  function getDocumentation() {
@@ -2243,12 +2741,12 @@ function getSchemaPath(name = "widget-manifest.schema.json") {
2243
2741
  const toolchain = getToolchain();
2244
2742
  const packageJson = { version: toolchain.cli };
2245
2743
  const sdkPackage = { version: toolchain.profile.sdk };
2246
- const contractsPackage = { version: toolchain.profile.contracts };
2247
2744
  function usage(path = []) {
2248
2745
  return renderHelp(path, {
2249
2746
  cli: packageJson.version,
2250
2747
  sdk: sdkPackage.version,
2251
- contracts: contractsPackage.version,
2748
+ protocol: toolchain.protocol,
2749
+ packageName: toolchain.packageName,
2252
2750
  documentation: getDocumentation(),
2253
2751
  generatedProjectNode: GENERATED_PROJECT_NODE
2254
2752
  });
@@ -2287,8 +2785,12 @@ function parseOptions(argv) {
2287
2785
  scheme: "widget-host",
2288
2786
  bridge: null,
2289
2787
  panelId: void 0,
2788
+ extensions: void 0,
2789
+ tool: void 0,
2290
2790
  hostCompat: void 0,
2291
- project: void 0
2791
+ project: void 0,
2792
+ plan: void 0,
2793
+ candidate: void 0
2292
2794
  };
2293
2795
  for (let index = 0; index < argv.length; index += 1) {
2294
2796
  const argument = argv[index];
@@ -2303,14 +2805,21 @@ function parseOptions(argv) {
2303
2805
  } else if (argument === "--panel-id") {
2304
2806
  options.panelId = argv[++index];
2305
2807
  if (!options.panelId) throw new Error("--panel-id 需要稳定的 Panel id");
2808
+ } else if (argument === "--extensions" || argument === "--tool") {
2809
+ const value = argv[++index];
2810
+ if (!value || value.startsWith("--")) throw new Error(`${argument} 需要非空值`);
2811
+ if (argument === "--extensions") options.extensions = value.split(",").map((item) => item.trim());
2812
+ else options.tool = value;
2306
2813
  } else if (argument === "--out") {
2307
2814
  options.output = argv[++index];
2308
2815
  if (!options.output) throw new Error("--out 需要输出路径");
2309
2816
  } else if (argument === "--open") options.open = true;
2310
- else if (argument === "--host-compat" || argument === "--project") {
2817
+ else if (argument === "--host-compat" || argument === "--project" || argument === "--plan" || argument === "--candidate") {
2311
2818
  const value = argv[++index];
2312
2819
  if (!value || value.startsWith("--")) throw new Error(`${argument} 需要路径`);
2313
2820
  if (argument === "--host-compat") options.hostCompat = value;
2821
+ else if (argument === "--plan") options.plan = value;
2822
+ else if (argument === "--candidate") options.candidate = value;
2314
2823
  else options.project = value;
2315
2824
  } else if (argument === "--watch") options.watch = true;
2316
2825
  else if (argument === "--no-build") options.build = false;
@@ -2328,11 +2837,15 @@ function parseOptions(argv) {
2328
2837
  function printResult(result, json) {
2329
2838
  if (json) console.log(JSON.stringify(result, null, 2));
2330
2839
  else {
2840
+ printDeprecations(result.deprecations);
2331
2841
  for (const check of result.checks ?? []) console.log(`${check.ok ? "✓" : "✗"} ${check.name}`);
2332
2842
  for (const error of result.errors ?? []) console.error(`错误:${error}`);
2333
2843
  console.log(result.ok ? "校验通过" : "校验失败");
2334
2844
  }
2335
2845
  }
2846
+ function printDeprecations(deprecations = []) {
2847
+ for (const diagnostic of deprecations) console.warn(`废弃 [${diagnostic.code}]:${diagnostic.message}`);
2848
+ }
2336
2849
  function structuredError({ code, message, context = {}, recovery }) {
2337
2850
  return {
2338
2851
  code,
@@ -2373,7 +2886,8 @@ async function projectPreflight(projectRoot, options = {}) {
2373
2886
  return {
2374
2887
  profile: toolchain.profile,
2375
2888
  hostCompatibility,
2376
- project
2889
+ project,
2890
+ deprecations: project.deprecations
2377
2891
  };
2378
2892
  }
2379
2893
  async function commandIntegration(options) {
@@ -2382,7 +2896,7 @@ async function commandIntegration(options) {
2382
2896
  const result = {
2383
2897
  ok: true,
2384
2898
  command: "integration",
2385
- interfaceVersion: 2,
2899
+ interfaceVersion: 3,
2386
2900
  requirements: {
2387
2901
  node: ">=20",
2388
2902
  generatedProjectNode: GENERATED_PROJECT_NODE
@@ -2396,10 +2910,14 @@ async function commandIntegration(options) {
2396
2910
  assetRoot: getAssetRoot(),
2397
2911
  documentation: getDocumentation(),
2398
2912
  hostCompatibility,
2913
+ deprecations: project?.deprecations ?? [],
2399
2914
  ...project ? { project } : {}
2400
2915
  };
2401
2916
  if (options.json) console.log(JSON.stringify(result, null, 2));
2402
- else console.log(`${toolchain.profile.id}\n${result.documentation.url}`);
2917
+ else {
2918
+ printDeprecations(result.deprecations);
2919
+ console.log(`${toolchain.profile.id}\n${result.documentation.url}`);
2920
+ }
2403
2921
  return result;
2404
2922
  }
2405
2923
  async function commandAdopt(directory, options) {
@@ -2413,10 +2931,14 @@ async function commandAdopt(directory, options) {
2413
2931
  projectRoot,
2414
2932
  profile: toolchain.profile,
2415
2933
  project,
2934
+ deprecations: project.deprecations,
2416
2935
  hostCompatibility
2417
2936
  };
2418
2937
  if (options.json) console.log(JSON.stringify(result, null, 2));
2419
- else console.log(`已锁定工程工具链:${toolchain.profile.id}`);
2938
+ else {
2939
+ printDeprecations(result.deprecations);
2940
+ console.log(`已锁定工程工具链:${toolchain.profile.id}`);
2941
+ }
2420
2942
  return result;
2421
2943
  }
2422
2944
  /**
@@ -2435,6 +2957,9 @@ async function commandCreate(directory, options) {
2435
2957
  if (!template) throw new Error(`未知模板:${options.template}`);
2436
2958
  if (!["vanilla", "react"].includes(options.ui)) throw new Error(`未知 UI 模式:${options.ui},可选 vanilla 或 react`);
2437
2959
  if (options.panelId && options.template !== "panel") throw new Error("--panel-id 只适用于 --template panel");
2960
+ if (options.extensions && options.template !== "file-preview") throw new Error("--extensions 只适用于 --template file-preview");
2961
+ if (options.extensions && (options.extensions.length > 32 || new Set(options.extensions).size !== options.extensions.length || options.extensions.some((extension) => !/^[a-z0-9][a-z0-9.-]{0,31}$/u.test(extension)))) throw new Error("--extensions 需要 1-32 个不重复的小写扩展名,用逗号分隔且不带前导点");
2962
+ if (options.tool && (!/^[A-Za-z0-9_.:-]+$/u.test(options.tool) || `tool-call-result:${options.tool}`.length > 128)) throw new Error("--tool 需要有效工具名,只能包含字母、数字、下划线、点、冒号和连字符,激活信号最长 128 位");
2438
2963
  if (options.panelId && !/^[A-Za-z0-9_.:-]{1,128}$/u.test(options.panelId)) throw new Error("--panel-id 必须为 1-128 位,只能包含字母、数字、下划线、点、冒号和连字符");
2439
2964
  const root = resolve(directory);
2440
2965
  if (existsSync(root)) throw new Error(`目标目录已存在:${root}`);
@@ -2445,7 +2970,9 @@ async function commandCreate(directory, options) {
2445
2970
  name,
2446
2971
  panelId,
2447
2972
  sdkVersion: sdkPackage.version,
2448
- ui: options.ui
2973
+ ui: options.ui,
2974
+ extensions: options.extensions,
2975
+ tool: options.tool
2449
2976
  };
2450
2977
  const manifest = template.manifest(templateContext);
2451
2978
  const sample = template.sample(templateContext);
@@ -2489,9 +3016,7 @@ async function commandCreate(directory, options) {
2489
3016
  await writeFile(join(root, "package.json"), `${JSON.stringify(projectPackage({
2490
3017
  name,
2491
3018
  ui: options.ui,
2492
- cliVersion: packageJson.version,
2493
- sdkVersion: sdkPackage.version,
2494
- contractsVersion: contractsPackage.version
3019
+ sdkVersion: sdkPackage.version
2495
3020
  }), null, 2)}\n`);
2496
3021
  const warnings = [];
2497
3022
  await writeProjectToolchain(root, toolchain);
@@ -2708,6 +3233,7 @@ async function commandCheck(input, options, commandName = "check") {
2708
3233
  ok: result.ok,
2709
3234
  checks: result.checks,
2710
3235
  errors: result.errors,
3236
+ ...result.deprecations?.length ? { deprecations: result.deprecations } : {},
2711
3237
  ...result.warnings?.length ? { warnings: result.warnings } : {}
2712
3238
  })),
2713
3239
  ...passed ? {} : { error: structuredError({
@@ -2812,8 +3338,9 @@ async function runProjectBuild(root, { capture = false } = {}) {
2812
3338
  async function commandBuild(directory, options = {}) {
2813
3339
  if (!directory) throw new Error("build 需要工程目录");
2814
3340
  const root = resolve(directory);
3341
+ let compatibility;
2815
3342
  try {
2816
- const compatibility = await projectPreflight(root, options);
3343
+ compatibility = await projectPreflight(root, options);
2817
3344
  await rm(join(root, BUILD_RECEIPT_FILE), { force: true });
2818
3345
  const inputsBefore = await captureBuildInputs(root);
2819
3346
  const build = await runProjectBuild(root, { capture: true });
@@ -2831,6 +3358,7 @@ async function commandBuild(directory, options = {}) {
2831
3358
  };
2832
3359
  if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
2833
3360
  else if (options.emit !== false) {
3361
+ printDeprecations(result.deprecations);
2834
3362
  if (build.stdout) process.stdout.write(build.stdout);
2835
3363
  if (build.stderr) process.stderr.write(build.stderr);
2836
3364
  }
@@ -2840,6 +3368,7 @@ async function commandBuild(directory, options = {}) {
2840
3368
  ok: false,
2841
3369
  command: "build",
2842
3370
  projectRoot: root,
3371
+ ...compatibility ? { deprecations: compatibility.deprecations } : {},
2843
3372
  error: commandError("build", error, {
2844
3373
  projectRoot: root,
2845
3374
  phase: "build"
@@ -2849,6 +3378,7 @@ async function commandBuild(directory, options = {}) {
2849
3378
  };
2850
3379
  if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
2851
3380
  if (!options.json && options.emit !== false) {
3381
+ printDeprecations(result.deprecations);
2852
3382
  if (result.stdout) process.stdout.write(result.stdout);
2853
3383
  if (result.stderr) process.stderr.write(result.stderr);
2854
3384
  }
@@ -2884,6 +3414,7 @@ async function commandPack(directory, options) {
2884
3414
  if (build.stderr) process.stderr.write(build.stderr);
2885
3415
  }
2886
3416
  if (!build.ok) {
3417
+ if (!options.json) printDeprecations(build.deprecations);
2887
3418
  if (options.json) console.log(JSON.stringify({
2888
3419
  ...build,
2889
3420
  command: "pack",
@@ -2897,12 +3428,16 @@ async function commandPack(directory, options) {
2897
3428
  requireBuiltEntry: true,
2898
3429
  toolchain: { sdkVersion: toolchain.profile.sdk }
2899
3430
  }));
2900
- if (!options.json) printResult(result, false);
3431
+ if (!options.json) printResult({
3432
+ ...result,
3433
+ deprecations: compatibility.deprecations
3434
+ }, false);
2901
3435
  if (!result.ok) {
2902
3436
  if (options.json) console.log(JSON.stringify({
2903
3437
  ok: false,
2904
3438
  command: "pack",
2905
3439
  projectRoot: result.projectRoot,
3440
+ deprecations: compatibility.deprecations,
2906
3441
  error: structuredError({
2907
3442
  code: "WIDGET_CLI_PACK_VALIDATION_FAILED",
2908
3443
  message: "Widget 工程校验失败,未生成可交付 Bundle。",
@@ -2929,9 +3464,9 @@ async function commandPack(directory, options) {
2929
3464
  const warnings = [];
2930
3465
  if (options.includePreviewFixtures && !hasFixtures) warnings.push("使用了 --include-preview-fixtures,但 preview/fixtures.json 不存在,ZIP 未包含任何 fixtures。");
2931
3466
  else if (hasFixtures && !options.includePreviewFixtures) warnings.push("工程包含 preview/fixtures.json,未使用 --include-preview-fixtures,ZIP 未包含 fixtures。");
2932
- const files = collectWidgetPackageFiles(result, { includePreviewFixtures: fixturesIncluded });
2933
- warnings.push(...oversizedAssetWarnings(files));
2934
- const bundle = storedZip(files);
3467
+ const prepared = prepareWidgetPackage(result, { includePreviewFixtures: fixturesIncluded });
3468
+ warnings.push(...oversizedAssetWarnings(prepared.files));
3469
+ const bundle = prepared.archive;
2935
3470
  await mkdir(dirname(destination), { recursive: true });
2936
3471
  await writeFile(destination, bundle);
2937
3472
  const artifact = {
@@ -2939,12 +3474,12 @@ async function commandPack(directory, options) {
2939
3474
  command: "pack",
2940
3475
  projectRoot: result.projectRoot,
2941
3476
  path: destination,
2942
- bytes: bundle.byteLength,
2943
- sha256: `sha256:${createHash("sha256").update(bundle).digest("hex")}`,
3477
+ bytes: prepared.bytes,
3478
+ sha256: prepared.sha256,
2944
3479
  toolchain: {
2945
3480
  cli: packageJson.version,
2946
3481
  sdk: sdkPackage.version,
2947
- contracts: contractsPackage.version
3482
+ protocol: toolchain.protocol
2948
3483
  },
2949
3484
  ...compatibility,
2950
3485
  hostCompatibility: await assertHostCompatibility(options.hostCompat, toolchain),
@@ -2970,7 +3505,20 @@ async function commandPack(directory, options) {
2970
3505
  return artifact;
2971
3506
  }
2972
3507
  async function commandMigrate(positional, options) {
2973
- if (positional[0] !== "dsh") throw new Error("migrate 目前只支持 dsh");
3508
+ if (positional[0] === "project") {
3509
+ const [, action, source, ...extra] = positional;
3510
+ if (!source || extra.length || !["plan", "apply"].includes(action)) throw new Error("用法:migrate project plan <source> [--candidate <候选目录>];apply 另需 --plan 与 --out。");
3511
+ if (action === "plan" && (options.plan || options.output || options.hostCompat)) throw new Error("plan 完全只读,不接受 --plan、--out 或 --host-compat;宿主声明在 apply 时核对。");
3512
+ if (action === "apply" && options.candidate) throw new Error("apply 的候选只从已审阅计划读取,不接受 --candidate 覆盖");
3513
+ const result = action === "plan" ? await planMigration(source, toolchain, options.candidate) : await applyMigration(source, options.plan, options.output, toolchain, options.hostCompat);
3514
+ if (options.json) console.log(JSON.stringify(result, null, 2));
3515
+ else if (action === "plan") {
3516
+ console.log(result.canApply ? "计划可应用;审阅 JSON 后在新目录 apply。" : "迁移已阻断,请处理 blockers 并准备当前工程候选。");
3517
+ for (const blocker of result.blockers) console.log(`[${blocker.code}] ${blocker.message}`);
3518
+ } else console.log(`已${result.reused ? "核验已有" : "创建"}迁移工程:${result.projectRoot}`);
3519
+ return result;
3520
+ }
3521
+ if (positional[0] !== "dsh") throw new Error("migrate 支持 project 或 dsh;Bundle/ZIP 请提供完整作者源码目录");
2974
3522
  const action = positional[1];
2975
3523
  const input = positional[2];
2976
3524
  if (!input) throw new Error(`migrate dsh ${action ?? ""} 需要源插件目录`);
@@ -2987,10 +3535,7 @@ async function commandMigrate(positional, options) {
2987
3535
  if (action === "scaffold") {
2988
3536
  if (!options.output) throw new Error("migrate dsh scaffold 需要 --out <widget-dir>");
2989
3537
  const hostCompatibility = await assertHostCompatibility(options.hostCompat, toolchain);
2990
- const result = await scaffoldDshWidget(input, options.output, {
2991
- sdkVersion: sdkPackage.version,
2992
- contractsVersion: contractsPackage.version
2993
- });
3538
+ const result = await scaffoldDshWidget(input, options.output, { sdkVersion: sdkPackage.version });
2994
3539
  await writeProjectToolchain(result.projectRoot, toolchain);
2995
3540
  result.profile = toolchain.profile;
2996
3541
  result.hostCompatibility = hostCompatibility;
@@ -3009,14 +3554,15 @@ async function commandDoctor(directory, options) {
3009
3554
  cli: packageJson.version,
3010
3555
  node: process.versions.node,
3011
3556
  sdk: sdkPackage.version,
3012
- contracts: contractsPackage.version,
3557
+ protocol: toolchain.protocol,
3013
3558
  project: root,
3014
3559
  packageJson: existsSync(packagePath),
3015
3560
  manifest: existsSync(manifestPath),
3016
3561
  nodeSupported: Number(process.versions.node.split(".")[0]) >= 20,
3017
3562
  dependenciesInstalled: existsSync(join(root, "node_modules")),
3018
3563
  manifestValid: false,
3019
- warnings: []
3564
+ warnings: [],
3565
+ deprecations: []
3020
3566
  };
3021
3567
  if (report.manifest) try {
3022
3568
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -3043,6 +3589,8 @@ async function commandDoctor(directory, options) {
3043
3589
  requireInstalled: report.dependenciesInstalled,
3044
3590
  requireLock: false
3045
3591
  });
3592
+ report.deprecations = report.projectToolchain.deprecations;
3593
+ report.warnings.push(...report.deprecations.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`));
3046
3594
  const lint = lintProjectManifest(root);
3047
3595
  const candidate = await checkWidgetProject(root, {
3048
3596
  requireBuiltEntry: false,
@@ -3081,7 +3629,7 @@ function commandVersion(options) {
3081
3629
  command: "version",
3082
3630
  cli: packageJson.version,
3083
3631
  sdk: sdkPackage.version,
3084
- contracts: contractsPackage.version
3632
+ protocol: toolchain.protocol
3085
3633
  };
3086
3634
  if (options.json) console.log(JSON.stringify(result, null, 2));
3087
3635
  else console.log(`${result.cli} (SDK ${result.sdk})`);
@@ -3095,15 +3643,35 @@ async function main(argv) {
3095
3643
  if (command === "dev") {
3096
3644
  const devArgs = [];
3097
3645
  let hostCompat;
3646
+ let deprecationsReported = false;
3647
+ let deprecations = [];
3098
3648
  for (let index = 1; index < argv.length; index += 1) if (argv[index] === "--host-compat") {
3099
3649
  hostCompat = argv[++index];
3100
3650
  if (!hostCompat || hostCompat.startsWith("--")) throw new Error("--host-compat 需要声明文件路径");
3101
3651
  } else devArgs.push(argv[index]);
3652
+ const devJson = devArgs.includes("--json");
3102
3653
  const exitCode = await runWidgetDevelopmentCli(devArgs, {
3103
- beforeBuild: ({ projectRoot }) => projectPreflight(projectRoot, { hostCompat }),
3654
+ ...devJson ? { emit: (line, error = false) => {
3655
+ const event = JSON.parse(line);
3656
+ event.data = {
3657
+ ...event.data,
3658
+ deprecations
3659
+ };
3660
+ if (error) console.error(JSON.stringify(event));
3661
+ else console.log(JSON.stringify(event));
3662
+ } } : {},
3663
+ beforeBuild: async ({ projectRoot }) => {
3664
+ const result = await projectPreflight(projectRoot, { hostCompat });
3665
+ deprecations = result.deprecations;
3666
+ if (!devJson && !deprecationsReported && deprecations.length) {
3667
+ printDeprecations(result.deprecations);
3668
+ deprecationsReported = true;
3669
+ }
3670
+ return result;
3671
+ },
3104
3672
  buildProject: runProjectBuild,
3105
3673
  checkProject: async (projectRoot, checkOptions) => {
3106
- await projectPreflight(projectRoot, { hostCompat });
3674
+ deprecations = (await projectPreflight(projectRoot, { hostCompat })).deprecations;
3107
3675
  const lint = lintProjectManifest(projectRoot);
3108
3676
  if (lint && !lint.ok) {
3109
3677
  const error = new Error(lint.errors.join("\n"));
@@ -3121,6 +3689,8 @@ async function main(argv) {
3121
3689
  return exitCode;
3122
3690
  }
3123
3691
  const { positional, options } = parseOptions(argv.slice(1));
3692
+ if (options.candidate && !(command === "migrate" && positional[0] === "project" && positional[1] === "plan")) throw new Error("--candidate 只适用于 migrate project plan");
3693
+ if (command !== "create" && (options.extensions || options.tool)) throw new Error("--extensions 和 --tool 只适用于 create");
3124
3694
  if (!COMMANDS.includes(command)) throw new Error(`未知命令:${command}`);
3125
3695
  if (command === "version") return commandVersion(options);
3126
3696
  if (command === "integration") return commandIntegration(options);