create-windy 0.2.18 → 0.2.20

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 (84) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +752 -186
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/apps/server/src/application-services.ts +2 -2
  6. package/template/apps/server/src/audit/search-summary.ts +15 -8
  7. package/template/apps/server/src/example-modules.ts +58 -70
  8. package/template/apps/server/src/foundation.ts +8 -4
  9. package/template/apps/server/src/guards.test.ts +13 -0
  10. package/template/apps/server/src/guards.ts +12 -2
  11. package/template/apps/server/src/index.ts +48 -91
  12. package/template/apps/server/src/installed-business-modules.ts +23 -4
  13. package/template/apps/server/src/module-bootstrap.ts +83 -0
  14. package/template/apps/server/src/module-composition.test.ts +45 -1
  15. package/template/apps/server/src/module-composition.ts +45 -0
  16. package/template/apps/server/src/module-host/initialize.test.ts +123 -0
  17. package/template/apps/server/src/module-host/initialize.ts +331 -0
  18. package/template/apps/server/src/module-host/request-context.test.ts +66 -0
  19. package/template/apps/server/src/module-host/request-context.ts +156 -0
  20. package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
  21. package/template/apps/server/src/module-host/role-presets.ts +76 -0
  22. package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
  23. package/template/apps/server/src/module-host/route-installer.ts +97 -0
  24. package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
  25. package/template/apps/server/src/module-host/search-providers.ts +31 -0
  26. package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
  27. package/template/apps/server/src/module-host/storage-port.ts +59 -0
  28. package/template/apps/server/src/module-host/task-definitions.ts +41 -0
  29. package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
  30. package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
  31. package/template/apps/server/src/module-host.test.ts +202 -58
  32. package/template/apps/server/src/module-host.ts +34 -113
  33. package/template/apps/server/src/route-guards.test.ts +112 -1
  34. package/template/apps/server/src/runtime-feature.ts +66 -31
  35. package/template/apps/server/src/runtime.test.ts +1 -0
  36. package/template/apps/server/src/runtime.ts +3 -1
  37. package/template/apps/server/src/system/built-in-roles.ts +1 -1
  38. package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
  39. package/template/apps/server/src/work-order/routes.test.ts +30 -18
  40. package/template/apps/server/src/work-order/routes.ts +104 -135
  41. package/template/apps/server/src/work-order/search-api.test.ts +14 -3
  42. package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
  43. package/template/apps/server/src/work-order/search-provider.ts +17 -12
  44. package/template/apps/server/src/work-order/task.test.ts +16 -11
  45. package/template/apps/web/Dockerfile +4 -1
  46. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
  47. package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
  48. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
  49. package/template/docs/architecture/object-storage.md +12 -0
  50. package/template/packages/config/index.test.ts +57 -0
  51. package/template/packages/config/package.json +1 -1
  52. package/template/packages/config/src/platform.ts +28 -1
  53. package/template/packages/database/package.json +1 -1
  54. package/template/packages/database/src/runner.ts +7 -9
  55. package/template/packages/example-work-order/index.ts +1 -0
  56. package/template/packages/example-work-order/package.json +2 -0
  57. package/template/packages/example-work-order/src/server-module.ts +61 -0
  58. package/template/packages/jobs/package.json +1 -1
  59. package/template/packages/jobs/src/types.ts +7 -1
  60. package/template/packages/modules/package.json +1 -1
  61. package/template/packages/modules/src/catalog-validation.test.ts +212 -0
  62. package/template/packages/modules/src/catalog-validation.ts +19 -2
  63. package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
  64. package/template/packages/server-sdk/index.ts +42 -0
  65. package/template/packages/server-sdk/package.json +16 -3
  66. package/template/packages/server-sdk/src/actor.ts +15 -0
  67. package/template/packages/server-sdk/src/audit-port.ts +22 -0
  68. package/template/packages/server-sdk/src/jobs-port.ts +24 -0
  69. package/template/packages/server-sdk/src/module-host.ts +36 -1
  70. package/template/packages/server-sdk/src/namespace.test.ts +26 -0
  71. package/template/packages/server-sdk/src/namespace.ts +13 -0
  72. package/template/packages/server-sdk/src/request-context.ts +21 -0
  73. package/template/packages/server-sdk/src/role-preset.ts +18 -0
  74. package/template/packages/server-sdk/src/route-definition.ts +26 -0
  75. package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
  76. package/template/packages/server-sdk/src/storage-port.ts +25 -0
  77. package/template/packages/server-sdk/src/task-registration.ts +18 -0
  78. package/template/packages/server-sdk/src/tool-registration.ts +29 -0
  79. package/template/packages/shared/index.ts +1 -0
  80. package/template/packages/shared/package.json +1 -1
  81. package/template/packages/shared/src/license-catalog.test.ts +73 -1
  82. package/template/packages/shared/src/license-catalog.ts +95 -0
  83. package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
  84. package/template/apps/server/src/work-order/task.ts +0 -30
package/dist/cli.js CHANGED
@@ -727,7 +727,7 @@ var helpText = `create-windy <项目名> [选项]
727
727
  `;
728
728
 
729
729
  // src/create-project.ts
730
- import { mkdir as mkdir4, readdir as readdir5, rm as rm4 } from "node:fs/promises";
730
+ import { mkdir as mkdir4, readdir as readdir6, rm as rm4 } from "node:fs/promises";
731
731
 
732
732
  // src/commands.ts
733
733
  import { spawn } from "node:child_process";
@@ -920,16 +920,147 @@ import {
920
920
  copyFile,
921
921
  lstat as lstat4,
922
922
  mkdir as mkdir3,
923
- readFile as readFile9,
924
- readdir as readdir4,
923
+ readFile as readFile10,
924
+ readdir as readdir5,
925
925
  rm as rm3,
926
- writeFile as writeFile8
926
+ writeFile as writeFile9
927
927
  } from "node:fs/promises";
928
- import { basename as basename3, extname as extname3, join as join9 } from "node:path";
928
+ import { basename as basename3, extname as extname3, join as join10 } from "node:path";
929
929
 
930
930
  // src/customer-docker.ts
931
- import { readFile as readFile2, writeFile } from "node:fs/promises";
931
+ import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
932
+ import { join as join3 } from "node:path";
933
+
934
+ // src/docker-copy-sync.ts
935
+ import { readdir, readFile as readFile2, writeFile } from "node:fs/promises";
932
936
  import { join as join2 } from "node:path";
