create-windy 0.2.5 → 0.2.7

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,32 +1410,47 @@ ${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
 
1446
+ 全新数据库的初始平台账号为 \`admin\`。初始密码不是所有项目共用的固定值,而是
1447
+ create-windy 在本机 \`.env\` 中随机生成的 \`WINDY_BOOTSTRAP_ADMIN_PASSWORD\`;该文件已
1448
+ 被 Git 忽略,请在本机直接查看,不要把密码提交到仓库或复制到文档。首次登录会要求
1449
+ 立即修改密码,新密码必须为 12-128 个字符,且不能与当前密码相同。
1450
+
1451
+ \`.env\` 只负责全新数据库的首次初始化。账号密码写入数据库后,重启不会用环境变量
1452
+ 覆盖已有密码;如果已经完成首次改密,应使用修改后的密码登录。
1453
+
1392
1454
  | 路径 | 访问要求 | 说明 |
1393
1455
  | ---- | -------- | ---- |
1394
1456
  | \`/login\` | 公开 | 登录成功后回到安全的站内目标,默认进入 \`/\` |
@@ -1425,6 +1487,7 @@ ${moduleRows}
1425
1487
  ## 常用命令
1426
1488
 
1427
1489
  \`\`\`bash
1490
+ bun run dev
1428
1491
  bun run dev:web
1429
1492
  bun run dev:server
1430
1493
  bun run typecheck
@@ -1497,7 +1560,7 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
1497
1560
  ...includeOxc ? {
1498
1561
  prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
1499
1562
  } : {},
1500
- dev: "bun run --cwd apps/web dev",
1563
+ dev: "docker compose up --build",
1501
1564
  ...generatorVersion ? { windy: "create-windy" } : {},
1502
1565
  "dev:web": "bun run --cwd apps/web dev",
1503
1566
  "dev:server": "bun run --cwd apps/server dev",
@@ -1614,21 +1677,21 @@ var excludedRuntimeFiles = new Set([
1614
1677
  ]);
1615
1678
 
1616
1679
  // 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";
1680
+ import { mkdir as mkdir2, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1681
+ import { join as join7 } from "node:path";
1619
1682
  var templateMetadataFile = ".windy-template.json";
1620
1683
  var generationMetadataFile = ".windy/generation.json";
1621
1684
  async function readTemplateMetadata(sourceRoot) {
1622
- const path = join6(sourceRoot, templateMetadataFile);
1623
- const parsed = JSON.parse(await readFile6(path, "utf8"));
1685
+ const path = join7(sourceRoot, templateMetadataFile);
1686
+ const parsed = JSON.parse(await readFile7(path, "utf8"));
1624
1687
  if (!isTemplateMetadata(parsed)) {
1625
1688
  throw new Error("create-windy 模板来源元数据无效");
1626
1689
  }
1627
1690
  return parsed;
1628
1691
  }
1629
1692
  async function readGenerationProvenance(targetDirectory) {
1630
- const path = join6(targetDirectory, generationMetadataFile);
1631
- const parsed = JSON.parse(await readFile6(path, "utf8"));
1693
+ const path = join7(targetDirectory, generationMetadataFile);
1694
+ const parsed = JSON.parse(await readFile7(path, "utf8"));
1632
1695
  if (!isGenerationProvenance(parsed)) {
1633
1696
  throw new Error("当前目录不是受支持的 Windy 项目:Project Lock 无效");
1634
1697
  }
@@ -1660,8 +1723,8 @@ async function writeGenerationProvenance(targetDirectory, metadata, profile, sel
1660
1723
  includeOxc
1661
1724
  }
1662
1725
  };
1663
- const path = join6(targetDirectory, generationMetadataFile);
1664
- await mkdir2(join6(path, ".."), { recursive: true });
1726
+ const path = join7(targetDirectory, generationMetadataFile);
1727
+ await mkdir2(join7(path, ".."), { recursive: true });
1665
1728
  await writeFile6(path, `${JSON.stringify(provenance, null, 2)}
1666
1729
  `);
1667
1730
  }
@@ -1697,27 +1760,27 @@ async function generateProject(input) {
1697
1760
  const templateMetadata = await readTemplateMetadata(input.sourceRoot);
1698
1761
  const selectedModules = input.selectedModules ?? templateMetadata.modules.filter(({ name }) => includeExample || name !== "work-order").map(({ name }) => name);
1699
1762
  for (const relativePath2 of generationTemplatePaths(true, includeOxc)) {
1700
- await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join7(input.targetDirectory, relativePath2));
1763
+ await copyTree(await resolveTemplateSource(input.sourceRoot, relativePath2), join8(input.targetDirectory, relativePath2));
1701
1764
  }
1702
1765
  await materializeSelectedModules(input.targetDirectory, selectedModules);
1703
- const packagePath = join7(input.targetDirectory, "package.json");
1704
- const packageJson = JSON.parse(await readFile7(packagePath, "utf8"));
1766
+ const packagePath = join8(input.targetDirectory, "package.json");
1767
+ const packageJson = JSON.parse(await readFile8(packagePath, "utf8"));
1705
1768
  await writeFile7(packagePath, `${JSON.stringify(createStarterPackage(packageJson, input.projectName, includeOxc, projectLicense, selectedModules, templateMetadata.generatorVersion), null, 2)}
1706
1769
  `);
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")));
1770
+ await writeFile7(join8(input.targetDirectory, "docker-compose.yml"), starterCompose);
1771
+ await writeFile7(join8(input.targetDirectory, ".env.example"), environmentExample(includeExample));
1772
+ await writeFile7(join8(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
1773
+ await writeFile7(join8(input.targetDirectory, "docs/README.md"), starterDocsReadme(selectedModules.includes("system.license")));
1711
1774
  await reflectCodeQualityChoice(input.targetDirectory, includeOxc);
1712
1775
  await applyWindyBaseLicense(input.targetDirectory);
1713
1776
  await applyProjectLicense(input.targetDirectory, projectLicense, input.licenseHolder);
1714
1777
  await prepareCustomerDockerfiles(input.targetDirectory);
1715
1778
  await writeGenerationProvenance(input.targetDirectory, templateMetadata, input.profile ?? starterProfile.key, selectedModules, includeOxc, projectLicense);
1716
- await rm3(join7(input.targetDirectory, "bun.lock"), { force: true });
1779
+ await rm3(join8(input.targetDirectory, "bun.lock"), { force: true });
1717
1780
  await writeManagedFilesManifest(input.targetDirectory, templateMetadata.generatorVersion);
1718
1781
  }
1719
1782
  async function resolveTemplateSource(sourceRoot, relativePath2) {
1720
- const source = join7(sourceRoot, relativePath2);
1783
+ const source = join8(sourceRoot, relativePath2);
1721
1784
  try {
1722
1785
  await lstat3(source);
1723
1786
  return source;
@@ -1728,7 +1791,7 @@ async function resolveTemplateSource(sourceRoot, relativePath2) {
1728
1791
  const alias = npmTemplateAliases[relativePath2];
1729
1792
  if (!alias)
1730
1793
  return source;
1731
- return join7(sourceRoot, alias);
1794
+ return join8(sourceRoot, alias);
1732
1795
  }
1733
1796
  async function assertGeneratedProjectIsClean(targetDirectory) {
1734
1797
  const violations = [];
@@ -1770,11 +1833,11 @@ async function copyTree(source, target) {
1770
1833
  if (metadata.isDirectory()) {
1771
1834
  await mkdir3(target, { recursive: true });
1772
1835
  for (const child of await readdir3(source)) {
1773
- await copyTree(join7(source, child), join7(target, child));
1836
+ await copyTree(join8(source, child), join8(target, child));
1774
1837
  }
1775
1838
  return;
1776
1839
  }
1777
- await mkdir3(join7(target, ".."), { recursive: true });
1840
+ await mkdir3(join8(target, ".."), { recursive: true });
1778
1841
  await copyFile(source, target);
1779
1842
  }
1780
1843
  function shouldExclude(name) {
@@ -1782,7 +1845,7 @@ function shouldExclude(name) {
1782
1845
  }
1783
1846
  async function walk(directory, visit) {
1784
1847
  for (const name of await readdir3(directory)) {
1785
- const path = join7(directory, name);
1848
+ const path = join8(directory, name);
1786
1849
  await visit(path);
1787
1850
  if ((await lstat3(path)).isDirectory())
1788
1851
  await walk(path, visit);
@@ -1815,6 +1878,7 @@ async function createProject(options, dependencies = {}) {
1815
1878
  const inspect = dependencies.inspect ?? assertGeneratedProjectIsClean;
1816
1879
  const resolveTemplate = dependencies.resolveTemplate ?? resolveTemplateRoot;
1817
1880
  let stage = "生成文件";
1881
+ let localEnvironment = "existing";
1818
1882
  try {
1819
1883
  await generate({
1820
1884
  sourceRoot: await resolveTemplate(),
@@ -1831,6 +1895,8 @@ async function createProject(options, dependencies = {}) {
1831
1895
  await inspect(options.targetDirectory);
1832
1896
  stage = "安装与验证";
1833
1897
  await installAndVerify(options.targetDirectory, options.install, options.verify, options.includeOxc, dependencies.commandExecutor);
1898
+ stage = "初始化本地环境";
1899
+ localEnvironment = await (dependencies.initializeEnvironment ?? initializeLocalEnvironment)(options.targetDirectory);
1834
1900
  } catch (error) {
1835
1901
  await rollbackFailedTarget(options, targetState);
1836
1902
  throw stageError(stage, error);
@@ -1848,7 +1914,13 @@ async function createProject(options, dependencies = {}) {
1848
1914
  已创建 ${options.projectName}`);
