create-windy 0.1.2 → 0.2.0

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
@@ -688,6 +688,12 @@ function readProjectLicense(args) {
688
688
  }
689
689
  var helpText = `create-windy <项目名> [选项]
690
690
 
691
+ 生命周期命令:
692
+ create-windy doctor [--cwd <目录>] [--repair]
693
+ create-windy update --check [--cwd <目录>]
694
+ create-windy update --dry-run [--to <版本>] [--cwd <目录>]
695
+ create-windy update [--to <版本>] [--cwd <目录>]
696
+
691
697
  选项:
692
698
  --profile private-enterprise 单组织、单租户、私有部署 profile
693
699
  --skip-install 只生成文件,不执行 bun install
@@ -799,7 +805,7 @@ function isMissingCommand(error) {
799
805
  }
800
806
 
801
807
  // src/git.ts
802
- async function initializeGitRepository(targetDirectory, execute = runCommand) {
808
+ async function initializeGitRepository(targetDirectory, execute = runCommand, activateHooks = false) {
803
809
  try {
804
810
  await execute("git", ["init", "-b", "main"], targetDirectory);
805
811
  } catch (error) {
@@ -808,6 +814,16 @@ async function initializeGitRepository(targetDirectory, execute = runCommand) {
808
814
  warning: `未能初始化 Git 仓库,项目文件已保留。${recoveryHint(error)}`
809
815
  };
810
816
  }
817
+ if (activateHooks) {
818
+ try {
819
+ await execute("bun", ["run", "prepare"], targetDirectory);
820
+ } catch (error) {
821
+ return {
822
+ status: "skipped",
823
+ warning: "Git 仓库已初始化,但提交钩子未能启用。请安装依赖后运行 bun run prepare。" + compactReason(error)
824
+ };
825
+ }
826
+ }
811
827
  try {
812
828
  await execute("git", ["add", "--all"], targetDirectory);
813
829
  } catch (error) {
@@ -874,8 +890,9 @@ function rewriteCustomerDockerfile(source) {
874
890
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
875
891
  import { join as join2 } from "node:path";
876
892
  var oxcStarterPolicy = [
877
- "- 代码检查与格式化统一使用 Oxc:OxlintOxfmt,不使用 ESLint 或 Prettier。",
878
- "- Oxc Starter 默认提供的代码质量方案;项目生成后由项目所有者决定是否保留、替换或并行使用其它工具。"
893
+ "- 生成时默认预装 OxlintOxfmt、lint-staged Husky。",
894
+ "- 提交前自动格式化暂存文件;lint、typecheck test 保留为显式命令。",
895
+ "- 这是 Starter 的初始配置;项目所有者可按团队需要保留、替换或扩展。"
879
896
  ].join(`
880
897
  `);
881
898
  var noCodeQualityPolicy = "- 本项目生成时未引入 lint 或 format 工具;项目所有者可按团队需要自行选择。";
@@ -895,7 +912,13 @@ import { mkdir, readFile as readFile3, rm, writeFile as writeFile3 } from "node:
895
912
  import { dirname, join as join3, resolve as resolve2 } from "node:path";
896
913
  import { fileURLToPath } from "node:url";
897
914
  var packageRoot = resolve2(dirname(fileURLToPath(import.meta.url)), "..");
898
- var apacheLicensePath = resolve2(packageRoot, "assets/LICENSE-Apache-2.0.txt");
915
+ var apacheLicensePath = resolve2(packageRoot, "LICENSE");
916
+ var windyLicensePath = ".windy/licenses/WINDY-APACHE-2.0.txt";
917
+ async function applyWindyBaseLicense(targetDirectory) {
918
+ const target = join3(targetDirectory, windyLicensePath);
919
+ await mkdir(dirname(target), { recursive: true });
920
+ await writeFile3(target, await readFile3(apacheLicensePath));
921
+ }
899
922
  async function applyProjectLicense(targetDirectory, license, holder, year = new Date().getFullYear()) {
900
923
  const target = join3(targetDirectory, "LICENSE");
901
924
  if (license === "UNLICENSED") {
@@ -944,7 +967,11 @@ import { lstat, readdir, readFile as readFile4, writeFile as writeFile4 } from "
944
967
  import { join as join4, relative, sep } from "node:path";
945
968
  var managedFilesManifestPath = ".windy/files.json";
946
969
  var ignoredDirectoryNames = new Set([".git", "node_modules", "dist"]);
947
- var ignoredFiles = new Set(["bun.lock", managedFilesManifestPath]);
970
+ var ignoredFiles = new Set([
971
+ "bun.lock",
972
+ managedFilesManifestPath,
973
+ ".windy/update-state.json"
974
+ ]);
948
975
  var projectOwnedRoots = [
949
976
  "apps/web/src/app",
950
977
  "apps/server/src/work-order",
@@ -977,6 +1004,16 @@ async function writeManagedFilesManifest(targetDirectory, generatorVersion) {
977
1004
  await writeFile4(join4(targetDirectory, managedFilesManifestPath), `${JSON.stringify(manifest, null, 2)}
978
1005
  `);
979
1006
  }
1007
+ async function readManagedFilesManifest(targetDirectory) {
1008
+ const parsed = JSON.parse(await readFile4(join4(targetDirectory, managedFilesManifestPath), "utf8"));
1009
+ if (!isManagedFilesManifest(parsed)) {
1010
+ throw new Error("受管文件清单无效,无法安全更新");
1011
+ }
1012
+ return parsed;
1013
+ }
1014
+ function hashContent(content) {
1015
+ return createHash("sha256").update(content).digest("hex");
1016
+ }
980
1017
  function classifyOwnership(path) {
981
1018
  if (matchesRoot(path, projectOwnedRoots))
982
1019
  return "project-owned";
@@ -997,6 +1034,10 @@ async function collectFiles(root) {
997
1034
  if (metadata.isSymbolicLink())
998
1035
  continue;
999
1036
  if (metadata.isDirectory()) {
1037
+ const relativeDirectory = relative(root, path).split(sep).join("/");
1038
+ if (relativeDirectory === ".windy/backups" || relativeDirectory === ".windy/reports") {
1039
+ continue;
1040
+ }
1000
1041
  await visit(path, output);
1001
1042
  continue;
1002
1043
  }
@@ -1009,6 +1050,17 @@ async function collectFiles(root) {
1009
1050
  function matchesRoot(path, roots) {
1010
1051
  return roots.some((root) => path === root || path.startsWith(`${root}/`));
1011
1052
  }
1053
+ function isManagedFilesManifest(value) {
1054
+ if (!value || typeof value !== "object")
1055
+ return false;
1056
+ const manifest = value;
1057
+ return manifest.schemaVersion === 1 && manifest.algorithm === "sha256" && typeof manifest.generatorVersion === "string" && Array.isArray(manifest.files) && manifest.files.every((entry) => {
1058
+ if (!entry || typeof entry !== "object")
1059
+ return false;
1060
+ const file = entry;
1061
+ return typeof file.path === "string" && (file.ownership === "windy-managed" || file.ownership === "shared-scaffold" || file.ownership === "project-owned") && typeof file.hash === "string";
1062
+ });
1063
+ }
1012
1064
 
1013
1065
  // src/module-materializer.ts
1014
1066
  import { lstat as lstat2, readFile as readFile5, readdir as readdir2, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
@@ -1294,8 +1346,10 @@ ${projectName} 是由 create-windy 生成的单组织、单租户企业应用项
1294
1346
  \`/\`,平台管理控制面位于 \`/admin/**\`;业务功能可以按自己的产品习惯开发,不需要
1295
1347
  复用 Admin 页面结构。
1296
1348
 
1297
- 本项目固定使用 Bun 1.3.14 或更高版本,当前法律许可为 \`${projectLicense}\`。项目不包含
1298
- 软件供应方的 License Authority、内部签名服务、发布密钥或私有源仓库信息。
1349
+ 本项目固定使用 Bun 1.3.14 或更高版本,客户业务代码的当前法律许可为
1350
+ \`${projectLicense}\`。Windy Starter 基座使用 Apache-2.0,正文保留在
1351
+ \`.windy/licenses/WINDY-APACHE-2.0.txt\`。项目不包含软件供应方的 License Authority、
1352
+ 内部签名服务、发布密钥或私有源仓库信息。
1299
1353
  ${includeExample ? "创建时包含了 `work-order` 示例模块,用于演示 Module Manifest 接入;它不是生产工单产品,可以在正式开发前替换。" : "创建时未选择 `work-order` 示例模块,当前项目只包含平台模块。"}
1300
1354
 
1301
1355
  ## 环境要求
@@ -1362,8 +1416,9 @@ bun run dev:web
1362
1416
  | ---------- | -------- | ------------ | ---- |
1363
1417
  ${moduleRows}
1364
1418
 
1365
- 精确生成来源和选择记录在 \`.windy/generation.json\`。当前版本尚未提供通用
1366
- \`module add/remove\` \`windy update\`,不要把 Feature Flag 关闭当成模块已经卸载。
1419
+ 精确生成来源和选择记录在 \`.windy/generation.json\`。当前版本已提供可回滚的
1420
+ \`windy update\`;通用 \`module add/remove\` 尚未交付,不要把 Feature Flag 关闭当成
1421
+ 模块已经卸载。
1367
1422
 
1368
1423
  ## 常用命令
1369
1424
 
@@ -1373,10 +1428,19 @@ bun run dev:server
1373
1428
  bun run typecheck
1374
1429
  bun run test
1375
1430
  bun run build
1431
+ bun run windy doctor
1432
+ bun run windy update --check
1433
+ bun run windy update --dry-run
1376
1434
  ${includeOxc ? `bun run lint
