create-windy 0.2.17 → 0.2.19
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/README.md +4 -3
- package/dist/cli.js +534 -76
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +11 -1
- package/template/README.md +8 -5
- package/template/apps/server/Dockerfile +3 -3
- package/template/apps/server/src/index.ts +8 -14
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +57 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +62 -0
- package/template/apps/server/src/settings/runtime.ts +30 -3
- package/template/apps/server/src/settings/watermark-repository.ts +32 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +203 -0
- package/template/apps/server/src/settings/watermark-routes.ts +134 -0
- package/template/apps/web/runtime-server.test.ts +4 -4
- package/template/apps/web/src/composables/useWatermarkSettings.ts +75 -33
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +86 -0
- package/template/apps/web/src/composables/watermark-settings.ts +9 -35
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +8 -32
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +81 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +34 -31
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +58 -7
- package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +1 -1
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +40 -18
- package/template/apps/web/src/services/platform-settings-api.ts +25 -2
- package/template/apps/web/vite.config.ts +3 -1
- package/template/docker-compose.yml +19 -6
- package/template/docs/platform/platform-shell-settings.md +24 -3
- package/template/docs/platform/web-system-crud.md +2 -2
- package/template/drizzle.config.ts +1 -1
- package/template/package.json +4 -1
- package/template/packages/database/drizzle/0031_panoramic_typhoid_mary.sql +12 -0
- package/template/packages/database/drizzle/meta/0031_snapshot.json +7387 -0
- package/template/packages/database/drizzle/meta/_journal.json +8 -1
- package/template/packages/database/src/schema/platform-settings.ts +16 -1
- package/template/packages/modules/src/system-api-permissions.ts +4 -0
- package/template/packages/modules/src/system-api-watermark.ts +26 -0
- package/template/packages/modules/src/system-features.ts +1 -1
- package/template/packages/modules/src/system-watermark-policy.test.ts +31 -0
- package/template/packages/shared/src/platform-settings.ts +13 -0
package/dist/cli.js
CHANGED
|
@@ -412,13 +412,20 @@ var platformModules = [
|
|
|
412
412
|
"apps/web/src/pages/settings",
|
|
413
413
|
"apps/web/src/layout/GlobalWatermark.vue",
|
|
414
414
|
"apps/web/src/layout/GlobalWatermark.webtest.ts",
|
|
415
|
+
"apps/web/src/layout/GlobalWatermark.layering.webtest.ts",
|
|
415
416
|
"apps/web/src/services/platform-settings-api.ts",
|
|
416
417
|
"apps/web/src/composables/useWatermarkSettings.ts",
|
|
418
|
+
"apps/web/src/composables/useWatermarkSettings.webtest.ts",
|
|
417
419
|
"apps/web/src/composables/watermark-settings.ts",
|
|
418
|
-
"apps/web/src/composables/watermark-settings.webtest.ts"
|
|
420
|
+
"apps/web/src/composables/watermark-settings.webtest.ts",
|
|
421
|
+
"packages/modules/src/system-api-watermark.ts",
|
|
422
|
+
"packages/modules/src/system-watermark-policy.test.ts"
|
|
419
423
|
],
|
|
420
424
|
documentation: ["docs/platform/platform-shell-settings.md"],
|
|
421
|
-
databaseTables: [
|
|
425
|
+
databaseTables: [
|
|
426
|
+
"platform_branding_settings",
|
|
427
|
+
"platform_watermark_policies"
|
|
428
|
+
]
|
|
422
429
|
},
|
|
423
430
|
{
|
|
424
431
|
...module2("system.configuration", "配置版本", "类型化配置、应用和回滚。", "recommended", ["system"]),
|
|
@@ -1033,6 +1040,7 @@ var ignoredFiles = new Set([
|
|
|
1033
1040
|
]);
|
|
1034
1041
|
var projectOwnedRoots = [
|
|
1035
1042
|
"apps/web/src/app",
|
|
1043
|
+
"apps/server/src/installed-business-modules.ts",
|
|
1036
1044
|
"apps/server/src/work-order",
|
|
1037
1045
|
"packages/example-work-order"
|
|
1038
1046
|
];
|
|
@@ -1041,8 +1049,13 @@ var sharedScaffoldRoots = [
|
|
|
1041
1049
|
"AGENTS.md",
|
|
1042
1050
|
".env.example",
|
|
1043
1051
|
"docker-compose.yml",
|
|
1052
|
+
"drizzle.config.ts",
|
|
1053
|
+
"apps/server/Dockerfile",
|
|
1054
|
+
"apps/server/src/index.ts",
|
|
1055
|
+
"apps/web/Dockerfile",
|
|
1044
1056
|
"apps/web/src/layout",
|
|
1045
|
-
"apps/web/src/router"
|
|
1057
|
+
"apps/web/src/router",
|
|
1058
|
+
"packages/database/src/schema/index.ts"
|
|
1046
1059
|
];
|
|
1047
1060
|
async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
|
|
1048
1061
|
const paths = await collectFiles(targetDirectory);
|
|
@@ -1305,7 +1318,7 @@ var starterCompose = `services:
|
|
|
1305
1318
|
POSTGRES_USER: windy
|
|
1306
1319
|
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?请在 .env 中设置 POSTGRES_PASSWORD}
|
|
1307
1320
|
ports:
|
|
1308
|
-
- "
|
|
1321
|
+
- "15432:5432"
|
|
1309
1322
|
volumes:
|
|
1310
1323
|
- postgres-data:/var/lib/postgresql/data
|
|
1311
1324
|
healthcheck:
|
|
@@ -1334,14 +1347,15 @@ var starterCompose = `services:
|
|
|
1334
1347
|
command: ["bun", "run", "--cwd", "apps/server", "dev"]
|
|
1335
1348
|
environment:
|
|
1336
1349
|
NODE_ENV: development
|
|
1337
|
-
PORT:
|
|
1350
|
+
PORT: 9747
|
|
1338
1351
|
DATABASE_URL: postgresql://windy:\${POSTGRES_PASSWORD}@postgres:5432/windy
|
|
1339
1352
|
WINDY_BOOTSTRAP_ADMIN_PASSWORD: \${WINDY_BOOTSTRAP_ADMIN_PASSWORD:?请在 .env 中设置管理员初始密码}
|
|
1340
1353
|
WINDY_AUDIT_QUEUE_FILE: /app/data/audit/recovery-queue.json
|
|
1341
1354
|
WINDY_CONTAINERIZED: "1"
|
|
1355
|
+
BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD: "true"
|
|
1342
1356
|
WINDY_SYSTEM_USAGE_PATHS: /app/data/audit
|
|
1343
1357
|
ports:
|
|
1344
|
-
- "
|
|
1358
|
+
- "9747:9747"
|
|
1345
1359
|
volumes:
|
|
1346
1360
|
- ./apps/server:/app/apps/server
|
|
1347
1361
|
- ./packages:/app/packages
|
|
@@ -1357,14 +1371,26 @@ var starterCompose = `services:
|
|
|
1357
1371
|
command: ["bun", "run", "--cwd", "apps/web", "dev"]
|
|
1358
1372
|
environment:
|
|
1359
1373
|
NODE_ENV: development
|
|
1360
|
-
WINDY_DEV_PORT:
|
|
1361
|
-
|
|
1374
|
+
WINDY_DEV_PORT: 18746
|
|
1375
|
+
WINDY_DEV_USE_POLLING: "1"
|
|
1376
|
+
API_UPSTREAM: http://server:9747
|
|
1362
1377
|
VITE_DEV_HOST: 0.0.0.0
|
|
1363
1378
|
ports:
|
|
1364
|
-
- "
|
|
1379
|
+
- "18746:18746"
|
|
1365
1380
|
volumes:
|
|
1366
1381
|
- ./apps/web:/app/apps/web
|
|
1367
1382
|
- ./packages:/app/packages
|
|
1383
|
+
healthcheck:
|
|
1384
|
+
test:
|
|
1385
|
+
[
|
|
1386
|
+
"CMD",
|
|
1387
|
+
"bun",
|
|
1388
|
+
"-e",
|
|
1389
|
+
"const r=await fetch('http://127.0.0.1:18746/-/windy-dev');process.exit(r.ok?0:1)",
|
|
1390
|
+
]
|
|
1391
|
+
interval: 5s
|
|
1392
|
+
timeout: 3s
|
|
1393
|
+
retries: 6
|
|
1368
1394
|
depends_on:
|
|
1369
1395
|
- server
|
|
1370
1396
|
|
|
@@ -1435,15 +1461,17 @@ create-windy 已创建仅供本机使用且被 Git 忽略的 \`.env\`,其中
|
|
|
1435
1461
|
文件权限为 \`0600\`。\`bun run dev\` 会通过 Docker Compose 等待 PostgreSQL、执行
|
|
1436
1462
|
Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接把 Server 连接到
|
|
1437
1463
|
空数据库。首次启动需要本机已经安装并启动 Docker 与 Docker Compose,并可能需要下载
|
|
1438
|
-
|
|
1464
|
+
基础镜像。后续 \`bun run dev\` 会复用已有镜像,不会重复构建;依赖、Dockerfile 或
|
|
1465
|
+
\`bun.lock\` 变化后运行 \`bun run dev:build\`。
|
|
1439
1466
|
|
|
1440
1467
|
| 服务 | 地址 |
|
|
1441
1468
|
| ---- | ---- |
|
|
1442
|
-
| Web | http://localhost:
|
|
1443
|
-
| Server 健康检查 | http://localhost:
|
|
1444
|
-
| PostgreSQL | localhost:
|
|
1469
|
+
| Web | http://localhost:18746 |
|
|
1470
|
+
| Server 健康检查 | http://localhost:9747/api/health |
|
|
1471
|
+
| PostgreSQL | localhost:15432 |
|
|
1445
1472
|
|
|
1446
|
-
Docker Web 固定使用 \`
|
|
1473
|
+
Docker Web 固定使用 \`18746\`,Web 与 Server 都挂载宿主机源码并保持热更新。只启动
|
|
1474
|
+
单个应用时使用:
|
|
1447
1475
|
|
|
1448
1476
|
\`\`\`bash
|
|
1449
1477
|
bun run dev:web
|
|
@@ -1502,6 +1530,7 @@ ${moduleRows}
|
|
|
1502
1530
|
|
|
1503
1531
|
\`\`\`bash
|
|
1504
1532
|
bun run dev
|
|
1533
|
+
bun run dev:build
|
|
1505
1534
|
bun run dev:web
|
|
1506
1535
|
bun run dev:server
|
|
1507
1536
|
bun run typecheck
|
|
@@ -1574,7 +1603,8 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
|
|
|
1574
1603
|
...includeOxc ? {
|
|
1575
1604
|
prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
|
|
1576
1605
|
} : {},
|
|
1577
|
-
dev: "docker compose up
|
|
1606
|
+
dev: "docker compose up",
|
|
1607
|
+
"dev:build": "docker compose up --build",
|
|
1578
1608
|
...generatorVersion ? { windy: "create-windy" } : {},
|
|
1579
1609
|
"dev:web": "bun run --cwd apps/web dev",
|
|
1580
1610
|
"dev:server": "bun run --cwd apps/server dev",
|
|
@@ -3788,7 +3818,7 @@ function sentenceWithoutStop(value) {
|
|
|
3788
3818
|
|
|
3789
3819
|
// src/update/cli.ts
|
|
3790
3820
|
import { rm as rm8 } from "node:fs/promises";
|
|
3791
|
-
import { resolve as
|
|
3821
|
+
import { resolve as resolve5 } from "node:path";
|
|
3792
3822
|
|
|
3793
3823
|
// src/update/doctor.ts
|
|
3794
3824
|
import { readFile as readFile11 } from "node:fs/promises";
|
|
@@ -3826,9 +3856,10 @@ import {
|
|
|
3826
3856
|
mkdir as mkdir5,
|
|
3827
3857
|
readFile as readFile10,
|
|
3828
3858
|
rm as rm5,
|
|
3859
|
+
rmdir,
|
|
3829
3860
|
writeFile as writeFile9
|
|
3830
3861
|
} from "node:fs/promises";
|
|
3831
|
-
import { dirname as dirname3, join as join10 } from "node:path";
|
|
3862
|
+
import { dirname as dirname3, join as join10, resolve as resolve4, sep as sep3 } from "node:path";
|
|
3832
3863
|
var updateStatePath = ".windy/update-state.json";
|
|
3833
3864
|
async function applyTransaction(plan) {
|
|
3834
3865
|
if (plan.conflicts.length) {
|
|
@@ -3866,7 +3897,11 @@ async function applyTransaction(plan) {
|
|
|
3866
3897
|
for (const file of changed) {
|
|
3867
3898
|
const target = join10(plan.projectDirectory, file.path);
|
|
3868
3899
|
if (file.action === "delete") {
|
|
3900
|
+
const existed = await exists(target);
|
|
3869
3901
|
await rm5(target, { force: true });
|
|
3902
|
+
if (existed) {
|
|
3903
|
+
await removeEmptyDeletedFileParents(target, plan.projectDirectory);
|
|
3904
|
+
}
|
|
3870
3905
|
continue;
|
|
3871
3906
|
}
|
|
3872
3907
|
if (!file.content)
|
|
@@ -3929,6 +3964,26 @@ async function exists(path2) {
|
|
|
3929
3964
|
return false;
|
|
3930
3965
|
}
|
|
3931
3966
|
}
|
|
3967
|
+
async function removeEmptyDeletedFileParents(deletedFile, projectDirectory) {
|
|
3968
|
+
const projectRoot = resolve4(projectDirectory);
|
|
3969
|
+
let directory = dirname3(resolve4(deletedFile));
|
|
3970
|
+
while (directory !== projectRoot && directory.startsWith(`${projectRoot}${sep3}`)) {
|
|
3971
|
+
try {
|
|
3972
|
+
await rmdir(directory);
|
|
3973
|
+
} catch (error) {
|
|
3974
|
+
if (isDirectoryNotRemovable(error))
|
|
3975
|
+
return;
|
|
3976
|
+
throw error;
|
|
3977
|
+
}
|
|
3978
|
+
directory = dirname3(directory);
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
function isDirectoryNotRemovable(error) {
|
|
3982
|
+
if (!(error instanceof Error) || !("code" in error))
|
|
3983
|
+
return false;
|
|
3984
|
+
const code = error.code;
|
|
3985
|
+
return code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST";
|
|
3986
|
+
}
|
|
3932
3987
|
function assertSafePath(path2) {
|
|
3933
3988
|
if (!path2 || path2.startsWith("/") || path2.split("/").includes("..")) {
|
|
3934
3989
|
throw new Error(`更新计划包含不安全路径:${path2}`);
|
|
@@ -4013,7 +4068,9 @@ var recipes = [
|
|
|
4013
4068
|
{ id: "starter-0.2.13-to-0.2.14", from: "0.2.13", to: "0.2.14" },
|
|
4014
4069
|
{ id: "starter-0.2.14-to-0.2.15", from: "0.2.14", to: "0.2.15" },
|
|
4015
4070
|
{ id: "starter-0.2.15-to-0.2.16", from: "0.2.15", to: "0.2.16" },
|
|
4016
|
-
{ id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" }
|
|
4071
|
+
{ id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" },
|
|
4072
|
+
{ id: "starter-0.2.17-to-0.2.18", from: "0.2.17", to: "0.2.18" },
|
|
4073
|
+
{ id: "starter-0.2.18-to-0.2.19", from: "0.2.18", to: "0.2.19" }
|
|
4017
4074
|
];
|
|
4018
4075
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
4019
4076
|
if (sourceVersion === targetVersion)
|
|
@@ -4038,8 +4095,8 @@ function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
|
4038
4095
|
}
|
|
4039
4096
|
|
|
4040
4097
|
// src/update/updater.ts
|
|
4041
|
-
import { mkdir as mkdir7, readFile as
|
|
4042
|
-
import { dirname as dirname4, join as
|
|
4098
|
+
import { mkdir as mkdir7, readFile as readFile15, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
|
|
4099
|
+
import { dirname as dirname4, join as join16 } from "node:path";
|
|
4043
4100
|
|
|
4044
4101
|
// src/update/artifacts.ts
|
|
4045
4102
|
import { mkdir as mkdir6, mkdtemp, readFile as readFile12 } from "node:fs/promises";
|
|
@@ -4101,8 +4158,8 @@ async function readProjectName2(projectDirectory) {
|
|
|
4101
4158
|
}
|
|
4102
4159
|
|
|
4103
4160
|
// src/update/planner.ts
|
|
4104
|
-
import { access as access3, readFile as
|
|
4105
|
-
import { join as
|
|
4161
|
+
import { access as access3, readFile as readFile14 } from "node:fs/promises";
|
|
4162
|
+
import { join as join15 } from "node:path";
|
|
4106
4163
|
|
|
4107
4164
|
// src/update/known-repairs.ts
|
|
4108
4165
|
var wrongTextVersions = new Set(["0.2.12", "0.2.13"]);
|
|
@@ -4163,13 +4220,397 @@ async function mergeTextFile(base, ours, theirs) {
|
|
|
4163
4220
|
if (result.code === 0) {
|
|
4164
4221
|
return { content: new TextEncoder().encode(result.stdout) };
|
|
4165
4222
|
}
|
|
4166
|
-
if (result.code === 1)
|
|
4167
|
-
|
|
4223
|
+
if (result.code === 1) {
|
|
4224
|
+
const resolved = resolveConservativeConflicts(result.stdout);
|
|
4225
|
+
return resolved ? { content: new TextEncoder().encode(resolved) } : { conflict: "三方合并存在重叠修改" };
|
|
4226
|
+
}
|
|
4168
4227
|
return { conflict: result.stderr.trim() || "git merge-file 执行失败" };
|
|
4169
4228
|
} finally {
|
|
4170
4229
|
await rm6(root, { recursive: true, force: true });
|
|
4171
4230
|
}
|
|
4172
4231
|
}
|
|
4232
|
+
function resolveConservativeConflicts(content) {
|
|
4233
|
+
const lines = content.split(`
|
|
4234
|
+
`);
|
|
4235
|
+
const output = [];
|
|
4236
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
4237
|
+
if (!lines[index]?.startsWith("<<<<<<< ")) {
|
|
4238
|
+
output.push(lines[index] ?? "");
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
const baseMarker = lines.findIndex((line, candidate) => candidate > index && line.startsWith("||||||| "));
|
|
4242
|
+
const separator = lines.indexOf("=======", baseMarker + 1);
|
|
4243
|
+
const endMarker = lines.findIndex((line, candidate) => candidate > separator && line.startsWith(">>>>>>> "));
|
|
4244
|
+
if (baseMarker < 0 || separator < 0 || endMarker < 0)
|
|
4245
|
+
return;
|
|
4246
|
+
const ours = lines.slice(index + 1, baseMarker);
|
|
4247
|
+
const base = lines.slice(baseMarker + 1, separator);
|
|
4248
|
+
const theirs = lines.slice(separator + 1, endMarker);
|
|
4249
|
+
const resolved = resolveRegion(ours, base, theirs);
|
|
4250
|
+
if (!resolved)
|
|
4251
|
+
return;
|
|
4252
|
+
output.push(...resolved);
|
|
4253
|
+
index = endMarker;
|
|
4254
|
+
}
|
|
4255
|
+
return output.join(`
|
|
4256
|
+
`);
|
|
4257
|
+
}
|
|
4258
|
+
function resolveRegion(ours, base, theirs) {
|
|
4259
|
+
const oursRebased = replaceSubsequence(ours, base, theirs);
|
|
4260
|
+
if (oursRebased)
|
|
4261
|
+
return oursRebased;
|
|
4262
|
+
const theirsRebased = replaceSubsequence(theirs, base, ours);
|
|
4263
|
+
if (theirsRebased)
|
|
4264
|
+
return theirsRebased;
|
|
4265
|
+
if (normalizeWhitespace(ours) === normalizeWhitespace(base))
|
|
4266
|
+
return theirs;
|
|
4267
|
+
if (normalizeWhitespace(theirs) === normalizeWhitespace(base))
|
|
4268
|
+
return ours;
|
|
4269
|
+
return applySemanticTokenEdits(ours, base, theirs) ?? mergeMarkdownTable(ours, base, theirs);
|
|
4270
|
+
}
|
|
4271
|
+
function replaceSubsequence(content, baseline, replacement) {
|
|
4272
|
+
if (!baseline.length || baseline.length > content.length)
|
|
4273
|
+
return;
|
|
4274
|
+
const starts = [];
|
|
4275
|
+
for (let index = 0;index <= content.length - baseline.length; index += 1) {
|
|
4276
|
+
if (baseline.every((line, offset) => content[index + offset] === line)) {
|
|
4277
|
+
starts.push(index);
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
if (starts.length !== 1)
|
|
4281
|
+
return;
|
|
4282
|
+
const start = starts[0] ?? 0;
|
|
4283
|
+
return [
|
|
4284
|
+
...content.slice(0, start),
|
|
4285
|
+
...replacement,
|
|
4286
|
+
...content.slice(start + baseline.length)
|
|
4287
|
+
];
|
|
4288
|
+
}
|
|
4289
|
+
function normalizeWhitespace(lines) {
|
|
4290
|
+
return lines.join(" ").replaceAll(/\s+/g, " ").trim();
|
|
4291
|
+
}
|
|
4292
|
+
function applySemanticTokenEdits(oursLines, baseLines, theirsLines) {
|
|
4293
|
+
const oursText = oursLines.join(`
|
|
4294
|
+
`);
|
|
4295
|
+
const baseText = baseLines.join(`
|
|
4296
|
+
`);
|
|
4297
|
+
const theirsText = theirsLines.join(`
|
|
4298
|
+
`);
|
|
4299
|
+
const ours = semanticTokens(oursText);
|
|
4300
|
+
const base = semanticTokens(baseText);
|
|
4301
|
+
const theirs = semanticTokens(theirsText);
|
|
4302
|
+
let prefix = 0;
|
|
4303
|
+
while (prefix < base.length && prefix < theirs.length && base[prefix]?.value === theirs[prefix]?.value) {
|
|
4304
|
+
prefix += 1;
|
|
4305
|
+
}
|
|
4306
|
+
let suffix = 0;
|
|
4307
|
+
while (suffix < base.length - prefix && suffix < theirs.length - prefix && base[base.length - 1 - suffix]?.value === theirs[theirs.length - 1 - suffix]?.value) {
|
|
4308
|
+
suffix += 1;
|
|
4309
|
+
}
|
|
4310
|
+
const baseEnd = base.length - suffix;
|
|
4311
|
+
const targetEnd = theirs.length - suffix;
|
|
4312
|
+
const needle = base.slice(prefix, baseEnd).map(({ value }) => value);
|
|
4313
|
+
if (!needle.length)
|
|
4314
|
+
return;
|
|
4315
|
+
const starts = findTokenSequence(ours, needle);
|
|
4316
|
+
if (starts.length !== 1)
|
|
4317
|
+
return;
|
|
4318
|
+
const oursStart = starts[0] ?? 0;
|
|
4319
|
+
const oursEnd = oursStart + needle.length - 1;
|
|
4320
|
+
const targetFirst = theirs[prefix];
|
|
4321
|
+
const targetLast = theirs[targetEnd - 1];
|
|
4322
|
+
const replacement = targetFirst && targetLast ? theirsText.slice(targetFirst.start, targetLast.end) : "";
|
|
4323
|
+
const merged = oursText.slice(0, ours[oursStart]?.start ?? 0) + replacement + oursText.slice(ours[oursEnd]?.end ?? 0);
|
|
4324
|
+
return merged.split(`
|
|
4325
|
+
`);
|
|
4326
|
+
}
|
|
4327
|
+
function semanticTokens(content) {
|
|
4328
|
+
return [...content.matchAll(/[A-Za-z0-9_@./:-]+|[^\s]/g)].map((match) => ({
|
|
4329
|
+
value: match[0],
|
|
4330
|
+
start: match.index,
|
|
4331
|
+
end: match.index + match[0].length
|
|
4332
|
+
}));
|
|
4333
|
+
}
|
|
4334
|
+
function findTokenSequence(tokens, needle) {
|
|
4335
|
+
const starts = [];
|
|
4336
|
+
for (let index = 0;index <= tokens.length - needle.length; index += 1) {
|
|
4337
|
+
if (needle.every((value, offset) => tokens[index + offset]?.value === value)) {
|
|
4338
|
+
starts.push(index);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
return starts;
|
|
4342
|
+
}
|
|
4343
|
+
function mergeMarkdownTable(ours, base, theirs) {
|
|
4344
|
+
if (![ours, base, theirs].every((rows) => rows.length === base.length && rows.every(isMarkdownTableRow))) {
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
const parsed = [ours, base, theirs].map((rows) => rows.map(parseTableRow));
|
|
4348
|
+
const [oursRows, baseRows, theirsRows] = parsed;
|
|
4349
|
+
if (!oursRows || !baseRows || !theirsRows || !oursRows.every((cells, index) => cells.length === baseRows[index]?.length && cells.length === theirsRows[index]?.length)) {
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
const merged = [];
|
|
4353
|
+
for (let row = 0;row < baseRows.length; row += 1) {
|
|
4354
|
+
const cells = [];
|
|
4355
|
+
for (let column = 0;column < (baseRows[row]?.length ?? 0); column += 1) {
|
|
4356
|
+
const baseCell = baseRows[row]?.[column] ?? "";
|
|
4357
|
+
const oursCell = oursRows[row]?.[column] ?? "";
|
|
4358
|
+
const theirsCell = theirsRows[row]?.[column] ?? "";
|
|
4359
|
+
if (isTableSeparator(baseCell)) {
|
|
4360
|
+
cells.push("---");
|
|
4361
|
+
} else if (oursCell === baseCell || oursCell === theirsCell) {
|
|
4362
|
+
cells.push(theirsCell);
|
|
4363
|
+
} else if (theirsCell === baseCell) {
|
|
4364
|
+
cells.push(oursCell);
|
|
4365
|
+
} else {
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
merged.push(`| ${cells.join(" | ")} |`);
|
|
4370
|
+
}
|
|
4371
|
+
return merged;
|
|
4372
|
+
}
|
|
4373
|
+
function isMarkdownTableRow(line) {
|
|
4374
|
+
const trimmed = line.trim();
|
|
4375
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|");
|
|
4376
|
+
}
|
|
4377
|
+
function parseTableRow(line) {
|
|
4378
|
+
return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
|
|
4379
|
+
}
|
|
4380
|
+
function isTableSeparator(cell) {
|
|
4381
|
+
return /^:?-{3,}:?$/.test(cell);
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
// src/update/migration-reconciler.ts
|
|
4385
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
4386
|
+
import { join as join14 } from "node:path";
|
|
4387
|
+
|
|
4388
|
+
// src/update/snapshot-delta.ts
|
|
4389
|
+
function applySnapshotDelta(before, after, current, path2, conflicts, conflictPath) {
|
|
4390
|
+
if (deepEqual(before, after))
|
|
4391
|
+
return structuredClone(current);
|
|
4392
|
+
if (!isRecord(before) || !isRecord(after) || !isRecord(current)) {
|
|
4393
|
+
if (deepEqual(current, before) || deepEqual(current, after)) {
|
|
4394
|
+
return structuredClone(after);
|
|
4395
|
+
}
|
|
4396
|
+
conflicts.push(`${conflictPath}:平台与业务同时修改 snapshot 节点 ${path2.join(".") || "<root>"}`);
|
|
4397
|
+
return structuredClone(current);
|
|
4398
|
+
}
|
|
4399
|
+
const merged = structuredClone(current);
|
|
4400
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
4401
|
+
for (const key of keys) {
|
|
4402
|
+
if (path2.length === 0 && (key === "id" || key === "prevId"))
|
|
4403
|
+
continue;
|
|
4404
|
+
const nextPath = [...path2, key];
|
|
4405
|
+
if (!(key in after)) {
|
|
4406
|
+
if (!(key in current))
|
|
4407
|
+
continue;
|
|
4408
|
+
if (deepEqual(current[key], before[key]))
|
|
4409
|
+
delete merged[key];
|
|
4410
|
+
else
|
|
4411
|
+
conflicts.push(`${conflictPath}:平台删除了业务已修改的 snapshot 节点 ${nextPath.join(".")}`);
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (!(key in before)) {
|
|
4415
|
+
if (!(key in current) || deepEqual(current[key], after[key])) {
|
|
4416
|
+
merged[key] = structuredClone(after[key]);
|
|
4417
|
+
} else {
|
|
4418
|
+
conflicts.push(`${conflictPath}:平台新增的 snapshot 节点已被业务占用 ${nextPath.join(".")}`);
|
|
4419
|
+
}
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
merged[key] = applySnapshotDelta(before[key], after[key], current[key], nextPath, conflicts, conflictPath);
|
|
4423
|
+
}
|
|
4424
|
+
return merged;
|
|
4425
|
+
}
|
|
4426
|
+
function isRecord(value) {
|
|
4427
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4428
|
+
}
|
|
4429
|
+
function deepEqual(left, right) {
|
|
4430
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
// src/update/migration-reconciler.ts
|
|
4434
|
+
var drizzleRoot = "packages/database/drizzle";
|
|
4435
|
+
var journalPath = `${drizzleRoot}/meta/_journal.json`;
|
|
4436
|
+
var migrationPattern = /^(\d{4})_(.+)$/;
|
|
4437
|
+
async function reconcileDrizzleMigrations(input) {
|
|
4438
|
+
const journals = await readJournals(input);
|
|
4439
|
+
if (!journals) {
|
|
4440
|
+
return unchanged(input.targetManifest);
|
|
4441
|
+
}
|
|
4442
|
+
const { base, project, target } = journals;
|
|
4443
|
+
const conflicts = [];
|
|
4444
|
+
validateUniqueNames(base, "来源模板", conflicts);
|
|
4445
|
+
validateUniqueNames(project, "当前项目", conflicts);
|
|
4446
|
+
validateUniqueNames(target, "目标模板", conflicts);
|
|
4447
|
+
if (conflicts.length) {
|
|
4448
|
+
return { ...unchanged(input.targetManifest), conflicts };
|
|
4449
|
+
}
|
|
4450
|
+
const baseNames = new Set(base.entries.map(({ tag }) => stableName(tag)));
|
|
4451
|
+
const projectNames = new Set(project.entries.map(({ tag }) => stableName(tag)));
|
|
4452
|
+
const managedNames = new Set(input.sourceManifest.files.filter(({ path: path2 }) => isSqlMigration(path2)).map(({ path: path2 }) => stableName(path2.slice(drizzleRoot.length + 1, -4))));
|
|
4453
|
+
const candidates = target.entries.filter(({ tag }) => !baseNames.has(stableName(tag)));
|
|
4454
|
+
for (const { tag } of candidates) {
|
|
4455
|
+
const name = stableName(tag);
|
|
4456
|
+
if (projectNames.has(name) && !managedNames.has(name)) {
|
|
4457
|
+
conflicts.push(`${journalPath}:平台 migration 名称与下游业务历史冲突 ${name}`);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
const additions = candidates.filter(({ tag }) => !projectNames.has(stableName(tag)));
|
|
4461
|
+
const contentOverrides = new Map;
|
|
4462
|
+
const files = await preservePublishedHistory(input.sourceManifest, input.projectDirectory, contentOverrides);
|
|
4463
|
+
const publishedPaths = new Set(files.map(({ path: path2 }) => path2));
|
|
4464
|
+
files.push(...input.targetManifest.files.filter(({ path: path2 }) => !isDrizzleHistoryPath(path2)));
|
|
4465
|
+
if (!additions.length) {
|
|
4466
|
+
const journalContent2 = encodeJson(project);
|
|
4467
|
+
contentOverrides.set(journalPath, journalContent2);
|
|
4468
|
+
files.push(managedEntry(journalPath, journalContent2));
|
|
4469
|
+
return result(input.targetManifest, files, contentOverrides, publishedPaths, conflicts);
|
|
4470
|
+
}
|
|
4471
|
+
let nextPrefix = highestPrefix(project) + 1;
|
|
4472
|
+
let nextIndex = highestIndex(project) + 1;
|
|
4473
|
+
let previousWhen = project.entries.at(-1)?.when ?? 0;
|
|
4474
|
+
let currentSnapshot = await readLatestSnapshot(input.projectDirectory, project);
|
|
4475
|
+
const mergedJournal = structuredClone(project);
|
|
4476
|
+
for (const addition of additions) {
|
|
4477
|
+
const targetPosition = target.entries.indexOf(addition);
|
|
4478
|
+
const previousTarget = target.entries[targetPosition - 1];
|
|
4479
|
+
if (!previousTarget) {
|
|
4480
|
+
conflicts.push(`${journalPath}:目标 migration 缺少前置快照`);
|
|
4481
|
+
break;
|
|
4482
|
+
}
|
|
4483
|
+
const targetBefore = await readSnapshot(input.targetDirectory, previousTarget.tag);
|
|
4484
|
+
const targetAfter = await readSnapshot(input.targetDirectory, addition.tag);
|
|
4485
|
+
const merged = applySnapshotDelta(targetBefore, targetAfter, currentSnapshot, [], conflicts, journalPath);
|
|
4486
|
+
if (conflicts.length)
|
|
4487
|
+
break;
|
|
4488
|
+
const prefix = String(nextPrefix).padStart(4, "0");
|
|
4489
|
+
const name = stableName(addition.tag);
|
|
4490
|
+
const tag = `${prefix}_${name}`;
|
|
4491
|
+
const sqlPath = `${drizzleRoot}/${tag}.sql`;
|
|
4492
|
+
const snapshotPath = `${drizzleRoot}/meta/${prefix}_snapshot.json`;
|
|
4493
|
+
const sql = await readFile13(join14(input.targetDirectory, `${drizzleRoot}/${addition.tag}.sql`));
|
|
4494
|
+
merged.id = targetAfter.id;
|
|
4495
|
+
merged.prevId = currentSnapshot.id;
|
|
4496
|
+
const snapshot = encodeJson(merged);
|
|
4497
|
+
contentOverrides.set(sqlPath, sql);
|
|
4498
|
+
contentOverrides.set(snapshotPath, snapshot);
|
|
4499
|
+
files.push(managedEntry(sqlPath, sql), managedEntry(snapshotPath, snapshot));
|
|
4500
|
+
previousWhen = Math.max(previousWhen + 1, addition.when);
|
|
4501
|
+
mergedJournal.entries.push({
|
|
4502
|
+
...addition,
|
|
4503
|
+
idx: nextIndex,
|
|
4504
|
+
when: previousWhen,
|
|
4505
|
+
tag
|
|
4506
|
+
});
|
|
4507
|
+
currentSnapshot = merged;
|
|
4508
|
+
nextPrefix += 1;
|
|
4509
|
+
nextIndex += 1;
|
|
4510
|
+
}
|
|
4511
|
+
const journalContent = encodeJson(mergedJournal);
|
|
4512
|
+
contentOverrides.set(journalPath, journalContent);
|
|
4513
|
+
files.push(managedEntry(journalPath, journalContent));
|
|
4514
|
+
return result(input.targetManifest, files, contentOverrides, publishedPaths, conflicts);
|
|
4515
|
+
}
|
|
4516
|
+
function result(targetManifest, files, contentOverrides, forcePreserves, conflicts) {
|
|
4517
|
+
return {
|
|
4518
|
+
targetManifest: {
|
|
4519
|
+
...targetManifest,
|
|
4520
|
+
files: deduplicate(files).sort((left, right) => left.path.localeCompare(right.path))
|
|
4521
|
+
},
|
|
4522
|
+
contentOverrides,
|
|
4523
|
+
forceUpdates: new Set([journalPath]),
|
|
4524
|
+
forcePreserves,
|
|
4525
|
+
conflicts
|
|
4526
|
+
};
|
|
4527
|
+
}
|
|
4528
|
+
function unchanged(targetManifest) {
|
|
4529
|
+
return {
|
|
4530
|
+
targetManifest,
|
|
4531
|
+
contentOverrides: new Map,
|
|
4532
|
+
forceUpdates: new Set,
|
|
4533
|
+
forcePreserves: new Set,
|
|
4534
|
+
conflicts: []
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
async function readJournals(input) {
|
|
4538
|
+
try {
|
|
4539
|
+
const [base, project, target] = await Promise.all([
|
|
4540
|
+
readJson(join14(input.baseDirectory, journalPath)),
|
|
4541
|
+
readJson(join14(input.projectDirectory, journalPath)),
|
|
4542
|
+
readJson(join14(input.targetDirectory, journalPath))
|
|
4543
|
+
]);
|
|
4544
|
+
return { base, project, target };
|
|
4545
|
+
} catch (error) {
|
|
4546
|
+
if (isMissing4(error))
|
|
4547
|
+
return;
|
|
4548
|
+
throw error;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
async function preservePublishedHistory(manifest, projectDirectory, overrides) {
|
|
4552
|
+
const entries = manifest.files.filter(({ path: path2 }) => isMigrationArtifact(path2) && path2 !== journalPath);
|
|
4553
|
+
await Promise.all(entries.map(async ({ path: path2 }) => {
|
|
4554
|
+
overrides.set(path2, await readFile13(join14(projectDirectory, path2)));
|
|
4555
|
+
}));
|
|
4556
|
+
return entries;
|
|
4557
|
+
}
|
|
4558
|
+
async function readLatestSnapshot(root, journal) {
|
|
4559
|
+
const latest = journal.entries.at(-1);
|
|
4560
|
+
if (!latest)
|
|
4561
|
+
throw new Error(`${journalPath}:当前项目没有 migration`);
|
|
4562
|
+
return await readSnapshot(root, latest.tag);
|
|
4563
|
+
}
|
|
4564
|
+
async function readSnapshot(root, tag) {
|
|
4565
|
+
const match = migrationPattern.exec(tag);
|
|
4566
|
+
if (!match)
|
|
4567
|
+
throw new Error(`${journalPath}:migration tag 格式无效:${tag}`);
|
|
4568
|
+
return await readJson(join14(root, `${drizzleRoot}/meta/${match[1]}_snapshot.json`));
|
|
4569
|
+
}
|
|
4570
|
+
function validateUniqueNames(journal, label, conflicts) {
|
|
4571
|
+
const seen = new Set;
|
|
4572
|
+
for (const { tag } of journal.entries) {
|
|
4573
|
+
const name = stableName(tag);
|
|
4574
|
+
if (seen.has(name)) {
|
|
4575
|
+
conflicts.push(`${journalPath}:${label}存在重复 migration 名称 ${name}`);
|
|
4576
|
+
}
|
|
4577
|
+
seen.add(name);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
function stableName(tag) {
|
|
4581
|
+
return migrationPattern.exec(tag)?.[2] ?? tag;
|
|
4582
|
+
}
|
|
4583
|
+
function highestPrefix(journal) {
|
|
4584
|
+
return Math.max(-1, ...journal.entries.map(({ tag }) => Number(migrationPattern.exec(tag)?.[1] ?? -1)));
|
|
4585
|
+
}
|
|
4586
|
+
function highestIndex(journal) {
|
|
4587
|
+
return Math.max(-1, ...journal.entries.map(({ idx }) => idx));
|
|
4588
|
+
}
|
|
4589
|
+
function isDrizzleHistoryPath(path2) {
|
|
4590
|
+
return path2 === journalPath || isMigrationArtifact(path2);
|
|
4591
|
+
}
|
|
4592
|
+
function isMigrationArtifact(path2) {
|
|
4593
|
+
return isSqlMigration(path2) || new RegExp(`^${drizzleRoot}/meta/\\d{4}_snapshot\\.json$`).test(path2);
|
|
4594
|
+
}
|
|
4595
|
+
function isSqlMigration(path2) {
|
|
4596
|
+
return new RegExp(`^${drizzleRoot}/\\d{4}_.+\\.sql$`).test(path2);
|
|
4597
|
+
}
|
|
4598
|
+
function managedEntry(path2, content) {
|
|
4599
|
+
return { path: path2, ownership: "windy-managed", hash: hashContent(content) };
|
|
4600
|
+
}
|
|
4601
|
+
function deduplicate(files) {
|
|
4602
|
+
return [...new Map(files.map((entry) => [entry.path, entry])).values()];
|
|
4603
|
+
}
|
|
4604
|
+
function encodeJson(value) {
|
|
4605
|
+
return new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
|
|
4606
|
+
`);
|
|
4607
|
+
}
|
|
4608
|
+
async function readJson(path2) {
|
|
4609
|
+
return JSON.parse(await readFile13(path2, "utf8"));
|
|
4610
|
+
}
|
|
4611
|
+
function isMissing4(error) {
|
|
4612
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4613
|
+
}
|
|
4173
4614
|
|
|
4174
4615
|
// src/update/package-merge.ts
|
|
4175
4616
|
function mergePackageJson(base, ours, theirs) {
|
|
@@ -4234,10 +4675,18 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4234
4675
|
throw new Error(`当前 CLI 只能更新到 ${targetProvenance.generatorVersion},请运行 create-windy@${targetVersion}`);
|
|
4235
4676
|
}
|
|
4236
4677
|
const recipes2 = resolveRecipeChain(provenance.windyVersion, targetVersion);
|
|
4237
|
-
const [sourceManifest,
|
|
4678
|
+
const [sourceManifest, generatedTargetManifest] = await Promise.all([
|
|
4238
4679
|
readManagedFilesManifest(request.projectDirectory),
|
|
4239
4680
|
readManagedFilesManifest(artifactSet.targetDirectory)
|
|
4240
4681
|
]);
|
|
4682
|
+
const migrations = await reconcileDrizzleMigrations({
|
|
4683
|
+
projectDirectory: request.projectDirectory,
|
|
4684
|
+
baseDirectory: artifactSet.baseDirectory,
|
|
4685
|
+
targetDirectory: artifactSet.targetDirectory,
|
|
4686
|
+
sourceManifest,
|
|
4687
|
+
targetManifest: generatedTargetManifest
|
|
4688
|
+
});
|
|
4689
|
+
const targetManifest = migrations.targetManifest;
|
|
4241
4690
|
targetProvenance.appliedRecipes = [
|
|
4242
4691
|
...provenance.appliedRecipes,
|
|
4243
4692
|
...recipes2.map(({ id }) => id)
|
|
@@ -4250,7 +4699,9 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4250
4699
|
for (const path2 of [...paths].sort()) {
|
|
4251
4700
|
if (metadataPaths.has(path2))
|
|
4252
4701
|
continue;
|
|
4253
|
-
const
|
|
4702
|
+
const forcedContent = migrations.forceUpdates.has(path2) ? migrations.contentOverrides.get(path2) : undefined;
|
|
4703
|
+
const forcePreserve = migrations.forcePreserves.has(path2);
|
|
4704
|
+
const file = forcePreserve ? ready(path2, sourceEntries.get(path2)?.ownership ?? classifyOwnership(path2), "preserve", "保留项目已发布 migration 历史") : forcedContent ? ready(path2, targetEntries.get(path2)?.ownership ?? sourceEntries.get(path2)?.ownership ?? classifyOwnership(path2), "update", "合并平台与下游 migration 历史", forcedContent) : await planFile(path2, request.projectDirectory, artifactSet.baseDirectory, artifactSet.targetDirectory, sourceEntries.get(path2), targetEntries.get(path2), provenance, migrations.contentOverrides.get(path2));
|
|
4254
4705
|
if (oxcManagedPaths.has(path2) && file.reason === "客户已修改") {
|
|
4255
4706
|
file.action = "preserve";
|
|
4256
4707
|
delete file.conflict;
|
|
@@ -4258,7 +4709,10 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4258
4709
|
}
|
|
4259
4710
|
files.push(file);
|
|
4260
4711
|
}
|
|
4261
|
-
const conflicts =
|
|
4712
|
+
const conflicts = [
|
|
4713
|
+
...migrations.conflicts,
|
|
4714
|
+
...files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : [])
|
|
4715
|
+
];
|
|
4262
4716
|
return {
|
|
4263
4717
|
id: createUpdateId(provenance.windyVersion, targetVersion),
|
|
4264
4718
|
projectDirectory: request.projectDirectory,
|
|
@@ -4273,11 +4727,11 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4273
4727
|
temporaryRoot: artifactSet.temporaryRoot
|
|
4274
4728
|
};
|
|
4275
4729
|
}
|
|
4276
|
-
async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance) {
|
|
4730
|
+
async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance, targetContentOverride) {
|
|
4277
4731
|
const [ours, base, theirs] = await Promise.all([
|
|
4278
|
-
readOptional(
|
|
4279
|
-
readOptional(
|
|
4280
|
-
readOptional(
|
|
4732
|
+
readOptional(join15(project, path2)),
|
|
4733
|
+
readOptional(join15(baseRoot, path2)),
|
|
4734
|
+
targetContentOverride ? Promise.resolve(targetContentOverride) : readOptional(join15(targetRoot, path2))
|
|
4281
4735
|
]);
|
|
4282
4736
|
const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
|
|
4283
4737
|
const repaired = ours && theirs && provenance ? repairKnownDerivedFile({
|
|
@@ -4322,16 +4776,17 @@ async function planFile(path2, project, baseRoot, targetRoot, source, target, pr
|
|
|
4322
4776
|
return conflict(path2, ownership, "文件状态不完整,不能安全推断");
|
|
4323
4777
|
}
|
|
4324
4778
|
const oursChanged = hashContent(ours) !== source.hash;
|
|
4325
|
-
const targetChanged =
|
|
4779
|
+
const targetChanged = targetChangedSinceBase(source.hash, base, theirs);
|
|
4780
|
+
const reconstructedBaseDrifted = !!base && hashContent(base) !== source.hash;
|
|
4326
4781
|
if (!oursChanged && !targetChanged)
|
|
4327
4782
|
return ready(path2, ownership, "preserve", "双方未修改");
|
|
4328
|
-
if (!oursChanged)
|
|
4783
|
+
if (!oursChanged && !reconstructedBaseDrifted)
|
|
4329
4784
|
return ready(path2, ownership, "update", "仅 Windy 修改", theirs);
|
|
4330
4785
|
if (!targetChanged)
|
|
4331
4786
|
return ready(path2, ownership, "preserve", "客户已修改");
|
|
4332
4787
|
if (hashContent(ours) === target.hash)
|
|
4333
4788
|
return ready(path2, ownership, "preserve", "已包含目标内容");
|
|
4334
|
-
if (path2
|
|
4789
|
+
if (path2.endsWith("package.json") && base) {
|
|
4335
4790
|
const merged = mergePackageJson(base, ours, theirs);
|
|
4336
4791
|
return merged.content ? ready(path2, ownership, "update", "package.json 字段级合并", merged.content) : conflict(path2, ownership, `字段冲突:${merged.conflicts.join("、")}`);
|
|
4337
4792
|
}
|
|
@@ -4341,6 +4796,9 @@ async function planFile(path2, project, baseRoot, targetRoot, source, target, pr
|
|
|
4341
4796
|
}
|
|
4342
4797
|
return conflict(path2, ownership, "客户和 Windy 均已修改");
|
|
4343
4798
|
}
|
|
4799
|
+
function targetChangedSinceBase(sourceHash, base, target) {
|
|
4800
|
+
return base && target ? hashContent(base) !== hashContent(target) : !!target && hashContent(target) !== sourceHash;
|
|
4801
|
+
}
|
|
4344
4802
|
function ready(path2, ownership, action, reason, content) {
|
|
4345
4803
|
return { path: path2, ownership, action, reason, content };
|
|
4346
4804
|
}
|
|
@@ -4355,7 +4813,7 @@ function conflict(path2, ownership, reason) {
|
|
|
4355
4813
|
}
|
|
4356
4814
|
async function readOptional(path2) {
|
|
4357
4815
|
try {
|
|
4358
|
-
return await
|
|
4816
|
+
return await readFile14(path2);
|
|
4359
4817
|
} catch (error) {
|
|
4360
4818
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
4361
4819
|
return;
|
|
@@ -4363,15 +4821,15 @@ async function readOptional(path2) {
|
|
|
4363
4821
|
}
|
|
4364
4822
|
}
|
|
4365
4823
|
async function assertGitClean(projectDirectory) {
|
|
4366
|
-
const
|
|
4367
|
-
if (
|
|
4824
|
+
const result2 = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
|
|
4825
|
+
if (result2.code !== 0)
|
|
4368
4826
|
throw new Error("更新要求当前目录是有效的 Git 仓库");
|
|
4369
|
-
if (
|
|
4827
|
+
if (result2.stdout.trim())
|
|
4370
4828
|
throw new Error("更新前请先提交或暂存之外妥善处理 Git 工作区改动");
|
|
4371
4829
|
}
|
|
4372
4830
|
async function assertNoPendingUpdate(projectDirectory) {
|
|
4373
4831
|
try {
|
|
4374
|
-
await access3(
|
|
4832
|
+
await access3(join15(projectDirectory, ".windy/update-state.json"));
|
|
4375
4833
|
throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
|
|
4376
4834
|
} catch (error) {
|
|
4377
4835
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
@@ -4402,75 +4860,75 @@ class DefaultStarterUpdater {
|
|
|
4402
4860
|
throw error;
|
|
4403
4861
|
}
|
|
4404
4862
|
}
|
|
4405
|
-
async verify(
|
|
4863
|
+
async verify(result2) {
|
|
4406
4864
|
const steps = [];
|
|
4407
4865
|
try {
|
|
4408
|
-
await this.#execute("bun", ["install", "--force"],
|
|
4866
|
+
await this.#execute("bun", ["install", "--force"], result2.plan.projectDirectory);
|
|
4409
4867
|
steps.push({ name: "bun install --force", status: "passed" });
|
|
4410
|
-
const packageJson = JSON.parse(await
|
|
4868
|
+
const packageJson = JSON.parse(await readFile15(join16(result2.plan.projectDirectory, "package.json"), "utf8"));
|
|
4411
4869
|
for (const name of ["lint", "typecheck", "test", "build"]) {
|
|
4412
4870
|
if (!packageJson.scripts?.[name]) {
|
|
4413
4871
|
steps.push({ name: `bun run ${name}`, status: "skipped" });
|
|
4414
4872
|
continue;
|
|
4415
4873
|
}
|
|
4416
|
-
await this.#execute("bun", ["run", name],
|
|
4874
|
+
await this.#execute("bun", ["run", name], result2.plan.projectDirectory);
|
|
4417
4875
|
steps.push({ name: `bun run ${name}`, status: "passed" });
|
|
4418
4876
|
}
|
|
4419
|
-
await finalize(
|
|
4420
|
-
const reportPath = `.windy/reports/${
|
|
4421
|
-
await writeReport(
|
|
4422
|
-
await clearUpdateState(
|
|
4423
|
-
await rm7(
|
|
4424
|
-
return { updateId:
|
|
4877
|
+
await finalize(result2);
|
|
4878
|
+
const reportPath = `.windy/reports/${result2.plan.id}.md`;
|
|
4879
|
+
await writeReport(result2, steps, reportPath);
|
|
4880
|
+
await clearUpdateState(result2.plan.projectDirectory);
|
|
4881
|
+
await rm7(result2.plan.temporaryRoot, { recursive: true, force: true });
|
|
4882
|
+
return { updateId: result2.plan.id, status: "passed", steps, reportPath };
|
|
4425
4883
|
} catch (error) {
|
|
4426
|
-
await restoreUpdateState(
|
|
4884
|
+
await restoreUpdateState(result2.plan.projectDirectory, {
|
|
4427
4885
|
schemaVersion: 1,
|
|
4428
|
-
updateId:
|
|
4429
|
-
backupDirectory:
|
|
4430
|
-
entries:
|
|
4886
|
+
updateId: result2.plan.id,
|
|
4887
|
+
backupDirectory: result2.backupDirectory,
|
|
4888
|
+
entries: result2.backupEntries
|
|
4431
4889
|
});
|
|
4432
|
-
await rm7(
|
|
4433
|
-
throw new Error(`更新验证失败,项目已回滚到 ${
|
|
4890
|
+
await rm7(result2.plan.temporaryRoot, { recursive: true, force: true });
|
|
4891
|
+
throw new Error(`更新验证失败,项目已回滚到 ${result2.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4434
4892
|
}
|
|
4435
4893
|
}
|
|
4436
4894
|
}
|
|
4437
|
-
async function finalize(
|
|
4438
|
-
const root =
|
|
4439
|
-
const generationContent = `${JSON.stringify(
|
|
4895
|
+
async function finalize(result2) {
|
|
4896
|
+
const root = result2.plan.projectDirectory;
|
|
4897
|
+
const generationContent = `${JSON.stringify(result2.plan.targetProvenance, null, 2)}
|
|
4440
4898
|
`;
|
|
4441
|
-
await writeFile11(
|
|
4899
|
+
await writeFile11(join16(root, ".windy/generation.json"), generationContent);
|
|
4442
4900
|
const refreshedPaths = new Set([
|
|
4443
4901
|
".windy/generation.json",
|
|
4444
|
-
...
|
|
4902
|
+
...result2.plan.files.filter(({ action }) => action === "create" || action === "update").map(({ path: path2 }) => path2)
|
|
4445
4903
|
]);
|
|
4446
|
-
const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await
|
|
4904
|
+
const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile15(join16(root, path2)))])));
|
|
4447
4905
|
const targetManifest = {
|
|
4448
|
-
...
|
|
4449
|
-
files:
|
|
4906
|
+
...result2.plan.targetManifest,
|
|
4907
|
+
files: result2.plan.targetManifest.files.map((entry) => ({
|
|
4450
4908
|
...entry,
|
|
4451
4909
|
hash: refreshedHashes.get(entry.path) ?? entry.hash
|
|
4452
4910
|
}))
|
|
4453
4911
|
};
|
|
4454
|
-
await writeFile11(
|
|
4912
|
+
await writeFile11(join16(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
|
|
4455
4913
|
`);
|
|
4456
4914
|
}
|
|
4457
|
-
async function writeReport(
|
|
4458
|
-
const path2 =
|
|
4915
|
+
async function writeReport(result2, steps, relativePath2) {
|
|
4916
|
+
const path2 = join16(result2.plan.projectDirectory, relativePath2);
|
|
4459
4917
|
await mkdir7(dirname4(path2), { recursive: true });
|
|
4460
4918
|
const lines = [
|
|
4461
|
-
`# Windy 更新报告 ${
|
|
4919
|
+
`# Windy 更新报告 ${result2.plan.id}`,
|
|
4462
4920
|
"",
|
|
4463
|
-
`- 来源版本:${
|
|
4464
|
-
`- 目标版本:${
|
|
4465
|
-
`- Recipe:${
|
|
4466
|
-
`- 修改文件:${
|
|
4467
|
-
`- Oxc 代管:${
|
|
4921
|
+
`- 来源版本:${result2.plan.sourceVersion}`,
|
|
4922
|
+
`- 目标版本:${result2.plan.targetVersion}`,
|
|
4923
|
+
`- Recipe:${result2.plan.recipes.join("、") || "无"}`,
|
|
4924
|
+
`- 修改文件:${result2.changedFiles.length}`,
|
|
4925
|
+
`- Oxc 代管:${result2.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
|
|
4468
4926
|
"",
|
|
4469
4927
|
"## 验证",
|
|
4470
4928
|
"",
|
|
4471
4929
|
...steps.map(({ name, status }) => `- ${name}:${status}`),
|
|
4472
4930
|
"",
|
|
4473
|
-
`备份保留在 \`.windy/backups/${
|
|
4931
|
+
`备份保留在 \`.windy/backups/${result2.plan.id}\`,确认稳定后可人工删除。`,
|
|
4474
4932
|
""
|
|
4475
4933
|
];
|
|
4476
4934
|
await writeFile11(path2, lines.join(`
|
|
@@ -4482,7 +4940,7 @@ async function runLifecycleCommand(args) {
|
|
|
4482
4940
|
const command = args[0];
|
|
4483
4941
|
if (command !== "doctor" && command !== "update")
|
|
4484
4942
|
return false;
|
|
4485
|
-
const projectDirectory =
|
|
4943
|
+
const projectDirectory = resolve5(readValue2(args, "--cwd") || process.cwd());
|
|
4486
4944
|
if (command === "doctor") {
|
|
4487
4945
|
const report = await diagnoseProject(projectDirectory, args.includes("--repair"));
|
|
4488
4946
|
printDoctor(report);
|
|
@@ -4534,8 +4992,8 @@ async function runUpdate(args, projectDirectory) {
|
|
|
4534
4992
|
await rm8(plan.temporaryRoot, { recursive: true, force: true });
|
|
4535
4993
|
throw new Error("存在更新冲突;请按上方清单处理后重新运行,项目未被修改");
|
|
4536
4994
|
}
|
|
4537
|
-
const
|
|
4538
|
-
const report = await updater.verify(
|
|
4995
|
+
const result2 = await updater.apply(plan);
|
|
4996
|
+
const report = await updater.verify(result2);
|
|
4539
4997
|
console.log(`
|
|
4540
4998
|
更新完成:${plan.sourceVersion} → ${plan.targetVersion}`);
|
|
4541
4999
|
console.log(`报告:${report.reportPath}`);
|