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