@databricks-solutions/lakebase-scm-utils 0.2.51 → 0.3.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.
Files changed (77) hide show
  1. package/dist/scripts/index.cjs +2632 -2120
  2. package/dist/scripts/index.d.cts +1 -1
  3. package/dist/scripts/index.d.ts +1 -1
  4. package/dist/scripts/index.js +676 -174
  5. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
  7. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
  8. package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
  9. package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
  10. package/dist/scripts/lakebase/detect-language.cli.js +489 -67
  11. package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
  12. package/dist/scripts/lakebase/doctor.cli.js +532 -91
  13. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  14. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  15. package/dist/scripts/lakebase/index.cjs +2629 -2117
  16. package/dist/scripts/lakebase/index.d.cts +59 -10
  17. package/dist/scripts/lakebase/index.d.ts +59 -10
  18. package/dist/scripts/lakebase/index.js +670 -168
  19. package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
  20. package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
  21. package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
  22. package/dist/scripts/lakebase/new-migration.cli.js +477 -58
  23. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  24. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  25. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2461 -2029
  26. package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
  27. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
  29. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
  30. package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
  31. package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
  32. package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
  33. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
  35. package/dist/scripts/util/index.cjs +2 -2
  36. package/dist/scripts/util/index.js +2 -2
  37. package/package.json +1 -1
  38. package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
  39. package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
  40. package/scripts/lakebase/collapse-heads.cli.ts +1 -1
  41. package/scripts/lakebase/create-project.ts +6 -2
  42. package/scripts/lakebase/detect-language.cli.ts +2 -2
  43. package/scripts/lakebase/doctor.ts +29 -0
  44. package/scripts/lakebase/dotnet-project.ts +112 -0
  45. package/scripts/lakebase/index.ts +1 -0
  46. package/scripts/lakebase/migration-layout.ts +43 -2
  47. package/scripts/lakebase/new-migration.cli.ts +1 -1
  48. package/scripts/lakebase/scaffold-language.ts +23 -2
  49. package/scripts/lakebase/scaffold.ts +4 -1
  50. package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
  51. package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
  52. package/scripts/lakebase/schema-migrate.cli.ts +3 -1
  53. package/scripts/lakebase/schema-migrate.ts +24 -10
  54. package/templates/project/common/.github/workflows/merge.yml +13 -1
  55. package/templates/project/common/.github/workflows/pr.yml +25 -5
  56. package/templates/project/common/scripts/flyway-migrate.sh +54 -1
  57. package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
  58. package/templates/project/common/scripts/run-dev.sh +10 -1
  59. package/templates/project/common/scripts/run-tests.sh +6 -1
  60. package/templates/project/dotnet/.gitignore.extra +8 -0
  61. package/templates/project/dotnet/App.sln +56 -0
  62. package/templates/project/dotnet/Makefile +13 -0
  63. package/templates/project/dotnet/global.json +6 -0
  64. package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
  65. package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
  66. package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
  67. package/templates/project/dotnet/src/App/Program.cs +34 -0
  68. package/templates/project/dotnet/src/App/appsettings.json +9 -0
  69. package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
  70. package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
  71. package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
  72. package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
  73. package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
  74. package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
  75. package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
  76. package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
  77. package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
@@ -1667,8 +1667,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1667
1667
  var cached;
