@dp-bohrium/widget-cli 0.2.0 → 0.4.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,10 @@
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-BzxDpEcZ.mjs";
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-BvnlfCRX.mjs";
3
+ import { createRequire } from "node:module";
3
4
  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";
5
+ import { constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
6
+ import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { spawn, spawnSync } from "node:child_process";
9
10
  import { createServer } from "node:http";
@@ -1126,9 +1127,10 @@ async function startWidgetDevelopment(projectRoot, options) {
1126
1127
  session
1127
1128
  };
1128
1129
  }
1129
- function emitCliEnvelope(type, data, error = false) {
1130
+ function emitCliEnvelope(type, data, error = false, emit) {
1130
1131
  const line = formatDevelopmentEvent(type, data);
1131
- if (error) console.error(line);
1132
+ if (emit) emit(line, error);
1133
+ else if (error) console.error(line);
1132
1134
  else console.log(line);
1133
1135
  }
1134
1136
  async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides = {}) {
@@ -1156,7 +1158,7 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides =
1156
1158
  projectRoot,
1157
1159
  errors: check.errors
1158
1160
  })
1159
- });
1161
+ }, false, overrides.emit);
1160
1162
  else {
1161
1163
  console.error("Widget Dev 启动前 L0 check 失败:");
1162
1164
  check.errors.forEach((error) => {
@@ -1175,7 +1177,7 @@ async function runWidgetDevelopmentCli(argv = process.argv.slice(2), overrides =
1175
1177
  ok: false,
1176
1178
  command: "dev",
1177
1179
  error: createWidgetDevError(error, { input: argv[0] ?? null })
1178
- });
1180
+ }, false, overrides.emit);
1179
1181
  else console.error(`${String(error)}\n\n${usage$1()}`);
1180
1182
  return 1;
1181
1183
  }
@@ -1215,10 +1217,16 @@ function commandTopics(context) {
1215
1217
  ]
1216
1218
  });
1217
1219
  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
+ description: "只读比较来源与 Agent 准备的当前工程候选,再安全复制到新目录;另提供 DSH 来源审计与脚手架。",
1221
+ usage: [
1222
+ "bohr-widget migrate project plan <source> [--candidate <候选目录>] --json",
1223
+ "bohr-widget migrate project apply <source> --plan <plan.json> --out <新目录> --json",
1224
+ "bohr-widget migrate dsh audit <plugin-dir> [选项]",
1225
+ "bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [选项]"
1226
+ ],
1220
1227
  arguments: [
1221
- "dsh 当前唯一支持的迁移来源",
1228
+ "project 比较已准备候选,再按摘要绑定的计划创建新工程",
1229
+ "dsh 已审查来源的审计与脚手架",
1222
1230
  "audit 只读审计来源、指纹和能力缺口",
1223
1231
  "scaffold 使用匹配的已审查配方创建新工程"
1224
1232
  ],
@@ -1230,7 +1238,32 @@ function commandTopics(context) {
1230
1238
  "使用 bohr-widget migrate dsh <audit|scaffold> --help 查看详细参数。"
1231
1239
  ]
1232
1240
  };
1241
+ const projectMigration = (action) => ({
1242
+ description: action === "apply" ? "重新核对已审阅计划、来源、候选和目标摘要,在新目录复制已准备候选。" : "只读比较完整来源和独立准备的候选,列出全部文件变更、阻断与人工步骤。",
1243
+ usage: [action === "apply" ? `bohr-widget migrate project apply <source> --plan <plan.json> --out <新目录> [选项]` : `bohr-widget migrate project plan <source> [选项]`],
1244
+ arguments: ["source 必填;完整作者源码目录;只作为数据,不选择或执行旧工具链"],
1245
+ options: [...action === "apply" ? [
1246
+ "--plan <plan.json> 必填;plan --json 保存的完整对象",
1247
+ "--out <新目录> 必填;与来源和候选目录树分离,不覆盖已有目录",
1248
+ HOST_OPTION
1249
+ ] : ["--candidate <候选目录> Agent 或开发者依据当前 SDK 独立准备的当前工程;不提供时 canApply=false"], JSON_OPTION],
1250
+ 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`],
1251
+ notes: [
1252
+ "plan 的 ok 表示报告生成成功;只有 canApply=true 且 blockers 为空才能 apply。",
1253
+ "没有自动版本转换或源码 codemod;Agent 先依据 SDK 模板、示例和文档准备并审阅独立候选。",
1254
+ "不提供候选时只给准备步骤且 canApply=false;ZIP/编译产物需要先提供完整作者源码。",
1255
+ "apply 只从计划读取候选路径,不接受 --candidate 覆盖;重算完整计划并核验来源/候选/目标摘要。",
1256
+ "不执行任何来源或候选脚本;原字节复制候选源码、配置和依赖锁,校验仅覆盖结构与依赖声明。",
1257
+ "复制结果仍须安装依赖、build、check、pack 与真实 Host 验收;不复用安装、构建产物或旧 receipt。"
1258
+ ]
1259
+ });
1233
1260
  return {
1261
+ "migrate project": {
1262
+ ...projectMigration("plan"),
1263
+ usage: ["bohr-widget migrate project plan|apply <source> [选项]"]
1264
+ },
1265
+ "migrate project plan": projectMigration("plan"),
1266
+ "migrate project apply": projectMigration("apply"),
1234
1267
  create: {
1235
1268
  description: "创建可独立安装、构建和打包的 Widget 工程,生成源码、样例和工具链锁定文件。",
1236
1269
  usage: ["bohr-widget create <目录> [选项]"],
@@ -1239,10 +1272,16 @@ function commandTopics(context) {
1239
1272
  "--template <模板> panel 或 file-preview(默认 panel)",
1240
1273
  "--ui <模式> vanilla 或 react(默认 vanilla)",
1241
1274
  "--panel-id <id> 仅用于 panel;默认取规范化后的工程名",
1275
+ "--extensions <列表> 仅用于 file-preview;逗号分隔扩展名(默认 txt)",
1276
+ "--tool <名称> 按指定工具成功结果激活,不注册工具卡片",
1242
1277
  HOST_OPTION,
1243
1278
  JSON_OPTION
1244
1279
  ],
1245
- examples: ["bohr-widget create workspace-browser --ui react --panel-id workspace-files", "bohr-widget create text-preview --template file-preview --ui vanilla --json"],
1280
+ examples: [
1281
+ "bohr-widget create workspace-browser --ui react --panel-id workspace-files",
1282
+ "bohr-widget create text-preview --template file-preview --extensions txt,csv --ui vanilla --json",
1283
+ "bohr-widget create search-panel --tool ScholarSearch --json"
1284
+ ],
1246
1285
  notes: [
1247
1286
  `默认组合为 panel + vanilla;生成工程的构建环境要求 Node.js ${context.generatedProjectNode}。`,
1248
1287
  "不会自动安装依赖;创建后进入目录,运行 pnpm install 或 npm install。",
@@ -1327,6 +1366,7 @@ function commandTopics(context) {
1327
1366
  "ok 表示环境、清单与候选 profile 检查通过;ready 还要求工具链锁、已安装依赖和构建入口。",
1328
1367
  "尚未安装或构建时可能 ok=true、ready=false;先按 warnings 修复再继续。",
1329
1368
  "ready 不保证 pack --no-build 的 receipt 可复用;doctor 只检查 CLI 的 Node 要求,不验证构建器环境。",
1369
+ "deprecations 列出已废弃的作者用法;直接 contracts 依赖将在统一入口切换发布 R1 前移除,当前使用 Agent 引导的候选准备、plan 对比与 apply 复制流程,不自动转换旧源码。",
1330
1370
  "旧工程缺少 widget-toolchain.json 时,先核对精确依赖并安装,再运行 adopt。"
1331
1371
  ]
1332
1372
  },
@@ -1385,7 +1425,9 @@ function commandTopics(context) {
1385
1425
  migrate: migrationGroup,
1386
1426
  "migrate dsh": {
1387
1427
  ...migrationGroup,
1388
- arguments: migrationGroup.arguments.slice(1)
1428
+ description: "审计 DSH 插件,并按已审查来源配方生成 Widget 工程。",
1429
+ usage: migrationGroup.usage.filter((line) => line.includes("migrate dsh")),
1430
+ arguments: migrationGroup.arguments.slice(2)
1389
1431
  },
1390
1432
  "migrate dsh audit": {
1391
1433
  description: "只读审计 DSH 插件的来源指纹、已知配方与能力迁移缺口。",
@@ -1441,7 +1483,7 @@ function section(title, lines) {
1441
1483
  return lines.length ? `\n${title}:\n${lines.map((line) => ` ${line}`).join("\n")}\n` : "";
1442
1484
  }
1443
1485
  function globalHelp(context) {
1444
- const { cli, sdk, contracts, generatedProjectNode, documentation } = context;
1486
+ const { cli, sdk, contracts, packageName = "@dp-bohrium/widget-cli", generatedProjectNode, documentation } = context;
1445
1487
  return `bohr-widget ${cli} — Bohrium Widget 插件工程工具
1446
1488
  SDK ${sdk} / contracts ${contracts}
1447
1489
  创建、校验、构建、开发调试与打包;上传、发布和 Agent 绑定由 Bohr CLI 或平台执行。
@@ -1461,6 +1503,7 @@ SDK ${sdk} / contracts ${contracts}
1461
1503
  integration 发现工具链、执行器和文档入口
1462
1504
  adopt <目录> 核验旧工程并写入工具链锁
1463
1505
  schema 打印作者 Manifest Schema 的本地路径
1506
+ migrate project plan|apply 比较 Agent 准备的候选并安全复制到新目录
1464
1507
  migrate dsh audit|scaffold 审计 DSH 来源或按已审查配方生成工程
1465
1508
  version 查看 CLI 与固定的 SDK/contracts 版本
1466
1509
  help [命令...] 查看全局或指定主题帮助
@@ -1473,12 +1516,12 @@ SDK ${sdk} / contracts ${contracts}
1473
1516
 
1474
1517
  调用方式:
1475
1518
  工程内:pnpm exec bohr-widget ... 或 npx --no-install bohr-widget ...
1476
- 未安装:pnpm dlx @dp-bohrium/widget-cli <命令> ...
1519
+ 未安装:pnpm dlx ${packageName} <命令> ...
1477
1520
  下方示例中的 bohr-widget 可使用上述工程内调用前缀。
1478
1521
  CLI 要求 Node.js >=20;默认生成工程要求 Node.js ${generatedProjectNode}。
1479
1522
 
1480
1523
  典型开发流程:
1481
- pnpm dlx @dp-bohrium/widget-cli create my-widget --ui react --panel-id workspace-files
1524
+ pnpm dlx ${packageName} create my-widget --ui react --panel-id workspace-files
1482
1525
  cd my-widget
1483
1526
  pnpm install
1484
1527
  pnpm exec bohr-widget build .
@@ -1536,7 +1579,7 @@ const AUTHOR_MANIFEST_KEYS = Object.freeze([
1536
1579
  "runtime",
1537
1580
  "samples"
1538
1581
  ]);
1539
- const AUTHOR_MANIFEST_SCHEMA_ID = "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json";
1582
+ const AUTHOR_MANIFEST_SCHEMA_ID = "@dp-bohrium/widget-sdk/schemas/widget-plugin-manifest.schema.json";
1540
1583
  function isPlainObject(value) {
1541
1584
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1542
1585
  }
@@ -1614,7 +1657,7 @@ function failure(code, message, context, recovery) {
1614
1657
  recovery
1615
1658
  });
1616
1659
  }
