create-windy 0.2.19 → 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 +312 -154
  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([
@@ -1060,7 +1192,7 @@ var sharedScaffoldRoots = [
1060
1192
  async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
1061
1193
  const paths = await collectFiles(targetDirectory);
1062
1194
  const files = await Promise.all(paths.map(async (path) => {
1063
- const content = await readFile5(join5(targetDirectory, path));
1195
+ const content = await readFile6(join6(targetDirectory, path));
1064
1196
  return {
1065
1197
  path,
1066
1198
  ownership: classifyOwnership(path),
@@ -1073,11 +1205,11 @@ async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
1073
1205
  algorithm: "sha256",
1074
1206
  files
1075
1207
  };
1076
- await writeFile4(join5(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
1208
+ await writeFile5(join6(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
1077
1209
  `);
1078
1210
  }
1079
1211
  async function readManagedFilesManifest(targetDirectory) {
1080
- const parsed = JSON.parse(await readFile5(join5(targetDirectory, managedFilesManifestPath), "utf8"));
1212
+ const parsed = JSON.parse(await readFile6(join6(targetDirectory, managedFilesManifestPath), "utf8"));
1081
1213
  if (!isManagedFilesManifest(parsed)) {
1082
1214
  throw new Error("受管文件清单无效,无法安全更新");
1083
1215
  }
@@ -1098,10 +1230,10 @@ async function collectFiles(root) {
1098
1230
  await visit(root, files);
1099
1231
  return files.sort();
1100
1232
  async function visit(directory, output) {
1101
- for (const name of (await readdir(directory)).sort()) {
1233
+ for (const name of (await readdir2(directory)).sort()) {
1102
1234
  if (ignoredDirectoryNames.has(name))
1103
1235
  continue;
1104
- const path = join5(directory, name);
1236
+ const path = join6(directory, name);
1105
1237
  const metadata = await lstat(path);
1106
1238
  if (metadata.isSymbolicLink())
1107
1239
  continue;
@@ -1135,8 +1267,8 @@ function isManagedFilesManifest(value) {
1135
1267
  }
1136
1268
 
1137
1269
  // src/module-materializer.ts
1138
- import { lstat as lstat2, readFile as readFile6, readdir as readdir2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
1139
- 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";
1140
1272
  var textExtensions = new Set([
1141
1273
  ".css",
1142
1274
  ".html",
@@ -1169,18 +1301,18 @@ async function materializeSelectedModules(targetDirectory, selectedNames) {
1169
1301
  ...manifest.documentation,
1170
1302
  ...manifest.schemaFiles
1171
1303
  ]);
1172
- 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 })));
1173
1305
  await pruneWorkspaceReferences(targetDirectory, removedPackageNames, removedRoots);
1174
1306
  }
1175
1307
  async function stripModuleBlocks(root, selected) {
1176
1308
  for (const path of await collectFiles2(root)) {
1177
1309
  if (!textExtensions.has(extname(path)))
1178
1310
  continue;
1179
- const source = await readFile6(path, "utf8");
1311
+ const source = await readFile7(path, "utf8");
1180
1312
  if (!source.includes("@windy-module"))
1181
1313
  continue;
1182
1314
  const output = stripTaggedSource(source, selected, relativePath(root, path));
1183
- await writeFile5(path, output);
1315
+ await writeFile6(path, output);
1184
1316
  }
1185
1317
  }
1186
1318
  function stripTaggedSource(source, selected, path = "source") {
@@ -1213,11 +1345,11 @@ async function readRemovedPackageNames(root, roots) {
1213
1345
  const names = new Set;
1214
1346
  for (const packageRoot2 of roots) {
1215
1347
  try {
1216
- 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"));
1217
1349
  if (value.name)
1218
1350
  names.add(value.name);
1219
1351
  } catch (error) {
1220
- if (!isMissing(error))
1352
+ if (!isMissing2(error))
1221
1353
  throw error;
1222
1354
  }
1223
1355
  }
@@ -1226,7 +1358,7 @@ async function readRemovedPackageNames(root, roots) {
1226
1358
  async function pruneWorkspaceReferences(root, removedPackages, removedRoots) {
1227
1359
  for (const path of await collectFiles2(root)) {
1228
1360
  if (basename(path) === "package.json") {
1229
- const value = JSON.parse(await readFile6(path, "utf8"));
1361
+ const value = JSON.parse(await readFile7(path, "utf8"));
1230
1362
  let changed = false;
1231
1363
  for (const section of ["dependencies", "devDependencies"]) {
1232
1364
  const dependencies = value[section];
@@ -1238,45 +1370,45 @@ async function pruneWorkspaceReferences(root, removedPackages, removedRoots) {
1238
1370
  }
1239
1371
  }
1240
1372
  if (changed)
1241
- await writeFile5(path, `${JSON.stringify(value, null, 2)}
1373
+ await writeFile6(path, `${JSON.stringify(value, null, 2)}
1242
1374
  `);
1243
1375
  continue;
1244
1376
  }
1245
1377
  if (basename(path) !== "Dockerfile")
1246
1378
  continue;
1247
- const source = await readFile6(path, "utf8");
1379
+ const source = await readFile7(path, "utf8");
1248
1380
  const output = source.split(`
1249
1381
  `).filter((line) => !removedRoots.some((root2) => line.includes(root2))).join(`
1250
1382
  `);
1251
1383
  if (output !== source)
1252
- await writeFile5(path, output);
1384
+ await writeFile6(path, output);
1253
1385
  }
1254
1386
  }
1255
1387
  async function pruneDatabaseArtifacts(root, tableNames, statementTokens) {
1256
1388
  if (!tableNames.length && !statementTokens.length)
1257
1389
  return;
1258
1390
  const tablePatterns = tableNames.map((name) => new RegExp(`(?:"${escapeRegExp(name)}"|\\b${escapeRegExp(name)}\\b)`));
1259
- const drizzleRoot = join6(root, "packages/database/drizzle");
1391
+ const drizzleRoot = join7(root, "packages/database/drizzle");
1260
1392
  try {
1261
1393
  for (const path of await collectFiles2(drizzleRoot)) {
1262
1394
  if (path.endsWith(".sql")) {
1263
- const source = await readFile6(path, "utf8");
1395
+ const source = await readFile7(path, "utf8");
1264
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)));
1265
- await writeFile5(path, statements.length ? `${statements.join(`
1397
+ await writeFile6(path, statements.length ? `${statements.join(`
1266
1398
  --> statement-breakpoint
1267
1399
  `)}
1268
1400
  ` : `-- 此 migration 的模块未被当前项目选择。
1269
1401
  `);
1270
1402
  } else if (path.endsWith("_snapshot.json")) {
1271
- const snapshot = JSON.parse(await readFile6(path, "utf8"));
1403
+ const snapshot = JSON.parse(await readFile7(path, "utf8"));
1272
1404
  for (const name of tableNames)
1273
1405
  delete snapshot.tables?.[`public.${name}`];
1274
- await writeFile5(path, `${JSON.stringify(snapshot, null, 2)}
1406
+ await writeFile6(path, `${JSON.stringify(snapshot, null, 2)}
1275
1407
  `);
1276
1408
  }
1277
1409
  }
1278
1410
  } catch (error) {
1279
- if (!isMissing(error))
1411
+ if (!isMissing2(error))
1280
1412
  throw error;
1281
1413
  }
1282
1414
  }
@@ -1285,10 +1417,10 @@ async function collectFiles2(root) {
1285
1417
  await visit(root);
1286
1418
  return files.sort();
1287
1419
  async function visit(directory) {
1288
- for (const name of (await readdir2(directory)).sort()) {
1420
+ for (const name of (await readdir3(directory)).sort()) {
1289
1421
  if ([".git", "node_modules", "dist"].includes(name))
1290
1422
  continue;
1291
- const path = join6(directory, name);
1423
+ const path = join7(directory, name);
1292
1424
  const metadata = await lstat2(path);
1293
1425
  if (metadata.isSymbolicLink())
1294
1426
  continue;
@@ -1305,7 +1437,7 @@ function relativePath(root, path) {
1305
1437
  function escapeRegExp(value) {
1306
1438
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1307
1439
  }
1308
- function isMissing(error) {
1440
+ function isMissing2(error) {
1309
1441
  return error instanceof Error && "code" in error && error.code === "ENOENT";
1310
1442
  }
1311
1443
 
@@ -1604,7 +1736,8 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
1604
1736
  prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
1605
1737
  } : {},
1606
1738
  dev: "docker compose up",
1607
- "dev:build": "docker compose up --build",
1739
+ "docker:sync": "create-windy sync-docker",
1740
+ "dev:build": "bun run docker:sync && docker compose up --build",
1608
1741
  ...generatorVersion ? { windy: "create-windy" } : {},
1609
1742
  "dev:web": "bun run --cwd apps/web dev",
1610
1743
  "dev:server": "bun run --cwd apps/server dev",
@@ -1725,21 +1858,21 @@ var excludedRuntimeFiles = new Set([
1725
1858
  ]);
1726
1859
 
1727
1860
  // src/provenance.ts
1728
- import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1729
- 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";
1730
1863
  var templateMetadataFile = ".windy-template.json";
1731
1864
  var generationMetadataFile = ".windy/generation.json";
1732
1865
  async function readTemplateMetadata(sourceRoot) {
1733
- const path = join7(sourceRoot, templateMetadataFile);
1734
- const parsed = JSON.parse(await readFile7(path, "utf8"));
1866
+ const path = join8(sourceRoot, templateMetadataFile);
1867
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
1735
1868
  if (!isTemplateMetadata(parsed)) {
1736
1869
  throw new Error("create-windy 模板来源元数据无效");
1737
1870
  }
1738
1871
  return parsed;
1739
1872
  }
1740
1873
  async function readGenerationProvenance(targetDirectory) {
1741
- const path = join7(targetDirectory, generationMetadataFile);
1742
- const parsed = JSON.parse(await readFile7(path, "utf8"));
1874
+ const path = join8(targetDirectory, generationMetadataFile);
1875
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
1743
1876
  if (!isGenerationProvenance(parsed)) {
1744
1877
  throw new Error("当前目录不是受支持的 Windy 项目:Project Lock 无效");
1745
1878
  }
@@ -1771,9 +1904,9 @@ async function writeGenerationProvenance(targetDirectory, metadata, profile, sel
1771
1904
  includeOxc
1772
1905
  }
1773
1906
  };
1774
- const path = join7(targetDirectory, generationMetadataFile);
1775
- await mkdir2(join7(path, ".."), { recursive: true });
1776
- 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)}
1777
1910
  `);
1778
1911
  }
1779
1912
  function isTemplateMetadata(value) {
@@ -1798,8 +1931,8 @@ function isModuleVersion(value) {
1798
1931
  }
1799
1932
 
1800
1933
  // src/registry-dependencies.ts
1801
- import { lstat as lstat3, readFile as readFile8, readdir as readdir3, writeFile as writeFile7 } from "node:fs/promises";
1802
- 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";
1803
1936
  var dependencySections = [
1804
1937
  "dependencies",
1805
1938
  "devDependencies",
@@ -1827,14 +1960,14 @@ async function materializeRegistryDependencies(root, dependencies) {
1827
1960
  await rewritePackageJson(path, dependencies);
1828
1961
  continue;
1829
1962
  }
1830
- const source = await readFile8(path, "utf8");
1963
+ const source = await readFile9(path, "utf8");
1831
1964
  const output = dependencies.reduce((value, dependency) => value.replaceAll(dependency.sourceName, dependency.name), source);
1832
1965
  if (output !== source)
1833
- await writeFile7(path, output);
1966
+ await writeFile8(path, output);
1834
1967
  }
1835
1968
  }
1836
1969
  async function rewritePackageJson(path, registryDependencies) {
1837
- const value = JSON.parse(await readFile8(path, "utf8"));
1970
+ const value = JSON.parse(await readFile9(path, "utf8"));
1838
1971
  let changed = false;
1839
1972
  for (const section of dependencySections) {
1840
1973
  const dependencies = value[section];
@@ -1849,7 +1982,7 @@ async function rewritePackageJson(path, registryDependencies) {
1849
1982
  }
1850
1983
  }
1851
1984
  if (changed)
1852
- await writeFile7(path, `${JSON.stringify(value, null, 2)}
1985
+ await writeFile8(path, `${JSON.stringify(value, null, 2)}
1853
1986
  `);
1854
1987
  }
1855
1988
  async function collectFiles3(root) {
@@ -1857,10 +1990,10 @@ async function collectFiles3(root) {
1857
1990
  await visit(root);
1858
1991
  return files.sort();
1859
1992
  async function visit(directory) {
1860
- for (const name of (await readdir3(directory)).sort()) {
1993
+ for (const name of (await readdir4(directory)).sort()) {
1861
1994
  if ([".git", "node_modules", "dist"].includes(name))
1862
1995
  continue;
1863
- const path = join8(directory, name);
1996
+ const path = join9(directory, name);
1864
1997
  const metadata = await lstat3(path);
1865
1998
  if (metadata.isSymbolicLink())
1866
1999
  continue;
@@ -1884,39 +2017,39 @@ async function generateProject(input) {
1884
2017
  const selectedModules = input.selectedModules ?? templateMetadata.modules.filter(({ name }) => requestedIncludeExample || name !== "work-order").map(({ name }) => name);
1885
2018
  const includeExample = selectedModules.includes("work-order");
1886
2019
  for (const relativePath2 of generationTemplatePaths(true, includeOxc)) {
1887
- await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join9(input.targetDirectory, relativePath2));
2020
+ await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join10(input.targetDirectory, relativePath2));
1888
2021
  }
1889
2022
  await materializeSelectedModules(input.targetDirectory, selectedModules);
1890
2023
  await materializeRegistryDependencies(input.targetDirectory, selectedModuleManifests(selectedModules).flatMap(({ registryDependencies }) => registryDependencies));
1891
- const packagePath = join9(input.targetDirectory, "package.json");
1892
- const packageJson = JSON.parse(await readFile9(packagePath, "utf8"));
1893
- 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)}
1894
2027
  `);
1895
- await writeFile8(join9(input.targetDirectory, "docker-compose.yml"), starterCompose);
1896
- await writeFile8(join9(input.targetDirectory, ".env.example"), environmentExample(includeExample));
1897
- await writeFile8(join9(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
1898
- 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")));
1899
2032
  await reflectCodeQualityChoice(input.targetDirectory, includeOxc);
1900
2033
  await applyWindyBaseLicense(input.targetDirectory);
1901
2034
  await applyProjectLicense(input.targetDirectory, projectLicense, input.licenseHolder);
1902
2035
  await prepareCustomerDockerfiles(input.targetDirectory);
1903
2036
  await writeGenerationProvenance(input.targetDirectory, templateMetadata, input.profile ?? starterProfile.key, selectedModules, includeOxc, projectLicense);
1904
- await rm3(join9(input.targetDirectory, "bun.lock"), { force: true });
2037
+ await rm3(join10(input.targetDirectory, "bun.lock"), { force: true });
1905
2038
  await writeManagedFilesManifest(input.targetDirectory, templateMetadata.generatorVersion);
1906
2039
  }
1907
2040
  async function resolveTemplateSource(sourceRoot, relativePath2) {
1908
- const source = join9(sourceRoot, relativePath2);
2041
+ const source = join10(sourceRoot, relativePath2);
1909
2042
  try {
1910
2043
  await lstat4(source);
1911
2044
  return source;
1912
2045
  } catch (error) {
1913
- if (!isMissing2(error))
2046
+ if (!isMissing3(error))
1914
2047
  throw error;
1915
2048
  }
1916
2049
  const alias = npmTemplateAliases[relativePath2];
1917
2050
  if (!alias)
1918
2051
  return source;
1919
- return join9(sourceRoot, alias);
2052
+ return join10(sourceRoot, alias);
1920
2053
  }
1921
2054
  async function assertGeneratedProjectIsClean(targetDirectory) {
1922
2055
  const violations = [];
@@ -1941,11 +2074,11 @@ function validateProjectName(projectName) {
1941
2074
  }
1942
2075
  async function assertTargetAvailable(targetDirectory) {
1943
2076
  try {
1944
- const entries = await readdir4(targetDirectory);
2077
+ const entries = await readdir5(targetDirectory);
1945
2078
  if (entries.length)
1946
2079
  throw new Error("目标目录不是空目录");
1947
2080
  } catch (error) {
1948
- if (isMissing2(error))
2081
+ if (isMissing3(error))
1949
2082
  return;
1950
2083
  throw error;
1951
2084
  }
@@ -1957,26 +2090,26 @@ async function copyTree(source, target) {
1957
2090
  return;
1958
2091
  if (metadata.isDirectory()) {
1959
2092
  await mkdir3(target, { recursive: true });
1960
- for (const child of await readdir4(source)) {
1961
- 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));
1962
2095
  }
1963
2096
  return;
1964
2097
  }
1965
- await mkdir3(join9(target, ".."), { recursive: true });
2098
+ await mkdir3(join10(target, ".."), { recursive: true });
1966
2099
  await copyFile(source, target);
1967
2100
  }
1968
2101
  function shouldExclude(name) {
1969
2102
  return excludedNames.has(name) || excludedRuntimeFiles.has(name);
1970
2103
  }
1971
2104
  async function walk(directory, visit) {
1972
- for (const name of await readdir4(directory)) {
1973
- const path = join9(directory, name);
2105
+ for (const name of await readdir5(directory)) {
2106
+ const path = join10(directory, name);
1974
2107
  await visit(path);
1975
2108
  if ((await lstat4(path)).isDirectory())
1976
2109
  await walk(path, visit);
1977
2110
  }
1978
2111
  }
1979
- function isMissing2(error) {
2112
+ function isMissing3(error) {
1980
2113
  return error instanceof Error && "code" in error && error.code === "ENOENT";
1981
2114
  }
1982
2115
 
@@ -2049,9 +2182,9 @@ async function createProject(options, dependencies = {}) {
2049
2182
  }
2050
2183
  async function inspectTarget(path) {
2051
2184
  try {
2052
- return (await readdir5(path)).length === 0 ? "empty" : "non-empty";
2185
+ return (await readdir6(path)).length === 0 ? "empty" : "non-empty";
2053
2186
  } catch (error) {
2054
- if (isMissing3(error))
2187
+ if (isMissing4(error))
2055
2188
  return "missing";
2056
2189
  throw error;
2057
2190
  }
@@ -2073,7 +2206,7 @@ function stageError(stage, error) {
2073
2206
  function sanitizeReason(reason) {
2074
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:[已隐藏]@");
2075
2208
  }
2076
- function isMissing3(error) {
2209
+ function isMissing4(error) {
2077
2210
  return error instanceof Error && "code" in error && error.code === "ENOENT";
2078
2211
  }
2079
2212
 
@@ -3821,8 +3954,8 @@ import { rm as rm8 } from "node:fs/promises";
3821
3954
  import { resolve as resolve5 } from "node:path";
3822
3955
 
3823
3956
  // src/update/doctor.ts
3824
- import { readFile as readFile11 } from "node:fs/promises";
3825
- import { join as join11 } from "node:path";
3957
+ import { readFile as readFile12 } from "node:fs/promises";
3958
+ import { join as join12 } from "node:path";
3826
3959
 
3827
3960
  // src/update/process.ts
3828
3961
  import { spawn as spawn3 } from "node:child_process";
@@ -3854,12 +3987,12 @@ import {
3854
3987
  access as access2,
3855
3988
  copyFile as copyFile2,
3856
3989
  mkdir as mkdir5,
3857
- readFile as readFile10,
3990
+ readFile as readFile11,
3858
3991
  rm as rm5,
3859
3992
  rmdir,
3860
- writeFile as writeFile9
3993
+ writeFile as writeFile10
3861
3994
  } from "node:fs/promises";
3862
- 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";
3863
3996
  var updateStatePath = ".windy/update-state.json";
3864
3997
  async function applyTransaction(plan) {
3865
3998
  if (plan.conflicts.length) {
@@ -3868,21 +4001,22 @@ async function applyTransaction(plan) {
3868
4001
  const changed = plan.files.filter(({ action }) => action === "create" || action === "update" || action === "delete");
3869
4002
  for (const file of changed)
3870
4003
  assertSafePath(file.path);
3871
- const backupDirectory = join10(plan.projectDirectory, ".windy/backups", plan.id);
4004
+ const backupDirectory = join11(plan.projectDirectory, ".windy/backups", plan.id);
3872
4005
  const protectedPaths = new Set([
3873
4006
  ...changed.map(({ path: path2 }) => path2),
3874
4007
  ".windy/generation.json",
3875
4008
  ".windy/files.json",
4009
+ "apps/web/Dockerfile",
3876
4010
  "bun.lock"
3877
4011
  ]);
3878
4012
  const backupEntries = [];
3879
4013
  for (const path2 of protectedPaths) {
3880
- const source = join10(plan.projectDirectory, path2);
4014
+ const source = join11(plan.projectDirectory, path2);
3881
4015
  const existed = await exists(source);
3882
4016
  backupEntries.push({ path: path2, existed });
3883
4017
  if (!existed)
3884
4018
  continue;
3885
- const target = join10(backupDirectory, "files", path2);
4019
+ const target = join11(backupDirectory, "files", path2);
3886
4020
  await mkdir5(dirname3(target), { recursive: true });
3887
4021
  await copyFile2(source, target);
3888
4022
  }
@@ -3895,7 +4029,7 @@ async function applyTransaction(plan) {
3895
4029
  await writeState(plan.projectDirectory, state);
3896
4030
  try {
3897
4031
  for (const file of changed) {
3898
- const target = join10(plan.projectDirectory, file.path);
4032
+ const target = join11(plan.projectDirectory, file.path);
3899
4033
  if (file.action === "delete") {
3900
4034
  const existed = await exists(target);
3901
4035
  await rm5(target, { force: true });
@@ -3907,7 +4041,7 @@ async function applyTransaction(plan) {
3907
4041
  if (!file.content)
3908
4042
  throw new Error(`更新计划缺少文件内容:${file.path}`);
3909
4043
  await mkdir5(dirname3(target), { recursive: true });
3910
- await writeFile9(target, file.content);
4044
+ await writeFile10(target, file.content);
3911
4045
  }
3912
4046
  } catch (error) {
3913
4047
  await restoreUpdateState(plan.projectDirectory, state);
@@ -3922,7 +4056,7 @@ async function applyTransaction(plan) {
3922
4056
  }
3923
4057
  async function readUpdateState(projectDirectory) {
3924
4058
  try {
3925
- const parsed = JSON.parse(await readFile10(join10(projectDirectory, updateStatePath), "utf8"));
4059
+ const parsed = JSON.parse(await readFile11(join11(projectDirectory, updateStatePath), "utf8"));
3926
4060
  if (!isUpdateState(parsed))
3927
4061
  throw new Error("更新恢复状态无效,请人工检查 .windy 目录");
3928
4062
  return parsed;
@@ -3935,25 +4069,25 @@ async function readUpdateState(projectDirectory) {
3935
4069
  async function restoreUpdateState(projectDirectory, state) {
3936
4070
  for (const entry of state.entries) {
3937
4071
  assertSafePath(entry.path);
3938
- const target = join10(projectDirectory, entry.path);
4072
+ const target = join11(projectDirectory, entry.path);
3939
4073
  if (!entry.existed) {
3940
4074
  await rm5(target, { force: true });
3941
4075
  continue;
3942
4076
  }
3943
- const backup = join10(state.backupDirectory, "files", entry.path);
4077
+ const backup = join11(state.backupDirectory, "files", entry.path);
3944
4078
  await mkdir5(dirname3(target), { recursive: true });
3945
4079
  await copyFile2(backup, target);
3946
4080
  }
3947
- await rm5(join10(projectDirectory, updateStatePath), { force: true });
4081
+ await rm5(join11(projectDirectory, updateStatePath), { force: true });
3948
4082
  await rm5(state.backupDirectory, { recursive: true, force: true });
3949
4083
  }
3950
4084
  async function clearUpdateState(projectDirectory) {
3951
- await rm5(join10(projectDirectory, updateStatePath), { force: true });
4085
+ await rm5(join11(projectDirectory, updateStatePath), { force: true });
3952
4086
  }
3953
4087
  async function writeState(projectDirectory, state) {
3954
- const path2 = join10(projectDirectory, updateStatePath);
4088
+ const path2 = join11(projectDirectory, updateStatePath);
3955
4089
  await mkdir5(dirname3(path2), { recursive: true });
3956
- await writeFile9(path2, `${JSON.stringify(state, null, 2)}
4090
+ await writeFile10(path2, `${JSON.stringify(state, null, 2)}
3957
4091
  `);
3958
4092
  }
3959
4093
  async function exists(path2) {
@@ -4021,7 +4155,7 @@ async function diagnoseProject(projectDirectory, repair = false) {
4021
4155
  if (file.ownership === "project-owned")
4022
4156
  continue;
4023
4157
  try {
4024
- const changed = hashContent(await readFile11(join11(projectDirectory, file.path))) !== file.hash;
4158
+ const changed = hashContent(await readFile12(join12(projectDirectory, file.path))) !== file.hash;
4025
4159
  if (!changed)
4026
4160
  continue;
4027
4161
  modifiedManagedFiles.push(file.path);
@@ -4070,7 +4204,8 @@ var recipes = [
4070
4204
  { id: "starter-0.2.15-to-0.2.16", from: "0.2.15", to: "0.2.16" },
4071
4205
  { id: "starter-0.2.16-to-0.2.17", from: "0.2.16", to: "0.2.17" },
4072
4206
  { 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" }
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" }
4074
4209
  ];
4075
4210
  function resolveRecipeChain(sourceVersion, targetVersion) {
4076
4211
  if (sourceVersion === targetVersion)
@@ -4095,19 +4230,19 @@ function resolveRecipeChain(sourceVersion, targetVersion) {
4095
4230
  }
4096
4231
 
4097
4232
  // src/update/updater.ts
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";
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";
4100
4235
 
4101
4236
  // src/update/artifacts.ts
4102
- 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";
4103
4238
  import { tmpdir } from "node:os";
4104
- import { basename as basename4, join as join12 } from "node:path";
4239
+ import { basename as basename4, join as join13 } from "node:path";
4105
4240
  class PublishedArtifactProvider {
4106
4241
  async prepare(projectDirectory, provenance) {
4107
- const temporaryRoot = await mkdtemp(join12(tmpdir(), "windy-update-"));
4242
+ const temporaryRoot = await mkdtemp(join13(tmpdir(), "windy-update-"));
4108
4243
  const projectName = await readProjectName2(projectDirectory);
4109
- const baseDirectory = join12(temporaryRoot, "base", projectName);
4110
- const targetDirectory = join12(temporaryRoot, "target", projectName);
4244
+ const baseDirectory = join13(temporaryRoot, "base", projectName);
4245
+ const targetDirectory = join13(temporaryRoot, "target", projectName);
4111
4246
  await generatePublishedBase(temporaryRoot, projectName, provenance, baseDirectory);
4112
4247
  await generateProject({
4113
4248
  sourceRoot: await resolveTemplateRoot(),
@@ -4124,7 +4259,7 @@ class PublishedArtifactProvider {
4124
4259
  }
4125
4260
  }
4126
4261
  async function generatePublishedBase(temporaryRoot, projectName, provenance, expectedDirectory) {
4127
- const parent = join12(temporaryRoot, "base");
4262
+ const parent = join13(temporaryRoot, "base");
4128
4263
  await mkdir6(parent, { recursive: true });
4129
4264
  const args = [
4130
4265
  "--bun",
@@ -4148,18 +4283,18 @@ async function generatePublishedBase(temporaryRoot, projectName, provenance, exp
4148
4283
  }
4149
4284
  }
4150
4285
  await assertProcess("bunx", args, parent);
4151
- if (join12(parent, projectName) !== expectedDirectory) {
4286
+ if (join13(parent, projectName) !== expectedDirectory) {
4152
4287
  throw new Error("历史 artifact 生成目录异常");
4153
4288
  }
4154
4289
  }
4155
4290
  async function readProjectName2(projectDirectory) {
4156
- const source = JSON.parse(await readFile12(join12(projectDirectory, "package.json"), "utf8"));
4291
+ const source = JSON.parse(await readFile13(join13(projectDirectory, "package.json"), "utf8"));
4157
4292
  return source.name || basename4(projectDirectory);
4158
4293
  }
4159
4294
 
4160
4295
  // src/update/planner.ts
4161
- import { access as access3, readFile as readFile14 } from "node:fs/promises";
4162
- import { join as join15 } from "node:path";
4296
+ import { access as access3, readFile as readFile15 } from "node:fs/promises";
4297
+ import { join as join16 } from "node:path";
4163
4298
 
4164
4299
  // src/update/known-repairs.ts
4165
4300
  var wrongTextVersions = new Set(["0.2.12", "0.2.13"]);
@@ -4202,19 +4337,19 @@ function normalizeTableCell(cell) {
4202
4337
  }
4203
4338
 
4204
4339
  // src/update/merge.ts
4205
- 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";
4206
4341
  import { tmpdir as tmpdir2 } from "node:os";
4207
- import { join as join13 } from "node:path";
4342
+ import { join as join14 } from "node:path";
4208
4343
  async function mergeTextFile(base, ours, theirs) {
4209
- const root = await mkdtemp2(join13(tmpdir2(), "windy-merge-"));
4344
+ const root = await mkdtemp2(join14(tmpdir2(), "windy-merge-"));
4210
4345
  try {
4211
- const oursPath = join13(root, "ours");
4212
- const basePath = join13(root, "base");
4213
- const theirsPath = join13(root, "theirs");
4346
+ const oursPath = join14(root, "ours");
4347
+ const basePath = join14(root, "base");
4348
+ const theirsPath = join14(root, "theirs");
4214
4349
  await Promise.all([
4215
- writeFile10(oursPath, ours),
4216
- writeFile10(basePath, base),
4217
- writeFile10(theirsPath, theirs)
4350
+ writeFile11(oursPath, ours),
4351
+ writeFile11(basePath, base),
4352
+ writeFile11(theirsPath, theirs)
4218
4353
  ]);
4219
4354
  const result = await captureProcess("git", ["merge-file", "-p", "--diff3", oursPath, basePath, theirsPath], root);
4220
4355
  if (result.code === 0) {
@@ -4382,8 +4517,8 @@ function isTableSeparator(cell) {
4382
4517
  }
4383
4518
 
4384
4519
  // src/update/migration-reconciler.ts
4385
- import { readFile as readFile13 } from "node:fs/promises";
4386
- import { join as join14 } from "node:path";
4520
+ import { readFile as readFile14 } from "node:fs/promises";
4521
+ import { join as join15 } from "node:path";
4387
4522
 
4388
4523
  // src/update/snapshot-delta.ts
4389
4524
  function applySnapshotDelta(before, after, current, path2, conflicts, conflictPath) {
@@ -4490,7 +4625,7 @@ async function reconcileDrizzleMigrations(input) {
4490
4625
  const tag = `${prefix}_${name}`;
4491
4626
  const sqlPath = `${drizzleRoot}/${tag}.sql`;
4492
4627
  const snapshotPath = `${drizzleRoot}/meta/${prefix}_snapshot.json`;
4493
- const sql = await readFile13(join14(input.targetDirectory, `${drizzleRoot}/${addition.tag}.sql`));
4628
+ const sql = await readFile14(join15(input.targetDirectory, `${drizzleRoot}/${addition.tag}.sql`));
4494
4629
  merged.id = targetAfter.id;
4495
4630
  merged.prevId = currentSnapshot.id;
4496
4631
  const snapshot = encodeJson(merged);
@@ -4537,13 +4672,13 @@ function unchanged(targetManifest) {
4537
4672
  async function readJournals(input) {
4538
4673
  try {
4539
4674
  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))
4675
+ readJson(join15(input.baseDirectory, journalPath)),
4676
+ readJson(join15(input.projectDirectory, journalPath)),
4677
+ readJson(join15(input.targetDirectory, journalPath))
4543
4678
  ]);
4544
4679
  return { base, project, target };
4545
4680
  } catch (error) {
4546
- if (isMissing4(error))
4681
+ if (isMissing5(error))
4547
4682
  return;
4548
4683
  throw error;
4549
4684
  }
@@ -4551,7 +4686,7 @@ async function readJournals(input) {
4551
4686
  async function preservePublishedHistory(manifest, projectDirectory, overrides) {
4552
4687
  const entries = manifest.files.filter(({ path: path2 }) => isMigrationArtifact(path2) && path2 !== journalPath);
4553
4688
  await Promise.all(entries.map(async ({ path: path2 }) => {
4554
- overrides.set(path2, await readFile13(join14(projectDirectory, path2)));
4689
+ overrides.set(path2, await readFile14(join15(projectDirectory, path2)));
4555
4690
  }));
4556
4691
  return entries;
4557
4692
  }
@@ -4565,7 +4700,7 @@ async function readSnapshot(root, tag) {
4565
4700
  const match = migrationPattern.exec(tag);
4566
4701
  if (!match)
4567
4702
  throw new Error(`${journalPath}:migration tag 格式无效:${tag}`);
4568
- return await readJson(join14(root, `${drizzleRoot}/meta/${match[1]}_snapshot.json`));
4703
+ return await readJson(join15(root, `${drizzleRoot}/meta/${match[1]}_snapshot.json`));
4569
4704
  }
4570
4705
  function validateUniqueNames(journal, label, conflicts) {
4571
4706
  const seen = new Set;
@@ -4606,9 +4741,9 @@ function encodeJson(value) {
4606
4741
  `);
4607
4742
  }
4608
4743
  async function readJson(path2) {
4609
- return JSON.parse(await readFile13(path2, "utf8"));
4744
+ return JSON.parse(await readFile14(path2, "utf8"));
4610
4745
  }
4611
- function isMissing4(error) {
4746
+ function isMissing5(error) {
4612
4747
  return error instanceof Error && "code" in error && error.code === "ENOENT";
4613
4748
  }
4614
4749
 
@@ -4729,9 +4864,9 @@ async function createUpdatePlan(request, artifacts) {
4729
4864
  }
4730
4865
  async function planFile(path2, project, baseRoot, targetRoot, source, target, provenance, targetContentOverride) {
4731
4866
  const [ours, base, theirs] = await Promise.all([
4732
- readOptional(join15(project, path2)),
4733
- readOptional(join15(baseRoot, path2)),
4734
- targetContentOverride ? Promise.resolve(targetContentOverride) : readOptional(join15(targetRoot, path2))
4867
+ readOptional(join16(project, path2)),
4868
+ readOptional(join16(baseRoot, path2)),
4869
+ targetContentOverride ? Promise.resolve(targetContentOverride) : readOptional(join16(targetRoot, path2))
4735
4870
  ]);
4736
4871
  const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
4737
4872
  const repaired = ours && theirs && provenance ? repairKnownDerivedFile({
@@ -4813,7 +4948,7 @@ function conflict(path2, ownership, reason) {
4813
4948
  }
4814
4949
  async function readOptional(path2) {
4815
4950
  try {
4816
- return await readFile14(path2);
4951
+ return await readFile15(path2);
4817
4952
  } catch (error) {
4818
4953
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
4819
4954
  return;
@@ -4829,7 +4964,7 @@ async function assertGitClean(projectDirectory) {
4829
4964
  }
4830
4965
  async function assertNoPendingUpdate(projectDirectory) {
4831
4966
  try {
4832
- await access3(join15(projectDirectory, ".windy/update-state.json"));
4967
+ await access3(join16(projectDirectory, ".windy/update-state.json"));
4833
4968
  throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
4834
4969
  } catch (error) {
4835
4970
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
@@ -4863,9 +4998,17 @@ class DefaultStarterUpdater {
4863
4998
  async verify(result2) {
4864
4999
  const steps = [];
4865
5000
  try {
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
+ });
4866
5009
  await this.#execute("bun", ["install", "--force"], result2.plan.projectDirectory);
4867
5010
  steps.push({ name: "bun install --force", status: "passed" });
4868
- const packageJson = JSON.parse(await readFile15(join16(result2.plan.projectDirectory, "package.json"), "utf8"));
5011
+ const packageJson = JSON.parse(await readFile16(join17(result2.plan.projectDirectory, "package.json"), "utf8"));
4869
5012
  for (const name of ["lint", "typecheck", "test", "build"]) {
4870
5013
  if (!packageJson.scripts?.[name]) {
4871
5014
  steps.push({ name: `bun run ${name}`, status: "skipped" });
@@ -4896,12 +5039,12 @@ async function finalize(result2) {
4896
5039
  const root = result2.plan.projectDirectory;
4897
5040
  const generationContent = `${JSON.stringify(result2.plan.targetProvenance, null, 2)}
4898
5041
  `;
4899
- await writeFile11(join16(root, ".windy/generation.json"), generationContent);
5042
+ await writeFile12(join17(root, ".windy/generation.json"), generationContent);
4900
5043
  const refreshedPaths = new Set([
4901
5044
  ".windy/generation.json",
4902
- ...result2.plan.files.filter(({ action }) => action === "create" || action === "update").map(({ path: path2 }) => path2)
5045
+ ...result2.changedFiles
4903
5046
  ]);
4904
- const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile15(join16(root, path2)))])));
5047
+ const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile16(join17(root, path2)))])));
4905
5048
  const targetManifest = {
4906
5049
  ...result2.plan.targetManifest,
4907
5050
  files: result2.plan.targetManifest.files.map((entry) => ({
@@ -4909,11 +5052,15 @@ async function finalize(result2) {
4909
5052
  hash: refreshedHashes.get(entry.path) ?? entry.hash
4910
5053
  }))
4911
5054
  };
4912
- await writeFile11(join16(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
5055
+ await writeFile12(join17(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
4913
5056
  `);
4914
5057
  }
5058
+ function addChangedFile(result2, path2) {
5059
+ if (!result2.changedFiles.includes(path2))
5060
+ result2.changedFiles.push(path2);
5061
+ }
4915
5062
  async function writeReport(result2, steps, relativePath2) {
4916
- const path2 = join16(result2.plan.projectDirectory, relativePath2);
5063
+ const path2 = join17(result2.plan.projectDirectory, relativePath2);
4917
5064
  await mkdir7(dirname4(path2), { recursive: true });
4918
5065
  const lines = [
4919
5066
  `# Windy 更新报告 ${result2.plan.id}`,
@@ -4931,16 +5078,27 @@ async function writeReport(result2, steps, relativePath2) {
4931
5078
  `备份保留在 \`.windy/backups/${result2.plan.id}\`,确认稳定后可人工删除。`,
4932
5079
  ""
4933
5080
  ];
4934
- await writeFile11(path2, lines.join(`
5081
+ await writeFile12(path2, lines.join(`
4935
5082
  `));
4936
5083
  }
4937
5084
 
4938
5085
  // src/update/cli.ts
4939
5086
  async function runLifecycleCommand(args) {
4940
5087
  const command = args[0];
4941
- if (command !== "doctor" && command !== "update")
5088
+ if (command !== "doctor" && command !== "update" && command !== "sync-docker") {
4942
5089
  return false;
5090
+ }
4943
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
+ }
4944
5102
  if (command === "doctor") {
4945
5103
  const report = await diagnoseProject(projectDirectory, args.includes("--repair"));
4946
5104
  printDoctor(report);