@databricks-solutions/lakebase-scm-utils 0.2.44 → 0.2.46

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 (48) hide show
  1. package/README.md +13 -4
  2. package/dist/scripts/index.cjs +651 -605
  3. package/dist/scripts/index.d.cts +1 -1
  4. package/dist/scripts/index.d.ts +1 -1
  5. package/dist/scripts/index.js +650 -605
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +12 -3
  7. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +12 -3
  8. package/dist/scripts/lakebase/collapse-heads.cli.cjs +12 -3
  9. package/dist/scripts/lakebase/collapse-heads.cli.js +12 -3
  10. package/dist/scripts/lakebase/detect-language.cli.cjs +12 -3
  11. package/dist/scripts/lakebase/detect-language.cli.js +12 -3
  12. package/dist/scripts/lakebase/doctor.cli.cjs +13 -4
  13. package/dist/scripts/lakebase/doctor.cli.js +13 -4
  14. package/dist/scripts/lakebase/get-connection.cli.cjs +12 -3
  15. package/dist/scripts/lakebase/get-connection.cli.js +12 -3
  16. package/dist/scripts/lakebase/index.cjs +472 -395
  17. package/dist/scripts/lakebase/index.d.cts +33 -12
  18. package/dist/scripts/lakebase/index.d.ts +33 -12
  19. package/dist/scripts/lakebase/index.js +471 -395
  20. package/dist/scripts/lakebase/infra-runner.cli.cjs +12 -3
  21. package/dist/scripts/lakebase/infra-runner.cli.js +12 -3
  22. package/dist/scripts/lakebase/new-migration.cli.cjs +12 -3
  23. package/dist/scripts/lakebase/new-migration.cli.js +12 -3
  24. package/dist/scripts/lakebase/schema-diff.cli.cjs +12 -3
  25. package/dist/scripts/lakebase/schema-diff.cli.js +12 -3
  26. package/dist/scripts/lakebase/schema-migrate.cli.cjs +12 -3
  27. package/dist/scripts/lakebase/schema-migrate.cli.js +12 -3
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +12 -3
  29. package/dist/scripts/lakebase/scm-claim-feature.cli.js +12 -3
  30. package/dist/scripts/lakebase/scm-doctor.cli.cjs +12 -3
  31. package/dist/scripts/lakebase/scm-doctor.cli.js +12 -3
  32. package/dist/scripts/lakebase/scm-merge.cli.cjs +2294 -2034
  33. package/dist/scripts/lakebase/scm-merge.cli.js +349 -89
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +12 -3
  35. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +12 -3
  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/constants.ts +23 -5
  40. package/scripts/lakebase/doctor.ts +1 -1
  41. package/scripts/lakebase/get-connection.ts +17 -3
  42. package/scripts/lakebase/scm-merge.ts +80 -1
  43. package/skills/lakebase-scm-workflows/README.md +2 -1
  44. package/templates/project/nodejs/package.json +3 -1
  45. package/templates/project/nodejs/src/index.js +8 -0
  46. package/templates/project/nodejs/src/openapi.js +20 -0
  47. package/templates/project/nodejs/src/routes/health.js +18 -0
  48. package/templates/project/nodejs/tests/app.test.js +16 -0
