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/README.md +31 -8
- package/dist/cli.js +890 -37
- package/package.json +3 -3
- package/template/.husky/pre-commit +15 -0
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +28 -75
- package/template/README.md +20 -6
- package/template/_gitignore +2 -0
- package/template/package.json +5 -0
- /package/{assets/LICENSE-Apache-2.0.txt → LICENSE} +0 -0
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
|
-
"-
|
|
878
|
-
"-
|
|
893
|
+
"- 生成时默认预装 Oxlint、Oxfmt、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, "
|
|
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([
|
|
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
|
|
1298
|
-
|
|
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\`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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 ? {
|
|
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 ||
|
|
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.
|
|
3429
|
-
choices:
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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;
|