1377
1435
  bun run format` : "# 本项目未预装 lint/format 工具,可按团队需要自行选择。"}
1378
1436
  \`\`\`
1379
1437
 
1438
+ ${includeOxc ? "提交时 Husky 会通过 lint-staged 与 Oxfmt 格式化暂存文件,不会把未暂存内容带入提交。lint、typecheck 和 test 保留为需要时显式运行的命令。" : "当前项目没有生成 Husky 代码质量钩子;如后续自行引入工具,请同步配置提交工作流。"}
1439
+
1440
+ 更新前必须保持 Git 工作区干净。\`--dry-run\` 只生成计划,不写项目;正式更新先备份,
1441
+ 再做 package 字段级合并、共享脚手架三方合并与完整验证。任何冲突都会在写入前中止,
1442
+ 验证失败会自动回滚;中断恢复使用 \`bun run windy doctor --repair\`。
1443
+
1380
1444
  ## 开发约束
1381
1445
 
1382
1446
  - 新增路由默认需要登录;只有显式 public 的页面允许匿名访问。
@@ -1399,27 +1463,40 @@ create-windy 默认建立 \`main\` 分支并创建 \`chore: initialize project\`
1399
1463
 
1400
1464
  ## License
1401
1465
 
1402
- 本项目当前法律许可证为 \`${projectLicense}\`。${projectLicense === "UNLICENSED" ? "项目没有附带开源许可证。" : "完整许可证正文位于根目录 `LICENSE`。"}法律许可证与 \`system.license\` 离线运行时授权模块是两个不同概念;更换其中一个
1403
- 不会自动更换另一个。
1466
+ 客户业务代码当前法律许可证为 \`${projectLicense}\`。${projectLicense === "UNLICENSED" ? "项目没有附带开源许可证。" : "完整许可证正文位于根目录 `LICENSE`。"}Windy Starter 基座继续适用
1467
+ \`.windy/licenses/WINDY-APACHE-2.0.txt\` 中的 Apache-2.0。项目法律许可证与
1468
+ \`system.license\` 离线运行时授权模块是两个不同概念;更换其中一个不会自动更换
1469
+ 另一个。
1404
1470
  `;
1405
1471
  }
1406
1472
 
1407
1473
  // src/project-package.ts
1408
- function createStarterPackage(source, projectName, includeOxc = true, projectLicense = "UNLICENSED", selectedModules = []) {
1474
+ var defaultLintStaged = {
1475
+ "*.{cjs,css,cts,html,js,json,jsonc,jsx,less,md,mdx,mjs,mts,scss,ts,tsx,vue,yaml,yml}": "oxfmt --write"
1476
+ };
1477
+ function createStarterPackage(source, projectName, includeOxc = true, projectLicense = "UNLICENSED", selectedModules = [], generatorVersion) {
1478
+ const { "lint-staged": sourceLintStaged, ...sourceWithoutLintStaged } = source;
1409
1479
  const devDependencies = { ...source.devDependencies };
1410
1480
  if (!includeOxc) {
1481
+ delete devDependencies.husky;
1482
+ delete devDependencies["lint-staged"];
1411
1483
  delete devDependencies.oxfmt;
1412
1484
  delete devDependencies.oxlint;
1413
1485
  }
1414
1486
  return {
1415
- ...source,
1487
+ ...sourceWithoutLintStaged,
1416
1488
  name: projectName,
1417
1489
  private: true,
1418
1490
  license: projectLicense,
1419
1491
  packageManager: "bun@1.3.14",
1420
1492
  engines: { ...source.engines, bun: ">=1.3.14" },
1493
+ ...includeOxc ? { "lint-staged": sourceLintStaged ?? defaultLintStaged } : {},
1421
1494
  scripts: {
1495
+ ...includeOxc ? {
1496
+ prepare: "git rev-parse --git-dir >/dev/null 2>&1 || exit 0; husky"
1497
+ } : {},
1422
1498
  dev: "bun run --cwd apps/web dev",
1499
+ ...generatorVersion ? { windy: "create-windy" } : {},
1423
1500
  "dev:web": "bun run --cwd apps/web dev",
1424
1501
  "dev:server": "bun run --cwd apps/server dev",
1425
1502
  build: "bun run --cwd apps/web build",
@@ -1434,10 +1511,17 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
1434
1511
  "bun run --cwd packages/database typecheck",
1435
1512
  "bun run --cwd packages/crud-generator typecheck"
1436
1513
  ].join(" && "),
1437
- ...includeOxc ? { lint: "oxlint .", format: "oxfmt" } : {},
1514
+ ...includeOxc ? {
1515
+ lint: "oxlint .",
1516
+ format: "oxfmt",
1517
+ "format:staged": "lint-staged"
1518
+ } : {},
1438
1519
  test: "bun test && bun run --cwd apps/web test"
1439
1520
  },
1440
- devDependencies
1521
+ devDependencies: {
1522
+ ...devDependencies,
1523
+ ...generatorVersion ? { "create-windy": generatorVersion } : {}
1524
+ }
1441
1525
  };
1442
1526
  }
1443
1527
 