1849
1915
  log(`运行时:Bun ${bunVersion}`);
1850
1916
  log(options.initializeGit ? gitInitialized ? "Git:已创建 main 分支和首次提交" : "Git:项目已保留,请按上述提示完成初始化" : "Git:已按 --no-git 跳过");
1851
- log(`下一步:cd ${options.projectName} && bun run dev`);
1917
+ log(localEnvironment === "created" ? "本地 .env:已生成随机开发密码(权限 0600)" : "本地 .env:已存在,未覆盖");
1918
+ log("初始平台账号:admin");
1919
+ log("初始平台密码:查看 .env 中的 WINDY_BOOTSTRAP_ADMIN_PASSWORD(首次登录后必须修改)");
1920
+ log("下一步(启动数据库、迁移、后端与前端):");
1921
+ log(` cd ${options.projectName}`);
1922
+ log(" bun run dev");
1923
+ log("仅启动前端:bun run dev:web");
1852
1924
  }
1853
1925
  async function inspectTarget(path) {
1854
1926
  try {
@@ -3624,8 +3696,8 @@ import { rm as rm8 } from "node:fs/promises";
3624
3696
  import { resolve as resolve4 } from "node:path";
3625
3697
 
3626
3698
  // src/update/doctor.ts
3627
- import { readFile as readFile9 } from "node:fs/promises";
3628
- import { join as join9 } from "node:path";
3699
+ import { readFile as readFile10 } from "node:fs/promises";
3700
+ import { join as join10 } from "node:path";
3629
3701
 
3630
3702
  // src/update/process.ts
3631
3703
  import { spawn as spawn3 } from "node:child_process";
@@ -3657,11 +3729,11 @@ import {
3657
3729
  access as access2,
3658
3730
  copyFile as copyFile2,
3659
3731
  mkdir as mkdir5,
3660
- readFile as readFile8,
3732
+ readFile as readFile9,
3661
3733
  rm as rm5,
3662
3734
  writeFile as writeFile8
3663
3735
  } from "node:fs/promises";
3664
- import { dirname as dirname3, join as join8 } from "node:path";
3736
+ import { dirname as dirname3, join as join9 } from "node:path";
3665
3737
  var updateStatePath = ".windy/update-state.json";
3666
3738
  async function applyTransaction(plan) {
3667
3739
  if (plan.conflicts.length) {
@@ -3670,7 +3742,7 @@ async function applyTransaction(plan) {
3670
3742
  const changed = plan.files.filter(({ action }) => action === "create" || action === "update" || action === "delete");
3671
3743
  for (const file of changed)
3672
3744
  assertSafePath(file.path);
3673
- const backupDirectory = join8(plan.projectDirectory, ".windy/backups", plan.id);
3745
+ const backupDirectory = join9(plan.projectDirectory, ".windy/backups", plan.id);
3674
3746
  const protectedPaths = new Set([
3675
3747
  ...changed.map(({ path: path2 }) => path2),
3676
3748
  ".windy/generation.json",
@@ -3679,12 +3751,12 @@ async function applyTransaction(plan) {
3679
3751
  ]);
3680
3752
  const backupEntries = [];
3681
3753
  for (const path2 of protectedPaths) {
3682
- const source = join8(plan.projectDirectory, path2);
3754
+ const source = join9(plan.projectDirectory, path2);
3683
3755
  const existed = await exists(source);
3684
3756
  backupEntries.push({ path: path2, existed });
3685
3757
  if (!existed)
3686
3758
  continue;
3687
- const target = join8(backupDirectory, "files", path2);
3759
+ const target = join9(backupDirectory, "files", path2);
3688
3760
  await mkdir5(dirname3(target), { recursive: true });
3689
3761
  await copyFile2(source, target);
3690
3762
  }
@@ -3697,7 +3769,7 @@ async function applyTransaction(plan) {
3697
3769
  await writeState(plan.projectDirectory, state);
3698
3770
  try {
3699
3771
  for (const file of changed) {
3700
- const target = join8(plan.projectDirectory, file.path);
3772
+ const target = join9(plan.projectDirectory, file.path);
3701
3773
  if (file.action === "delete") {
3702
3774
  await rm5(target, { force: true });
3703
3775
  continue;
@@ -3720,7 +3792,7 @@ async function applyTransaction(plan) {
3720
3792
  }
3721
3793
  async function readUpdateState(projectDirectory) {
3722
3794
  try {
3723
- const parsed = JSON.parse(await readFile8(join8(projectDirectory, updateStatePath), "utf8"));
3795
+ const parsed = JSON.parse(await readFile9(join9(projectDirectory, updateStatePath), "utf8"));
3724
3796
  if (!isUpdateState(parsed))
3725
3797
  throw new Error("更新恢复状态无效,请人工检查 .windy 目录");
3726
3798
  return parsed;
@@ -3733,23 +3805,23 @@ async function readUpdateState(projectDirectory) {
3733
3805
  async function restoreUpdateState(projectDirectory, state) {
3734
3806
  for (const entry of state.entries) {
3735
3807
  assertSafePath(entry.path);
3736
- const target = join8(projectDirectory, entry.path);
3808
+ const target = join9(projectDirectory, entry.path);
3737
3809
  if (!entry.existed) {
3738
3810
  await rm5(target, { force: true });
3739
3811
  continue;
3740
3812
  }
3741
- const backup = join8(state.backupDirectory, "files", entry.path);
3813
+ const backup = join9(state.backupDirectory, "files", entry.path);
3742
3814
  await mkdir5(dirname3(target), { recursive: true });
3743
3815
  await copyFile2(backup, target);
3744
3816
  }
3745
- await rm5(join8(projectDirectory, updateStatePath), { force: true });
3817
+ await rm5(join9(projectDirectory, updateStatePath), { force: true });
3746
3818
  await rm5(state.backupDirectory, { recursive: true, force: true });
3747
3819
  }
3748
3820
  async function clearUpdateState(projectDirectory) {
3749
- await rm5(join8(projectDirectory, updateStatePath), { force: true });
3821
+ await rm5(join9(projectDirectory, updateStatePath), { force: true });
3750
3822
  }
3751
3823
  async function writeState(projectDirectory, state) {
3752
- const path2 = join8(projectDirectory, updateStatePath);
3824
+ const path2 = join9(projectDirectory, updateStatePath);
3753
3825
  await mkdir5(dirname3(path2), { recursive: true });
3754
3826
  await writeFile8(path2, `${JSON.stringify(state, null, 2)}
3755
3827
  `);
@@ -3799,7 +3871,7 @@ async function diagnoseProject(projectDirectory, repair = false) {
3799
3871
  if (file.ownership === "project-owned")
3800
3872
  continue;
3801
3873
  try {
3802
- const changed = hashContent(await readFile9(join9(projectDirectory, file.path))) !== file.hash;
3874
+ const changed = hashContent(await readFile10(join10(projectDirectory, file.path))) !== file.hash;
3803
3875
  if (!changed)
3804
3876
  continue;
3805
3877
  modifiedManagedFiles.push(file.path);
@@ -3834,7 +3906,9 @@ var recipes = [
3834
3906
  { id: "starter-0.2.1-to-0.2.2", from: "0.2.1", to: "0.2.2" },
3835
3907
  { id: "starter-0.2.2-to-0.2.3", from: "0.2.2", to: "0.2.3" },
3836
3908
  { 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" }
3909
+ { id: "starter-0.2.4-to-0.2.5", from: "0.2.4", to: "0.2.5" },
3910
+ { id: "starter-0.2.5-to-0.2.6", from: "0.2.5", to: "0.2.6" },
3911
+ { id: "starter-0.2.6-to-0.2.7", from: "0.2.6", to: "0.2.7" }
3838
3912
  ];
3839
3913
  function resolveRecipeChain(sourceVersion, targetVersion) {
3840
3914
  if (sourceVersion === targetVersion)
@@ -3859,19 +3933,19 @@ function resolveRecipeChain(sourceVersion, targetVersion) {
3859
3933
  }
3860
3934
 
3861
3935
  // 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";
3936
+ import { mkdir as mkdir7, readFile as readFile13, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
3937
+ import { dirname as dirname4, join as join14 } from "node:path";
3864
3938
 
3865
3939
  // src/update/artifacts.ts
3866
- import { mkdir as mkdir6, mkdtemp, readFile as readFile10 } from "node:fs/promises";
3940
+ import { mkdir as mkdir6, mkdtemp, readFile as readFile11 } from "node:fs/promises";
3867
3941
  import { tmpdir } from "node:os";
3868
- import { basename as basename3, join as join10 } from "node:path";
3942
+ import { basename as basename3, join as join11 } from "node:path";
3869
3943
  class PublishedArtifactProvider {
3870
3944
  async prepare(projectDirectory, provenance) {
3871
- const temporaryRoot = await mkdtemp(join10(tmpdir(), "windy-update-"));
3945
+ const temporaryRoot = await mkdtemp(join11(tmpdir(), "windy-update-"));
3872
3946
  const projectName = await readProjectName2(projectDirectory);
3873
- const baseDirectory = join10(temporaryRoot, "base", projectName);
3874
- const targetDirectory = join10(temporaryRoot, "target", projectName);
3947
+ const baseDirectory = join11(temporaryRoot, "base", projectName);
3948
+ const targetDirectory = join11(temporaryRoot, "target", projectName);
3875
3949
  await generatePublishedBase(temporaryRoot, projectName, provenance, baseDirectory);
3876
3950
  await generateProject({
3877
3951
  sourceRoot: await resolveTemplateRoot(),
@@ -3887,7 +3961,7 @@ class PublishedArtifactProvider {
3887
3961
  }
3888
3962
  }
3889
3963
  async function generatePublishedBase(temporaryRoot, projectName, provenance, expectedDirectory) {
3890
- const parent = join10(temporaryRoot, "base");
3964
+ const parent = join11(temporaryRoot, "base");
3891
3965
  await mkdir6(parent, { recursive: true });
3892
3966
  const args = [
3893
3967
  "--bun",
@@ -3911,29 +3985,29 @@ async function generatePublishedBase(temporaryRoot, projectName, provenance, exp
3911
3985
  }
3912
3986
  }
3913
3987
  await assertProcess("bunx", args, parent);
3914
- if (join10(parent, projectName) !== expectedDirectory) {
3988
+ if (join11(parent, projectName) !== expectedDirectory) {
3915
3989
  throw new Error("历史 artifact 生成目录异常");
3916
3990
  }
3917
3991
  }
3918
3992
  async function readProjectName2(projectDirectory) {
3919
- const source = JSON.parse(await readFile10(join10(projectDirectory, "package.json"), "utf8"));
3993
+ const source = JSON.parse(await readFile11(join11(projectDirectory, "package.json"), "utf8"));
3920
3994
  return source.name || basename3(projectDirectory);
3921
3995
  }
3922
3996
 
3923
3997
  // src/update/planner.ts
3924
- import { access as access3, readFile as readFile11 } from "node:fs/promises";
3925
- import { join as join12 } from "node:path";
3998
+ import { access as access3, readFile as readFile12 } from "node:fs/promises";
3999
+ import { join as join13 } from "node:path";
3926
4000
 
3927
4001
  // src/update/merge.ts
3928
4002
  import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
3929
4003
  import { tmpdir as tmpdir2 } from "node:os";
3930
- import { join as join11 } from "node:path";
4004
+ import { join as join12 } from "node:path";
3931
4005
  async function mergeTextFile(base, ours, theirs) {
3932
- const root = await mkdtemp2(join11(tmpdir2(), "windy-merge-"));
4006
+ const root = await mkdtemp2(join12(tmpdir2(), "windy-merge-"));
3933
4007
  try {
3934
- const oursPath = join11(root, "ours");
3935
- const basePath = join11(root, "base");
3936
- const theirsPath = join11(root, "theirs");
4008
+ const oursPath = join12(root, "ours");
4009
+ const basePath = join12(root, "base");
4010
+ const theirsPath = join12(root, "theirs");
3937
4011
  await Promise.all([
3938
4012
  writeFile9(oursPath, ours),
3939
4013
  writeFile9(basePath, base),
@@ -4055,9 +4129,9 @@ async function createUpdatePlan(request, artifacts) {
4055
4129
  }
4056
4130
  async function planFile(path2, project, baseRoot, targetRoot, source, target) {
4057
4131
  const [ours, base, theirs] = await Promise.all([
4058
- readOptional(join12(project, path2)),
4059
- readOptional(join12(baseRoot, path2)),
4060
- readOptional(join12(targetRoot, path2))
4132
+ readOptional(join13(project, path2)),
4133
+ readOptional(join13(baseRoot, path2)),
4134
+ readOptional(join13(targetRoot, path2))
4061
4135
  ]);
4062
4136
  const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
4063
4137
  if (path2.startsWith("packages/database/drizzle/") && path2.endsWith(".sql")) {
@@ -4125,7 +4199,7 @@ function conflict(path2, ownership, reason) {
4125
4199
  }
4126
4200
  async function readOptional(path2) {
4127
4201
  try {
4128
- return await readFile11(path2);
4202
+ return await readFile12(path2);
4129
4203
  } catch (error) {
4130
4204
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
4131
4205
  return;
@@ -4141,7 +4215,7 @@ async function assertGitClean(projectDirectory) {
4141
4215
  }
4142
4216
  async function assertNoPendingUpdate(projectDirectory) {
4143
4217
  try {
4144
- await access3(join12(projectDirectory, ".windy/update-state.json"));
4218
+ await access3(join13(projectDirectory, ".windy/update-state.json"));
4145
4219
  throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
4146
4220
  } catch (error) {
4147
4221
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
@@ -4177,7 +4251,7 @@ class DefaultStarterUpdater {
4177
4251
  try {
4178
4252
  await this.#execute("bun", ["install", "--force"], result.plan.projectDirectory);
4179
4253
  steps.push({ name: "bun install --force", status: "passed" });
4180
- const packageJson = JSON.parse(await readFile12(join13(result.plan.projectDirectory, "package.json"), "utf8"));
4254
+ const packageJson = JSON.parse(await readFile13(join14(result.plan.projectDirectory, "package.json"), "utf8"));
4181
4255
  for (const name of ["lint", "typecheck", "test", "build"]) {
4182
4256
  if (!packageJson.scripts?.[name]) {
4183
4257
  steps.push({ name: `bun run ${name}`, status: "skipped" });
@@ -4206,13 +4280,13 @@ class DefaultStarterUpdater {
4206
4280
  }
4207
4281
  async function finalize(result) {
4208
4282
  const root = result.plan.projectDirectory;
4209
- await writeFile10(join13(root, ".windy/generation.json"), `${JSON.stringify(result.plan.targetProvenance, null, 2)}
4283
+ await writeFile10(join14(root, ".windy/generation.json"), `${JSON.stringify(result.plan.targetProvenance, null, 2)}
4210
4284
  `);
4211
- await writeFile10(join13(root, ".windy/files.json"), `${JSON.stringify(result.plan.targetManifest, null, 2)}
4285
+ await writeFile10(join14(root, ".windy/files.json"), `${JSON.stringify(result.plan.targetManifest, null, 2)}
4212
4286
  `);
4213
4287
  }
4214
4288
  async function writeReport(result, steps, relativePath2) {
4215
- const path2 = join13(result.plan.projectDirectory, relativePath2);
4289
+ const path2 = join14(result.plan.projectDirectory, relativePath2);
4216
4290
  await mkdir7(dirname4(path2), { recursive: true });
4217
4291
  const lines = [
4218
4292
  `# Windy 更新报告 ${result.plan.id}`,