@dp-bohrium/widget-cli 0.3.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/AGENTS.md +25 -7
- package/CHANGELOG.md +19 -1
- package/README.md +29 -7
- package/assets/toolchain.json +6 -6
- package/dist/cli.mjs +658 -67
- package/dist/core.mjs +1 -1
- package/dist/{dsh-DbOeoKFJ.mjs → dsh-BvnlfCRX.mjs} +9 -10
- package/llms.txt +6 -2
- package/package.json +3 -3
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-
|
|
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 (
|
|
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: "
|
|
1219
|
-
usage: [
|
|
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
|
-
"
|
|
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: [
|
|
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
|
-
|
|
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
|
|
1519
|
+
未安装:pnpm dlx ${packageName} <命令> ...
|
|
1477
1520
|
下方示例中的 bohr-widget 可使用上述工程内调用前缀。
|
|
1478
1521
|
CLI 要求 Node.js >=20;默认生成工程要求 Node.js ${generatedProjectNode}。
|
|
1479
1522
|
|
|
1480
1523
|
典型开发流程:
|
|
1481
|
-
pnpm dlx
|
|
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-
|
|
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
|
-
}, ["
|
|
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
|
-
|
|
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
|
|
1723
|
-
if (!record(pkg)) throw failure("PROJECT_SDK_MISMATCH", "工程 package.json 必须是对象", { projectRoot: root }, ["恢复工程 package.json 中的精确 SDK
|
|
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
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
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}
|
|
1922
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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:
|
|
2192
|
-
extension:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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]
|
|
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
|
-
|
|
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);
|