@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
@@ -78993,6 +78993,7 @@ __export(lakebase_exports, {
78993
78993
  createUatPairedBranch: () => createUatPairedBranch,
78994
78994
  cutBackup: () => cutBackup,
78995
78995
  databricksAuthPrereqMessage: () => databricksAuthPrereqMessage,
78996
+ defaultRefreshPromoteWorkflows: () => defaultRefreshPromoteWorkflows,
78996
78997
  deleteAppEndpoint: () => deleteAppEndpoint,
78997
78998
  deleteBranch: () => deleteBranch,
78998
78999
  deleteLakebaseProject: () => deleteLakebaseProject,
@@ -81576,6 +81577,7 @@ var RUNTIME_ARTIFACT_IGNORE = [
81576
81577
  var DEFAULT_ENDPOINT = "primary";
81577
81578
  var CONSORT_APPLICATION_NAME = "consort";
81578
81579
  var SCM_UTILS_APPLICATION_NAME = "scm-utils";
81580
+ var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
81579
81581
  var CONSORT_VERSION_ENV = "CONSORT_VERSION";
81580
81582
 
81581
81583
  // scripts/lakebase/self-version.ts
@@ -81587,8 +81589,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
81587
81589
  var cached;
81588
81590
  function substrateSelfVersion() {
81589
81591
  if (cached !== void 0) return cached;
81590
- if ("0.2.44".length > 0) {
81591
- cached = "0.2.44";
81592
+ if ("0.2.46".length > 0) {
81593
+ cached = "0.2.46";
81592
81594
  return cached;
81593
81595
  }
81594
81596
  cached = "unknown";
@@ -81616,9 +81618,17 @@ function substrateSelfVersion() {
81616
81618
  }
81617
81619
 
81618
81620
  // scripts/lakebase/get-connection.ts
81621
+ function sanitizeApplicationName(raw) {
81622
+ const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
81623
+ if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
81624
+ return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
81625
+ }
81619
81626
  function connectionApplicationName() {
81627
+ const client = process.env[SCM_CLIENT_ENV]?.trim();
81628
+ if (client) return sanitizeApplicationName(client);
81620
81629
  const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
81621
- return consortVersion ? `${CONSORT_APPLICATION_NAME}/${consortVersion}` : `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
81630
+ if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
81631
+ return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
81622
81632
  }
81623
81633
  async function getConnection(args) {
81624
81634
  const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
@@ -81816,6 +81826,14 @@ async function getCredential(args) {
81816
81826
 
81817
81827
  // scripts/git/status.ts
81818
81828
  init_cjs_shims();
81829
+ async function hasUpstream(args) {
81830
+ try {
81831
+ await exec2("git rev-parse --abbrev-ref @{u}", { cwd: args.cwd });
81832
+ return true;
81833
+ } catch {
81834
+ return false;
81835
+ }
81836
+ }
81819
81837
  async function getAheadBehind(args) {
81820
81838
  const { cwd } = args;
81821
81839
  try {
@@ -94919,117 +94937,460 @@ function pickRunUrl(pr) {
94919
94937
 
94920
94938
  // scripts/lakebase/scm-merge.ts
94921
94939
  init_cjs_shims();
94922
- async function reconcileTierToOrigin(args) {
94923
- const { cwd, tier } = args;
94940
+
94941
+ // scripts/git/sync.ts
94942
+ init_cjs_shims();
94943
+ async function currentBranchName(cwd) {
94924
94944
  try {
94925
- const head = await getCurrentBranch({ cwd });
94926
- if (head === tier) {
94927
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
94928
- } else {
94929
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
94930
- }
94931
- return null;
94932
- } catch (err) {
94933
- 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).`;
94945
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
94946
+ } catch {
94947
+ return "";
94934
94948
  }
94935
94949
  }
94936
- var ScmMergeError = class extends Error {
94937
- constructor(message, code) {
94938
- super(message);
94939
- this.code = code;
94940
- this.name = "ScmMergeError";
94950
+ async function pushCurrentBranchForPr(args) {
94951
+ const remote = args.remote ?? "origin";
94952
+ const branch = await currentBranchName(args.cwd);
94953
+ if (!branch) throw new Error("No current branch");
94954
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
94955
+ if (!upstreamSet) {
94956
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
94957
+ cwd: args.cwd
94958
+ });
94959
+ } else {
94960
+ await exec2("git push", { cwd: args.cwd });
94941
94961
  }
94942
- code;
94943
- };
94944
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
94945
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
94946
- function defaultMigratePredicate(run, mergedAt) {
94947
- if (!run.createdAt) return false;
94948
- const created = Date.parse(run.createdAt);
94949
- if (!Number.isFinite(created)) return false;
94950
- if (run.event && run.event !== "push") return false;
94951
- return created >= mergedAt.getTime() - 5e3;
94952
- }
94953
- function shaMigratePredicate(mergeCommitSha) {
94954
- return (run) => {
94955
- if (run.event && run.event !== "push") return false;
94956
- return !!run.headSha && run.headSha === mergeCommitSha;
94957
- };
94958
94962
  }
94959
- async function mergeFeature(args) {
94960
- const current = readWorkflowState(args.projectDir);
94961
- if (!current) {
94962
- throw new ScmMergeError(
94963
- "No SCM workflow state. wait-ci first.",
94964
- "no-state-file"
94963
+
94964
+ // scripts/lakebase/workflow-drift.ts
94965
+ init_cjs_shims();
94966
+ var fs30 = __toESM(require("fs"), 1);
94967
+ var path28 = __toESM(require("path"), 1);
94968
+ function findKitTemplatesDir(start) {
94969
+ let dir = start;
94970
+ for (let i2 = 0; i2 < 6; i2++) {
94971
+ const candidate = path28.join(
94972
+ dir,
94973
+ "templates",
94974
+ "project",
94975
+ "common",
94976
+ ".github",
94977
+ "workflows"
94965
94978
  );
94979
+ if (fs30.existsSync(candidate)) return candidate;
94980
+ const parent = path28.dirname(dir);
94981
+ if (parent === dir) break;
94982
+ dir = parent;
94966
94983
  }
94967
- if (current.state !== "ci-green") {
94968
- throw new ScmMergeError(
94969
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
94970
- "bad-precondition"
94971
- );
94984
+ throw new Error(
94985
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
94986
+ );
94987
+ }
94988
+ function unifiedDiff(name, projectContent, templateContent) {
94989
+ if (projectContent === templateContent) return "";
94990
+ const a = projectContent.split("\n");
94991
+ const b = templateContent.split("\n");
94992
+ const out = [`--- project/${name}`, `+++ template/${name}`];
94993
+ const max = Math.max(a.length, b.length);
94994
+ for (let i2 = 0; i2 < max; i2++) {
94995
+ const av = a[i2];
94996
+ const bv = b[i2];
94997
+ if (av === bv) continue;
94998
+ if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
94999
+ if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
94972
95000
  }
94973
- if (!current.pr_url) {
94974
- throw new ScmMergeError(
94975
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
94976
- "no-pr-url"
94977
- );
95001
+ return out.join("\n");
95002
+ }
95003
+ function detectWorkflowDrift(args) {
95004
+ const projectWorkflowsDir = path28.join(
95005
+ args.projectDir,
95006
+ ".github",
95007
+ "workflows"
95008
+ );
95009
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95010
+ const kitWorkflowsDir = args.kitDir ? path28.join(
95011
+ args.kitDir,
95012
+ "templates",
95013
+ "project",
95014
+ "common",
95015
+ ".github",
95016
+ "workflows"
95017
+ ) : findKitTemplatesDir(here);
95018
+ const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95019
+ const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95020
+ const version = readKitVersion(kitWorkflowsDir);
95021
+ const seen = /* @__PURE__ */ new Set();
95022
+ const files = [];
95023
+ for (const name of templateFiles) {
95024
+ seen.add(name);
95025
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95026
+ const templatePath = path28.join(kitWorkflowsDir, name);
95027
+ if (!fs30.existsSync(projectPath2)) {
95028
+ files.push({ name, status: "missing" });
95029
+ continue;
95030
+ }
95031
+ const projectContent = fs30.readFileSync(projectPath2, "utf8");
95032
+ const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
95033
+ if (projectContent === templateContent) {
95034
+ files.push({ name, status: "unchanged" });
95035
+ } else {
95036
+ files.push({
95037
+ name,
95038
+ status: "drifted",
95039
+ diff: unifiedDiff(name, projectContent, templateContent)
95040
+ });
95041
+ }
94978
95042
  }
94979
- if (!current.branch || !current.parent_branch) {
94980
- throw new ScmMergeError(
94981
- "ci-green row missing branch / parent_branch; refusing to merge.",
94982
- "bad-precondition"
94983
- );
95043
+ for (const name of projectFiles) {
95044
+ if (seen.has(name)) continue;
95045
+ files.push({ name, status: "extra" });
94984
95046
  }
94985
- const ownerRepo = await getOwnerRepo(args.projectDir);
94986
- if (!ownerRepo) {
94987
- throw new ScmMergeError(
94988
- "No GitHub remote found at origin.",
94989
- "no-github-remote"
94990
- );
95047
+ const order = {
95048
+ drifted: 0,
95049
+ missing: 1,
95050
+ extra: 2,
95051
+ unchanged: 3
95052
+ };
95053
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95054
+ const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95055
+ return {
95056
+ overall: hasDrift ? "drift" : "ok",
95057
+ files
95058
+ };
95059
+ }
95060
+ function readKitVersion(kitWorkflowsDir) {
95061
+ let dir = kitWorkflowsDir;
95062
+ for (let i2 = 0; i2 < 5; i2++) {
95063
+ dir = path28.dirname(dir);
94991
95064
  }
94992
- const pullNumber = extractPullNumber(current.pr_url);
94993
- if (!pullNumber) {
94994
- throw new ScmMergeError(
94995
- `Could not extract PR number from URL: ${current.pr_url}`,
94996
- "bad-pr-url"
95065
+ try {
95066
+ const raw = fs30.readFileSync(path28.join(dir, "package.json"), "utf-8");
95067
+ const pkg = JSON.parse(raw);
95068
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
95069
+ } catch {
95070
+ return "unknown";
95071
+ }
95072
+ }
95073
+ function applyPlaceholders(content, version) {
95074
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
95075
+ }
95076
+ function applyCommandPlaceholders(content, version) {
95077
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
95078
+ }
95079
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
95080
+ function findKitCommandsDir(start) {
95081
+ let dir = start;
95082
+ for (let i2 = 0; i2 < 6; i2++) {
95083
+ const candidate = path28.join(
95084
+ dir,
95085
+ "templates",
95086
+ "project",
95087
+ "common",
95088
+ ".claude",
95089
+ "commands"
94997
95090
  );
95091
+ if (fs30.existsSync(candidate)) return candidate;
95092
+ const parent = path28.dirname(dir);
95093
+ if (parent === dir) break;
95094
+ dir = parent;
94998
95095
  }
94999
- const instance = args.instance ?? current.project_id;
95000
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
95001
- const wantMigrate = args.waitMigrate !== false;
95002
- let authVerified = false;
95003
- if (wantMigrate && args.verifyMigrateAuth) {
95004
- const probe = await args.verifyMigrateAuth();
95005
- if (!probe.ok) {
95006
- throw new ScmMergeError(
95007
- `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.`,
95008
- "migrate-auth"
95009
- );
95096
+ throw new Error(
95097
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
95098
+ );
95099
+ }
95100
+ function parsePinnedVersion(content) {
95101
+ const m2 = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
95102
+ return m2 ? m2[1] : void 0;
95103
+ }
95104
+ function detectCommandDrift(args) {
95105
+ const projectCommandsDir = path28.join(args.projectDir, ".claude", "commands");
95106
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95107
+ const kitCommandsDir = args.kitDir ? path28.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
95108
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
95109
+ const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95110
+ const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95111
+ const seen = /* @__PURE__ */ new Set();
95112
+ const files = [];
95113
+ for (const name of templateFiles) {
95114
+ seen.add(name);
95115
+ const projectPath2 = path28.join(projectCommandsDir, name);
95116
+ const templatePath = path28.join(kitCommandsDir, name);
95117
+ const templateRaw = fs30.readFileSync(templatePath, "utf8");
95118
+ if (!fs30.existsSync(projectPath2)) {
95119
+ files.push({ name, status: "missing", kit_version: kitVersion });
95120
+ continue;
95121
+ }
95122
+ const projectContent = fs30.readFileSync(projectPath2, "utf8");
95123
+ const pinned = parsePinnedVersion(projectContent);
95124
+ const versionForCompare = pinned ?? kitVersion;
95125
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
95126
+ if (projectContent === templateContent) {
95127
+ files.push({
95128
+ name,
95129
+ status: "unchanged",
95130
+ pinned_version: pinned,
95131
+ kit_version: kitVersion
95132
+ });
95133
+ } else {
95134
+ files.push({
95135
+ name,
95136
+ status: "drifted",
95137
+ pinned_version: pinned,
95138
+ kit_version: kitVersion,
95139
+ diff: unifiedDiff(name, projectContent, templateContent)
95140
+ });
95010
95141
  }
95011
- authVerified = true;
95012
95142
  }
95013
- let paired;
95014
- try {
95015
- paired = await mergePairedPullRequest({
95016
- ownerRepo,
95017
- pullNumber,
95018
- lakebaseInstance: instance,
95019
- method: args.method ?? "squash"
95020
- });
95021
- } catch (err) {
95022
- throw new ScmMergeError(
95023
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
95024
- "merge-failed"
95025
- );
95143
+ for (const name of projectFiles) {
95144
+ if (seen.has(name)) continue;
95145
+ files.push({ name, status: "extra", kit_version: kitVersion });
95026
95146
  }
95027
- const warnings = [...paired.warnings];
95028
- let localBranchDeleted = false;
95029
- let headAfter = current.branch;
95030
- if (!args.skipLocalCleanup) {
95031
- const switchTo = args.switchTo ?? gitBase;
95032
- const head = await getCurrentBranch({ cwd: args.projectDir });
95147
+ const order = {
95148
+ drifted: 0,
95149
+ missing: 1,
95150
+ extra: 2,
95151
+ unchanged: 3
95152
+ };
95153
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95154
+ const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95155
+ return { overall: hasDrift ? "drift" : "ok", files };
95156
+ }
95157
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
95158
+ let dir = kitCommandsDir;
95159
+ for (let i2 = 0; i2 < 5; i2++) {
95160
+ dir = path28.dirname(dir);
95161
+ }
95162
+ try {
95163
+ const raw = fs30.readFileSync(path28.join(dir, "package.json"), "utf-8");
95164
+ const pkg = JSON.parse(raw);
95165
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
95166
+ } catch {
95167
+ return "unknown";
95168
+ }
95169
+ }
95170
+ function detectScaffoldedDrift(args) {
95171
+ const workflows = detectWorkflowDrift(args);
95172
+ const commands = detectCommandDrift(args);
95173
+ return {
95174
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
95175
+ workflows,
95176
+ commands
95177
+ };
95178
+ }
95179
+ function updateWorkflows(args) {
95180
+ const projectWorkflowsDir = path28.join(
95181
+ args.projectDir,
95182
+ ".github",
95183
+ "workflows"
95184
+ );
95185
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95186
+ const kitWorkflowsDir = args.kitDir ? path28.join(
95187
+ args.kitDir,
95188
+ "templates",
95189
+ "project",
95190
+ "common",
95191
+ ".github",
95192
+ "workflows"
95193
+ ) : findKitTemplatesDir(here);
95194
+ const substitute = args.substitute !== false;
95195
+ const dryRun = args.dryRun === true;
95196
+ const pruneExtras = args.pruneExtras === true;
95197
+ const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95198
+ const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95199
+ if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
95200
+ fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
95201
+ }
95202
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
95203
+ const seen = /* @__PURE__ */ new Set();
95204
+ const files = [];
95205
+ for (const name of templateFiles) {
95206
+ seen.add(name);
95207
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95208
+ const templatePath = path28.join(kitWorkflowsDir, name);
95209
+ const templateRaw = fs30.readFileSync(templatePath, "utf-8");
95210
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
95211
+ const existed = fs30.existsSync(projectPath2);
95212
+ const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
95213
+ let outcome;
95214
+ if (!existed) {
95215
+ outcome = "added";
95216
+ } else if (current === desired) {
95217
+ outcome = "unchanged";
95218
+ } else {
95219
+ outcome = "updated";
95220
+ }
95221
+ if (!dryRun && outcome !== "unchanged") {
95222
+ fs30.writeFileSync(projectPath2, desired);
95223
+ }
95224
+ files.push({ name, outcome });
95225
+ }
95226
+ if (pruneExtras) {
95227
+ for (const name of projectFiles) {
95228
+ if (seen.has(name)) continue;
95229
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95230
+ if (!dryRun) {
95231
+ fs30.unlinkSync(projectPath2);
95232
+ }
95233
+ files.push({ name, outcome: "removed" });
95234
+ }
95235
+ }
95236
+ const order = {
95237
+ added: 0,
95238
+ updated: 1,
95239
+ removed: 2,
95240
+ unchanged: 3
95241
+ };
95242
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
95243
+ const changed = files.some((f3) => f3.outcome !== "unchanged");
95244
+ return { files, changed };
95245
+ }
95246
+
95247
+ // scripts/lakebase/scm-merge.ts
95248
+ async function reconcileTierToOrigin(args) {
95249
+ const { cwd, tier } = args;
95250
+ try {
95251
+ const head = await getCurrentBranch({ cwd });
95252
+ if (head === tier) {
95253
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
95254
+ } else {
95255
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
95256
+ }
95257
+ return null;
95258
+ } catch (err) {
95259
+ 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).`;
95260
+ }
95261
+ }
95262
+ var ScmMergeError = class extends Error {
95263
+ constructor(message, code) {
95264
+ super(message);
95265
+ this.code = code;
95266
+ this.name = "ScmMergeError";
95267
+ }
95268
+ code;
95269
+ };
95270
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
95271
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
95272
+ function defaultMigratePredicate(run, mergedAt) {
95273
+ if (!run.createdAt) return false;
95274
+ const created = Date.parse(run.createdAt);
95275
+ if (!Number.isFinite(created)) return false;
95276
+ if (run.event && run.event !== "push") return false;
95277
+ return created >= mergedAt.getTime() - 5e3;
95278
+ }
95279
+ function shaMigratePredicate(mergeCommitSha) {
95280
+ return (run) => {
95281
+ if (run.event && run.event !== "push") return false;
95282
+ return !!run.headSha && run.headSha === mergeCommitSha;
95283
+ };
95284
+ }
95285
+ async function defaultRefreshPromoteWorkflows(projectDir) {
95286
+ const drift = detectWorkflowDrift({ projectDir });
95287
+ const touched = drift.files.filter((f3) => f3.status === "drifted").map((f3) => f3.name);
95288
+ if (touched.length === 0) return { refreshed: false };
95289
+ const result = updateWorkflows({ projectDir });
95290
+ if (!result.changed) return { refreshed: false };
95291
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
95292
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
95293
+ cwd: projectDir,
95294
+ timeout: 1e4
95295
+ });
95296
+ if (!staged.trim()) return { refreshed: false };
95297
+ await exec2(
95298
+ `git commit -m ${shellEscape2(
95299
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
95300
+ )} -- .github/workflows`,
95301
+ { cwd: projectDir, timeout: 15e3 }
95302
+ );
95303
+ await pushCurrentBranchForPr({ cwd: projectDir });
95304
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
95305
+ }
95306
+ async function mergeFeature(args) {
95307
+ const current = readWorkflowState(args.projectDir);
95308
+ if (!current) {
95309
+ throw new ScmMergeError(
95310
+ "No SCM workflow state. wait-ci first.",
95311
+ "no-state-file"
95312
+ );
95313
+ }
95314
+ if (current.state !== "ci-green") {
95315
+ throw new ScmMergeError(
95316
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
95317
+ "bad-precondition"
95318
+ );
95319
+ }
95320
+ if (!current.pr_url) {
95321
+ throw new ScmMergeError(
95322
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
95323
+ "no-pr-url"
95324
+ );
95325
+ }
95326
+ if (!current.branch || !current.parent_branch) {
95327
+ throw new ScmMergeError(
95328
+ "ci-green row missing branch / parent_branch; refusing to merge.",
95329
+ "bad-precondition"
95330
+ );
95331
+ }
95332
+ const ownerRepo = await getOwnerRepo(args.projectDir);
95333
+ if (!ownerRepo) {
95334
+ throw new ScmMergeError(
95335
+ "No GitHub remote found at origin.",
95336
+ "no-github-remote"
95337
+ );
95338
+ }
95339
+ const pullNumber = extractPullNumber(current.pr_url);
95340
+ if (!pullNumber) {
95341
+ throw new ScmMergeError(
95342
+ `Could not extract PR number from URL: ${current.pr_url}`,
95343
+ "bad-pr-url"
95344
+ );
95345
+ }
95346
+ const instance = args.instance ?? current.project_id;
95347
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
95348
+ const wantMigrate = args.waitMigrate !== false;
95349
+ let authVerified = false;
95350
+ if (wantMigrate && args.verifyMigrateAuth) {
95351
+ const probe = await args.verifyMigrateAuth();
95352
+ if (!probe.ok) {
95353
+ throw new ScmMergeError(
95354
+ `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.`,
95355
+ "migrate-auth"
95356
+ );
95357
+ }
95358
+ authVerified = true;
95359
+ }
95360
+ const preflightNotes = [];
95361
+ {
95362
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
95363
+ try {
95364
+ const r2 = await refresh();
95365
+ if (r2.refreshed) {
95366
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r2.detail ? ` (${r2.detail})` : ""}.`);
95367
+ }
95368
+ } catch (err) {
95369
+ preflightNotes.push(
95370
+ `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').`
95371
+ );
95372
+ }
95373
+ }
95374
+ let paired;
95375
+ try {
95376
+ paired = await mergePairedPullRequest({
95377
+ ownerRepo,
95378
+ pullNumber,
95379
+ lakebaseInstance: instance,
95380
+ method: args.method ?? "squash"
95381
+ });
95382
+ } catch (err) {
95383
+ throw new ScmMergeError(
95384
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
95385
+ "merge-failed"
95386
+ );
95387
+ }
95388
+ const warnings = [...preflightNotes, ...paired.warnings];
95389
+ let localBranchDeleted = false;
95390
+ let headAfter = current.branch;
95391
+ if (!args.skipLocalCleanup) {
95392
+ const switchTo = args.switchTo ?? gitBase;
95393
+ const head = await getCurrentBranch({ cwd: args.projectDir });
95033
95394
  if (head === current.branch) {
95034
95395
  try {
95035
95396
  await exec2(`git checkout ${shellEscape2(switchTo)}`, {
@@ -95221,7 +95582,7 @@ init_cjs_shims();
95221
95582
 
95222
95583
  // scripts/git/branches.ts
95223
95584
  init_cjs_shims();
95224
- async function currentBranchName(cwd) {
95585
+ async function currentBranchName2(cwd) {
95225
95586
  try {
95226
95587
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
95227
95588
  } catch {
@@ -95240,7 +95601,7 @@ async function listLocalBranches(args) {
95240
95601
  return [];
95241
95602
  }
95242
95603
  if (!raw) return [];
95243
- const current = await currentBranchName(cwd);
95604
+ const current = await currentBranchName2(cwd);
95244
95605
  return raw.split("\n").filter(Boolean).map((line) => {
95245
95606
  const [name, tracking, trackInfo] = line.split("|");
95246
95607
  let ahead = 0;
@@ -95405,15 +95766,15 @@ function parentForTopology(t2, defaultLeaf) {
95405
95766
 
95406
95767
  // scripts/lakebase/scm-doctor.ts
95407
95768
  init_cjs_shims();
95408
- var fs30 = __toESM(require("fs"), 1);
95409
- var path28 = __toESM(require("path"), 1);
95769
+ var fs31 = __toESM(require("fs"), 1);
95770
+ var path29 = __toESM(require("path"), 1);
95410
95771
  var FEATURE_PREFIX = "feature/";
95411
95772
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
95412
95773
  function readEnv(projectDir) {
95413
- const envPath = path28.join(projectDir, ".env");
95774
+ const envPath = path29.join(projectDir, ".env");
95414
95775
  const out = /* @__PURE__ */ new Map();
95415
- if (!fs30.existsSync(envPath)) return out;
95416
- const lines = fs30.readFileSync(envPath, "utf8").split("\n");
95776
+ if (!fs31.existsSync(envPath)) return out;
95777
+ const lines = fs31.readFileSync(envPath, "utf8").split("\n");
95417
95778
  for (const line of lines) {
95418
95779
  const m2 = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
95419
95780
  if (m2) out.set(m2[1], m2[2].replace(/^["']|["']$/g, ""));
@@ -95468,9 +95829,9 @@ async function runDoctor(args, deps = {}) {
95468
95829
  }
95469
95830
  for (const wf of ["pr.yml", "merge.yml"]) {
95470
95831
  try {
95471
- const p = path28.join(projectDir, ".github", "workflows", wf);
95472
- if (!fs30.existsSync(p)) continue;
95473
- const body = fs30.readFileSync(p, "utf8");
95832
+ const p = path29.join(projectDir, ".github", "workflows", wf);
95833
+ if (!fs31.existsSync(p)) continue;
95834
+ const body = fs31.readFileSync(p, "utf8");
95474
95835
  if (/lakebase-scm-utils#v\d/.test(body)) {
95475
95836
  findings.push({
95476
95837
  id: "ci-workflow-substrate-pin",
@@ -95685,7 +96046,7 @@ async function fixFinding(args) {
95685
96046
  );
95686
96047
  }
95687
96048
  const sanitized = sanitizeBranchName(branch);
95688
- const envFile = path28.join(args.projectDir, ".env");
96049
+ const envFile = path29.join(args.projectDir, ".env");
95689
96050
  updateEnvConnection({
95690
96051
  envPath: envFile,
95691
96052
  projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
@@ -95790,291 +96151,6 @@ init_cjs_shims();
95790
96151
  var fs32 = __toESM(require("fs"), 1);
95791
96152
  var path30 = __toESM(require("path"), 1);
95792
96153
  var cp6 = __toESM(require("child_process"), 1);
95793
-
95794
- // scripts/lakebase/workflow-drift.ts
95795
- init_cjs_shims();
95796
- var fs31 = __toESM(require("fs"), 1);
95797
- var path29 = __toESM(require("path"), 1);
95798
- function findKitTemplatesDir(start) {
95799
- let dir = start;
95800
- for (let i2 = 0; i2 < 6; i2++) {
95801
- const candidate = path29.join(
95802
- dir,
95803
- "templates",
95804
- "project",
95805
- "common",
95806
- ".github",
95807
- "workflows"
95808
- );
95809
- if (fs31.existsSync(candidate)) return candidate;
95810
- const parent = path29.dirname(dir);
95811
- if (parent === dir) break;
95812
- dir = parent;
95813
- }
95814
- throw new Error(
95815
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
95816
- );
95817
- }
95818
- function unifiedDiff(name, projectContent, templateContent) {
95819
- if (projectContent === templateContent) return "";
95820
- const a = projectContent.split("\n");
95821
- const b = templateContent.split("\n");
95822
- const out = [`--- project/${name}`, `+++ template/${name}`];
95823
- const max = Math.max(a.length, b.length);
95824
- for (let i2 = 0; i2 < max; i2++) {
95825
- const av = a[i2];
95826
- const bv = b[i2];
95827
- if (av === bv) continue;
95828
- if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
95829
- if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
95830
- }
95831
- return out.join("\n");
95832
- }
95833
- function detectWorkflowDrift(args) {
95834
- const projectWorkflowsDir = path29.join(
95835
- args.projectDir,
95836
- ".github",
95837
- "workflows"
95838
- );
95839
- const here = path29.dirname(new URL(importMetaUrl).pathname);
95840
- const kitWorkflowsDir = args.kitDir ? path29.join(
95841
- args.kitDir,
95842
- "templates",
95843
- "project",
95844
- "common",
95845
- ".github",
95846
- "workflows"
95847
- ) : findKitTemplatesDir(here);
95848
- const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95849
- const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95850
- const version = readKitVersion(kitWorkflowsDir);
95851
- const seen = /* @__PURE__ */ new Set();
95852
- const files = [];
95853
- for (const name of templateFiles) {
95854
- seen.add(name);
95855
- const projectPath2 = path29.join(projectWorkflowsDir, name);
95856
- const templatePath = path29.join(kitWorkflowsDir, name);
95857
- if (!fs31.existsSync(projectPath2)) {
95858
- files.push({ name, status: "missing" });
95859
- continue;
95860
- }
95861
- const projectContent = fs31.readFileSync(projectPath2, "utf8");
95862
- const templateContent = applyPlaceholders(fs31.readFileSync(templatePath, "utf8"), version);
95863
- if (projectContent === templateContent) {
95864
- files.push({ name, status: "unchanged" });
95865
- } else {
95866
- files.push({
95867
- name,
95868
- status: "drifted",
95869
- diff: unifiedDiff(name, projectContent, templateContent)
95870
- });
95871
- }
95872
- }
95873
- for (const name of projectFiles) {
95874
- if (seen.has(name)) continue;
95875
- files.push({ name, status: "extra" });
95876
- }
95877
- const order = {
95878
- drifted: 0,
95879
- missing: 1,
95880
- extra: 2,
95881
- unchanged: 3
95882
- };
95883
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95884
- const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95885
- return {
95886
- overall: hasDrift ? "drift" : "ok",
95887
- files
95888
- };
95889
- }
95890
- function readKitVersion(kitWorkflowsDir) {
95891
- let dir = kitWorkflowsDir;
95892
- for (let i2 = 0; i2 < 5; i2++) {
95893
- dir = path29.dirname(dir);
95894
- }
95895
- try {
95896
- const raw = fs31.readFileSync(path29.join(dir, "package.json"), "utf-8");
95897
- const pkg = JSON.parse(raw);
95898
- return typeof pkg.version === "string" ? pkg.version : "unknown";
95899
- } catch {
95900
- return "unknown";
95901
- }
95902
- }
95903
- function applyPlaceholders(content, version) {
95904
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
95905
- }
95906
- function applyCommandPlaceholders(content, version) {
95907
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
95908
- }
95909
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
95910
- function findKitCommandsDir(start) {
95911
- let dir = start;
95912
- for (let i2 = 0; i2 < 6; i2++) {
95913
- const candidate = path29.join(
95914
- dir,
95915
- "templates",
95916
- "project",
95917
- "common",
95918
- ".claude",
95919
- "commands"
95920
- );
95921
- if (fs31.existsSync(candidate)) return candidate;
95922
- const parent = path29.dirname(dir);
95923
- if (parent === dir) break;
95924
- dir = parent;
95925
- }
95926
- throw new Error(
95927
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
95928
- );
95929
- }
95930
- function parsePinnedVersion(content) {
95931
- const m2 = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
95932
- return m2 ? m2[1] : void 0;
95933
- }
95934
- function detectCommandDrift(args) {
95935
- const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
95936
- const here = path29.dirname(new URL(importMetaUrl).pathname);
95937
- const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
95938
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
95939
- const templateFiles = fs31.existsSync(kitCommandsDir) ? fs31.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95940
- const projectFiles = fs31.existsSync(projectCommandsDir) ? fs31.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95941
- const seen = /* @__PURE__ */ new Set();
95942
- const files = [];
95943
- for (const name of templateFiles) {
95944
- seen.add(name);
95945
- const projectPath2 = path29.join(projectCommandsDir, name);
95946
- const templatePath = path29.join(kitCommandsDir, name);
95947
- const templateRaw = fs31.readFileSync(templatePath, "utf8");
95948
- if (!fs31.existsSync(projectPath2)) {
95949
- files.push({ name, status: "missing", kit_version: kitVersion });
95950
- continue;
95951
- }
95952
- const projectContent = fs31.readFileSync(projectPath2, "utf8");
95953
- const pinned = parsePinnedVersion(projectContent);
95954
- const versionForCompare = pinned ?? kitVersion;
95955
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
95956
- if (projectContent === templateContent) {
95957
- files.push({
95958
- name,
95959
- status: "unchanged",
95960
- pinned_version: pinned,
95961
- kit_version: kitVersion
95962
- });
95963
- } else {
95964
- files.push({
95965
- name,
95966
- status: "drifted",
95967
- pinned_version: pinned,
95968
- kit_version: kitVersion,
95969
- diff: unifiedDiff(name, projectContent, templateContent)
95970
- });
95971
- }
95972
- }
95973
- for (const name of projectFiles) {
95974
- if (seen.has(name)) continue;
95975
- files.push({ name, status: "extra", kit_version: kitVersion });
95976
- }
95977
- const order = {
95978
- drifted: 0,
95979
- missing: 1,
95980
- extra: 2,
95981
- unchanged: 3
95982
- };
95983
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95984
- const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95985
- return { overall: hasDrift ? "drift" : "ok", files };
95986
- }
95987
- function readKitVersionFromCommandsDir(kitCommandsDir) {
95988
- let dir = kitCommandsDir;
95989
- for (let i2 = 0; i2 < 5; i2++) {
95990
- dir = path29.dirname(dir);
95991
- }
95992
- try {
95993
- const raw = fs31.readFileSync(path29.join(dir, "package.json"), "utf-8");
95994
- const pkg = JSON.parse(raw);
95995
- return typeof pkg.version === "string" ? pkg.version : "unknown";
95996
- } catch {
95997
- return "unknown";
95998
- }
95999
- }
96000
- function detectScaffoldedDrift(args) {
96001
- const workflows = detectWorkflowDrift(args);
96002
- const commands = detectCommandDrift(args);
96003
- return {
96004
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
96005
- workflows,
96006
- commands
96007
- };
96008
- }
96009
- function updateWorkflows(args) {
96010
- const projectWorkflowsDir = path29.join(
96011
- args.projectDir,
96012
- ".github",
96013
- "workflows"
96014
- );
96015
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96016
- const kitWorkflowsDir = args.kitDir ? path29.join(
96017
- args.kitDir,
96018
- "templates",
96019
- "project",
96020
- "common",
96021
- ".github",
96022
- "workflows"
96023
- ) : findKitTemplatesDir(here);
96024
- const substitute = args.substitute !== false;
96025
- const dryRun = args.dryRun === true;
96026
- const pruneExtras = args.pruneExtras === true;
96027
- const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96028
- const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96029
- if (!dryRun && templateFiles.length > 0 && !fs31.existsSync(projectWorkflowsDir)) {
96030
- fs31.mkdirSync(projectWorkflowsDir, { recursive: true });
96031
- }
96032
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
96033
- const seen = /* @__PURE__ */ new Set();
96034
- const files = [];
96035
- for (const name of templateFiles) {
96036
- seen.add(name);
96037
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96038
- const templatePath = path29.join(kitWorkflowsDir, name);
96039
- const templateRaw = fs31.readFileSync(templatePath, "utf-8");
96040
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
96041
- const existed = fs31.existsSync(projectPath2);
96042
- const current = existed ? fs31.readFileSync(projectPath2, "utf-8") : "";
96043
- let outcome;
96044
- if (!existed) {
96045
- outcome = "added";
96046
- } else if (current === desired) {
96047
- outcome = "unchanged";
96048
- } else {
96049
- outcome = "updated";
96050
- }
96051
- if (!dryRun && outcome !== "unchanged") {
96052
- fs31.writeFileSync(projectPath2, desired);
96053
- }
96054
- files.push({ name, outcome });
96055
- }
96056
- if (pruneExtras) {
96057
- for (const name of projectFiles) {
96058
- if (seen.has(name)) continue;
96059
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96060
- if (!dryRun) {
96061
- fs31.unlinkSync(projectPath2);
96062
- }
96063
- files.push({ name, outcome: "removed" });
96064
- }
96065
- }
96066
- const order = {
96067
- added: 0,
96068
- updated: 1,
96069
- removed: 2,
96070
- unchanged: 3
96071
- };
96072
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
96073
- const changed = files.some((f3) => f3.outcome !== "unchanged");
96074
- return { files, changed };
96075
- }
96076
-
96077
- // scripts/lakebase/doctor.ts
96078
96154
  function readEnvFile(projectDir) {
96079
96155
  const envPath = path30.join(projectDir, ".env");
96080
96156
  if (!fs32.existsSync(envPath)) return {};
@@ -96507,7 +96583,7 @@ function checkWorkflowDrift(projectDir) {
96507
96583
  status: "warn",
96508
96584
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
96509
96585
  detail: { files: report.files.map((f3) => ({ name: f3.name, status: f3.status })) },
96510
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
96586
+ 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."
96511
96587
  };
96512
96588
  } catch (err) {
96513
96589
  return {
@@ -96816,6 +96892,7 @@ function isUcMissingError(msg) {
96816
96892
  createUatPairedBranch,
96817
96893
  cutBackup,
96818
96894
  databricksAuthPrereqMessage,
96895
+ defaultRefreshPromoteWorkflows,
96819
96896
  deleteAppEndpoint,
96820
96897
  deleteBranch,
96821
96898
  deleteLakebaseProject,