1617
- function record(value) {
1660
+ function record$1(value) {
1618
1661
  return value !== null && typeof value === "object" && !Array.isArray(value);
1619
1662
  }
1620
1663
  function text(value) {
@@ -1648,7 +1691,7 @@ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.n
1648
1691
  }, ["重新获取本次目标环境的宿主兼容声明,再通过 --host-compat 传入。"]);
1649
1692
  };
1650
1693
  const value = await json(hostCompatPath, "HOST_COMPAT_INVALID", ["重新获取本次目标环境的宿主兼容声明。"]);
1651
- if (!record(value) || !record(value.statement)) invalid("缺少 statement");
1694
+ if (!record$1(value) || !record$1(value.statement)) invalid("缺少 statement");
1652
1695
  const { statement, observation } = value;
1653
1696
  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
1697
  if (expectedHost && statement.hostId !== expectedHost || expectedEnvironment && statement.environment !== expectedEnvironment) invalid("hostId/environment 与目标环境不符");
@@ -1656,7 +1699,7 @@ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.n
1656
1699
  const expiresAt = date(statement.expiresAt);
1657
1700
  const observedNow = now instanceof Date ? now.getTime() : Number(now);
1658
1701
  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");
1702
+ 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
1703
  if (!statement.acceptedProfiles.includes(profileId)) throw failure("HOST_PROFILE_UNSUPPORTED", `宿主 ${statement.hostId}/${statement.environment} 未接受当前工具链 ${profileId}`, {
1661
1704
  profileId,
1662
1705
  hostId: statement.hostId,
@@ -1679,11 +1722,11 @@ async function assertHostCompatibility(hostCompatPath, toolchain, { now = Date.n
1679
1722
  }
1680
1723
  function assertMatchingLock(lock, toolchain, root) {
1681
1724
  const { profile } = toolchain;
1682
- if (!record(lock) || lock.formatVersion !== 1 || lock.profileId !== profile.id) throw failure("PROJECT_PROFILE_MISMATCH", "工程锁定的 profile 与当前 Widget CLI 不一致", {
1725
+ if (!record$1(lock) || lock.formatVersion !== 1 || lock.profileId !== profile.id) throw failure("PROJECT_PROFILE_MISMATCH", "工程锁定的 profile 与当前 Widget CLI 不一致", {
1683
1726
  projectRoot: root,
1684
1727
  expectedProfile: profile.id,
1685
1728
  actualProfile: lock?.profileId
1686
- }, ["使用携带工程 profile 的 Widget CLI;跨 profile 迁移需显式修改和验收工程。"]);
1729
+ }, ["使用当前受支持的 CLI,并在新目录显式迁移工程与验收;不要回选旧 CLI 绕过 profile 差异。"]);
1687
1730
  if (lock.sdk !== profile.sdk || lock.contracts !== profile.contracts) throw failure("PROJECT_SDK_MISMATCH", "工程锁定的 SDK/contracts 与当前 profile 不一致", {
1688
1731
  projectRoot: root,
1689
1732
  expected: profile,
@@ -1715,12 +1758,47 @@ async function readLock(root, toolchain, required) {
1715
1758
  }
1716
1759
  return assertMatchingLock(lock, toolchain, root);
1717
1760
  }
1718
- /** Resolve from this project's node_modules, never from an ancestor or the CLI. */
1761
+ function assertInstalledPackage(actual, name, expected, root) {
1762
+ if (!record$1(actual) || actual.name !== name || actual.version !== expected) throw failure("PROJECT_SDK_MISMATCH", `${name} 实际安装版本与工程工具链不一致`, {
1763
+ projectRoot: root,
1764
+ package: name,
1765
+ expected,
1766
+ installed: actual?.version
1767
+ }, ["按工程锁定信息恢复依赖并重新安装,再重新构建。"]);
1768
+ }
1769
+ /** 只接受 SDK 自身依赖或其安装目录中的提升依赖,不向工作区祖先借用。 */
1770
+ async function sdkContractsMetadata(sdkPath, root) {
1771
+ const sdkRoot = dirname(await realpath(sdkPath));
1772
+ const resolver = createRequire(join(sdkRoot, "package.json"));
1773
+ const lookupRoots = /* @__PURE__ */ new Set([join(sdkRoot, "node_modules")]);
1774
+ for (let current = sdkRoot; dirname(current) !== current; current = dirname(current)) if (basename(current) === "node_modules") {
1775
+ lookupRoots.add(current);
1776
+ break;
1777
+ }
1778
+ try {
1779
+ const resolvedPath = await realpath(resolver.resolve(`${PACKAGES.contracts}/package.json`));
1780
+ for (const lookup of resolver.resolve.paths(PACKAGES.contracts) ?? []) {
1781
+ if (!lookupRoots.has(lookup)) continue;
1782
+ try {
1783
+ if (await realpath(join(lookup, PACKAGES.contracts, "package.json")) === resolvedPath) return await json(resolvedPath, "PROJECT_SDK_MISMATCH", ["重新安装工程 SDK 及其配套 contracts 依赖。"]);
1784
+ } catch (error) {
1785
+ if (error.code !== "ENOENT") throw error;
1786
+ }
1787
+ }
1788
+ } catch (error) {
1789
+ if (error.cliCode) throw error;
1790
+ }
1791
+ throw failure("PROJECT_SDK_MISMATCH", "工程 SDK 缺少自身可解析的配套 contracts,不能借用祖先目录或 CLI 的依赖", {
1792
+ projectRoot: root,
1793
+ package: PACKAGES.contracts
1794
+ }, ["在该 Widget 工程目录重新安装依赖,并保留 SDK 自身的传递依赖。"]);
1795
+ }
1796
+ /** SDK 必须由工程直接安装,contracts 从该 SDK 的真实依赖核对。 */
1719
1797
  async function validateProjectToolchain(projectRoot, toolchain, { requireInstalled = true, requireLock = true } = {}) {
1720
1798
  const root = resolve(projectRoot);
1721
1799
  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 依赖。"]);
1800
+ const pkg = await json(join(root, "package.json"), "PROJECT_SDK_MISMATCH", ["恢复工程 package.json 中的精确 SDK 依赖。"]);
1801
+ if (!record$1(pkg)) throw failure("PROJECT_SDK_MISMATCH", "工程 package.json 必须是对象", { projectRoot: root }, ["恢复工程 package.json 中的精确 SDK 依赖。"]);
1724
1802
  const declared = {};
1725
1803
  const installed = {};
1726
1804
  for (const [key, name] of Object.entries(PACKAGES)) {
@@ -1730,28 +1808,66 @@ async function validateProjectToolchain(projectRoot, toolchain, { requireInstall
1730
1808
  "optionalDependencies"
1731
1809
  ].map((section) => pkg[section]?.[name]).filter((version) => version !== void 0);
1732
1810
  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}`, {
1811
+ 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
1812
  projectRoot: root,
1735
1813
  package: name,
1736
1814
  expected,
1737
1815
  declared: declarations
1738
1816
  }, ["按 widget-toolchain.json 恢复精确版本,再重新安装工程依赖;不要使用 ^、~、workspace 或 latest。"]);
1817
+ if (declarations.length === 0) continue;
1739
1818
  declared[key] = expected;
1740
1819
  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
- }, ["按工程锁定信息恢复依赖并重新安装,再重新构建。"]);
1820
+ const path = join(root, "node_modules", name, "package.json");
1821
+ const actual = await json(path, "PROJECT_SDK_MISMATCH", ["在该 Widget 工程目录安装依赖,再重试;父目录或 CLI 的安装不能替代工程安装。"]);
1822
+ assertInstalledPackage(actual, name, expected, root);
1748
1823
  installed[key] = actual.version;
1824
+ if (key === "sdk") {
1825
+ const dependency = actual.dependencies?.[PACKAGES.contracts];
1826
+ if (dependency !== toolchain.profile.contracts && dependency !== "workspace:*" && dependency !== `workspace:${toolchain.profile.contracts}`) throw failure("PROJECT_SDK_MISMATCH", "工程 SDK 的 contracts 依赖与固定 profile 不一致", {
1827
+ projectRoot: root,
1828
+ package: name,
1829
+ expected: toolchain.profile.contracts,
1830
+ declared: dependency
1831
+ }, ["安装固定 profile 对应的 SDK;发布包的 contracts 依赖必须为精确版本。"]);
1832
+ const contracts = await sdkContractsMetadata(path, root);
1833
+ assertInstalledPackage(contracts, PACKAGES.contracts, toolchain.profile.contracts, root);
1834
+ installed.contracts = contracts.version;
1835
+ }
1749
1836
  }
1750
1837
  return {
1751
1838
  projectRoot: root,
1752
1839
  profile: { ...toolchain.profile },
1753
1840
  declared,
1754
1841
  installed,
1842
+ deprecations: declared.contracts ? [{
1843
+ code: "WIDGET_CLI_DIRECT_CONTRACTS_DEPRECATED",
1844
+ message: "独立消费者直接声明 contracts 的旧入口已废弃,将在统一入口切换发布 R1 前移除;协议类型与资产改从 SDK 导入。",
1845
+ replacement: {
1846
+ package: PACKAGES.sdk,
1847
+ entryPoints: [
1848
+ ".",
1849
+ "./contracts",
1850
+ "./schemas/*.json",
1851
+ "./conformance/*"
1852
+ ]
1853
+ },
1854
+ removal: {
1855
+ release: "R1",
1856
+ phase: "before-publish",
1857
+ policyDocument: "docs/plans/2026-10-29-widget-sdk-forge-unified-entry.md",
1858
+ prerequisites: [
1859
+ "unified-sdk-entries-deliverable",
1860
+ "agent-guided-project-migration-ready",
1861
+ "bohr-and-hosts-upgraded"
1862
+ ]
1863
+ },
1864
+ migration: {
1865
+ status: "agent-guided",
1866
+ project: "migrate project plan/apply",
1867
+ candidateRequired: true
1868
+ },
1869
+ recovery: ["先核实目标 SDK 的统一出口已公开,再迁移类型和 Schema 导入、删除工程直接 contracts 依赖,并重新安装、构建、校验和打包。", "由 Agent 或开发者依据当前 SDK 准备候选,再用 migrate project plan --candidate 只读审阅差异,确认 canApply 后在新目录 apply;不自动改写源码或依赖,adopt 只补锁。"]
1870
+ }] : [],
1755
1871
  dependenciesVerified: requireInstalled,
1756
1872
  lock
1757
1873
  };
@@ -1868,7 +1984,7 @@ async function assertBuildReceipt(projectRoot, toolchain) {
1868
1984
  };
1869
1985
  const receipt = await json(join(root, BUILD_RECEIPT_FILE), "BUILD_RECEIPT_MISMATCH", ["先执行 bohr-widget build,或去掉 pack 的 --no-build。"]);
1870
1986
  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 工具链或实际依赖与本次执行不一致");
1987
+ if (!record$1(receipt) || receipt.formatVersion !== 1 || JSON.stringify(receipt.toolchain) !== JSON.stringify(receiptToolchain(toolchain)) || JSON.stringify(receipt.dependencies) !== JSON.stringify(project.installed)) mismatch("receipt 工具链或实际依赖与本次执行不一致");
1872
1988
  let inputs;
1873
1989
  let outputs;
1874
1990
  try {
@@ -1881,9 +1997,411 @@ async function assertBuildReceipt(projectRoot, toolchain) {
1881
1997
  return receipt;
1882
1998
  }
1883
1999
  //#endregion
2000
+ //#region src/migration/source.mjs
2001
+ const MAX_FILES = 2e3;
2002
+ const MAX_FILE_BYTES = 8388608;
2003
+ const MAX_TOTAL_BYTES = 67108864;
2004
+ const DERIVED = /* @__PURE__ */ new Set([
2005
+ ".git",
2006
+ "node_modules",
2007
+ "dist",
2008
+ "build",
2009
+ "coverage",
2010
+ ".artifacts",
2011
+ ".widget-build-receipt.json",
2012
+ "widget-migration.json",
2013
+ ".npmrc"
2014
+ ]);
2015
+ function migrationError(code, message) {
2016
+ return Object.assign(new Error(message), {
2017
+ cliCode: `WIDGET_CLI_MIGRATION_${code}`,
2018
+ recovery: "保留源目录与候选,依据当前 SDK 文档准备并审阅候选;重新生成 plan 后使用新的 --out 目录。"
2019
+ });
2020
+ }
2021
+ function canonical(value) {
2022
+ if (Array.isArray(value)) return value.map(canonical);
2023
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
2024
+ return value;
2025
+ }
2026
+ function sha256(value) {
2027
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
2028
+ }
2029
+ function objectSha256(value) {
2030
+ return sha256(JSON.stringify(canonical(value)));
2031
+ }
2032
+ function inside(root, path) {
2033
+ const child = relative(root, path);
2034
+ return child === "" || !isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`);
2035
+ }
2036
+ /** 不跟随最终符号链接,读取前后核对同一普通文件。 */
2037
+ async function sourceFile(path, maxBytes = MAX_FILE_BYTES) {
2038
+ const before = await lstat(path);
2039
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) throw migrationError("SOURCE_UNSAFE", "来源必须是预算内的普通文件,不接受符号链接。");
2040
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2041
+ try {
2042
+ const info = await handle.stat();
2043
+ if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) throw migrationError("SOURCE_CHANGED", "读取期间来源文件身份发生变化。");
2044
+ const bytes = await handle.readFile();
2045
+ const after = await handle.stat();
2046
+ if (bytes.length > maxBytes || before.size !== after.size || before.mtimeMs !== after.mtimeMs) throw migrationError("SOURCE_CHANGED", "读取期间来源文件发生变化。");
2047
+ return bytes;
2048
+ } finally {
2049
+ await handle.close();
2050
+ }
2051
+ }
2052
+ /** 捕获作者文件和依赖锁;安装、构建、本地环境及旧 receipt 明确排除。 */
2053
+ async function sourceSnapshot(input) {
2054
+ const requested = resolve(typeof input === "string" ? input : fileURLToPath(input));
2055
+ const info = await lstat(requested);
2056
+ if (info.isSymbolicLink()) throw migrationError("SOURCE_UNSAFE", "来源不能是符号链接。");
2057
+ const root = await realpath(requested);
2058
+ if (!info.isDirectory()) throw migrationError("AUTHOR_SOURCE_REQUIRED", "迁移需要完整作者源码目录;ZIP 或编译产物不能还原源码,请先提供源码。");
2059
+ const files = /* @__PURE__ */ new Map();
2060
+ const modes = /* @__PURE__ */ new Map();
2061
+ const excluded = [];
2062
+ let total = 0;
2063
+ async function visit(directory, prefix = "") {
2064
+ for (const name of (await readdir(directory)).sort()) {
2065
+ const path = prefix ? `${prefix}/${name}` : name;
2066
+ if (DERIVED.has(name) && (!prefix || [
2067
+ ".git",
2068
+ "node_modules",
2069
+ ".npmrc"
2070
+ ].includes(name)) || /^\.env(?:\.|$)/u.test(name)) {
2071
+ excluded.push(path);
2072
+ continue;
2073
+ }
2074
+ if (normalizeArchivePath(path) !== path) throw migrationError("SOURCE_UNSAFE", "来源含非规范路径。");
2075
+ const full = join(root, path);
2076
+ const entry = await lstat(full);
2077
+ if (entry.isSymbolicLink()) throw migrationError("SOURCE_UNSAFE", `来源含符号链接:${path}`);
2078
+ if (entry.isDirectory()) await visit(full, path);
2079
+ else if (entry.isFile()) {
2080
+ if (files.size >= MAX_FILES || total + entry.size > MAX_TOTAL_BYTES) throw migrationError("SOURCE_LIMIT", "来源超出扫描预算,请人工拆分工程。");
2081
+ const bytes = await sourceFile(full);
2082
+ total += bytes.length;
2083
+ files.set(path, bytes);
2084
+ modes.set(path, entry.mode & 73);
2085
+ } else throw migrationError("SOURCE_UNSAFE", `来源含非普通文件:${path}`);
2086
+ }
2087
+ }
2088
+ await visit(root);
2089
+ return {
2090
+ root,
2091
+ kind: "directory",
2092
+ files,
2093
+ excluded,
2094
+ sha256: filesSha256(files, modes),
2095
+ modes
2096
+ };
2097
+ }
2098
+ function filesSha256(files, modes = /* @__PURE__ */ new Map()) {
2099
+ return objectSha256([...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([path, bytes]) => [
2100
+ path,
2101
+ sha256(bytes),
2102
+ modes.get(path) ?? 0
2103
+ ]));
2104
+ }
2105
+ function jsonFile(files, path) {
2106
+ if (!files.has(path)) return null;
2107
+ try {
2108
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(files.get(path)));
2109
+ } catch {
2110
+ throw migrationError("SOURCE_JSON", `来源不是有效 UTF-8 JSON:${path}`);
2111
+ }
2112
+ }
2113
+ function jsonBytes(value) {
2114
+ return Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
2115
+ }
2116
+ async function sourceRulesSha256() {
2117
+ return sha256(await sourceFile(fileURLToPath(import.meta.url), MAX_TOTAL_BYTES));
2118
+ }
2119
+ //#endregion
2120
+ //#region src/migration/project.mjs
2121
+ const RECEIPT = "widget-migration.json";
2122
+ const HASH_SCOPE = "作者文件与依赖锁的字节、执行权限;排除安装、根构建、本地环境、Git 和旧 receipt";
2123
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value);
2124
+ function authorSource(snapshot) {
2125
+ const manifest = jsonFile(snapshot.files, "manifest.json");
2126
+ const pkg = jsonFile(snapshot.files, "package.json");
2127
+ const hasSource = [...snapshot.files.keys()].some((path) => /\.[cm]?[jt]sx?$/u.test(path) && (path.startsWith("src/") || path === manifest?.entry));
2128
+ if (!record(manifest) || !record(pkg) || !hasSource) throw migrationError("AUTHOR_SOURCE_REQUIRED", "需要包含 package.json、manifest.json 和作者源码的完整目录;编译产物不能代替源码。");
2129
+ return manifest;
2130
+ }
2131
+ function snapshotDescription(snapshot, kind) {
2132
+ return {
2133
+ kind,
2134
+ path: snapshot.root,
2135
+ sha256: snapshot.sha256,
2136
+ hashScope: HASH_SCOPE,
2137
+ excluded: snapshot.excluded
2138
+ };
2139
+ }
2140
+ function fileState(snapshot, path) {
2141
+ const bytes = snapshot.files.get(path);
2142
+ return bytes ? {
2143
+ sha256: sha256(bytes),
2144
+ executable: snapshot.modes.get(path) ?? 0
2145
+ } : null;
2146
+ }
2147
+ function changesBetween(source, candidate) {
2148
+ return [.../* @__PURE__ */ new Set([...source.files.keys(), ...candidate.files.keys()])].sort().flatMap((path) => {
2149
+ const before = fileState(source, path);
2150
+ const after = fileState(candidate, path);
2151
+ if (JSON.stringify(before) === JSON.stringify(after)) return [];
2152
+ return [{
2153
+ path,
2154
+ action: before ? after ? "update" : "delete" : "create",
2155
+ before,
2156
+ after
2157
+ }];
2158
+ });
2159
+ }
2160
+ function pendingValidation(reason) {
2161
+ return {
2162
+ status: "not-run",
2163
+ ok: false,
2164
+ scope: "structure-and-declarations",
2165
+ installation: "not-run",
2166
+ build: "not-run",
2167
+ host: "not-run",
2168
+ checks: [],
2169
+ errors: [],
2170
+ reason
2171
+ };
2172
+ }
2173
+ /** 只检查已准备候选的当前声明和内容结构,不执行候选代码或安装脚本。 */
2174
+ async function candidateValidation(snapshot, toolchain) {
2175
+ const validation = {
2176
+ ...pendingValidation("候选尚未校验"),
2177
+ status: "completed"
2178
+ };
2179
+ delete validation.reason;
2180
+ let manifest;
2181
+ try {
2182
+ manifest = authorSource(snapshot);
2183
+ const lint = lintAuthorManifest(manifest);
2184
+ if (!manifest.contributes || !lint.ok) {
2185
+ validation.errors.push(...lint.errors.length ? lint.errors : ["候选必须使用当前 contributes 作者清单。"]);
2186
+ return validation;
2187
+ }
2188
+ if (typeof manifest.entry !== "string" || normalizeArchivePath(manifest.entry) !== manifest.entry || !snapshot.files.has(manifest.entry)) {
2189
+ validation.errors.push("候选 entry 必须指向安全快照中的作者源码。");
2190
+ return validation;
2191
+ }
2192
+ const compiled = compileAuthorManifest(manifest, { sdkVersion: toolchain.profile.sdk });
2193
+ for (const sample of compiled.widgetConfig?.samples ?? []) if (typeof sample.request !== "string" || normalizeArchivePath(sample.request) !== sample.request || !snapshot.files.has(sample.request)) {
2194
+ validation.errors.push("候选 sample 不在安全源码快照中。");
2195
+ return validation;
2196
+ }
2197
+ let component = snapshot.root;
2198
+ const hostEntry = compiled.widgetConfig?.entry;
2199
+ if (typeof hostEntry !== "string" || normalizeArchivePath(hostEntry) !== hostEntry) {
2200
+ validation.errors.push("候选的编译入口不是规范相对路径。");
2201
+ return validation;
2202
+ }
2203
+ for (const segment of hostEntry.split("/")) {
2204
+ component = join(component, segment);
2205
+ const entry = await lstat(component).catch((error) => {
2206
+ if (error.code === "ENOENT") return null;
2207
+ throw error;
2208
+ });
2209
+ if (!entry) break;
2210
+ if (entry.isSymbolicLink()) throw migrationError("SOURCE_UNSAFE", "校验入口不能穿过符号链接。");
2211
+ }
2212
+ const project = await validateProjectToolchain(snapshot.root, toolchain, { requireInstalled: false });
2213
+ const pkg = jsonFile(snapshot.files, "package.json");
2214
+ const cliVersions = [
2215
+ "dependencies",
2216
+ "devDependencies",
2217
+ "optionalDependencies",
2218
+ "peerDependencies"
2219
+ ].flatMap((section) => pkg[section]?.["@dp-bohrium/widget-cli"] === void 0 ? [] : [pkg[section]["@dp-bohrium/widget-cli"]]);
2220
+ if (cliVersions.length > 1 || cliVersions.some((version) => version !== toolchain.cli)) {
2221
+ validation.errors.push("候选声明的 Widget CLI 必须是本次执行器的单一精确版本。");
2222
+ return validation;
2223
+ }
2224
+ validation.checks.push({
2225
+ name: "候选精确工具链锁与依赖声明",
2226
+ ok: true
2227
+ });
2228
+ validation.toolchain = {
2229
+ profile: project.profile,
2230
+ declared: project.declared,
2231
+ dependenciesVerified: false
2232
+ };
2233
+ const checked = await checkWidgetProject(snapshot.root, {
2234
+ requireBuiltEntry: false,
2235
+ sdkVersion: toolchain.profile.sdk
2236
+ });
2237
+ validation.checks.push(...checked.checks.map(({ name, ok }) => ({
2238
+ name,
2239
+ ok
2240
+ })));
2241
+ validation.errors.push(...checked.errors);
2242
+ validation.ok = checked.ok;
2243
+ return validation;
2244
+ } catch (error) {
2245
+ if (error.cliCode === "WIDGET_CLI_MIGRATION_SOURCE_UNSAFE") throw error;
2246
+ validation.errors.push(error.message);
2247
+ return validation;
2248
+ }
2249
+ }
2250
+ async function prepare(source, candidatePath, toolchain) {
2251
+ const snapshot = await sourceSnapshot(source);
2252
+ authorSource(snapshot);
2253
+ const report = {
2254
+ ok: true,
2255
+ command: "migrate project plan",
2256
+ formatVersion: 2,
2257
+ mode: "agent-guided",
2258
+ canApply: false,
2259
+ source: snapshotDescription(snapshot, "author-source"),
2260
+ candidate: null,
2261
+ target: {
2262
+ cli: toolchain.cli,
2263
+ profile: toolchain.profile,
2264
+ execution: toolchain.digests,
2265
+ migration: objectSha256([await sourceRulesSha256(), sha256(await sourceFile(fileURLToPath(import.meta.url), 67108864))])
2266
+ },
2267
+ changes: [],
2268
+ blockers: [],
2269
+ manualSteps: [
2270
+ "依据当前 SDK 模板、示例和文档,在来源目录树之外独立准备并审阅候选;源码、依赖和锁由 Agent 或开发者明确修改,不自动转换旧格式。",
2271
+ "使用 plan --candidate <候选目录> 保存并审阅来源/候选摘要和全部文件变更,再 apply 到与两者分离的新目录。",
2272
+ "apply 不安装或运行来源与候选脚本;随后审查安装脚本、安装依赖、build、check、pack,并在真实 Host 验收。"
2273
+ ],
2274
+ validation: pendingValidation("尚未提供已准备的当前工程候选。")
2275
+ };
2276
+ let candidate = null;
2277
+ if (!candidatePath) report.blockers.push({
2278
+ code: "WIDGET_CLI_MIGRATION_CANDIDATE_REQUIRED",
2279
+ message: "没有自动源码迁移规则;请由 Agent 或开发者准备候选后用 --candidate 重新 plan。"
2280
+ });
2281
+ else {
2282
+ candidate = await sourceSnapshot(candidatePath);
2283
+ if (inside(snapshot.root, candidate.root) || inside(candidate.root, snapshot.root)) throw migrationError("CANDIDATE_UNSAFE", "候选必须独立于来源目录树,不能使用来源、子目录或祖先。");
2284
+ report.candidate = snapshotDescription(candidate, "prepared-current-project");
2285
+ report.changes = changesBetween(snapshot, candidate);
2286
+ report.validation = await candidateValidation(candidate, toolchain);
2287
+ if (!report.validation.ok) report.blockers.push({
2288
+ code: "WIDGET_CLI_MIGRATION_CANDIDATE_INVALID",
2289
+ message: "候选未通过当前工程锁、依赖声明或 Manifest/sample 结构校验;请修正候选后重新 plan。"
2290
+ });
2291
+ if ((await sourceSnapshot(candidate.root)).sha256 !== candidate.sha256) throw migrationError("CANDIDATE_CHANGED", "校验期间候选发生变化,请重新 plan。");
2292
+ }
2293
+ if ((await sourceSnapshot(snapshot.root)).sha256 !== snapshot.sha256) throw migrationError("SOURCE_CHANGED", "规划期间来源发生变化,请重新 plan。");
2294
+ report.canApply = report.blockers.length === 0;
2295
+ report.planSha256 = objectSha256(report);
2296
+ return {
2297
+ report,
2298
+ snapshot,
2299
+ candidate
2300
+ };
2301
+ }
2302
+ async function planMigration(source, toolchain, candidate) {
2303
+ return (await prepare(source, candidate, toolchain)).report;
2304
+ }
2305
+ function receiptFor(plan) {
2306
+ return {
2307
+ formatVersion: 2,
2308
+ mode: "agent-guided",
2309
+ planSha256: plan.planSha256,
2310
+ source: plan.source,
2311
+ candidate: plan.candidate,
2312
+ target: plan.target,
2313
+ outputSha256: plan.candidate.sha256
2314
+ };
2315
+ }
2316
+ async function existingResult(out, plan) {
2317
+ const info = await lstat(out);
2318
+ if (!info.isDirectory() || info.isSymbolicLink()) throw migrationError("OUT_EXISTS", "目标已存在且不是可核验的同计划迁移目录。");
2319
+ const current = await sourceSnapshot(out);
2320
+ let receipt;
2321
+ try {
2322
+ receipt = JSON.parse((await sourceFile(join(out, RECEIPT))).toString("utf8"));
2323
+ } catch {
2324
+ throw migrationError("OUT_EXISTS", "目标已有内容但缺少有效迁移记录,拒绝覆盖。");
2325
+ }
2326
+ if (JSON.stringify(canonical(receipt)) !== JSON.stringify(canonical(receiptFor(plan))) || current.sha256 !== plan.candidate.sha256) throw migrationError("OUT_EXISTS", "目标已存在或迁移结果已改变,拒绝覆盖;请使用新的 --out。");
2327
+ return true;
2328
+ }
2329
+ /** 候选路径只来自已审阅计划;全部写入私有阶段目录后排他发布,不执行代码。 */
2330
+ async function applyMigration(source, planPath, output, toolchain, hostCompat) {
2331
+ if (!planPath || !output) throw migrationError("ARGUMENT", "apply 需要 --plan <plan.json> 和 --out <新目录>。");
2332
+ const plan = JSON.parse((await sourceFile(resolve(planPath))).toString("utf8"));
2333
+ if (plan.formatVersion !== 2 || plan.mode !== "agent-guided") throw migrationError("PLAN_MISMATCH", "需要当前 agent-guided 格式 v2 的完整计划,请重新 plan。");
2334
+ const prepared = await prepare(source, plan.candidate?.path, toolchain);
2335
+ if (JSON.stringify(canonical(plan)) !== JSON.stringify(canonical(prepared.report))) throw migrationError("PLAN_MISMATCH", "计划、来源、候选或目标执行器摘要已变化;请重新 plan 并审阅,不能修改计划绕过检查。");
2336
+ if (!plan.canApply) throw migrationError("BLOCKED", "计划含 blockers,不能 apply;请先独立准备并校验候选。");
2337
+ const hostCompatibility = await assertHostCompatibility(hostCompat, toolchain);
2338
+ const requested = resolve(output);
2339
+ const parent = await realpath(dirname(requested));
2340
+ const out = join(parent, basename(requested));
2341
+ for (const input of [prepared.snapshot.root, prepared.candidate.root]) if (inside(input, out) || inside(out, input)) throw migrationError("OUT_UNSAFE", "目标必须位于来源和候选目录树之外,不能覆盖任一输入、子目录或祖先。");
2342
+ const stage = await mkdtemp(join(parent, ".widget-migration-"));
2343
+ let reservation;
2344
+ try {
2345
+ for (const [path, bytes] of prepared.candidate.files) {
2346
+ await mkdir(dirname(join(stage, path)), { recursive: true });
2347
+ await writeFile(join(stage, path), bytes, { flag: "wx" });
2348
+ await chmod(join(stage, path), 420 | (prepared.candidate.modes.get(path) ?? 0));
2349
+ }
2350
+ const checked = await candidateValidation({
2351
+ ...prepared.candidate,
2352
+ root: stage
2353
+ }, toolchain);
2354
+ if (!checked.ok) throw migrationError("TARGET_INVALID", `复制后的候选未通过结构校验:${checked.errors.join(";")}`);
2355
+ if ((await sourceSnapshot(stage)).sha256 !== prepared.candidate.sha256) throw migrationError("TARGET_CHANGED", "复制结果与已审阅候选不同,未发布结果。");
2356
+ await writeFile(join(stage, RECEIPT), jsonBytes(receiptFor(plan)), { flag: "wx" });
2357
+ if ((await sourceSnapshot(prepared.snapshot.root)).sha256 !== plan.source.sha256) throw migrationError("SOURCE_CHANGED", "生成期间来源发生变化;未发布结果,请重新 plan。");
2358
+ if ((await sourceSnapshot(prepared.candidate.root)).sha256 !== plan.candidate.sha256) throw migrationError("CANDIDATE_CHANGED", "生成期间候选发生变化;未发布结果,请重新 plan。");
2359
+ let reused = false;
2360
+ try {
2361
+ await mkdir(out);
2362
+ reservation = await lstat(out);
2363
+ } catch (error) {
2364
+ if (error.code !== "EEXIST") throw error;
2365
+ reused = await existingResult(out, plan);
2366
+ }
2367
+ if (!reused) {
2368
+ const current = await lstat(out);
2369
+ if (current.ino !== reservation.ino || current.dev !== reservation.dev || current.isSymbolicLink()) throw migrationError("OUT_CHANGED", "目标占位目录已被替换,拒绝写入。");
2370
+ await rename(stage, out);
2371
+ reservation = null;
2372
+ }
2373
+ return {
2374
+ ok: true,
2375
+ command: "migrate project apply",
2376
+ formatVersion: 2,
2377
+ mode: "agent-guided",
2378
+ projectRoot: out,
2379
+ planSha256: plan.planSha256,
2380
+ source: plan.source,
2381
+ candidate: plan.candidate,
2382
+ target: plan.target,
2383
+ outputSha256: plan.candidate.sha256,
2384
+ reused,
2385
+ hostCompatibility,
2386
+ validation: checked,
2387
+ nextSteps: plan.manualSteps
2388
+ };
2389
+ } finally {
2390
+ await rm(stage, {
2391
+ recursive: true,
2392
+ force: true
2393
+ });
2394
+ if (reservation) {
2395
+ const current = await lstat(out).catch(() => null);
2396
+ if (current?.ino === reservation.ino && current?.dev === reservation.dev) await rmdir(out).catch(() => {});
2397
+ }
2398
+ }
2399
+ }
2400
+ //#endregion
1884
2401
  //#region src/templates.mjs
1885
2402
  const GENERATED_PROJECT_NODE = "^22.18.0 || >=24.11.0";
1886
- function projectPackage({ name, ui, cliVersion, sdkVersion, contractsVersion }) {
2403
+ const PROJECT_EXECUTOR_PACKAGE = "@dp-bohrium/widget-sdk";
2404
+ function projectPackage({ name, ui, sdkVersion }) {
1887
2405
  return {
1888
2406
  name: `@dp-bohrium/widget-example-${name}`,
1889
2407
  private: true,
@@ -1902,8 +2420,6 @@ function projectPackage({ name, ui, cliVersion, sdkVersion, contractsVersion })
1902
2420
  } : {}
1903
2421
  },
1904
2422
  devDependencies: {
1905
- "@dp-bohrium/widget-cli": cliVersion,
1906
- "@dp-bohrium/widget-contracts": contractsVersion,
1907
2423
  tsdown: "0.22.14",
1908
2424
  typescript: "5.9.3",
1909
2425
  ...ui === "react" ? {
@@ -1918,8 +2434,8 @@ function projectReadme({ name, template, ui, sourceFile, hostFile }) {
1918
2434
 
1919
2435
  Bohrium Widget ${template} 插件工程(${ui})。
1920
2436
 
1921
- 默认构建器要求 Node.js \`${GENERATED_PROJECT_NODE}\`,高于 CLI 自身的 Node.js 20 要求。
1922
- 工程已经精确依赖创建它的 CLI 版本;在本目录安装后使用本地命令,无需全局安装或源码仓库。
2437
+ 默认构建器要求 Node.js \`${GENERATED_PROJECT_NODE}\`,高于执行器自身的 Node.js 20 要求。
2438
+ 工程已经精确依赖提供命令的 \`${PROJECT_EXECUTOR_PACKAGE}\`;在本目录安装后使用本地命令,无需全局安装或源码仓库。
1923
2439
 
1924
2440
  \`\`\`bash
1925
2441
  pnpm install
@@ -1965,8 +2481,7 @@ function reactSource({ reportSize = false } = {}) {
1965
2481
  resized = true;
1966
2482
  }` : "";
1967
2483
  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";
2484
+ import type { RenderRequest, WidgetContext } from "@dp-bohrium/widget-sdk";
1970
2485
 
1971
2486
  function WidgetView({ request }: { request?: RenderRequest }) {
1972
2487
  const data = request?.content ?? { state: "等待 Host 提供数据" };
@@ -2018,13 +2533,13 @@ runWidgetHost(widget, {
2018
2533
  }
2019
2534
  const TEMPLATES = Object.freeze({
2020
2535
  panel: {
2021
- manifest: ({ name, panelId = name, ui = "vanilla" }) => ({
2022
- $schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
2536
+ manifest: ({ name, panelId = name, ui = "vanilla", tool }) => ({
2537
+ $schema: "@dp-bohrium/widget-sdk/schemas/widget-plugin-manifest.schema.json",
2023
2538
  name,
2024
2539
  version: "0.1.0",
2025
2540
  displayName: name,
2026
2541
  entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
2027
- activationEvents: ["session.start"],
2542
+ activationEvents: [tool ? `tool-call-result:${tool}` : "session.start"],
2028
2543
  contributes: { widget: {
2029
2544
  type: "panel",
2030
2545
  id: panelId,
@@ -2130,16 +2645,16 @@ export { activate };
2130
2645
  })
2131
2646
  },
2132
2647
  "file-preview": {
2133
- manifest: ({ name, ui = "vanilla" }) => ({
2134
- $schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
2648
+ manifest: ({ name, ui = "vanilla", extensions = ["txt"], tool }) => ({
2649
+ $schema: "@dp-bohrium/widget-sdk/schemas/widget-plugin-manifest.schema.json",
2135
2650
  name,
2136
2651
  version: "0.1.0",
2137
2652
  displayName: name,
2138
2653
  entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
2139
- activationEvents: ["workspace-file-opened"],
2654
+ activationEvents: [tool ? `tool-call-result:${tool}` : "workspace-file-opened"],
2140
2655
  contributes: { widget: {
2141
2656
  type: "file-preview",
2142
- extensions: ["txt"]
2657
+ extensions
2143
2658
  } },
2144
2659
  samples: [{
2145
2660
  id: "default",
@@ -2177,7 +2692,7 @@ export { activate };
2177
2692
  reportSize: true
2178
2693
  }),
2179
2694
  hostSource,
2180
- sample: () => ({
2695
+ sample: ({ extensions = ["txt"] } = {}) => ({
2181
2696
  schemaVersion: 1,
2182
2697
  requestId: "sample-file-preview",
2183
2698
  instanceId: "sample-file-preview",
@@ -2188,8 +2703,8 @@ export { activate };
2188
2703
  contribution: "file-preview",
2189
2704
  file: {
2190
2705
  id: "sample",
2191
- name: "README.txt",
2192
- extension: "txt",
2706
+ name: `README.${extensions[0]}`,
2707
+ extension: extensions[0],
2193
2708
  kind: "file"
2194
2709
  },
2195
2710
  preview: { format: "text" }
@@ -2226,7 +2741,7 @@ function getAssetRoot() {
2226
2741
  }
2227
2742
  function getToolchain() {
2228
2743
  const path = join(getAssetRoot(), "toolchain.json");
2229
- if (!existsSync(path)) throw new Error("缺少 Widget CLI 工具链资产;源码运行前请执行 pnpm --filter @dp-bohrium/widget-cli build:assets,发布包请重新安装。");
2744
+ if (!existsSync(path)) throw new Error("缺少 Widget 工具链资产;源码运行前请构建当前分发包的 CLI assets,发布包请重新安装。");
2230
2745
  return JSON.parse(readFileSync(path, "utf8"));
2231
2746
  }
2232
2747
  function getDocumentation() {
@@ -2249,6 +2764,7 @@ function usage(path = []) {
2249
2764
  cli: packageJson.version,
2250
2765
  sdk: sdkPackage.version,
2251
2766
  contracts: contractsPackage.version,
2767
+ packageName: toolchain.packageName,
2252
2768
  documentation: getDocumentation(),
2253
2769
  generatedProjectNode: GENERATED_PROJECT_NODE
2254
2770
  });
@@ -2287,8 +2803,12 @@ function parseOptions(argv) {
2287
2803
  scheme: "widget-host",
2288
2804
  bridge: null,
2289
2805
  panelId: void 0,
2806
+ extensions: void 0,
2807
+ tool: void 0,
2290
2808
  hostCompat: void 0,
2291
- project: void 0
2809
+ project: void 0,
2810
+ plan: void 0,
2811
+ candidate: void 0
2292
2812
  };
2293
2813
  for (let index = 0; index < argv.length; index += 1) {
2294
2814
  const argument = argv[index];
@@ -2303,14 +2823,21 @@ function parseOptions(argv) {
2303
2823
  } else if (argument === "--panel-id") {
2304
2824
  options.panelId = argv[++index];
2305
2825
  if (!options.panelId) throw new Error("--panel-id 需要稳定的 Panel id");
2826
+ } else if (argument === "--extensions" || argument === "--tool") {
2827
+ const value = argv[++index];
2828
+ if (!value || value.startsWith("--")) throw new Error(`${argument} 需要非空值`);
2829
+ if (argument === "--extensions") options.extensions = value.split(",").map((item) => item.trim());
2830
+ else options.tool = value;
2306
2831
  } else if (argument === "--out") {
2307
2832
  options.output = argv[++index];
2308
2833
  if (!options.output) throw new Error("--out 需要输出路径");
2309
2834
  } else if (argument === "--open") options.open = true;
2310
- else if (argument === "--host-compat" || argument === "--project") {
2835
+ else if (argument === "--host-compat" || argument === "--project" || argument === "--plan" || argument === "--candidate") {
2311
2836
  const value = argv[++index];
2312
2837
  if (!value || value.startsWith("--")) throw new Error(`${argument} 需要路径`);
2313
2838
  if (argument === "--host-compat") options.hostCompat = value;
2839
+ else if (argument === "--plan") options.plan = value;
2840
+ else if (argument === "--candidate") options.candidate = value;
2314
2841
  else options.project = value;
2315
2842
  } else if (argument === "--watch") options.watch = true;
2316
2843
  else if (argument === "--no-build") options.build = false;
@@ -2328,11 +2855,15 @@ function parseOptions(argv) {
2328
2855
  function printResult(result, json) {
2329
2856
  if (json) console.log(JSON.stringify(result, null, 2));
2330
2857
  else {
2858
+ printDeprecations(result.deprecations);
2331
2859
  for (const check of result.checks ?? []) console.log(`${check.ok ? "✓" : "✗"} ${check.name}`);
2332
2860
  for (const error of result.errors ?? []) console.error(`错误:${error}`);
2333
2861
  console.log(result.ok ? "校验通过" : "校验失败");
2334
2862
  }
2335
2863
  }
2864
+ function printDeprecations(deprecations = []) {
2865
+ for (const diagnostic of deprecations) console.warn(`废弃 [${diagnostic.code}]:${diagnostic.message}`);
2866
+ }
2336
2867
  function structuredError({ code, message, context = {}, recovery }) {
2337
2868
  return {
2338
2869
  code,
@@ -2373,7 +2904,8 @@ async function projectPreflight(projectRoot, options = {}) {
2373
2904
  return {
2374
2905
  profile: toolchain.profile,
2375
2906
  hostCompatibility,
2376
- project
2907
+ project,
2908
+ deprecations: project.deprecations
2377
2909
  };
2378
2910
  }
2379
2911
  async function commandIntegration(options) {
@@ -2396,10 +2928,14 @@ async function commandIntegration(options) {
2396
2928
  assetRoot: getAssetRoot(),
2397
2929
  documentation: getDocumentation(),
2398
2930
  hostCompatibility,
2931
+ deprecations: project?.deprecations ?? [],
2399
2932
  ...project ? { project } : {}
2400
2933
  };
2401
2934
  if (options.json) console.log(JSON.stringify(result, null, 2));
2402
- else console.log(`${toolchain.profile.id}\n${result.documentation.url}`);
2935
+ else {
2936
+ printDeprecations(result.deprecations);
2937
+ console.log(`${toolchain.profile.id}\n${result.documentation.url}`);
2938
+ }
2403
2939
  return result;
2404
2940
  }
2405
2941
  async function commandAdopt(directory, options) {
@@ -2413,10 +2949,14 @@ async function commandAdopt(directory, options) {
2413
2949
  projectRoot,
2414
2950
  profile: toolchain.profile,
2415
2951
  project,
2952
+ deprecations: project.deprecations,
2416
2953
  hostCompatibility
2417
2954
  };
2418
2955
  if (options.json) console.log(JSON.stringify(result, null, 2));
2419
- else console.log(`已锁定工程工具链:${toolchain.profile.id}`);
2956
+ else {
2957
+ printDeprecations(result.deprecations);
2958
+ console.log(`已锁定工程工具链:${toolchain.profile.id}`);
2959
+ }
2420
2960
  return result;
2421
2961
  }
2422
2962
  /**
@@ -2435,6 +2975,9 @@ async function commandCreate(directory, options) {
2435
2975
  if (!template) throw new Error(`未知模板:${options.template}`);
2436
2976
  if (!["vanilla", "react"].includes(options.ui)) throw new Error(`未知 UI 模式:${options.ui},可选 vanilla 或 react`);
2437
2977
  if (options.panelId && options.template !== "panel") throw new Error("--panel-id 只适用于 --template panel");
2978
+ if (options.extensions && options.template !== "file-preview") throw new Error("--extensions 只适用于 --template file-preview");
2979
+ 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 个不重复的小写扩展名,用逗号分隔且不带前导点");
2980
+ if (options.tool && (!/^[A-Za-z0-9_.:-]+$/u.test(options.tool) || `tool-call-result:${options.tool}`.length > 128)) throw new Error("--tool 需要有效工具名,只能包含字母、数字、下划线、点、冒号和连字符,激活信号最长 128 位");
2438
2981
  if (options.panelId && !/^[A-Za-z0-9_.:-]{1,128}$/u.test(options.panelId)) throw new Error("--panel-id 必须为 1-128 位,只能包含字母、数字、下划线、点、冒号和连字符");
2439
2982
  const root = resolve(directory);
2440
2983
  if (existsSync(root)) throw new Error(`目标目录已存在:${root}`);
@@ -2445,7 +2988,9 @@ async function commandCreate(directory, options) {
2445
2988
  name,
2446
2989
  panelId,
2447
2990
  sdkVersion: sdkPackage.version,
2448
- ui: options.ui
2991
+ ui: options.ui,
2992
+ extensions: options.extensions,
2993
+ tool: options.tool
2449
2994
  };
2450
2995
  const manifest = template.manifest(templateContext);
2451
2996
  const sample = template.sample(templateContext);
@@ -2489,9 +3034,7 @@ async function commandCreate(directory, options) {
2489
3034
  await writeFile(join(root, "package.json"), `${JSON.stringify(projectPackage({
2490
3035
  name,
2491
3036
  ui: options.ui,
2492
- cliVersion: packageJson.version,
2493
- sdkVersion: sdkPackage.version,
2494
- contractsVersion: contractsPackage.version
3037
+ sdkVersion: sdkPackage.version
2495
3038
  }), null, 2)}\n`);
2496
3039
  const warnings = [];
2497
3040
  await writeProjectToolchain(root, toolchain);
@@ -2708,6 +3251,7 @@ async function commandCheck(input, options, commandName = "check") {
2708
3251
  ok: result.ok,
2709
3252
  checks: result.checks,
2710
3253
  errors: result.errors,
3254
+ ...result.deprecations?.length ? { deprecations: result.deprecations } : {},
2711
3255
  ...result.warnings?.length ? { warnings: result.warnings } : {}
2712
3256
  })),
2713
3257
  ...passed ? {} : { error: structuredError({
@@ -2812,8 +3356,9 @@ async function runProjectBuild(root, { capture = false } = {}) {
2812
3356
  async function commandBuild(directory, options = {}) {
2813
3357
  if (!directory) throw new Error("build 需要工程目录");
2814
3358
  const root = resolve(directory);
3359
+ let compatibility;
2815
3360
  try {
2816
- const compatibility = await projectPreflight(root, options);
3361
+ compatibility = await projectPreflight(root, options);
2817
3362
  await rm(join(root, BUILD_RECEIPT_FILE), { force: true });
2818
3363
  const inputsBefore = await captureBuildInputs(root);
2819
3364
  const build = await runProjectBuild(root, { capture: true });
@@ -2831,6 +3376,7 @@ async function commandBuild(directory, options = {}) {
2831
3376
  };
2832
3377
  if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
2833
3378
  else if (options.emit !== false) {
3379
+ printDeprecations(result.deprecations);
2834
3380
  if (build.stdout) process.stdout.write(build.stdout);
2835
3381
  if (build.stderr) process.stderr.write(build.stderr);
2836
3382
  }
@@ -2840,6 +3386,7 @@ async function commandBuild(directory, options = {}) {
2840
3386
  ok: false,
2841
3387
  command: "build",
2842
3388
  projectRoot: root,
3389
+ ...compatibility ? { deprecations: compatibility.deprecations } : {},
2843
3390
  error: commandError("build", error, {
2844
3391
  projectRoot: root,
2845
3392
  phase: "build"
@@ -2849,6 +3396,7 @@ async function commandBuild(directory, options = {}) {
2849
3396
  };
2850
3397
  if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
2851
3398
  if (!options.json && options.emit !== false) {
3399
+ printDeprecations(result.deprecations);
2852
3400
  if (result.stdout) process.stdout.write(result.stdout);
2853
3401
  if (result.stderr) process.stderr.write(result.stderr);
2854
3402
  }
@@ -2884,6 +3432,7 @@ async function commandPack(directory, options) {
2884
3432
  if (build.stderr) process.stderr.write(build.stderr);
2885
3433
  }
2886
3434
  if (!build.ok) {
3435
+ if (!options.json) printDeprecations(build.deprecations);
2887
3436
  if (options.json) console.log(JSON.stringify({
2888
3437
  ...build,
2889
3438
  command: "pack",
@@ -2897,12 +3446,16 @@ async function commandPack(directory, options) {
2897
3446
  requireBuiltEntry: true,
2898
3447
  toolchain: { sdkVersion: toolchain.profile.sdk }
2899
3448
  }));
2900
- if (!options.json) printResult(result, false);
3449
+ if (!options.json) printResult({
3450
+ ...result,
3451
+ deprecations: compatibility.deprecations
3452
+ }, false);
2901
3453
  if (!result.ok) {
2902
3454
  if (options.json) console.log(JSON.stringify({
2903
3455
  ok: false,
2904
3456
  command: "pack",
2905
3457
  projectRoot: result.projectRoot,
3458
+ deprecations: compatibility.deprecations,
2906
3459
  error: structuredError({
2907
3460
  code: "WIDGET_CLI_PACK_VALIDATION_FAILED",
2908
3461
  message: "Widget 工程校验失败,未生成可交付 Bundle。",
@@ -2970,7 +3523,20 @@ async function commandPack(directory, options) {
2970
3523
  return artifact;
2971
3524
  }
2972
3525
  async function commandMigrate(positional, options) {
2973
- if (positional[0] !== "dsh") throw new Error("migrate 目前只支持 dsh");
3526
+ if (positional[0] === "project") {
3527
+ const [, action, source, ...extra] = positional;
3528
+ if (!source || extra.length || !["plan", "apply"].includes(action)) throw new Error("用法:migrate project plan <source> [--candidate <候选目录>];apply 另需 --plan 与 --out。");
3529
+ if (action === "plan" && (options.plan || options.output || options.hostCompat)) throw new Error("plan 完全只读,不接受 --plan、--out 或 --host-compat;宿主声明在 apply 时核对。");
3530
+ if (action === "apply" && options.candidate) throw new Error("apply 的候选只从已审阅计划读取,不接受 --candidate 覆盖");
3531
+ const result = action === "plan" ? await planMigration(source, toolchain, options.candidate) : await applyMigration(source, options.plan, options.output, toolchain, options.hostCompat);
3532
+ if (options.json) console.log(JSON.stringify(result, null, 2));
3533
+ else if (action === "plan") {
3534
+ console.log(result.canApply ? "计划可应用;审阅 JSON 后在新目录 apply。" : "迁移已阻断,请处理 blockers 并准备当前工程候选。");
3535
+ for (const blocker of result.blockers) console.log(`[${blocker.code}] ${blocker.message}`);
3536
+ } else console.log(`已${result.reused ? "核验已有" : "创建"}迁移工程:${result.projectRoot}`);
3537
+ return result;
3538
+ }
3539
+ if (positional[0] !== "dsh") throw new Error("migrate 支持 project 或 dsh;Bundle/ZIP 请提供完整作者源码目录");
2974
3540
  const action = positional[1];
2975
3541
  const input = positional[2];
2976
3542
  if (!input) throw new Error(`migrate dsh ${action ?? ""} 需要源插件目录`);
@@ -3016,7 +3582,8 @@ async function commandDoctor(directory, options) {
3016
3582
  nodeSupported: Number(process.versions.node.split(".")[0]) >= 20,
3017
3583
  dependenciesInstalled: existsSync(join(root, "node_modules")),
3018
3584
  manifestValid: false,
3019
- warnings: []
3585
+ warnings: [],
3586
+ deprecations: []
3020
3587
  };
3021
3588
  if (report.manifest) try {
3022
3589
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -3043,6 +3610,8 @@ async function commandDoctor(directory, options) {
3043
3610
  requireInstalled: report.dependenciesInstalled,
3044
3611
  requireLock: false
3045
3612
  });
3613
+ report.deprecations = report.projectToolchain.deprecations;
3614
+ report.warnings.push(...report.deprecations.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`));
3046
3615
  const lint = lintProjectManifest(root);
3047
3616
  const candidate = await checkWidgetProject(root, {
3048
3617
  requireBuiltEntry: false,
@@ -3095,15 +3664,35 @@ async function main(argv) {
3095
3664
  if (command === "dev") {
3096
3665
  const devArgs = [];
3097
3666
  let hostCompat;
3667
+ let deprecationsReported = false;
3668
+ let deprecations = [];
3098
3669
  for (let index = 1; index < argv.length; index += 1) if (argv[index] === "--host-compat") {
3099
3670
  hostCompat = argv[++index];
3100
3671
  if (!hostCompat || hostCompat.startsWith("--")) throw new Error("--host-compat 需要声明文件路径");
3101
3672
  } else devArgs.push(argv[index]);
3673
+ const devJson = devArgs.includes("--json");
3102
3674
  const exitCode = await runWidgetDevelopmentCli(devArgs, {
3103
- beforeBuild: ({ projectRoot }) => projectPreflight(projectRoot, { hostCompat }),
3675
+ ...devJson ? { emit: (line, error = false) => {
3676
+ const event = JSON.parse(line);
3677
+ event.data = {
3678
+ ...event.data,
3679
+ deprecations
3680
+ };
3681
+ if (error) console.error(JSON.stringify(event));
3682
+ else console.log(JSON.stringify(event));
3683
+ } } : {},
3684
+ beforeBuild: async ({ projectRoot }) => {
3685
+ const result = await projectPreflight(projectRoot, { hostCompat });
3686
+ deprecations = result.deprecations;
3687
+ if (!devJson && !deprecationsReported && deprecations.length) {
3688
+ printDeprecations(result.deprecations);
3689
+ deprecationsReported = true;
3690
+ }
3691
+ return result;
3692
+ },
3104
3693
  buildProject: runProjectBuild,
3105
3694
  checkProject: async (projectRoot, checkOptions) => {
3106
- await projectPreflight(projectRoot, { hostCompat });
3695
+ deprecations = (await projectPreflight(projectRoot, { hostCompat })).deprecations;
3107
3696
  const lint = lintProjectManifest(projectRoot);
3108
3697
  if (lint && !lint.ok) {
3109
3698
  const error = new Error(lint.errors.join("\n"));
@@ -3121,6 +3710,8 @@ async function main(argv) {
3121
3710
  return exitCode;
3122
3711
  }
3123
3712
  const { positional, options } = parseOptions(argv.slice(1));
3713
+ if (options.candidate && !(command === "migrate" && positional[0] === "project" && positional[1] === "plan")) throw new Error("--candidate 只适用于 migrate project plan");
3714
+ if (command !== "create" && (options.extensions || options.tool)) throw new Error("--extensions 和 --tool 只适用于 create");
3124
3715
  if (!COMMANDS.includes(command)) throw new Error(`未知命令:${command}`);
3125
3716
  if (command === "version") return commandVersion(options);
3126
3717
  if (command === "integration") return commandIntegration(options);