@databricks-solutions/lakebase-scm-utils 0.2.51 → 0.3.1

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 (79) hide show
  1. package/README.md +10 -4
  2. package/dist/scripts/index.cjs +2813 -2244
  3. package/dist/scripts/index.d.cts +1 -1
  4. package/dist/scripts/index.d.ts +1 -1
  5. package/dist/scripts/index.js +857 -298
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
  7. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
  8. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
  9. package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
  10. package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
  11. package/dist/scripts/lakebase/detect-language.cli.js +489 -67
  12. package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
  13. package/dist/scripts/lakebase/doctor.cli.js +532 -91
  14. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  15. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  16. package/dist/scripts/lakebase/index.cjs +2810 -2205
  17. package/dist/scripts/lakebase/index.d.cts +59 -10
  18. package/dist/scripts/lakebase/index.d.ts +59 -10
  19. package/dist/scripts/lakebase/index.js +851 -256
  20. package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
  21. package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
  22. package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
  23. package/dist/scripts/lakebase/new-migration.cli.js +477 -58
  24. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  25. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  26. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2461 -2029
  27. package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
  29. package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
  30. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
  31. package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
  32. package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
  33. package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
  35. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
  36. package/dist/scripts/util/index.cjs +2 -2
  37. package/dist/scripts/util/index.js +2 -2
  38. package/package.json +1 -1
  39. package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
  40. package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
  41. package/scripts/lakebase/client-lockfile.ts +102 -0
  42. package/scripts/lakebase/collapse-heads.cli.ts +1 -1
  43. package/scripts/lakebase/create-project.ts +25 -2
  44. package/scripts/lakebase/detect-language.cli.ts +2 -2
  45. package/scripts/lakebase/doctor.ts +29 -0
  46. package/scripts/lakebase/dotnet-project.ts +112 -0
  47. package/scripts/lakebase/index.ts +1 -0
  48. package/scripts/lakebase/migration-layout.ts +43 -2
  49. package/scripts/lakebase/new-migration.cli.ts +1 -1
  50. package/scripts/lakebase/scaffold-language.ts +23 -2
  51. package/scripts/lakebase/scaffold.ts +4 -1
  52. package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
  53. package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
  54. package/scripts/lakebase/schema-migrate.cli.ts +3 -1
  55. package/scripts/lakebase/schema-migrate.ts +24 -10
  56. package/templates/project/common/.github/workflows/merge.yml +13 -1
  57. package/templates/project/common/.github/workflows/pr.yml +25 -5
  58. package/templates/project/common/scripts/flyway-migrate.sh +54 -1
  59. package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
  60. package/templates/project/common/scripts/run-dev.sh +10 -1
  61. package/templates/project/common/scripts/run-tests.sh +6 -1
  62. package/templates/project/dotnet/.gitignore.extra +8 -0
  63. package/templates/project/dotnet/App.sln +56 -0
  64. package/templates/project/dotnet/Makefile +13 -0
  65. package/templates/project/dotnet/global.json +6 -0
  66. package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
  67. package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
  68. package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
  69. package/templates/project/dotnet/src/App/Program.cs +34 -0
  70. package/templates/project/dotnet/src/App/appsettings.json +9 -0
  71. package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
  72. package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
  73. package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
  74. package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
  75. package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
  76. package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
  77. package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
  78. package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
  79. package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // scripts/lakebase/doctor.cli.ts
4
- import * as path14 from "path";
4
+ import * as path17 from "path";
5
5
 
6
6
  // scripts/lakebase/doctor.ts
7
- import * as fs13 from "fs";
8
- import * as path13 from "path";
7
+ import * as fs15 from "fs";
8
+ import * as path16 from "path";
9
9
  import * as cp2 from "child_process";
10
10
 
11
11
  // scripts/util/exec.ts
@@ -351,9 +351,9 @@ function asBranchUid(s) {
351
351
  }
352
352
  return s;
353
353
  }