@@ -2506,6 +2506,7 @@ var RUNTIME_ARTIFACT_IGNORE = [
2506
2506
  var DEFAULT_ENDPOINT = "primary";
2507
2507
  var CONSORT_APPLICATION_NAME = "consort";
2508
2508
  var SCM_UTILS_APPLICATION_NAME = "scm-utils";
2509
+ var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
2509
2510
  var CONSORT_VERSION_ENV = "CONSORT_VERSION";
2510
2511
 
2511
2512
  // scripts/lakebase/self-version.ts
@@ -2517,8 +2518,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
2517
2518
  var cached;
2518
2519
  function substrateSelfVersion() {
2519
2520
  if (cached !== void 0) return cached;
2520
- if ("0.2.44".length > 0) {
2521
- cached = "0.2.44";
2521
+ if ("0.2.46".length > 0) {
2522
+ cached = "0.2.46";
2522
2523
  return cached;
2523
2524
  }
2524
2525
  cached = "unknown";
@@ -2546,9 +2547,17 @@ function substrateSelfVersion() {
2546
2547
  }
2547
2548
 
2548
2549
  // scripts/lakebase/get-connection.ts
2550
+ function sanitizeApplicationName(raw) {
2551
+ const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
2552
+ if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
2553
+ return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
2554
+ }
2549
2555
  function connectionApplicationName() {
2556
+ const client = process.env[SCM_CLIENT_ENV]?.trim();
2557
+ if (client) return sanitizeApplicationName(client);
2550
2558
  const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
2551
- return consortVersion ? `${CONSORT_APPLICATION_NAME}/${consortVersion}` : `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
2559
+ if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
2560
+ return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
2552
2561
  }
2553
2562
  async function getConnection(args) {
2554
2563
  const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
@@ -2746,6 +2755,14 @@ async function getCredential(args) {
2746
2755
 
2747
2756
  // scripts/git/status.ts
2748
2757
  init_esm_shims();
2758
+ async function hasUpstream(args) {
2759
+ try {
2760
+ await exec2("git rev-parse --abbrev-ref @{u}", { cwd: args.cwd });
2761
+ return true;
2762
+ } catch {
2763
+ return false;
2764
+ }
2765
+ }
2749
2766
  async function getAheadBehind(args) {
2750
2767
  const { cwd } = args;
2751
2768
  try {
@@ -15849,117 +15866,460 @@ function pickRunUrl(pr) {
15849
15866
 
15850
15867
  // scripts/lakebase/scm-merge.ts
15851
15868
  init_esm_shims();
15852
- async function reconcileTierToOrigin(args) {
15853
- const { cwd, tier } = args;
15869
+
15870
+ // scripts/git/sync.ts
15871
+ init_esm_shims();
15872
+ async function currentBranchName(cwd) {
15854
15873
  try {
15855
- const head = await getCurrentBranch({ cwd });
15856
- if (head === tier) {
15857
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
15858
- } else {
15859
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
15860
- }
15861
- return null;
15862
- } catch (err) {
15863
- return `local reconcile of ${tier} to origin/${tier} failed: ${err instanceof Error ? err.message : String(err)}. Reconcile it by hand (git checkout ${tier} && git pull --ff-only).`;
15874
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
15875
+ } catch {
15876
+ return "";
15864
15877
  }
15865
15878
  }
15866
- var ScmMergeError = class extends Error {
15867
- constructor(message, code) {
15868
- super(message);
15869
- this.code = code;
15870
- this.name = "ScmMergeError";
15879
+ async function pushCurrentBranchForPr(args) {
15880
+ const remote = args.remote ?? "origin";
15881
+ const branch = await currentBranchName(args.cwd);
15882
+ if (!branch) throw new Error("No current branch");
15883
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
15884
+ if (!upstreamSet) {
15885
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
15886
+ cwd: args.cwd
15887
+ });
15888
+ } else {
15889
+ await exec2("git push", { cwd: args.cwd });
15871
15890
  }
15872
- code;
15873
- };
15874
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
15875
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
15876
- function defaultMigratePredicate(run, mergedAt) {
15877
- if (!run.createdAt) return false;
15878
- const created = Date.parse(run.createdAt);
15879
- if (!Number.isFinite(created)) return false;
15880
- if (run.event && run.event !== "push") return false;
15881
- return created >= mergedAt.getTime() - 5e3;
15882
- }
15883
- function shaMigratePredicate(mergeCommitSha) {
15884
- return (run) => {
15885
- if (run.event && run.event !== "push") return false;
15886
- return !!run.headSha && run.headSha === mergeCommitSha;
15887
- };
15888
15891
  }
15889
- async function mergeFeature(args) {
15890
- const current = readWorkflowState(args.projectDir);
15891
- if (!current) {
15892
- throw new ScmMergeError(
15893
- "No SCM workflow state. wait-ci first.",
15894
- "no-state-file"
15892
+
15893
+ // scripts/lakebase/workflow-drift.ts
15894
+ init_esm_shims();
15895
+ import * as fs29 from "fs";
15896
+ import * as path29 from "path";
15897
+ function findKitTemplatesDir(start) {
15898
+ let dir = start;
15899
+ for (let i = 0; i < 6; i++) {
15900
+ const candidate = path29.join(
15901
+ dir,
15902
+ "templates",
15903
+ "project",
15904
+ "common",
15905
+ ".github",
15906
+ "workflows"
15895
15907
  );
15908
+ if (fs29.existsSync(candidate)) return candidate;
15909
+ const parent = path29.dirname(dir);
15910
+ if (parent === dir) break;
15911
+ dir = parent;
15896
15912
  }
15897
- if (current.state !== "ci-green") {
15898
- throw new ScmMergeError(
15899
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
15900
- "bad-precondition"
15901
- );
15913
+ throw new Error(
15914
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
15915
+ );
15916
+ }
15917
+ function unifiedDiff(name, projectContent, templateContent) {
15918
+ if (projectContent === templateContent) return "";
15919
+ const a = projectContent.split("\n");
15920
+ const b = templateContent.split("\n");
15921
+ const out = [`--- project/${name}`, `+++ template/${name}`];
15922
+ const max = Math.max(a.length, b.length);
15923
+ for (let i = 0; i < max; i++) {
15924
+ const av = a[i];
15925
+ const bv = b[i];
15926
+ if (av === bv) continue;
15927
+ if (av !== void 0) out.push(`-${i + 1}: ${av}`);
15928
+ if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
15902
15929
  }
15903
- if (!current.pr_url) {
15904
- throw new ScmMergeError(
15905
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
15906
- "no-pr-url"
15907
- );
15930
+ return out.join("\n");
15931
+ }
15932
+ function detectWorkflowDrift(args) {
15933
+ const projectWorkflowsDir = path29.join(
15934
+ args.projectDir,
15935
+ ".github",
15936
+ "workflows"
15937
+ );
15938
+ const here = path29.dirname(new URL(import.meta.url).pathname);
15939
+ const kitWorkflowsDir = args.kitDir ? path29.join(
15940
+ args.kitDir,
15941
+ "templates",
15942
+ "project",
15943
+ "common",
15944
+ ".github",
15945
+ "workflows"
15946
+ ) : findKitTemplatesDir(here);
15947
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
15948
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
15949
+ const version = readKitVersion(kitWorkflowsDir);
15950
+ const seen = /* @__PURE__ */ new Set();
15951
+ const files = [];
15952
+ for (const name of templateFiles) {
15953
+ seen.add(name);
15954
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
15955
+ const templatePath = path29.join(kitWorkflowsDir, name);
15956
+ if (!fs29.existsSync(projectPath2)) {
15957
+ files.push({ name, status: "missing" });
15958
+ continue;
15959
+ }
15960
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
15961
+ const templateContent = applyPlaceholders(fs29.readFileSync(templatePath, "utf8"), version);
15962
+ if (projectContent === templateContent) {
15963
+ files.push({ name, status: "unchanged" });
15964
+ } else {
15965
+ files.push({
15966
+ name,
15967
+ status: "drifted",
15968
+ diff: unifiedDiff(name, projectContent, templateContent)
15969
+ });
15970
+ }
15908
15971
  }
15909
- if (!current.branch || !current.parent_branch) {
15910
- throw new ScmMergeError(
15911
- "ci-green row missing branch / parent_branch; refusing to merge.",
15912
- "bad-precondition"
15913
- );
15972
+ for (const name of projectFiles) {
15973
+ if (seen.has(name)) continue;
15974
+ files.push({ name, status: "extra" });
15914
15975
  }
15915
- const ownerRepo = await getOwnerRepo(args.projectDir);
15916
- if (!ownerRepo) {
15917
- throw new ScmMergeError(
15918
- "No GitHub remote found at origin.",
15919
- "no-github-remote"
15920
- );
15976
+ const order = {
15977
+ drifted: 0,
15978
+ missing: 1,
15979
+ extra: 2,
15980
+ unchanged: 3
15981
+ };
15982
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
15983
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
15984
+ return {
15985
+ overall: hasDrift ? "drift" : "ok",
15986
+ files
15987
+ };
15988
+ }
15989
+ function readKitVersion(kitWorkflowsDir) {
15990
+ let dir = kitWorkflowsDir;
15991
+ for (let i = 0; i < 5; i++) {
15992
+ dir = path29.dirname(dir);
15921
15993
  }
15922
- const pullNumber = extractPullNumber(current.pr_url);
15923
- if (!pullNumber) {
15924
- throw new ScmMergeError(
15925
- `Could not extract PR number from URL: ${current.pr_url}`,
15926
- "bad-pr-url"
15994
+ try {
15995
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
15996
+ const pkg = JSON.parse(raw);
15997
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
15998
+ } catch {
15999
+ return "unknown";
16000
+ }
16001
+ }
16002
+ function applyPlaceholders(content, version) {
16003
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
16004
+ }
16005
+ function applyCommandPlaceholders(content, version) {
16006
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
16007
+ }
16008
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16009
+ function findKitCommandsDir(start) {
16010
+ let dir = start;
16011
+ for (let i = 0; i < 6; i++) {
16012
+ const candidate = path29.join(
16013
+ dir,
16014
+ "templates",
16015
+ "project",
16016
+ "common",
16017
+ ".claude",
16018
+ "commands"
15927
16019
  );
16020
+ if (fs29.existsSync(candidate)) return candidate;
16021
+ const parent = path29.dirname(dir);
16022
+ if (parent === dir) break;
16023
+ dir = parent;
15928
16024
  }
15929
- const instance = args.instance ?? current.project_id;
15930
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
15931
- const wantMigrate = args.waitMigrate !== false;
15932
- let authVerified = false;
15933
- if (wantMigrate && args.verifyMigrateAuth) {
15934
- const probe = await args.verifyMigrateAuth();
15935
- if (!probe.ok) {
15936
- throw new ScmMergeError(
15937
- `Migrate-auth precondition failed: ${probe.detail ?? "the Databricks credential is not usable"}. Refusing to merge: the downstream migrate would promote git without applying the schema. Refresh your credential (databricks auth login), then re-run.`,
15938
- "migrate-auth"
15939
- );
16025
+ throw new Error(
16026
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16027
+ );
16028
+ }
16029
+ function parsePinnedVersion(content) {
16030
+ const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16031
+ return m ? m[1] : void 0;
16032
+ }
16033
+ function detectCommandDrift(args) {
16034
+ const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
16035
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16036
+ const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16037
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16038
+ const templateFiles = fs29.existsSync(kitCommandsDir) ? fs29.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16039
+ const projectFiles = fs29.existsSync(projectCommandsDir) ? fs29.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16040
+ const seen = /* @__PURE__ */ new Set();
16041
+ const files = [];
16042
+ for (const name of templateFiles) {
16043
+ seen.add(name);
16044
+ const projectPath2 = path29.join(projectCommandsDir, name);
16045
+ const templatePath = path29.join(kitCommandsDir, name);
16046
+ const templateRaw = fs29.readFileSync(templatePath, "utf8");
16047
+ if (!fs29.existsSync(projectPath2)) {
16048
+ files.push({ name, status: "missing", kit_version: kitVersion });
16049
+ continue;
16050
+ }
16051
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16052
+ const pinned = parsePinnedVersion(projectContent);
16053
+ const versionForCompare = pinned ?? kitVersion;
16054
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16055
+ if (projectContent === templateContent) {
16056
+ files.push({
16057
+ name,
16058
+ status: "unchanged",
16059
+ pinned_version: pinned,
16060
+ kit_version: kitVersion
16061
+ });
16062
+ } else {
16063
+ files.push({
16064
+ name,
16065
+ status: "drifted",
16066
+ pinned_version: pinned,
16067
+ kit_version: kitVersion,
16068
+ diff: unifiedDiff(name, projectContent, templateContent)
16069
+ });
15940
16070
  }
15941
- authVerified = true;
15942
16071
  }
15943
- let paired;
15944
- try {
15945
- paired = await mergePairedPullRequest({
15946
- ownerRepo,
15947
- pullNumber,
15948
- lakebaseInstance: instance,
15949
- method: args.method ?? "squash"
15950
- });
15951
- } catch (err) {
15952
- throw new ScmMergeError(
15953
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
15954
- "merge-failed"
15955
- );
16072
+ for (const name of projectFiles) {
16073
+ if (seen.has(name)) continue;
16074
+ files.push({ name, status: "extra", kit_version: kitVersion });
15956
16075
  }
15957
- const warnings = [...paired.warnings];
15958
- let localBranchDeleted = false;
15959
- let headAfter = current.branch;
15960
- if (!args.skipLocalCleanup) {
15961
- const switchTo = args.switchTo ?? gitBase;
15962
- const head = await getCurrentBranch({ cwd: args.projectDir });
16076
+ const order = {
16077
+ drifted: 0,
16078
+ missing: 1,
16079
+ extra: 2,
16080
+ unchanged: 3
16081
+ };
16082
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16083
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16084
+ return { overall: hasDrift ? "drift" : "ok", files };
16085
+ }
16086
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
16087
+ let dir = kitCommandsDir;
16088
+ for (let i = 0; i < 5; i++) {
16089
+ dir = path29.dirname(dir);
16090
+ }
16091
+ try {
16092
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16093
+ const pkg = JSON.parse(raw);
16094
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16095
+ } catch {
16096
+ return "unknown";
16097
+ }
16098
+ }
16099
+ function detectScaffoldedDrift(args) {
16100
+ const workflows = detectWorkflowDrift(args);
16101
+ const commands = detectCommandDrift(args);
16102
+ return {
16103
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16104
+ workflows,
16105
+ commands
16106
+ };
16107
+ }
16108
+ function updateWorkflows(args) {
16109
+ const projectWorkflowsDir = path29.join(
16110
+ args.projectDir,
16111
+ ".github",
16112
+ "workflows"
16113
+ );
16114
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16115
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16116
+ args.kitDir,
16117
+ "templates",
16118
+ "project",
16119
+ "common",
16120
+ ".github",
16121
+ "workflows"
16122
+ ) : findKitTemplatesDir(here);
16123
+ const substitute = args.substitute !== false;
16124
+ const dryRun = args.dryRun === true;
16125
+ const pruneExtras = args.pruneExtras === true;
16126
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16127
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16128
+ if (!dryRun && templateFiles.length > 0 && !fs29.existsSync(projectWorkflowsDir)) {
16129
+ fs29.mkdirSync(projectWorkflowsDir, { recursive: true });
16130
+ }
16131
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16132
+ const seen = /* @__PURE__ */ new Set();
16133
+ const files = [];
16134
+ for (const name of templateFiles) {
16135
+ seen.add(name);
16136
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16137
+ const templatePath = path29.join(kitWorkflowsDir, name);
16138
+ const templateRaw = fs29.readFileSync(templatePath, "utf-8");
16139
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16140
+ const existed = fs29.existsSync(projectPath2);
16141
+ const current = existed ? fs29.readFileSync(projectPath2, "utf-8") : "";
16142
+ let outcome;
16143
+ if (!existed) {
16144
+ outcome = "added";
16145
+ } else if (current === desired) {
16146
+ outcome = "unchanged";
16147
+ } else {
16148
+ outcome = "updated";
16149
+ }
16150
+ if (!dryRun && outcome !== "unchanged") {
16151
+ fs29.writeFileSync(projectPath2, desired);
16152
+ }
16153
+ files.push({ name, outcome });
16154
+ }
16155
+ if (pruneExtras) {
16156
+ for (const name of projectFiles) {
16157
+ if (seen.has(name)) continue;
16158
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16159
+ if (!dryRun) {
16160
+ fs29.unlinkSync(projectPath2);
16161
+ }
16162
+ files.push({ name, outcome: "removed" });
16163
+ }
16164
+ }
16165
+ const order = {
16166
+ added: 0,
16167
+ updated: 1,
16168
+ removed: 2,
16169
+ unchanged: 3
16170
+ };
16171
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
16172
+ const changed = files.some((f) => f.outcome !== "unchanged");
16173
+ return { files, changed };
16174
+ }
16175
+
16176
+ // scripts/lakebase/scm-merge.ts
16177
+ async function reconcileTierToOrigin(args) {
16178
+ const { cwd, tier } = args;
16179
+ try {
16180
+ const head = await getCurrentBranch({ cwd });
16181
+ if (head === tier) {
16182
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16183
+ } else {
16184
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16185
+ }
16186
+ return null;
16187
+ } catch (err) {
16188
+ return `local reconcile of ${tier} to origin/${tier} failed: ${err instanceof Error ? err.message : String(err)}. Reconcile it by hand (git checkout ${tier} && git pull --ff-only).`;
16189
+ }
16190
+ }
16191
+ var ScmMergeError = class extends Error {
16192
+ constructor(message, code) {
16193
+ super(message);
16194
+ this.code = code;
16195
+ this.name = "ScmMergeError";
16196
+ }
16197
+ code;
16198
+ };
16199
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16200
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16201
+ function defaultMigratePredicate(run, mergedAt) {
16202
+ if (!run.createdAt) return false;
16203
+ const created = Date.parse(run.createdAt);
16204
+ if (!Number.isFinite(created)) return false;
16205
+ if (run.event && run.event !== "push") return false;
16206
+ return created >= mergedAt.getTime() - 5e3;
16207
+ }
16208
+ function shaMigratePredicate(mergeCommitSha) {
16209
+ return (run) => {
16210
+ if (run.event && run.event !== "push") return false;
16211
+ return !!run.headSha && run.headSha === mergeCommitSha;
16212
+ };
16213
+ }
16214
+ async function defaultRefreshPromoteWorkflows(projectDir) {
16215
+ const drift = detectWorkflowDrift({ projectDir });
16216
+ const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
16217
+ if (touched.length === 0) return { refreshed: false };
16218
+ const result = updateWorkflows({ projectDir });
16219
+ if (!result.changed) return { refreshed: false };
16220
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
16221
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
16222
+ cwd: projectDir,
16223
+ timeout: 1e4
16224
+ });
16225
+ if (!staged.trim()) return { refreshed: false };
16226
+ await exec2(
16227
+ `git commit -m ${shellEscape2(
16228
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
16229
+ )} -- .github/workflows`,
16230
+ { cwd: projectDir, timeout: 15e3 }
16231
+ );
16232
+ await pushCurrentBranchForPr({ cwd: projectDir });
16233
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
16234
+ }
16235
+ async function mergeFeature(args) {
16236
+ const current = readWorkflowState(args.projectDir);
16237
+ if (!current) {
16238
+ throw new ScmMergeError(
16239
+ "No SCM workflow state. wait-ci first.",
16240
+ "no-state-file"
16241
+ );
16242
+ }
16243
+ if (current.state !== "ci-green") {
16244
+ throw new ScmMergeError(
16245
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16246
+ "bad-precondition"
16247
+ );
16248
+ }
16249
+ if (!current.pr_url) {
16250
+ throw new ScmMergeError(
16251
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16252
+ "no-pr-url"
16253
+ );
16254
+ }
16255
+ if (!current.branch || !current.parent_branch) {
16256
+ throw new ScmMergeError(
16257
+ "ci-green row missing branch / parent_branch; refusing to merge.",
16258
+ "bad-precondition"
16259
+ );
16260
+ }
16261
+ const ownerRepo = await getOwnerRepo(args.projectDir);
16262
+ if (!ownerRepo) {
16263
+ throw new ScmMergeError(
16264
+ "No GitHub remote found at origin.",
16265
+ "no-github-remote"
16266
+ );
16267
+ }
16268
+ const pullNumber = extractPullNumber(current.pr_url);
16269
+ if (!pullNumber) {
16270
+ throw new ScmMergeError(
16271
+ `Could not extract PR number from URL: ${current.pr_url}`,
16272
+ "bad-pr-url"
16273
+ );
16274
+ }
16275
+ const instance = args.instance ?? current.project_id;
16276
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16277
+ const wantMigrate = args.waitMigrate !== false;
16278
+ let authVerified = false;
16279
+ if (wantMigrate && args.verifyMigrateAuth) {
16280
+ const probe = await args.verifyMigrateAuth();
16281
+ if (!probe.ok) {
16282
+ throw new ScmMergeError(
16283
+ `Migrate-auth precondition failed: ${probe.detail ?? "the Databricks credential is not usable"}. Refusing to merge: the downstream migrate would promote git without applying the schema. Refresh your credential (databricks auth login), then re-run.`,
16284
+ "migrate-auth"
16285
+ );
16286
+ }
16287
+ authVerified = true;
16288
+ }
16289
+ const preflightNotes = [];
16290
+ {
16291
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
16292
+ try {
16293
+ const r = await refresh();
16294
+ if (r.refreshed) {
16295
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
16296
+ }
16297
+ } catch (err) {
16298
+ preflightNotes.push(
16299
+ `Workflow self-heal skipped (${err instanceof Error ? err.message : String(err)}); if the promote's migrate-target fails on the tier guard, refresh .github/workflows (merge.yml must call 'lakebase-schema-migrate apply-tier').`
16300
+ );
16301
+ }
16302
+ }
16303
+ let paired;
16304
+ try {
16305
+ paired = await mergePairedPullRequest({
16306
+ ownerRepo,
16307
+ pullNumber,
16308
+ lakebaseInstance: instance,
16309
+ method: args.method ?? "squash"
16310
+ });
16311
+ } catch (err) {
16312
+ throw new ScmMergeError(
16313
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
16314
+ "merge-failed"
16315
+ );
16316
+ }
16317
+ const warnings = [...preflightNotes, ...paired.warnings];
16318
+ let localBranchDeleted = false;
16319
+ let headAfter = current.branch;
16320
+ if (!args.skipLocalCleanup) {
16321
+ const switchTo = args.switchTo ?? gitBase;
16322
+ const head = await getCurrentBranch({ cwd: args.projectDir });
15963
16323
  if (head === current.branch) {
15964
16324
  try {
15965
16325
  await exec2(`git checkout ${shellEscape2(switchTo)}`, {
@@ -16151,7 +16511,7 @@ init_esm_shims();
16151
16511
 
16152
16512
  // scripts/git/branches.ts
16153
16513
  init_esm_shims();
16154
- async function currentBranchName(cwd) {
16514
+ async function currentBranchName2(cwd) {
16155
16515
  try {
16156
16516
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16157
16517
  } catch {
@@ -16170,7 +16530,7 @@ async function listLocalBranches(args) {
16170
16530
  return [];
16171
16531
  }
16172
16532
  if (!raw) return [];
16173
- const current = await currentBranchName(cwd);
16533
+ const current = await currentBranchName2(cwd);
16174
16534
  return raw.split("\n").filter(Boolean).map((line) => {
16175
16535
  const [name, tracking, trackInfo] = line.split("|");
16176
16536
  let ahead = 0;
@@ -16335,15 +16695,15 @@ function parentForTopology(t, defaultLeaf) {
16335
16695
 
16336
16696
  // scripts/lakebase/scm-doctor.ts
16337
16697
  init_esm_shims();
16338
- import * as fs29 from "fs";
16339
- import * as path29 from "path";
16698
+ import * as fs30 from "fs";
16699
+ import * as path30 from "path";
16340
16700
  var FEATURE_PREFIX = "feature/";
16341
16701
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
16342
16702
  function readEnv(projectDir) {
16343
- const envPath = path29.join(projectDir, ".env");
16703
+ const envPath = path30.join(projectDir, ".env");
16344
16704
  const out = /* @__PURE__ */ new Map();
16345
- if (!fs29.existsSync(envPath)) return out;
16346
- const lines = fs29.readFileSync(envPath, "utf8").split("\n");
16705
+ if (!fs30.existsSync(envPath)) return out;
16706
+ const lines = fs30.readFileSync(envPath, "utf8").split("\n");
16347
16707
  for (const line of lines) {
16348
16708
  const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
16349
16709
  if (m) out.set(m[1], m[2].replace(/^["']|["']$/g, ""));
@@ -16398,9 +16758,9 @@ async function runDoctor(args, deps = {}) {
16398
16758
  }
16399
16759
  for (const wf of ["pr.yml", "merge.yml"]) {
16400
16760
  try {
16401
- const p = path29.join(projectDir, ".github", "workflows", wf);
16402
- if (!fs29.existsSync(p)) continue;
16403
- const body = fs29.readFileSync(p, "utf8");
16761
+ const p = path30.join(projectDir, ".github", "workflows", wf);
16762
+ if (!fs30.existsSync(p)) continue;
16763
+ const body = fs30.readFileSync(p, "utf8");
16404
16764
  if (/lakebase-scm-utils#v\d/.test(body)) {
16405
16765
  findings.push({
16406
16766
  id: "ci-workflow-substrate-pin",
@@ -16615,7 +16975,7 @@ async function fixFinding(args) {
16615
16975
  );
16616
16976
  }
16617
16977
  const sanitized = sanitizeBranchName(branch);
16618
- const envFile = path29.join(args.projectDir, ".env");
16978
+ const envFile = path30.join(args.projectDir, ".env");
16619
16979
  updateEnvConnection({
16620
16980
  envPath: envFile,
16621
16981
  projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
@@ -16720,291 +17080,6 @@ init_esm_shims();
16720
17080
  import * as fs31 from "fs";
16721
17081
  import * as path31 from "path";
16722
17082
  import * as cp6 from "child_process";
16723
-
16724
- // scripts/lakebase/workflow-drift.ts
16725
- init_esm_shims();
16726
- import * as fs30 from "fs";
16727
- import * as path30 from "path";
16728
- function findKitTemplatesDir(start) {
16729
- let dir = start;
16730
- for (let i = 0; i < 6; i++) {
16731
- const candidate = path30.join(
16732
- dir,
16733
- "templates",
16734
- "project",
16735
- "common",
16736
- ".github",
16737
- "workflows"
16738
- );
16739
- if (fs30.existsSync(candidate)) return candidate;
16740
- const parent = path30.dirname(dir);
16741
- if (parent === dir) break;
16742
- dir = parent;
16743
- }
16744
- throw new Error(
16745
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16746
- );
16747
- }
16748
- function unifiedDiff(name, projectContent, templateContent) {
16749
- if (projectContent === templateContent) return "";
16750
- const a = projectContent.split("\n");
16751
- const b = templateContent.split("\n");
16752
- const out = [`--- project/${name}`, `+++ template/${name}`];
16753
- const max = Math.max(a.length, b.length);
16754
- for (let i = 0; i < max; i++) {
16755
- const av = a[i];
16756
- const bv = b[i];
16757
- if (av === bv) continue;
16758
- if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16759
- if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16760
- }
16761
- return out.join("\n");
16762
- }
16763
- function detectWorkflowDrift(args) {
16764
- const projectWorkflowsDir = path30.join(
16765
- args.projectDir,
16766
- ".github",
16767
- "workflows"
16768
- );
16769
- const here = path30.dirname(new URL(import.meta.url).pathname);
16770
- const kitWorkflowsDir = args.kitDir ? path30.join(
16771
- args.kitDir,
16772
- "templates",
16773
- "project",
16774
- "common",
16775
- ".github",
16776
- "workflows"
16777
- ) : findKitTemplatesDir(here);
16778
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16779
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16780
- const version = readKitVersion(kitWorkflowsDir);
16781
- const seen = /* @__PURE__ */ new Set();
16782
- const files = [];
16783
- for (const name of templateFiles) {
16784
- seen.add(name);
16785
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16786
- const templatePath = path30.join(kitWorkflowsDir, name);
16787
- if (!fs30.existsSync(projectPath2)) {
16788
- files.push({ name, status: "missing" });
16789
- continue;
16790
- }
16791
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16792
- const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
16793
- if (projectContent === templateContent) {
16794
- files.push({ name, status: "unchanged" });
16795
- } else {
16796
- files.push({
16797
- name,
16798
- status: "drifted",
16799
- diff: unifiedDiff(name, projectContent, templateContent)
16800
- });
16801
- }
16802
- }
16803
- for (const name of projectFiles) {
16804
- if (seen.has(name)) continue;
16805
- files.push({ name, status: "extra" });
16806
- }
16807
- const order = {
16808
- drifted: 0,
16809
- missing: 1,
16810
- extra: 2,
16811
- unchanged: 3
16812
- };
16813
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16814
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16815
- return {
16816
- overall: hasDrift ? "drift" : "ok",
16817
- files
16818
- };
16819
- }
16820
- function readKitVersion(kitWorkflowsDir) {
16821
- let dir = kitWorkflowsDir;
16822
- for (let i = 0; i < 5; i++) {
16823
- dir = path30.dirname(dir);
16824
- }
16825
- try {
16826
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
16827
- const pkg = JSON.parse(raw);
16828
- return typeof pkg.version === "string" ? pkg.version : "unknown";
16829
- } catch {
16830
- return "unknown";
16831
- }
16832
- }
16833
- function applyPlaceholders(content, version) {
16834
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
16835
- }
16836
- function applyCommandPlaceholders(content, version) {
16837
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
16838
- }
16839
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16840
- function findKitCommandsDir(start) {
16841
- let dir = start;
16842
- for (let i = 0; i < 6; i++) {
16843
- const candidate = path30.join(
16844
- dir,
16845
- "templates",
16846
- "project",
16847
- "common",
16848
- ".claude",
16849
- "commands"
16850
- );
16851
- if (fs30.existsSync(candidate)) return candidate;
16852
- const parent = path30.dirname(dir);
16853
- if (parent === dir) break;
16854
- dir = parent;
16855
- }
16856
- throw new Error(
16857
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16858
- );
16859
- }
16860
- function parsePinnedVersion(content) {
16861
- const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16862
- return m ? m[1] : void 0;
16863
- }
16864
- function detectCommandDrift(args) {
16865
- const projectCommandsDir = path30.join(args.projectDir, ".claude", "commands");
16866
- const here = path30.dirname(new URL(import.meta.url).pathname);
16867
- const kitCommandsDir = args.kitDir ? path30.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16868
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16869
- const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16870
- const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16871
- const seen = /* @__PURE__ */ new Set();
16872
- const files = [];
16873
- for (const name of templateFiles) {
16874
- seen.add(name);
16875
- const projectPath2 = path30.join(projectCommandsDir, name);
16876
- const templatePath = path30.join(kitCommandsDir, name);
16877
- const templateRaw = fs30.readFileSync(templatePath, "utf8");
16878
- if (!fs30.existsSync(projectPath2)) {
16879
- files.push({ name, status: "missing", kit_version: kitVersion });
16880
- continue;
16881
- }
16882
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16883
- const pinned = parsePinnedVersion(projectContent);
16884
- const versionForCompare = pinned ?? kitVersion;
16885
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16886
- if (projectContent === templateContent) {
16887
- files.push({
16888
- name,
16889
- status: "unchanged",
16890
- pinned_version: pinned,
16891
- kit_version: kitVersion
16892
- });
16893
- } else {
16894
- files.push({
16895
- name,
16896
- status: "drifted",
16897
- pinned_version: pinned,
16898
- kit_version: kitVersion,
16899
- diff: unifiedDiff(name, projectContent, templateContent)
16900
- });
16901
- }
16902
- }
16903
- for (const name of projectFiles) {
16904
- if (seen.has(name)) continue;
16905
- files.push({ name, status: "extra", kit_version: kitVersion });
16906
- }
16907
- const order = {
16908
- drifted: 0,
16909
- missing: 1,
16910
- extra: 2,
16911
- unchanged: 3
16912
- };
16913
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16914
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16915
- return { overall: hasDrift ? "drift" : "ok", files };
16916
- }
16917
- function readKitVersionFromCommandsDir(kitCommandsDir) {
16918
- let dir = kitCommandsDir;
16919
- for (let i = 0; i < 5; i++) {
16920
- dir = path30.dirname(dir);
16921
- }
16922
- try {
16923
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
16924
- const pkg = JSON.parse(raw);
16925
- return typeof pkg.version === "string" ? pkg.version : "unknown";
16926
- } catch {
16927
- return "unknown";
16928
- }
16929
- }
16930
- function detectScaffoldedDrift(args) {
16931
- const workflows = detectWorkflowDrift(args);
16932
- const commands = detectCommandDrift(args);
16933
- return {
16934
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16935
- workflows,
16936
- commands
16937
- };
16938
- }
16939
- function updateWorkflows(args) {
16940
- const projectWorkflowsDir = path30.join(
16941
- args.projectDir,
16942
- ".github",
16943
- "workflows"
16944
- );
16945
- const here = path30.dirname(new URL(import.meta.url).pathname);
16946
- const kitWorkflowsDir = args.kitDir ? path30.join(
16947
- args.kitDir,
16948
- "templates",
16949
- "project",
16950
- "common",
16951
- ".github",
16952
- "workflows"
16953
- ) : findKitTemplatesDir(here);
16954
- const substitute = args.substitute !== false;
16955
- const dryRun = args.dryRun === true;
16956
- const pruneExtras = args.pruneExtras === true;
16957
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16958
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16959
- if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
16960
- fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
16961
- }
16962
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16963
- const seen = /* @__PURE__ */ new Set();
16964
- const files = [];
16965
- for (const name of templateFiles) {
16966
- seen.add(name);
16967
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16968
- const templatePath = path30.join(kitWorkflowsDir, name);
16969
- const templateRaw = fs30.readFileSync(templatePath, "utf-8");
16970
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16971
- const existed = fs30.existsSync(projectPath2);
16972
- const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
16973
- let outcome;
16974
- if (!existed) {
16975
- outcome = "added";
16976
- } else if (current === desired) {
16977
- outcome = "unchanged";
16978
- } else {
16979
- outcome = "updated";
16980
- }
16981
- if (!dryRun && outcome !== "unchanged") {
16982
- fs30.writeFileSync(projectPath2, desired);
16983
- }
16984
- files.push({ name, outcome });
16985
- }
16986
- if (pruneExtras) {
16987
- for (const name of projectFiles) {
16988
- if (seen.has(name)) continue;
16989
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16990
- if (!dryRun) {
16991
- fs30.unlinkSync(projectPath2);
16992
- }
16993
- files.push({ name, outcome: "removed" });
16994
- }
16995
- }
16996
- const order = {
16997
- added: 0,
16998
- updated: 1,
16999
- removed: 2,
17000
- unchanged: 3
17001
- };
17002
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
17003
- const changed = files.some((f) => f.outcome !== "unchanged");
17004
- return { files, changed };
17005
- }
17006
-
17007
- // scripts/lakebase/doctor.ts
17008
17083
  function readEnvFile(projectDir) {
17009
17084
  const envPath = path31.join(projectDir, ".env");
17010
17085
  if (!fs31.existsSync(envPath)) return {};
@@ -17437,7 +17512,7 @@ function checkWorkflowDrift(projectDir) {
17437
17512
  status: "warn",
17438
17513
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
17439
17514
  detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
17440
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
17515
+ hint: "Refresh with updateWorkflows (or the lakebase_workflow_drift MCP tool). A stale merge.yml calling 'lakebase-schema-migrate apply' (not 'apply-tier') makes the promote's migrate-target fail on the tier guard; the scm-merge promote also self-heals drifted workflows onto the PR head before merging."
17441
17516
  };
17442
17517
  } catch (err) {
17443
17518
  return {
@@ -17745,6 +17820,7 @@ export {
17745
17820
  createUatPairedBranch,
17746
17821
  cutBackup,
17747
17822
  databricksAuthPrereqMessage,
17823
+ defaultRefreshPromoteWorkflows,
17748
17824
  deleteAppEndpoint,
17749
17825
  deleteBranch,
17750
17826
  deleteLakebaseProject,