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.
- package/README.md +4 -0
- package/dist/cli.js +752 -186
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/application-services.ts +2 -2
- package/template/apps/server/src/audit/search-summary.ts +15 -8
- package/template/apps/server/src/example-modules.ts +58 -70
- package/template/apps/server/src/foundation.ts +8 -4
- package/template/apps/server/src/guards.test.ts +13 -0
- package/template/apps/server/src/guards.ts +12 -2
- package/template/apps/server/src/index.ts +48 -91
- package/template/apps/server/src/installed-business-modules.ts +23 -4
- package/template/apps/server/src/module-bootstrap.ts +83 -0
- package/template/apps/server/src/module-composition.test.ts +45 -1
- package/template/apps/server/src/module-composition.ts +45 -0
- package/template/apps/server/src/module-host/initialize.test.ts +123 -0
- package/template/apps/server/src/module-host/initialize.ts +331 -0
- package/template/apps/server/src/module-host/request-context.test.ts +66 -0
- package/template/apps/server/src/module-host/request-context.ts +156 -0
- package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
- package/template/apps/server/src/module-host/role-presets.ts +76 -0
- package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
- package/template/apps/server/src/module-host/route-installer.ts +97 -0
- package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
- package/template/apps/server/src/module-host/search-providers.ts +31 -0
- package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
- package/template/apps/server/src/module-host/storage-port.ts +59 -0
- package/template/apps/server/src/module-host/task-definitions.ts +41 -0
- package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
- package/template/apps/server/src/module-host.test.ts +202 -58
- package/template/apps/server/src/module-host.ts +34 -113
- package/template/apps/server/src/route-guards.test.ts +112 -1
- package/template/apps/server/src/runtime-feature.ts +66 -31
- package/template/apps/server/src/runtime.test.ts +1 -0
- package/template/apps/server/src/runtime.ts +3 -1
- package/template/apps/server/src/system/built-in-roles.ts +1 -1
- package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
- package/template/apps/server/src/work-order/routes.test.ts +30 -18
- package/template/apps/server/src/work-order/routes.ts +104 -135
- package/template/apps/server/src/work-order/search-api.test.ts +14 -3
- package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
- package/template/apps/server/src/work-order/search-provider.ts +17 -12
- package/template/apps/server/src/work-order/task.test.ts +16 -11
- package/template/apps/web/Dockerfile +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
- package/template/docs/architecture/object-storage.md +12 -0
- package/template/packages/config/index.test.ts +57 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +28 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/runner.ts +7 -9
- package/template/packages/example-work-order/index.ts +1 -0
- package/template/packages/example-work-order/package.json +2 -0
- package/template/packages/example-work-order/src/server-module.ts +61 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/types.ts +7 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/catalog-validation.test.ts +212 -0
- package/template/packages/modules/src/catalog-validation.ts +19 -2
- package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
- package/template/packages/server-sdk/index.ts +42 -0
- package/template/packages/server-sdk/package.json +16 -3
- package/template/packages/server-sdk/src/actor.ts +15 -0
- package/template/packages/server-sdk/src/audit-port.ts +22 -0
- package/template/packages/server-sdk/src/jobs-port.ts +24 -0
- package/template/packages/server-sdk/src/module-host.ts +36 -1
- package/template/packages/server-sdk/src/namespace.test.ts +26 -0
- package/template/packages/server-sdk/src/namespace.ts +13 -0
- package/template/packages/server-sdk/src/request-context.ts +21 -0
- package/template/packages/server-sdk/src/role-preset.ts +18 -0
- package/template/packages/server-sdk/src/route-definition.ts +26 -0
- package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
- package/template/packages/server-sdk/src/storage-port.ts +25 -0
- package/template/packages/server-sdk/src/task-registration.ts +18 -0
- package/template/packages/server-sdk/src/tool-registration.ts +29 -0
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-catalog.test.ts +73 -1
- package/template/packages/shared/src/license-catalog.ts +95 -0
- package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
- 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
|
|
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
|
|
924
|
-
readdir as
|
|
923
|
+
readFile as readFile10,
|
|
924
|
+
readdir as readdir5,
|
|
925
925
|
rm as rm3,
|
|
926
|
-
writeFile as
|
|
926
|
+
writeFile as writeFile9
|
|
927
927
|
} from "node:fs/promises";
|
|
928
|
-
import { basename as basename3, extname as extname3, join as
|
|
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
|
|
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",
|
|
941
|
-
const absolutePath =
|
|
942
|
-
const source = await
|
|
943
|
-
await
|
|
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
|
|
957
|
-
import { join as
|
|
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 =
|
|
967
|
-
const source = await
|
|
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
|
|
1105
|
+
await writeFile3(path, source.replace(oxcStarterPolicy, noCodeQualityPolicy));
|
|
974
1106
|
}
|
|
975
1107
|
|
|
976
1108
|
// src/license-files.ts
|
|
977
|
-
import { mkdir, readFile as
|
|
978
|
-
import { dirname, join as
|
|
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 =
|
|
1116
|
+
const target = join5(targetDirectory, windyLicensePath);
|
|
985
1117
|
await mkdir(dirname(target), { recursive: true });
|
|
986
|
-
await
|
|
1118
|
+
await writeFile4(target, await readFile5(apacheLicensePath));
|
|
987
1119
|
}
|
|
988
1120
|
async function applyProjectLicense(targetDirectory, license, holder, year = new Date().getFullYear()) {
|
|
989
|
-
const target =
|
|
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
|
|
1131
|
+
await writeFile4(target, mitLicense(year, owner));
|
|
1000
1132
|
return;
|
|
1001
1133
|
}
|
|
1002
1134
|
await mkdir(targetDirectory, { recursive: true });
|
|
1003
|
-
await
|
|
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
|
|
1033
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1233
|
+
for (const name of (await readdir2(directory)).sort()) {
|
|
1096
1234
|
if (ignoredDirectoryNames.has(name))
|
|
1097
1235
|
continue;
|
|
1098
|
-
const path =
|
|
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
|
|
1133
|
-
import { basename, extname, join as
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1406
|
+
await writeFile6(path, `${JSON.stringify(snapshot, null, 2)}
|
|
1269
1407
|
`);
|
|
1270
1408
|
}
|
|
1271
1409
|
}
|
|
1272
1410
|
} catch (error) {
|
|
1273
|
-
if (!
|
|
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
|
|
1420
|
+
for (const name of (await readdir3(directory)).sort()) {
|
|
1283
1421
|
if ([".git", "node_modules", "dist"].includes(name))
|
|
1284
1422
|
continue;
|
|
1285
|
-
const path =
|
|
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
|
|
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
|
-
"
|
|
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
|
|
1723
|
-
import { join as
|
|
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 =
|
|
1728
|
-
const parsed = JSON.parse(await
|
|
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 =
|
|
1736
|
-
const parsed = JSON.parse(await
|
|
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 =
|
|
1769
|
-
await mkdir2(
|
|
1770
|
-
await
|
|
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
|
|
1796
|
-
import { basename as basename2, extname as extname2, join as
|
|
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
|
|
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
|
|
1966
|
+
await writeFile8(path, output);
|
|
1828
1967
|
}
|
|
1829
1968
|
}
|
|
1830
1969
|
async function rewritePackageJson(path, registryDependencies) {
|
|
1831
|
-
const value = JSON.parse(await
|
|
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
|
|
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
|
|
1993
|
+
for (const name of (await readdir4(directory)).sort()) {
|
|
1855
1994
|
if ([".git", "node_modules", "dist"].includes(name))
|
|
1856
1995
|
continue;
|
|
1857
|
-
const path =
|
|
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),
|
|
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 =
|
|
1886
|
-
const packageJson = JSON.parse(await
|
|
1887
|
-
await
|
|
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
|
|
1890
|
-
await
|
|
1891
|
-
await
|
|
1892
|
-
await
|
|
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(
|
|
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 =
|
|
2041
|
+
const source = join10(sourceRoot, relativePath2);
|
|
1903
2042
|
try {
|
|
1904
2043
|
await lstat4(source);
|
|
1905
2044
|
return source;
|
|
1906
2045
|
} catch (error) {
|
|
1907
|
-
if (!
|
|
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
|
|
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
|
|
2077
|
+
const entries = await readdir5(targetDirectory);
|
|
1939
2078
|
if (entries.length)
|
|
1940
2079
|
throw new Error("目标目录不是空目录");
|
|
1941
2080
|
} catch (error) {
|
|
1942
|
-
if (
|
|
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
|
|
1955
|
-
await copyTree(
|
|
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(
|
|
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
|
|
1967
|
-
const path =
|
|
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
|
|
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
|
|
2185
|
+
return (await readdir6(path)).length === 0 ? "empty" : "non-empty";
|
|
2047
2186
|
} catch (error) {
|
|
2048
|
-
if (
|
|
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
|
|
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
|
|
3819
|
-
import { join as
|
|
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
|
|
3990
|
+
readFile as readFile11,
|
|
3852
3991
|
rm as rm5,
|
|
3853
3992
|
rmdir,
|
|
3854
|
-
writeFile as
|
|
3993
|
+
writeFile as writeFile10
|
|
3855
3994
|
} from "node:fs/promises";
|
|
3856
|
-
import { dirname as dirname3, join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
4085
|
+
await rm5(join11(projectDirectory, updateStatePath), { force: true });
|
|
3946
4086
|
}
|
|
3947
4087
|
async function writeState(projectDirectory, state) {
|
|
3948
|
-
const path2 =
|
|
4088
|
+
const path2 = join11(projectDirectory, updateStatePath);
|
|
3949
4089
|
await mkdir5(dirname3(path2), { recursive: true });
|
|
3950
|
-
await
|
|
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
|
|
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
|
|
4092
|
-
import { dirname as dirname4, join as
|
|
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
|
|
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
|
|
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(
|
|
4242
|
+
const temporaryRoot = await mkdtemp(join13(tmpdir(), "windy-update-"));
|
|
4101
4243
|
const projectName = await readProjectName2(projectDirectory);
|
|
4102
|
-
const baseDirectory =
|
|
4103
|
-
const targetDirectory =
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
4155
|
-
import { join as
|
|
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
|
|
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
|
|
4342
|
+
import { join as join14 } from "node:path";
|
|
4201
4343
|
async function mergeTextFile(base, ours, theirs) {
|
|
4202
|
-
const root = await mkdtemp2(
|
|
4344
|
+
const root = await mkdtemp2(join14(tmpdir2(), "windy-merge-"));
|
|
4203
4345
|
try {
|
|
4204
|
-
const oursPath =
|
|
4205
|
-
const basePath =
|
|
4206
|
-
const theirsPath =
|
|
4346
|
+
const oursPath = join14(root, "ours");
|
|
4347
|
+
const basePath = join14(root, "base");
|
|
4348
|
+
const theirsPath = join14(root, "theirs");
|
|
4207
4349
|
await Promise.all([
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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(
|
|
4329
|
-
readOptional(
|
|
4330
|
-
readOptional(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
4417
|
-
if (
|
|
4959
|
+
const result2 = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
|
|
4960
|
+
if (result2.code !== 0)
|
|
4418
4961
|
throw new Error("更新要求当前目录是有效的 Git 仓库");
|
|
4419
|
-
if (
|
|
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(
|
|
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(
|
|
4998
|
+
async verify(result2) {
|
|
4456
4999
|
const steps = [];
|
|
4457
5000
|
try {
|
|
4458
|
-
|
|
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
|
|
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],
|
|
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(
|
|
4470
|
-
const reportPath = `.windy/reports/${
|
|
4471
|
-
await writeReport(
|
|
4472
|
-
await clearUpdateState(
|
|
4473
|
-
await rm7(
|
|
4474
|
-
return { updateId:
|
|
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(
|
|
5027
|
+
await restoreUpdateState(result2.plan.projectDirectory, {
|
|
4477
5028
|
schemaVersion: 1,
|
|
4478
|
-
updateId:
|
|
4479
|
-
backupDirectory:
|
|
4480
|
-
entries:
|
|
5029
|
+
updateId: result2.plan.id,
|
|
5030
|
+
backupDirectory: result2.backupDirectory,
|
|
5031
|
+
entries: result2.backupEntries
|
|
4481
5032
|
});
|
|
4482
|
-
await rm7(
|
|
4483
|
-
throw new 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(
|
|
4488
|
-
const root =
|
|
4489
|
-
const generationContent = `${JSON.stringify(
|
|
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
|
|
5042
|
+
await writeFile12(join17(root, ".windy/generation.json"), generationContent);
|
|
4492
5043
|
const refreshedPaths = new Set([
|
|
4493
5044
|
".windy/generation.json",
|
|
4494
|
-
...
|
|
5045
|
+
...result2.changedFiles
|
|
4495
5046
|
]);
|
|
4496
|
-
const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await
|
|
5047
|
+
const refreshedHashes = new Map(await Promise.all([...refreshedPaths].map(async (path2) => [path2, hashContent(await readFile16(join17(root, path2)))])));
|
|
4497
5048
|
const targetManifest = {
|
|
4498
|
-
...
|
|
4499
|
-
files:
|
|
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
|
|
5055
|
+
await writeFile12(join17(root, ".windy/files.json"), `${JSON.stringify(targetManifest, null, 2)}
|
|
4505
5056
|
`);
|
|
4506
5057
|
}
|
|
4507
|
-
|
|
4508
|
-
|
|
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 更新报告 ${
|
|
5066
|
+
`# Windy 更新报告 ${result2.plan.id}`,
|
|
4512
5067
|
"",
|
|
4513
|
-
`- 来源版本:${
|
|
4514
|
-
`- 目标版本:${
|
|
4515
|
-
`- Recipe:${
|
|
4516
|
-
`- 修改文件:${
|
|
4517
|
-
`- Oxc 代管:${
|
|
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/${
|
|
5078
|
+
`备份保留在 \`.windy/backups/${result2.plan.id}\`,确认稳定后可人工删除。`,
|
|
4524
5079
|
""
|
|
4525
5080
|
];
|
|
4526
|
-
await
|
|
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
|
|
4588
|
-
const report = await updater.verify(
|
|
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}`);
|