create-windy 0.2.5 → 0.2.6
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 +8 -0
- package/dist/cli.js +168 -105
- package/package.json +1 -1
- package/template/.dockerignore +1 -0
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +1 -0
- package/template/README.md +15 -7
- package/template/apps/server/src/persistence.ts +2 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +3 -0
- package/template/apps/web/src/pages/ServiceUnavailablePage.vue +38 -0
- package/template/apps/web/src/pages/system/resource-config.ts +2 -0
- package/template/apps/web/src/router/auth-guard.webtest.ts +146 -0
- package/template/apps/web/src/router/auth-navigation.webtest.ts +14 -0
- package/template/apps/web/src/router/index.ts +25 -11
- package/template/apps/web/src/router/index.webtest.ts +26 -209
- package/template/apps/web/src/router/manifest-routes.webtest.ts +49 -0
- package/template/apps/web/src/router/router-test-fixtures.ts +75 -0
- package/template/package.json +1 -1
- package/template/packages/database/src/schema/governance.ts +27 -22
- package/template/packages/database/src/schema/platform-settings.ts +31 -17
- package/template/packages/modules/src/system-admin-routes.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -852,21 +852,68 @@ function compactReason(error) {
|
|
|
852
852
|
return message ? `(原因:${message})` : "";
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
+
// src/local-environment.ts
|
|
856
|
+
import { randomBytes } from "node:crypto";
|
|
857
|
+
import { open, readFile } from "node:fs/promises";
|
|
858
|
+
import { join } from "node:path";
|
|
859
|
+
async function initializeLocalEnvironment(projectRoot, generateSecret = defaultSecret) {
|
|
860
|
+
const target = join(projectRoot, ".env");
|
|
861
|
+
const [example, gitignore] = await Promise.all([
|
|
862
|
+
readFile(join(projectRoot, ".env.example"), "utf8"),
|
|
863
|
+
readFile(join(projectRoot, ".gitignore"), "utf8")
|
|
864
|
+
]);
|
|
865
|
+
if (!gitignore.split(/\r?\n/).some((line) => line.trim() === ".env")) {
|
|
866
|
+
throw new Error("生成项目的 .gitignore 未排除 .env,拒绝写入本地密码");
|
|
867
|
+
}
|
|
868
|
+
const content = setEmptyVariable(setEmptyVariable(example, "POSTGRES_PASSWORD", generateSecret()), "WINDY_BOOTSTRAP_ADMIN_PASSWORD", generateSecret());
|
|
869
|
+
let handle;
|
|
870
|
+
try {
|
|
871
|
+
handle = await open(target, "wx", 384);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (isAlreadyExists(error))
|
|
874
|
+
return "existing";
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
await handle.writeFile(content);
|
|
879
|
+
await handle.chmod(384);
|
|
880
|
+
} finally {
|
|
881
|
+
await handle.close();
|
|
882
|
+
}
|
|
883
|
+
return "created";
|
|
884
|
+
}
|
|
885
|
+
function setEmptyVariable(source, key, value) {
|
|
886
|
+
if (value.length < 12 || /\s|=/.test(value)) {
|
|
887
|
+
throw new Error(`${key} 的随机值不满足本地密码安全要求`);
|
|
888
|
+
}
|
|
889
|
+
const pattern = new RegExp(`^${key}=[ \\t\\r]*$`, "m");
|
|
890
|
+
if (!pattern.test(source)) {
|
|
891
|
+
throw new Error(`.env.example 缺少空的 ${key}`);
|
|
892
|
+
}
|
|
893
|
+
return source.replace(pattern, `${key}=${value}`);
|
|
894
|
+
}
|
|
895
|
+
function defaultSecret() {
|
|
896
|
+
return randomBytes(24).toString("base64url");
|
|
897
|
+
}
|
|
898
|
+
function isAlreadyExists(error) {
|
|
899
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
900
|
+
}
|
|
901
|
+
|
|
855
902
|
// src/generator.ts
|
|
856
903
|
import {
|
|
857
904
|
copyFile,
|
|
858
905
|
lstat as lstat3,
|
|
859
906
|
mkdir as mkdir3,
|
|
860
|
-
readFile as
|
|
907
|
+
readFile as readFile8,
|
|
861
908
|
readdir as readdir3,
|
|
862
909
|
rm as rm3,
|
|
863
910
|
writeFile as writeFile7
|
|
864
911
|
} from "node:fs/promises";
|
|
865
|
-
import { basename as basename2, extname as extname2, join as
|
|
912
|
+
import { basename as basename2, extname as extname2, join as join8 } from "node:path";
|
|
866
913
|
|
|
867
914
|
// src/customer-docker.ts
|
|
868
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
869
|
-
import { join } from "node:path";
|
|
915
|
+
import { readFile as readFile2, writeFile } from "node:fs/promises";
|
|
916
|
+
import { join as join2 } from "node:path";
|
|
870
917
|
var supplierWorkspaceCopies = [
|
|
871
918
|
"COPY apps/license/package.json apps/license/package.json",
|
|
872
919
|
"COPY apps/signing-service/package.json apps/signing-service/package.json",
|
|
@@ -874,8 +921,8 @@ var supplierWorkspaceCopies = [
|
|
|
874
921
|
];
|
|
875
922
|
async function prepareCustomerDockerfiles(root) {
|
|
876
923
|
await Promise.all(["apps/server/Dockerfile", "apps/web/Dockerfile"].map(async (path) => {
|
|
877
|
-
const absolutePath =
|
|
878
|
-
const source = await
|
|
924
|
+
const absolutePath = join2(root, path);
|
|
925
|
+
const source = await readFile2(absolutePath, "utf8");
|
|
879
926
|
await writeFile(absolutePath, rewriteCustomerDockerfile(source));
|
|
880
927
|
}));
|
|
881
928
|
}
|
|
@@ -889,8 +936,8 @@ function rewriteCustomerDockerfile(source) {
|
|
|
889
936
|
}
|
|
890
937
|
|
|
891
938
|
// src/code-quality.ts
|
|
892
|
-
import { readFile as
|
|
893
|
-
import { join as
|
|
939
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
940
|
+
import { join as join3 } from "node:path";
|
|
894
941
|
var oxcStarterPolicy = [
|
|
895
942
|
"- 生成时默认预装 Oxlint、Oxfmt、lint-staged 与 Husky。",
|
|
896
943
|
"- 提交前自动格式化暂存文件;lint、typecheck 和 test 保留为显式命令。",
|
|
@@ -899,8 +946,8 @@ var oxcStarterPolicy = [
|
|
|
899
946
|
`);
|
|
900
947
|
var noCodeQualityPolicy = "- 本项目生成时未引入 lint 或 format 工具;项目所有者可按团队需要自行选择。";
|
|
901
948
|
async function reflectCodeQualityChoice(targetDirectory, includeOxc) {
|
|
902
|
-
const path =
|
|
903
|
-
const source = await
|
|
949
|
+
const path = join3(targetDirectory, "AGENTS.md");
|
|
950
|
+
const source = await readFile3(path, "utf8");
|
|
904
951
|
if (includeOxc)
|
|
905
952
|
return;
|
|
906
953
|
if (!source.includes(oxcStarterPolicy)) {
|
|
@@ -910,19 +957,19 @@ async function reflectCodeQualityChoice(targetDirectory, includeOxc) {
|
|
|
910
957
|
}
|
|
911
958
|
|
|
912
959
|
// src/license-files.ts
|
|
913
|
-
import { mkdir, readFile as
|
|
914
|
-
import { dirname, join as
|
|
960
|
+
import { mkdir, readFile as readFile4, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
961
|
+
import { dirname, join as join4, resolve as resolve2 } from "node:path";
|
|
915
962
|
import { fileURLToPath } from "node:url";
|
|
916
963
|
var packageRoot = resolve2(dirname(fileURLToPath(import.meta.url)), "..");
|
|
917
964
|
var apacheLicensePath = resolve2(packageRoot, "LICENSE");
|
|
918
965
|
var windyLicensePath = ".windy/licenses/WINDY-APACHE-2.0.txt";
|
|
919
966
|
async function applyWindyBaseLicense(targetDirectory) {
|
|
920
|
-
const target =
|
|
967
|
+
const target = join4(targetDirectory, windyLicensePath);
|
|
921
968
|
await mkdir(dirname(target), { recursive: true });
|
|
922
|
-
await writeFile3(target, await
|
|
969
|
+
await writeFile3(target, await readFile4(apacheLicensePath));
|
|
923
970
|
}
|
|
924
971
|
async function applyProjectLicense(targetDirectory, license, holder, year = new Date().getFullYear()) {
|
|
925
|
-
const target =
|
|
972
|
+
const target = join4(targetDirectory, "LICENSE");
|
|
926
973
|
if (license === "UNLICENSED") {
|
|
927
974
|
await rm(target, { force: true });
|
|
928
975
|
return;
|
|
@@ -936,7 +983,7 @@ async function applyProjectLicense(targetDirectory, license, holder, year = new
|
|
|
936
983
|
return;
|
|
937
984
|
}
|
|
938
985
|
await mkdir(targetDirectory, { recursive: true });
|
|
939
|
-
await writeFile3(target, await
|
|
986
|
+
await writeFile3(target, await readFile4(apacheLicensePath));
|
|
940
987
|
}
|
|
941
988
|
function mitLicense(year, holder) {
|
|
942
989
|
return `MIT License
|
|
@@ -965,8 +1012,8 @@ SOFTWARE.
|
|
|
965
1012
|
|
|
966
1013
|
// src/managed-files.ts
|
|
967
1014
|
import { createHash } from "node:crypto";
|
|
968
|
-
import { lstat, readdir, readFile as
|
|
969
|
-
import { join as
|
|
1015
|
+
import { lstat, readdir, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
1016
|
+
import { join as join5, relative, sep } from "node:path";
|
|
970
1017
|
var managedFilesManifestPath = ".windy/files.json";
|
|
971
1018
|
var ignoredDirectoryNames = new Set([".git", "node_modules", "dist"]);
|
|
972
1019
|
var ignoredFiles = new Set([
|
|
@@ -990,7 +1037,7 @@ var sharedScaffoldRoots = [
|
|
|
990
1037
|
async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
|
|
991
1038
|
const paths = await collectFiles(targetDirectory);
|
|
992
1039
|
const files = await Promise.all(paths.map(async (path) => {
|
|
993
|
-
const content = await
|
|
1040
|
+
const content = await readFile5(join5(targetDirectory, path));
|
|
994
1041
|
return {
|
|
995
1042
|
path,
|
|
996
1043
|
ownership: classifyOwnership(path),
|
|
@@ -1003,11 +1050,11 @@ async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
|
|
|
1003
1050
|
algorithm: "sha256",
|
|
1004
1051
|
files
|
|
1005
1052
|
};
|
|
1006
|
-
await writeFile4(
|
|
1053
|
+
await writeFile4(join5(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
|
|
1007
1054
|
`);
|
|
1008
1055
|
}
|
|
1009
1056
|
async function readManagedFilesManifest(targetDirectory) {
|
|
1010
|
-
const parsed = JSON.parse(await
|
|
1057
|
+
const parsed = JSON.parse(await readFile5(join5(targetDirectory, managedFilesManifestPath), "utf8"));
|
|
1011
1058
|
if (!isManagedFilesManifest(parsed)) {
|
|
1012
1059
|
throw new Error("受管文件清单无效,无法安全更新");
|
|
1013
1060
|
}
|
|
@@ -1031,7 +1078,7 @@ async function collectFiles(root) {
|
|
|
1031
1078
|
for (const name of (await readdir(directory)).sort()) {
|
|
1032
1079
|
if (ignoredDirectoryNames.has(name))
|
|
1033
1080
|
continue;
|
|
1034
|
-
const path =
|
|
1081
|
+
const path = join5(directory, name);
|
|
1035
1082
|
const metadata = await lstat(path);
|
|
1036
1083
|
if (metadata.isSymbolicLink())
|
|
1037
1084
|
continue;
|
|
@@ -1065,8 +1112,8 @@ function isManagedFilesManifest(value) {
|
|
|
1065
1112
|
}
|
|
1066
1113
|
|
|
1067
1114
|
// src/module-materializer.ts
|
|
1068
|
-
import { lstat as lstat2, readFile as
|
|
1069
|
-
import { basename, extname, join as
|
|
1115
|
+
import { lstat as lstat2, readFile as readFile6, readdir as readdir2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
|
|
1116
|
+
import { basename, extname, join as join6, relative as relative2, sep as sep2 } from "node:path";
|
|
1070
1117
|
var textExtensions = new Set([
|
|
1071
1118
|
".css",
|
|
1072
1119
|
".html",
|
|
@@ -1095,14 +1142,14 @@ async function materializeSelectedModules(targetDirectory, selectedNames) {
|
|
|
1095
1142
|
...manifest.documentation,
|
|
1096
1143
|
...manifest.schemaFiles
|
|
1097
1144
|
]);
|
|
1098
|
-
await Promise.all([...new Set(removedRoots)].map((path) => rm2(
|
|
1145
|
+
await Promise.all([...new Set(removedRoots)].map((path) => rm2(join6(targetDirectory, path), { recursive: true, force: true })));
|
|
1099
1146
|
await pruneWorkspaceReferences(targetDirectory, removedPackageNames, removedRoots);
|
|
1100
1147
|
}
|
|
1101
1148
|
async function stripModuleBlocks(root, selected) {
|
|
1102
1149
|
for (const path of await collectFiles2(root)) {
|
|
1103
1150
|
if (!textExtensions.has(extname(path)))
|
|
1104
1151
|
continue;
|
|
1105
|
-
const source = await
|
|
1152
|
+
const source = await readFile6(path, "utf8");
|
|
1106
1153
|
if (!source.includes("@windy-module"))
|
|
1107
1154
|
continue;
|
|
1108
1155
|
const output = stripTaggedSource(source, selected, relativePath(root, path));
|
|
@@ -1139,7 +1186,7 @@ async function readRemovedPackageNames(root, roots) {
|
|
|
1139
1186
|
const names = new Set;
|
|
1140
1187
|
for (const packageRoot2 of roots) {
|
|
1141
1188
|
try {
|
|
1142
|
-
const value = JSON.parse(await
|
|
1189
|
+
const value = JSON.parse(await readFile6(join6(root, packageRoot2, "package.json"), "utf8"));
|
|
1143
1190
|
if (value.name)
|
|
1144
1191
|
names.add(value.name);
|
|
1145
1192
|
} catch (error) {
|
|
@@ -1152,7 +1199,7 @@ async function readRemovedPackageNames(root, roots) {
|
|
|
1152
1199
|
async function pruneWorkspaceReferences(root, removedPackages, removedRoots) {
|
|
1153
1200
|
for (const path of await collectFiles2(root)) {
|
|
1154
1201
|
if (basename(path) === "package.json") {
|
|
1155
|
-
const value = JSON.parse(await
|
|
1202
|
+
const value = JSON.parse(await readFile6(path, "utf8"));
|
|
1156
1203
|
let changed = false;
|
|
1157
1204
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
1158
1205
|
const dependencies = value[section];
|
|
@@ -1170,7 +1217,7 @@ async function pruneWorkspaceReferences(root, removedPackages, removedRoots) {
|
|
|
1170
1217
|
}
|
|
1171
1218
|
if (basename(path) !== "Dockerfile")
|
|
1172
1219
|
continue;
|
|
1173
|
-
const source = await
|
|
1220
|
+
const source = await readFile6(path, "utf8");
|
|
1174
1221
|
const output = source.split(`
|
|
1175
1222
|
`).filter((line) => !removedRoots.some((root2) => line.includes(root2))).join(`
|
|
1176
1223
|
`);
|
|
@@ -1182,11 +1229,11 @@ async function pruneDatabaseArtifacts(root, tableNames, statementTokens) {
|
|
|
1182
1229
|
if (!tableNames.length && !statementTokens.length)
|
|
1183
1230
|
return;
|
|
1184
1231
|
const tablePatterns = tableNames.map((name) => new RegExp(`(?:"${escapeRegExp(name)}"|\\b${escapeRegExp(name)}\\b)`));
|
|
1185
|
-
const drizzleRoot =
|
|
1232
|
+
const drizzleRoot = join6(root, "packages/database/drizzle");
|
|
1186
1233
|
try {
|
|
1187
1234
|
for (const path of await collectFiles2(drizzleRoot)) {
|
|
1188
1235
|
if (path.endsWith(".sql")) {
|
|
1189
|
-
const source = await
|
|
1236
|
+
const source = await readFile6(path, "utf8");
|
|
1190
1237
|
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)));
|
|
1191
1238
|
await writeFile5(path, statements.length ? `${statements.join(`
|
|
1192
1239
|
--> statement-breakpoint
|
|
@@ -1194,7 +1241,7 @@ async function pruneDatabaseArtifacts(root, tableNames, statementTokens) {
|
|
|
1194
1241
|
` : `-- 此 migration 的模块未被当前项目选择。
|
|
1195
1242
|
`);
|
|
1196
1243
|
} else if (path.endsWith("_snapshot.json")) {
|
|
1197
|
-
const snapshot = JSON.parse(await
|
|
1244
|
+
const snapshot = JSON.parse(await readFile6(path, "utf8"));
|
|
1198
1245
|
for (const name of tableNames)
|
|
1199
1246
|
delete snapshot.tables?.[`public.${name}`];
|
|
1200
1247
|
await writeFile5(path, `${JSON.stringify(snapshot, null, 2)}
|
|
@@ -1214,7 +1261,7 @@ async function collectFiles2(root) {
|
|
|
1214
1261
|
for (const name of (await readdir2(directory)).sort()) {
|
|
1215
1262
|
if ([".git", "node_modules", "dist"].includes(name))
|
|
1216
1263
|
continue;
|
|
1217
|
-
const path =
|
|
1264
|
+
const path = join6(directory, name);
|
|
1218
1265
|
const metadata = await lstat2(path);
|
|
1219
1266
|
if (metadata.isSymbolicLink())
|
|
1220
1267
|
continue;
|
|
@@ -1363,29 +1410,36 @@ ${includeExample ? "创建时包含了 `work-order` 示例模块,用于演示
|
|
|
1363
1410
|
不要使用 npm、Yarn 或 pnpm 安装本项目依赖,也不要提交它们产生的 lockfile;唯一的
|
|
1364
1411
|
依赖锁文件是 \`bun.lock\`。
|
|
1365
1412
|
|
|
1366
|
-
##
|
|
1413
|
+
## 第一次运行:完整开发环境
|
|
1367
1414
|
|
|
1368
1415
|
\`\`\`bash
|
|
1369
1416
|
bun install
|
|
1370
|
-
|
|
1371
|
-
# 为 POSTGRES_PASSWORD 和 WINDY_BOOTSTRAP_ADMIN_PASSWORD 设置本地值
|
|
1372
|
-
docker compose up --build
|
|
1417
|
+
bun run dev
|
|
1373
1418
|
\`\`\`
|
|
1374
1419
|
|
|
1420
|
+
create-windy 已创建仅供本机使用且被 Git 忽略的 \`.env\`,其中两个开发密码均为随机值,
|
|
1421
|
+
文件权限为 \`0600\`。\`bun run dev\` 会通过 Docker Compose 等待 PostgreSQL、执行
|
|
1422
|
+
Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接把 Server 连接到
|
|
1423
|
+
空数据库。首次启动需要本机已经安装并启动 Docker 与 Docker Compose,并可能需要下载
|
|
1424
|
+
基础镜像。
|
|
1425
|
+
|
|
1375
1426
|
| 服务 | 地址 |
|
|
1376
1427
|
| ---- | ---- |
|
|
1377
1428
|
| Web | http://localhost:9746 |
|
|
1378
1429
|
| Server 健康检查 | http://localhost:3000/api/health |
|
|
1379
1430
|
| PostgreSQL | localhost:5432 |
|
|
1380
1431
|
|
|
1381
|
-
Docker Web 固定使用 \`9746
|
|
1432
|
+
Docker Web 固定使用 \`9746\`。只启动单个应用时使用:
|
|
1382
1433
|
|
|
1383
1434
|
\`\`\`bash
|
|
1384
1435
|
bun run dev:web
|
|
1436
|
+
bun run dev:server
|
|
1385
1437
|
\`\`\`
|
|
1386
1438
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1439
|
+
\`dev:web\` 不会隐式启动后端;本地 Web 从 \`8746\` 开始探测。同一项目已经运行时会
|
|
1440
|
+
直接输出已有地址,其它服务占用时会自动使用后续端口,最多检查 100 个。实际地址始终
|
|
1441
|
+
以终端输出为准。\`dev:server\` 不会自动启动 PostgreSQL 或执行 migration;未配置
|
|
1442
|
+
\`DATABASE_URL\` 时使用内存模式。
|
|
1389
1443
|
|
|
1390
1444
|
## 页面与登录
|
|
1391
1445
|
|
|
@@ -1425,6 +1479,7 @@ ${moduleRows}
|
|
|
1425
1479
|
## 常用命令
|
|
1426
1480
|
|
|
1427
1481
|
\`\`\`bash
|
|
1482
|
+
bun run dev
|
|
1428
1483
|
bun run dev:web
|
|
1429
1484
|
bun run dev:server
|
|
1430
1485
|
bun run typecheck
|
|
@@ -1497,7 +1552,7 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
|
|
|
1497
1552
|
...includeOxc ? {
|
|
1498
1553
|
prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
|
|
1499
1554
|
} : {},
|
|
1500
|
-
dev: "
|
|
1555
|
+
dev: "docker compose up --build",
|
|
1501
1556
|
...generatorVersion ? { windy: "create-windy" } : {},
|
|
1502
1557
|
"dev:web": "bun run --cwd apps/web dev",
|
|
1503
1558
|
"dev:server": "bun run --cwd apps/server dev",
|
|
@@ -1614,21 +1669,21 @@ var excludedRuntimeFiles = new Set([
|
|
|
1614
1669
|
]);
|
|
1615
1670
|
|
|
1616
1671
|
// src/provenance.ts
|
|
1617
|
-
import { mkdir as mkdir2, readFile as
|
|
1618
|
-
import { join as
|
|
1672
|
+
import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
|
|
1673
|
+
import { join as join7 } from "node:path";
|
|
1619
1674
|
var templateMetadataFile = ".windy-template.json";
|
|
1620
1675
|
var generationMetadataFile = ".windy/generation.json";
|
|
1621
1676
|
async function readTemplateMetadata(sourceRoot) {
|
|
1622
|
-
const path =
|
|
1623
|
-
const parsed = JSON.parse(await
|
|
1677
|
+
const path = join7(sourceRoot, templateMetadataFile);
|
|
1678
|
+
const parsed = JSON.parse(await readFile7(path, "utf8"));
|
|
1624
1679
|
if (!isTemplateMetadata(parsed)) {
|
|
1625
1680
|
throw new Error("create-windy 模板来源元数据无效");
|
|
1626
1681
|
}
|
|
1627
1682
|
return parsed;
|
|
1628
1683
|
}
|
|
1629
1684
|
async function readGenerationProvenance(targetDirectory) {
|
|
1630
|
-
const path =
|
|
1631
|
-
const parsed = JSON.parse(await
|
|
1685
|
+
const path = join7(targetDirectory, generationMetadataFile);
|
|
1686
|
+
const parsed = JSON.parse(await readFile7(path, "utf8"));
|
|
1632
1687
|
if (!isGenerationProvenance(parsed)) {
|
|
1633
1688
|
throw new Error("当前目录不是受支持的 Windy 项目:Project Lock 无效");
|
|
1634
1689
|
}
|
|
@@ -1660,8 +1715,8 @@ async function writeGenerationProvenance(targetDirectory, metadata, profile, sel
|
|
|
1660
1715
|
includeOxc
|
|
1661
1716
|
}
|
|
1662
1717
|
};
|
|
1663
|
-
const path =
|
|
1664
|
-
await mkdir2(
|
|
1718
|
+
const path = join7(targetDirectory, generationMetadataFile);
|
|
1719
|
+
await mkdir2(join7(path, ".."), { recursive: true });
|
|
1665
1720
|
await writeFile6(path, `${JSON.stringify(provenance, null, 2)}
|
|
1666
1721
|
`);
|
|
1667
1722
|
}
|
|
@@ -1697,27 +1752,27 @@ async function generateProject(input) {
|
|
|
1697
1752
|
const templateMetadata = await readTemplateMetadata(input.sourceRoot);
|
|
1698
1753
|
const selectedModules = input.selectedModules ?? templateMetadata.modules.filter(({ name }) => includeExample || name !== "work-order").map(({ name }) => name);
|
|
1699
1754
|
for (const relativePath2 of generationTemplatePaths(true, includeOxc)) {
|
|
1700
|
-
await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2),
|
|
1755
|
+
await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join8(input.targetDirectory, relativePath2));
|
|
1701
1756
|
}
|
|
1702
1757
|
await materializeSelectedModules(input.targetDirectory, selectedModules);
|
|
1703
|
-
const packagePath =
|
|
1704
|
-
const packageJson = JSON.parse(await
|
|
1758
|
+
const packagePath = join8(input.targetDirectory, "package.json");
|
|
1759
|
+
const packageJson = JSON.parse(await readFile8(packagePath, "utf8"));
|
|
1705
1760
|
await writeFile7(packagePath, `${JSON.stringify(createStarterPackage(packageJson, input.projectName, includeOxc, projectLicense, selectedModules, templateMetadata.generatorVersion), null, 2)}
|
|
1706
1761
|
`);
|
|
1707
|
-
await writeFile7(
|
|
1708
|
-
await writeFile7(
|
|
1709
|
-
await writeFile7(
|
|
1710
|
-
await writeFile7(
|
|
1762
|
+
await writeFile7(join8(input.targetDirectory, "docker-compose.yml"), starterCompose);
|
|
1763
|
+
await writeFile7(join8(input.targetDirectory, ".env.example"), environmentExample(includeExample));
|
|
1764
|
+
await writeFile7(join8(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
|
|
1765
|
+
await writeFile7(join8(input.targetDirectory, "docs/README.md"), starterDocsReadme(selectedModules.includes("system.license")));
|
|
1711
1766
|
await reflectCodeQualityChoice(input.targetDirectory, includeOxc);
|
|
1712
1767
|
await applyWindyBaseLicense(input.targetDirectory);
|
|
1713
1768
|
await applyProjectLicense(input.targetDirectory, projectLicense, input.licenseHolder);
|
|
1714
1769
|
await prepareCustomerDockerfiles(input.targetDirectory);
|
|
1715
1770
|
await writeGenerationProvenance(input.targetDirectory, templateMetadata, input.profile ?? starterProfile.key, selectedModules, includeOxc, projectLicense);
|
|
1716
|
-
await rm3(
|
|
1771
|
+
await rm3(join8(input.targetDirectory, "bun.lock"), { force: true });
|
|
1717
1772
|
await writeManagedFilesManifest(input.targetDirectory, templateMetadata.generatorVersion);
|
|
1718
1773
|
}
|
|
1719
1774
|
async function resolveTemplateSource(sourceRoot, relativePath2) {
|
|
1720
|
-
const source =
|
|
1775
|
+
const source = join8(sourceRoot, relativePath2);
|
|
1721
1776
|
try {
|
|
1722
1777
|
await lstat3(source);
|
|
1723
1778
|
return source;
|
|
@@ -1728,7 +1783,7 @@ async function resolveTemplateSource(sourceRoot, relativePath2) {
|
|
|
1728
1783
|
const alias = npmTemplateAliases[relativePath2];
|
|
1729
1784
|
if (!alias)
|
|
1730
1785
|
return source;
|
|
1731
|
-
return
|
|
1786
|
+
return join8(sourceRoot, alias);
|
|
1732
1787
|
}
|
|
1733
1788
|
async function assertGeneratedProjectIsClean(targetDirectory) {
|
|
1734
1789
|
const violations = [];
|
|
@@ -1770,11 +1825,11 @@ async function copyTree(source, target) {
|
|
|
1770
1825
|
if (metadata.isDirectory()) {
|
|
1771
1826
|
await mkdir3(target, { recursive: true });
|
|
1772
1827
|
for (const child of await readdir3(source)) {
|
|
1773
|
-
await copyTree(
|
|
1828
|
+
await copyTree(join8(source, child), join8(target, child));
|
|
1774
1829
|
}
|
|
1775
1830
|
return;
|
|
1776
1831
|
}
|
|
1777
|
-
await mkdir3(
|
|
1832
|
+
await mkdir3(join8(target, ".."), { recursive: true });
|
|
1778
1833
|
await copyFile(source, target);
|
|
1779
1834
|
}
|
|
1780
1835
|
function shouldExclude(name) {
|
|
@@ -1782,7 +1837,7 @@ function shouldExclude(name) {
|
|
|
1782
1837
|
}
|
|
1783
1838
|
async function walk(directory, visit) {
|
|
1784
1839
|
for (const name of await readdir3(directory)) {
|
|
1785
|
-
const path =
|
|
1840
|
+
const path = join8(directory, name);
|
|
1786
1841
|
await visit(path);
|
|
1787
1842
|
if ((await lstat3(path)).isDirectory())
|
|
1788
1843
|
await walk(path, visit);
|
|
@@ -1815,6 +1870,7 @@ async function createProject(options, dependencies = {}) {
|
|
|
1815
1870
|
const inspect = dependencies.inspect ?? assertGeneratedProjectIsClean;
|
|
1816
1871
|
const resolveTemplate = dependencies.resolveTemplate ?? resolveTemplateRoot;
|
|
1817
1872
|
let stage = "生成文件";
|
|
1873
|
+
let localEnvironment = "existing";
|
|
1818
1874
|
try {
|
|
1819
1875
|
await generate({
|
|
1820
1876
|
sourceRoot: await resolveTemplate(),
|
|
@@ -1831,6 +1887,8 @@ async function createProject(options, dependencies = {}) {
|
|
|
1831
1887
|
await inspect(options.targetDirectory);
|
|
1832
1888
|
stage = "安装与验证";
|
|
1833
1889
|
await installAndVerify(options.targetDirectory, options.install, options.verify, options.includeOxc, dependencies.commandExecutor);
|
|
1890
|
+
stage = "初始化本地环境";
|
|
1891
|
+
localEnvironment = await (dependencies.initializeEnvironment ?? initializeLocalEnvironment)(options.targetDirectory);
|
|
1834
1892
|
} catch (error) {
|
|
1835
1893
|
await rollbackFailedTarget(options, targetState);
|
|
1836
1894
|
throw stageError(stage, error);
|
|
@@ -1848,7 +1906,11 @@ async function createProject(options, dependencies = {}) {
|
|
|
1848
1906
|
已创建 ${options.projectName}`);
|
|
1849
1907
|
log(`运行时:Bun ${bunVersion}`);
|
|
1850
1908
|
log(options.initializeGit ? gitInitialized ? "Git:已创建 main 分支和首次提交" : "Git:项目已保留,请按上述提示完成初始化" : "Git:已按 --no-git 跳过");
|
|
1851
|
-
log(
|
|
1909
|
+
log(localEnvironment === "created" ? "本地 .env:已生成随机开发密码(权限 0600)" : "本地 .env:已存在,未覆盖");
|
|
1910
|
+
log("下一步(启动数据库、迁移、后端与前端):");
|
|
1911
|
+
log(` cd ${options.projectName}`);
|
|
1912
|
+
log(" bun run dev");
|
|
1913
|
+
log("仅启动前端:bun run dev:web");
|
|
1852
1914
|
}
|
|
1853
1915
|
async function inspectTarget(path) {
|
|
1854
1916
|
try {
|
|
@@ -3624,8 +3686,8 @@ import { rm as rm8 } from "node:fs/promises";
|
|
|
3624
3686
|
import { resolve as resolve4 } from "node:path";
|
|
3625
3687
|
|
|
3626
3688
|
// src/update/doctor.ts
|
|
3627
|
-
import { readFile as
|
|
3628
|
-
import { join as
|
|
3689
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
3690
|
+
import { join as join10 } from "node:path";
|
|
3629
3691
|
|
|
3630
3692
|
// src/update/process.ts
|
|
3631
3693
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -3657,11 +3719,11 @@ import {
|
|
|
3657
3719
|
access as access2,
|
|
3658
3720
|
copyFile as copyFile2,
|
|
3659
3721
|
mkdir as mkdir5,
|
|
3660
|
-
readFile as
|
|
3722
|
+
readFile as readFile9,
|
|
3661
3723
|
rm as rm5,
|
|
3662
3724
|
writeFile as writeFile8
|
|
3663
3725
|
} from "node:fs/promises";
|
|
3664
|
-
import { dirname as dirname3, join as
|
|
3726
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
3665
3727
|
var updateStatePath = ".windy/update-state.json";
|
|
3666
3728
|
async function applyTransaction(plan) {
|
|
3667
3729
|
if (plan.conflicts.length) {
|
|
@@ -3670,7 +3732,7 @@ async function applyTransaction(plan) {
|
|
|
3670
3732
|
const changed = plan.files.filter(({ action }) => action === "create" || action === "update" || action === "delete");
|
|
3671
3733
|
for (const file of changed)
|
|
3672
3734
|
assertSafePath(file.path);
|
|
3673
|
-
const backupDirectory =
|
|
3735
|
+
const backupDirectory = join9(plan.projectDirectory, ".windy/backups", plan.id);
|
|
3674
3736
|
const protectedPaths = new Set([
|
|
3675
3737
|
...changed.map(({ path: path2 }) => path2),
|
|
3676
3738
|
".windy/generation.json",
|
|
@@ -3679,12 +3741,12 @@ async function applyTransaction(plan) {
|
|
|
3679
3741
|
]);
|
|
3680
3742
|
const backupEntries = [];
|
|
3681
3743
|
for (const path2 of protectedPaths) {
|
|
3682
|
-
const source =
|
|
3744
|
+
const source = join9(plan.projectDirectory, path2);
|
|
3683
3745
|
const existed = await exists(source);
|
|
3684
3746
|
backupEntries.push({ path: path2, existed });
|
|
3685
3747
|
if (!existed)
|
|
3686
3748
|
continue;
|
|
3687
|
-
const target =
|
|
3749
|
+
const target = join9(backupDirectory, "files", path2);
|
|
3688
3750
|
await mkdir5(dirname3(target), { recursive: true });
|
|
3689
3751
|
await copyFile2(source, target);
|
|
3690
3752
|
}
|
|
@@ -3697,7 +3759,7 @@ async function applyTransaction(plan) {
|
|
|
3697
3759
|
await writeState(plan.projectDirectory, state);
|
|
3698
3760
|
try {
|
|
3699
3761
|
for (const file of changed) {
|
|
3700
|
-
const target =
|
|
3762
|
+
const target = join9(plan.projectDirectory, file.path);
|
|
3701
3763
|
if (file.action === "delete") {
|
|
3702
3764
|
await rm5(target, { force: true });
|
|
3703
3765
|
continue;
|
|
@@ -3720,7 +3782,7 @@ async function applyTransaction(plan) {
|
|
|
3720
3782
|
}
|
|
3721
3783
|
async function readUpdateState(projectDirectory) {
|
|
3722
3784
|
try {
|
|
3723
|
-
const parsed = JSON.parse(await
|
|
3785
|
+
const parsed = JSON.parse(await readFile9(join9(projectDirectory, updateStatePath), "utf8"));
|
|
3724
3786
|
if (!isUpdateState(parsed))
|
|
3725
3787
|
throw new Error("更新恢复状态无效,请人工检查 .windy 目录");
|
|
3726
3788
|
return parsed;
|
|
@@ -3733,23 +3795,23 @@ async function readUpdateState(projectDirectory) {
|
|
|
3733
3795
|
async function restoreUpdateState(projectDirectory, state) {
|
|
3734
3796
|
for (const entry of state.entries) {
|
|
3735
3797
|
assertSafePath(entry.path);
|
|
3736
|
-
const target =
|
|
3798
|
+
const target = join9(projectDirectory, entry.path);
|
|
3737
3799
|
if (!entry.existed) {
|
|
3738
3800
|
await rm5(target, { force: true });
|
|
3739
3801
|
continue;
|
|
3740
3802
|
}
|
|
3741
|
-
const backup =
|
|
3803
|
+
const backup = join9(state.backupDirectory, "files", entry.path);
|
|
3742
3804
|
await mkdir5(dirname3(target), { recursive: true });
|
|
3743
3805
|
await copyFile2(backup, target);
|
|
3744
3806
|
}
|
|
3745
|
-
await rm5(
|
|
3807
|
+
await rm5(join9(projectDirectory, updateStatePath), { force: true });
|
|
3746
3808
|
await rm5(state.backupDirectory, { recursive: true, force: true });
|
|
3747
3809
|
}
|
|
3748
3810
|
async function clearUpdateState(projectDirectory) {
|
|
3749
|
-
await rm5(
|
|
3811
|
+
await rm5(join9(projectDirectory, updateStatePath), { force: true });
|
|
3750
3812
|
}
|
|
3751
3813
|
async function writeState(projectDirectory, state) {
|
|
3752
|
-
const path2 =
|
|
3814
|
+
const path2 = join9(projectDirectory, updateStatePath);
|
|
3753
3815
|
await mkdir5(dirname3(path2), { recursive: true });
|
|
3754
3816
|
await writeFile8(path2, `${JSON.stringify(state, null, 2)}
|
|
3755
3817
|
`);
|
|
@@ -3799,7 +3861,7 @@ async function diagnoseProject(projectDirectory, repair = false) {
|
|
|
3799
3861
|
if (file.ownership === "project-owned")
|
|
3800
3862
|
continue;
|
|
3801
3863
|
try {
|
|
3802
|
-
const changed = hashContent(await
|
|
3864
|
+
const changed = hashContent(await readFile10(join10(projectDirectory, file.path))) !== file.hash;
|
|
3803
3865
|
if (!changed)
|
|
3804
3866
|
continue;
|
|
3805
3867
|
modifiedManagedFiles.push(file.path);
|
|
@@ -3834,7 +3896,8 @@ var recipes = [
|
|
|
3834
3896
|
{ id: "starter-0.2.1-to-0.2.2", from: "0.2.1", to: "0.2.2" },
|
|
3835
3897
|
{ id: "starter-0.2.2-to-0.2.3", from: "0.2.2", to: "0.2.3" },
|
|
3836
3898
|
{ id: "starter-0.2.3-to-0.2.4", from: "0.2.3", to: "0.2.4" },
|
|
3837
|
-
{ id: "starter-0.2.4-to-0.2.5", from: "0.2.4", to: "0.2.5" }
|
|
3899
|
+
{ id: "starter-0.2.4-to-0.2.5", from: "0.2.4", to: "0.2.5" },
|
|
3900
|
+
{ id: "starter-0.2.5-to-0.2.6", from: "0.2.5", to: "0.2.6" }
|
|
3838
3901
|
];
|
|
3839
3902
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
3840
3903
|
if (sourceVersion === targetVersion)
|
|
@@ -3859,19 +3922,19 @@ function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
|
3859
3922
|
}
|
|
3860
3923
|
|
|
3861
3924
|
// src/update/updater.ts
|
|
3862
|
-
import { mkdir as mkdir7, readFile as
|
|
3863
|
-
import { dirname as dirname4, join as
|
|
3925
|
+
import { mkdir as mkdir7, readFile as readFile13, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
|
|
3926
|
+
import { dirname as dirname4, join as join14 } from "node:path";
|
|
3864
3927
|
|
|
3865
3928
|
// src/update/artifacts.ts
|
|
3866
|
-
import { mkdir as mkdir6, mkdtemp, readFile as
|
|
3929
|
+
import { mkdir as mkdir6, mkdtemp, readFile as readFile11 } from "node:fs/promises";
|
|
3867
3930
|
import { tmpdir } from "node:os";
|
|
3868
|
-
import { basename as basename3, join as
|
|
3931
|
+
import { basename as basename3, join as join11 } from "node:path";
|
|
3869
3932
|
class PublishedArtifactProvider {
|
|
3870
3933
|
async prepare(projectDirectory, provenance) {
|
|
3871
|
-
const temporaryRoot = await mkdtemp(
|
|
3934
|
+
const temporaryRoot = await mkdtemp(join11(tmpdir(), "windy-update-"));
|
|
3872
3935
|
const projectName = await readProjectName2(projectDirectory);
|
|
3873
|
-
const baseDirectory =
|
|
3874
|
-
const targetDirectory =
|
|
3936
|
+
const baseDirectory = join11(temporaryRoot, "base", projectName);
|
|
3937
|
+
const targetDirectory = join11(temporaryRoot, "target", projectName);
|
|
3875
3938
|
await generatePublishedBase(temporaryRoot, projectName, provenance, baseDirectory);
|
|
3876
3939
|
await generateProject({
|
|
3877
3940
|
sourceRoot: await resolveTemplateRoot(),
|
|
@@ -3887,7 +3950,7 @@ class PublishedArtifactProvider {
|
|
|
3887
3950
|
}
|
|
3888
3951
|
}
|
|
3889
3952
|
async function generatePublishedBase(temporaryRoot, projectName, provenance, expectedDirectory) {
|
|
3890
|
-
const parent =
|
|
3953
|
+
const parent = join11(temporaryRoot, "base");
|
|
3891
3954
|
await mkdir6(parent, { recursive: true });
|
|
3892
3955
|
const args = [
|
|
3893
3956
|
"--bun",
|
|
@@ -3911,29 +3974,29 @@ async function generatePublishedBase(temporaryRoot, projectName, provenance, exp
|
|
|
3911
3974
|
}
|
|
3912
3975
|
}
|
|
3913
3976
|
await assertProcess("bunx", args, parent);
|
|
3914
|
-
if (
|
|
3977
|
+
if (join11(parent, projectName) !== expectedDirectory) {
|
|
3915
3978
|
throw new Error("历史 artifact 生成目录异常");
|
|
3916
3979
|
}
|
|
3917
3980
|
}
|
|
3918
3981
|
async function readProjectName2(projectDirectory) {
|
|
3919
|
-
const source = JSON.parse(await
|
|
3982
|
+
const source = JSON.parse(await readFile11(join11(projectDirectory, "package.json"), "utf8"));
|
|
3920
3983
|
return source.name || basename3(projectDirectory);
|
|
3921
3984
|
}
|
|
3922
3985
|
|
|
3923
3986
|
// src/update/planner.ts
|
|
3924
|
-
import { access as access3, readFile as
|
|
3925
|
-
import { join as
|
|
3987
|
+
import { access as access3, readFile as readFile12 } from "node:fs/promises";
|
|
3988
|
+
import { join as join13 } from "node:path";
|
|
3926
3989
|
|
|
3927
3990
|
// src/update/merge.ts
|
|
3928
3991
|
import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
|
|
3929
3992
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3930
|
-
import { join as
|
|
3993
|
+
import { join as join12 } from "node:path";
|
|
3931
3994
|
async function mergeTextFile(base, ours, theirs) {
|
|
3932
|
-
const root = await mkdtemp2(
|
|
3995
|
+
const root = await mkdtemp2(join12(tmpdir2(), "windy-merge-"));
|
|
3933
3996
|
try {
|
|
3934
|
-
const oursPath =
|
|
3935
|
-
const basePath =
|
|
3936
|
-
const theirsPath =
|
|
3997
|
+
const oursPath = join12(root, "ours");
|
|
3998
|
+
const basePath = join12(root, "base");
|
|
3999
|
+
const theirsPath = join12(root, "theirs");
|
|
3937
4000
|
await Promise.all([
|
|
3938
4001
|
writeFile9(oursPath, ours),
|
|
3939
4002
|
writeFile9(basePath, base),
|
|
@@ -4055,9 +4118,9 @@ async function createUpdatePlan(request, artifacts) {
|
|
|
4055
4118
|
}
|
|
4056
4119
|
async function planFile(path2, project, baseRoot, targetRoot, source, target) {
|
|
4057
4120
|
const [ours, base, theirs] = await Promise.all([
|
|
4058
|
-
readOptional(
|
|
4059
|
-
readOptional(
|
|
4060
|
-
readOptional(
|
|
4121
|
+
readOptional(join13(project, path2)),
|
|
4122
|
+
readOptional(join13(baseRoot, path2)),
|
|
4123
|
+
readOptional(join13(targetRoot, path2))
|
|
4061
4124
|
]);
|
|
4062
4125
|
const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
|
|
4063
4126
|
if (path2.startsWith("packages/database/drizzle/") && path2.endsWith(".sql")) {
|
|
@@ -4125,7 +4188,7 @@ function conflict(path2, ownership, reason) {
|
|
|
4125
4188
|
}
|
|
4126
4189
|
async function readOptional(path2) {
|
|
4127
4190
|
try {
|
|
4128
|
-
return await
|
|
4191
|
+
return await readFile12(path2);
|
|
4129
4192
|
} catch (error) {
|
|
4130
4193
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
4131
4194
|
return;
|
|
@@ -4141,7 +4204,7 @@ async function assertGitClean(projectDirectory) {
|
|
|
4141
4204
|
}
|
|
4142
4205
|
async function assertNoPendingUpdate(projectDirectory) {
|
|
4143
4206
|
try {
|
|
4144
|
-
await access3(
|
|
4207
|
+
await access3(join13(projectDirectory, ".windy/update-state.json"));
|
|
4145
4208
|
throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
|
|
4146
4209
|
} catch (error) {
|
|
4147
4210
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
@@ -4177,7 +4240,7 @@ class DefaultStarterUpdater {
|
|
|
4177
4240
|
try {
|
|
4178
4241
|
await this.#execute("bun", ["install", "--force"], result.plan.projectDirectory);
|
|
4179
4242
|
steps.push({ name: "bun install --force", status: "passed" });
|
|
4180
|
-
const packageJson = JSON.parse(await
|
|
4243
|
+
const packageJson = JSON.parse(await readFile13(join14(result.plan.projectDirectory, "package.json"), "utf8"));
|
|
4181
4244
|
for (const name of ["lint", "typecheck", "test", "build"]) {
|
|
4182
4245
|
if (!packageJson.scripts?.[name]) {
|
|
4183
4246
|
steps.push({ name: `bun run ${name}`, status: "skipped" });
|
|
@@ -4206,13 +4269,13 @@ class DefaultStarterUpdater {
|
|
|
4206
4269
|
}
|
|
4207
4270
|
async function finalize(result) {
|
|
4208
4271
|
const root = result.plan.projectDirectory;
|
|
4209
|
-
await writeFile10(
|
|
4272
|
+
await writeFile10(join14(root, ".windy/generation.json"), `${JSON.stringify(result.plan.targetProvenance, null, 2)}
|
|
4210
4273
|
`);
|
|
4211
|
-
await writeFile10(
|
|
4274
|
+
await writeFile10(join14(root, ".windy/files.json"), `${JSON.stringify(result.plan.targetManifest, null, 2)}
|
|
4212
4275
|
`);
|
|
4213
4276
|
}
|
|
4214
4277
|
async function writeReport(result, steps, relativePath2) {
|
|
4215
|
-
const path2 =
|
|
4278
|
+
const path2 = join14(result.plan.projectDirectory, relativePath2);
|
|
4216
4279
|
await mkdir7(dirname4(path2), { recursive: true });
|
|
4217
4280
|
const lines = [
|
|
4218
4281
|
`# Windy 更新报告 ${result.plan.id}`,
|