937
+ var webDockerfilePath = "apps/web/Dockerfile";
938
+ var webEntryDirectory = "apps/web";
939
+ var depsCopyAnchor = "windy-copy:deps";
940
+ var buildCopyAnchor = "windy-copy:build";
941
+ async function listWorkspacePackages(root) {
942
+ const manifest = await readManifest(join2(root, "package.json"));
943
+ const packages = [];
944
+ for (const pattern of manifest.workspaces ?? []) {
945
+ if (!pattern.endsWith("/*")) {
946
+ throw new Error(`暂不支持的 workspace pattern: ${pattern}`);
947
+ }
948
+ const base = pattern.slice(0, -2);
949
+ const entries = await readdir(join2(root, base), {
950
+ withFileTypes: true
951
+ }).catch((error) => {
952
+ if (isMissing(error))
953
+ return [];
954
+ throw error;
955
+ });
956
+ for (const entry of entries) {
957
+ if (!entry.isDirectory())
958
+ continue;
959
+ const directory = `${base}/${entry.name}`;
960
+ const manifestPath = join2(root, directory, "package.json");
961
+ if (!await Bun.file(manifestPath).exists())
962
+ continue;
963
+ const value = await readManifest(manifestPath);
964
+ packages.push({
965
+ name: value.name,
966
+ directory,
967
+ dependencies: [
968
+ ...Object.keys(value.dependencies ?? {}),
969
+ ...Object.keys(value.devDependencies ?? {})
970
+ ]
971
+ });
972
+ }
973
+ }
974
+ return packages.sort((a, b) => a.directory.localeCompare(b.directory));
975
+ }
976
+ function workspaceDependencyClosure(packages, entryDirectory) {
977
+ const directoryByName = new Map;
978
+ const packageByDirectory = new Map;
979
+ for (const pkg of packages) {
980
+ if (pkg.name)
981
+ directoryByName.set(pkg.name, pkg.directory);
982
+ packageByDirectory.set(pkg.directory, pkg);
983
+ }
984
+ const visited = new Set([entryDirectory]);
985
+ const queue = [entryDirectory];
986
+ while (queue.length) {
987
+ const current = packageByDirectory.get(queue.shift());
988
+ if (!current)
989
+ continue;
990
+ for (const dependency of current.dependencies) {
991
+ const directory = directoryByName.get(dependency);
992
+ if (directory && !visited.has(directory)) {
993
+ visited.add(directory);
994
+ queue.push(directory);
995
+ }
996
+ }
997
+ }
998
+ return [...visited].sort((a, b) => a.localeCompare(b));
999
+ }
1000
+ function dockerCopyLines(directories) {
1001
+ return directories.map((directory) => `COPY ${directory} ${directory}`);
1002
+ }
1003
+ function dockerManifestCopyLines(directories) {
1004
+ return directories.map((directory) => `COPY ${directory}/package.json ${directory}/package.json`);
1005
+ }
1006
+ function rewriteAnchoredSection(source, anchor, lines) {
1007
+ const { begin, end } = locateAnchoredSection(source, anchor);
1008
+ const all = source.split(`
1009
+ `);
1010
+ return [...all.slice(0, begin + 1), ...lines, ...all.slice(end)].join(`
1011
+ `);
1012
+ }
1013
+ function hasCopyAnchors(source) {
1014
+ return source.includes(`# ${depsCopyAnchor} begin`) && source.includes(`# ${buildCopyAnchor} begin`);
1015
+ }
1016
+ function syncWebDockerfileSource(source, workspaceDirectories, buildDirectories) {
1017
+ const withDeps = rewriteAnchoredSection(source, depsCopyAnchor, dockerManifestCopyLines(workspaceDirectories));
1018
+ return rewriteAnchoredSection(withDeps, buildCopyAnchor, dockerCopyLines(buildDirectories));
1019
+ }
1020
+ async function syncWebDockerfileInProject(root, options = {}) {
1021
+ const path = join2(root, webDockerfilePath);
1022
+ let source;
1023
+ try {
1024
+ source = await readFile2(path, "utf8");
1025
+ } catch (error) {
1026
+ if (isMissing(error))
1027
+ return { synced: false, changed: false };
1028
+ throw error;
1029
+ }
1030
+ if (!hasCopyAnchors(source)) {
1031
+ if (!options.requireAnchors)
1032
+ return { synced: false, changed: false };
1033
+ throw new Error(`${webDockerfilePath} 缺少 # ${depsCopyAnchor} / # ${buildCopyAnchor} 锚点段,` + `请先按最新模板为 deps 与 build 阶段的 COPY 段添加锚点后再同步`);
1034
+ }
1035
+ const packages = await listWorkspacePackages(root);
1036
+ const next = syncWebDockerfileSource(source, packages.map(({ directory }) => directory), workspaceDependencyClosure(packages, webEntryDirectory));
1037
+ if (next !== source)
1038
+ await writeFile(path, next);
1039
+ return { synced: true, changed: next !== source };
1040
+ }
1041
+ function locateAnchoredSection(source, anchor) {
1042
+ const beginMarker = `# ${anchor} begin`;
1043
+ const endMarker = `# ${anchor} end`;
1044
+ const lines = source.split(`
1045
+ `);
1046
+ const begin = lines.indexOf(beginMarker);
1047
+ const end = lines.indexOf(endMarker);
1048
+ if (begin === -1 || end === -1 || end <= begin) {
1049
+ throw new Error(`Dockerfile 缺少锚点段 ${beginMarker} ... ${endMarker},请先迁移到锚点格式`);
1050
+ }
1051
+ if (lines.indexOf(beginMarker, begin + 1) !== -1 || lines.indexOf(endMarker, end + 1) !== -1) {
1052
+ throw new Error(`Dockerfile 锚点段 ${anchor} 重复,请保留唯一一段`);
1053
+ }
1054
+ return { begin, end };
1055
+ }
1056
+ async function readManifest(path) {
1057
+ return JSON.parse(await readFile2(path, "utf8"));
1058
+ }
1059
+ function isMissing(error) {
1060
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1061
+ }
1062
+
1063
+ // src/customer-docker.ts
933
1064
  var supplierWorkspaceCopies = [
934
1065
  "COPY apps/license/package.json apps/license/package.json",
935
1066
  "COPY apps/signing-service/package.json apps/signing-service/package.json",
@@ -937,11 +1068,12 @@ var supplierWorkspaceCopies = [
937
1068
  ];
938
1069
  var supplierWorkspacePaths = ["packages/license-sdk"];
939
1070
  async function prepareCustomerDockerfiles(root) {
940
- await Promise.all(["apps/server/Dockerfile", "apps/web/Dockerfile"].map(async (path) => {
941
- const absolutePath = join2(root, path);
942
- const source = await readFile2(absolutePath, "utf8");
943
- await writeFile(absolutePath, rewriteCustomerDockerfile(source));
1071
+ await Promise.all(["apps/server/Dockerfile", webDockerfilePath].map(async (path) => {
1072
+ const absolutePath = join3(root, path);
1073
+ const source = await readFile3(absolutePath, "utf8");
1074
+ await writeFile2(absolutePath, rewriteCustomerDockerfile(source));
944
1075
  }));
1076
+ await syncWebDockerfileInProject(root, { requireAnchors: true });
945
1077
  }
946
1078
  function rewriteCustomerDockerfile(source) {
947
1079
  const lines = source.split(`
@@ -953,8 +1085,8 @@ function rewriteCustomerDockerfile(source) {
953
1085
  }
954
1086
 
955
1087
  // src/code-quality.ts
956
- import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
957
- import { join as join3 } from "node:path";
1088
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1089
+ import { join as join4 } from "node:path";
958
1090
  var oxcStarterPolicy = [
959
1091
  "- 生成时默认预装 Oxlint、Oxfmt、lint-staged 与 Husky。",
960
1092
  "- 提交前自动格式化暂存文件;lint、typecheck 和 test 保留为显式命令。",
@@ -963,30 +1095,30 @@ var oxcStarterPolicy = [
963
1095
  `);
964
1096
  var noCodeQualityPolicy = "- 本项目生成时未引入 lint 或 format 工具;项目所有者可按团队需要自行选择。";
965
1097
  async function reflectCodeQualityChoice(targetDirectory, includeOxc) {
966
- const path = join3(targetDirectory, "AGENTS.md");
967
- const source = await readFile3(path, "utf8");
1098
+ const path = join4(targetDirectory, "AGENTS.md");
1099
+ const source = await readFile4(path, "utf8");
968
1100
  if (includeOxc)
969
1101
  return;
970
1102
  if (!source.includes(oxcStarterPolicy)) {
971
1103
  throw new Error("无法同步 --no-oxc 的项目开发约束");
972
1104
  }
973
- await writeFile2(path, source.replace(oxcStarterPolicy, noCodeQualityPolicy));
1105
+ await writeFile3(path, source.replace(oxcStarterPolicy, noCodeQualityPolicy));
974
1106
  }
975
1107
 
976
1108
  // src/license-files.ts
977
- import { mkdir, readFile as readFile4, rm, writeFile as writeFile3 } from "node:fs/promises";
978
- import { dirname, join as join4, resolve as resolve2 } from "node:path";
1109
+ import { mkdir, readFile as readFile5, rm, writeFile as writeFile4 } from "node:fs/promises";
1110
+ import { dirname, join as join5, resolve as resolve2 } from "node:path";
979
1111
  import { fileURLToPath } from "node:url";
980
1112
  var packageRoot = resolve2(dirname(fileURLToPath(import.meta.url)), "..");
981
1113
  var apacheLicensePath = resolve2(packageRoot, "LICENSE");
982
1114
  var windyLicensePath = ".windy/licenses/WINDY-APACHE-2.0.txt";
983
1115
  async function applyWindyBaseLicense(targetDirectory) {
984
- const target = join4(targetDirectory, windyLicensePath);
1116
+ const target = join5(targetDirectory, windyLicensePath);
985
1117
  await mkdir(dirname(target), { recursive: true });
986
- await writeFile3(target, await readFile4(apacheLicensePath));
1118
+ await writeFile4(target, await readFile5(apacheLicensePath));
987
1119
  }
988
1120
  async function applyProjectLicense(targetDirectory, license, holder, year = new Date().getFullYear()) {
989
- const target = join4(targetDirectory, "LICENSE");
1121
+ const target = join5(targetDirectory, "LICENSE");
990
1122
  if (license === "UNLICENSED") {
991
1123
  await rm(target, { force: true });
992
1124
  return;
@@ -996,11 +1128,11 @@ async function applyProjectLicense(targetDirectory, license, holder, year = new
996
1128
  if (!owner)
997
1129
  throw new Error("选择 MIT 时必须填写许可证权利人名称");
998
1130
  await mkdir(targetDirectory, { recursive: true });
999
- await writeFile3(target, mitLicense(year, owner));
1131
+ await writeFile4(target, mitLicense(year, owner));
1000
1132
  return;
1001
1133
  }
1002
1134
  await mkdir(targetDirectory, { recursive: true });
1003
- await writeFile3(target, await readFile4(apacheLicensePath));
1135
+ await writeFile4(target, await readFile5(apacheLicensePath));
1004
1136
  }
1005
1137
  function mitLicense(year, holder) {
1006
1138
  return `MIT License
@@ -1029,8 +1161,8 @@ SOFTWARE.
1029
1161
 
1030
1162
  // src/managed-files.ts
1031
1163
  import { createHash } from "node:crypto";
1032
- import { lstat, readdir, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
1033
- import { join as join5, relative, sep } from "node:path";
1164
+ import { lstat, readdir as readdir2, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
1165
+ import { join as join6, relative, sep } from "node:path";
1034
1166
  var managedFilesManifestPath = ".windy/files.json";
1035
1167
  var ignoredDirectoryNames = new Set([".git", "node_modules", "dist"]);
1036
1168
  var ignoredFiles = new Set([
@@ -1040,6 +1172,7 @@ var ignoredFiles = new Set([
1040
1172
  ]);
1041
1173
  var projectOwnedRoots = [
1042
1174
  "apps/web/src/app",
1175
+ "apps/server/src/installed-business-modules.ts",
1043
1176
  "apps/server/src/work-order",
1044
1177
  "packages/example-work-order"
1045
1178
  ];
@@ -1048,13 +1181,18 @@ var sharedScaffoldRoots = [
1048
1181
  "AGENTS.md",
1049
1182
  ".env.example",
1050
1183
  "docker-compose.yml",
1184
+ "drizzle.config.ts",
1185
+ "apps/server/Dockerfile",
1186
+ "apps/server/src/index.ts",
1187
+ "apps/web/Dockerfile",
1051
1188
  "apps/web/src/layout",
1052
- "apps/web/src/router"
1189
+ "apps/web/src/router",
1190
+ "packages/database/src/schema/index.ts"
1053
1191
  ];
1054
1192
  async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
1055
1193
  const paths = await collectFiles(targetDirectory);
1056
1194
  const files = await Promise.all(paths.map(async (path) => {
1057
- const content = await readFile5(join5(targetDirectory, path));
1195
+ const content = await readFile6(join6(targetDirectory, path));
1058
1196
  return {
1059
1197
  path,
1060
1198
  ownership: classifyOwnership(path),
@@ -1067,11 +1205,11 @@ async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
1067
1205
  algorithm: "sha256",
1068
1206
  files
1069
1207
  };
1070
- await writeFile4(join5(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
1208
+ await writeFile5(join6(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
1071
1209
  `);
1072
1210
  }
1073
1211
  async function readManagedFilesManifest(targetDirectory) {
1074
- const parsed = JSON.parse(await readFile5(join5(targetDirectory, managedFilesManifestPath), "utf8"));
1212
+ const parsed = JSON.parse(await readFile6(join6(targetDirectory, managedFilesManifestPath), "utf8"));
1075
1213
  if (!isManagedFilesManifest(parsed)) {
1076
1214
  throw new Error("受管文件清单无效,无法安全更新");
1077
1215
  }
@@ -1092,10 +1230,10 @@ async function collectFiles(root) {
1092
1230
  await visit(root, files);
1093
1231
  return files.sort();
1094
1232
  async function visit(directory, output) {
1095
- for (const name of (await readdir(directory)).sort()) {
1233
+ for (const name of (await readdir2(directory)).sort()) {
1096
1234
  if (ignoredDirectoryNames.has(name))
1097
1235
  continue;
1098
- const path = join5(directory, name);
1236
+ const path = join6(directory, name);
1099
1237
  const metadata = await lstat(path);
1100
1238
  if (metadata.isSymbolicLink())
1101
1239
  continue;
@@ -1129,8 +1267,8 @@ function isManagedFilesManifest(value) {
1129
1267
  }
1130
1268
 
1131
1269
  // src/module-materializer.ts
1132
- import { lstat as lstat2, readFile as readFile6, readdir as readdir2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
1133
- import { basename, extname, join as join6, relative as relative2, sep as sep2 } from "node:path";
1270
+ import { lstat as lstat2, readFile as readFile7, readdir as readdir3, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
1271
+ import { basename, extname, join as join7, relative as relative2, sep as sep2 } from "node:path";
1134
1272
  var textExtensions = new Set([
1135
1273
  ".css",
1136
1274
  ".html",
@@ -1163,18 +1301,18 @@ async function materializeSelectedModules(targetDirectory, selectedNames) {
1163
1301
  ...manifest.documentation,
1164
1302
  ...manifest.schemaFiles
1165
1303
  ]);
1166
- await Promise.all([...new Set(removedRoots)].map((path) => rm2(join6(targetDirectory, path), { recursive: true, force: true })));
1304
+ await Promise.all([...new Set(removedRoots)].map((path) => rm2(join7(targetDirectory, path), { recursive: true, force: true })));
1167
1305
  await pruneWorkspaceReferences(targetDirectory, removedPackageNames, removedRoots);
1168
1306
  }
1169
1307
  async function stripModuleBlocks(root, selected) {
1170
1308
  for (const path of await collectFiles2(root)) {
1171
1309
  if (!textExtensions.has(extname(path)))
1172
1310
  continue;
1173
- const source = await readFile6(path, "utf8");
1311
+ const source = await readFile7(path, "utf8");
1174
1312
  if (!source.includes("@windy-module"))
1175
1313
  continue;
1176
1314
  const output = stripTaggedSource(source, selected, relativePath(root, path));
1177
- await writeFile5(path, output);
1315
+ await writeFile6(path, output);
1178
1316
  }
1179
1317
  }
1180
1318
  function stripTaggedSource(source, selected, path = "source") {
@@ -1207,11 +1345,11 @@ async function readRemovedPackageNames(root, roots) {
1207
1345
  const names = new Set;
1208
1346
  for (const packageRoot2 of roots) {
1209
1347
  try {
1210
- const value = JSON.parse(await readFile6(join6(root, packageRoot2, "package.json"), "utf8"));
1348
+ const value = JSON.parse(await readFile7(join7(root, packageRoot2, "package.json"), "utf8"));
1211
1349
  if (value.name)
1212
1350
  names.add(value.name);
1213
1351
  } catch (error) {
1214
- if (!isMissing(error))
1352
+ if (!isMissing2(error))
1215
1353
  throw error;
1216
1354
  }
1217
1355
  }
@@ -1220,7 +1358,7 @@ async function readRemovedPackageNames(root, roots) {
1220
1358
  async function pruneWorkspaceReferences(root, removedPackages, removedRoots) {
1221
1359
  for (const path of await collectFiles2(root)) {
1222
1360
  if (basename(path) === "package.json") {
1223
- const value = JSON.parse(await readFile6(path, "utf8"));
1361
+ const value = JSON.parse(await readFile7(path, "utf8"));
1224
1362
  let changed = false;
1225
1363
  for (const section of ["dependencies", "devDependencies"]) {
1226
1364
  const dependencies = value[section];
@@ -1232,45 +1370,45 @@ async function pruneWorkspaceReferences(root, removedPackages, removedRoots) {
1232
1370
  }
1233
1371
  }
1234
1372
  if (changed)
1235
- await writeFile5(path, `${JSON.stringify(value, null, 2)}
1373
+ await writeFile6(path, `${JSON.stringify(value, null, 2)}
1236
1374
  `);
1237
1375
  continue;
1238
1376
  }
1239
1377
  if (basename(path) !== "Dockerfile")
1240
1378
  continue;
1241
- const source = await readFile6(path, "utf8");
1379
+ const source = await readFile7(path, "utf8");
1242
1380
  const output = source.split(`
1243
1381
  `).filter((line) => !removedRoots.some((root2) => line.includes(root2))).join(`
1244
1382
  `);
1245
1383
  if (output !== source)
1246
- await writeFile5(path, output);
1384
+ await writeFile6(path, output);
1247
1385
  }
1248
1386
  }
1249
1387
  async function pruneDatabaseArtifacts(root, tableNames, statementTokens) {
1250
1388
  if (!tableNames.length && !statementTokens.length)
1251
1389
  return;
1252
1390
  const tablePatterns = tableNames.map((name) => new RegExp(`(?:"${escapeRegExp(name)}"|\\b${escapeRegExp(name)}\\b)`));
1253
- const drizzleRoot = join6(root, "packages/database/drizzle");
1391
+ const drizzleRoot = join7(root, "packages/database/drizzle");
1254
1392
  try {
1255
1393
  for (const path of await collectFiles2(drizzleRoot)) {
1256
1394
  if (path.endsWith(".sql")) {
1257
- const source = await readFile6(path, "utf8");
1395
+ const source = await readFile7(path, "utf8");
1258
1396
  const statements = source.split("--> statement-breakpoint").map((statement) => statement.trim()).filter(Boolean).filter((statement) => !tablePatterns.some((pattern) => pattern.test(statement)) && !statementTokens.some((token) => statement.includes(token)));
1259
- await writeFile5(path, statements.length ? `${statements.join(`
1397
+ await writeFile6(path, statements.length ? `${statements.join(`
1260
1398
  --> statement-breakpoint
1261
1399
  `)}
1262
1400
  ` : `-- 此 migration 的模块未被当前项目选择。
1263
1401
  `);
1264
1402
  } else if (path.endsWith("_snapshot.json")) {
1265
- const snapshot = JSON.parse(await readFile6(path, "utf8"));
1403
+ const snapshot = JSON.parse(await readFile7(path, "utf8"));
1266
1404
  for (const name of tableNames)
1267
1405
  delete snapshot.tables?.[`public.${name}`];
1268
- await writeFile5(path, `${JSON.stringify(snapshot, null, 2)}
1406
+ await writeFile6(path, `${JSON.stringify(snapshot, null, 2)}
1269
1407
  `);
1270
1408
  }
1271
1409
  }
1272
1410
  } catch (error) {
1273
- if (!isMissing(error))
1411
+ if (!isMissing2(error))
1274
1412
  throw error;
1275
1413
  }
1276
1414
  }
@@ -1279,10 +1417,10 @@ async function collectFiles2(root) {
1279
1417
  await visit(root);
1280
1418
  return files.sort();
1281
1419
  async function visit(directory) {
1282
- for (const name of (await readdir2(directory)).sort()) {
1420
+ for (const name of (await readdir3(directory)).sort()) {
1283
1421
  if ([".git", "node_modules", "dist"].includes(name))
1284
1422
  continue;
1285
- const path = join6(directory, name);
1423
+ const path = join7(directory, name);
1286
1424
  const metadata = await lstat2(path);
1287
1425
  if (metadata.isSymbolicLink())
1288
1426
  continue;
@@ -1299,7 +1437,7 @@ function relativePath(root, path) {
1299
1437
  function escapeRegExp(value) {
1300
1438
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1301
1439
  }
1302
- function isMissing(error) {
1440
+ function isMissing2(error) {
1303
1441
  return error instanceof Error && "code" in error && error.code === "ENOENT";
1304
1442
  }
1305
1443
 
@@ -1598,7 +1736,8 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
1598
1736
  prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
1599
1737
  } : {},
1600
1738
  dev: "docker compose up",
1601
- "dev:build": "docker compose up --build",
1739
+ "docker:sync": "create-windy sync-docker",
1740
+ "dev:build": "bun run docker:sync && docker compose up --build",
1602
1741
  ...generatorVersion ? { windy: "create-windy" } : {},
1603
1742
  "dev:web": "bun run --cwd apps/web dev",
1604
1743
  "dev:server": "bun run --cwd apps/server dev",
@@ -1719,21 +1858,21 @@ var excludedRuntimeFiles = new Set([
1719
1858
  ]);
1720
1859
 
1721
1860
  // src/provenance.ts
1722
- import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1723
- import { join as join7 } from "node:path";
1861
+ import { mkdir as mkdir2, readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
1862
+ import { join as join8 } from "node:path";
1724
1863
  var templateMetadataFile = ".windy-template.json";
1725
1864
  var generationMetadataFile = ".windy/generation.json";
1726
1865
  async function readTemplateMetadata(sourceRoot) {
1727
- const path = join7(sourceRoot, templateMetadataFile);
1728
- const parsed = JSON.parse(await readFile7(path, "utf8"));
1866
+ const path = join8(sourceRoot, templateMetadataFile);
1867
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
1729
1868
  if (!isTemplateMetadata(parsed)) {
1730
1869
  throw new Error("create-windy 模板来源元数据无效");
1731
1870
  }
1732
1871
  return parsed;
1733
1872
  }
1734
1873
  async function readGenerationProvenance(targetDirectory) {
1735
- const path = join7(targetDirectory, generationMetadataFile);
1736
- const parsed = JSON.parse(await readFile7(path, "utf8"));
1874
+ const path = join8(targetDirectory, generationMetadataFile);
1875
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
1737
1876
  if (!isGenerationProvenance(parsed)) {
1738
1877
  throw new Error("当前目录不是受支持的 Windy 项目:Project Lock 无效");
1739
1878
  }
@@ -1765,9 +1904,9 @@ async function writeGenerationProvenance(targetDirectory, metadata, profile, sel
1765
1904
  includeOxc
1766
1905
  }
1767
1906
  };
1768
- const path = join7(targetDirectory, generationMetadataFile);
1769
- await mkdir2(join7(path, ".."), { recursive: true });
1770
- await writeFile6(path, `${JSON.stringify(provenance, null, 2)}
1907
+ const path = join8(targetDirectory, generationMetadataFile);
1908
+ await mkdir2(join8(path, ".."), { recursive: true });
1909
+ await writeFile7(path, `${JSON.stringify(provenance, null, 2)}
1771
1910
  `);
1772
1911
  }
1773
1912
  function isTemplateMetadata(value) {
@@ -1792,8 +1931,8 @@ function isModuleVersion(value) {
1792
1931
  }
1793
1932
 
1794
1933
  // src/registry-dependencies.ts
1795
- import { lstat as lstat3, readFile as readFile8, readdir as readdir3, writeFile as writeFile7 } from "node:fs/promises";
1796
- import { basename as basename2, extname as extname2, join as join8 } from "node:path";
1934
+ import { lstat as lstat3, readFile as readFile9, readdir as readdir4, writeFile as writeFile8 } from "node:fs/promises";
1935
+ import { basename as basename2, extname as extname2, join as join9 } from "node:path";
1797
1936
  var dependencySections = [
1798
1937
  "dependencies",
1799
1938
  "devDependencies",
@@ -1821,14 +1960,14 @@ async function materializeRegistryDependencies(root, dependencies) {
1821
1960
  await rewritePackageJson(path, dependencies);
1822
1961
  continue;
1823
1962
  }
1824
- const source = await readFile8(path, "utf8");
1963
+ const source = await readFile9(path, "utf8");
1825
1964
  const output = dependencies.reduce((value, dependency) => value.replaceAll(dependency.sourceName, dependency.name), source);
1826
1965
  if (output !== source)
1827
- await writeFile7(path, output);
1966
+ await writeFile8(path, output);
1828
1967
  }
1829
1968
  }
1830
1969
  async function rewritePackageJson(path, registryDependencies) {
1831
- const value = JSON.parse(await readFile8(path, "utf8"));
1970
+ const value = JSON.parse(await readFile9(path, "utf8"));
1832
1971
  let changed = false;
1833
1972
  for (const section of dependencySections) {
1834
1973
  const dependencies = value[section];
@@ -1843,7 +1982,7 @@ async function rewritePackageJson(path, registryDependencies) {
1843
1982
  }
1844
1983
  }
1845
1984
  if (changed)
1846
- await writeFile7(path, `${JSON.stringify(value, null, 2)}
1985
+ await writeFile8(path, `${JSON.stringify(value, null, 2)}
1847
1986
  `);
1848
1987
  }
1849
1988
  async function collectFiles3(root) {
@@ -1851,10 +1990,10 @@ async function collectFiles3(root) {
1851
1990
  await visit(root);
1852
1991
  return files.sort();
1853
1992
  async function visit(directory) {
1854
- for (const name of (await readdir3(directory)).sort()) {
1993
+ for (const name of (await readdir4(directory)).sort()) {
1855
1994
  if ([".git", "node_modules", "dist"].includes(name))
1856
1995
  continue;
1857
- const path = join8(directory, name);
1996
+ const path = join9(directory, name);
1858
1997
  const metadata = await lstat3(path);
1859
1998
  if (metadata.isSymbolicLink())
1860
1999
  continue;
@@ -1878,39 +2017,39 @@ async function generateProject(input) {
1878
2017
  const selectedModules = input.selectedModules ?? templateMetadata.modules.filter(({ name }) => requestedIncludeExample || name !== "work-order").map(({ name }) => name);
1879
2018
  const includeExample = selectedModules.includes("work-order");
1880
2019
  for (const relativePath2 of generationTemplatePaths(true, includeOxc)) {
1881
- await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join9(input.targetDirectory, relativePath2));
2020
+ await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join10(input.targetDirectory, relativePath2));
1882
2021
  }
1883
2022
  await materializeSelectedModules(input.targetDirectory, selectedModules);
1884
2023
  await materializeRegistryDependencies(input.targetDirectory, selectedModuleManifests(selectedModules).flatMap(({ registryDependencies }) => registryDependencies));
1885
- const packagePath = join9(input.targetDirectory, "package.json");
1886
- const packageJson = JSON.parse(await readFile9(packagePath, "utf8"));
1887
- await writeFile8(packagePath, `${JSON.stringify(createStarterPackage(packageJson, input.projectName, includeOxc, projectLicense, selectedModules, templateMetadata.generatorVersion), null, 2)}
2024
+ const packagePath = join10(input.targetDirectory, "package.json");
2025
+ const packageJson = JSON.parse(await readFile10(packagePath, "utf8"));
2026
+ await writeFile9(packagePath, `${JSON.stringify(createStarterPackage(packageJson, input.projectName, includeOxc, projectLicense, selectedModules, templateMetadata.generatorVersion), null, 2)}
1888
2027
  `);
1889
- await writeFile8(join9(input.targetDirectory, "docker-compose.yml"), starterCompose);
1890
- await writeFile8(join9(input.targetDirectory, ".env.example"), environmentExample(includeExample));
1891
- await writeFile8(join9(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
1892
- await writeFile8(join9(input.targetDirectory, "docs/README.md"), starterDocsReadme(selectedModules.includes("system.license")));
2028
+ await writeFile9(join10(input.targetDirectory, "docker-compose.yml"), starterCompose);
2029
+ await writeFile9(join10(input.targetDirectory, ".env.example"), environmentExample(includeExample));
2030
+ await writeFile9(join10(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
2031
+ await writeFile9(join10(input.targetDirectory, "docs/README.md"), starterDocsReadme(selectedModules.includes("system.license")));
1893
2032
  await reflectCodeQualityChoice(input.targetDirectory, includeOxc);
1894
2033
  await applyWindyBaseLicense(input.targetDirectory);
1895
2034
  await applyProjectLicense(input.targetDirectory, projectLicense, input.licenseHolder);
1896
2035
  await prepareCustomerDockerfiles(input.targetDirectory);
1897
2036
  await writeGenerationProvenance(input.targetDirectory, templateMetadata, input.profile ?? starterProfile.key, selectedModules, includeOxc, projectLicense);
1898
- await rm3(join9(input.targetDirectory, "bun.lock"), { force: true });
2037
+ await rm3(join10(input.targetDirectory, "bun.lock"), { force: true });
1899
2038
  await writeManagedFilesManifest(input.targetDirectory, templateMetadata.generatorVersion);
1900
2039
  }
1901
2040
  async function resolveTemplateSource(sourceRoot, relativePath2) {
1902
- const source = join9(sourceRoot, relativePath2);
2041
+ const source = join10(sourceRoot, relativePath2);
1903
2042
  try {
1904
2043
  await lstat4(source);
1905
2044
  return source;
1906
2045
  } catch (error) {
1907
- if (!isMissing2(error))
2046
+ if (!isMissing3(error))
1908
2047
  throw error;
1909
2048
  }
1910
2049
  const alias = npmTemplateAliases[relativePath2];
1911
2050
  if (!alias)
1912
2051
  return source;
1913
- return join9(sourceRoot, alias);
2052
+ return join10(sourceRoot, alias);
1914
2053
  }
1915
2054
  async function assertGeneratedProjectIsClean(targetDirectory) {
1916
2055
  const violations = [];
@@ -1935,11 +2074,11 @@ function validateProjectName(projectName) {
1935
2074
  }
1936
2075
  async function assertTargetAvailable(targetDirectory) {
1937
2076
  try {
1938
- const entries = await readdir4(targetDirectory);
2077
+ const entries = await readdir5(targetDirectory);
1939
2078
  if (entries.length)
1940
2079
  throw new Error("目标目录不是空目录");
1941
2080
  } catch (error) {
1942
- if (isMissing2(error))
2081
+ if (isMissing3(error))
1943
2082
  return;
1944
2083
  throw error;
1945
2084
  }
@@ -1951,26 +2090,26 @@ async function copyTree(source, target) {
1951
2090
  return;
1952
2091
  if (metadata.isDirectory()) {
1953
2092
  await mkdir3(target, { recursive: true });
1954
- for (const child of await readdir4(source)) {
1955
- await copyTree(join9(source, child), join9(target, child));
2093
+ for (const child of await readdir5(source)) {
2094
+ await copyTree(join10(source, child), join10(target, child));
1956
2095
  }
1957
2096
  return;
1958
2097
  }
1959
- await mkdir3(join9(target, ".."), { recursive: true });
2098
+ await mkdir3(join10(target, ".."), { recursive: true });
1960
2099
  await copyFile(source, target);
1961
2100
  }
1962
2101
  function shouldExclude(name) {
1963
2102
  return excludedNames.has(name) || excludedRuntimeFiles.has(name);
1964
2103
  }
1965
2104
  async function walk(directory, visit) {
1966
- for (const name of await readdir4(directory)) {
1967
- const path = join9(directory, name);
2105
+ for (const name of await readdir5(directory)) {
2106
+ const path = join10(directory, name);
1968
2107
  await visit(path);
1969
2108
  if ((await lstat4(path)).isDirectory())
1970
2109
  await walk(path, visit);
1971
2110
  }
1972
2111
  }
1973
- function isMissing2(error) {
2112
+ function isMissing3(error) {
1974
2113
  return error instanceof Error && "code" in error && error.code === "ENOENT";
1975
2114
  }
1976
2115
 
@@ -2043,9 +2182,9 @@ async function createProject(options, dependencies = {}) {
2043
2182
  }
2044
2183
  async function inspectTarget(path) {
2045
2184
  try {
2046
- return (await readdir5(path)).length === 0 ? "empty" : "non-empty";
2185
+ return (await readdir6(path)).length === 0 ? "empty" : "non-empty";
2047
2186
  } catch (error) {
2048
- if (isMissing3(error))
2187
+ if (isMissing4(error))
2049
2188
  return "missing";
2050
2189
  throw error;
2051
2190
  }
@@ -2067,7 +2206,7 @@ function stageError(stage, error) {
2067
2206
  function sanitizeReason(reason) {
2068
2207
  return reason.replace(/\b([A-Z][A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|DATABASE_URL)[A-Z0-9_]*)=\S+/g, "$1=[已隐藏]").replace(/:\/\/([^\s:/]+):([^\s@]+)@/g, "://$1:[已隐藏]@");
2069
2208
  }
2070
- function isMissing3(error) {
2209
+ function isMissing4(error) {
2071
2210
  return error instanceof Error && "code" in error && error.code === "ENOENT";
2072
2211
  }
2073
2212
 
@@ -3815,8 +3954,8 @@ import { rm as rm8 } from "node:fs/promises";
3815
3954
  import { resolve as resolve5 } from "node:path";
3816
3955
 
3817
3956
  // src/update/doctor.ts
3818
- import { readFile as readFile11 } from "node:fs/promises";
3819
- import { join as join11 } from "node:path";
3957
+ import { readFile as readFile12 } from "node:fs/promises";
3958
+ import { join as join12 } from "node:path";
3820
3959
 
3821
3960
  // src/update/process.ts
3822
3961
  import { spawn as spawn3 } from "node:child_process";
@@ -3848,12 +3987,12 @@ import {
3848
3987
  access as access2,
3849
3988
  copyFile as copyFile2,
3850
3989
  mkdir as mkdir5,
3851
- readFile as readFile10,
3990
+ readFile as readFile11,
3852
3991
  rm as rm5,
3853
3992
  rmdir,
3854
- writeFile as writeFile9
3993
+ writeFile as writeFile10
3855
3994
  } from "node:fs/promises";
3856
- import { dirname as dirname3, join as join10, resolve as resolve4, sep as sep3 } from "node:path";
3995
+ import { dirname as dirname3, join as join11, resolve as resolve4, sep as sep3 } from "node:path";
3857
3996
  var updateStatePath = ".windy/update-state.json";
3858
3997
  async function applyTransaction(plan) {
3859
3998
  if (plan.conflicts.length) {
@@ -3862,21 +4001,22 @@ async function applyTransaction(plan) {
3862
4001
  const changed = plan.files.filter(({ action }) => action === "create" || action === "update" || action === "delete");
3863
4002
  for (const file of changed)
3864
4003
  assertSafePath(file.path);
3865
- const backupDirectory = join10(plan.projectDirectory, ".windy/backups", plan.id);
4004
+ const backupDirectory = join11(plan.projectDirectory, ".windy/backups", plan.id);
3866
4005
  const protectedPaths = new Set([
3867
4006
  ...changed.map(({ path: path2 }) => path2),
3868
4007
  ".windy/generation.json",
3869
4008
  ".windy/files.json",
4009
+ "apps/web/Dockerfile",
3870
4010
  "bun.lock"
3871
4011
  ]);
3872
4012
  const backupEntries = [];
3873
4013
  for (const path2 of protectedPaths) {
3874
- const source = join10(plan.projectDirectory, path2);
4014
+ const source = join11(plan.projectDirectory, path2);
3875
4015
  const existed = await exists(source);
3876
4016
  backupEntries.push({ path: path2, existed });
3877
4017
  if (!existed)
3878
4018
  continue;
3879
- const target = join10(backupDirectory, "files", path2);
4019
+ const target = join11(backupDirectory, "files", path2);
3880
4020
  await mkdir5(dirname3(target), { recursive: true });
3881
4021
  await copyFile2(source, target);
3882
4022
  }
@@ -3889,7 +4029,7 @@ async function applyTransaction(plan) {
3889
4029
  await writeState(plan.projectDirectory, state);
3890
4030
  try {
3891
4031
  for (const file of changed) {
3892
- const target = join10(plan.projectDirectory, file.path);
4032
+ const target = join11(plan.projectDirectory, file.path);
3893
4033
  if (file.action === "delete") {
3894
4034
  const existed = await exists(target);
3895
4035
  await rm5(target, { force: true });
@@ -3901,7 +4041,7 @@ async function applyTransaction(plan) {
3901
4041
  if (!file.content)
3902
4042
  throw new Error(`更新计划缺少文件内容:${file.path}`);
3903
4043
  await mkdir5(dirname3(target), { recursive: true });
3904
- await writeFile9(target, file.content);
4044
+ await writeFile10(target, file.content);
3905
4045
  }
3906
4046
  } catch (error) {
3907
4047
  await restoreUpdateState(plan.projectDirectory, state);
@@ -3916,7 +4056,7 @@ async function applyTransaction(plan) {
3916
4056
  }
3917
4057
  async function readUpdateState(projectDirectory) {
3918
4058
  try {
3919
- const parsed = JSON.parse(await readFile10(join10(projectDirectory, updateStatePath), "utf8"));
4059
+ const parsed = JSON.parse(await readFile11(join11(projectDirectory, updateStatePath), "utf8"));
3920
4060
  if (!isUpdateState(parsed))
3921
4061
  throw new Error("更新恢复状态无效,请人工检查 .windy 目录");
3922
4062
  return parsed;
@@ -3929,25 +4069,25 @@ async function readUpdateState(projectDirectory) {
3929
4069
  async function restoreUpdateState(projectDirectory, state) {
3930
4070
  for (const entry of state.entries) {
3931
4071
  assertSafePath(entry.path);
3932
- const target = join10(projectDirectory, entry.path);
4072
+ const target = join11(projectDirectory, entry.path);
3933
4073
  if (!entry.existed) {
3934
4074
  await rm5(target, { force: true });
3935
4075
  continue;
3936
4076
  }
3937
- const backup = join10(state.backupDirectory, "files", entry.path);
4077
+ const backup = join11(state.backupDirectory, "files", entry.path);
3938
4078
  await mkdir5(dirname3(target), { recursive: true });
3939
4079
  await copyFile2(backup, target);
3940
4080
  }
3941
- await rm5(join10(projectDirectory, updateStatePath), { force: true });
4081
+ await rm5(join11(projectDirectory, updateStatePath), { force: true });
3942
4082
  await rm5(state.backupDirectory, { recursive: true, force: true });
3943
4083
  }
3944
4084
  async function clearUpdateState(projectDirectory) {
3945
- await rm5(join10(projectDirectory, updateStatePath), { force: true });
4085
+ await rm5(join11(projectDirectory, updateStatePath), { force: true });
3946
4086
  }
3947
4087
  async function writeState(projectDirectory, state) {
3948
- const path2 = join10(projectDirectory, updateStatePath);
4088
+ const path2 = join11(projectDirectory, updateStatePath);
3949
4089
  await mkdir5(dirname3(path2), { recursive: true });
3950
- await writeFile9(path2, `${JSON.stringify(state, null, 2)}
4090
+ await writeFile10(path2, `${JSON.stringify(state, null, 2)}
3951
4091
  `);
3952
4092
  }
3953
4093
  async function exists(path2) {
@@ -4015,7 +4155,7 @@ async function diagnoseProject(projectDirectory, repair = false) {
4015
4155
  if (file.ownership === "project-owned")
4016
4156
  continue;
4017
4157
  try {
4018
- const changed = hashContent(await readFile11(join11(projectDirectory, file.path))) !== file.hash;
4158
+ const changed = hashContent(await readFile12(join12(projectDirectory, file.path))) !== file.hash;
4019
4159
  if (!changed)
4020
4160
  continue;
4021
4161
  modifiedManagedFiles.push(file.path);
@@ -4063,7 +4203,9 @@ var recipes = [
4063
4203
  { id: "starter-0.2.14-to-0.2.15", from: "0.2.14", to: "0.2.15" },
4064
4204
  { id: "starter-0.2.15-to-0.2.16", from: "0.2.15", to: "0.2.16" },
4065
4205
  { id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" },
4066
- { id: "starter-0.2.17-to-0.2.18", from: "0.2.17", to: "0.2.18" }
4206
+ { id: "starter-0.2.17-to-0.2.18", from: "0.2.17", to: "0.2.18" },
4207
+ { id: "starter-0.2.18-to-0.2.19", from: "0.2.18", to: "0.2.19" },
4208
+ { id: "starter-0.2.19-to-0.2.20", from: "0.2.19", to: "0.2.20" }
4067
4209
  ];
4068
4210
  function resolveRecipeChain(sourceVersion, targetVersion) {
4069
4211
  if (sourceVersion === targetVersion)
@@ -4088,19 +4230,19 @@ function resolveRecipeChain(sourceVersion, targetVersion) {
4088
4230
  }
4089
4231
 
4090
4232
  // src/update/updater.ts
4091
- import { mkdir as mkdir7, readFile as readFile14, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
4092
- import { dirname as dirname4, join as join15 } from "node:path";
4233
+ import { mkdir as mkdir7, readFile as readFile16, rm as rm7, writeFile as writeFile12 } from "node:fs/promises";
4234
+ import { dirname as dirname4, join as join17 } from "node:path";
4093
4235
 
4094
4236
  // src/update/artifacts.ts
4095
- import { mkdir as mkdir6, mkdtemp, readFile as readFile12 } from "node:fs/promises";
4237
+ import { mkdir as mkdir6, mkdtemp, readFile as readFile13 } from "node:fs/promises";
4096
4238
  import { tmpdir } from "node:os";
4097
- import { basename as basename4, join as join12 } from "node:path";
4239
+ import { basename as basename4, join as join13 } from "node:path";
4098
4240
  class PublishedArtifactProvider {
4099
4241
  async prepare(projectDirectory, provenance) {
4100
- const temporaryRoot = await mkdtemp(join12(tmpdir(), "windy-update-"));
4242
+ const temporaryRoot = await mkdtemp(join13(tmpdir(), "windy-update-"));
4101
4243
  const projectName = await readProjectName2(projectDirectory);
4102
- const baseDirectory = join12(temporaryRoot, "base", projectName);
4103
- const targetDirectory = join12(temporaryRoot, "target", projectName);
4244
+ const baseDirectory = join13(temporaryRoot, "base", projectName);
4245
+ const targetDirectory = join13(temporaryRoot, "target", projectName);
4104
4246
  await generatePublishedBase(temporaryRoot, projectName, provenance, baseDirectory);
4105
4247
  await generateProject({
4106
4248
  sourceRoot: await resolveTemplateRoot(),
@@ -4117,7 +4259,7 @@ class PublishedArtifactProvider {
4117
4259
  }
4118
4260
  }
4119
4261
  async function generatePublishedBase(temporaryRoot, projectName, provenance, expectedDirectory) {
4120
- const parent = join12(temporaryRoot, "base");
4262
+ const parent = join13(temporaryRoot, "base");
4121
4263
  await mkdir6(parent, { recursive: true });
4122
4264
  const args = [
4123
4265
  "--bun",
@@ -4141,18 +4283,18 @@ async function generatePublishedBase(temporaryRoot, projectName, provenance, exp
4141
4283
  }
4142
4284
  }
4143
4285
  await assertProcess("bunx", args, parent);
4144
- if (join12(parent, projectName) !== expectedDirectory) {
4286
+ if (join13(parent, projectName) !== expectedDirectory) {
4145
4287
  throw new Error("历史 artifact 生成目录异常");
4146
4288
  }
4147
4289
  }
4148
4290
  async function readProjectName2(projectDirectory) {
4149
- const source = JSON.parse(await readFile12(join12(projectDirectory, "package.json"), "utf8"));
4291
+ const source = JSON.parse(await readFile13(join13(projectDirectory, "package.json"), "utf8"));
4150
4292
  return source.name || basename4(projectDirectory);
4151
4293
  }
4152
4294
 
4153
4295
  // src/update/planner.ts
4154
- import { access as access3, readFile as readFile13 } from "node:fs/promises";
4155
- import { join as join14 } from "node:path";
4296
+ import { access as access3, readFile as readFile15 } from "node:fs/promises";
4297
+ import { join as join16 } from "node:path";
4156
4298
 
4157
4299
  // src/update/known-repairs.ts
4158
4300
  var wrongTextVersions = new Set(["0.2.12", "0.2.13"]);
@@ -4195,31 +4337,415 @@ function normalizeTableCell(cell) {
4195
4337
  }
4196
4338
 
4197
4339
  // src/update/merge.ts
4198
- import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile10 } from "node:fs/promises";
4340
+ import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile11 } from "node:fs/promises";
4199
4341
  import { tmpdir as tmpdir2 } from "node:os";
4200
- import { join as join13 } from "node:path";
4342
+ import { join as join14 } from "node:path";
4201
4343
  async function mergeTextFile(base, ours, theirs) {
4202
- const root = await mkdtemp2(join13(tmpdir2(), "windy-merge-"));
4344
+ const root = await mkdtemp2(join14(tmpdir2(), "windy-merge-"));
4203
4345
  try {
4204
- const oursPath = join13(root, "ours");
4205
- const basePath = join13(root, "base");
4206
- const theirsPath = join13(root, "theirs");
4346
+ const oursPath = join14(root, "ours");
4347
+ const basePath = join14(root, "base");
4348
+ const theirsPath = join14(root, "theirs");
4207
4349
  await Promise.all([
4208
- writeFile10(oursPath, ours),
4209
- writeFile10(basePath, base),
4210
- writeFile10(theirsPath, theirs)
4350
+ writeFile11(oursPath, ours),
4351
+ writeFile11(basePath, base),
4352
+ writeFile11(theirsPath, theirs)
4211
4353
  ]);
4212
4354
  const result = await captureProcess("git", ["merge-file", "-p", "--diff3", oursPath, basePath, theirsPath], root);
4213
4355
  if (result.code === 0) {
4214
4356
  return { content: new TextEncoder().encode(result.stdout) };
4215
4357
  }
4216
- if (result.code === 1)
4217
- return { conflict: "三方合并存在重叠修改" };
4358
+ if (result.code === 1) {
4359
+ const resolved = resolveConservativeConflicts(result.stdout);
4360
+ return resolved ? { content: new TextEncoder().encode(resolved) } : { conflict: "三方合并存在重叠修改" };
4361
+ }
4218
4362
  return { conflict: result.stderr.trim() || "git merge-file 执行失败" };
4219
4363
  } finally {
4220
4364
  await rm6(root, { recursive: true, force: true });
4221
4365
  }
4222
4366
  }
4367
+ function resolveConservativeConflicts(content) {
4368
+ const lines = content.split(`
4369
+ `);
4370
+ const output = [];
4371
+ for (let index = 0;index < lines.length; index += 1) {
4372
+ if (!lines[index]?.startsWith("<<<<<<< ")) {
4373
+ output.push(lines[index] ?? "");
4374
+ continue;
4375
+ }
4376
+ const baseMarker = lines.findIndex((line, candidate) => candidate > index && line.startsWith("||||||| "));
4377
+ const separator = lines.indexOf("=======", baseMarker + 1);
4378
+ const endMarker = lines.findIndex((line, candidate) => candidate > separator && line.startsWith(">>>>>>> "));
4379
+ if (baseMarker < 0 || separator < 0 || endMarker < 0)
4380
+ return;
4381
+ const ours = lines.slice(index + 1, baseMarker);
4382
+ const base = lines.slice(baseMarker + 1, separator);
4383
+ const theirs = lines.slice(separator + 1, endMarker);
4384
+ const resolved = resolveRegion(ours, base, theirs);
4385
+ if (!resolved)
4386
+ return;
4387
+ output.push(...resolved);
4388
+ index = endMarker;
4389
+ }
4390
+ return output.join(`
4391
+ `);
4392
+ }
4393
+ function resolveRegion(ours, base, theirs) {
4394
+ const oursRebased = replaceSubsequence(ours, base, theirs);
4395
+ if (oursRebased)
4396
+ return oursRebased;
4397
+ const theirsRebased = replaceSubsequence(theirs, base, ours);
4398
+ if (theirsRebased)
4399
+ return theirsRebased;
4400
+ if (normalizeWhitespace(ours) === normalizeWhitespace(base))
4401
+ return theirs;
4402
+ if (normalizeWhitespace(theirs) === normalizeWhitespace(base))
4403
+ return ours;
4404
+ return applySemanticTokenEdits(ours, base, theirs) ?? mergeMarkdownTable(ours, base, theirs);
4405
+ }
4406
+ function replaceSubsequence(content, baseline, replacement) {
4407
+ if (!baseline.length || baseline.length > content.length)
4408
+ return;
4409
+ const starts = [];
4410
+ for (let index = 0;index <= content.length - baseline.length; index += 1) {
4411
+ if (baseline.every((line, offset) => content[index + offset] === line)) {
4412
+ starts.push(index);
4413
+ }
4414
+ }
4415
+ if (starts.length !== 1)
4416
+ return;
4417
+ const start = starts[0] ?? 0;
4418
+ return [
4419
+ ...content.slice(0, start),
4420
+ ...replacement,
4421
+ ...content.slice(start + baseline.length)
4422
+ ];
4423
+ }
4424
+ function normalizeWhitespace(lines) {
4425
+ return lines.join(" ").replaceAll(/\s+/g, " ").trim();
4426
+ }
4427
+ function applySemanticTokenEdits(oursLines, baseLines, theirsLines) {
4428
+ const oursText = oursLines.join(`
4429
+ `);
4430
+ const baseText = baseLines.join(`
4431
+ `);
4432
+ const theirsText = theirsLines.join(`
4433
+ `);
4434
+ const ours = semanticTokens(oursText);
4435
+ const base = semanticTokens(baseText);
4436
+ const theirs = semanticTokens(theirsText);
4437
+ let prefix = 0;
4438
+ while (prefix < base.length && prefix < theirs.length && base[prefix]?.value === theirs[prefix]?.value) {
4439
+ prefix += 1;
4440
+ }
4441
+ let suffix = 0;
4442
+ while (suffix < base.length - prefix && suffix < theirs.length - prefix && base[base.length - 1 - suffix]?.value === theirs[theirs.length - 1 - suffix]?.value) {
4443
+ suffix += 1;
4444
+ }
4445
+ const baseEnd = base.length - suffix;
4446
+ const targetEnd = theirs.length - suffix;
4447
+ const needle = base.slice(prefix, baseEnd).map(({ value }) => value);
4448
+ if (!needle.length)
4449
+ return;
4450
+ const starts = findTokenSequence(ours, needle);
4451
+ if (starts.length !== 1)
4452
+ return;
4453
+ const oursStart = starts[0] ?? 0;
4454
+ const oursEnd = oursStart + needle.length - 1;
4455
+ const targetFirst = theirs[prefix];
4456
+ const targetLast = theirs[targetEnd - 1];
4457
+ const replacement = targetFirst && targetLast ? theirsText.slice(targetFirst.start, targetLast.end) : "";
4458
+ const merged = oursText.slice(0, ours[oursStart]?.start ?? 0) + replacement + oursText.slice(ours[oursEnd]?.end ?? 0);
4459
+ return merged.split(`
4460
+ `);
4461
+ }
4462
+ function semanticTokens(content) {
4463
+ return [...content.matchAll(/[A-Za-z0-9_@./:-]+|[^\s]/g)].map((match) => ({
4464
+ value: match[0],
4465
+ start: match.index,
4466
+ end: match.index + match[0].length
4467
+ }));
4468
+ }
4469
+ function findTokenSequence(tokens, needle) {
4470
+ const starts = [];
4471
+ for (let index = 0;index <= tokens.length - needle.length; index += 1) {
4472
+ if (needle.every((value, offset) => tokens[index + offset]?.value === value)) {
4473
+ starts.push(index);
4474
+ }
4475
+ }
4476
+ return starts;
4477
+ }
4478
+ function mergeMarkdownTable(ours, base, theirs) {
4479
+ if (![ours, base, theirs].every((rows) => rows.length === base.length && rows.every(isMarkdownTableRow))) {
4480
+ return;
4481
+ }
4482
+ const parsed = [ours, base, theirs].map((rows) => rows.map(parseTableRow));
4483
+ const [oursRows, baseRows, theirsRows] = parsed;
4484
+ if (!oursRows || !baseRows || !theirsRows || !oursRows.every((cells, index) => cells.length === baseRows[index]?.length && cells.length === theirsRows[index]?.length)) {
4485
+ return;
4486
+ }
4487
+ const merged = [];
4488
+ for (let row = 0;row < baseRows.length; row += 1) {
4489
+ const cells = [];
4490
+ for (let column = 0;column < (baseRows[row]?.length ?? 0); column += 1) {
4491
+ const baseCell = baseRows[row]?.[column] ?? "";
4492
+ const oursCell = oursRows[row]?.[column] ?? "";
4493
+ const theirsCell = theirsRows[row]?.[column] ?? "";
4494
+ if (isTableSeparator(baseCell)) {
4495
+ cells.push("---");
4496
+ } else if (oursCell === baseCell || oursCell === theirsCell) {
4497
+ cells.push(theirsCell);
4498
+ } else if (theirsCell === baseCell) {
4499
+ cells.push(oursCell);
4500
+ } else {
4501
+ return;
4502
+ }
4503
+ }
4504
+ merged.push(`| ${cells.join(" | ")} |`);
4505
+ }
4506
+ return merged;
4507
+ }
4508
+ function isMarkdownTableRow(line) {
4509
+ const trimmed = line.trim();
4510
+ return trimmed.startsWith("|") && trimmed.endsWith("|");
4511
+ }
4512
+ function parseTableRow(line) {
4513
+ return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
4514
+ }
4515
+ function isTableSeparator(cell) {
4516
+ return /^:?-{3,}:?$/.test(cell);
4517
+ }
4518
+
4519
+ // src/update/migration-reconciler.ts
4520
+ import { readFile as readFile14 } from "node:fs/promises";
4521
+ import { join as join15 } from "node:path";
4522
+
4523
+ // src/update/snapshot-delta.ts
4524
+ function applySnapshotDelta(before, after, current, path2, conflicts, conflictPath) {
4525
+ if (deepEqual(before, after))
4526
+ return structuredClone(current);
4527
+ if (!isRecord(before) || !isRecord(after) || !isRecord(current)) {
4528
+ if (deepEqual(current, before) || deepEqual(current, after)) {
4529
+ return structuredClone(after);
4530
+ }
4531
+ conflicts.push(`${conflictPath}:平台与业务同时修改 snapshot 节点 ${path2.join(".") || "<root>"}`);
4532
+ return structuredClone(current);
4533
+ }
4534
+ const merged = structuredClone(current);
4535
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
4536
+ for (const key of keys) {
4537
+ if (path2.length === 0 && (key === "id" || key === "prevId"))
4538
+ continue;
4539
+ const nextPath = [...path2, key];
4540
+ if (!(key in after)) {
4541
+ if (!(key in current))
4542
+ continue;
4543
+ if (deepEqual(current[key], before[key]))
4544
+ delete merged[key];
4545
+ else
4546
+ conflicts.push(`${conflictPath}:平台删除了业务已修改的 snapshot 节点 ${nextPath.join(".")}`);
4547
+ continue;
4548
+ }
4549
+ if (!(key in before)) {
4550
+ if (!(key in current) || deepEqual(current[key], after[key])) {
4551
+ merged[key] = structuredClone(after[key]);
4552
+ } else {
4553
+ conflicts.push(`${conflictPath}:平台新增的 snapshot 节点已被业务占用 ${nextPath.join(".")}`);
4554
+ }
4555
+ continue;
4556
+ }
4557
+ merged[key] = applySnapshotDelta(before[key], after[key], current[key], nextPath, conflicts, conflictPath);
4558
+ }
4559
+ return merged;
4560
+ }
4561
+ function isRecord(value) {
4562
+ return !!value && typeof value === "object" && !Array.isArray(value);
4563
+ }
4564
+ function deepEqual(left, right) {
4565
+ return JSON.stringify(left) === JSON.stringify(right);
4566
+ }
4567
+
4568
+ // src/update/migration-reconciler.ts
4569
+ var drizzleRoot = "packages/database/drizzle";
4570
+ var journalPath = `${drizzleRoot}/meta/_journal.json`;
4571
+ var migrationPattern = /^(\d{4})_(.+)$/;
4572
+ async function reconcileDrizzleMigrations(input) {
4573
+ const journals = await readJournals(input);
4574
+ if (!journals) {
4575
+ return unchanged(input.targetManifest);
4576
+ }
4577
+ const { base, project, target } = journals;
4578
+ const conflicts = [];
4579
+ validateUniqueNames(base, "来源模板", conflicts);
4580
+ validateUniqueNames(project, "当前项目", conflicts);
4581
+ validateUniqueNames(target, "目标模板", conflicts);
4582
+ if (conflicts.length) {
4583
+ return { ...unchanged(input.targetManifest), conflicts };
4584
+ }
4585
+ const baseNames = new Set(base.entries.map(({ tag }) => stableName(tag)));
4586
+ const projectNames = new Set(project.entries.map(({ tag }) => stableName(tag)));
4587
+ const managedNames = new Set(input.sourceManifest.files.filter(({ path: path2 }) => isSqlMigration(path2)).map(({ path: path2 }) => stableName(path2.slice(drizzleRoot.length + 1, -4))));
4588
+ const candidates = target.entries.filter(({ tag }) => !baseNames.has(stableName(tag)));
4589
+ for (const { tag } of candidates) {
4590
+ const name = stableName(tag);
4591
+ if (projectNames.has(name) && !managedNames.has(name)) {
4592
+ conflicts.push(`${journalPath}:平台 migration 名称与下游业务历史冲突 ${name}`);
4593
+ }
4594
+ }
4595
+ const additions = candidates.filter(({ tag }) => !projectNames.has(stableName(tag)));
4596
+ const contentOverrides = new Map;
4597
+ const files = await preservePublishedHistory(input.sourceManifest, input.projectDirectory, contentOverrides);
4598
+ const publishedPaths = new Set(files.map(({ path: path2 }) => path2));
4599
+ files.push(...input.targetManifest.files.filter(({ path: path2 }) => !isDrizzleHistoryPath(path2)));
4600
+ if (!additions.length) {
4601
+ const journalContent2 = encodeJson(project);
4602
+ contentOverrides.set(journalPath, journalContent2);
4603
+ files.push(managedEntry(journalPath, journalContent2));
4604
+ return result(input.targetManifest, files, contentOverrides, publishedPaths, conflicts);
4605
+ }
4606
+ let nextPrefix = highestPrefix(project) + 1;
4607
+ let nextIndex = highestIndex(project) + 1;
4608
+ let previousWhen = project.entries.at(-1)?.when ?? 0;
4609
+ let currentSnapshot = await readLatestSnapshot(input.projectDirectory, project);
4610
+ const mergedJournal = structuredClone(project);
4611
+ for (const addition of additions) {
4612
+ const targetPosition = target.entries.indexOf(addition);
4613
+ const previousTarget = target.entries[targetPosition - 1];
4614
+ if (!previousTarget) {
4615
+ conflicts.push(`${journalPath}:目标 migration 缺少前置快照`);
4616
+ break;
4617
+ }
4618
+ const targetBefore = await readSnapshot(input.targetDirectory, previousTarget.tag);
4619
+ const targetAfter = await readSnapshot(input.targetDirectory, addition.tag);
4620
+ const merged = applySnapshotDelta(targetBefore, targetAfter, currentSnapshot, [], conflicts, journalPath);
4621
+ if (conflicts.length)
4622
+ break;
4623
+ const prefix = String(nextPrefix).padStart(4, "0");
4624
+ const name = stableName(addition.tag);
4625
+ const tag = `${prefix}_${name}`;
4626
+ const sqlPath = `${drizzleRoot}/${tag}.sql`;
4627
+ const snapshotPath = `${drizzleRoot}/meta/${prefix}_snapshot.json`;
4628
+ const sql = await readFile14(join15(input.targetDirectory, `${drizzleRoot}/${addition.tag}.sql`));
4629
+ merged.id = targetAfter.id;
4630
+ merged.prevId = currentSnapshot.id;
4631
+ const snapshot = encodeJson(merged);
4632
+ contentOverrides.set(sqlPath, sql);
4633
+ contentOverrides.set(snapshotPath, snapshot);
4634
+ files.push(managedEntry(sqlPath, sql), managedEntry(snapshotPath, snapshot));
4635
+ previousWhen = Math.max(previousWhen + 1, addition.when);
4636
+ mergedJournal.entries.push({
4637
+ ...addition,
4638
+ idx: nextIndex,
4639
+ when: previousWhen,
4640
+ tag
4641
+ });
4642
+ currentSnapshot = merged;
4643
+ nextPrefix += 1;
4644
+ nextIndex += 1;
4645
+ }
4646
+ const journalContent = encodeJson(mergedJournal);
4647
+ contentOverrides.set(journalPath, journalContent);
4648
+ files.push(managedEntry(journalPath, journalContent));
4649
+ return result(input.targetManifest, files, contentOverrides, publishedPaths, conflicts);
4650
+ }
4651
+ function result(targetManifest, files, contentOverrides, forcePreserves, conflicts) {
4652
+ return {
4653
+ targetManifest: {
4654
+ ...targetManifest,
4655
+ files: deduplicate(files).sort((left, right) => left.path.localeCompare(right.path))
4656
+ },
4657
+ contentOverrides,
4658
+ forceUpdates: new Set([journalPath]),
4659
+ forcePreserves,
4660
+ conflicts
4661
+ };
4662
+ }
4663
+ function unchanged(targetManifest) {
4664
+ return {
4665
+ targetManifest,
4666
+ contentOverrides: new Map,
4667
+ forceUpdates: new Set,
4668
+ forcePreserves: new Set,
4669
+ conflicts: []
4670
+ };
4671
+ }
4672
+ async function readJournals(input) {
4673
+ try {
4674
+ const [base, project, target] = await Promise.all([
4675
+ readJson(join15(input.baseDirectory, journalPath)),
4676
+ readJson(join15(input.projectDirectory, journalPath)),
4677
+ readJson(join15(input.targetDirectory, journalPath))
4678
+ ]);
4679
+ return { base, project, target };
4680
+ } catch (error) {
4681
+ if (isMissing5(error))
4682
+ return;
4683
+ throw error;
4684
+ }
4685
+ }
4686
+ async function preservePublishedHistory(manifest, projectDirectory, overrides) {
4687
+ const entries = manifest.files.filter(({ path: path2 }) => isMigrationArtifact(path2) && path2 !== journalPath);
4688
+ await Promise.all(entries.map(async ({ path: path2 }) => {
4689
+ overrides.set(path2, await readFile14(join15(projectDirectory, path2)));
4690
+ }));
4691
+ return entries;
4692
+ }
4693
+ async function readLatestSnapshot(root, journal) {
4694
+ const latest = journal.entries.at(-1);
4695
+ if (!latest)
4696
+ throw new Error(`${journalPath}:当前项目没有 migration`);
4697
+ return await readSnapshot(root, latest.tag);
4698
+ }
4699
+ async function readSnapshot(root, tag) {
4700
+ const match = migrationPattern.exec(tag);
4701
+ if (!match)
4702
+ throw new Error(`${journalPath}:migration tag 格式无效:${tag}`);
4703
+ return await readJson(join15(root, `${drizzleRoot}/meta/${match[1]}_snapshot.json`));
4704
+ }
4705
+ function validateUniqueNames(journal, label, conflicts) {
4706
+ const seen = new Set;
4707
+ for (const { tag } of journal.entries) {
4708
+ const name = stableName(tag);
4709
+ if (seen.has(name)) {
4710
+ conflicts.push(`${journalPath}:${label}存在重复 migration 名称 ${name}`);
4711
+ }
4712
+ seen.add(name);
4713
+ }
4714
+ }
4715
+ function stableName(tag) {
4716
+ return migrationPattern.exec(tag)?.[2] ?? tag;
4717
+ }
4718
+ function highestPrefix(journal) {
4719
+ return Math.max(-1, ...journal.entries.map(({ tag }) => Number(migrationPattern.exec(tag)?.[1] ?? -1)));
4720
+ }
4721
+ function highestIndex(journal) {
4722
+ return Math.max(-1, ...journal.entries.map(({ idx }) => idx));
4723
+ }
4724
+ function isDrizzleHistoryPath(path2) {
4725
+ return path2 === journalPath || isMigrationArtifact(path2);
4726
+ }
4727
+ function isMigrationArtifact(path2) {
4728
+ return isSqlMigration(path2) || new RegExp(`^${drizzleRoot}/meta/\\d{4}_snapshot\\.json$`).test(path2);
4729
+ }
4730
+ function isSqlMigration(path2) {
4731
+ return new RegExp(`^${drizzleRoot}/\\d{4}_.+\\.sql$`).test(path2);
4732
+ }
4733
+ function managedEntry(path2, content) {
4734
+ return { path: path2, ownership: "windy-managed", hash: hashContent(content) };
4735
+ }
4736
+ function deduplicate(files) {
4737
+ return [...new Map(files.map((entry) => [entry.path, entry])).values()];
4738
+ }
4739
+ function encodeJson(value) {
4740
+ return new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
4741
+ `);
4742
+ }
4743
+ async function readJson(path2) {
4744
+ return JSON.parse(await readFile14(path2, "utf8"));
4745
+ }
4746
+ function isMissing5(error) {
4747
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
4748
+ }
4223
4749
 
4224
4750
  // src/update/package-merge.ts
4225
4751
  function mergePackageJson(base, ours, theirs) {
@@ -4284,10 +4810,18 @@ async function createUpdatePlan(request, artifacts) {
4284
4810
  throw new Error(`当前 CLI 只能更新到 ${targetProvenance.generatorVersion},请运行 create-windy@${targetVersion}`);
4285
4811
  }
4286
4812
  const recipes2 = resolveRecipeChain(provenance.windyVersion, targetVersion);
4287
- const [sourceManifest, targetManifest] = await Promise.all([
4813
+ const [sourceManifest, generatedTargetManifest] = await Promise.all([
4288
4814
  readManagedFilesManifest(request.projectDirectory),
4289
4815
  readManagedFilesManifest(artifactSet.targetDirectory)
4290
4816
  ]);
4817
+ const migrations = await reconcileDrizzleMigrations({
4818
+ projectDirectory: request.projectDirectory,
4819
+ baseDirectory: artifactSet.baseDirectory,
4820
+ targetDirectory: artifactSet.targetDirectory,
4821
+ sourceManifest,
4822
+ targetManifest: generatedTargetManifest
4823
+ });
4824
+ const targetManifest = migrations.targetManifest;
4291
4825
  targetProvenance.appliedRecipes = [
4292
4826
  ...provenance.appliedRecipes,
4293
4827
  ...recipes2.map(({ id }) => id)
@@ -4300,7 +4834,9 @@ async function createUpdatePlan(request, artifacts) {
4300
4834
  for (const path2 of [...paths].sort()) {
4301
4835
  if (metadataPaths.has(path2))
4302
4836
  continue;
4303
- const file = await planFile(path2, request.projectDirectory, artifactSet.baseDirectory, artifactSet.targetDirectory, sourceEntries.get(path2), targetEntries.get(path2), provenance);
4837
+ const forcedContent = migrations.forceUpdates.has(path2) ? migrations.contentOverrides.get(path2) : undefined;
4838
+ const forcePreserve = migrations.forcePreserves.has(path2);
4839
+ 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));
4304
4840
  if (oxcManagedPaths.has(path2) && file.reason === "客户已修改") {
4305
4841
  file.action = "preserve";
4306
4842
  delete file.conflict;
@@ -4308,7 +4844,10 @@ async function createUpdatePlan(request, artifacts) {
4308
4844
  }
4309
4845
  files.push(file);
4310
4846
  }
4311
- const conflicts = files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : []);
4847
+ const conflicts = [
4848
+ ...migrations.conflicts,
4849
+ ...files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : [])
4850
+ ];
4312
4851
  return {
4313
4852
  id: createUpdateId(provenance.windyVersion, targetVersion),
4314
4853
  projectDirectory: request.projectDirectory,
@@ -4323,11 +4862,11 @@ async function createUpdatePlan(request, artifacts) {
4323
4862
  temporaryRoot: artifactSet.temporaryRoot
4324
4863
  };
4325
4864
  }
4326
- async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance) {
4865
+ async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance, targetContentOverride) {
4327
4866
  const [ours, base, theirs] = await Promise.all([
4328
- readOptional(join14(project, path2)),
4329
- readOptional(join14(baseRoot, path2)),
4330
- readOptional(join14(targetRoot, path2))
4867
+ readOptional(join16(project, path2)),
4868
+ readOptional(join16(baseRoot, path2)),
4869
+ targetContentOverride ? Promise.resolve(targetContentOverride) : readOptional(join16(targetRoot, path2))
4331
4870
  ]);
4332
4871
  const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
4333
4872
  const repaired = ours && theirs && provenance ? repairKnownDerivedFile({
@@ -4372,16 +4911,17 @@ async function planFile(path2, project, baseRoot, targetRoot, source, target, pr
4372
4911
  return conflict(path2, ownership, "文件状态不完整,不能安全推断");
4373
4912
  }
4374
4913
  const oursChanged = hashContent(ours) !== source.hash;
4375
- const targetChanged = target.hash !== source.hash;
4914
+ const targetChanged = targetChangedSinceBase(source.hash, base, theirs);
4915
+ const reconstructedBaseDrifted = !!base && hashContent(base) !== source.hash;
4376
4916
  if (!oursChanged && !targetChanged)
4377
4917
  return ready(path2, ownership, "preserve", "双方未修改");
4378
- if (!oursChanged)
4918
+ if (!oursChanged && !reconstructedBaseDrifted)
4379
4919
  return ready(path2, ownership, "update", "仅 Windy 修改", theirs);
4380
4920
  if (!targetChanged)
4381
4921
  return ready(path2, ownership, "preserve", "客户已修改");
4382
4922
  if (hashContent(ours) === target.hash)
4383
4923
  return ready(path2, ownership, "preserve", "已包含目标内容");
4384
- if (path2 === "package.json" && base) {
4924
+ if (path2.endsWith("package.json") && base) {
4385
4925
  const merged = mergePackageJson(base, ours, theirs);
4386
4926
  return merged.content ? ready(path2, ownership, "update", "package.json 字段级合并", merged.content) : conflict(path2, ownership, `字段冲突:${merged.conflicts.join("、")}`);
4387
4927
  }
@@ -4391,6 +4931,9 @@ async function planFile(path2, project, baseRoot, targetRoot, source, target, pr
4391
4931
  }
4392
4932
  return conflict(path2, ownership, "客户和 Windy 均已修改");
4393
4933
  }
4934
+ function targetChangedSinceBase(sourceHash, base, target) {
4935
+ return base && target ? hashContent(base) !== hashContent(target) : !!target && hashContent(target) !== sourceHash;
4936
+ }
4394
4937
  function ready(path2, ownership, action, reason, content) {
4395
4938
  return { path: path2, ownership, action, reason, content };
4396
4939
  }
@@ -4405,7 +4948,7 @@ function conflict(path2, ownership, reason) {
4405
4948
  }
4406
4949
  async function readOptional(path2) {
4407
4950
  try {
4408
- return await readFile13(path2);
4951
+ return await readFile15(path2);
4409
4952
  } catch (error) {
4410
4953
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
4411
4954
  return;
@@ -4413,15 +4956,15 @@ async function readOptional(path2) {
4413
4956
  }
4414
4957
  }
4415
4958
  async function assertGitClean(projectDirectory) {
4416
- const result = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4417
- if (result.code !== 0)
4959
+ const result2 = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4960
+ if (result2.code !== 0)
4418
4961
  throw new Error("更新要求当前目录是有效的 Git 仓库");
4419
- if (result.stdout.trim())
4962
+ if (result2.stdout.trim())
4420
4963
  throw new Error("更新前请先提交或暂存之外妥善处理 Git 工作区改动");
4421
4964
  }
4422
4965
  async function assertNoPendingUpdate(projectDirectory) {
4423
4966
  try {
4424
- await access3(join14(projectDirectory, ".windy/update-state.json"));
4967
+ await access3(join16(projectDirectory, ".windy/update-state.json"));
4425
4968
  throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
4426
4969
  } catch (error) {
4427
4970
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
@@ -4452,87 +4995,110 @@ class DefaultStarterUpdater {
4452
4995
  throw error;
4453
4996
  }
4454
4997
  }
4455
- async verify(result) {
4998
+ async verify(result2) {
4456
4999
  const steps = [];
4457
5000
  try {
4458
- await this.#execute("bun", ["install", "--force"], result.plan.projectDirectory);
5001
+ const dockerSync = await syncWebDockerfileInProject(result2.plan.projectDirectory, { requireAnchors: true });
5002
+ if (dockerSync.changed) {
5003
+ addChangedFile(result2, "apps/web/Dockerfile");
5004
+ }
5005
+ steps.push({
5006
+ name: "同步 Web Dockerfile workspace COPY 清单",
5007
+ status: dockerSync.synced ? "passed" : "skipped"
5008
+ });
5009
+ await this.#execute("bun", ["install", "--force"], result2.plan.projectDirectory);
4459
5010
  steps.push({ name: "bun install --force", status: "passed" });
4460
- const packageJson = JSON.parse(await readFile14(join15(result.plan.projectDirectory, "package.json"), "utf8"));
5011
+ const packageJson = JSON.parse(await readFile16(join17(result2.plan.projectDirectory, "package.json"), "utf8"));
4461
5012
  for (const name of ["lint", "typecheck", "test", "build"]) {
4462
5013
  if (!packageJson.scripts?.[name]) {
4463
5014
  steps.push({ name: `bun run ${name}`, status: "skipped" });
4464
5015
  continue;
4465
5016
  }
4466
- await this.#execute("bun", ["run", name], result.plan.projectDirectory);
5017
+ await this.#execute("bun", ["run", name], result2.plan.projectDirectory);
4467
5018
  steps.push({ name: `bun run ${name}`, status: "passed" });
4468
5019
  }
4469
- await finalize(result);
4470
- const reportPath = `.windy/reports/${result.plan.id}.md`;
4471
- await writeReport(result, steps, reportPath);
4472
- await clearUpdateState(result.plan.projectDirectory);
4473
- await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4474
- return { updateId: result.plan.id, status: "passed", steps, reportPath };
5020
+ await finalize(result2);
5021
+ const reportPath = `.windy/reports/${result2.plan.id}.md`;
5022
+ await writeReport(result2, steps, reportPath);
5023
+ await clearUpdateState(result2.plan.projectDirectory);
5024
+ await rm7(result2.plan.temporaryRoot, { recursive: true, force: true });
5025
+ return { updateId: result2.plan.id, status: "passed", steps, reportPath };
4475
5026
  } catch (error) {
4476
- await restoreUpdateState(result.plan.projectDirectory, {
5027
+ await restoreUpdateState(result2.plan.projectDirectory, {
4477
5028
  schemaVersion: 1,
4478
- updateId: result.plan.id,
4479
- backupDirectory: result.backupDirectory,
4480
- entries: result.backupEntries
5029
+ updateId: result2.plan.id,
5030
+ backupDirectory: result2.backupDirectory,
5031
+ entries: result2.backupEntries
4481
5032
  });
4482
- await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4483
- throw new Error(`更新验证失败,项目已回滚到 ${result.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: error });
5033
+ await rm7(result2.plan.temporaryRoot, { recursive: true, force: true });
5034
+ throw new Error(`更新验证失败,项目已回滚到 ${result2.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: error });
4484
5035
  }
4485
5036
  }
4486
5037
  }
4487
- async function finalize(result) {
4488
- const root = result.plan.projectDirectory;
4489
- const generationContent = `${JSON.stringify(result.plan.targetProvenance, null, 2)}
5038
+ async function finalize(result2) {
5039
+ const root = result2.plan.projectDirectory;
5040
+ const generationContent = `${JSON.stringify(result2.plan.targetProvenance, null, 2)}
4490
5041
  `;
4491
- await writeFile11(join15(root, ".windy/generation.json"), generationContent);
5042
+ await writeFile12(join17(root, ".windy/generation.json"), generationContent);
4492
5043
  const refreshedPaths = new Set([
4493
5044
  ".windy/generation.json",
4494
- ...result.plan.files.filter(({ action }) => action === "create" || action === "update").map(({ path: path2 }) => path2)
5045
+ ...result2.changedFiles
4495
5046
  ]);
4496
- const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile14(join15(root, path2)))])));
5047
+ const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile16(join17(root, path2)))])));
4497
5048
  const targetManifest = {
4498
- ...result.plan.targetManifest,
4499
- files: result.plan.targetManifest.files.map((entry) => ({
5049
+ ...result2.plan.targetManifest,
5050
+ files: result2.plan.targetManifest.files.map((entry) => ({
4500
5051
  ...entry,
4501
5052
  hash: refreshedHashes.get(entry.path) ?? entry.hash
4502
5053
  }))
4503
5054
  };
4504
- await writeFile11(join15(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
5055
+ await writeFile12(join17(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
4505
5056
  `);
4506
5057
  }
4507
- async function writeReport(result, steps, relativePath2) {
4508
- const path2 = join15(result.plan.projectDirectory, relativePath2);
5058
+ function addChangedFile(result2, path2) {
5059
+ if (!result2.changedFiles.includes(path2))
5060
+ result2.changedFiles.push(path2);
5061
+ }
5062
+ async function writeReport(result2, steps, relativePath2) {
5063
+ const path2 = join17(result2.plan.projectDirectory, relativePath2);
4509
5064
  await mkdir7(dirname4(path2), { recursive: true });
4510
5065
  const lines = [
4511
- `# Windy 更新报告 ${result.plan.id}`,
5066
+ `# Windy 更新报告 ${result2.plan.id}`,
4512
5067
  "",
4513
- `- 来源版本:${result.plan.sourceVersion}`,
4514
- `- 目标版本:${result.plan.targetVersion}`,
4515
- `- Recipe:${result.plan.recipes.join("、") || "无"}`,
4516
- `- 修改文件:${result.changedFiles.length}`,
4517
- `- Oxc 代管:${result.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
5068
+ `- 来源版本:${result2.plan.sourceVersion}`,
5069
+ `- 目标版本:${result2.plan.targetVersion}`,
5070
+ `- Recipe:${result2.plan.recipes.join("、") || "无"}`,
5071
+ `- 修改文件:${result2.changedFiles.length}`,
5072
+ `- Oxc 代管:${result2.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
4518
5073
  "",
4519
5074
  "## 验证",
4520
5075
  "",
4521
5076
  ...steps.map(({ name, status }) => `- ${name}:${status}`),
4522
5077
  "",
4523
- `备份保留在 \`.windy/backups/${result.plan.id}\`,确认稳定后可人工删除。`,
5078
+ `备份保留在 \`.windy/backups/${result2.plan.id}\`,确认稳定后可人工删除。`,
4524
5079
  ""
4525
5080
  ];
4526
- await writeFile11(path2, lines.join(`
5081
+ await writeFile12(path2, lines.join(`
4527
5082
  `));
4528
5083
  }
4529
5084
 
4530
5085
  // src/update/cli.ts
4531
5086
  async function runLifecycleCommand(args) {
4532
5087
  const command = args[0];
4533
- if (command !== "doctor" && command !== "update")
5088
+ if (command !== "doctor" && command !== "update" && command !== "sync-docker") {
4534
5089
  return false;
5090
+ }
4535
5091
  const projectDirectory = resolve5(readValue2(args, "--cwd") || process.cwd());
5092
+ if (command === "sync-docker") {
5093
+ const result2 = await syncWebDockerfileInProject(projectDirectory, {
5094
+ requireAnchors: true
5095
+ });
5096
+ if (!result2.synced) {
5097
+ throw new Error("项目缺少 apps/web/Dockerfile,无法同步 COPY 清单");
5098
+ }
5099
+ console.log(result2.changed ? "已同步 apps/web/Dockerfile workspace COPY 清单" : "apps/web/Dockerfile workspace COPY 清单已是最新");
5100
+ return true;
5101
+ }
4536
5102
  if (command === "doctor") {
4537
5103
  const report = await diagnoseProject(projectDirectory, args.includes("--repair"));
4538
5104
  printDoctor(report);
@@ -4584,8 +5150,8 @@ async function runUpdate(args, projectDirectory) {
4584
5150
  await rm8(plan.temporaryRoot, { recursive: true, force: true });
4585
5151
  throw new Error("存在更新冲突;请按上方清单处理后重新运行,项目未被修改");
4586
5152
  }
4587
- const result = await updater.apply(plan);
4588
- const report = await updater.verify(result);
5153
+ const result2 = await updater.apply(plan);
5154
+ const report = await updater.verify(result2);
4589
5155
  console.log(`
4590
5156
  更新完成:${plan.sourceVersion} → ${plan.targetVersion}`);
4591
5157
  console.log(`报告:${report.reportPath}`);