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.
Files changed (40) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.js +534 -76
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +11 -1
  6. package/template/README.md +8 -5
  7. package/template/apps/server/Dockerfile +3 -3
  8. package/template/apps/server/src/index.ts +8 -14
  9. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +57 -0
  10. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +62 -0
  11. package/template/apps/server/src/settings/runtime.ts +30 -3
  12. package/template/apps/server/src/settings/watermark-repository.ts +32 -0
  13. package/template/apps/server/src/settings/watermark-routes.test.ts +203 -0
  14. package/template/apps/server/src/settings/watermark-routes.ts +134 -0
  15. package/template/apps/web/runtime-server.test.ts +4 -4
  16. package/template/apps/web/src/composables/useWatermarkSettings.ts +75 -33
  17. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +86 -0
  18. package/template/apps/web/src/composables/watermark-settings.ts +9 -35
  19. package/template/apps/web/src/composables/watermark-settings.webtest.ts +8 -32
  20. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +81 -0
  21. package/template/apps/web/src/layout/GlobalWatermark.vue +34 -31
  22. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +58 -7
  23. package/template/apps/web/src/pages/settings/GlobalSettingsPage.vue +1 -1
  24. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +40 -18
  25. package/template/apps/web/src/services/platform-settings-api.ts +25 -2
  26. package/template/apps/web/vite.config.ts +3 -1
  27. package/template/docker-compose.yml +19 -6
  28. package/template/docs/platform/platform-shell-settings.md +24 -3
  29. package/template/docs/platform/web-system-crud.md +2 -2
  30. package/template/drizzle.config.ts +1 -1
  31. package/template/package.json +4 -1
  32. package/template/packages/database/drizzle/0031_panoramic_typhoid_mary.sql +12 -0
  33. package/template/packages/database/drizzle/meta/0031_snapshot.json +7387 -0
  34. package/template/packages/database/drizzle/meta/_journal.json +8 -1
  35. package/template/packages/database/src/schema/platform-settings.ts +16 -1
  36. package/template/packages/modules/src/system-api-permissions.ts +4 -0
  37. package/template/packages/modules/src/system-api-watermark.ts +26 -0
  38. package/template/packages/modules/src/system-features.ts +1 -1
  39. package/template/packages/modules/src/system-watermark-policy.test.ts +31 -0
  40. 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: ["platform_branding_settings"]
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
- - "5432:5432"
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: 3000
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
- - "3000:3000"
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: 9746
1361
- API_UPSTREAM: http://server:3000
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
- - "9746:9746"
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:9746 |
1443
- | Server 健康检查 | http://localhost:3000/api/health |
1444
- | PostgreSQL | localhost:5432 |
1469
+ | Web | http://localhost:18746 |
1470
+ | Server 健康检查 | http://localhost:9747/api/health |
1471
+ | PostgreSQL | localhost:15432 |
1445
1472
 
1446
- Docker Web 固定使用 \`9746\`。只启动单个应用时使用:
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 --build",
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 resolve4 } from "node:path";
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 readFile14, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
4042
- import { dirname as dirname4, join as join15 } from "node:path";
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 readFile13 } from "node:fs/promises";
4105
- import { join as join14 } from "node:path";
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
- return { conflict: "三方合并存在重叠修改" };
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, targetManifest] = await Promise.all([
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 file = await planFile(path2, request.projectDirectory, artifactSet.baseDirectory, artifactSet.targetDirectory, sourceEntries.get(path2), targetEntries.get(path2), provenance);
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 = files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : []);
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(join14(project, path2)),
4279
- readOptional(join14(baseRoot, path2)),
4280
- readOptional(join14(targetRoot, path2))
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 = target.hash !== source.hash;
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 === "package.json" && base) {
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 readFile13(path2);
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 result = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4367
- if (result.code !== 0)
4824
+ const result2 = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4825
+ if (result2.code !== 0)
4368
4826
  throw new Error("更新要求当前目录是有效的 Git 仓库");
4369
- if (result.stdout.trim())
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(join14(projectDirectory, ".windy/update-state.json"));
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(result) {
4863
+ async verify(result2) {
4406
4864
  const steps = [];
4407
4865
  try {
4408
- await this.#execute("bun", ["install", "--force"], result.plan.projectDirectory);
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 readFile14(join15(result.plan.projectDirectory, "package.json"), "utf8"));
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], result.plan.projectDirectory);
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(result);
4420
- const reportPath = `.windy/reports/${result.plan.id}.md`;
4421
- await writeReport(result, steps, reportPath);
4422
- await clearUpdateState(result.plan.projectDirectory);
4423
- await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4424
- return { updateId: result.plan.id, status: "passed", steps, reportPath };
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(result.plan.projectDirectory, {
4884
+ await restoreUpdateState(result2.plan.projectDirectory, {
4427
4885
  schemaVersion: 1,
4428
- updateId: result.plan.id,
4429
- backupDirectory: result.backupDirectory,
4430
- entries: result.backupEntries
4886
+ updateId: result2.plan.id,
4887
+ backupDirectory: result2.backupDirectory,
4888
+ entries: result2.backupEntries
4431
4889
  });
4432
- await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4433
- throw new Error(`更新验证失败,项目已回滚到 ${result.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: 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(result) {
4438
- const root = result.plan.projectDirectory;
4439
- const generationContent = `${JSON.stringify(result.plan.targetProvenance, null, 2)}
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(join15(root, ".windy/generation.json"), generationContent);
4899
+ await writeFile11(join16(root, ".windy/generation.json"), generationContent);
4442
4900
  const refreshedPaths = new Set([
4443
4901
  ".windy/generation.json",
4444
- ...result.plan.files.filter(({ action }) => action === "create" || action === "update").map(({ path: path2 }) => path2)
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 readFile14(join15(root, path2)))])));
4904
+ const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile15(join16(root, path2)))])));
4447
4905
  const targetManifest = {
4448
- ...result.plan.targetManifest,
4449
- files: result.plan.targetManifest.files.map((entry) => ({
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(join15(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
4912
+ await writeFile11(join16(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
4455
4913
  `);
4456
4914
  }
4457
- async function writeReport(result, steps, relativePath2) {
4458
- const path2 = join15(result.plan.projectDirectory, relativePath2);
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 更新报告 ${result.plan.id}`,
4919
+ `# Windy 更新报告 ${result2.plan.id}`,
4462
4920
  "",
4463
- `- 来源版本:${result.plan.sourceVersion}`,
4464
- `- 目标版本:${result.plan.targetVersion}`,
4465
- `- Recipe:${result.plan.recipes.join("、") || "无"}`,
4466
- `- 修改文件:${result.changedFiles.length}`,
4467
- `- Oxc 代管:${result.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
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/${result.plan.id}\`,确认稳定后可人工删除。`,
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 = resolve4(readValue2(args, "--cwd") || process.cwd());
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 result = await updater.apply(plan);
4538
- const report = await updater.verify(result);
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}`);