354
- function branchNameFromResourcePath(path15) {
355
- if (!path15.includes("/branches/")) return null;
356
- const leaf = path15.split("/branches/").pop();
354
+ function branchNameFromResourcePath(path18) {
355
+ if (!path18.includes("/branches/")) return null;
356
+ const leaf = path18.split("/branches/").pop();
357
357
  if (!leaf) return null;
358
358
  try {
359
359
  return asBranchName(leaf);
@@ -460,8 +460,8 @@ function verifyHooks(projectDir) {
460
460
  }
461
461
 
462
462
  // scripts/lakebase/schema-migrate.ts
463
- import * as fs11 from "fs";
464
- import * as path11 from "path";
463
+ import * as fs13 from "fs";
464
+ import * as path14 from "path";
465
465
 
466
466
  // scripts/lakebase/get-connection.ts
467
467
  import { createLakebasePool } from "@databricks/lakebase";
@@ -484,8 +484,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
484
484
  var cached;
485
485
  function substrateSelfVersion() {
486
486
  if (cached !== void 0) return cached;
487
- if ("0.2.51".length > 0) {
488
- cached = "0.2.51";
487
+ if ("0.3.1".length > 0) {
488
+ cached = "0.3.1";
489
489
  return cached;
490
490
  }
491
491
  cached = "unknown";
@@ -594,23 +594,88 @@ function dbcli2(args) {
594
594
  }
595
595
 
596
596
  // scripts/lakebase/migration-layout.ts
597
+ import * as fs6 from "fs";
598
+ import * as path5 from "path";
599
+
600
+ // scripts/lakebase/dotnet-project.ts
597
601
  import * as fs5 from "fs";
598
602
  import * as path4 from "path";
603
+ var EF_DESIGN_PACKAGE = "Microsoft.EntityFrameworkCore.Design";
604
+ var DOTNET_FLYWAY_DIR = path4.join("db", "migrations");
605
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["bin", "obj", "node_modules", ".git", "client", "tests", "test"]);
606
+ function safeReaddir(dir) {
607
+ try {
608
+ return fs5.readdirSync(dir, { withFileTypes: true });
609
+ } catch {
610
+ return [];
611
+ }
612
+ }
613
+ function hasDotnetMarker(dir) {
614
+ return safeReaddir(dir).some(
615
+ (e) => e.isFile() && /\.(sln|slnx|csproj)$/i.test(e.name)
616
+ );
617
+ }
618
+ function findCsprojFiles(projectDir) {
619
+ const found = [];
620
+ const walk = (dir, depth) => {
621
+ for (const e of safeReaddir(dir)) {
622
+ const full = path4.join(dir, e.name);
623
+ if (e.isFile() && e.name.toLowerCase().endsWith(".csproj")) {
624
+ found.push(full);
625
+ } else if (e.isDirectory() && depth < 2 && !SKIP_DIRS.has(e.name.toLowerCase()) && !e.name.startsWith(".")) {
626
+ walk(full, depth + 1);
627
+ }
628
+ }
629
+ };
630
+ walk(projectDir, 0);
631
+ return found.sort();
632
+ }
633
+ function findEfCoreProject(projectDir) {
634
+ for (const csproj of findCsprojFiles(projectDir)) {
635
+ let content = "";
636
+ try {
637
+ content = fs5.readFileSync(csproj, "utf-8");
638
+ } catch {
639
+ continue;
640
+ }
641
+ if (content.includes(EF_DESIGN_PACKAGE)) {
642
+ const dir = path4.dirname(csproj);
643
+ return { csproj, projectDir: dir, migrationsDir: path4.join(dir, "Migrations") };
644
+ }
645
+ }
646
+ return null;
647
+ }
648
+ function isDotnetProject(projectDir) {
649
+ return hasDotnetMarker(projectDir);
650
+ }
651
+ function isDotnetFlywayProject(projectDir) {
652
+ return isDotnetProject(projectDir) && fs5.existsSync(path4.join(projectDir, DOTNET_FLYWAY_DIR));
653
+ }
654
+ var MAVEN_FLYWAY_DIR = path4.join("src", "main", "resources", "db", "migration");
655
+ function flywayMigrationsDir(projectDir) {
656
+ const maven = path4.join(projectDir, MAVEN_FLYWAY_DIR);
657
+ if (fs5.existsSync(maven) || fs5.existsSync(path4.join(projectDir, "pom.xml"))) return maven;
658
+ if (isDotnetFlywayProject(projectDir)) return path4.join(projectDir, DOTNET_FLYWAY_DIR);
659
+ return maven;
660
+ }
661
+
662
+ // scripts/lakebase/migration-layout.ts
599
663
  var MIGRATION_LANGUAGES = [
600
664
  "java",
601
665
  "kotlin",
602
666
  "python",
603
667
  "nodejs",
668
+ "dotnet",
604
669
  "unknown"
605
670
  ];
606
671
  function detectLanguageAt(dir) {
607
- if (fs5.existsSync(path4.join(dir, "pom.xml"))) {
608
- const kotlinDir = path4.join(dir, "src", "main", "kotlin");
609
- if (fs5.existsSync(kotlinDir)) {
672
+ if (fs6.existsSync(path5.join(dir, "pom.xml"))) {
673
+ const kotlinDir = path5.join(dir, "src", "main", "kotlin");
674
+ if (fs6.existsSync(kotlinDir)) {
610
675
  return "kotlin";
611
676
  }
612
677
  try {
613
- const pom = fs5.readFileSync(path4.join(dir, "pom.xml"), "utf-8");
678
+ const pom = fs6.readFileSync(path5.join(dir, "pom.xml"), "utf-8");
614
679
  if (pom.includes("kotlin-maven-plugin")) {
615
680
  return "kotlin";
616
681
  }
@@ -618,10 +683,13 @@ function detectLanguageAt(dir) {
618
683
  }
619
684
  return "java";
620
685
  }
621
- if (fs5.existsSync(path4.join(dir, "pyproject.toml")) || fs5.existsSync(path4.join(dir, "requirements.txt")) || fs5.existsSync(path4.join(dir, "alembic.ini"))) {
686
+ if (fs6.existsSync(path5.join(dir, "pyproject.toml")) || fs6.existsSync(path5.join(dir, "requirements.txt")) || fs6.existsSync(path5.join(dir, "alembic.ini"))) {
622
687
  return "python";
623
688
  }
624
- if (fs5.existsSync(path4.join(dir, "package.json"))) {
689
+ if (hasDotnetMarker(dir)) {
690
+ return "dotnet";
691
+ }
692
+ if (fs6.existsSync(path5.join(dir, "package.json"))) {
625
693
  return "nodejs";
626
694
  }
627
695
  return "unknown";
@@ -642,14 +710,14 @@ function resolveMigrationLanguage(projectDir, configuredMigrationPath, override)
642
710
  if (!rel) {
643
711
  return "unknown";
644
712
  }
645
- const rootResolved = path4.resolve(projectDir);
646
- let dir = path4.resolve(projectDir, rel);
647
- while (dir === rootResolved || dir.startsWith(rootResolved + path4.sep)) {
713
+ const rootResolved = path5.resolve(projectDir);
714
+ let dir = path5.resolve(projectDir, rel);
715
+ while (dir === rootResolved || dir.startsWith(rootResolved + path5.sep)) {
648
716
  const lang = detectLanguageAt(dir);
649
717
  if (lang !== "unknown") {
650
718
  return lang;
651
719
  }
652
- const parent = path4.dirname(dir);
720
+ const parent = path5.dirname(dir);
653
721
  if (parent === dir) {
654
722
  break;
655
723
  }
@@ -659,21 +727,21 @@ function resolveMigrationLanguage(projectDir, configuredMigrationPath, override)
659
727
  }
660
728
 
661
729
  // scripts/lakebase/adapters/alembic-adapter.ts
662
- import * as fs7 from "fs";
663
- import * as path6 from "path";
730
+ import * as fs8 from "fs";
731
+ import * as path7 from "path";
664
732
 
665
733
  // scripts/lakebase/schema-migrate-runners/alembic.ts
666
734
  import { spawn } from "child_process";
667
- import * as fs6 from "fs";
668
- import * as path5 from "path";
735
+ import * as fs7 from "fs";
736
+ import * as path6 from "path";
669
737
  function resolveAlembicBin(projectDir) {
670
738
  const candidates = [
671
- path5.join(projectDir, ".venv", "bin", "alembic"),
672
- path5.join(projectDir, "venv", "bin", "alembic")
739
+ path6.join(projectDir, ".venv", "bin", "alembic"),
740
+ path6.join(projectDir, "venv", "bin", "alembic")
673
741
  ];
674
742
  for (const candidate of candidates) {
675
743
  try {
676
- if (fs6.existsSync(candidate)) return candidate;
744
+ if (fs7.existsSync(candidate)) return candidate;
677
745
  } catch {
678
746
  }
679
747
  }
@@ -683,7 +751,7 @@ function spawnAlembic(projectDir, args, dsn) {
683
751
  return new Promise((resolve2, reject) => {
684
752
  const bin = resolveAlembicBin(projectDir);
685
753
  const env = { ...process.env };
686
- env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path5.delimiter);
754
+ env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path6.delimiter);
687
755
  if (dsn) env.DATABASE_URL = dsn;
688
756
  const child = spawn(bin, args, {
689
757
  cwd: projectDir,
@@ -731,10 +799,10 @@ async function createAlembicRevision(opts) {
731
799
  const m = stdout.match(/Generating\s+(\S+\.py)/);
732
800
  if (m) return m[1].trim();
733
801
  for (const rel of ["migrations/versions", "alembic/versions"]) {
734
- const dir = path5.join(opts.projectDir, rel);
735
- if (!fs6.existsSync(dir)) continue;
736
- const hit = fs6.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
737
- if (hit) return path5.join(dir, hit);
802
+ const dir = path6.join(opts.projectDir, rel);
803
+ if (!fs7.existsSync(dir)) continue;
804
+ const hit = fs7.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
805
+ if (hit) return path6.join(dir, hit);
738
806
  }
739
807
  throw new SchemaMigrationError(
740
808
  `alembic revision succeeded but the created file could not be located.
@@ -845,15 +913,15 @@ async function buildDsn(args) {
845
913
  }
846
914
  function findVersionsDir(projectDir) {
847
915
  const candidates = [
848
- path6.join(projectDir, "migrations", "versions"),
849
- path6.join(projectDir, "alembic", "versions")
916
+ path7.join(projectDir, "migrations", "versions"),
917
+ path7.join(projectDir, "alembic", "versions")
850
918
  ];
851
- return candidates.find((p) => fs7.existsSync(p));
919
+ return candidates.find((p) => fs8.existsSync(p));
852
920
  }
853
921
  function listAlembicFiles(projectDir) {
854
922
  const dir = findVersionsDir(projectDir);
855
923
  if (!dir) return [];
856
- const files = fs7.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
924
+ const files = fs8.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
857
925
  return files.map((filename) => {
858
926
  const stem = filename.replace(/\.py$/, "");
859
927
  const sep2 = stem.indexOf("_");
@@ -878,9 +946,9 @@ var AlembicAdapter = {
878
946
  * here. Callers can still force-select via project.yaml#migration_tool.
879
947
  */
880
948
  detect(projectDir) {
881
- if (fs7.existsSync(path6.join(projectDir, "alembic.ini"))) return true;
882
- if (fs7.existsSync(path6.join(projectDir, "migrations", "env.py"))) return true;
883
- if (fs7.existsSync(path6.join(projectDir, "alembic", "env.py"))) return true;
949
+ if (fs8.existsSync(path7.join(projectDir, "alembic.ini"))) return true;
950
+ if (fs8.existsSync(path7.join(projectDir, "migrations", "env.py"))) return true;
951
+ if (fs8.existsSync(path7.join(projectDir, "alembic", "env.py"))) return true;
884
952
  return false;
885
953
  },
886
954
  async apply(args) {
@@ -986,7 +1054,7 @@ var AlembicAdapter = {
986
1054
  autogenerate: !!args.autogenerate,
987
1055
  dsn
988
1056
  });
989
- return { status: "ok", version: revId, filename: path6.basename(created), path: created };
1057
+ return { status: "ok", version: revId, filename: path7.basename(created), path: created };
990
1058
  } catch (err) {
991
1059
  return {
992
1060
  status: "error",
@@ -1003,7 +1071,7 @@ var AlembicAdapter = {
1003
1071
  if (heads.length <= 1) return { status: "noop", headsBefore: heads };
1004
1072
  if (args.dryRun) return { status: "ok", headsBefore: heads };
1005
1073
  const created = await mergeAlembicHeads(args.projectDir, args.message ?? "merge heads");
1006
- const mergeRevision = path6.basename(created).replace(/\.py$/, "").split("_")[0];
1074
+ const mergeRevision = path7.basename(created).replace(/\.py$/, "").split("_")[0];
1007
1075
  return { status: "ok", headsBefore: heads, mergeRevision, path: created };
1008
1076
  } catch (err) {
1009
1077
  return {
@@ -1017,12 +1085,12 @@ var AlembicAdapter = {
1017
1085
  registerSchemaMigrationAdapter(AlembicAdapter);
1018
1086
 
1019
1087
  // scripts/lakebase/adapters/flyway-adapter.ts
1020
- import * as fs8 from "fs";
1021
- import * as path8 from "path";
1088
+ import * as fs9 from "fs";
1089
+ import * as path9 from "path";
1022
1090
 
1023
1091
  // scripts/lakebase/schema-migrate-runners/flyway.ts
1024
1092
  import { spawn as spawn2 } from "child_process";
1025
- import * as path7 from "path";
1093
+ import * as path8 from "path";
1026
1094
  function dsnToFlywayEnv(dsn) {
1027
1095
  const u = new URL(dsn);
1028
1096
  const user = decodeURIComponent(u.username);
@@ -1032,7 +1100,7 @@ function dsnToFlywayEnv(dsn) {
1032
1100
  return { url, user, password };
1033
1101
  }
1034
1102
  function migrationsLocation(projectDir) {
1035
- return `filesystem:${path7.join(projectDir, "src", "main", "resources", "db", "migration")}`;
1103
+ return `filesystem:${flywayMigrationsDir(projectDir)}`;
1036
1104
  }
1037
1105
  function runFlyway(ctx, args) {
1038
1106
  const { url, user, password } = dsnToFlywayEnv(ctx.dsn);
@@ -1133,7 +1201,7 @@ async function statusFlyway(ctx) {
1133
1201
  if (state === "SUCCESS" || state === "BASELINE") {
1134
1202
  current = m.version;
1135
1203
  } else if (state === "PENDING") {
1136
- const filename = m.filepath ? path7.basename(m.filepath) : `V${m.version}__migration.sql`;
1204
+ const filename = m.filepath ? path8.basename(m.filepath) : `V${m.version}__migration.sql`;
1137
1205
  pending.push({
1138
1206
  version: m.version,
1139
1207
  filename,
@@ -1156,9 +1224,9 @@ async function buildDsn2(args) {
1156
1224
  return result.url;
1157
1225
  }
1158
1226
  function listFlywayFiles(projectDir) {
1159
- const dir = path8.join(projectDir, "src", "main", "resources", "db", "migration");
1160
- if (!fs8.existsSync(dir)) return [];
1161
- const files = fs8.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
1227
+ const dir = flywayMigrationsDir(projectDir);
1228
+ if (!fs9.existsSync(dir)) return [];
1229
+ const files = fs9.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
1162
1230
  return files.map((filename) => {
1163
1231
  const m = filename.match(/^V(\d+(?:\.\d+)*)__(.+)\.sql$/);
1164
1232
  const version = m[1];
@@ -1179,9 +1247,9 @@ function versionCompare(a, b) {
1179
1247
  }
1180
1248
  var FlywayAdapter = {
1181
1249
  id: "flyway",
1182
- languages: ["java", "kotlin"],
1250
+ languages: ["java", "kotlin", "dotnet"],
1183
1251
  detect(projectDir) {
1184
- return fs8.existsSync(path8.join(projectDir, "pom.xml"));
1252
+ return fs9.existsSync(path9.join(projectDir, "pom.xml")) || isDotnetFlywayProject(projectDir);
1185
1253
  },
1186
1254
  async apply(args) {
1187
1255
  const dsn = await buildDsn2(args);
@@ -1239,14 +1307,14 @@ var FlywayAdapter = {
1239
1307
  // optional-protocol shape makes this additive.
1240
1308
  async newMigration(args) {
1241
1309
  try {
1242
- const dir = path8.join(args.projectDir, "src", "main", "resources", "db", "migration");
1243
- fs8.mkdirSync(dir, { recursive: true });
1310
+ const dir = flywayMigrationsDir(args.projectDir);
1311
+ fs9.mkdirSync(dir, { recursive: true });
1244
1312
  const version = migrationTimestamp();
1245
1313
  const slug = migrationSlug2(args.slug);
1246
1314
  const filename = `V${version}__${slug}.sql`;
1247
- const full = path8.join(dir, filename);
1248
- if (fs8.existsSync(full)) throw new Error(`${filename} already exists`);
1249
- fs8.writeFileSync(
1315
+ const full = path9.join(dir, filename);
1316
+ if (fs9.existsSync(full)) throw new Error(`${filename} already exists`);
1317
+ fs9.writeFileSync(
1250
1318
  full,
1251
1319
  `-- V${version}: ${args.slug}
1252
1320
  -- Flyway migration (write your DDL/DML below).
@@ -1268,18 +1336,18 @@ var FlywayAdapter = {
1268
1336
  registerSchemaMigrationAdapter(FlywayAdapter);
1269
1337
 
1270
1338
  // scripts/lakebase/adapters/knex-adapter.ts
1271
- import * as fs10 from "fs";
1272
- import * as path10 from "path";
1339
+ import * as fs11 from "fs";
1340
+ import * as path11 from "path";
1273
1341
 
1274
1342
  // scripts/lakebase/schema-migrate-runners/knex.ts
1275
1343
  import { spawn as spawn3 } from "child_process";
1276
- import * as fs9 from "fs";
1277
- import * as path9 from "path";
1344
+ import * as fs10 from "fs";
1345
+ import * as path10 from "path";
1278
1346
  var KNEXFILE_VARIANTS = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
1279
1347
  function findKnexfile(projectDir) {
1280
1348
  for (const name of KNEXFILE_VARIANTS) {
1281
- const p = path9.join(projectDir, name);
1282
- if (fs9.existsSync(p)) return p;
1349
+ const p = path10.join(projectDir, name);
1350
+ if (fs10.existsSync(p)) return p;
1283
1351
  }
1284
1352
  return void 0;
1285
1353
  }
@@ -1431,9 +1499,9 @@ async function buildDsn3(args) {
1431
1499
  }
1432
1500
  var KNEXFILE_VARIANTS2 = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
1433
1501
  function listKnexFiles(projectDir) {
1434
- const dir = path10.join(projectDir, "migrations");
1435
- if (!fs10.existsSync(dir)) return [];
1436
- const files = fs10.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
1502
+ const dir = path11.join(projectDir, "migrations");
1503
+ if (!fs11.existsSync(dir)) return [];
1504
+ const files = fs11.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
1437
1505
  return files.map((filename) => {
1438
1506
  const stem = filename.replace(/\.(js|ts)$/, "");
1439
1507
  const m = stem.match(/^(\d{14})_(.+)$/);
@@ -1453,7 +1521,7 @@ var KnexAdapter = {
1453
1521
  * project.yaml#migration_tool.
1454
1522
  */
1455
1523
  detect(projectDir) {
1456
- return KNEXFILE_VARIANTS2.some((name) => fs10.existsSync(path10.join(projectDir, name)));
1524
+ return KNEXFILE_VARIANTS2.some((name) => fs11.existsSync(path11.join(projectDir, name)));
1457
1525
  },
1458
1526
  async apply(args) {
1459
1527
  const dsn = await buildDsn3(args);
@@ -1526,9 +1594,9 @@ var KnexAdapter = {
1526
1594
  async newMigration(args) {
1527
1595
  try {
1528
1596
  const created = await createKnexMigration({ projectDir: args.projectDir, slug: migrationSlug2(args.slug) });
1529
- const stem = path10.basename(created).replace(/\.(js|ts)$/, "");
1597
+ const stem = path11.basename(created).replace(/\.(js|ts)$/, "");
1530
1598
  const version = stem.match(/^(\d{14})_/)?.[1] ?? stem;
1531
- return { status: "ok", version, filename: path10.basename(created), path: created };
1599
+ return { status: "ok", version, filename: path11.basename(created), path: created };
1532
1600
  } catch (err) {
1533
1601
  return {
1534
1602
  status: "error",
@@ -1542,6 +1610,360 @@ var KnexAdapter = {
1542
1610
  };
1543
1611
  registerSchemaMigrationAdapter(KnexAdapter);
1544
1612
 
1613
+ // scripts/lakebase/adapters/efcore-adapter.ts
1614
+ import * as path13 from "path";
1615
+
1616
+ // scripts/lakebase/schema-migrate-runners/efcore.ts
1617
+ import { spawn as spawn4 } from "child_process";
1618
+ import * as fs12 from "fs";
1619
+ import * as path12 from "path";
1620
+ import { Client as Client2 } from "pg";
1621
+ var MIGRATION_ID = /^(\d{14})_(.+)$/;
1622
+ function efVersion(id) {
1623
+ return MIGRATION_ID.exec(id)?.[1] ?? id;
1624
+ }
1625
+ function efDescription(id) {
1626
+ const name = MIGRATION_ID.exec(id)?.[2] ?? id;
1627
+ return name.replace(/_/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
1628
+ }
1629
+ function dsnToNpgsql(dsn) {
1630
+ const u = new URL(dsn);
1631
+ const parts = [
1632
+ `Host=${u.hostname}`,
1633
+ `Port=${u.port || "5432"}`,
1634
+ `Database=${decodeURIComponent(u.pathname.replace(/^\//, "")) || "databricks_postgres"}`
1635
+ ];
1636
+ if (u.username) parts.push(`Username=${decodeURIComponent(u.username)}`);
1637
+ if (u.password) parts.push(`Password=${decodeURIComponent(u.password)}`);
1638
+ const sslmode = (u.searchParams.get("sslmode") ?? "require").toLowerCase();
1639
+ const ssl = {
1640
+ disable: "Disable",
1641
+ allow: "Allow",
1642
+ prefer: "Prefer",
1643
+ require: "Require",
1644
+ "verify-ca": "VerifyCA",
1645
+ "verify-full": "VerifyFull"
1646
+ };
1647
+ parts.push(`SSL Mode=${ssl[sslmode] ?? "Require"}`);
1648
+ const app = u.searchParams.get("application_name");
1649
+ if (app) parts.push(`Application Name=${app}`);
1650
+ return parts.map((p) => p.replace(/;/g, "")).join(";");
1651
+ }
1652
+ function parseEfJson(stdout) {
1653
+ const body = stdout.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice("data:".length)).join("\n").trim();
1654
+ if (!body) {
1655
+ throw new SchemaMigrationError(`dotnet ef returned no JSON payload: ${stdout.slice(0, 400)}`);
1656
+ }
1657
+ try {
1658
+ return JSON.parse(body);
1659
+ } catch (err) {
1660
+ throw new SchemaMigrationError(
1661
+ `dotnet ef JSON parse failed: ${err instanceof Error ? err.message : String(err)}.
1662
+ Body (first 400 chars): ${body.slice(0, 400)}`
1663
+ );
1664
+ }
1665
+ }
1666
+ function requireProject(projectDir) {
1667
+ const project = findEfCoreProject(projectDir);
1668
+ if (!project) {
1669
+ throw new SchemaMigrationError(
1670
+ `No EF Core project found under ${projectDir}. Expected a *.csproj that references ${EF_DESIGN_PACKAGE}.`
1671
+ );
1672
+ }
1673
+ return project;
1674
+ }
1675
+ function spawnDotnet(projectDir, args, env) {
1676
+ return new Promise((resolve2, reject) => {
1677
+ const child = spawn4("dotnet", args, {
1678
+ cwd: projectDir,
1679
+ env: { ...process.env, DOTNET_NOLOGO: "1", DOTNET_CLI_TELEMETRY_OPTOUT: "1", ...env },
1680
+ stdio: ["ignore", "pipe", "pipe"]
1681
+ });
1682
+ let stdout = "";
1683
+ let stderr = "";
1684
+ child.stdout.on("data", (chunk) => {
1685
+ stdout += chunk.toString("utf8");
1686
+ });
1687
+ child.stderr.on("data", (chunk) => {
1688
+ stderr += chunk.toString("utf8");
1689
+ });
1690
+ child.on("error", (err) => {
1691
+ reject(
1692
+ new SchemaMigrationError(
1693
+ `Could not spawn dotnet. Is the .NET SDK installed and on PATH? ${err.message}`,
1694
+ err
1695
+ )
1696
+ );
1697
+ });
1698
+ child.on("close", (code) => {
1699
+ if (code === 0) {
1700
+ resolve2({ stdout, stderr });
1701
+ } else {
1702
+ reject(
1703
+ new SchemaMigrationError(
1704
+ `dotnet ${args.join(" ")} exited with code ${code}.
1705
+ stdout: ${stdout}
1706
+ stderr: ${stderr}`
1707
+ )
1708
+ );
1709
+ }
1710
+ });
1711
+ });
1712
+ }
1713
+ var restored = /* @__PURE__ */ new Set();
1714
+ async function ensureRestored(projectDir, csproj) {
1715
+ if (restored.has(projectDir)) return;
1716
+ if (fs12.existsSync(path12.join(projectDir, ".config", "dotnet-tools.json"))) {
1717
+ await spawnDotnet(projectDir, ["tool", "restore"], {});
1718
+ }
1719
+ await spawnDotnet(projectDir, ["restore", csproj], {});
1720
+ restored.add(projectDir);
1721
+ }
1722
+ async function runEf(projectDir, args, dsn, opts = {}) {
1723
+ const project = requireProject(projectDir);
1724
+ const csproj = path12.relative(projectDir, project.csproj) || project.csproj;
1725
+ await ensureRestored(projectDir, csproj);
1726
+ const full = ["ef", ...args, "--project", csproj, "--prefix-output"];
1727
+ if (opts.noBuild) full.push("--no-build");
1728
+ const env = {};
1729
+ if (dsn) {
1730
+ env.DATABASE_URL = dsn;
1731
+ if (process.env.LAKEBASE_EFCORE_CONNECTION_ARG === "1") full.push("--connection", dsnToNpgsql(dsn));
1732
+ }
1733
+ return spawnDotnet(projectDir, full, env);
1734
+ }
1735
+ async function listEfMigrations(ctx, opts = {}) {
1736
+ const { stdout, stderr } = await runEf(ctx.projectDir, ["migrations", "list", "--json"], ctx.dsn, opts);
1737
+ const entries = parseEfJson(stdout);
1738
+ if (ctx.dsn && entries.some((e) => e.applied === null)) {
1739
+ const warning = stdout.split(/\r?\n/).filter((l) => /^(warn|error):/.test(l)).join("\n");
1740
+ throw new SchemaMigrationError(
1741
+ `dotnet ef could not read migration state from the database (check connectivity and credentials).
1742
+ ${warning || stderr}`.trim()
1743
+ );
1744
+ }
1745
+ return entries;
1746
+ }
1747
+ async function readEfHistory(dsn) {
1748
+ const client = new Client2({ connectionString: dsn, application_name: connectionApplicationName() });
1749
+ await client.connect();
1750
+ try {
1751
+ const exists = await client.query(`SELECT to_regclass('"__EFMigrationsHistory"') IS NOT NULL AS present`);
1752
+ if (!exists.rows[0]?.present) return null;
1753
+ const r = await client.query(`SELECT "MigrationId" FROM "__EFMigrationsHistory" ORDER BY "MigrationId"`);
1754
+ return r.rows.map((row) => row.MigrationId);
1755
+ } finally {
1756
+ await client.end();
1757
+ }
1758
+ }
1759
+ function toApplied(entries) {
1760
+ return entries.map((e) => ({ version: efVersion(e.id), description: efDescription(e.id) }));
1761
+ }
1762
+ async function statusEfcore(ctx) {
1763
+ const entries = await listEfMigrations(ctx);
1764
+ const history = await (ctx.readHistory ?? readEfHistory)(ctx.dsn);
1765
+ const current = history && history.length > 0 ? efVersion(history[history.length - 1]) : void 0;
1766
+ const pending = entries.filter((e) => e.applied !== true).map((e) => ({ version: efVersion(e.id), filename: `${e.id}.cs`, description: efDescription(e.id) }));
1767
+ return { current, pending, tool: "efcore" };
1768
+ }
1769
+ async function applyEfcore(ctx) {
1770
+ const before = new Set((await listEfMigrations(ctx)).filter((e) => e.applied).map((e) => e.id));
1771
+ await runEf(ctx.projectDir, ["database", "update"], ctx.dsn, { noBuild: true });
1772
+ const after = await listEfMigrations(ctx, { noBuild: true });
1773
+ const applied = toApplied(after.filter((e) => e.applied && !before.has(e.id)));
1774
+ return { applied, alreadyAtLatest: applied.length === 0, tool: "efcore" };
1775
+ }
1776
+ function resolveEfTarget(target, applied) {
1777
+ const t = target.trim();
1778
+ if (t === "0") return "0";
1779
+ const rel = /^-(\d+)$/.exec(t);
1780
+ if (rel) {
1781
+ const steps = Number(rel[1]);
1782
+ if (steps < 1) throw new SchemaMigrationError(`Invalid rollback step: ${target}`);
1783
+ const index = applied.length - 1 - steps;
1784
+ if (index < -1) {
1785
+ throw new SchemaMigrationError(`Cannot roll back ${steps} migration(s); only ${applied.length} applied.`);
1786
+ }
1787
+ return index === -1 ? "0" : applied[index].id;
1788
+ }
1789
+ const match = applied.find((e) => e.id === t || efVersion(e.id) === t || e.name === t);
1790
+ if (!match) throw new SchemaMigrationError(`Rollback target '${target}' is not an applied migration.`);
1791
+ return match.id;
1792
+ }
1793
+ async function rollbackEfcore(ctx) {
1794
+ const beforeEntries = (await listEfMigrations(ctx)).filter((e) => e.applied);
1795
+ const targetId = resolveEfTarget(ctx.target, beforeEntries);
1796
+ await runEf(ctx.projectDir, ["database", "update", targetId], ctx.dsn, { noBuild: true });
1797
+ const stillApplied = new Set((await listEfMigrations(ctx, { noBuild: true })).filter((e) => e.applied).map((e) => e.id));
1798
+ return { rolledBack: toApplied(beforeEntries.filter((e) => !stillApplied.has(e.id)).reverse()), tool: "efcore" };
1799
+ }
1800
+ function efMigrationName(slug) {
1801
+ const name = slug.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
1802
+ if (!name) throw new SchemaMigrationError(`Cannot derive a migration name from '${slug}'.`);
1803
+ return /^[0-9]/.test(name) ? `M${name}` : name;
1804
+ }
1805
+ async function createEfcoreMigration(opts) {
1806
+ const project = requireProject(opts.projectDir);
1807
+ const name = efMigrationName(opts.slug);
1808
+ const existing = new Set(fs12.existsSync(project.migrationsDir) ? fs12.readdirSync(project.migrationsDir) : []);
1809
+ await runEf(opts.projectDir, ["migrations", "add", name]);
1810
+ const created = fs12.readdirSync(project.migrationsDir).filter((f) => !existing.has(f) && MIGRATION_ID.test(f.replace(/\.cs$/, "")) && !f.endsWith(".Designer.cs"));
1811
+ if (created.length !== 1) {
1812
+ throw new SchemaMigrationError(`Expected one new migration file for ${name}, found: ${created.join(", ") || "none"}`);
1813
+ }
1814
+ return { id: created[0].replace(/\.cs$/, ""), path: path12.join(project.migrationsDir, created[0]) };
1815
+ }
1816
+ function localEfMigrationIds(projectDir) {
1817
+ const project = findEfCoreProject(projectDir);
1818
+ if (!project || !fs12.existsSync(project.migrationsDir)) return [];
1819
+ return fs12.readdirSync(project.migrationsDir).filter((f) => f.endsWith(".cs") && !f.endsWith(".Designer.cs") && MIGRATION_ID.test(f.slice(0, -3))).map((f) => f.slice(0, -3)).sort();
1820
+ }
1821
+ function efProductVersion(project) {
1822
+ try {
1823
+ const csproj = fs12.readFileSync(project.csproj, "utf-8");
1824
+ const m = new RegExp(`${EF_DESIGN_PACKAGE.replace(/\./g, "\\.")}"\\s+Version="([^"]+)"`).exec(csproj);
1825
+ if (m) return m[1];
1826
+ } catch {
1827
+ }
1828
+ return "unknown";
1829
+ }
1830
+ async function stampEfcore(ctx) {
1831
+ const project = requireProject(ctx.projectDir);
1832
+ const ids = localEfMigrationIds(ctx.projectDir);
1833
+ if (ids.length === 0) throw new SchemaMigrationError("No local EF Core migrations to stamp.");
1834
+ const rev = ctx.revision.trim();
1835
+ const end = rev === "head" ? ids.length - 1 : ids.findIndex((id) => id === rev || efVersion(id) === rev);
1836
+ if (end < 0) throw new SchemaMigrationError(`Stamp revision '${ctx.revision}' is not a local migration.`);
1837
+ const toStamp = ids.slice(0, end + 1);
1838
+ const productVersion = efProductVersion(project);
1839
+ const client = new Client2({ connectionString: ctx.dsn, application_name: connectionApplicationName() });
1840
+ await client.connect();
1841
+ try {
1842
+ await client.query("BEGIN");
1843
+ await client.query(
1844
+ `CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
1845
+ "MigrationId" character varying(150) NOT NULL,
1846
+ "ProductVersion" character varying(32) NOT NULL,
1847
+ CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId"))`
1848
+ );
1849
+ await client.query(`DELETE FROM "__EFMigrationsHistory" WHERE NOT ("MigrationId" = ANY($1::text[]))`, [toStamp]);
1850
+ for (const id of toStamp) {
1851
+ await client.query(
1852
+ `INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ($1, $2)
1853
+ ON CONFLICT ("MigrationId") DO NOTHING`,
1854
+ [id, productVersion]
1855
+ );
1856
+ }
1857
+ await client.query("COMMIT");
1858
+ } catch (err) {
1859
+ await client.query("ROLLBACK").catch(() => void 0);
1860
+ throw err;
1861
+ } finally {
1862
+ await client.end();
1863
+ }
1864
+ return efVersion(toStamp[toStamp.length - 1]);
1865
+ }
1866
+
1867
+ // scripts/lakebase/adapters/efcore-adapter.ts
1868
+ async function buildDsn4(args) {
1869
+ const result = await getConnection({
1870
+ output: "dsn",
1871
+ instance: args.instance,
1872
+ branch: args.branch,
1873
+ database: args.database,
1874
+ endpointName: args.endpointName
1875
+ });
1876
+ return result.url;
1877
+ }
1878
+ function errMsg(err) {
1879
+ return err instanceof Error ? err.message : String(err);
1880
+ }
1881
+ function listEfCoreFiles(projectDir) {
1882
+ return localEfMigrationIds(projectDir).map((id) => ({
1883
+ version: efVersion(id),
1884
+ filename: `${id}.cs`,
1885
+ description: efDescription(id),
1886
+ type: "CSharp",
1887
+ tool: "efcore"
1888
+ }));
1889
+ }
1890
+ var EfCoreAdapter = {
1891
+ id: "efcore",
1892
+ languages: ["dotnet"],
1893
+ /** A .NET project (root marker) with a *.csproj that references Microsoft.EntityFrameworkCore.Design. */
1894
+ detect(projectDir) {
1895
+ return isDotnetProject(projectDir) && findEfCoreProject(projectDir) !== null;
1896
+ },
1897
+ async apply(args) {
1898
+ try {
1899
+ const dsn = await buildDsn4(args);
1900
+ const r = await applyEfcore({ projectDir: args.projectDir, dsn });
1901
+ return {
1902
+ applied_migrations: r.applied,
1903
+ status: r.alreadyAtLatest ? "noop" : "ok",
1904
+ tool_specific: { alreadyAtLatest: r.alreadyAtLatest, tool: r.tool }
1905
+ };
1906
+ } catch (err) {
1907
+ return { applied_migrations: [], status: "error", error: errMsg(err) };
1908
+ }
1909
+ },
1910
+ async rollback(args) {
1911
+ try {
1912
+ const dsn = await buildDsn4(args);
1913
+ const r = await rollbackEfcore({ projectDir: args.projectDir, dsn, target: args.target });
1914
+ return {
1915
+ rolled_back: r.rolledBack,
1916
+ status: r.rolledBack.length === 0 ? "noop" : "ok",
1917
+ tool_specific: { tool: r.tool }
1918
+ };
1919
+ } catch (err) {
1920
+ return { rolled_back: [], status: "error", error: errMsg(err) };
1921
+ }
1922
+ },
1923
+ async stamp(args) {
1924
+ try {
1925
+ const dsn = await buildDsn4(args);
1926
+ const stamped = await stampEfcore({ projectDir: args.projectDir, dsn, revision: args.revision });
1927
+ return { status: "ok", stamped_revision: stamped, tool_specific: { tool: "efcore" } };
1928
+ } catch (err) {
1929
+ return { status: "error", stamped_revision: null, error: errMsg(err) };
1930
+ }
1931
+ },
1932
+ async status(args) {
1933
+ try {
1934
+ const dsn = await buildDsn4(args);
1935
+ const r = await statusEfcore({ projectDir: args.projectDir, dsn });
1936
+ return {
1937
+ applied_version: r.current ?? null,
1938
+ pending: r.pending,
1939
+ applied: [],
1940
+ status: "ok",
1941
+ tool_specific: { tool: r.tool }
1942
+ };
1943
+ } catch (err) {
1944
+ return { applied_version: null, pending: [], applied: [], status: "error", error: errMsg(err) };
1945
+ }
1946
+ },
1947
+ async list(args) {
1948
+ return { files: listEfCoreFiles(args.projectDir) };
1949
+ },
1950
+ // baseline: use stamp (EF has no separate baseline concept).
1951
+ async newMigration(args) {
1952
+ try {
1953
+ const created = await createEfcoreMigration({ projectDir: args.projectDir, slug: args.slug });
1954
+ return {
1955
+ status: "ok",
1956
+ version: efVersion(created.id),
1957
+ filename: path13.basename(created.path),
1958
+ path: created.path
1959
+ };
1960
+ } catch (err) {
1961
+ return { status: "error", version: "", filename: "", path: "", error: errMsg(err) };
1962
+ }
1963
+ }
1964
+ };
1965
+ registerSchemaMigrationAdapter(EfCoreAdapter);
1966
+
1545
1967
  // scripts/lakebase/schema-migrate.ts
1546
1968
  var SchemaMigrationError = class extends Error {
1547
1969
  constructor(message, cause) {
@@ -1555,7 +1977,7 @@ function detectLanguage(projectDir) {
1555
1977
  const lang = resolveMigrationLanguage(projectDir);
1556
1978
  if (lang === "unknown") {
1557
1979
  throw new SchemaMigrationError(
1558
- `Could not detect project language in ${projectDir}. Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs). Pass {language} explicitly to override.`
1980
+ `Could not detect project language in ${projectDir}. Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs), *.sln or *.csproj (dotnet). Pass {language} explicitly to override.`
1559
1981
  );
1560
1982
  }
1561
1983
  return lang;
@@ -1569,12 +1991,12 @@ function migrationSlug2(description) {
1569
1991
  }
1570
1992
 
1571
1993
  // scripts/lakebase/workflow-drift.ts
1572
- import * as fs12 from "fs";
1573
- import * as path12 from "path";
1994
+ import * as fs14 from "fs";
1995
+ import * as path15 from "path";
1574
1996
  function findKitTemplatesDir(start) {
1575
1997
  let dir = start;
1576
1998
  for (let i = 0; i < 6; i++) {
1577
- const candidate = path12.join(
1999
+ const candidate = path15.join(
1578
2000
  dir,
1579
2001
  "templates",
1580
2002
  "project",
@@ -1582,8 +2004,8 @@ function findKitTemplatesDir(start) {
1582
2004
  ".github",
1583
2005
  "workflows"
1584
2006
  );
1585
- if (fs12.existsSync(candidate)) return candidate;
1586
- const parent = path12.dirname(dir);
2007
+ if (fs14.existsSync(candidate)) return candidate;
2008
+ const parent = path15.dirname(dir);
1587
2009
  if (parent === dir) break;
1588
2010
  dir = parent;
1589
2011
  }
@@ -1607,13 +2029,13 @@ function unifiedDiff(name, projectContent, templateContent) {
1607
2029
  return out.join("\n");
1608
2030
  }
1609
2031
  function detectWorkflowDrift(args) {
1610
- const projectWorkflowsDir = path12.join(
2032
+ const projectWorkflowsDir = path15.join(
1611
2033
  args.projectDir,
1612
2034
  ".github",
1613
2035
  "workflows"
1614
2036
  );
1615
- const here = path12.dirname(new URL(import.meta.url).pathname);
1616
- const kitWorkflowsDir = args.kitDir ? path12.join(
2037
+ const here = path15.dirname(new URL(import.meta.url).pathname);
2038
+ const kitWorkflowsDir = args.kitDir ? path15.join(
1617
2039
  args.kitDir,
1618
2040
  "templates",
1619
2041
  "project",
@@ -1621,21 +2043,21 @@ function detectWorkflowDrift(args) {
1621
2043
  ".github",
1622
2044
  "workflows"
1623
2045
  ) : findKitTemplatesDir(here);
1624
- const templateFiles = fs12.existsSync(kitWorkflowsDir) ? fs12.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
1625
- const projectFiles = fs12.existsSync(projectWorkflowsDir) ? fs12.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
2046
+ const templateFiles = fs14.existsSync(kitWorkflowsDir) ? fs14.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
2047
+ const projectFiles = fs14.existsSync(projectWorkflowsDir) ? fs14.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
1626
2048
  const version = readKitVersion(kitWorkflowsDir);
1627
2049
  const seen = /* @__PURE__ */ new Set();
1628
2050
  const files = [];
1629
2051
  for (const name of templateFiles) {
1630
2052
  seen.add(name);
1631
- const projectPath2 = path12.join(projectWorkflowsDir, name);
1632
- const templatePath = path12.join(kitWorkflowsDir, name);
1633
- if (!fs12.existsSync(projectPath2)) {
2053
+ const projectPath2 = path15.join(projectWorkflowsDir, name);
2054
+ const templatePath = path15.join(kitWorkflowsDir, name);
2055
+ if (!fs14.existsSync(projectPath2)) {
1634
2056
  files.push({ name, status: "missing" });
1635
2057
  continue;
1636
2058
  }
1637
- const projectContent = fs12.readFileSync(projectPath2, "utf8");
1638
- const templateContent = applyPlaceholders(fs12.readFileSync(templatePath, "utf8"), version);
2059
+ const projectContent = fs14.readFileSync(projectPath2, "utf8");
2060
+ const templateContent = applyPlaceholders(fs14.readFileSync(templatePath, "utf8"), version);
1639
2061
  if (projectContent === templateContent) {
1640
2062
  files.push({ name, status: "unchanged" });
1641
2063
  } else {
@@ -1666,10 +2088,10 @@ function detectWorkflowDrift(args) {
1666
2088
  function readKitVersion(kitWorkflowsDir) {
1667
2089
  let dir = kitWorkflowsDir;
1668
2090
  for (let i = 0; i < 5; i++) {
1669
- dir = path12.dirname(dir);
2091
+ dir = path15.dirname(dir);
1670
2092
  }
1671
2093
  try {
1672
- const raw = fs12.readFileSync(path12.join(dir, "package.json"), "utf-8");
2094
+ const raw = fs14.readFileSync(path15.join(dir, "package.json"), "utf-8");
1673
2095
  const pkg = JSON.parse(raw);
1674
2096
  return typeof pkg.version === "string" ? pkg.version : "unknown";
1675
2097
  } catch {
@@ -1682,10 +2104,10 @@ function applyPlaceholders(content, version) {
1682
2104
 
1683
2105
  // scripts/lakebase/doctor.ts
1684
2106
  function readEnvFile(projectDir) {
1685
- const envPath = path13.join(projectDir, ".env");
1686
- if (!fs13.existsSync(envPath)) return {};
2107
+ const envPath = path16.join(projectDir, ".env");
2108
+ if (!fs15.existsSync(envPath)) return {};
1687
2109
  const out = {};
1688
- for (const line of fs13.readFileSync(envPath, "utf8").split("\n")) {
2110
+ for (const line of fs15.readFileSync(envPath, "utf8").split("\n")) {
1689
2111
  const m = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)\s*$/);
1690
2112
  if (!m) continue;
1691
2113
  let val = m[2];
@@ -1839,6 +2261,23 @@ async function checkPrereq(spec, run) {
1839
2261
  detail: { version: trimmed }
1840
2262
  };
1841
2263
  }
2264
+ var DOTNET_PREREQ = {
2265
+ name: "dotnet-sdk",
2266
+ cmd: "dotnet",
2267
+ versionArgs: ["--version"],
2268
+ minMajor: 10,
2269
+ label: ".NET SDK",
2270
+ 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."
2271
+ };
2272
+ async function checkLanguagePrerequisites(projectDir, run = defaultVersionRunner) {
2273
+ let lang;
2274
+ try {
2275
+ lang = detectLanguage(projectDir);
2276
+ } catch {
2277
+ return [];
2278
+ }
2279
+ return lang === "dotnet" ? [await checkPrereq(DOTNET_PREREQ, run)] : [];
2280
+ }
1842
2281
  async function checkPrerequisites(run = defaultVersionRunner) {
1843
2282
  return Promise.all(PREREQS.map((spec) => checkPrereq(spec, run)));
1844
2283
  }
@@ -1940,7 +2379,7 @@ function checkEnv(projectDir) {
1940
2379
  name: "env-file",
1941
2380
  status: "warn",
1942
2381
  message: ".env not found",
1943
- detail: { projectDir, envPath: path13.join(projectDir, ".env") },
2382
+ detail: { projectDir, envPath: path16.join(projectDir, ".env") },
1944
2383
  hint: "Run `lakebase-branch sync-env`, or `git checkout <branch>` so the post-checkout hook writes .env (or copy .env.example). The kit stores connection METADATA only , no DB token (the app mints a short-lived credential at runtime)."
1945
2384
  };
1946
2385
  }
@@ -2170,6 +2609,7 @@ async function runDoctor(args = {}) {
2170
2609
  const hooks = checkHooks(projectDir);
2171
2610
  const workflowDrift = checkWorkflowDrift(projectDir);
2172
2611
  const prereqs = await checkPrerequisites();
2612
+ const languagePrereqs = await checkLanguagePrerequisites(projectDir);
2173
2613
  const lakebaseEnabled = auth.status === "ok" ? await checkLakebaseEnabled(profile) : {
2174
2614
  name: "lakebase-enabled",
2175
2615
  status: "skip",
@@ -2178,6 +2618,7 @@ async function runDoctor(args = {}) {
2178
2618
  const checks = [
2179
2619
  cli,
2180
2620
  ...prereqs,
2621
+ ...languagePrereqs,
2181
2622
  auth,
2182
2623
  identity,
2183
2624
  lakebaseEnabled,
@@ -2291,7 +2732,7 @@ async function main() {
2291
2732
  return 0;
2292
2733
  }
2293
2734
  if (args.fix) {
2294
- const envPath = path14.join(args.projectDir ?? process.cwd(), ".env");
2735
+ const envPath = path17.join(args.projectDir ?? process.cwd(), ".env");
2295
2736
  const res = await ensureProfilePinned({ envPath });
2296
2737
  if (!args.json) {
2297
2738
  if (res.pinned) {