@@ -1445,6 +1529,7 @@ function createStarterPackage(source, projectName, includeOxc = true, projectLic
1445
1529
  var templateRootFiles = [
1446
1530
  ".dockerignore",
1447
1531
  ".gitignore",
1532
+ ".husky/pre-commit",
1448
1533
  ".oxfmtrc.json",
1449
1534
  ".oxlintrc.json",
1450
1535
  "AGENTS.md",
@@ -1505,8 +1590,13 @@ var npmTemplateAliases = {
1505
1590
  "bunfig.toml": "_bunfig.toml"
1506
1591
  };
1507
1592
  function generationTemplatePaths(includeExample, includeOxc = true) {
1508
- return templatePaths.filter((path) => (includeExample || path !== "packages/example-work-order") && (includeOxc || path !== ".oxfmtrc.json" && path !== ".oxlintrc.json"));
1593
+ return templatePaths.filter((path) => (includeExample || path !== "packages/example-work-order") && (includeOxc || !oxcTemplatePaths.has(path)));
1509
1594
  }
1595
+ var oxcTemplatePaths = new Set([
1596
+ ".husky/pre-commit",
1597
+ ".oxfmtrc.json",
1598
+ ".oxlintrc.json"
1599
+ ]);
1510
1600
  var excludedNames = new Set([
1511
1601
  ".DS_Store",
1512
1602
  ".env",
@@ -1534,6 +1624,14 @@ async function readTemplateMetadata(sourceRoot) {
1534
1624
  }
1535
1625
  return parsed;
1536
1626
  }
1627
+ async function readGenerationProvenance(targetDirectory) {
1628
+ const path = join6(targetDirectory, generationMetadataFile);
1629
+ const parsed = JSON.parse(await readFile6(path, "utf8"));
1630
+ if (!isGenerationProvenance(parsed)) {
1631
+ throw new Error("当前目录不是受支持的 Windy 项目:Project Lock 无效");
1632
+ }
1633
+ return parsed;
1634
+ }
1537
1635
  async function writeGenerationProvenance(targetDirectory, metadata, profile, selectedModules, includeOxc, legalLicense) {
1538
1636
  const requested = selectedModuleManifests(selectedModules);
1539
1637
  const versions = new Map(metadata.modules.map((item) => [item.name, item]));
@@ -1571,6 +1669,14 @@ function isTemplateMetadata(value) {
1571
1669
  const metadata = value;
1572
1670
  return metadata.schemaVersion === 1 && typeof metadata.templateCommit === "string" && /^[0-9a-f]{40}$/.test(metadata.templateCommit) && typeof metadata.generatorVersion === "string" && metadata.generatorVersion.length > 0 && Array.isArray(metadata.modules) && metadata.modules.every(isModuleVersion);
1573
1671
  }
1672
+ function isGenerationProvenance(value) {
1673
+ if (!isTemplateMetadata(value))
1674
+ return false;
1675
+ const metadata = value;
1676
+ const toolchain = metadata.toolchain;
1677
+ const choices = metadata.choices;
1678
+ return typeof metadata.windyVersion === "string" && metadata.profile === "private-enterprise" && toolchain?.runtime === "bun" && (toolchain?.codeQuality === "oxc" || toolchain?.codeQuality === "none") && (metadata.legalLicense === "UNLICENSED" || metadata.legalLicense === "MIT" || metadata.legalLicense === "Apache-2.0") && Array.isArray(metadata.appliedRecipes) && metadata.appliedRecipes.every((item) => typeof item === "string") && metadata.managedFilesManifest === ".windy/files.json" && typeof choices?.includeExample === "boolean" && typeof choices.includeOxc === "boolean";
1679
+ }
1574
1680
  function isModuleVersion(value) {
1575
1681
  if (!value || typeof value !== "object")
1576
1682
  return false;
@@ -1594,13 +1700,14 @@ async function generateProject(input) {
1594
1700
  await materializeSelectedModules(input.targetDirectory, selectedModules);
1595
1701
  const packagePath = join7(input.targetDirectory, "package.json");
1596
1702
  const packageJson = JSON.parse(await readFile7(packagePath, "utf8"));
1597
- await writeFile7(packagePath, `${JSON.stringify(createStarterPackage(packageJson, input.projectName, includeOxc, projectLicense, selectedModules), null, 2)}
1703
+ await writeFile7(packagePath, `${JSON.stringify(createStarterPackage(packageJson, input.projectName, includeOxc, projectLicense, selectedModules, templateMetadata.generatorVersion), null, 2)}
1598
1704
  `);
1599
1705
  await writeFile7(join7(input.targetDirectory, "docker-compose.yml"), starterCompose);
1600
1706
  await writeFile7(join7(input.targetDirectory, ".env.example"), environmentExample(includeExample));
1601
1707
  await writeFile7(join7(input.targetDirectory, "README.md"), starterReadme(input.projectName, includeExample, includeOxc, projectLicense, selectedModules));
1602
1708
  await writeFile7(join7(input.targetDirectory, "docs/README.md"), starterDocsReadme(selectedModules.includes("system.license")));
1603
1709
  await reflectCodeQualityChoice(input.targetDirectory, includeOxc);
1710
+ await applyWindyBaseLicense(input.targetDirectory);
1604
1711
  await applyProjectLicense(input.targetDirectory, projectLicense, input.licenseHolder);
1605
1712
  await prepareCustomerDockerfiles(input.targetDirectory);
1606
1713
  await writeGenerationProvenance(input.targetDirectory, templateMetadata, input.profile ?? starterProfile.key, selectedModules, includeOxc, projectLicense);
@@ -1729,7 +1836,7 @@ async function createProject(options, dependencies = {}) {
1729
1836
  const log = dependencies.log ?? console.log;
1730
1837
  let gitInitialized = false;
1731
1838
  if (options.initializeGit) {
1732
- const result = await (dependencies.initializeGit ?? initializeGitRepository)(options.targetDirectory, dependencies.commandExecutor);
1839
+ const result = await (dependencies.initializeGit ?? initializeGitRepository)(options.targetDirectory, dependencies.commandExecutor, options.includeOxc && (options.install || options.verify));
1733
1840
  gitInitialized = result.status === "committed";
1734
1841
  if (result.status === "skipped")
1735
1842
  log(`
@@ -3421,19 +3528,27 @@ var dist_default4 = createPrompt((config, done) => {
3421
3528
  });
3422
3529
 
3423
3530
  // src/interactive-selection.ts
3531
+ var oxcChoiceKey = "toolchain.oxc";
3424
3532
  async function applyInteractiveSelection(options, prompter = terminalPrompter()) {
3425
3533
  if (!options.interactive)
3426
3534
  return options;
3427
3535
  const optional = moduleDistributionCatalog.filter(({ name }) => !requiredModuleNames.includes(name));
3428
- const requested = await prompter.selectModules({
3429
- choices: optional.map(({ name, label, selection }) => ({
3430
- value: name,
3431
- label: `${label} (${name})`,
3432
- hint: selection === "recommended" ? "推荐,默认选中" : "可选"
3433
- })),
3434
- defaults: [...recommendedModuleNames]
3536
+ const requested = await prompter.selectGenerationChoices({
3537
+ choices: [
3538
+ ...optional.map(({ name, label, description, selection }) => ({
3539
+ value: name,
3540
+ label: `${label}(${sentenceWithoutStop(description)})`,
3541
+ hint: selection === "recommended" ? "推荐,默认选中" : "可选模块"
3542
+ })),
3543
+ {
3544
+ value: oxcChoiceKey,
3545
+ label: "Oxc 代码质量(Oxlint、Oxfmt 与提交格式化)",
3546
+ hint: "可选基础设施,默认选中"
3547
+ }
3548
+ ],
3549
+ defaults: [...recommendedModuleNames, oxcChoiceKey]
3435
3550
  });
3436
- const includeOxc = await prompter.confirm("是否引入 Oxlint + Oxfmt?", true);
3551
+ const includeOxc = requested.includes(oxcChoiceKey);
3437
3552
  const projectLicense = await prompter.selectLicense();
3438
3553
  const licenseHolder = projectLicense === "MIT" ? (await prompter.text("MIT 许可证权利人名称:")).trim() : undefined;
3439
3554
  if (projectLicense === "MIT" && !licenseHolder) {
@@ -3441,7 +3556,7 @@ async function applyInteractiveSelection(options, prompter = terminalPrompter())
3441
3556
  }
3442
3557
  const selectedModules = resolveModuleSelection({
3443
3558
  includeRecommended: false,
3444
- requested
3559
+ requested: requested.filter((value) => value !== oxcChoiceKey)
3445
3560
  });
3446
3561
  return {
3447
3562
  ...options,
@@ -3463,9 +3578,9 @@ function terminalPrompter() {
3463
3578
  }
3464
3579
  };
3465
3580
  return {
3466
- async selectModules({ choices, defaults }) {
3581
+ async selectGenerationChoices({ choices, defaults }) {
3467
3582
  return dist_default4({
3468
- message: "请选择要生成的平台模块",
3583
+ message: "请选择要生成的平台模块和基础设施",
3469
3584
  choices: choices.map((choice) => ({
3470
3585
  value: choice.value,
3471
3586
  name: choice.label,
@@ -3482,13 +3597,6 @@ function terminalPrompter() {
3482
3597
  }
3483
3598
  });
3484
3599
  },
3485
- async confirm(message, defaultValue) {
3486
- const hint = defaultValue ? "Y/n" : "y/N";
3487
- const answer = (await question(`${message} (${hint}) `)).trim().toLowerCase();
3488
- if (!answer)
3489
- return defaultValue;
3490
- return answer === "y" || answer === "yes";
3491
- },
3492
3600
  async selectLicense() {
3493
3601
  const answer = (await question(`项目法律许可证:1. UNLICENSED(默认) 2. MIT 3. Apache-2.0
3494
3602
  > `)).trim();
@@ -3505,10 +3613,755 @@ function terminalPrompter() {
3505
3613
  }
3506
3614
  };
3507
3615
  }
3616
+ function sentenceWithoutStop(value) {
3617
+ return value.replace(/[。.]$/, "");
3618
+ }
3619
+
3620
+ // src/update/cli.ts
3621
+ import { rm as rm8 } from "node:fs/promises";
3622
+ import { resolve as resolve4 } from "node:path";
3623
+
3624
+ // src/update/doctor.ts
3625
+ import { readFile as readFile9 } from "node:fs/promises";
3626
+ import { join as join9 } from "node:path";
3627
+
3628
+ // src/update/process.ts
3629
+ import { spawn as spawn3 } from "node:child_process";
3630
+ async function captureProcess(command, args, cwd, inherit = false) {
3631
+ return await new Promise((resolve4, reject) => {
3632
+ const child = spawn3(command, args, {
3633
+ cwd,
3634
+ stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"]
3635
+ });
3636
+ let stdout2 = "";
3637
+ let stderr = "";
3638
+ child.stdout?.on("data", (chunk) => stdout2 += chunk.toString());
3639
+ child.stderr?.on("data", (chunk) => stderr += chunk.toString());
3640
+ child.once("error", reject);
3641
+ child.once("exit", (code) => resolve4({ code: code ?? 1, stdout: stdout2, stderr }));
3642
+ });
3643
+ }
3644
+ async function assertProcess(command, args, cwd, inherit = false) {
3645
+ const result = await captureProcess(command, args, cwd, inherit);
3646
+ if (result.code !== 0) {
3647
+ const detail = result.stderr.trim() || result.stdout.trim();
3648
+ throw new Error(`${command} ${args.join(" ")} 执行失败${detail ? `:${detail}` : ""}`);
3649
+ }
3650
+ return result;
3651
+ }
3652
+
3653
+ // src/update/transaction.ts
3654
+ import {
3655
+ access as access2,
3656
+ copyFile as copyFile2,
3657
+ mkdir as mkdir5,
3658
+ readFile as readFile8,
3659
+ rm as rm5,
3660
+ writeFile as writeFile8
3661
+ } from "node:fs/promises";
3662
+ import { dirname as dirname3, join as join8 } from "node:path";
3663
+ var updateStatePath = ".windy/update-state.json";
3664
+ async function applyTransaction(plan) {
3665
+ if (plan.conflicts.length) {
3666
+ throw new Error(`更新存在 ${plan.conflicts.length} 个冲突,未写入任何项目文件`);
3667
+ }
3668
+ const changed = plan.files.filter(({ action }) => action === "create" || action === "update" || action === "delete");
3669
+ for (const file of changed)
3670
+ assertSafePath(file.path);
3671
+ const backupDirectory = join8(plan.projectDirectory, ".windy/backups", plan.id);
3672
+ const protectedPaths = new Set([
3673
+ ...changed.map(({ path: path2 }) => path2),
3674
+ ".windy/generation.json",
3675
+ ".windy/files.json",
3676
+ "bun.lock"
3677
+ ]);
3678
+ const backupEntries = [];
3679
+ for (const path2 of protectedPaths) {
3680
+ const source = join8(plan.projectDirectory, path2);
3681
+ const existed = await exists(source);
3682
+ backupEntries.push({ path: path2, existed });
3683
+ if (!existed)
3684
+ continue;
3685
+ const target = join8(backupDirectory, "files", path2);
3686
+ await mkdir5(dirname3(target), { recursive: true });
3687
+ await copyFile2(source, target);
3688
+ }
3689
+ const state = {
3690
+ schemaVersion: 1,
3691
+ updateId: plan.id,
3692
+ backupDirectory,
3693
+ entries: backupEntries
3694
+ };
3695
+ await writeState(plan.projectDirectory, state);
3696
+ try {
3697
+ for (const file of changed) {
3698
+ const target = join8(plan.projectDirectory, file.path);
3699
+ if (file.action === "delete") {
3700
+ await rm5(target, { force: true });
3701
+ continue;
3702
+ }
3703
+ if (!file.content)
3704
+ throw new Error(`更新计划缺少文件内容:${file.path}`);
3705
+ await mkdir5(dirname3(target), { recursive: true });
3706
+ await writeFile8(target, file.content);
3707
+ }
3708
+ } catch (error) {
3709
+ await restoreUpdateState(plan.projectDirectory, state);
3710
+ throw error;
3711
+ }
3712
+ return {
3713
+ plan,
3714
+ backupDirectory,
3715
+ backupEntries,
3716
+ changedFiles: changed.map(({ path: path2 }) => path2)
3717
+ };
3718
+ }
3719
+ async function readUpdateState(projectDirectory) {
3720
+ try {
3721
+ const parsed = JSON.parse(await readFile8(join8(projectDirectory, updateStatePath), "utf8"));
3722
+ if (!isUpdateState(parsed))
3723
+ throw new Error("更新恢复状态无效,请人工检查 .windy 目录");
3724
+ return parsed;
3725
+ } catch (error) {
3726
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
3727
+ return;
3728
+ throw error;
3729
+ }
3730
+ }
3731
+ async function restoreUpdateState(projectDirectory, state) {
3732
+ for (const entry of state.entries) {
3733
+ assertSafePath(entry.path);
3734
+ const target = join8(projectDirectory, entry.path);
3735
+ if (!entry.existed) {
3736
+ await rm5(target, { force: true });
3737
+ continue;
3738
+ }
3739
+ const backup = join8(state.backupDirectory, "files", entry.path);
3740
+ await mkdir5(dirname3(target), { recursive: true });
3741
+ await copyFile2(backup, target);
3742
+ }
3743
+ await rm5(join8(projectDirectory, updateStatePath), { force: true });
3744
+ }
3745
+ async function clearUpdateState(projectDirectory) {
3746
+ await rm5(join8(projectDirectory, updateStatePath), { force: true });
3747
+ }
3748
+ async function writeState(projectDirectory, state) {
3749
+ const path2 = join8(projectDirectory, updateStatePath);
3750
+ await mkdir5(dirname3(path2), { recursive: true });
3751
+ await writeFile8(path2, `${JSON.stringify(state, null, 2)}
3752
+ `);
3753
+ }
3754
+ async function exists(path2) {
3755
+ try {
3756
+ await access2(path2);
3757
+ return true;
3758
+ } catch {
3759
+ return false;
3760
+ }
3761
+ }
3762
+ function assertSafePath(path2) {
3763
+ if (!path2 || path2.startsWith("/") || path2.split("/").includes("..")) {
3764
+ throw new Error(`更新计划包含不安全路径:${path2}`);
3765
+ }
3766
+ }
3767
+ function isUpdateState(value) {
3768
+ if (!value || typeof value !== "object")
3769
+ return false;
3770
+ const state = value;
3771
+ return state.schemaVersion === 1 && typeof state.updateId === "string" && typeof state.backupDirectory === "string" && Array.isArray(state.entries);
3772
+ }
3773
+
3774
+ // src/update/doctor.ts
3775
+ var oxcPaths = new Set([
3776
+ ".oxfmtrc.json",
3777
+ ".oxlintrc.json",
3778
+ ".husky/pre-commit"
3779
+ ]);
3780
+ async function diagnoseProject(projectDirectory, repair = false) {
3781
+ const pending = await readUpdateState(projectDirectory);
3782
+ let repaired = false;
3783
+ if (pending && repair) {
3784
+ await restoreUpdateState(projectDirectory, pending);
3785
+ repaired = true;
3786
+ }
3787
+ const [provenance, manifest, git] = await Promise.all([
3788
+ readGenerationProvenance(projectDirectory),
3789
+ readManagedFilesManifest(projectDirectory),
3790
+ captureProcess("git", ["status", "--porcelain"], projectDirectory)
3791
+ ]);
3792
+ const modifiedManagedFiles = [];
3793
+ const missingManagedFiles = [];
3794
+ let detachedToolchain = false;
3795
+ for (const file of manifest.files) {
3796
+ if (file.ownership === "project-owned")
3797
+ continue;
3798
+ try {
3799
+ const changed = hashContent(await readFile9(join9(projectDirectory, file.path))) !== file.hash;
3800
+ if (!changed)
3801
+ continue;
3802
+ modifiedManagedFiles.push(file.path);
3803
+ if (oxcPaths.has(file.path))
3804
+ detachedToolchain = true;
3805
+ } catch (error) {
3806
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3807
+ missingManagedFiles.push(file.path);
3808
+ if (oxcPaths.has(file.path))
3809
+ detachedToolchain = true;
3810
+ continue;
3811
+ }
3812
+ throw error;
3813
+ }
3814
+ }
3815
+ return {
3816
+ version: provenance.windyVersion,
3817
+ gitClean: git.code === 0 && !git.stdout.trim(),
3818
+ modifiedManagedFiles,
3819
+ missingManagedFiles,
3820
+ detachedToolchain,
3821
+ recoveryRequired: Boolean(pending) && !repaired,
3822
+ repaired
3823
+ };
3824
+ }
3825
+
3826
+ // src/update/recipes.ts
3827
+ var recipes = [
3828
+ { id: "starter-0.1.1-to-0.1.2", from: "0.1.1", to: "0.1.2" },
3829
+ { id: "starter-0.1.2-to-0.2.0", from: "0.1.2", to: "0.2.0" }
3830
+ ];
3831
+ function resolveRecipeChain(sourceVersion, targetVersion) {
3832
+ if (sourceVersion === targetVersion)
3833
+ return [];
3834
+ const chain = [];
3835
+ let current = sourceVersion;
3836
+ const visited = new Set;
3837
+ while (current !== targetVersion) {
3838
+ if (visited.has(current))
3839
+ break;
3840
+ visited.add(current);
3841
+ const recipe = recipes.find(({ from }) => from === current);
3842
+ if (!recipe)
3843
+ break;
3844
+ chain.push(recipe);
3845
+ current = recipe.to;
3846
+ }
3847
+ if (current !== targetVersion) {
3848
+ throw new Error(`没有从 ${sourceVersion} 到 ${targetVersion} 的连续更新 recipe,禁止跳级`);
3849
+ }
3850
+ return chain;
3851
+ }
3852
+
3853
+ // src/update/updater.ts
3854
+ import { mkdir as mkdir7, readFile as readFile12, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
3855
+ import { dirname as dirname4, join as join13 } from "node:path";
3856
+
3857
+ // src/update/artifacts.ts
3858
+ import { mkdir as mkdir6, mkdtemp, readFile as readFile10 } from "node:fs/promises";
3859
+ import { tmpdir } from "node:os";
3860
+ import { basename as basename3, join as join10 } from "node:path";
3861
+ class PublishedArtifactProvider {
3862
+ async prepare(projectDirectory, provenance) {
3863
+ const temporaryRoot = await mkdtemp(join10(tmpdir(), "windy-update-"));
3864
+ const projectName = await readProjectName2(projectDirectory);
3865
+ const baseDirectory = join10(temporaryRoot, "base", projectName);
3866
+ const targetDirectory = join10(temporaryRoot, "target", projectName);
3867
+ await generatePublishedBase(temporaryRoot, projectName, provenance, baseDirectory);
3868
+ await generateProject({
3869
+ sourceRoot: await resolveTemplateRoot(),
3870
+ targetDirectory,
3871
+ projectName,
3872
+ selectedModules: provenance.modules.map(({ name }) => name),
3873
+ includeOxc: provenance.choices.includeOxc,
3874
+ projectLicense: provenance.legalLicense,
3875
+ licenseHolder: provenance.legalLicense === "MIT" ? "Windy project owner" : undefined,
3876
+ profile: provenance.profile
3877
+ });
3878
+ return { temporaryRoot, baseDirectory, targetDirectory };
3879
+ }
3880
+ }
3881
+ async function generatePublishedBase(temporaryRoot, projectName, provenance, expectedDirectory) {
3882
+ const parent = join10(temporaryRoot, "base");
3883
+ await mkdir6(parent, { recursive: true });
3884
+ const args = [
3885
+ "--bun",
3886
+ `create-windy@${provenance.generatorVersion}`,
3887
+ projectName,
3888
+ "--yes",
3889
+ "--skip-install",
3890
+ "--no-git",
3891
+ "--no-optional-modules",
3892
+ "--license",
3893
+ provenance.legalLicense
3894
+ ];
3895
+ if (!provenance.choices.includeOxc)
3896
+ args.push("--no-oxc");
3897
+ if (provenance.legalLicense === "MIT") {
3898
+ args.push("--license-holder", "Windy project owner");
3899
+ }
3900
+ for (const { name } of provenance.modules) {
3901
+ if (!requiredModuleNames.includes(name)) {
3902
+ args.push("--module", name);
3903
+ }
3904
+ }
3905
+ await assertProcess("bunx", args, parent);
3906
+ if (join10(parent, projectName) !== expectedDirectory) {
3907
+ throw new Error("历史 artifact 生成目录异常");
3908
+ }
3909
+ }
3910
+ async function readProjectName2(projectDirectory) {
3911
+ const source = JSON.parse(await readFile10(join10(projectDirectory, "package.json"), "utf8"));
3912
+ return source.name || basename3(projectDirectory);
3913
+ }
3914
+
3915
+ // src/update/planner.ts
3916
+ import { access as access3, readFile as readFile11 } from "node:fs/promises";
3917
+ import { join as join12 } from "node:path";
3918
+
3919
+ // src/update/merge.ts
3920
+ import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile9 } from "node:fs/promises";
3921
+ import { tmpdir as tmpdir2 } from "node:os";
3922
+ import { join as join11 } from "node:path";
3923
+ async function mergeTextFile(base, ours, theirs) {
3924
+ const root = await mkdtemp2(join11(tmpdir2(), "windy-merge-"));
3925
+ try {
3926
+ const oursPath = join11(root, "ours");
3927
+ const basePath = join11(root, "base");
3928
+ const theirsPath = join11(root, "theirs");
3929
+ await Promise.all([
3930
+ writeFile9(oursPath, ours),
3931
+ writeFile9(basePath, base),
3932
+ writeFile9(theirsPath, theirs)
3933
+ ]);
3934
+ const result = await captureProcess("git", ["merge-file", "-p", "--diff3", oursPath, basePath, theirsPath], root);
3935
+ if (result.code === 0) {
3936
+ return { content: new TextEncoder().encode(result.stdout) };
3937
+ }
3938
+ if (result.code === 1)
3939
+ return { conflict: "三方合并存在重叠修改" };
3940
+ return { conflict: result.stderr.trim() || "git merge-file 执行失败" };
3941
+ } finally {
3942
+ await rm6(root, { recursive: true, force: true });
3943
+ }
3944
+ }
3945
+
3946
+ // src/update/package-merge.ts
3947
+ function mergePackageJson(base, ours, theirs) {
3948
+ const conflicts = [];
3949
+ const merged = mergeValue(JSON.parse(new TextDecoder().decode(base)), JSON.parse(new TextDecoder().decode(ours)), JSON.parse(new TextDecoder().decode(theirs)), "package.json", conflicts);
3950
+ return conflicts.length ? { conflicts } : {
3951
+ conflicts,
3952
+ content: new TextEncoder().encode(`${JSON.stringify(merged, null, 2)}
3953
+ `)
3954
+ };
3955
+ }
3956
+ function mergeValue(base, ours, theirs, path2, conflicts) {
3957
+ if (equal(ours, theirs))
3958
+ return ours;
3959
+ if (equal(ours, base))
3960
+ return theirs;
3961
+ if (equal(theirs, base))
3962
+ return ours;
3963
+ if (isObject(base) && isObject(ours) && isObject(theirs)) {
3964
+ const output = {};
3965
+ const keys = new Set([
3966
+ ...Object.keys(base),
3967
+ ...Object.keys(ours),
3968
+ ...Object.keys(theirs)
3969
+ ]);
3970
+ for (const key of keys) {
3971
+ const value = mergeValue(base[key], ours[key], theirs[key], `${path2}.${key}`, conflicts);
3972
+ if (value !== undefined)
3973
+ output[key] = value;
3974
+ }
3975
+ return output;
3976
+ }
3977
+ conflicts.push(path2);
3978
+ return ours;
3979
+ }
3980
+ function equal(left, right) {
3981
+ return JSON.stringify(left) === JSON.stringify(right);
3982
+ }
3983
+ function isObject(value) {
3984
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3985
+ }
3986
+
3987
+ // src/update/planner.ts
3988
+ var metadataPaths = new Set([
3989
+ ".windy/generation.json",
3990
+ ".windy/files.json",
3991
+ ".windy-template.json"
3992
+ ]);
3993
+ var oxcManagedPaths = new Set([
3994
+ ".oxfmtrc.json",
3995
+ ".oxlintrc.json",
3996
+ ".husky/pre-commit"
3997
+ ]);
3998
+ async function createUpdatePlan(request, artifacts) {
3999
+ await assertGitClean(request.projectDirectory);
4000
+ await assertNoPendingUpdate(request.projectDirectory);
4001
+ const provenance = await readGenerationProvenance(request.projectDirectory);
4002
+ const artifactSet = await artifacts.prepare(request.projectDirectory, provenance);
4003
+ const targetProvenance = await readGenerationProvenance(artifactSet.targetDirectory);
4004
+ const targetVersion = request.targetVersion || targetProvenance.generatorVersion;
4005
+ if (targetVersion !== targetProvenance.generatorVersion) {
4006
+ throw new Error(`当前 CLI 只能更新到 ${targetProvenance.generatorVersion},请运行 create-windy@${targetVersion}`);
4007
+ }
4008
+ const recipes2 = resolveRecipeChain(provenance.windyVersion, targetVersion);
4009
+ const [sourceManifest, targetManifest] = await Promise.all([
4010
+ readManagedFilesManifest(request.projectDirectory),
4011
+ readManagedFilesManifest(artifactSet.targetDirectory)
4012
+ ]);
4013
+ targetProvenance.appliedRecipes = [
4014
+ ...provenance.appliedRecipes,
4015
+ ...recipes2.map(({ id }) => id)
4016
+ ];
4017
+ const sourceEntries = new Map(sourceManifest.files.map((item) => [item.path, item]));
4018
+ const targetEntries = new Map(targetManifest.files.map((item) => [item.path, item]));
4019
+ const paths = new Set([...sourceEntries.keys(), ...targetEntries.keys()]);
4020
+ const files = [];
4021
+ let detachedToolchain = false;
4022
+ for (const path2 of [...paths].sort()) {
4023
+ if (metadataPaths.has(path2))
4024
+ continue;
4025
+ const file = await planFile(path2, request.projectDirectory, artifactSet.baseDirectory, artifactSet.targetDirectory, sourceEntries.get(path2), targetEntries.get(path2));
4026
+ if (oxcManagedPaths.has(path2) && file.reason === "客户已修改") {
4027
+ file.action = "preserve";
4028
+ delete file.conflict;
4029
+ detachedToolchain = true;
4030
+ }
4031
+ files.push(file);
4032
+ }
4033
+ const conflicts = files.flatMap((file) => file.conflict ? [`${file.path}:${file.conflict}`] : []);
4034
+ return {
4035
+ id: createUpdateId(provenance.windyVersion, targetVersion),
4036
+ projectDirectory: request.projectDirectory,
4037
+ sourceVersion: provenance.windyVersion,
4038
+ targetVersion,
4039
+ recipes: recipes2.map(({ id }) => id),
4040
+ files,
4041
+ conflicts,
4042
+ detachedToolchain,
4043
+ targetProvenance,
4044
+ targetManifest,
4045
+ temporaryRoot: artifactSet.temporaryRoot
4046
+ };
4047
+ }
4048
+ async function planFile(path2, project, baseRoot, targetRoot, source, target) {
4049
+ const [ours, base, theirs] = await Promise.all([
4050
+ readOptional(join12(project, path2)),
4051
+ readOptional(join12(baseRoot, path2)),
4052
+ readOptional(join12(targetRoot, path2))
4053
+ ]);
4054
+ const ownership = target?.ownership || source?.ownership || classifyOwnership(path2);
4055
+ if (path2.startsWith("packages/database/drizzle/") && path2.endsWith(".sql")) {
4056
+ if (source && !target)
4057
+ return conflict(path2, ownership, "已发布 migration 不可删除");
4058
+ if (source && target && source.hash !== target.hash) {
4059
+ return conflict(path2, ownership, "已发布 migration 不可改写,只能追加新文件");
4060
+ }
4061
+ }
4062
+ if (ownership === "project-owned") {
4063
+ if (!ours && theirs)
4064
+ return ready(path2, ownership, "create", "新增项目扩展点", theirs);
4065
+ return ready(path2, ownership, "preserve", "项目拥有,Windy 不覆盖");
4066
+ }
4067
+ if (!source && target) {
4068
+ if (!ours)
4069
+ return ready(path2, ownership, "create", "目标版本新增", theirs);
4070
+ if (theirs && hashContent(ours) === target.hash) {
4071
+ return ready(path2, ownership, "preserve", "已包含目标内容");
4072
+ }
4073
+ return conflict(path2, ownership, "目标版本新增路径已被项目占用");
4074
+ }
4075
+ if (source && !target) {
4076
+ if (!ours)
4077
+ return ready(path2, ownership, "preserve", "项目已删除");
4078
+ if (hashContent(ours) === source.hash) {
4079
+ return ready(path2, ownership, "delete", "目标版本删除");
4080
+ }
4081
+ return conflict(path2, ownership, "客户修改后 Windy 又删除");
4082
+ }
4083
+ if (!source || !target || !ours || !theirs) {
4084
+ return conflict(path2, ownership, "文件状态不完整,不能安全推断");
4085
+ }
4086
+ const oursChanged = hashContent(ours) !== source.hash;
4087
+ const targetChanged = target.hash !== source.hash;
4088
+ if (!oursChanged && !targetChanged)
4089
+ return ready(path2, ownership, "preserve", "双方未修改");
4090
+ if (!oursChanged)
4091
+ return ready(path2, ownership, "update", "仅 Windy 修改", theirs);
4092
+ if (!targetChanged)
4093
+ return ready(path2, ownership, "preserve", "客户已修改");
4094
+ if (hashContent(ours) === target.hash)
4095
+ return ready(path2, ownership, "preserve", "已包含目标内容");
4096
+ if (path2 === "package.json" && base) {
4097
+ const merged = mergePackageJson(base, ours, theirs);
4098
+ return merged.content ? ready(path2, ownership, "update", "package.json 字段级合并", merged.content) : conflict(path2, ownership, `字段冲突:${merged.conflicts.join("、")}`);
4099
+ }
4100
+ if (ownership === "shared-scaffold" && base) {
4101
+ const merged = await mergeTextFile(base, ours, theirs);
4102
+ return merged.content ? ready(path2, ownership, "update", "三方合并", merged.content) : conflict(path2, ownership, merged.conflict || "三方合并失败");
4103
+ }
4104
+ return conflict(path2, ownership, "客户和 Windy 均已修改");
4105
+ }
4106
+ function ready(path2, ownership, action, reason, content) {
4107
+ return { path: path2, ownership, action, reason, content };
4108
+ }
4109
+ function conflict(path2, ownership, reason) {
4110
+ return {
4111
+ path: path2,
4112
+ ownership,
4113
+ action: "preserve",
4114
+ reason: "客户已修改",
4115
+ conflict: reason
4116
+ };
4117
+ }
4118
+ async function readOptional(path2) {
4119
+ try {
4120
+ return await readFile11(path2);
4121
+ } catch (error) {
4122
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
4123
+ return;
4124
+ throw error;
4125
+ }
4126
+ }
4127
+ async function assertGitClean(projectDirectory) {
4128
+ const result = await captureProcess("git", ["status", "--porcelain"], projectDirectory);
4129
+ if (result.code !== 0)
4130
+ throw new Error("更新要求当前目录是有效的 Git 仓库");
4131
+ if (result.stdout.trim())
4132
+ throw new Error("更新前请先提交或暂存之外妥善处理 Git 工作区改动");
4133
+ }
4134
+ async function assertNoPendingUpdate(projectDirectory) {
4135
+ try {
4136
+ await access3(join12(projectDirectory, ".windy/update-state.json"));
4137
+ throw new Error("检测到未完成更新,请先运行 bun run windy doctor --repair");
4138
+ } catch (error) {
4139
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
4140
+ return;
4141
+ throw error;
4142
+ }
4143
+ }
4144
+ function createUpdateId(from, to) {
4145
+ return `${new Date().toISOString().replaceAll(/[:.]/g, "-")}-${from}-to-${to}`;
4146
+ }
4147
+
4148
+ // src/update/updater.ts
4149
+ class DefaultStarterUpdater {
4150
+ #artifacts;
4151
+ #execute;
4152
+ constructor(dependencies = {}) {
4153
+ this.#artifacts = dependencies.artifacts ?? new PublishedArtifactProvider;
4154
+ this.#execute = dependencies.execute ?? runCommand;
4155
+ }
4156
+ async plan(input) {
4157
+ return await createUpdatePlan(input, this.#artifacts);
4158
+ }
4159
+ async apply(plan) {
4160
+ try {
4161
+ return await applyTransaction(plan);
4162
+ } catch (error) {
4163
+ await rm7(plan.temporaryRoot, { recursive: true, force: true });
4164
+ throw error;
4165
+ }
4166
+ }
4167
+ async verify(result) {
4168
+ const steps = [];
4169
+ try {
4170
+ await this.#execute("bun", ["install"], result.plan.projectDirectory);
4171
+ steps.push({ name: "bun install", status: "passed" });
4172
+ const packageJson = JSON.parse(await readFile12(join13(result.plan.projectDirectory, "package.json"), "utf8"));
4173
+ for (const name of ["lint", "typecheck", "test", "build"]) {
4174
+ if (!packageJson.scripts?.[name]) {
4175
+ steps.push({ name: `bun run ${name}`, status: "skipped" });
4176
+ continue;
4177
+ }
4178
+ await this.#execute("bun", ["run", name], result.plan.projectDirectory);
4179
+ steps.push({ name: `bun run ${name}`, status: "passed" });
4180
+ }
4181
+ await finalize(result);
4182
+ const reportPath = `.windy/reports/${result.plan.id}.md`;
4183
+ await writeReport(result, steps, reportPath);
4184
+ await clearUpdateState(result.plan.projectDirectory);
4185
+ await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4186
+ return { updateId: result.plan.id, status: "passed", steps, reportPath };
4187
+ } catch (error) {
4188
+ await restoreUpdateState(result.plan.projectDirectory, {
4189
+ schemaVersion: 1,
4190
+ updateId: result.plan.id,
4191
+ backupDirectory: result.backupDirectory,
4192
+ entries: result.backupEntries
4193
+ });
4194
+ await rm7(result.plan.temporaryRoot, { recursive: true, force: true });
4195
+ throw new Error(`更新验证失败,项目已回滚到 ${result.plan.sourceVersion}:${error instanceof Error ? error.message : String(error)}`, { cause: error });
4196
+ }
4197
+ }
4198
+ }
4199
+ async function finalize(result) {
4200
+ const root = result.plan.projectDirectory;
4201
+ await writeFile10(join13(root, ".windy/generation.json"), `${JSON.stringify(result.plan.targetProvenance, null, 2)}
4202
+ `);
4203
+ await writeFile10(join13(root, ".windy/files.json"), `${JSON.stringify(result.plan.targetManifest, null, 2)}
4204
+ `);
4205
+ }
4206
+ async function writeReport(result, steps, relativePath2) {
4207
+ const path2 = join13(result.plan.projectDirectory, relativePath2);
4208
+ await mkdir7(dirname4(path2), { recursive: true });
4209
+ const lines = [
4210
+ `# Windy 更新报告 ${result.plan.id}`,
4211
+ "",
4212
+ `- 来源版本:${result.plan.sourceVersion}`,
4213
+ `- 目标版本:${result.plan.targetVersion}`,
4214
+ `- Recipe:${result.plan.recipes.join("、") || "无"}`,
4215
+ `- 修改文件:${result.changedFiles.length}`,
4216
+ `- Oxc 代管:${result.plan.detachedToolchain ? "已检测到客户接管并保留" : "保持"}`,
4217
+ "",
4218
+ "## 验证",
4219
+ "",
4220
+ ...steps.map(({ name, status }) => `- ${name}:${status}`),
4221
+ "",
4222
+ `备份保留在 \`.windy/backups/${result.plan.id}\`,确认稳定后可人工删除。`,
4223
+ ""
4224
+ ];
4225
+ await writeFile10(path2, lines.join(`
4226
+ `));
4227
+ }
4228
+
4229
+ // src/update/cli.ts
4230
+ async function runLifecycleCommand(args) {
4231
+ const command = args[0];
4232
+ if (command !== "doctor" && command !== "update")
4233
+ return false;
4234
+ const projectDirectory = resolve4(readValue2(args, "--cwd") || process.cwd());
4235
+ if (command === "doctor") {
4236
+ const report = await diagnoseProject(projectDirectory, args.includes("--repair"));
4237
+ printDoctor(report);
4238
+ if (report.recoveryRequired)
4239
+ process.exitCode = 1;
4240
+ return true;
4241
+ }
4242
+ await runUpdate(args, projectDirectory);
4243
+ return true;
4244
+ }
4245
+ async function runUpdate(args, projectDirectory) {
4246
+ const template = await readTemplateMetadata(await resolveTemplateRoot());
4247
+ const requested = readValue2(args, "--to");
4248
+ const registryVersion = requested ? undefined : await readRegistryLatest();
4249
+ if (registryVersion && compareVersions2(registryVersion, template.generatorVersion) > 0 && !args.includes("--no-delegate")) {
4250
+ if (args.includes("--check")) {
4251
+ const current2 = await readGenerationProvenance(projectDirectory);
4252
+ console.log(`可更新:${current2.windyVersion} → ${registryVersion}(请使用新版 CLI 生成完整计划)`);
4253
+ return;
4254
+ }
4255
+ await delegateUpdate(args, registryVersion, projectDirectory);
4256
+ return;
4257
+ }
4258
+ if (requested && requested !== template.generatorVersion && !args.includes("--no-delegate")) {
4259
+ await delegateUpdate(args, requested, projectDirectory);
4260
+ return;
4261
+ }
4262
+ const current = await readGenerationProvenance(projectDirectory);
4263
+ const target = requested || template.generatorVersion;
4264
+ const recipes2 = resolveRecipeChain(current.windyVersion, target);
4265
+ if (args.includes("--check")) {
4266
+ console.log(recipes2.length ? `可更新:${current.windyVersion} → ${target}(${recipes2.length} 个连续 recipe)` : `已是当前 CLI 支持的最新版本 ${current.windyVersion}`);
4267
+ return;
4268
+ }
4269
+ if (!recipes2.length) {
4270
+ console.log(`项目已经是 ${target},无需更新`);
4271
+ return;
4272
+ }
4273
+ const updater = new DefaultStarterUpdater;
4274
+ const plan = await updater.plan({ projectDirectory, targetVersion: target });
4275
+ printPlan(plan);
4276
+ if (args.includes("--dry-run")) {
4277
+ await rm8(plan.temporaryRoot, { recursive: true, force: true });
4278
+ console.log(`
4279
+ 演练完成:未写入项目文件`);
4280
+ return;
4281
+ }
4282
+ if (plan.conflicts.length) {
4283
+ await rm8(plan.temporaryRoot, { recursive: true, force: true });
4284
+ throw new Error("存在更新冲突;请按上方清单处理后重新运行,项目未被修改");
4285
+ }
4286
+ const result = await updater.apply(plan);
4287
+ const report = await updater.verify(result);
4288
+ console.log(`
4289
+ 更新完成:${plan.sourceVersion} → ${plan.targetVersion}`);
4290
+ console.log(`报告:${report.reportPath}`);
4291
+ }
4292
+ async function delegateUpdate(args, target, projectDirectory) {
4293
+ const forwarded = args.slice(1).filter((item, index, all) => {
4294
+ if (item === "--cwd" || item === "--to")
4295
+ return false;
4296
+ return all[index - 1] !== "--cwd" && all[index - 1] !== "--to";
4297
+ });
4298
+ await assertProcess("bunx", [
4299
+ "--bun",
4300
+ `create-windy@${target}`,
4301
+ "update",
4302
+ "--cwd",
4303
+ projectDirectory,
4304
+ "--to",
4305
+ target,
4306
+ "--no-delegate",
4307
+ ...forwarded
4308
+ ], projectDirectory, true);
4309
+ }
4310
+ function printPlan(plan) {
4311
+ const changed = plan.files.filter(({ action }) => action !== "preserve");
4312
+ console.log(`Windy 更新计划:${plan.sourceVersion} → ${plan.targetVersion}`);
4313
+ console.log(`Recipe:${plan.recipes.join("、")}`);
4314
+ console.log(`计划改动:${changed.length} 个文件;冲突:${plan.conflicts.length}`);
4315
+ for (const file of changed)
4316
+ console.log(` ${file.action.padEnd(6)} ${file.path}(${file.reason})`);
4317
+ for (const conflict2 of plan.conflicts)
4318
+ console.log(` conflict ${conflict2}`);
4319
+ if (plan.detachedToolchain)
4320
+ console.log("Oxc:检测到客户接管,相关配置保持不变");
4321
+ }
4322
+ function printDoctor(report) {
4323
+ console.log(`Windy Project Lock:${report.version}`);
4324
+ console.log(`Git 工作区:${report.gitClean ? "干净" : "有改动"}`);
4325
+ console.log(`客户修改的受管文件:${report.modifiedManagedFiles.length}`);
4326
+ console.log(`缺失的受管文件:${report.missingManagedFiles.length}`);
4327
+ console.log(`Oxc:${report.detachedToolchain ? "客户已接管" : "仍由 Windy 代管或未启用"}`);
4328
+ if (report.recoveryRequired)
4329
+ console.log("恢复:检测到中断更新,请运行 doctor --repair");
4330
+ if (report.repaired)
4331
+ console.log("恢复:已从事务备份回滚");
4332
+ }
4333
+ function readValue2(args, flag) {
4334
+ const index = args.indexOf(flag);
4335
+ return index >= 0 ? args[index + 1] : undefined;
4336
+ }
4337
+ async function readRegistryLatest() {
4338
+ try {
4339
+ const response = await fetch("https://registry.npmjs.org/create-windy/latest");
4340
+ if (!response.ok)
4341
+ return;
4342
+ const payload = await response.json();
4343
+ return payload.version;
4344
+ } catch {
4345
+ return;
4346
+ }
4347
+ }
4348
+ function compareVersions2(left, right) {
4349
+ const leftParts = left.split(".").map(Number);
4350
+ const rightParts = right.split(".").map(Number);
4351
+ for (let index = 0;index < 3; index += 1) {
4352
+ const difference = (leftParts[index] || 0) - (rightParts[index] || 0);
4353
+ if (difference)
4354
+ return difference;
4355
+ }
4356
+ return 0;
4357
+ }
3508
4358
 
3509
4359
  // src/cli.ts
3510
4360
  async function main() {
3511
- const options = parseArguments(process.argv.slice(2));
4361
+ const args = process.argv.slice(2);
4362
+ if (await runLifecycleCommand(args))
4363
+ return;
4364
+ const options = parseArguments(args);
3512
4365
  if (options === "help") {
3513
4366
  console.log(helpText);
3514
4367
  return;