@databricks-solutions/lakebase-scm-utils 0.2.43 → 0.2.45

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 (63) hide show
  1. package/dist/scripts/github/index.cjs +21 -4
  2. package/dist/scripts/github/index.js +21 -4
  3. package/dist/scripts/github/pr.cli.cjs +21 -4
  4. package/dist/scripts/github/pr.cli.js +21 -4
  5. package/dist/scripts/index.cjs +1224 -1157
  6. package/dist/scripts/index.d.cts +1 -1
  7. package/dist/scripts/index.d.ts +1 -1
  8. package/dist/scripts/index.js +1223 -1157
  9. package/dist/scripts/lakebase/branch.cli.cjs +21 -4
  10. package/dist/scripts/lakebase/branch.cli.js +21 -4
  11. package/dist/scripts/lakebase/ci-app-endpoint.cli.cjs +21 -4
  12. package/dist/scripts/lakebase/ci-app-endpoint.cli.js +21 -4
  13. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +23 -6
  14. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +23 -6
  15. package/dist/scripts/lakebase/collapse-heads.cli.cjs +23 -6
  16. package/dist/scripts/lakebase/collapse-heads.cli.js +23 -6
  17. package/dist/scripts/lakebase/cut-backup.cli.cjs +21 -4
  18. package/dist/scripts/lakebase/cut-backup.cli.js +21 -4
  19. package/dist/scripts/lakebase/cut-tier.cli.cjs +21 -4
  20. package/dist/scripts/lakebase/cut-tier.cli.js +21 -4
  21. package/dist/scripts/lakebase/detect-language.cli.cjs +23 -6
  22. package/dist/scripts/lakebase/detect-language.cli.js +23 -6
  23. package/dist/scripts/lakebase/doctor.cli.cjs +24 -7
  24. package/dist/scripts/lakebase/doctor.cli.js +24 -7
  25. package/dist/scripts/lakebase/get-connection.cli.cjs +23 -6
  26. package/dist/scripts/lakebase/get-connection.cli.js +23 -6
  27. package/dist/scripts/lakebase/index.cjs +489 -391
  28. package/dist/scripts/lakebase/index.d.cts +33 -1
  29. package/dist/scripts/lakebase/index.d.ts +33 -1
  30. package/dist/scripts/lakebase/index.js +488 -391
  31. package/dist/scripts/lakebase/infra-runner.cli.cjs +23 -6
  32. package/dist/scripts/lakebase/infra-runner.cli.js +23 -6
  33. package/dist/scripts/lakebase/new-migration.cli.cjs +23 -6
  34. package/dist/scripts/lakebase/new-migration.cli.js +23 -6
  35. package/dist/scripts/lakebase/schema-diff.cli.cjs +23 -6
  36. package/dist/scripts/lakebase/schema-diff.cli.js +23 -6
  37. package/dist/scripts/lakebase/schema-migrate.cli.cjs +23 -6
  38. package/dist/scripts/lakebase/schema-migrate.cli.js +23 -6
  39. package/dist/scripts/lakebase/scm-abandon-feature.cli.cjs +21 -4
  40. package/dist/scripts/lakebase/scm-abandon-feature.cli.js +21 -4
  41. package/dist/scripts/lakebase/scm-adopt-state.cli.cjs +21 -4
  42. package/dist/scripts/lakebase/scm-adopt-state.cli.js +21 -4
  43. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +23 -6
  44. package/dist/scripts/lakebase/scm-claim-feature.cli.js +23 -6
  45. package/dist/scripts/lakebase/scm-cleanup.cli.cjs +21 -4
  46. package/dist/scripts/lakebase/scm-cleanup.cli.js +21 -4
  47. package/dist/scripts/lakebase/scm-doctor.cli.cjs +23 -6
  48. package/dist/scripts/lakebase/scm-doctor.cli.js +23 -6
  49. package/dist/scripts/lakebase/scm-merge.cli.cjs +2305 -2037
  50. package/dist/scripts/lakebase/scm-merge.cli.js +360 -92
  51. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +23 -6
  52. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +23 -6
  53. package/dist/scripts/lakebase/scm-recover-orphans.cli.cjs +21 -4
  54. package/dist/scripts/lakebase/scm-recover-orphans.cli.js +21 -4
  55. package/dist/scripts/util/index.cjs +23 -6
  56. package/dist/scripts/util/index.js +23 -6
  57. package/package.json +1 -1
  58. package/scripts/lakebase/databricks-cli.ts +44 -4
  59. package/scripts/lakebase/doctor.ts +1 -1
  60. package/scripts/lakebase/runner-setup.ts +18 -2
  61. package/scripts/lakebase/scm-merge.ts +80 -1
  62. package/scripts/lakebase/secret-auth.ts +9 -2
  63. package/templates/project/common/scripts/run-tests.sh +10 -0
@@ -1784,16 +1784,33 @@ stdout: ${stdout}` : killed || signal ? `
1784
1784
  }
