create-windy 0.2.28 → 0.2.30

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
@@ -394,8 +394,9 @@ var optionalModuleManifests = [
394
394
  "apps/server/src/example-permissions.ts"
395
395
  ],
396
396
  documentation: ["docs/development/example-work-order.md"],
397
- schemaFiles: ["packages/database/src/schema/work-orders.ts"],
398
- databaseTables: ["work_orders"]
397
+ schemaFiles: ["packages/example-work-order/src/drizzle-schema.ts"],
398
+ databaseTables: ["work_orders"],
399
+ databaseLineageOwner: "module"
399
400
  }
400
401
  ];
401
402
  function module(name, label, description, dependencies) {
@@ -414,6 +415,7 @@ function module(name, label, description, dependencies) {
414
415
  documentation: [],
415
416
  schemaFiles: [],
416
417
  databaseTables: [],
418
+ databaseLineageOwner: "platform",
417
419
  transientDatabaseTables: [],
418
420
  databaseStatementTokens: [],
419
421
  uninstallPolicy: "retain-data"
@@ -628,6 +630,7 @@ function module2(name, label, description, selection, dependencies = []) {
628
630
  documentation: [],
629
631
  schemaFiles: [],
630
632
  databaseTables: [],
633
+ databaseLineageOwner: "platform",
631
634
  transientDatabaseTables: [],
632
635
  databaseStatementTokens: [],
633
636
  uninstallPolicy: "retain-data"
@@ -735,6 +738,7 @@ var helpText = `create-windy <项目名> [选项]
735
738
  create-windy update --check [--cwd <目录>]
736
739
  create-windy update --dry-run [--to <版本>] [--cwd <目录>]
737
740
  create-windy update [--to <版本>] [--cwd <目录>]
741
+ create-windy schema handoff [--config <JSON>] [--cwd <目录>]
738
742
 
739
743
  选项:
740
744
  --profile private-enterprise 单组织、单租户、私有部署 profile
@@ -1200,8 +1204,11 @@ var ignoredFiles = new Set([
1200
1204
  ".windy/update-state.json"
1201
1205
  ]);
1202
1206
  var projectOwnedRoots = [
1207
+ "module-schema.lineages.json",
1208
+ "module-schema",
1203
1209
  "apps/web/src/app",
1204
1210
  "apps/server/src/installed-business-modules.ts",
1211
+ "apps/server/src/domain-schema",
1205
1212
  "apps/server/src/work-order",
1206
1213
  "packages/example-work-order"
1207
1214
  ];
@@ -1320,10 +1327,13 @@ async function materializeSelectedModules(targetDirectory, selectedNames) {
1320
1327
  removedPackageNames.add(dependency.name);
1321
1328
  }
1322
1329
  await stripModuleBlocks(targetDirectory, selected);
1323
- await pruneDatabaseArtifacts(targetDirectory, removed.flatMap(({ databaseTables, transientDatabaseTables }) => [
1324
- ...databaseTables,
1325
- ...transientDatabaseTables
1326
- ]), removed.flatMap(({ databaseStatementTokens }) => databaseStatementTokens));
1330
+ await pruneDatabaseArtifacts(targetDirectory, [
1331
+ ...removed.flatMap(({ databaseTables, transientDatabaseTables }) => [
1332
+ ...databaseTables,
1333
+ ...transientDatabaseTables
1334
+ ]),
1335
+ ...moduleDistributionCatalog.filter(({ databaseLineageOwner }) => databaseLineageOwner === "module").flatMap(({ databaseTables }) => databaseTables)
1336
+ ], removed.flatMap(({ databaseStatementTokens }) => databaseStatementTokens));
1327
1337
  const removedRoots = removed.flatMap((manifest) => [
1328
1338
  ...manifest.packages,
1329
1339
  ...manifest.fileRoots,
@@ -1624,6 +1634,7 @@ License 签发中心、独立签名服务、Vault、发布治理、内部任务
1624
1634
  规划属于软件供应方控制面,不进入客户 Starter。
1625
1635
 
1626
1636
  - 架构:从 [Module Manifest 组合](./architecture/module-manifest-composition.md) 开始。
1637
+ - 数据库:业务表使用 [Migration Bundle 与独立 Drizzle lineage](./architecture/migration-bundle.md)。
1627
1638
  - 平台开发:从 [系统 CRUD API](./platform/system-crud-api.md) 和 [CRUD 生成器](./development/crud-generator.md) 开始。
1628
1639
  ${includeOfflineLicense ? "- License 消费端:[离线激活与安装协议](./license/license-offline-activation-code.md);运行时能力由 npm 依赖 `@southwind-ai/license` 提供。" : ""}
1629
1640
  - 运维:[可观测性](./operations/observability.md) 与 [运维总览](./platform/operations-overview.md)。
@@ -1748,6 +1759,9 @@ create-windy 在本机 \`.env\` 中随机生成的 \`WINDY_BOOTSTRAP_ADMIN_PASSW
1748
1759
  | ---------- | -------- | ------------ | ---- |
1749
1760
  ${moduleRows}
1750
1761
 
1762
+ 项目自有业务模块以 \`apps/server/src/installed-business-modules.ts\` 装配的 Module Manifest
1763
+ 为准;shared-scaffold README 不复制项目业务 Key,避免平台升级固化产品命名。
1764
+
1751
1765
  精确生成来源和选择记录在 \`.windy/generation.json\`。当前版本已提供可回滚的
1752
1766
  \`windy update\`;通用 \`module add/remove\` 尚未交付,不要把 Feature Flag 关闭当成
1753
1767
  模块已经卸载。
@@ -1876,6 +1890,7 @@ var templateRootFiles = [
1876
1890
  "bunfig.toml",
1877
1891
  "docker-compose.yml",
1878
1892
  "drizzle.config.ts",
1893
+ "module-schema.lineages.json",
1879
1894
  "package.json"
1880
1895
  ];
1881
1896
  var templateDirectories = [
@@ -1902,6 +1917,7 @@ var templateDocumentationFiles = [
1902
1917
  "docs/architecture/data-scope-query-enforcement.md",
1903
1918
  "docs/architecture/durable-jobs.md",
1904
1919
  "docs/architecture/minimal-approval-workflow.md",
1920
+ "docs/architecture/migration-bundle.md",
1905
1921
  "docs/architecture/module-manifest-composition.md",
1906
1922
  "docs/architecture/object-storage.md",
1907
1923
  "docs/architecture/search-provider-protocol.md",
@@ -4337,7 +4353,9 @@ var recipes = [
4337
4353
  { id: "starter-0.2.24-to-0.2.25", from: "0.2.24", to: "0.2.25" },
4338
4354
  { id: "starter-0.2.25-to-0.2.26", from: "0.2.25", to: "0.2.26" },
4339
4355
  { id: "starter-0.2.26-to-0.2.27", from: "0.2.26", to: "0.2.27" },
4340
- { id: "starter-0.2.27-to-0.2.28", from: "0.2.27", to: "0.2.28" }
4356
+ { id: "starter-0.2.27-to-0.2.28", from: "0.2.27", to: "0.2.28" },
4357
+ { id: "starter-0.2.28-to-0.2.29", from: "0.2.28", to: "0.2.29" },
4358
+ { id: "starter-0.2.29-to-0.2.30", from: "0.2.29", to: "0.2.30" }
4341
4359
  ];
4342
4360
  function resolveRecipeChain(sourceVersion, targetVersion) {
4343
4361
  if (sourceVersion === targetVersion)
@@ -4436,8 +4454,13 @@ var known026DownstreamHashes = new Map([
4436
4454
  [
4437
4455
  "apps/server/src/module-bootstrap.ts",
4438
4456
  "4b79ab5d0271b3111c7cb546e0b581d483da8c5de263f8d2f9671d53f60c5099"
4457
+ ],
4458
+ [
4459
+ "apps/web/src/router/router-test-fixtures.ts",
4460
+ "e498fa34cf6ab6dbabf014ec11233772f30995dceab250ff4f993323834ebc82"
4439
4461
  ]
4440
4462
  ]);
4463
+ var known026RouterHash = "55a60a28d7a8e88386a38cab763b1eac43d66a11e60b84a21acefacc29c6f44d";
4441
4464
  var known023DownstreamHashes = new Map([
4442
4465
  [
4443
4466
  "apps/server/src/http-app.ts",
@@ -4460,11 +4483,32 @@ var known023DownstreamHashes = new Map([
4460
4483
  "81fb08f26e5baa3238606d62b9c46761a68c90f3967ea639b178c80c74c69fc1"
4461
4484
  ]
4462
4485
  ]);
4486
+ var known029DownstreamHashes = new Map([
4487
+ [
4488
+ "README.md",
4489
+ "a8b10907e151c28768678c016d102f98c3414981a65a314d75b2049fb1f9fe35"
4490
+ ],
4491
+ [
4492
+ "drizzle.config.ts",
4493
+ "c765b611d68b3a42375ea3530048fc7f32b1c7161257fa16f9dd093b580963e3"
4494
+ ],
4495
+ [
4496
+ "packages/database/src/schema/index.ts",
4497
+ "250682d5182b580a9e1fae27e7a596e6a503b41d6f5dca9e4322f5c6094d0976"
4498
+ ],
4499
+ [
4500
+ "apps/web/src/router/shared-scaffold-neutrality.webtest.ts",
4501
+ "420f1093eefcccf2a321e12c403628854c9a4a0b459ee14e96883557cd71dcaa"
4502
+ ]
4503
+ ]);
4463
4504
  var wrongExampleDescription = "创建时包含了 `work-order` 示例模块,用于演示 Module Manifest 接入;它不是生产工单产品,可以在正式开发前替换。";
4464
4505
  var correctCoreDescription = "创建时未选择 `work-order` 示例模块,当前项目只包含平台模块。";
4465
4506
  var wrongExampleRow = /^\|\s*示例工单\s*\|\s*已包含\s*\|$/m;
4466
4507
  var correctCoreRow = /^\|\s*示例工单\s*\|\s*未包含\s*\|$/m;
4467
4508
  function repairKnownDerivedFile(input) {
4509
+ const ownershipRepair = repairKnown029Downstream(input);
4510
+ if (ownershipRepair !== undefined)
4511
+ return ownershipRepair;
4468
4512
  const neutralRouterRepair = repairKnown026Downstream(input);
4469
4513
  if (neutralRouterRepair !== undefined)
4470
4514
  return neutralRouterRepair;
@@ -4488,29 +4532,55 @@ function repairKnownDerivedFile(input) {
4488
4532
  }
4489
4533
  return input.content.replace(wrongExampleDescription, correctCoreDescription).replace(wrongExampleRow, targetRow);
4490
4534
  }
4491
- function repairKnown026Downstream(input) {
4492
- if (input.sourceVersion !== "0.2.26")
4535
+ function repairKnown029Downstream(input) {
4536
+ if (input.sourceVersion !== "0.2.29" || known029DownstreamHashes.get(input.path) !== contentHash(input.content)) {
4493
4537
  return;
4494
- if (known026DownstreamHashes.get(input.path) === contentHash(input.content)) {
4538
+ }
4539
+ if (input.path === "drizzle.config.ts" || input.path === "apps/web/src/router/shared-scaffold-neutrality.webtest.ts") {
4495
4540
  return input.targetContent;
4496
4541
  }
4497
- if (input.path === "apps/web/src/router/index.ts" && input.targetContent.includes("@/pages/auth/LoginPage.vue")) {
4498
- return replaceKnown(input.content, 'import LoginPage from "@/app/auth/LijianLoginPage.vue";', 'import LoginPage from "@/pages/auth/LoginPage.vue";');
4542
+ if (input.path === "packages/database/src/schema/index.ts") {
4543
+ return removeUnknownSchemaExports(input.content, input.targetContent);
4499
4544
  }
4500
- if (input.path !== "apps/web/src/router/router-test-fixtures.ts" || input.targetContent.includes("lijian.")) {
4501
- return;
4545
+ if (input.path === "README.md") {
4546
+ return removeProjectModuleRows(input.content, input.targetContent);
4502
4547
  }
4503
- const replacements2 = [
4504
- ["lijian.case.read", "case.read"],
4505
- ["lijian.evidence.read", "evidence.read"],
4506
- ["lijian.case", "case"],
4507
- ["lijian.evidence.import", "evidence.import"],
4508
- ["lijian.evidence", "evidence"]
4509
- ];
4510
- if (!replacements2.every(([source]) => input.content.includes(source))) {
4548
+ }
4549
+ function removeUnknownSchemaExports(content, targetContent) {
4550
+ const targetExports = new Set(targetContent.split(`
4551
+ `).filter((line) => line.startsWith("export ") && line.includes(" from ")));
4552
+ return content.split(`
4553
+ `).filter((line) => !line.startsWith("export ") || !line.includes(" from ") || targetExports.has(line)).join(`
4554
+ `);
4555
+ }
4556
+ function removeProjectModuleRows(content, targetContent) {
4557
+ const neutralNote = extractProjectModuleNote(targetContent);
4558
+ const withoutProjectRows = content.split(`
4559
+ `).filter((line) => !/^\|.*\|\s*产品模块\s*\|.*\|$/.test(line)).join(`
4560
+ `);
4561
+ if (!neutralNote || withoutProjectRows.includes(neutralNote.trim())) {
4562
+ return withoutProjectRows;
4563
+ }
4564
+ return replaceKnown(withoutProjectRows, `
4565
+ 精确生成来源和选择记录在`, `
4566
+ ${neutralNote}
4567
+ 精确生成来源和选择记录在`);
4568
+ }
4569
+ function extractProjectModuleNote(content) {
4570
+ const start = content.indexOf("项目自有业务模块以");
4571
+ const end = content.indexOf(`
4572
+ 精确生成来源和选择记录在`, start);
4573
+ return start < 0 || end < 0 ? undefined : content.slice(start, end);
4574
+ }
4575
+ function repairKnown026Downstream(input) {
4576
+ if (input.sourceVersion !== "0.2.26")
4511
4577
  return;
4578
+ if (known026DownstreamHashes.get(input.path) === contentHash(input.content)) {
4579
+ return input.targetContent;
4580
+ }
4581
+ if (input.path === "apps/web/src/router/index.ts" && contentHash(input.content) === known026RouterHash && input.targetContent.includes("@/pages/auth/LoginPage.vue")) {
4582
+ return replaceKnown(input.content, /import LoginPage from "@\/app\/auth\/[^"\n]+LoginPage\.vue";/, 'import LoginPage from "@/pages/auth/LoginPage.vue";');
4512
4583
  }
4513
- return replacements2.reduce((content, [source, target]) => content.replaceAll(source, target), input.content);
4514
4584
  }
4515
4585
  function repairKnown023Downstream(input) {
4516
4586
  if (input.sourceVersion !== "0.2.23" || known023DownstreamHashes.get(input.path) !== contentHash(input.content)) {
@@ -5466,9 +5536,301 @@ function compareVersions2(left, right) {
5466
5536
  return 0;
5467
5537
  }
5468
5538
 
5539
+ // src/module-schema/cli.ts
5540
+ import { resolve as resolve7 } from "node:path";
5541
+
5542
+ // src/module-schema/handoff.ts
5543
+ import {
5544
+ access as access4,
5545
+ cp,
5546
+ mkdir as mkdir8,
5547
+ mkdtemp as mkdtemp3,
5548
+ readFile as readFile18,
5549
+ readdir as readdir7,
5550
+ rename,
5551
+ rm as rm9,
5552
+ writeFile as writeFile13
5553
+ } from "node:fs/promises";
5554
+ import { dirname as dirname5, join as join18, relative as relative4 } from "node:path";
5555
+
5556
+ // src/module-schema/config.ts
5557
+ import { readFile as readFile17 } from "node:fs/promises";
5558
+ import { isAbsolute, relative as relative3, resolve as resolve6 } from "node:path";
5559
+ async function readModuleSchemaLineageConfig(projectDirectory, configPath) {
5560
+ const absolutePath = resolveProjectPath(projectDirectory, configPath);
5561
+ const parsed = JSON.parse(await readFile17(absolutePath, "utf8"));
5562
+ if (!isConfig(parsed)) {
5563
+ throw new Error("Module schema lineage 配置无效");
5564
+ }
5565
+ validatePaths(projectDirectory, parsed);
5566
+ validateUniqueness(parsed);
5567
+ if (parsed.modules.length === 0) {
5568
+ throw new Error("Module schema lineage 配置没有待交接的业务模块");
5569
+ }
5570
+ return parsed;
5571
+ }
5572
+ function resolveProjectPath(projectDirectory, path2) {
5573
+ if (isAbsolute(path2))
5574
+ throw new Error(`路径必须相对项目根目录:${path2}`);
5575
+ const resolved = resolve6(projectDirectory, path2);
5576
+ const projectRelative = relative3(projectDirectory, resolved);
5577
+ if (projectRelative.startsWith("..") || isAbsolute(projectRelative)) {
5578
+ throw new Error(`路径越出项目根目录:${path2}`);
5579
+ }
5580
+ return resolved;
5581
+ }
5582
+ function isConfig(value) {
5583
+ if (!value || typeof value !== "object")
5584
+ return false;
5585
+ const config = value;
5586
+ return config.schemaVersion === 1 && isPlatform(config.platform) && Array.isArray(config.modules) && config.modules.every(isModule);
5587
+ }
5588
+ function isPlatform(value) {
5589
+ if (!value || typeof value !== "object")
5590
+ return false;
5591
+ const platform = value;
5592
+ return isStringList(platform.schema) && typeof platform.out === "string";
5593
+ }
5594
+ function isModule(value) {
5595
+ if (!value || typeof value !== "object")
5596
+ return false;
5597
+ const module3 = value;
5598
+ return typeof module3.key === "string" && /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(module3.key) && isStringList(module3.schema) && typeof module3.out === "string" && isStringList(module3.legacyPlatformTables) && module3.legacyPlatformTables.every(isTableName);
5599
+ }
5600
+ function isStringList(value) {
5601
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
5602
+ }
5603
+ function isTableName(value) {
5604
+ return /^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/.test(value);
5605
+ }
5606
+ function validatePaths(projectDirectory, config) {
5607
+ for (const path2 of [
5608
+ ...config.platform.schema,
5609
+ config.platform.out,
5610
+ ...config.modules.flatMap(({ schema, out }) => [...schema, out])
5611
+ ]) {
5612
+ resolveProjectPath(projectDirectory, path2);
5613
+ }
5614
+ const platformOut = resolveProjectPath(projectDirectory, config.platform.out);
5615
+ for (const module3 of config.modules) {
5616
+ const moduleOut = resolveProjectPath(projectDirectory, module3.out);
5617
+ if (moduleOut === platformOut || moduleOut.startsWith(`${platformOut}/`) || platformOut.startsWith(`${moduleOut}/`)) {
5618
+ throw new Error(`${module3.key} 的 lineage 输出不能与平台目录重叠`);
5619
+ }
5620
+ }
5621
+ }
5622
+ function validateUniqueness(config) {
5623
+ const keys = new Set;
5624
+ const outputs = new Set;
5625
+ const tables = new Set;
5626
+ for (const module3 of config.modules) {
5627
+ if (keys.has(module3.key))
5628
+ throw new Error(`Module key 重复:${module3.key}`);
5629
+ if (outputs.has(module3.out))
5630
+ throw new Error(`Module lineage 输出重复:${module3.out}`);
5631
+ keys.add(module3.key);
5632
+ outputs.add(module3.out);
5633
+ for (const table of module3.legacyPlatformTables) {
5634
+ if (tables.has(table))
5635
+ throw new Error(`旧平台表重复声明:${table}`);
5636
+ tables.add(table);
5637
+ }
5638
+ }
5639
+ }
5640
+
5641
+ // src/module-schema/sql.ts
5642
+ function validatePlatformHandoffSql(sql, allowedTables) {
5643
+ const allowed = new Set(allowedTables);
5644
+ const dropped = new Set;
5645
+ const statements = sql.replaceAll("--> statement-breakpoint", "").split(";").map((statement) => statement.trim()).filter(Boolean);
5646
+ for (const statement of statements) {
5647
+ const match = statement.match(/^DROP TABLE(?: IF EXISTS)? (?:(?:"([^"]+)")\.)?"([^"]+)"(?: CASCADE)?$/i);
5648
+ if (!match) {
5649
+ throw new Error(`交接差异包含非 DROP TABLE DDL:${statement}`);
5650
+ }
5651
+ const table = normalizeTableName(match[1], match[2]);
5652
+ if (!allowed.has(table)) {
5653
+ throw new Error(`交接差异试图删除未声明的表:${table}`);
5654
+ }
5655
+ dropped.add(table);
5656
+ }
5657
+ const missing = [...allowed].filter((table) => !dropped.has(table));
5658
+ if (missing.length > 0) {
5659
+ throw new Error(`平台 snapshot 未包含声明的旧表:${missing.join("、")}`);
5660
+ }
5661
+ }
5662
+ function metadataOnlySql(kind) {
5663
+ const owner = kind === "platform" ? "platform lineage release" : "module lineage baseline";
5664
+ return `-- Windy ${owner}.
5665
+ -- Metadata-only migration: intentionally no executable DDL.
5666
+ `;
5667
+ }
5668
+ function normalizeTableName(schema, table) {
5669
+ return schema && schema !== "public" ? `${schema}.${table}` : table;
5670
+ }
5671
+
5672
+ // src/module-schema/handoff.ts
5673
+ async function handoffModuleSchemaLineages(options) {
5674
+ const project = options.projectDirectory;
5675
+ await assertCleanGit(project);
5676
+ const config = await readModuleSchemaLineageConfig(project, options.configPath || "module-schema.lineages.json");
5677
+ await validateSchemaSources(project, config);
5678
+ const temporaryRoot = await createTemporaryRoot(project);
5679
+ const execute = options.execute || executeDrizzleGenerate;
5680
+ try {
5681
+ const stagedModules = await stageModuleLineages(project, temporaryRoot, config, execute);
5682
+ const stagedPlatform = await stagePlatformLineage(project, temporaryRoot, config, execute);
5683
+ await installStagedLineages(project, temporaryRoot, config, stagedModules, stagedPlatform);
5684
+ } finally {
5685
+ await rm9(temporaryRoot, { recursive: true, force: true });
5686
+ }
5687
+ return {
5688
+ moduleCount: config.modules.length,
5689
+ tableCount: config.modules.reduce((total, module3) => total + module3.legacyPlatformTables.length, 0)
5690
+ };
5691
+ }
5692
+ async function stageModuleLineages(project, temporaryRoot, config, execute) {
5693
+ const outputs = [];
5694
+ for (const [index, module3] of config.modules.entries()) {
5695
+ const output = join18(temporaryRoot, `module-${index}`);
5696
+ await mkdir8(output, { recursive: true });
5697
+ const generatedConfig = join18(temporaryRoot, `module-${index}.config.ts`);
5698
+ await writeDrizzleConfig(generatedConfig, project, module3.schema, relative4(temporaryRoot, output));
5699
+ await execute(generatedConfig, temporaryRoot);
5700
+ const sqlFiles = await listSqlFiles(output);
5701
+ if (sqlFiles.length !== 1) {
5702
+ throw new Error(`${module3.key} baseline 必须恰好生成一个 migration`);
5703
+ }
5704
+ await writeFile13(join18(output, sqlFiles[0]), metadataOnlySql("module"));
5705
+ await writeFile13(join18(output, "lineage.json"), `${JSON.stringify({
5706
+ schemaVersion: 1,
5707
+ moduleKey: module3.key,
5708
+ schema: module3.schema,
5709
+ legacyPlatformTables: module3.legacyPlatformTables,
5710
+ productionExecutor: "module-migration-bundle"
5711
+ }, null, 2)}
5712
+ `);
5713
+ outputs.push(output);
5714
+ }
5715
+ return outputs;
5716
+ }
5717
+ async function stagePlatformLineage(project, temporaryRoot, config, execute) {
5718
+ const source = resolveProjectPath(project, config.platform.out);
5719
+ const output = join18(temporaryRoot, "platform");
5720
+ await cp(source, output, { recursive: true });
5721
+ const before = new Set(await listSqlFiles(output));
5722
+ const generatedConfig = join18(temporaryRoot, "platform.config.ts");
5723
+ await writeDrizzleConfig(generatedConfig, project, config.platform.schema, relative4(temporaryRoot, output));
5724
+ await execute(generatedConfig, temporaryRoot);
5725
+ const created = (await listSqlFiles(output)).filter((path2) => !before.has(path2));
5726
+ if (created.length !== 1) {
5727
+ throw new Error("平台 lineage handoff 必须恰好生成一个 migration");
5728
+ }
5729
+ const sqlPath = join18(output, created[0]);
5730
+ const allowed = config.modules.flatMap(({ legacyPlatformTables }) => legacyPlatformTables);
5731
+ validatePlatformHandoffSql(await readFile18(sqlPath, "utf8"), allowed);
5732
+ await writeFile13(sqlPath, metadataOnlySql("platform"));
5733
+ return output;
5734
+ }
5735
+ async function installStagedLineages(project, temporaryRoot, config, stagedModules, stagedPlatform) {
5736
+ const platform = resolveProjectPath(project, config.platform.out);
5737
+ const backup = join18(temporaryRoot, "platform-backup");
5738
+ const installed = [];
5739
+ await rename(platform, backup);
5740
+ try {
5741
+ await rename(stagedPlatform, platform);
5742
+ for (const [index, module3] of config.modules.entries()) {
5743
+ const output = resolveProjectPath(project, module3.out);
5744
+ if (await exists2(output)) {
5745
+ throw new Error(`${module3.key} lineage 输出已存在,禁止覆盖`);
5746
+ }
5747
+ await mkdir8(dirname5(output), { recursive: true });
5748
+ await rename(stagedModules[index], output);
5749
+ installed.push(output);
5750
+ }
5751
+ } catch (error) {
5752
+ await rm9(platform, { recursive: true, force: true });
5753
+ await rename(backup, platform);
5754
+ for (const output of installed)
5755
+ await rm9(output, { recursive: true, force: true });
5756
+ throw error;
5757
+ }
5758
+ }
5759
+ async function writeDrizzleConfig(path2, project, schemas, out) {
5760
+ const absoluteSchemas = schemas.map((schema) => resolveProjectPath(project, schema));
5761
+ await writeFile13(path2, `import { defineConfig } from "drizzle-kit";
5762
+ export default defineConfig(${JSON.stringify({
5763
+ dialect: "postgresql",
5764
+ schema: absoluteSchemas,
5765
+ out,
5766
+ casing: "snake_case"
5767
+ })});
5768
+ `);
5769
+ }
5770
+ async function validateSchemaSources(project, config) {
5771
+ for (const schema of [
5772
+ ...config.platform.schema,
5773
+ ...config.modules.flatMap(({ schema: schema2 }) => schema2)
5774
+ ]) {
5775
+ await access4(resolveProjectPath(project, schema));
5776
+ }
5777
+ for (const module3 of config.modules) {
5778
+ if (await exists2(resolveProjectPath(project, module3.out))) {
5779
+ throw new Error(`${module3.key} lineage 输出已存在,禁止覆盖`);
5780
+ }
5781
+ }
5782
+ }
5783
+ async function createTemporaryRoot(project) {
5784
+ const windy = join18(project, ".windy");
5785
+ await mkdir8(windy, { recursive: true });
5786
+ return await mkdtemp3(join18(windy, "module-schema-handoff-"));
5787
+ }
5788
+ async function listSqlFiles(root) {
5789
+ return (await readdir7(root)).filter((path2) => path2.endsWith(".sql")).sort();
5790
+ }
5791
+ async function executeDrizzleGenerate(configPath, cwd) {
5792
+ await assertProcess("bunx", ["--bun", "drizzle-kit", "generate", "--config", configPath], cwd, true);
5793
+ }
5794
+ async function assertCleanGit(project) {
5795
+ const result2 = await captureProcess("git", ["status", "--porcelain"], project);
5796
+ if (result2.code !== 0 || result2.stdout.trim()) {
5797
+ throw new Error("schema lineage handoff 前必须保持 Git 工作区干净");
5798
+ }
5799
+ }
5800
+ async function exists2(path2) {
5801
+ try {
5802
+ await access4(path2);
5803
+ return true;
5804
+ } catch {
5805
+ return false;
5806
+ }
5807
+ }
5808
+
5809
+ // src/module-schema/cli.ts
5810
+ async function runModuleSchemaCommand(args) {
5811
+ if (args[0] !== "schema")
5812
+ return false;
5813
+ if (args[1] !== "handoff") {
5814
+ throw new Error("当前 schema 命令只支持 handoff");
5815
+ }
5816
+ const projectDirectory = resolve7(readValue3(args, "--cwd") || process.cwd());
5817
+ const result2 = await handoffModuleSchemaLineages({
5818
+ projectDirectory,
5819
+ configPath: readValue3(args, "--config")
5820
+ });
5821
+ console.log(`Module schema lineage 交接完成:${result2.moduleCount} 个模块、${result2.tableCount} 张旧平台 snapshot 表`);
5822
+ return true;
5823
+ }
5824
+ function readValue3(args, flag) {
5825
+ const index = args.indexOf(flag);
5826
+ return index >= 0 ? args[index + 1] : undefined;
5827
+ }
5828
+
5469
5829
  // src/cli.ts
5470
5830
  async function main() {
5471
5831
  const args = process.argv.slice(2);
5832
+ if (await runModuleSchemaCommand(args))
5833
+ return;
5472
5834
  if (await runLifecycleCommand(args))
5473
5835
  return;
5474
5836
  const options = parseArguments(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "1cdccb9ee1b4e496461282ed7bdab3689fc7009f",
4
- "generatorVersion": "0.2.28",
3
+ "templateCommit": "c61be0fc258ae679c0c0b43d7a838688985d7f0f",
4
+ "generatorVersion": "0.2.30",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -107,6 +107,9 @@ create-windy 在本机 `.env` 中随机生成的 `WINDY_BOOTSTRAP_ADMIN_PASSWORD
107
107
  | `system.data-governance` | 未包含 | H2 | 敏感字段、明文访问和治理导出。 |
108
108
  | `work-order` | 未包含 | H2 | 可替换的业务模块接入示例。 |
109
109
 
110
+ 项目自有业务模块以 `apps/server/src/installed-business-modules.ts` 装配的 Module Manifest
111
+ 为准;shared-scaffold README 不复制项目业务 Key,避免平台升级固化产品命名。
112
+
110
113
  精确生成来源和选择记录在 `.windy/generation.json`。当前版本已提供可回滚的
111
114
  `windy update`;通用 `module add/remove` 尚未交付,不要把 Feature Flag 关闭当成
112
115
  模块已经卸载。
@@ -1,5 +1,4 @@
1
- import { workOrders } from "@southwind-ai/database";
2
- import type { WorkOrder } from "@southwind-ai/example-work-order";
1
+ import { workOrders, type WorkOrder } from "@southwind-ai/example-work-order";
3
2
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
4
3
  import {
5
4
  DrizzleScopedEntityRepository,
@@ -1,7 +1,9 @@
1
1
  import type { WebAccessSnapshot } from "@/layout/navigation";
2
+ import type { RouteRecordRaw } from "vue-router";
2
3
  // @windy-module system.license begin
3
4
  import type { PlatformLicenseSnapshot } from "@/services/license-api";
4
5
  // @windy-module system.license end
6
+ import { routes } from "./index";
5
7
 
6
8
  export function provider(snapshot: WebAccessSnapshot) {
7
9
  return { refresh: async () => snapshot };
@@ -41,6 +43,7 @@ export function authProvider(
41
43
  }
42
44
 
43
45
  export function accessSnapshot(): WebAccessSnapshot {
46
+ const routeAccess = collectRouteAccess(routes);
44
47
  return {
45
48
  actor: {
46
49
  id: "user_1",
@@ -51,6 +54,7 @@ export function accessSnapshot(): WebAccessSnapshot {
51
54
  "system.admin.access",
52
55
  "system.audit.read",
53
56
  "system.feature.read",
57
+ ...routeAccess.permissionKeys,
54
58
  ],
55
59
  },
56
60
  menus: [],
@@ -70,6 +74,34 @@ export function accessSnapshot(): WebAccessSnapshot {
70
74
  allowed: true,
71
75
  reason: "allowed",
72
76
  },
77
+ ...routeAccess.featureKeys.map((key) => ({
78
+ key,
79
+ enabled: true,
80
+ visible: "visible" as const,
81
+ allowed: true,
82
+ reason: "allowed" as const,
83
+ })),
73
84
  ],
74
85
  };
75
86
  }
87
+
88
+ export function collectRouteAccess(routeRecords: RouteRecordRaw[]) {
89
+ const permissionKeys = new Set<string>();
90
+ const featureKeys = new Set<string>();
91
+ const visit = (records: RouteRecordRaw[]) => {
92
+ for (const route of records) {
93
+ if (typeof route.meta?.permissionKey === "string") {
94
+ permissionKeys.add(route.meta.permissionKey);
95
+ }
96
+ if (typeof route.meta?.featureKey === "string") {
97
+ featureKeys.add(route.meta.featureKey);
98
+ }
99
+ if (route.children) visit(route.children);
100
+ }
101
+ };
102
+ visit(routeRecords);
103
+ return {
104
+ permissionKeys: [...permissionKeys],
105
+ featureKeys: [...featureKeys],
106
+ };
107
+ }
@@ -1,14 +1,46 @@
1
1
  import { describe, expect, test } from "vitest";
2
+ import type { RouteRecordRaw } from "vue-router";
2
3
  import routerSource from "./index.ts?raw";
3
4
  import fixtureSource from "./router-test-fixtures.ts?raw";
5
+ import { collectRouteAccess } from "./router-test-fixtures";
4
6
 
5
7
  describe("Router shared-scaffold 中性契约", () => {
6
- test("平台登录页与测试快照不固化下游产品命名", () => {
8
+ test("平台登录页使用中性共享页面", () => {
7
9
  expect(routerSource).toContain(
8
10
  'import LoginPage from "@/pages/auth/LoginPage.vue";',
9
11
  );
10
- expect(`${routerSource}\n${fixtureSource}`).not.toMatch(
11
- /LijianLoginPage|lijian\./,
12
- );
12
+ expect(fixtureSource).toContain("collectRouteAccess");
13
+ });
14
+
15
+ test("测试快照只从项目注入路由递归派生访问键", () => {
16
+ expect(collectRouteAccess([])).toEqual({
17
+ permissionKeys: [],
18
+ featureKeys: [],
19
+ });
20
+ const projectRoutes: RouteRecordRaw[] = [
21
+ {
22
+ path: "/records",
23
+ component: {},
24
+ meta: {
25
+ permissionKey: "records.read",
26
+ featureKey: "records",
27
+ },
28
+ children: [
29
+ {
30
+ path: "import",
31
+ component: {},
32
+ meta: {
33
+ permissionKey: "records.import",
34
+ featureKey: "records.import",
35
+ },
36
+ },
37
+ ],
38
+ },
39
+ ];
40
+
41
+ expect(collectRouteAccess(projectRoutes)).toEqual({
42
+ permissionKeys: ["records.read", "records.import"],
43
+ featureKeys: ["records", "records.import"],
44
+ });
13
45
  });
14
46
  });