1668
1668
  function substrateSelfVersion() {
1669
1669
  if (cached !== void 0) return cached;
1670
- if ("0.2.51".length > 0) {
1671
- cached = "0.2.51";
1670
+ if ("0.3.0".length > 0) {
1671
+ cached = "0.3.0";
1672
1672
  return cached;
1673
1673
  }
1674
1674
  cached = "unknown";
@@ -1630,8 +1630,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1630
1630
  var cached;
1631
1631
  function substrateSelfVersion() {
1632
1632
  if (cached !== void 0) return cached;
1633
- if ("0.2.51".length > 0) {
1634
- cached = "0.2.51";
1633
+ if ("0.3.0".length > 0) {
1634
+ cached = "0.3.0";
1635
1635
  return cached;
1636
1636
  }
1637
1637
  cached = "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks-solutions/lakebase-scm-utils",
3
- "version": "0.2.51",
3
+ "version": "0.3.0",
4
4
  "description": "Lakebase SCM + substrate engine: database branching, paired-branch SCM workflows, connection/credential minting, schema migration, project scaffold + deploy primitives, and the git/github utility layer. Extracted from consort for portable, standalone consumption by lakebase-scm-extension and the SFTDD orchestration.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -0,0 +1,158 @@
1
+ // EfCoreAdapter: SchemaMigrationAdapter implementation for .NET projects
2
+ // using EF Core migrations.
3
+ //
4
+ // Wraps scripts/lakebase/schema-migrate-runners/efcore.ts in the cross-tool
5
+ // adapter contract from ADR-0005. Supports rollback (EF generates Down()) and
6
+ // stamp (writes "__EFMigrationsHistory" rows without running migrations).
7
+ // collapseHeads is omitted: EF history is linear (flat-list, like Flyway).
8
+
9
+ import * as path from "node:path";
10
+
11
+ import { getConnection } from "../get-connection.js";
12
+ import { findEfCoreProject, isDotnetProject } from "../dotnet-project.js";
13
+ import {
14
+ applyEfcore,
15
+ createEfcoreMigration,
16
+ efDescription,
17
+ efVersion,
18
+ localEfMigrationIds,
19
+ rollbackEfcore,
20
+ stampEfcore,
21
+ statusEfcore,
22
+ } from "../schema-migrate-runners/efcore.js";
23
+ import type { AppliedSchemaMigration, PendingSchemaMigration, SchemaMigrationFile } from "../schema-migrate.js";
24
+ import {
25
+ registerSchemaMigrationAdapter,
26
+ type ApplyArgs,
27
+ type ApplyResult,
28
+ type ListArgs,
29
+ type ListResult,
30
+ type NewMigrationArgs,
31
+ type NewMigrationResult,
32
+ type RollbackArgs,
33
+ type RollbackResult,
34
+ type SchemaMigrationAdapter,
35
+ type StampArgs,
36
+ type StampResult,
37
+ type StatusArgs,
38
+ type StatusResult,
39
+ } from "../schema-migration-adapter.js";
40
+
41
+ async function buildDsn(args: {
42
+ instance: string;
43
+ branch: string;
44
+ database?: string;
45
+ endpointName?: string;
46
+ }): Promise<string> {
47
+ const result = await getConnection({
48
+ output: "dsn",
49
+ instance: args.instance,
50
+ branch: args.branch,
51
+ database: args.database,
52
+ endpointName: args.endpointName,
53
+ });
54
+ return result.url;
55
+ }
56
+
57
+ function errMsg(err: unknown): string {
58
+ return err instanceof Error ? err.message : String(err);
59
+ }
60
+
61
+ /** Migration files in apply order: Migrations/<timestamp>_<Name>.cs (not .Designer.cs / the snapshot). */
62
+ export function listEfCoreFiles(projectDir: string): SchemaMigrationFile[] {
63
+ return localEfMigrationIds(projectDir).map((id) => ({
64
+ version: efVersion(id),
65
+ filename: `${id}.cs`,
66
+ description: efDescription(id),
67
+ type: "CSharp" as const,
68
+ tool: "efcore" as const,
69
+ }));
70
+ }
71
+
72
+ export const EfCoreAdapter: SchemaMigrationAdapter = {
73
+ id: "efcore",
74
+ languages: ["dotnet"],
75
+
76
+ /** A .NET project (root marker) with a *.csproj that references Microsoft.EntityFrameworkCore.Design. */
77
+ detect(projectDir: string): boolean {
78
+ return isDotnetProject(projectDir) && findEfCoreProject(projectDir) !== null;
79
+ },
80
+
81
+ async apply(args: ApplyArgs): Promise<ApplyResult> {
82
+ try {
83
+ const dsn = await buildDsn(args);
84
+ const r = await applyEfcore({ projectDir: args.projectDir, dsn });
85
+ return {
86
+ applied_migrations: r.applied,
87
+ status: r.alreadyAtLatest ? "noop" : "ok",
88
+ tool_specific: { alreadyAtLatest: r.alreadyAtLatest, tool: r.tool },
89
+ };
90
+ } catch (err) {
91
+ return { applied_migrations: [], status: "error", error: errMsg(err) };
92
+ }
93
+ },
94
+
95
+ async rollback(args: RollbackArgs): Promise<RollbackResult> {
96
+ try {
97
+ const dsn = await buildDsn(args);
98
+ const r = await rollbackEfcore({ projectDir: args.projectDir, dsn, target: args.target });
99
+ return {
100
+ rolled_back: r.rolledBack as AppliedSchemaMigration[],
101
+ status: r.rolledBack.length === 0 ? "noop" : "ok",
102
+ tool_specific: { tool: r.tool },
103
+ };
104
+ } catch (err) {
105
+ return { rolled_back: [], status: "error", error: errMsg(err) };
106
+ }
107
+ },
108
+
109
+ async stamp(args: StampArgs): Promise<StampResult> {
110
+ try {
111
+ const dsn = await buildDsn(args);
112
+ const stamped = await stampEfcore({ projectDir: args.projectDir, dsn, revision: args.revision });
113
+ return { status: "ok", stamped_revision: stamped, tool_specific: { tool: "efcore" } };
114
+ } catch (err) {
115
+ return { status: "error", stamped_revision: null, error: errMsg(err) };
116
+ }
117
+ },
118
+
119
+ async status(args: StatusArgs): Promise<StatusResult> {
120
+ try {
121
+ const dsn = await buildDsn(args);
122
+ const r = await statusEfcore({ projectDir: args.projectDir, dsn });
123
+ return {
124
+ applied_version: r.current ?? null,
125
+ pending: r.pending as PendingSchemaMigration[],
126
+ applied: [],
127
+ status: "ok",
128
+ tool_specific: { tool: r.tool },
129
+ };
130
+ } catch (err) {
131
+ return { applied_version: null, pending: [], applied: [], status: "error", error: errMsg(err) };
132
+ }
133
+ },
134
+
135
+ async list(args: ListArgs): Promise<ListResult> {
136
+ return { files: listEfCoreFiles(args.projectDir) };
137
+ },
138
+
139
+ // baseline: use stamp (EF has no separate baseline concept).
140
+
141
+ async newMigration(args: NewMigrationArgs): Promise<NewMigrationResult> {
142
+ // `dotnet ef migrations add` always generates from the model diff, so
143
+ // autogenerate is implicit and needs no database connection.
144
+ try {
145
+ const created = await createEfcoreMigration({ projectDir: args.projectDir, slug: args.slug });
146
+ return {
147
+ status: "ok",
148
+ version: efVersion(created.id),
149
+ filename: path.basename(created.path),
150
+ path: created.path,
151
+ };
152
+ } catch (err) {
153
+ return { status: "error", version: "", filename: "", path: "", error: errMsg(err) };
154
+ }
155
+ },
156
+ };
157
+
158
+ registerSchemaMigrationAdapter(EfCoreAdapter);
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
16
  import { getConnection } from "../get-connection.js";
17
+ import { flywayMigrationsDir, isDotnetFlywayProject } from "../dotnet-project.js";
17
18
  import {
18
19
  applyFlyway,
19
20
  statusFlyway,
@@ -55,8 +56,9 @@ async function buildDsn(args: {
55
56
  }
56
57
 
57
58
  function listFlywayFiles(projectDir: string): SchemaMigrationFile[] {
58
- // Flyway convention: src/main/resources/db/migration/V<n>__<desc>.sql
59
- const dir = path.join(projectDir, "src", "main", "resources", "db", "migration");
59
+ // Flyway convention: V<n>__<desc>.sql under the Maven layout (Java/Kotlin)
60
+ // or db/migrations/ (.NET); see flywayMigrationsDir.
61
+ const dir = flywayMigrationsDir(projectDir);
60
62
  if (!fs.existsSync(dir)) return [];
61
63
  const files = fs.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
62
64
  return files
@@ -83,10 +85,11 @@ function versionCompare(a: string, b: string): number {
83
85
 
84
86
  export const FlywayAdapter: SchemaMigrationAdapter = {
85
87
  id: "flyway",
86
- languages: ["java", "kotlin"],
88
+ languages: ["java", "kotlin", "dotnet"],
87
89
 
88
90
  detect(projectDir: string): boolean {
89
- return fs.existsSync(path.join(projectDir, "pom.xml"));
91
+ // Maven project, or a .NET project that keeps Flyway SQL in db/migrations/.
92
+ return fs.existsSync(path.join(projectDir, "pom.xml")) || isDotnetFlywayProject(projectDir);
90
93
  },
91
94
 
92
95
  async apply(args: ApplyArgs): Promise<ApplyResult> {
@@ -155,7 +158,7 @@ export const FlywayAdapter: SchemaMigrationAdapter = {
155
158
  // so a flat Flyway list never collides across sibling branches. Flyway has
156
159
  // no DAG, so no merge-time head collapse is needed. autogenerate is ignored.
157
160
  try {
158
- const dir = path.join(args.projectDir, "src", "main", "resources", "db", "migration");
161
+ const dir = flywayMigrationsDir(args.projectDir);
159
162
  fs.mkdirSync(dir, { recursive: true });
160
163
  const version = migrationTimestamp();
161
164
  const slug = migrationSlug(args.slug);
@@ -67,7 +67,7 @@ Usage:
67
67
 
68
68
  Flags:
69
69
  --project-dir <dir> Project root (default: cwd)
70
- --language <lang> java | kotlin | python | nodejs (default: auto-detect)
70
+ --language <lang> java | kotlin | python | nodejs | dotnet (default: auto-detect)
71
71
  --message, -m <txt> Message for the merge revision (Alembic; default: "merge heads")
72
72
  --dry-run Report the heads without creating a merge revision
73
73
  --pretty Pretty-print JSON output
@@ -54,7 +54,7 @@ export interface ConsortSetupHooks {
54
54
  agentModels?: Record<string, string>;
55
55
  uiTrack?: boolean;
56
56
  clientFramework?: string;
57
- language?: "java" | "kotlin" | "python" | "nodejs";
57
+ language?: "java" | "kotlin" | "python" | "nodejs" | "dotnet";
58
58
  },
59
59
  ): void;
60
60
  }
@@ -77,7 +77,9 @@ export interface CreateProjectArgs {
77
77
  /** Whether to make the GitHub repo private (default: true). */
78
78
  privateRepo?: boolean;
79
79
  /** Project language stack (default: 'java'). */
80
- language?: "java" | "kotlin" | "python" | "nodejs";
80
+ language?: "java" | "kotlin" | "python" | "nodejs" | "dotnet";
81
+ /** .NET only: migration tool, "efcore" (default) or "flyway". */
82
+ migrationTool?: "efcore" | "flyway";
81
83
  /** CI runner type (default: 'self-hosted'). */
82
84
  runnerType?: "self-hosted" | "github-hosted";
83
85
  /**
@@ -365,6 +367,7 @@ export async function createProject(
365
367
  databricksHost: host,
366
368
  lakebaseProjectId,
367
369
  language,
370
+ migrationTool: input.migrationTool,
368
371
  runnerType,
369
372
  skipCommands,
370
373
  clientFramework,
@@ -581,6 +584,7 @@ export async function createProject(
581
584
  kotlin: "Kotlin/Spring Boot",
582
585
  python: "Python/FastAPI",
583
586
  nodejs: "Node.js/Express",
587
+ dotnet: `.NET/ASP.NET Core (${input.migrationTool === "flyway" ? "Flyway" : "EF Core"})`,
584
588
  };
585
589
  const langLabel = langLabels[language] ?? language;
586
590
  report("Creating initial commit...");
@@ -3,7 +3,7 @@
3
3
  // pr.yml + merge.yml to populate `steps.detect-lang.outputs.lang` without
4
4
  // duplicating the detection bash inline.
5
5
  //
6
- // Output: prints "java" | "python" | "nodejs" to stdout. Exits non-zero
6
+ // Output: prints "java" | "kotlin" | "python" | "nodejs" | "dotnet" to stdout. Exits non-zero
7
7
  // if no recognised marker file is present.
8
8
  //
9
9
  // Optional flag: --project-dir <path> (defaults to cwd).
@@ -38,7 +38,7 @@ function printHelpAndExit(): never {
38
38
  `lakebase-detect-language – print the project's language\n\n` +
39
39
  `Usage:\n` +
40
40
  ` lakebase-detect-language [--project-dir <path>]\n\n` +
41
- `Output (stdout): one of "java", "python", "nodejs"\n` +
41
+ `Output (stdout): one of "java", "kotlin", "python", "nodejs", "dotnet"\n` +
42
42
  `Exits 1 with an error message on stderr if no marker found.\n`,
43
43
  );
44
44
  process.exit(0);
@@ -285,6 +285,33 @@ async function checkPrereq(
285
285
  };
286
286
  }
287
287
 
288
+ /** .NET SDK floor for `dotnet` projects (the scaffold targets net10.0). */
289
+ const DOTNET_PREREQ: PrereqSpec = {
290
+ name: "dotnet-sdk",
291
+ cmd: "dotnet",
292
+ versionArgs: ["--version"],
293
+ minMajor: 10,
294
+ label: ".NET SDK",
295
+ hint: "Install the .NET 10 SDK (e.g. `brew install --cask dotnet-sdk` or https://dot.net). EF Core's dotnet-ef is restored per project from .config/dotnet-tools.json.",
296
+ };
297
+
298
+ /**
299
+ * Language-gated prerequisites: checked only when the project is that
300
+ * language, so a Java or Python user never sees a missing-.NET failure.
301
+ */
302
+ export async function checkLanguagePrerequisites(
303
+ projectDir: string,
304
+ run: VersionRunner = defaultVersionRunner
305
+ ): Promise<CheckResult[]> {
306
+ let lang: string;
307
+ try {
308
+ lang = detectLanguage(projectDir);
309
+ } catch {
310
+ return [];
311
+ }
312
+ return lang === "dotnet" ? [await checkPrereq(DOTNET_PREREQ, run)] : [];
313
+ }
314
+
288
315
  /** All cold-start prerequisite checks, in README order. */
289
316
  export async function checkPrerequisites(
290
317
  run: VersionRunner = defaultVersionRunner
@@ -712,6 +739,7 @@ export async function runDoctor(args: DoctorArgs = {}): Promise<DoctorReport> {
712
739
  // Cold-start prerequisites (Node/Python/JDK/gh/npm): always run, they are
713
740
  // local-environment checks with no auth dependency.
714
741
  const prereqs = await checkPrerequisites();
742
+ const languagePrereqs = await checkLanguagePrerequisites(projectDir);
715
743
 
716
744
  // Lakebase-enabled probe: needs a reachable workspace, so gate on auth like
717
745
  // the identity + lakebase-project checks.
@@ -727,6 +755,7 @@ export async function runDoctor(args: DoctorArgs = {}): Promise<DoctorReport> {
727
755
  const checks: CheckResult[] = [
728
756
  cli,
729
757
  ...prereqs,
758
+ ...languagePrereqs,
730
759
  auth,
731
760
  identity,
732
761
  lakebaseEnabled,
@@ -0,0 +1,112 @@
1
+ // .NET project discovery shared by language detection, the Flyway adapter
2
+ // (dotnet layout), and the EF Core adapter. Pure filesystem scans; no
3
+ // `dotnet` invocation, so detection stays fast and runtime-light.
4
+ //
5
+ // Layout the kit scaffolds (templates/project/dotnet*):
6
+ // App.sln
7
+ // src/App/App.csproj <- application project
8
+ // src/App/Migrations/ <- EF Core migrations (efcore variant)
9
+ // db/migrations/V1__*.sql <- Flyway migrations (flyway variant)
10
+ // tests/App.Tests/App.Tests.csproj
11
+ //
12
+ // A .NET project is identified by a *.sln / *.slnx / *.csproj at the ROOT
13
+ // (the same rule language detection uses, so the kit never half-recognizes a
14
+ // repo). Once identified, the EF Core project may live up to two levels down
15
+ // (src/<Name>/, <Name>/).
16
+
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+
20
+ /** Package whose reference marks a project as the EF Core migrations owner. */
21
+ export const EF_DESIGN_PACKAGE = "Microsoft.EntityFrameworkCore.Design";
22
+
23
+ /** Flyway migrations directory for .NET projects (relative to the project root). */
24
+ export const DOTNET_FLYWAY_DIR = path.join("db", "migrations");
25
+
26
+ const SKIP_DIRS = new Set(["bin", "obj", "node_modules", ".git", "client", "tests", "test"]);
27
+
28
+ function safeReaddir(dir: string): fs.Dirent[] {
29
+ try {
30
+ return fs.readdirSync(dir, { withFileTypes: true });
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ /** True when `dir` itself carries a .NET marker (*.sln, *.slnx, *.csproj). */
37
+ export function hasDotnetMarker(dir: string): boolean {
38
+ return safeReaddir(dir).some(
39
+ (e) => e.isFile() && /\.(sln|slnx|csproj)$/i.test(e.name),
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Find *.csproj files at the root and up to two directory levels below it,
45
+ * skipping build output and test folders. Sorted for deterministic results.
46
+ */
47
+ export function findCsprojFiles(projectDir: string): string[] {
48
+ const found: string[] = [];
49
+ const walk = (dir: string, depth: number) => {
50
+ for (const e of safeReaddir(dir)) {
51
+ const full = path.join(dir, e.name);
52
+ if (e.isFile() && e.name.toLowerCase().endsWith(".csproj")) {
53
+ found.push(full);
54
+ } else if (e.isDirectory() && depth < 2 && !SKIP_DIRS.has(e.name.toLowerCase()) && !e.name.startsWith(".")) {
55
+ walk(full, depth + 1);
56
+ }
57
+ }
58
+ };
59
+ walk(projectDir, 0);
60
+ return found.sort();
61
+ }
62
+
63
+ export interface EfCoreProject {
64
+ /** Absolute path to the .csproj that references EF Core Design. */
65
+ csproj: string;
66
+ /** Directory containing the .csproj. */
67
+ projectDir: string;
68
+ /** Absolute path to the project's Migrations/ directory (may not exist yet). */
69
+ migrationsDir: string;
70
+ }
71
+
72
+ /** Locate the project that owns EF Core migrations, or null. */
73
+ export function findEfCoreProject(projectDir: string): EfCoreProject | null {
74
+ for (const csproj of findCsprojFiles(projectDir)) {
75
+ let content = "";
76
+ try {
77
+ content = fs.readFileSync(csproj, "utf-8");
78
+ } catch {
79
+ continue;
80
+ }
81
+ if (content.includes(EF_DESIGN_PACKAGE)) {
82
+ const dir = path.dirname(csproj);
83
+ return { csproj, projectDir: dir, migrationsDir: path.join(dir, "Migrations") };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /** True for a .NET repo: a *.sln / *.slnx / *.csproj at the root. */
90
+ export function isDotnetProject(projectDir: string): boolean {
91
+ return hasDotnetMarker(projectDir);
92
+ }
93
+
94
+ /** True for a .NET repo that uses Flyway SQL migrations under db/migrations/. */
95
+ export function isDotnetFlywayProject(projectDir: string): boolean {
96
+ return isDotnetProject(projectDir) && fs.existsSync(path.join(projectDir, DOTNET_FLYWAY_DIR));
97
+ }
98
+
99
+ /** Maven/Spring Flyway layout (Java/Kotlin). */
100
+ export const MAVEN_FLYWAY_DIR = path.join("src", "main", "resources", "db", "migration");
101
+
102
+ /**
103
+ * Absolute Flyway migrations directory for a project: the Maven layout for
104
+ * Java/Kotlin (and as the historical default), or `db/migrations` for a .NET
105
+ * project that uses Flyway.
106
+ */
107
+ export function flywayMigrationsDir(projectDir: string): string {
108
+ const maven = path.join(projectDir, MAVEN_FLYWAY_DIR);
109
+ if (fs.existsSync(maven) || fs.existsSync(path.join(projectDir, "pom.xml"))) return maven;
110
+ if (isDotnetFlywayProject(projectDir)) return path.join(projectDir, DOTNET_FLYWAY_DIR);
111
+ return maven;
112
+ }
@@ -73,6 +73,7 @@ export * from "./workflow-drift.js";
73
73
  export * from "./spring-initializr.js";
74
74
  export * from "./uc-resources.js";
75
75
  export * from "./migration-layout.js";
76
+ export * from "./dotnet-project.js";
76
77
  export {
77
78
  applySchemaMigrations,
78
79
  rollbackSchemaMigration,
@@ -11,17 +11,19 @@
11
11
 
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
+ import { findEfCoreProject, hasDotnetMarker, isDotnetFlywayProject } from "./dotnet-project.js";
14
15
 
15
16
  /** Languages the kit knows how to detect. `unknown` maps to the Flyway
16
17
  * defaults (the historical fallback) so callers never crash on an
17
18
  * unrecognized layout. */
18
- export type MigrationLanguage = "java" | "kotlin" | "python" | "nodejs" | "unknown";
19
+ export type MigrationLanguage = "java" | "kotlin" | "python" | "nodejs" | "dotnet" | "unknown";
19
20
 
20
21
  const MIGRATION_LANGUAGES: readonly MigrationLanguage[] = [
21
22
  "java",
22
23
  "kotlin",
23
24
  "python",
24
25
  "nodejs",
26
+ "dotnet",
25
27
  "unknown",
26
28
  ];
27
29
 
@@ -41,6 +43,9 @@ export const MIGRATION_DEFAULTS: Record<MigrationLanguage, MigrationDefaults> =
41
43
  kotlin: { path: "src/main/resources/db/migration", pattern: /^V\d+.*\.sql$/i, glob: "*.sql" },
42
44
  python: { path: "alembic/versions", pattern: /^[0-9a-f][\w]*.*\.py$/i, glob: "*.py" },
43
45
  nodejs: { path: "migrations", pattern: /^\d+.*\.(js|ts)$/i, glob: "*.{js,ts}" },
46
+ // .NET + Flyway; EF Core projects get their Migrations/ dir from
47
+ // resolveMigrationLayout (see DOTNET_EFCORE_DEFAULTS).
48
+ dotnet: { path: "db/migrations", pattern: /^V\d+.*\.sql$/i, glob: "*.sql" },
44
49
  unknown: { path: "src/main/resources/db/migration", pattern: /^V\d+.*\.sql$/i, glob: "*.sql" },
45
50
  };
46
51
 
@@ -70,6 +75,11 @@ export function detectLanguageAt(dir: string): MigrationLanguage {
70
75
  ) {
71
76
  return "python";
72
77
  }
78
+ // .NET before package.json: an ASP.NET repo may carry a package.json for
79
+ // front-end tooling, but a *.sln / *.csproj at the root is unambiguous.
80
+ if (hasDotnetMarker(dir)) {
81
+ return "dotnet";
82
+ }
73
83
  if (fs.existsSync(path.join(dir, "package.json"))) {
74
84
  return "nodejs";
75
85
  }
@@ -144,6 +154,37 @@ export function compileMigrationPattern(src?: string): RegExp | undefined {
144
154
  }
145
155
  }
146
156
 
157
+ /** EF Core: Migrations/<timestamp>_<Name>.cs, excluding *.Designer.cs and the model snapshot. */
158
+ export const DOTNET_EFCORE_DEFAULTS: MigrationDefaults = {
159
+ path: "Migrations",
160
+ pattern: /^\d{14}_(?!.*\.Designer\.cs$).+\.cs$/i,
161
+ glob: "*.cs",
162
+ };
163
+
164
+ // IDE callers resolve the layout on every refresh; .NET resolution scans and
165
+ // reads *.csproj files, so cache it briefly per project root.
166
+ const DOTNET_LAYOUT_TTL_MS = 5000;
167
+ const dotnetLayoutCache = new Map<string, { at: number; value: MigrationDefaults }>();
168
+
169
+ /** .NET defaults depend on the migration tool: Flyway (db/migrations) or EF Core. */
170
+ function dotnetDefaults(projectDir?: string): MigrationDefaults {
171
+ if (!projectDir) return MIGRATION_DEFAULTS.dotnet;
172
+ const key = path.resolve(projectDir);
173
+ const hit = dotnetLayoutCache.get(key);
174
+ if (hit && Date.now() - hit.at < DOTNET_LAYOUT_TTL_MS) return hit.value;
175
+
176
+ let value = MIGRATION_DEFAULTS.dotnet;
177
+ if (!isDotnetFlywayProject(projectDir)) {
178
+ const ef = findEfCoreProject(projectDir);
179
+ if (ef) {
180
+ const rel = path.relative(projectDir, ef.migrationsDir).split(path.sep).join("/");
181
+ value = { ...DOTNET_EFCORE_DEFAULTS, path: rel };
182
+ }
183
+ }
184
+ dotnetLayoutCache.set(key, { at: Date.now(), value });
185
+ return value;
186
+ }
187
+
147
188
  export interface ResolveMigrationLayoutArgs {
148
189
  /** Project root. */
149
190
  projectDir?: string;
@@ -175,7 +216,7 @@ export interface MigrationLayout {
175
216
  export function resolveMigrationLayout(args: ResolveMigrationLayoutArgs): MigrationLayout {
176
217
  const configuredMigrationPath = (args.migrationPath ?? "").trim();
177
218
  const language = resolveMigrationLanguage(args.projectDir, configuredMigrationPath, args.language);
178
- const defaults = MIGRATION_DEFAULTS[language];
219
+ const defaults = language === "dotnet" ? dotnetDefaults(args.projectDir) : MIGRATION_DEFAULTS[language];
179
220
  return {
180
221
  language,
181
222
  migrationPath: configuredMigrationPath || defaults.path,
@@ -92,7 +92,7 @@ Usage:
92
92
  Flags:
93
93
  --name, -m <text> Human description; slugified into the filename (required)
94
94
  --project-dir <dir> Project root (default: cwd)
95
- --language <lang> java | kotlin | python | nodejs (default: auto-detect)
95
+ --language <lang> java | kotlin | python | nodejs | dotnet (default: auto-detect)
96
96
  --autogenerate Alembic only: diff models vs the branch DB to populate
97
97
  the body (requires --instance + --branch)
98
98
  --instance <id> Lakebase project id (only with --autogenerate)
@@ -1,6 +1,8 @@
1
1
  // Language-specific scaffold router. Mirrors ScaffoldService.deployLanguageProject:
2
2
  // - java / kotlin → Spring Initializr (with bundled fallback)
3
3
  // - python / nodejs → static template copy with {{PROJECT_NAME}} substitution
4
+ // - dotnet → static template copy (dotnet/) + a migration-tool
5
+ // overlay (dotnet-efcore/ or dotnet-flyway/)
4
6
 
5
7
  import * as fs from "node:fs";
6
8
  import * as path from "node:path";
@@ -13,7 +15,10 @@ import {
13
15
  ScaffoldReportFn,
14
16
  } from "./spring-initializr.js";
15
17
 
16
- export type ProjectLanguage = "java" | "kotlin" | "python" | "nodejs";
18
+ export type ProjectLanguage = "java" | "kotlin" | "python" | "nodejs" | "dotnet";
19
+
20
+ /** Migration tool for a .NET project. EF Core is the default. */
21
+ export type DotnetMigrationTool = "efcore" | "flyway";
17
22
 
18
23
  /**
19
24
  * The frontend the project ships. "react" scaffolds the first-class SPA client
@@ -28,6 +33,8 @@ export interface DeployLanguageProjectArgs {
28
33
  targetDir: string;
29
34
  language: ProjectLanguage;
30
35
  projectName?: string;
36
+ /** .NET only: which migration tool the scaffold is set up for (default "efcore"). */
37
+ migrationTool?: DotnetMigrationTool;
31
38
  /** Override templates dir (tests). */
32
39
  templatesDir?: string;
33
40
  /** Override Initializr client (tests). */
@@ -66,8 +73,22 @@ export async function deployLanguageProject(args: DeployLanguageProjectArgs): Pr
66
73
  return;
67
74
  }
68
75
 
69
- // Python / Node.js – static template copy with placeholder substitution.
76
+ // Python / Node.js / .NET – static template copy with placeholder substitution.
70
77
  const templatesDir = args.templatesDir ?? findTemplatesDir();
78
+ if (args.language === "dotnet") {
79
+ const tool = args.migrationTool ?? "efcore";
80
+ if (tool !== "efcore" && tool !== "flyway") {
81
+ throw new Error(`Unsupported .NET migration tool: ${String(tool)} (expected efcore or flyway)`);
82
+ }
83
+ const base = path.join(templatesDir, "dotnet");
84
+ const overlay = path.join(templatesDir, `dotnet-${tool}`);
85
+ if (!fs.existsSync(base) || !fs.existsSync(overlay)) {
86
+ throw new Error(`No template found for language: dotnet (${tool})`);
87
+ }
88
+ copyDirSubstituted(base, args.targetDir, { projectName: args.projectName });
89
+ copyDirSubstituted(overlay, args.targetDir, { projectName: args.projectName });
90
+ return;
91
+ }
71
92
  const langSrc = path.join(templatesDir, args.language);
72
93
  if (!fs.existsSync(langSrc)) {
73
94
  throw new Error(`No template found for language: ${args.language}`);
@@ -11,7 +11,7 @@ import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { deployLanguageProject } from "./scaffold-language.js";
14
- import type { ProjectLanguage, ClientFramework } from "./scaffold-language.js";
14
+ import type { ProjectLanguage, ClientFramework, DotnetMigrationTool } from "./scaffold-language.js";
15
15
  import { copyDirSubstituted } from "../util/copy-dir-substituted.js";
16
16
  import type { SpringInitializrClient } from "./spring-initializr.js";
17
17
 
@@ -635,6 +635,8 @@ export interface ScaffoldStaticAllArgs extends ScaffoldOptions {
635
635
  databricksHost?: string;
636
636
  lakebaseProjectId?: string;
637
637
  language?: ProjectLanguage;
638
+ /** .NET only: "efcore" (default) or "flyway". Ignored for other languages. */
639
+ migrationTool?: DotnetMigrationTool;
638
640
  runnerType?: RunnerType;
639
641
  report?: ScaffoldReportFn;
640
642
  /**
@@ -766,6 +768,7 @@ export async function scaffoldAll(args: ScaffoldAllArgs): Promise<ScaffoldStatic
766
768
  targetDir: args.targetDir,
767
769
  language,
768
770
  projectName,
771
+ migrationTool: args.migrationTool,
769
772
  templatesDir: args.templatesDir,
770
773
  initializrClient: args.initializrClient,
771
774
  report,