1785
1785
  async function runDatabricks(args, opts = {}) {
1786
1786
  const { argv, env, profile } = buildInvocation(args, opts);
1787
+ const timeout = opts.timeout ?? KIT_TIMEOUTS.cliDefault;
1787
1788
  try {
1788
- const { stdout } = await execFileP("databricks", argv, {
1789
- env,
1790
- timeout: opts.timeout ?? KIT_TIMEOUTS.cliDefault
1791
- });
1789
+ if (opts.input !== void 0) {
1790
+ return await execDatabricksWithStdin(argv, opts.input, env, timeout);
1791
+ }
1792
+ const { stdout } = await execFileP("databricks", argv, { env, timeout });
1792
1793
  return stdout.toString();
1793
1794
  } catch (err) {
1794
1795
  throw classifyDatabricksError(err, argv, profile);
1795
1796
  }
1796
1797
  }
1798
+ function execDatabricksWithStdin(argv, input, env, timeout) {
1799
+ return new Promise((resolve2, reject) => {
1800
+ const child = execFile("databricks", argv, { env, timeout }, (err, stdout, stderr) => {
1801
+ if (err) {
1802
+ err.stdout = String(stdout ?? "");
1803
+ err.stderr = String(stderr ?? "");
1804
+ reject(err);
1805
+ return;
1806
+ }
1807
+ resolve2(String(stdout ?? ""));
1808
+ });
1809
+ child.stdin?.on("error", () => {
1810
+ });
1811
+ child.stdin?.end(input);
1812
+ });
1813
+ }
1797
1814
  function runDatabricksSync(args, opts = {}) {
1798
1815
  const { argv, env, profile } = buildInvocation(args, opts);
1799
1816
  try {
@@ -2500,8 +2517,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
2500
2517
  var cached;
2501
2518
  function substrateSelfVersion() {
2502
2519
  if (cached !== void 0) return cached;
2503
- if ("0.2.43".length > 0) {
2504
- cached = "0.2.43";
2520
+ if ("0.2.45".length > 0) {
2521
+ cached = "0.2.45";
2505
2522
  return cached;
2506
2523
  }
2507
2524
  cached = "unknown";
@@ -2729,6 +2746,14 @@ async function getCredential(args) {
2729
2746
 
2730
2747
  // scripts/git/status.ts
2731
2748
  init_esm_shims();
2749
+ async function hasUpstream(args) {
2750
+ try {
2751
+ await exec2("git rev-parse --abbrev-ref @{u}", { cwd: args.cwd });
2752
+ return true;
2753
+ } catch {
2754
+ return false;
2755
+ }
2756
+ }
2732
2757
  async function getAheadBehind(args) {
2733
2758
  const { cwd } = args;
2734
2759
  try {
@@ -12760,8 +12785,20 @@ async function setupRunner(args) {
12760
12785
  if (needsConfig) {
12761
12786
  report("Registering runner with GitHub...");
12762
12787
  const regToken = await createRegistrationToken(args.fullRepoName);
12763
- cp4.execSync(
12764
- `./config.sh --url "https://github.com/${args.fullRepoName}" --token "${regToken}" --name "${name}" --labels self-hosted --unattended --replace`,
12788
+ cp4.execFileSync(
12789
+ "./config.sh",
12790
+ [
12791
+ "--url",
12792
+ `https://github.com/${args.fullRepoName}`,
12793
+ "--token",
12794
+ regToken,
12795
+ "--name",
12796
+ name,
12797
+ "--labels",
12798
+ "self-hosted",
12799
+ "--unattended",
12800
+ "--replace"
12801
+ ],
12765
12802
  { cwd: dir, timeout: KIT_TIMEOUTS.cliLong }
12766
12803
  );
12767
12804
  }
@@ -15820,104 +15857,447 @@ function pickRunUrl(pr) {
15820
15857
 
15821
15858
  // scripts/lakebase/scm-merge.ts
15822
15859
  init_esm_shims();
15823
- async function reconcileTierToOrigin(args) {
15824
- const { cwd, tier } = args;
15860
+
15861
+ // scripts/git/sync.ts
15862
+ init_esm_shims();
15863
+ async function currentBranchName(cwd) {
15825
15864
  try {
15826
- const head = await getCurrentBranch({ cwd });
15827
- if (head === tier) {
15828
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
15829
- } else {
15830
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
15831
- }
15832
- return null;
15833
- } catch (err) {
15834
- 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).`;
15865
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
15866
+ } catch {
15867
+ return "";
15835
15868
  }
15836
15869
  }
15837
- var ScmMergeError = class extends Error {
15838
- constructor(message, code) {
15839
- super(message);
15840
- this.code = code;
15841
- this.name = "ScmMergeError";
15870
+ async function pushCurrentBranchForPr(args) {
15871
+ const remote = args.remote ?? "origin";
15872
+ const branch = await currentBranchName(args.cwd);
15873
+ if (!branch) throw new Error("No current branch");
15874
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
15875
+ if (!upstreamSet) {
15876
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
15877
+ cwd: args.cwd
15878
+ });
15879
+ } else {
15880
+ await exec2("git push", { cwd: args.cwd });
15842
15881
  }
15843
- code;
15844
- };
15845
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
15846
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
15847
- function defaultMigratePredicate(run, mergedAt) {
15848
- if (!run.createdAt) return false;
15849
- const created = Date.parse(run.createdAt);
15850
- if (!Number.isFinite(created)) return false;
15851
- if (run.event && run.event !== "push") return false;
15852
- return created >= mergedAt.getTime() - 5e3;
15853
- }
15854
- function shaMigratePredicate(mergeCommitSha) {
15855
- return (run) => {
15856
- if (run.event && run.event !== "push") return false;
15857
- return !!run.headSha && run.headSha === mergeCommitSha;
15858
- };
15859
15882
  }
15860
- async function mergeFeature(args) {
15861
- const current = readWorkflowState(args.projectDir);
15862
- if (!current) {
15863
- throw new ScmMergeError(
15864
- "No SCM workflow state. wait-ci first.",
15865
- "no-state-file"
15883
+
15884
+ // scripts/lakebase/workflow-drift.ts
15885
+ init_esm_shims();
15886
+ import * as fs29 from "fs";
15887
+ import * as path29 from "path";
15888
+ function findKitTemplatesDir(start) {
15889
+ let dir = start;
15890
+ for (let i = 0; i < 6; i++) {
15891
+ const candidate = path29.join(
15892
+ dir,
15893
+ "templates",
15894
+ "project",
15895
+ "common",
15896
+ ".github",
15897
+ "workflows"
15866
15898
  );
15899
+ if (fs29.existsSync(candidate)) return candidate;
15900
+ const parent = path29.dirname(dir);
15901
+ if (parent === dir) break;
15902
+ dir = parent;
15867
15903
  }
15868
- if (current.state !== "ci-green") {
15869
- throw new ScmMergeError(
15870
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
15871
- "bad-precondition"
15872
- );
15904
+ throw new Error(
15905
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
15906
+ );
15907
+ }
15908
+ function unifiedDiff(name, projectContent, templateContent) {
15909
+ if (projectContent === templateContent) return "";
15910
+ const a = projectContent.split("\n");
15911
+ const b = templateContent.split("\n");
15912
+ const out = [`--- project/${name}`, `+++ template/${name}`];
15913
+ const max = Math.max(a.length, b.length);
15914
+ for (let i = 0; i < max; i++) {
15915
+ const av = a[i];
15916
+ const bv = b[i];
15917
+ if (av === bv) continue;
15918
+ if (av !== void 0) out.push(`-${i + 1}: ${av}`);
15919
+ if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
15873
15920
  }
15874
- if (!current.pr_url) {
15875
- throw new ScmMergeError(
15876
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
15877
- "no-pr-url"
15878
- );
15921
+ return out.join("\n");
15922
+ }
15923
+ function detectWorkflowDrift(args) {
15924
+ const projectWorkflowsDir = path29.join(
15925
+ args.projectDir,
15926
+ ".github",
15927
+ "workflows"
15928
+ );
15929
+ const here = path29.dirname(new URL(import.meta.url).pathname);
15930
+ const kitWorkflowsDir = args.kitDir ? path29.join(
15931
+ args.kitDir,
15932
+ "templates",
15933
+ "project",
15934
+ "common",
15935
+ ".github",
15936
+ "workflows"
15937
+ ) : findKitTemplatesDir(here);
15938
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
15939
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
15940
+ const version = readKitVersion(kitWorkflowsDir);
15941
+ const seen = /* @__PURE__ */ new Set();
15942
+ const files = [];
15943
+ for (const name of templateFiles) {
15944
+ seen.add(name);
15945
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
15946
+ const templatePath = path29.join(kitWorkflowsDir, name);
15947
+ if (!fs29.existsSync(projectPath2)) {
15948
+ files.push({ name, status: "missing" });
15949
+ continue;
15950
+ }
15951
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
15952
+ const templateContent = applyPlaceholders(fs29.readFileSync(templatePath, "utf8"), version);
15953
+ if (projectContent === templateContent) {
15954
+ files.push({ name, status: "unchanged" });
15955
+ } else {
15956
+ files.push({
15957
+ name,
15958
+ status: "drifted",
15959
+ diff: unifiedDiff(name, projectContent, templateContent)
15960
+ });
15961
+ }
15879
15962
  }
15880
- if (!current.branch || !current.parent_branch) {
15881
- throw new ScmMergeError(
15882
- "ci-green row missing branch / parent_branch; refusing to merge.",
15883
- "bad-precondition"
15884
- );
15963
+ for (const name of projectFiles) {
15964
+ if (seen.has(name)) continue;
15965
+ files.push({ name, status: "extra" });
15885
15966
  }
15886
- const ownerRepo = await getOwnerRepo(args.projectDir);
15887
- if (!ownerRepo) {
15888
- throw new ScmMergeError(
15889
- "No GitHub remote found at origin.",
15890
- "no-github-remote"
15891
- );
15967
+ const order = {
15968
+ drifted: 0,
15969
+ missing: 1,
15970
+ extra: 2,
15971
+ unchanged: 3
15972
+ };
15973
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
15974
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
15975
+ return {
15976
+ overall: hasDrift ? "drift" : "ok",
15977
+ files
15978
+ };
15979
+ }
15980
+ function readKitVersion(kitWorkflowsDir) {
15981
+ let dir = kitWorkflowsDir;
15982
+ for (let i = 0; i < 5; i++) {
15983
+ dir = path29.dirname(dir);
15892
15984
  }
15893
- const pullNumber = extractPullNumber(current.pr_url);
15894
- if (!pullNumber) {
15895
- throw new ScmMergeError(
15896
- `Could not extract PR number from URL: ${current.pr_url}`,
15897
- "bad-pr-url"
15985
+ try {
15986
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
15987
+ const pkg = JSON.parse(raw);
15988
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
15989
+ } catch {
15990
+ return "unknown";
15991
+ }
15992
+ }
15993
+ function applyPlaceholders(content, version) {
15994
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
15995
+ }
15996
+ function applyCommandPlaceholders(content, version) {
15997
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
15998
+ }
15999
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16000
+ function findKitCommandsDir(start) {
16001
+ let dir = start;
16002
+ for (let i = 0; i < 6; i++) {
16003
+ const candidate = path29.join(
16004
+ dir,
16005
+ "templates",
16006
+ "project",
16007
+ "common",
16008
+ ".claude",
16009
+ "commands"
15898
16010
  );
16011
+ if (fs29.existsSync(candidate)) return candidate;
16012
+ const parent = path29.dirname(dir);
16013
+ if (parent === dir) break;
16014
+ dir = parent;
15899
16015
  }
15900
- const instance = args.instance ?? current.project_id;
15901
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
15902
- const wantMigrate = args.waitMigrate !== false;
15903
- let authVerified = false;
15904
- if (wantMigrate && args.verifyMigrateAuth) {
15905
- const probe = await args.verifyMigrateAuth();
15906
- if (!probe.ok) {
15907
- throw new ScmMergeError(
15908
- `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.`,
15909
- "migrate-auth"
15910
- );
16016
+ throw new Error(
16017
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16018
+ );
16019
+ }
16020
+ function parsePinnedVersion(content) {
16021
+ const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16022
+ return m ? m[1] : void 0;
16023
+ }
16024
+ function detectCommandDrift(args) {
16025
+ const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
16026
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16027
+ const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16028
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16029
+ const templateFiles = fs29.existsSync(kitCommandsDir) ? fs29.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16030
+ const projectFiles = fs29.existsSync(projectCommandsDir) ? fs29.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16031
+ const seen = /* @__PURE__ */ new Set();
16032
+ const files = [];
16033
+ for (const name of templateFiles) {
16034
+ seen.add(name);
16035
+ const projectPath2 = path29.join(projectCommandsDir, name);
16036
+ const templatePath = path29.join(kitCommandsDir, name);
16037
+ const templateRaw = fs29.readFileSync(templatePath, "utf8");
16038
+ if (!fs29.existsSync(projectPath2)) {
16039
+ files.push({ name, status: "missing", kit_version: kitVersion });
16040
+ continue;
16041
+ }
16042
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16043
+ const pinned = parsePinnedVersion(projectContent);
16044
+ const versionForCompare = pinned ?? kitVersion;
16045
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16046
+ if (projectContent === templateContent) {
16047
+ files.push({
16048
+ name,
16049
+ status: "unchanged",
16050
+ pinned_version: pinned,
16051
+ kit_version: kitVersion
16052
+ });
16053
+ } else {
16054
+ files.push({
16055
+ name,
16056
+ status: "drifted",
16057
+ pinned_version: pinned,
16058
+ kit_version: kitVersion,
16059
+ diff: unifiedDiff(name, projectContent, templateContent)
16060
+ });
15911
16061
  }
15912
- authVerified = true;
15913
16062
  }
15914
- let paired;
15915
- try {
15916
- paired = await mergePairedPullRequest({
15917
- ownerRepo,
15918
- pullNumber,
15919
- lakebaseInstance: instance,
15920
- method: args.method ?? "squash"
16063
+ for (const name of projectFiles) {
16064
+ if (seen.has(name)) continue;
16065
+ files.push({ name, status: "extra", kit_version: kitVersion });
16066
+ }
16067
+ const order = {
16068
+ drifted: 0,
16069
+ missing: 1,
16070
+ extra: 2,
16071
+ unchanged: 3
16072
+ };
16073
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16074
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16075
+ return { overall: hasDrift ? "drift" : "ok", files };
16076
+ }
16077
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
16078
+ let dir = kitCommandsDir;
16079
+ for (let i = 0; i < 5; i++) {
16080
+ dir = path29.dirname(dir);
16081
+ }
16082
+ try {
16083
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16084
+ const pkg = JSON.parse(raw);
16085
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16086
+ } catch {
16087
+ return "unknown";
16088
+ }
16089
+ }
16090
+ function detectScaffoldedDrift(args) {
16091
+ const workflows = detectWorkflowDrift(args);
16092
+ const commands = detectCommandDrift(args);
16093
+ return {
16094
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16095
+ workflows,
16096
+ commands
16097
+ };
16098
+ }
16099
+ function updateWorkflows(args) {
16100
+ const projectWorkflowsDir = path29.join(
16101
+ args.projectDir,
16102
+ ".github",
16103
+ "workflows"
16104
+ );
16105
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16106
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16107
+ args.kitDir,
16108
+ "templates",
16109
+ "project",
16110
+ "common",
16111
+ ".github",
16112
+ "workflows"
16113
+ ) : findKitTemplatesDir(here);
16114
+ const substitute = args.substitute !== false;
16115
+ const dryRun = args.dryRun === true;
16116
+ const pruneExtras = args.pruneExtras === true;
16117
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16118
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16119
+ if (!dryRun && templateFiles.length > 0 && !fs29.existsSync(projectWorkflowsDir)) {
16120
+ fs29.mkdirSync(projectWorkflowsDir, { recursive: true });
16121
+ }
16122
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16123
+ const seen = /* @__PURE__ */ new Set();
16124
+ const files = [];
16125
+ for (const name of templateFiles) {
16126
+ seen.add(name);
16127
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16128
+ const templatePath = path29.join(kitWorkflowsDir, name);
16129
+ const templateRaw = fs29.readFileSync(templatePath, "utf-8");
16130
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16131
+ const existed = fs29.existsSync(projectPath2);
16132
+ const current = existed ? fs29.readFileSync(projectPath2, "utf-8") : "";
16133
+ let outcome;
16134
+ if (!existed) {
16135
+ outcome = "added";
16136
+ } else if (current === desired) {
16137
+ outcome = "unchanged";
16138
+ } else {
16139
+ outcome = "updated";
16140
+ }
16141
+ if (!dryRun && outcome !== "unchanged") {
16142
+ fs29.writeFileSync(projectPath2, desired);
16143
+ }
16144
+ files.push({ name, outcome });
16145
+ }
16146
+ if (pruneExtras) {
16147
+ for (const name of projectFiles) {
16148
+ if (seen.has(name)) continue;
16149
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16150
+ if (!dryRun) {
16151
+ fs29.unlinkSync(projectPath2);
16152
+ }
16153
+ files.push({ name, outcome: "removed" });
16154
+ }
16155
+ }
16156
+ const order = {
16157
+ added: 0,
16158
+ updated: 1,
16159
+ removed: 2,
16160
+ unchanged: 3
16161
+ };
16162
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
16163
+ const changed = files.some((f) => f.outcome !== "unchanged");
16164
+ return { files, changed };
16165
+ }
16166
+
16167
+ // scripts/lakebase/scm-merge.ts
16168
+ async function reconcileTierToOrigin(args) {
16169
+ const { cwd, tier } = args;
16170
+ try {
16171
+ const head = await getCurrentBranch({ cwd });
16172
+ if (head === tier) {
16173
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16174
+ } else {
16175
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16176
+ }
16177
+ return null;
16178
+ } catch (err) {
16179
+ 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).`;
16180
+ }
16181
+ }
16182
+ var ScmMergeError = class extends Error {
16183
+ constructor(message, code) {
16184
+ super(message);
16185
+ this.code = code;
16186
+ this.name = "ScmMergeError";
16187
+ }
16188
+ code;
16189
+ };
16190
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16191
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16192
+ function defaultMigratePredicate(run, mergedAt) {
16193
+ if (!run.createdAt) return false;
16194
+ const created = Date.parse(run.createdAt);
16195
+ if (!Number.isFinite(created)) return false;
16196
+ if (run.event && run.event !== "push") return false;
16197
+ return created >= mergedAt.getTime() - 5e3;
16198
+ }
16199
+ function shaMigratePredicate(mergeCommitSha) {
16200
+ return (run) => {
16201
+ if (run.event && run.event !== "push") return false;
16202
+ return !!run.headSha && run.headSha === mergeCommitSha;
16203
+ };
16204
+ }
16205
+ async function defaultRefreshPromoteWorkflows(projectDir) {
16206
+ const drift = detectWorkflowDrift({ projectDir });
16207
+ const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
16208
+ if (touched.length === 0) return { refreshed: false };
16209
+ const result = updateWorkflows({ projectDir });
16210
+ if (!result.changed) return { refreshed: false };
16211
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
16212
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
16213
+ cwd: projectDir,
16214
+ timeout: 1e4
16215
+ });
16216
+ if (!staged.trim()) return { refreshed: false };
16217
+ await exec2(
16218
+ `git commit -m ${shellEscape2(
16219
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
16220
+ )} -- .github/workflows`,
16221
+ { cwd: projectDir, timeout: 15e3 }
16222
+ );
16223
+ await pushCurrentBranchForPr({ cwd: projectDir });
16224
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
16225
+ }
16226
+ async function mergeFeature(args) {
16227
+ const current = readWorkflowState(args.projectDir);
16228
+ if (!current) {
16229
+ throw new ScmMergeError(
16230
+ "No SCM workflow state. wait-ci first.",
16231
+ "no-state-file"
16232
+ );
16233
+ }
16234
+ if (current.state !== "ci-green") {
16235
+ throw new ScmMergeError(
16236
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16237
+ "bad-precondition"
16238
+ );
16239
+ }
16240
+ if (!current.pr_url) {
16241
+ throw new ScmMergeError(
16242
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16243
+ "no-pr-url"
16244
+ );
16245
+ }
16246
+ if (!current.branch || !current.parent_branch) {
16247
+ throw new ScmMergeError(
16248
+ "ci-green row missing branch / parent_branch; refusing to merge.",
16249
+ "bad-precondition"
16250
+ );
16251
+ }
16252
+ const ownerRepo = await getOwnerRepo(args.projectDir);
16253
+ if (!ownerRepo) {
16254
+ throw new ScmMergeError(
16255
+ "No GitHub remote found at origin.",
16256
+ "no-github-remote"
16257
+ );
16258
+ }
16259
+ const pullNumber = extractPullNumber(current.pr_url);
16260
+ if (!pullNumber) {
16261
+ throw new ScmMergeError(
16262
+ `Could not extract PR number from URL: ${current.pr_url}`,
16263
+ "bad-pr-url"
16264
+ );
16265
+ }
16266
+ const instance = args.instance ?? current.project_id;
16267
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16268
+ const wantMigrate = args.waitMigrate !== false;
16269
+ let authVerified = false;
16270
+ if (wantMigrate && args.verifyMigrateAuth) {
16271
+ const probe = await args.verifyMigrateAuth();
16272
+ if (!probe.ok) {
16273
+ throw new ScmMergeError(
16274
+ `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.`,
16275
+ "migrate-auth"
16276
+ );
16277
+ }
16278
+ authVerified = true;
16279
+ }
16280
+ const preflightNotes = [];
16281
+ {
16282
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
16283
+ try {
16284
+ const r = await refresh();
16285
+ if (r.refreshed) {
16286
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
16287
+ }
16288
+ } catch (err) {
16289
+ preflightNotes.push(
16290
+ `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').`
16291
+ );
16292
+ }
16293
+ }
16294
+ let paired;
16295
+ try {
16296
+ paired = await mergePairedPullRequest({
16297
+ ownerRepo,
16298
+ pullNumber,
16299
+ lakebaseInstance: instance,
16300
+ method: args.method ?? "squash"
15921
16301
  });
15922
16302
  } catch (err) {
15923
16303
  throw new ScmMergeError(
@@ -15925,7 +16305,7 @@ async function mergeFeature(args) {
15925
16305
  "merge-failed"
15926
16306
  );
15927
16307
  }
15928
- const warnings = [...paired.warnings];
16308
+ const warnings = [...preflightNotes, ...paired.warnings];
15929
16309
  let localBranchDeleted = false;
15930
16310
  let headAfter = current.branch;
15931
16311
  if (!args.skipLocalCleanup) {
@@ -16122,7 +16502,7 @@ init_esm_shims();
16122
16502
 
16123
16503
  // scripts/git/branches.ts
16124
16504
  init_esm_shims();
16125
- async function currentBranchName(cwd) {
16505
+ async function currentBranchName2(cwd) {
16126
16506
  try {
16127
16507
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16128
16508
  } catch {
@@ -16141,7 +16521,7 @@ async function listLocalBranches(args) {
16141
16521
  return [];
16142
16522
  }
16143
16523
  if (!raw) return [];
16144
- const current = await currentBranchName(cwd);
16524
+ const current = await currentBranchName2(cwd);
16145
16525
  return raw.split("\n").filter(Boolean).map((line) => {
16146
16526
  const [name, tracking, trackInfo] = line.split("|");
16147
16527
  let ahead = 0;
@@ -16306,15 +16686,15 @@ function parentForTopology(t, defaultLeaf) {
16306
16686
 
16307
16687
  // scripts/lakebase/scm-doctor.ts
16308
16688
  init_esm_shims();
16309
- import * as fs29 from "fs";
16310
- import * as path29 from "path";
16689
+ import * as fs30 from "fs";
16690
+ import * as path30 from "path";
16311
16691
  var FEATURE_PREFIX = "feature/";
16312
16692
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
16313
16693
  function readEnv(projectDir) {
16314
- const envPath = path29.join(projectDir, ".env");
16694
+ const envPath = path30.join(projectDir, ".env");
16315
16695
  const out = /* @__PURE__ */ new Map();
16316
- if (!fs29.existsSync(envPath)) return out;
16317
- const lines = fs29.readFileSync(envPath, "utf8").split("\n");
16696
+ if (!fs30.existsSync(envPath)) return out;
16697
+ const lines = fs30.readFileSync(envPath, "utf8").split("\n");
16318
16698
  for (const line of lines) {
16319
16699
  const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
16320
16700
  if (m) out.set(m[1], m[2].replace(/^["']|["']$/g, ""));
@@ -16369,9 +16749,9 @@ async function runDoctor(args, deps = {}) {
16369
16749
  }
16370
16750
  for (const wf of ["pr.yml", "merge.yml"]) {
16371
16751
  try {
16372
- const p = path29.join(projectDir, ".github", "workflows", wf);
16373
- if (!fs29.existsSync(p)) continue;
16374
- const body = fs29.readFileSync(p, "utf8");
16752
+ const p = path30.join(projectDir, ".github", "workflows", wf);
16753
+ if (!fs30.existsSync(p)) continue;
16754
+ const body = fs30.readFileSync(p, "utf8");
16375
16755
  if (/lakebase-scm-utils#v\d/.test(body)) {
16376
16756
  findings.push({
16377
16757
  id: "ci-workflow-substrate-pin",
@@ -16586,7 +16966,7 @@ async function fixFinding(args) {
16586
16966
  );
16587
16967
  }
16588
16968
  const sanitized = sanitizeBranchName(branch);
16589
- const envFile = path29.join(args.projectDir, ".env");
16969
+ const envFile = path30.join(args.projectDir, ".env");
16590
16970
  updateEnvConnection({
16591
16971
  envPath: envFile,
16592
16972
  projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
@@ -16691,291 +17071,6 @@ init_esm_shims();
16691
17071
  import * as fs31 from "fs";
16692
17072
  import * as path31 from "path";
16693
17073
  import * as cp6 from "child_process";
16694
-
16695
- // scripts/lakebase/workflow-drift.ts
16696
- init_esm_shims();
16697
- import * as fs30 from "fs";
16698
- import * as path30 from "path";
16699
- function findKitTemplatesDir(start) {
16700
- let dir = start;
16701
- for (let i = 0; i < 6; i++) {
16702
- const candidate = path30.join(
16703
- dir,
16704
- "templates",
16705
- "project",
16706
- "common",
16707
- ".github",
16708
- "workflows"
16709
- );
16710
- if (fs30.existsSync(candidate)) return candidate;
16711
- const parent = path30.dirname(dir);
16712
- if (parent === dir) break;
16713
- dir = parent;
16714
- }
16715
- throw new Error(
16716
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16717
- );
16718
- }
16719
- function unifiedDiff(name, projectContent, templateContent) {
16720
- if (projectContent === templateContent) return "";
16721
- const a = projectContent.split("\n");
16722
- const b = templateContent.split("\n");
16723
- const out = [`--- project/${name}`, `+++ template/${name}`];
16724
- const max = Math.max(a.length, b.length);
16725
- for (let i = 0; i < max; i++) {
16726
- const av = a[i];
16727
- const bv = b[i];
16728
- if (av === bv) continue;
16729
- if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16730
- if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16731
- }
16732
- return out.join("\n");
16733
- }
16734
- function detectWorkflowDrift(args) {
16735
- const projectWorkflowsDir = path30.join(
16736
- args.projectDir,
16737
- ".github",
16738
- "workflows"
16739
- );
16740
- const here = path30.dirname(new URL(import.meta.url).pathname);
16741
- const kitWorkflowsDir = args.kitDir ? path30.join(
16742
- args.kitDir,
16743
- "templates",
16744
- "project",
16745
- "common",
16746
- ".github",
16747
- "workflows"
16748
- ) : findKitTemplatesDir(here);
16749
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16750
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16751
- const version = readKitVersion(kitWorkflowsDir);
16752
- const seen = /* @__PURE__ */ new Set();
16753
- const files = [];
16754
- for (const name of templateFiles) {
16755
- seen.add(name);
16756
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16757
- const templatePath = path30.join(kitWorkflowsDir, name);
16758
- if (!fs30.existsSync(projectPath2)) {
16759
- files.push({ name, status: "missing" });
16760
- continue;
16761
- }
16762
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16763
- const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
16764
- if (projectContent === templateContent) {
16765
- files.push({ name, status: "unchanged" });
16766
- } else {
16767
- files.push({
16768
- name,
16769
- status: "drifted",
16770
- diff: unifiedDiff(name, projectContent, templateContent)
16771
- });
16772
- }
16773
- }
16774
- for (const name of projectFiles) {
16775
- if (seen.has(name)) continue;
16776
- files.push({ name, status: "extra" });
16777
- }
16778
- const order = {
16779
- drifted: 0,
16780
- missing: 1,
16781
- extra: 2,
16782
- unchanged: 3
16783
- };
16784
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16785
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16786
- return {
16787
- overall: hasDrift ? "drift" : "ok",
16788
- files
16789
- };
16790
- }
16791
- function readKitVersion(kitWorkflowsDir) {
16792
- let dir = kitWorkflowsDir;
16793
- for (let i = 0; i < 5; i++) {
16794
- dir = path30.dirname(dir);
16795
- }
16796
- try {
16797
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
16798
- const pkg = JSON.parse(raw);
16799
- return typeof pkg.version === "string" ? pkg.version : "unknown";
16800
- } catch {
16801
- return "unknown";
16802
- }
16803
- }
16804
- function applyPlaceholders(content, version) {
16805
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
16806
- }
16807
- function applyCommandPlaceholders(content, version) {
16808
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
16809
- }
16810
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16811
- function findKitCommandsDir(start) {
16812
- let dir = start;
16813
- for (let i = 0; i < 6; i++) {
16814
- const candidate = path30.join(
16815
- dir,
16816
- "templates",
16817
- "project",
16818
- "common",
16819
- ".claude",
16820
- "commands"
16821
- );
16822
- if (fs30.existsSync(candidate)) return candidate;
16823
- const parent = path30.dirname(dir);
16824
- if (parent === dir) break;
16825
- dir = parent;
16826
- }
16827
- throw new Error(
16828
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16829
- );
16830
- }
16831
- function parsePinnedVersion(content) {
16832
- const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16833
- return m ? m[1] : void 0;
16834
- }
16835
- function detectCommandDrift(args) {
16836
- const projectCommandsDir = path30.join(args.projectDir, ".claude", "commands");
16837
- const here = path30.dirname(new URL(import.meta.url).pathname);
16838
- const kitCommandsDir = args.kitDir ? path30.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16839
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16840
- const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16841
- const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16842
- const seen = /* @__PURE__ */ new Set();
16843
- const files = [];
16844
- for (const name of templateFiles) {
16845
- seen.add(name);
16846
- const projectPath2 = path30.join(projectCommandsDir, name);
16847
- const templatePath = path30.join(kitCommandsDir, name);
16848
- const templateRaw = fs30.readFileSync(templatePath, "utf8");
16849
- if (!fs30.existsSync(projectPath2)) {
16850
- files.push({ name, status: "missing", kit_version: kitVersion });
16851
- continue;
16852
- }
16853
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16854
- const pinned = parsePinnedVersion(projectContent);
16855
- const versionForCompare = pinned ?? kitVersion;
16856
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16857
- if (projectContent === templateContent) {
16858
- files.push({
16859
- name,
16860
- status: "unchanged",
16861
- pinned_version: pinned,
16862
- kit_version: kitVersion
16863
- });
16864
- } else {
16865
- files.push({
16866
- name,
16867
- status: "drifted",
16868
- pinned_version: pinned,
16869
- kit_version: kitVersion,
16870
- diff: unifiedDiff(name, projectContent, templateContent)
16871
- });
16872
- }
16873
- }
16874
- for (const name of projectFiles) {
16875
- if (seen.has(name)) continue;
16876
- files.push({ name, status: "extra", kit_version: kitVersion });
16877
- }
16878
- const order = {
16879
- drifted: 0,
16880
- missing: 1,
16881
- extra: 2,
16882
- unchanged: 3
16883
- };
16884
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16885
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16886
- return { overall: hasDrift ? "drift" : "ok", files };
16887
- }
16888
- function readKitVersionFromCommandsDir(kitCommandsDir) {
16889
- let dir = kitCommandsDir;
16890
- for (let i = 0; i < 5; i++) {
16891
- dir = path30.dirname(dir);
16892
- }
16893
- try {
16894
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
16895
- const pkg = JSON.parse(raw);
16896
- return typeof pkg.version === "string" ? pkg.version : "unknown";
16897
- } catch {
16898
- return "unknown";
16899
- }
16900
- }
16901
- function detectScaffoldedDrift(args) {
16902
- const workflows = detectWorkflowDrift(args);
16903
- const commands = detectCommandDrift(args);
16904
- return {
16905
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16906
- workflows,
16907
- commands
16908
- };
16909
- }
16910
- function updateWorkflows(args) {
16911
- const projectWorkflowsDir = path30.join(
16912
- args.projectDir,
16913
- ".github",
16914
- "workflows"
16915
- );
16916
- const here = path30.dirname(new URL(import.meta.url).pathname);
16917
- const kitWorkflowsDir = args.kitDir ? path30.join(
16918
- args.kitDir,
16919
- "templates",
16920
- "project",
16921
- "common",
16922
- ".github",
16923
- "workflows"
16924
- ) : findKitTemplatesDir(here);
16925
- const substitute = args.substitute !== false;
16926
- const dryRun = args.dryRun === true;
16927
- const pruneExtras = args.pruneExtras === true;
16928
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16929
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16930
- if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
16931
- fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
16932
- }
16933
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16934
- const seen = /* @__PURE__ */ new Set();
16935
- const files = [];
16936
- for (const name of templateFiles) {
16937
- seen.add(name);
16938
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16939
- const templatePath = path30.join(kitWorkflowsDir, name);
16940
- const templateRaw = fs30.readFileSync(templatePath, "utf-8");
16941
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16942
- const existed = fs30.existsSync(projectPath2);
16943
- const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
16944
- let outcome;
16945
- if (!existed) {
16946
- outcome = "added";
16947
- } else if (current === desired) {
16948
- outcome = "unchanged";
16949
- } else {
16950
- outcome = "updated";
16951
- }
16952
- if (!dryRun && outcome !== "unchanged") {
16953
- fs30.writeFileSync(projectPath2, desired);
16954
- }
16955
- files.push({ name, outcome });
16956
- }
16957
- if (pruneExtras) {
16958
- for (const name of projectFiles) {
16959
- if (seen.has(name)) continue;
16960
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16961
- if (!dryRun) {
16962
- fs30.unlinkSync(projectPath2);
16963
- }
16964
- files.push({ name, outcome: "removed" });
16965
- }
16966
- }
16967
- const order = {
16968
- added: 0,
16969
- updated: 1,
16970
- removed: 2,
16971
- unchanged: 3
16972
- };
16973
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
16974
- const changed = files.some((f) => f.outcome !== "unchanged");
16975
- return { files, changed };
16976
- }
16977
-
16978
- // scripts/lakebase/doctor.ts
16979
17074
  function readEnvFile(projectDir) {
16980
17075
  const envPath = path31.join(projectDir, ".env");
16981
17076
  if (!fs31.existsSync(envPath)) return {};
@@ -17408,7 +17503,7 @@ function checkWorkflowDrift(projectDir) {
17408
17503
  status: "warn",
17409
17504
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
17410
17505
  detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
17411
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
17506
+ 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."
17412
17507
  };
17413
17508
  } catch (err) {
17414
17509
  return {
@@ -17519,9 +17614,10 @@ async function ensureLakebaseSecretAuth(args) {
17519
17614
  if (typeof pat !== "string" || !pat) {
17520
17615
  throw new Error("databricks tokens create returned no token_value");
17521
17616
  }
17522
- await runDatabricks(["secrets", "put-secret", scopeName, keyName, "--string-value", pat], {
17617
+ await runDatabricks(["secrets", "put-secret", "--json", "@/dev/stdin"], {
17523
17618
  profile,
17524
- timeout: timeoutMs
17619
+ timeout: timeoutMs,
17620
+ input: JSON.stringify({ scope: scopeName, key: keyName, string_value: pat })
17525
17621
  });
17526
17622
  let aclGranted = false;
17527
17623
  if (servicePrincipalClientId) {
@@ -17715,6 +17811,7 @@ export {
17715
17811
  createUatPairedBranch,
17716
17812
  cutBackup,
17717
17813
  databricksAuthPrereqMessage,
17814
+ defaultRefreshPromoteWorkflows,
17718
17815
  deleteAppEndpoint,
17719
17816
  deleteBranch,
17720
17817
  deleteLakebaseProject,