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/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 readFile7,
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 join7 } from "node:path";
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 = join(root, path);
878
- const source = await readFile(absolutePath, "utf8");
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 readFile2, writeFile as writeFile2 } from "node:fs/promises";
893
- import { join as join2 } from "node:path";
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 = join2(targetDirectory, "AGENTS.md");
903
- const source = await readFile2(path, "utf8");
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 readFile3, rm, writeFile as writeFile3 } from "node:fs/promises";
914
- import { dirname, join as join3, resolve as resolve2 } from "node:path";
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 = join3(targetDirectory, windyLicensePath);
967
+ const target = join4(targetDirectory, windyLicensePath);
921
968
  await mkdir(dirname(target), { recursive: true });
922
- await writeFile3(target, await readFile3(apacheLicensePath));
969
+ await writeFile3(target, await readFile4(apacheLicensePath));
923
970
  }
924
971
  async function applyProjectLicense(targetDirectory, license, holder, year = new Date().getFullYear()) {
925
- const target = join3(targetDirectory, "LICENSE");
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 readFile3(apacheLicensePath));
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 readFile4, writeFile as writeFile4 } from "node:fs/promises";
969
- import { join as join4, relative, sep } from "node:path";
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 readFile4(join4(targetDirectory, path));
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(join4(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
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 readFile4(join4(targetDirectory, managedFilesManifestPath), "utf8"));
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 = join4(directory, name);
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 readFile5, readdir as readdir2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
1069
- import { basename, extname, join as join5, relative as relative2, sep as sep2 } from "node:path";
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(join5(targetDirectory, path), { recursive: true, force: true })));
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 readFile5(path, "utf8");
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 readFile5(join5(root, packageRoot2, "package.json"), "utf8"));
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 readFile5(path, "utf8"));
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 readFile5(path, "utf8");
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 = join5(root, "packages/database/drizzle");
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 readFile5(path, "utf8");
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 readFile5(path, "utf8"));
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 = join5(directory, name);
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
- cp .env.example .env
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\`。只在宿主机运行 Web 时使用:
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
- 本地 Web 从 \`8746\` 开始探测。同一项目已经运行时会直接输出已有地址;其它服务占用
1388
- 时会自动使用后续端口,最多检查 100 个。实际地址始终以终端输出为准。
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: "bun run --cwd apps/web 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 readFile6, writeFile as writeFile6 } from "node:fs/promises";
1618
- import { join as join6 } from "node:path";
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 = join6(sourceRoot, templateMetadataFile);
1623
- const parsed = JSON.parse(await readFile6(path, "utf8"));
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 = join6(targetDirectory, generationMetadataFile);
1631
- const parsed = JSON.parse(await readFile6(path, "utf8"));
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 = join6(targetDirectory, generationMetadataFile);
1664
- await mkdir2(join6(path, ".."), { recursive: true });
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), join7(input.targetDirectory, 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 = join7(input.targetDirectory, "package.json");
1704
- const packageJson = JSON.parse(await readFile7(packagePath, "utf8"));
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(join7(input.targetDirectory, "docker-compose.yml"), starterCompose);
1708
- await writeFile7(join7(input.targetDirectory, ".env.example"), environmentExample(includeExample));
1709
- await writeFile7(join7(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
1710
- await writeFile7(join7(input.targetDirectory, "docs/README.md"), starterDocsReadme(selectedModules.includes("system.license")));
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(join7(input.targetDirectory, "bun.lock"), { force: true });
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 = join7(sourceRoot, relativePath2);
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 join7(sourceRoot, alias);
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(join7(source, child), join7(target, child));
1828
+ await copyTree(join8(source, child), join8(target, child));
1774
1829
  }
1775
1830
  return;
1776
1831
  }
1777
- await mkdir3(join7(target, ".."), { recursive: true });
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 = join7(directory, name);
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(`下一步:cd ${options.projectName} && bun run dev`);
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 readFile9 } from "node:fs/promises";
3628
- import { join as join9 } from "node:path";
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 readFile8,
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 join8 } from "node:path";
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 = join8(plan.projectDirectory, ".windy/backups", plan.id);
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 = join8(plan.projectDirectory, path2);
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 = join8(backupDirectory, "files", path2);
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 = join8(plan.projectDirectory, file.path);
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 readFile8(join8(projectDirectory, updateStatePath), "utf8"));
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 = join8(projectDirectory, entry.path);
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 = join8(state.backupDirectory, "files", entry.path);
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(join8(projectDirectory, updateStatePath), { force: true });
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(join8(projectDirectory, updateStatePath), { force: true });
3811
+ await rm5(join9(projectDirectory, updateStatePath), { force: true });
3750
3812
  }
3751
3813
  async function writeState(projectDirectory, state) {
3752
- const path2 = join8(projectDirectory, updateStatePath);
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 readFile9(join9(projectDirectory, file.path))) !== file.hash;
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 readFile12, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
3863
- import { dirname as dirname4, join as join13 } from "node:path";
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 readFile10 } from "node:fs/promises";
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 join10 } from "node:path";
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(join10(tmpdir(), "windy-update-"));
3934
+ const temporaryRoot = await mkdtemp(join11(tmpdir(), "windy-update-"));
3872
3935
  const projectName = await readProjectName2(projectDirectory);
3873
- const baseDirectory = join10(temporaryRoot, "base", projectName);
3874
- const targetDirectory = join10(temporaryRoot, "target", projectName);
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 = join10(temporaryRoot, "base");
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 (join10(parent, projectName) !== expectedDirectory) {
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 readFile10(join10(projectDirectory, "package.json"), "utf8"));
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 readFile11 } from "node:fs/promises";
3925
- import { join as join12 } from "node:path";
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 join11 } from "node:path";
3993
+ import { join as join12 } from "node:path";
3931
3994
  async function mergeTextFile(base, ours, theirs) {
3932
- const root = await mkdtemp2(join11(tmpdir2(), "windy-merge-"));
3995
+ const root = await mkdtemp2(join12(tmpdir2(), "windy-merge-"));
3933
3996
  try {
3934
- const oursPath = join11(root, "ours");
3935
- const basePath = join11(root, "base");
3936
- const theirsPath = join11(root, "theirs");
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(join12(project, path2)),
4059
- readOptional(join12(baseRoot, path2)),
4060
- readOptional(join12(targetRoot, path2))
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 readFile11(path2);
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(join12(projectDirectory, ".windy/update-state.json"));
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 readFile12(join13(result.plan.projectDirectory, "package.json"), "utf8"));
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(join13(root, ".windy/generation.json"), `${JSON.stringify(result.plan.targetProvenance, null, 2)}
4272
+ await writeFile10(join14(root, ".windy/generation.json"), `${JSON.stringify(result.plan.targetProvenance, null, 2)}
4210
4273
  `);
4211
- await writeFile10(join13(root, ".windy/files.json"), `${JSON.stringify(result.plan.targetManifest, null, 2)}
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 = join13(result.plan.projectDirectory, relativePath2);
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}`,