@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
@@ -9708,6 +9708,7 @@ var RUNTIME_ARTIFACT_IGNORE = [
9708
9708
  var DEFAULT_ENDPOINT = "primary";
9709
9709
  var CONSORT_APPLICATION_NAME = "consort";
9710
9710
  var SCM_UTILS_APPLICATION_NAME = "scm-utils";
9711
+ var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
9711
9712
  var CONSORT_VERSION_ENV = "CONSORT_VERSION";
9712
9713
 
9713
9714
  // scripts/lakebase/self-version.ts
@@ -9719,8 +9720,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9719
9720
  var cached;
9720
9721
  function substrateSelfVersion() {
9721
9722
  if (cached !== void 0) return cached;
9722
- if ("0.2.44".length > 0) {
9723
- cached = "0.2.44";
9723
+ if ("0.2.46".length > 0) {
9724
+ cached = "0.2.46";
9724
9725
  return cached;
9725
9726
  }
9726
9727
  cached = "unknown";
@@ -9748,9 +9749,17 @@ function substrateSelfVersion() {
9748
9749
  }
9749
9750
 
9750
9751
  // scripts/lakebase/get-connection.ts
9752
+ function sanitizeApplicationName(raw) {
9753
+ const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
9754
+ if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
9755
+ return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
9756
+ }
9751
9757
  function connectionApplicationName() {
9758
+ const client = process.env[SCM_CLIENT_ENV]?.trim();
9759
+ if (client) return sanitizeApplicationName(client);
9752
9760
  const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
9753
- return consortVersion ? `${CONSORT_APPLICATION_NAME}/${consortVersion}` : `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
9761
+ if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
9762
+ return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
9754
9763
  }
9755
9764
  async function getConnection(args) {
9756
9765
  const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
@@ -16021,122 +16030,499 @@ function pickRunUrl(pr) {
16021
16030
 
16022
16031
  // scripts/lakebase/scm-merge.ts
16023
16032
  init_esm_shims();
16024
- async function reconcileTierToOrigin(args) {
16025
- const { cwd, tier } = args;
16033
+
16034
+ // scripts/git/sync.ts
16035
+ init_esm_shims();
16036
+ async function currentBranchName(cwd) {
16026
16037
  try {
16027
- const head = await getCurrentBranch({ cwd });
16028
- if (head === tier) {
16029
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16030
- } else {
16031
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16032
- }
16033
- return null;
16034
- } catch (err) {
16035
- 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).`;
16038
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16039
+ } catch {
16040
+ return "";
16036
16041
  }
16037
16042
  }
16038
- var ScmMergeError = class extends Error {
16039
- constructor(message, code) {
16040
- super(message);
16041
- this.code = code;
16042
- this.name = "ScmMergeError";
16043
- }
16044
- code;
16045
- };
16046
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16047
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16048
- function defaultMigratePredicate(run, mergedAt) {
16049
- if (!run.createdAt) return false;
16050
- const created = Date.parse(run.createdAt);
16051
- if (!Number.isFinite(created)) return false;
16052
- if (run.event && run.event !== "push") return false;
16053
- return created >= mergedAt.getTime() - 5e3;
16043
+ async function push(args) {
16044
+ await exec2("git push", { cwd: args.cwd });
16054
16045
  }
16055
- function shaMigratePredicate(mergeCommitSha) {
16056
- return (run) => {
16057
- if (run.event && run.event !== "push") return false;
16058
- return !!run.headSha && run.headSha === mergeCommitSha;
16059
- };
16046
+ async function pull(args) {
16047
+ await exec2("git pull", { cwd: args.cwd });
16060
16048
  }
16061
- async function mergeFeature(args) {
16062
- const current = readWorkflowState(args.projectDir);
16063
- if (!current) {
16064
- throw new ScmMergeError(
16065
- "No SCM workflow state. wait-ci first.",
16066
- "no-state-file"
16067
- );
16068
- }
16069
- if (current.state !== "ci-green") {
16070
- throw new ScmMergeError(
16071
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16072
- "bad-precondition"
16073
- );
16049
+ async function publishBranch(args) {
16050
+ const remote = args.remote ?? "origin";
16051
+ const branch = await currentBranchName(args.cwd);
16052
+ if (!branch) throw new Error("No current branch");
16053
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
16054
+ cwd: args.cwd
16055
+ });
16056
+ }
16057
+ async function pushCurrentBranchForPr(args) {
16058
+ const remote = args.remote ?? "origin";
16059
+ const branch = await currentBranchName(args.cwd);
16060
+ if (!branch) throw new Error("No current branch");
16061
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
16062
+ if (!upstreamSet) {
16063
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
16064
+ cwd: args.cwd
16065
+ });
16066
+ } else {
16067
+ await exec2("git push", { cwd: args.cwd });
16074
16068
  }
16075
- if (!current.pr_url) {
16076
- throw new ScmMergeError(
16077
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16078
- "no-pr-url"
16069
+ }
16070
+ async function fetch2(args) {
16071
+ const parts = ["git fetch"];
16072
+ if (args.prune) parts.push("--prune");
16073
+ if (args.all) parts.push("--all");
16074
+ await exec2(parts.join(" "), { cwd: args.cwd });
16075
+ }
16076
+ async function pullFrom(args) {
16077
+ await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
16078
+ cwd: args.cwd
16079
+ });
16080
+ }
16081
+ async function pushTo(args) {
16082
+ await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
16083
+ cwd: args.cwd
16084
+ });
16085
+ }
16086
+ async function sync(args) {
16087
+ await exec2("git pull", { cwd: args.cwd });
16088
+ await exec2("git push", { cwd: args.cwd });
16089
+ }
16090
+
16091
+ // scripts/lakebase/workflow-drift.ts
16092
+ init_esm_shims();
16093
+ import * as fs29 from "fs";
16094
+ import * as path29 from "path";
16095
+ function findKitTemplatesDir(start) {
16096
+ let dir = start;
16097
+ for (let i = 0; i < 6; i++) {
16098
+ const candidate = path29.join(
16099
+ dir,
16100
+ "templates",
16101
+ "project",
16102
+ "common",
16103
+ ".github",
16104
+ "workflows"
16079
16105
  );
16106
+ if (fs29.existsSync(candidate)) return candidate;
16107
+ const parent = path29.dirname(dir);
16108
+ if (parent === dir) break;
16109
+ dir = parent;
16080
16110
  }
16081
- if (!current.branch || !current.parent_branch) {
16082
- throw new ScmMergeError(
16083
- "ci-green row missing branch / parent_branch; refusing to merge.",
16084
- "bad-precondition"
16085
- );
16111
+ throw new Error(
16112
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16113
+ );
16114
+ }
16115
+ function unifiedDiff(name, projectContent, templateContent) {
16116
+ if (projectContent === templateContent) return "";
16117
+ const a = projectContent.split("\n");
16118
+ const b = templateContent.split("\n");
16119
+ const out = [`--- project/${name}`, `+++ template/${name}`];
16120
+ const max = Math.max(a.length, b.length);
16121
+ for (let i = 0; i < max; i++) {
16122
+ const av = a[i];
16123
+ const bv = b[i];
16124
+ if (av === bv) continue;
16125
+ if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16126
+ if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16086
16127
  }
16087
- const ownerRepo = await getOwnerRepo(args.projectDir);
16088
- if (!ownerRepo) {
16089
- throw new ScmMergeError(
16090
- "No GitHub remote found at origin.",
16091
- "no-github-remote"
16092
- );
16128
+ return out.join("\n");
16129
+ }
16130
+ function detectWorkflowDrift(args) {
16131
+ const projectWorkflowsDir = path29.join(
16132
+ args.projectDir,
16133
+ ".github",
16134
+ "workflows"
16135
+ );
16136
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16137
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16138
+ args.kitDir,
16139
+ "templates",
16140
+ "project",
16141
+ "common",
16142
+ ".github",
16143
+ "workflows"
16144
+ ) : findKitTemplatesDir(here);
16145
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16146
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16147
+ const version = readKitVersion(kitWorkflowsDir);
16148
+ const seen = /* @__PURE__ */ new Set();
16149
+ const files = [];
16150
+ for (const name of templateFiles) {
16151
+ seen.add(name);
16152
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16153
+ const templatePath = path29.join(kitWorkflowsDir, name);
16154
+ if (!fs29.existsSync(projectPath2)) {
16155
+ files.push({ name, status: "missing" });
16156
+ continue;
16157
+ }
16158
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16159
+ const templateContent = applyPlaceholders(fs29.readFileSync(templatePath, "utf8"), version);
16160
+ if (projectContent === templateContent) {
16161
+ files.push({ name, status: "unchanged" });
16162
+ } else {
16163
+ files.push({
16164
+ name,
16165
+ status: "drifted",
16166
+ diff: unifiedDiff(name, projectContent, templateContent)
16167
+ });
16168
+ }
16093
16169
  }
16094
- const pullNumber = extractPullNumber(current.pr_url);
16095
- if (!pullNumber) {
16096
- throw new ScmMergeError(
16097
- `Could not extract PR number from URL: ${current.pr_url}`,
16098
- "bad-pr-url"
16099
- );
16170
+ for (const name of projectFiles) {
16171
+ if (seen.has(name)) continue;
16172
+ files.push({ name, status: "extra" });
16100
16173
  }
16101
- const instance = args.instance ?? current.project_id;
16102
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16103
- const wantMigrate = args.waitMigrate !== false;
16104
- let authVerified = false;
16105
- if (wantMigrate && args.verifyMigrateAuth) {
16106
- const probe = await args.verifyMigrateAuth();
16107
- if (!probe.ok) {
16108
- throw new ScmMergeError(
16109
- `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.`,
16110
- "migrate-auth"
16111
- );
16112
- }
16113
- authVerified = true;
16174
+ const order = {
16175
+ drifted: 0,
16176
+ missing: 1,
16177
+ extra: 2,
16178
+ unchanged: 3
16179
+ };
16180
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16181
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16182
+ return {
16183
+ overall: hasDrift ? "drift" : "ok",
16184
+ files
16185
+ };
16186
+ }
16187
+ function readKitVersion(kitWorkflowsDir) {
16188
+ let dir = kitWorkflowsDir;
16189
+ for (let i = 0; i < 5; i++) {
16190
+ dir = path29.dirname(dir);
16114
16191
  }
16115
- let paired;
16116
16192
  try {
16117
- paired = await mergePairedPullRequest({
16118
- ownerRepo,
16119
- pullNumber,
16120
- lakebaseInstance: instance,
16121
- method: args.method ?? "squash"
16122
- });
16123
- } catch (err) {
16124
- throw new ScmMergeError(
16125
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
16126
- "merge-failed"
16193
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16194
+ const pkg = JSON.parse(raw);
16195
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16196
+ } catch {
16197
+ return "unknown";
16198
+ }
16199
+ }
16200
+ function applyPlaceholders(content, version) {
16201
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
16202
+ }
16203
+ function applyCommandPlaceholders(content, version) {
16204
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
16205
+ }
16206
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16207
+ function findKitCommandsDir(start) {
16208
+ let dir = start;
16209
+ for (let i = 0; i < 6; i++) {
16210
+ const candidate = path29.join(
16211
+ dir,
16212
+ "templates",
16213
+ "project",
16214
+ "common",
16215
+ ".claude",
16216
+ "commands"
16127
16217
  );
16218
+ if (fs29.existsSync(candidate)) return candidate;
16219
+ const parent = path29.dirname(dir);
16220
+ if (parent === dir) break;
16221
+ dir = parent;
16128
16222
  }
16129
- const warnings = [...paired.warnings];
16130
- let localBranchDeleted = false;
16131
- let headAfter = current.branch;
16132
- if (!args.skipLocalCleanup) {
16133
- const switchTo = args.switchTo ?? gitBase;
16134
- const head = await getCurrentBranch({ cwd: args.projectDir });
16135
- if (head === current.branch) {
16136
- try {
16137
- await exec2(`git checkout ${shellEscape2(switchTo)}`, {
16138
- cwd: args.projectDir,
16139
- timeout: 1e4
16223
+ throw new Error(
16224
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16225
+ );
16226
+ }
16227
+ function parsePinnedVersion(content) {
16228
+ const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16229
+ return m ? m[1] : void 0;
16230
+ }
16231
+ function detectCommandDrift(args) {
16232
+ const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
16233
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16234
+ const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16235
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16236
+ const templateFiles = fs29.existsSync(kitCommandsDir) ? fs29.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16237
+ const projectFiles = fs29.existsSync(projectCommandsDir) ? fs29.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16238
+ const seen = /* @__PURE__ */ new Set();
16239
+ const files = [];
16240
+ for (const name of templateFiles) {
16241
+ seen.add(name);
16242
+ const projectPath2 = path29.join(projectCommandsDir, name);
16243
+ const templatePath = path29.join(kitCommandsDir, name);
16244
+ const templateRaw = fs29.readFileSync(templatePath, "utf8");
16245
+ if (!fs29.existsSync(projectPath2)) {
16246
+ files.push({ name, status: "missing", kit_version: kitVersion });
16247
+ continue;
16248
+ }
16249
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16250
+ const pinned = parsePinnedVersion(projectContent);
16251
+ const versionForCompare = pinned ?? kitVersion;
16252
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16253
+ if (projectContent === templateContent) {
16254
+ files.push({
16255
+ name,
16256
+ status: "unchanged",
16257
+ pinned_version: pinned,
16258
+ kit_version: kitVersion
16259
+ });
16260
+ } else {
16261
+ files.push({
16262
+ name,
16263
+ status: "drifted",
16264
+ pinned_version: pinned,
16265
+ kit_version: kitVersion,
16266
+ diff: unifiedDiff(name, projectContent, templateContent)
16267
+ });
16268
+ }
16269
+ }
16270
+ for (const name of projectFiles) {
16271
+ if (seen.has(name)) continue;
16272
+ files.push({ name, status: "extra", kit_version: kitVersion });
16273
+ }
16274
+ const order = {
16275
+ drifted: 0,
16276
+ missing: 1,
16277
+ extra: 2,
16278
+ unchanged: 3
16279
+ };
16280
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16281
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16282
+ return { overall: hasDrift ? "drift" : "ok", files };
16283
+ }
16284
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
16285
+ let dir = kitCommandsDir;
16286
+ for (let i = 0; i < 5; i++) {
16287
+ dir = path29.dirname(dir);
16288
+ }
16289
+ try {
16290
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16291
+ const pkg = JSON.parse(raw);
16292
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16293
+ } catch {
16294
+ return "unknown";
16295
+ }
16296
+ }
16297
+ function detectScaffoldedDrift(args) {
16298
+ const workflows = detectWorkflowDrift(args);
16299
+ const commands = detectCommandDrift(args);
16300
+ return {
16301
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16302
+ workflows,
16303
+ commands
16304
+ };
16305
+ }
16306
+ function updateWorkflows(args) {
16307
+ const projectWorkflowsDir = path29.join(
16308
+ args.projectDir,
16309
+ ".github",
16310
+ "workflows"
16311
+ );
16312
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16313
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16314
+ args.kitDir,
16315
+ "templates",
16316
+ "project",
16317
+ "common",
16318
+ ".github",
16319
+ "workflows"
16320
+ ) : findKitTemplatesDir(here);
16321
+ const substitute = args.substitute !== false;
16322
+ const dryRun = args.dryRun === true;
16323
+ const pruneExtras = args.pruneExtras === true;
16324
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16325
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16326
+ if (!dryRun && templateFiles.length > 0 && !fs29.existsSync(projectWorkflowsDir)) {
16327
+ fs29.mkdirSync(projectWorkflowsDir, { recursive: true });
16328
+ }
16329
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16330
+ const seen = /* @__PURE__ */ new Set();
16331
+ const files = [];
16332
+ for (const name of templateFiles) {
16333
+ seen.add(name);
16334
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16335
+ const templatePath = path29.join(kitWorkflowsDir, name);
16336
+ const templateRaw = fs29.readFileSync(templatePath, "utf-8");
16337
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16338
+ const existed = fs29.existsSync(projectPath2);
16339
+ const current = existed ? fs29.readFileSync(projectPath2, "utf-8") : "";
16340
+ let outcome;
16341
+ if (!existed) {
16342
+ outcome = "added";
16343
+ } else if (current === desired) {
16344
+ outcome = "unchanged";
16345
+ } else {
16346
+ outcome = "updated";
16347
+ }
16348
+ if (!dryRun && outcome !== "unchanged") {
16349
+ fs29.writeFileSync(projectPath2, desired);
16350
+ }
16351
+ files.push({ name, outcome });
16352
+ }
16353
+ if (pruneExtras) {
16354
+ for (const name of projectFiles) {
16355
+ if (seen.has(name)) continue;
16356
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16357
+ if (!dryRun) {
16358
+ fs29.unlinkSync(projectPath2);
16359
+ }
16360
+ files.push({ name, outcome: "removed" });
16361
+ }
16362
+ }
16363
+ const order = {
16364
+ added: 0,
16365
+ updated: 1,
16366
+ removed: 2,
16367
+ unchanged: 3
16368
+ };
16369
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
16370
+ const changed = files.some((f) => f.outcome !== "unchanged");
16371
+ return { files, changed };
16372
+ }
16373
+
16374
+ // scripts/lakebase/scm-merge.ts
16375
+ async function reconcileTierToOrigin(args) {
16376
+ const { cwd, tier } = args;
16377
+ try {
16378
+ const head = await getCurrentBranch({ cwd });
16379
+ if (head === tier) {
16380
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16381
+ } else {
16382
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16383
+ }
16384
+ return null;
16385
+ } catch (err) {
16386
+ 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).`;
16387
+ }
16388
+ }
16389
+ var ScmMergeError = class extends Error {
16390
+ constructor(message, code) {
16391
+ super(message);
16392
+ this.code = code;
16393
+ this.name = "ScmMergeError";
16394
+ }
16395
+ code;
16396
+ };
16397
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16398
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16399
+ function defaultMigratePredicate(run, mergedAt) {
16400
+ if (!run.createdAt) return false;
16401
+ const created = Date.parse(run.createdAt);
16402
+ if (!Number.isFinite(created)) return false;
16403
+ if (run.event && run.event !== "push") return false;
16404
+ return created >= mergedAt.getTime() - 5e3;
16405
+ }
16406
+ function shaMigratePredicate(mergeCommitSha) {
16407
+ return (run) => {
16408
+ if (run.event && run.event !== "push") return false;
16409
+ return !!run.headSha && run.headSha === mergeCommitSha;
16410
+ };
16411
+ }
16412
+ async function defaultRefreshPromoteWorkflows(projectDir) {
16413
+ const drift = detectWorkflowDrift({ projectDir });
16414
+ const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
16415
+ if (touched.length === 0) return { refreshed: false };
16416
+ const result = updateWorkflows({ projectDir });
16417
+ if (!result.changed) return { refreshed: false };
16418
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
16419
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
16420
+ cwd: projectDir,
16421
+ timeout: 1e4
16422
+ });
16423
+ if (!staged.trim()) return { refreshed: false };
16424
+ await exec2(
16425
+ `git commit -m ${shellEscape2(
16426
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
16427
+ )} -- .github/workflows`,
16428
+ { cwd: projectDir, timeout: 15e3 }
16429
+ );
16430
+ await pushCurrentBranchForPr({ cwd: projectDir });
16431
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
16432
+ }
16433
+ async function mergeFeature(args) {
16434
+ const current = readWorkflowState(args.projectDir);
16435
+ if (!current) {
16436
+ throw new ScmMergeError(
16437
+ "No SCM workflow state. wait-ci first.",
16438
+ "no-state-file"
16439
+ );
16440
+ }
16441
+ if (current.state !== "ci-green") {
16442
+ throw new ScmMergeError(
16443
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16444
+ "bad-precondition"
16445
+ );
16446
+ }
16447
+ if (!current.pr_url) {
16448
+ throw new ScmMergeError(
16449
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16450
+ "no-pr-url"
16451
+ );
16452
+ }
16453
+ if (!current.branch || !current.parent_branch) {
16454
+ throw new ScmMergeError(
16455
+ "ci-green row missing branch / parent_branch; refusing to merge.",
16456
+ "bad-precondition"
16457
+ );
16458
+ }
16459
+ const ownerRepo = await getOwnerRepo(args.projectDir);
16460
+ if (!ownerRepo) {
16461
+ throw new ScmMergeError(
16462
+ "No GitHub remote found at origin.",
16463
+ "no-github-remote"
16464
+ );
16465
+ }
16466
+ const pullNumber = extractPullNumber(current.pr_url);
16467
+ if (!pullNumber) {
16468
+ throw new ScmMergeError(
16469
+ `Could not extract PR number from URL: ${current.pr_url}`,
16470
+ "bad-pr-url"
16471
+ );
16472
+ }
16473
+ const instance = args.instance ?? current.project_id;
16474
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16475
+ const wantMigrate = args.waitMigrate !== false;
16476
+ let authVerified = false;
16477
+ if (wantMigrate && args.verifyMigrateAuth) {
16478
+ const probe = await args.verifyMigrateAuth();
16479
+ if (!probe.ok) {
16480
+ throw new ScmMergeError(
16481
+ `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.`,
16482
+ "migrate-auth"
16483
+ );
16484
+ }
16485
+ authVerified = true;
16486
+ }
16487
+ const preflightNotes = [];
16488
+ {
16489
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
16490
+ try {
16491
+ const r = await refresh();
16492
+ if (r.refreshed) {
16493
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
16494
+ }
16495
+ } catch (err) {
16496
+ preflightNotes.push(
16497
+ `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').`
16498
+ );
16499
+ }
16500
+ }
16501
+ let paired;
16502
+ try {
16503
+ paired = await mergePairedPullRequest({
16504
+ ownerRepo,
16505
+ pullNumber,
16506
+ lakebaseInstance: instance,
16507
+ method: args.method ?? "squash"
16508
+ });
16509
+ } catch (err) {
16510
+ throw new ScmMergeError(
16511
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
16512
+ "merge-failed"
16513
+ );
16514
+ }
16515
+ const warnings = [...preflightNotes, ...paired.warnings];
16516
+ let localBranchDeleted = false;
16517
+ let headAfter = current.branch;
16518
+ if (!args.skipLocalCleanup) {
16519
+ const switchTo = args.switchTo ?? gitBase;
16520
+ const head = await getCurrentBranch({ cwd: args.projectDir });
16521
+ if (head === current.branch) {
16522
+ try {
16523
+ await exec2(`git checkout ${shellEscape2(switchTo)}`, {
16524
+ cwd: args.projectDir,
16525
+ timeout: 1e4
16140
16526
  });
16141
16527
  headAfter = switchTo;
16142
16528
  try {
@@ -16323,7 +16709,7 @@ init_esm_shims();
16323
16709
 
16324
16710
  // scripts/git/branches.ts
16325
16711
  init_esm_shims();
16326
- async function currentBranchName(cwd) {
16712
+ async function currentBranchName2(cwd) {
16327
16713
  try {
16328
16714
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16329
16715
  } catch {
@@ -16342,7 +16728,7 @@ async function listLocalBranches(args) {
16342
16728
  return [];
16343
16729
  }
16344
16730
  if (!raw) return [];
16345
- const current = await currentBranchName(cwd);
16731
+ const current = await currentBranchName2(cwd);
16346
16732
  return raw.split("\n").filter(Boolean).map((line) => {
16347
16733
  const [name, tracking, trackInfo] = line.split("|");
16348
16734
  let ahead = 0;
@@ -16542,15 +16928,15 @@ function parentForTopology(t, defaultLeaf) {
16542
16928
 
16543
16929
  // scripts/lakebase/scm-doctor.ts
16544
16930
  init_esm_shims();
16545
- import * as fs29 from "fs";
16546
- import * as path29 from "path";
16931
+ import * as fs30 from "fs";
16932
+ import * as path30 from "path";
16547
16933
  var FEATURE_PREFIX = "feature/";
16548
16934
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
16549
16935
  function readEnv(projectDir) {
16550
- const envPath = path29.join(projectDir, ".env");
16936
+ const envPath = path30.join(projectDir, ".env");
16551
16937
  const out = /* @__PURE__ */ new Map();
16552
- if (!fs29.existsSync(envPath)) return out;
16553
- const lines = fs29.readFileSync(envPath, "utf8").split("\n");
16938
+ if (!fs30.existsSync(envPath)) return out;
16939
+ const lines = fs30.readFileSync(envPath, "utf8").split("\n");
16554
16940
  for (const line of lines) {
16555
16941
  const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
16556
16942
  if (m) out.set(m[1], m[2].replace(/^["']|["']$/g, ""));
@@ -16605,9 +16991,9 @@ async function runDoctor(args, deps = {}) {
16605
16991
  }
16606
16992
  for (const wf of ["pr.yml", "merge.yml"]) {
16607
16993
  try {
16608
- const p = path29.join(projectDir, ".github", "workflows", wf);
16609
- if (!fs29.existsSync(p)) continue;
16610
- const body = fs29.readFileSync(p, "utf8");
16994
+ const p = path30.join(projectDir, ".github", "workflows", wf);
16995
+ if (!fs30.existsSync(p)) continue;
16996
+ const body = fs30.readFileSync(p, "utf8");
16611
16997
  if (/lakebase-scm-utils#v\d/.test(body)) {
16612
16998
  findings.push({
16613
16999
  id: "ci-workflow-substrate-pin",
@@ -16769,449 +17155,164 @@ async function runDoctor(args, deps = {}) {
16769
17155
  workflowStatePresent,
16770
17156
  state: state ?? void 0,
16771
17157
  inferredTierTopology: inferredTopology,
16772
- findings
16773
- });
16774
- }
16775
- function finalize(report) {
16776
- let worst = "ok";
16777
- for (const f of report.findings) {
16778
- worst = worstOf(worst, f.severity);
16779
- }
16780
- return { ...report, worstSeverity: worst };
16781
- }
16782
- var ScmDoctorFixError = class extends Error {
16783
- constructor(message, code) {
16784
- super(message);
16785
- this.code = code;
16786
- this.name = "ScmDoctorFixError";
16787
- }
16788
- code;
16789
- };
16790
- var FIXABLE_FINDING_IDS = [
16791
- "env-branch-drift",
16792
- "head-branch-drift",
16793
- "tier-topology-mismatch",
16794
- "orphan-current-branch",
16795
- "multiple-migration-heads",
16796
- "db-ahead-of-code"
16797
- ];
16798
- async function fixFinding(args) {
16799
- if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
16800
- throw new ScmDoctorFixError(
16801
- `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
16802
- "unsupported-finding"
16803
- );
16804
- }
16805
- const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
16806
- const present = report.findings.find((f) => f.id === args.findingId);
16807
- if (!present) {
16808
- throw new ScmDoctorFixError(
16809
- `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
16810
- "finding-not-present"
16811
- );
16812
- }
16813
- let action = "";
16814
- try {
16815
- switch (args.findingId) {
16816
- case "env-branch-drift": {
16817
- const branch = report.state?.branch;
16818
- if (!branch) {
16819
- throw new ScmDoctorFixError(
16820
- "Cannot fix: workflow state has no branch field.",
16821
- "fix-failed"
16822
- );
16823
- }
16824
- const sanitized = sanitizeBranchName(branch);
16825
- const envFile = path29.join(args.projectDir, ".env");
16826
- updateEnvConnection({
16827
- envPath: envFile,
16828
- projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
16829
- branchId: sanitized,
16830
- username: readEnvVar(envFile, "DB_USERNAME") ?? "",
16831
- endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
16832
- });
16833
- action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
16834
- break;
16835
- }
16836
- case "head-branch-drift": {
16837
- const branch = report.state?.branch;
16838
- if (!branch) {
16839
- throw new ScmDoctorFixError(
16840
- "Cannot fix: workflow state has no branch field.",
16841
- "fix-failed"
16842
- );
16843
- }
16844
- await exec2(`git checkout ${shellEscape3(branch)}`, {
16845
- cwd: args.projectDir,
16846
- timeout: 15e3
16847
- });
16848
- action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
16849
- break;
16850
- }
16851
- case "tier-topology-mismatch": {
16852
- const instance = args.instance ?? report.state?.project_id;
16853
- if (!instance) {
16854
- throw new ScmDoctorFixError(
16855
- "Cannot fix: missing Lakebase project id.",
16856
- "fix-failed"
16857
- );
16858
- }
16859
- await adoptScmState({
16860
- projectDir: args.projectDir,
16861
- instance,
16862
- force: true
16863
- });
16864
- action = `adopted state with --force to re-infer tier_topology`;
16865
- break;
16866
- }
16867
- case "orphan-current-branch": {
16868
- const instance = args.instance ?? report.state?.project_id;
16869
- if (!instance) {
16870
- throw new ScmDoctorFixError(
16871
- "Cannot fix: missing Lakebase project id.",
16872
- "fix-failed"
16873
- );
16874
- }
16875
- const headBranch = await getCurrentBranch({ cwd: args.projectDir });
16876
- if (!headBranch) {
16877
- throw new ScmDoctorFixError(
16878
- "Cannot fix: detached HEAD or no current branch.",
16879
- "fix-failed"
16880
- );
16881
- }
16882
- await recoverOrphans({
16883
- projectDir: args.projectDir,
16884
- instance,
16885
- claim: true,
16886
- onlyBranch: headBranch
16887
- });
16888
- action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
16889
- break;
16890
- }
16891
- case "multiple-migration-heads": {
16892
- const r = await collapseMigrationHeads({ projectDir: args.projectDir });
16893
- if (r.status !== "ok" || !r.mergeRevision) {
16894
- throw new ScmDoctorFixError(
16895
- `Expected to create a merge revision but got status="${r.status}".`,
16896
- "fix-failed"
16897
- );
16898
- }
16899
- action = `collapsed ${r.headsBefore.length} heads into merge revision ${r.mergeRevision} (commit it)`;
16900
- break;
16901
- }
16902
- case "db-ahead-of-code": {
16903
- const r = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
16904
- action = `abandoned the feature (deleted the polluted paired branch${r.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r.state.state}'; re-claim to re-fork a clean branch from the tier`;
16905
- break;
16906
- }
16907
- }
16908
- } catch (err) {
16909
- if (err instanceof ScmDoctorFixError) throw err;
16910
- throw new ScmDoctorFixError(
16911
- `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
16912
- "fix-failed"
16913
- );
16914
- }
16915
- const postReport = await runDoctor({
16916
- projectDir: args.projectDir,
16917
- instance: args.instance
16918
- });
16919
- return { findingId: args.findingId, action, postReport };
16920
- }
16921
- function shellEscape3(s) {
16922
- return `'${s.replace(/'/g, "'\\''")}'`;
16923
- }
16924
-
16925
- // scripts/lakebase/doctor.ts
16926
- init_esm_shims();
16927
- import * as fs31 from "fs";
16928
- import * as path31 from "path";
16929
- import * as cp6 from "child_process";
16930
-
16931
- // scripts/lakebase/workflow-drift.ts
16932
- init_esm_shims();
16933
- import * as fs30 from "fs";
16934
- import * as path30 from "path";
16935
- function findKitTemplatesDir(start) {
16936
- let dir = start;
16937
- for (let i = 0; i < 6; i++) {
16938
- const candidate = path30.join(
16939
- dir,
16940
- "templates",
16941
- "project",
16942
- "common",
16943
- ".github",
16944
- "workflows"
16945
- );
16946
- if (fs30.existsSync(candidate)) return candidate;
16947
- const parent = path30.dirname(dir);
16948
- if (parent === dir) break;
16949
- dir = parent;
16950
- }
16951
- throw new Error(
16952
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16953
- );
16954
- }
16955
- function unifiedDiff(name, projectContent, templateContent) {
16956
- if (projectContent === templateContent) return "";
16957
- const a = projectContent.split("\n");
16958
- const b = templateContent.split("\n");
16959
- const out = [`--- project/${name}`, `+++ template/${name}`];
16960
- const max = Math.max(a.length, b.length);
16961
- for (let i = 0; i < max; i++) {
16962
- const av = a[i];
16963
- const bv = b[i];
16964
- if (av === bv) continue;
16965
- if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16966
- if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16967
- }
16968
- return out.join("\n");
16969
- }
16970
- function detectWorkflowDrift(args) {
16971
- const projectWorkflowsDir = path30.join(
16972
- args.projectDir,
16973
- ".github",
16974
- "workflows"
16975
- );
16976
- const here = path30.dirname(new URL(import.meta.url).pathname);
16977
- const kitWorkflowsDir = args.kitDir ? path30.join(
16978
- args.kitDir,
16979
- "templates",
16980
- "project",
16981
- "common",
16982
- ".github",
16983
- "workflows"
16984
- ) : findKitTemplatesDir(here);
16985
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16986
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16987
- const version = readKitVersion(kitWorkflowsDir);
16988
- const seen = /* @__PURE__ */ new Set();
16989
- const files = [];
16990
- for (const name of templateFiles) {
16991
- seen.add(name);
16992
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16993
- const templatePath = path30.join(kitWorkflowsDir, name);
16994
- if (!fs30.existsSync(projectPath2)) {
16995
- files.push({ name, status: "missing" });
16996
- continue;
16997
- }
16998
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16999
- const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
17000
- if (projectContent === templateContent) {
17001
- files.push({ name, status: "unchanged" });
17002
- } else {
17003
- files.push({
17004
- name,
17005
- status: "drifted",
17006
- diff: unifiedDiff(name, projectContent, templateContent)
17007
- });
17008
- }
17009
- }
17010
- for (const name of projectFiles) {
17011
- if (seen.has(name)) continue;
17012
- files.push({ name, status: "extra" });
17013
- }
17014
- const order = {
17015
- drifted: 0,
17016
- missing: 1,
17017
- extra: 2,
17018
- unchanged: 3
17019
- };
17020
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
17021
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
17022
- return {
17023
- overall: hasDrift ? "drift" : "ok",
17024
- files
17025
- };
17026
- }
17027
- function readKitVersion(kitWorkflowsDir) {
17028
- let dir = kitWorkflowsDir;
17029
- for (let i = 0; i < 5; i++) {
17030
- dir = path30.dirname(dir);
17031
- }
17032
- try {
17033
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
17034
- const pkg = JSON.parse(raw);
17035
- return typeof pkg.version === "string" ? pkg.version : "unknown";
17036
- } catch {
17037
- return "unknown";
17038
- }
17039
- }
17040
- function applyPlaceholders(content, version) {
17041
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
17042
- }
17043
- function applyCommandPlaceholders(content, version) {
17044
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
17045
- }
17046
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
17047
- function findKitCommandsDir(start) {
17048
- let dir = start;
17049
- for (let i = 0; i < 6; i++) {
17050
- const candidate = path30.join(
17051
- dir,
17052
- "templates",
17053
- "project",
17054
- "common",
17055
- ".claude",
17056
- "commands"
17057
- );
17058
- if (fs30.existsSync(candidate)) return candidate;
17059
- const parent = path30.dirname(dir);
17060
- if (parent === dir) break;
17061
- dir = parent;
17062
- }
17063
- throw new Error(
17064
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
17065
- );
17066
- }
17067
- function parsePinnedVersion(content) {
17068
- const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
17069
- return m ? m[1] : void 0;
17070
- }
17071
- function detectCommandDrift(args) {
17072
- const projectCommandsDir = path30.join(args.projectDir, ".claude", "commands");
17073
- const here = path30.dirname(new URL(import.meta.url).pathname);
17074
- const kitCommandsDir = args.kitDir ? path30.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
17075
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
17076
- const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
17077
- const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
17078
- const seen = /* @__PURE__ */ new Set();
17079
- const files = [];
17080
- for (const name of templateFiles) {
17081
- seen.add(name);
17082
- const projectPath2 = path30.join(projectCommandsDir, name);
17083
- const templatePath = path30.join(kitCommandsDir, name);
17084
- const templateRaw = fs30.readFileSync(templatePath, "utf8");
17085
- if (!fs30.existsSync(projectPath2)) {
17086
- files.push({ name, status: "missing", kit_version: kitVersion });
17087
- continue;
17088
- }
17089
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
17090
- const pinned = parsePinnedVersion(projectContent);
17091
- const versionForCompare = pinned ?? kitVersion;
17092
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
17093
- if (projectContent === templateContent) {
17094
- files.push({
17095
- name,
17096
- status: "unchanged",
17097
- pinned_version: pinned,
17098
- kit_version: kitVersion
17099
- });
17100
- } else {
17101
- files.push({
17102
- name,
17103
- status: "drifted",
17104
- pinned_version: pinned,
17105
- kit_version: kitVersion,
17106
- diff: unifiedDiff(name, projectContent, templateContent)
17107
- });
17108
- }
17109
- }
17110
- for (const name of projectFiles) {
17111
- if (seen.has(name)) continue;
17112
- files.push({ name, status: "extra", kit_version: kitVersion });
17113
- }
17114
- const order = {
17115
- drifted: 0,
17116
- missing: 1,
17117
- extra: 2,
17118
- unchanged: 3
17119
- };
17120
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
17121
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
17122
- return { overall: hasDrift ? "drift" : "ok", files };
17123
- }
17124
- function readKitVersionFromCommandsDir(kitCommandsDir) {
17125
- let dir = kitCommandsDir;
17126
- for (let i = 0; i < 5; i++) {
17127
- dir = path30.dirname(dir);
17128
- }
17129
- try {
17130
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
17131
- const pkg = JSON.parse(raw);
17132
- return typeof pkg.version === "string" ? pkg.version : "unknown";
17133
- } catch {
17134
- return "unknown";
17135
- }
17158
+ findings
17159
+ });
17136
17160
  }
17137
- function detectScaffoldedDrift(args) {
17138
- const workflows = detectWorkflowDrift(args);
17139
- const commands = detectCommandDrift(args);
17140
- return {
17141
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
17142
- workflows,
17143
- commands
17144
- };
17161
+ function finalize(report) {
17162
+ let worst = "ok";
17163
+ for (const f of report.findings) {
17164
+ worst = worstOf(worst, f.severity);
17165
+ }
17166
+ return { ...report, worstSeverity: worst };
17145
17167
  }
17146
- function updateWorkflows(args) {
17147
- const projectWorkflowsDir = path30.join(
17148
- args.projectDir,
17149
- ".github",
17150
- "workflows"
17151
- );
17152
- const here = path30.dirname(new URL(import.meta.url).pathname);
17153
- const kitWorkflowsDir = args.kitDir ? path30.join(
17154
- args.kitDir,
17155
- "templates",
17156
- "project",
17157
- "common",
17158
- ".github",
17159
- "workflows"
17160
- ) : findKitTemplatesDir(here);
17161
- const substitute = args.substitute !== false;
17162
- const dryRun = args.dryRun === true;
17163
- const pruneExtras = args.pruneExtras === true;
17164
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
17165
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
17166
- if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
17167
- fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
17168
+ var ScmDoctorFixError = class extends Error {
17169
+ constructor(message, code) {
17170
+ super(message);
17171
+ this.code = code;
17172
+ this.name = "ScmDoctorFixError";
17168
17173
  }
17169
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
17170
- const seen = /* @__PURE__ */ new Set();
17171
- const files = [];
17172
- for (const name of templateFiles) {
17173
- seen.add(name);
17174
- const projectPath2 = path30.join(projectWorkflowsDir, name);
17175
- const templatePath = path30.join(kitWorkflowsDir, name);
17176
- const templateRaw = fs30.readFileSync(templatePath, "utf-8");
17177
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
17178
- const existed = fs30.existsSync(projectPath2);
17179
- const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
17180
- let outcome;
17181
- if (!existed) {
17182
- outcome = "added";
17183
- } else if (current === desired) {
17184
- outcome = "unchanged";
17185
- } else {
17186
- outcome = "updated";
17187
- }
17188
- if (!dryRun && outcome !== "unchanged") {
17189
- fs30.writeFileSync(projectPath2, desired);
17190
- }
17191
- files.push({ name, outcome });
17174
+ code;
17175
+ };
17176
+ var FIXABLE_FINDING_IDS = [
17177
+ "env-branch-drift",
17178
+ "head-branch-drift",
17179
+ "tier-topology-mismatch",
17180
+ "orphan-current-branch",
17181
+ "multiple-migration-heads",
17182
+ "db-ahead-of-code"
17183
+ ];
17184
+ async function fixFinding(args) {
17185
+ if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
17186
+ throw new ScmDoctorFixError(
17187
+ `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
17188
+ "unsupported-finding"
17189
+ );
17192
17190
  }
17193
- if (pruneExtras) {
17194
- for (const name of projectFiles) {
17195
- if (seen.has(name)) continue;
17196
- const projectPath2 = path30.join(projectWorkflowsDir, name);
17197
- if (!dryRun) {
17198
- fs30.unlinkSync(projectPath2);
17191
+ const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
17192
+ const present = report.findings.find((f) => f.id === args.findingId);
17193
+ if (!present) {
17194
+ throw new ScmDoctorFixError(
17195
+ `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
17196
+ "finding-not-present"
17197
+ );
17198
+ }
17199
+ let action = "";
17200
+ try {
17201
+ switch (args.findingId) {
17202
+ case "env-branch-drift": {
17203
+ const branch = report.state?.branch;
17204
+ if (!branch) {
17205
+ throw new ScmDoctorFixError(
17206
+ "Cannot fix: workflow state has no branch field.",
17207
+ "fix-failed"
17208
+ );
17209
+ }
17210
+ const sanitized = sanitizeBranchName(branch);
17211
+ const envFile = path30.join(args.projectDir, ".env");
17212
+ updateEnvConnection({
17213
+ envPath: envFile,
17214
+ projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
17215
+ branchId: sanitized,
17216
+ username: readEnvVar(envFile, "DB_USERNAME") ?? "",
17217
+ endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
17218
+ });
17219
+ action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
17220
+ break;
17221
+ }
17222
+ case "head-branch-drift": {
17223
+ const branch = report.state?.branch;
17224
+ if (!branch) {
17225
+ throw new ScmDoctorFixError(
17226
+ "Cannot fix: workflow state has no branch field.",
17227
+ "fix-failed"
17228
+ );
17229
+ }
17230
+ await exec2(`git checkout ${shellEscape3(branch)}`, {
17231
+ cwd: args.projectDir,
17232
+ timeout: 15e3
17233
+ });
17234
+ action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
17235
+ break;
17236
+ }
17237
+ case "tier-topology-mismatch": {
17238
+ const instance = args.instance ?? report.state?.project_id;
17239
+ if (!instance) {
17240
+ throw new ScmDoctorFixError(
17241
+ "Cannot fix: missing Lakebase project id.",
17242
+ "fix-failed"
17243
+ );
17244
+ }
17245
+ await adoptScmState({
17246
+ projectDir: args.projectDir,
17247
+ instance,
17248
+ force: true
17249
+ });
17250
+ action = `adopted state with --force to re-infer tier_topology`;
17251
+ break;
17252
+ }
17253
+ case "orphan-current-branch": {
17254
+ const instance = args.instance ?? report.state?.project_id;
17255
+ if (!instance) {
17256
+ throw new ScmDoctorFixError(
17257
+ "Cannot fix: missing Lakebase project id.",
17258
+ "fix-failed"
17259
+ );
17260
+ }
17261
+ const headBranch = await getCurrentBranch({ cwd: args.projectDir });
17262
+ if (!headBranch) {
17263
+ throw new ScmDoctorFixError(
17264
+ "Cannot fix: detached HEAD or no current branch.",
17265
+ "fix-failed"
17266
+ );
17267
+ }
17268
+ await recoverOrphans({
17269
+ projectDir: args.projectDir,
17270
+ instance,
17271
+ claim: true,
17272
+ onlyBranch: headBranch
17273
+ });
17274
+ action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
17275
+ break;
17276
+ }
17277
+ case "multiple-migration-heads": {
17278
+ const r = await collapseMigrationHeads({ projectDir: args.projectDir });
17279
+ if (r.status !== "ok" || !r.mergeRevision) {
17280
+ throw new ScmDoctorFixError(
17281
+ `Expected to create a merge revision but got status="${r.status}".`,
17282
+ "fix-failed"
17283
+ );
17284
+ }
17285
+ action = `collapsed ${r.headsBefore.length} heads into merge revision ${r.mergeRevision} (commit it)`;
17286
+ break;
17287
+ }
17288
+ case "db-ahead-of-code": {
17289
+ const r = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
17290
+ action = `abandoned the feature (deleted the polluted paired branch${r.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r.state.state}'; re-claim to re-fork a clean branch from the tier`;
17291
+ break;
17199
17292
  }
17200
- files.push({ name, outcome: "removed" });
17201
17293
  }
17294
+ } catch (err) {
17295
+ if (err instanceof ScmDoctorFixError) throw err;
17296
+ throw new ScmDoctorFixError(
17297
+ `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
17298
+ "fix-failed"
17299
+ );
17202
17300
  }
17203
- const order = {
17204
- added: 0,
17205
- updated: 1,
17206
- removed: 2,
17207
- unchanged: 3
17208
- };
17209
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
17210
- const changed = files.some((f) => f.outcome !== "unchanged");
17211
- return { files, changed };
17301
+ const postReport = await runDoctor({
17302
+ projectDir: args.projectDir,
17303
+ instance: args.instance
17304
+ });
17305
+ return { findingId: args.findingId, action, postReport };
17306
+ }
17307
+ function shellEscape3(s) {
17308
+ return `'${s.replace(/'/g, "'\\''")}'`;
17212
17309
  }
17213
17310
 
17214
17311
  // scripts/lakebase/doctor.ts
17312
+ init_esm_shims();
17313
+ import * as fs31 from "fs";
17314
+ import * as path31 from "path";
17315
+ import * as cp6 from "child_process";
17215
17316
  function readEnvFile(projectDir) {
17216
17317
  const envPath = path31.join(projectDir, ".env");
17217
17318
  if (!fs31.existsSync(envPath)) return {};
@@ -17644,7 +17745,7 @@ function checkWorkflowDrift(projectDir) {
17644
17745
  status: "warn",
17645
17746
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
17646
17747
  detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
17647
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
17748
+ 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."
17648
17749
  };
17649
17750
  } catch (err) {
17650
17751
  return {
@@ -18120,63 +18221,6 @@ async function discardAllChanges(args) {
18120
18221
  await exec2("git clean -fd", { cwd: args.cwd });
18121
18222
  }
18122
18223
 
18123
- // scripts/git/sync.ts
18124
- init_esm_shims();
18125
- async function currentBranchName2(cwd) {
18126
- try {
18127
- return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
18128
- } catch {
18129
- return "";
18130
- }
18131
- }
18132
- async function push(args) {
18133
- await exec2("git push", { cwd: args.cwd });
18134
- }
18135
- async function pull(args) {
18136
- await exec2("git pull", { cwd: args.cwd });
18137
- }
18138
- async function publishBranch(args) {
18139
- const remote = args.remote ?? "origin";
18140
- const branch = await currentBranchName2(args.cwd);
18141
- if (!branch) throw new Error("No current branch");
18142
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
18143
- cwd: args.cwd
18144
- });
18145
- }
18146
- async function pushCurrentBranchForPr(args) {
18147
- const remote = args.remote ?? "origin";
18148
- const branch = await currentBranchName2(args.cwd);
18149
- if (!branch) throw new Error("No current branch");
18150
- const upstreamSet = await hasUpstream({ cwd: args.cwd });
18151
- if (!upstreamSet) {
18152
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
18153
- cwd: args.cwd
18154
- });
18155
- } else {
18156
- await exec2("git push", { cwd: args.cwd });
18157
- }
18158
- }
18159
- async function fetch2(args) {
18160
- const parts = ["git fetch"];
18161
- if (args.prune) parts.push("--prune");
18162
- if (args.all) parts.push("--all");
18163
- await exec2(parts.join(" "), { cwd: args.cwd });
18164
- }
18165
- async function pullFrom(args) {
18166
- await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
18167
- cwd: args.cwd
18168
- });
18169
- }
18170
- async function pushTo(args) {
18171
- await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
18172
- cwd: args.cwd
18173
- });
18174
- }
18175
- async function sync(args) {
18176
- await exec2("git pull", { cwd: args.cwd });
18177
- await exec2("git push", { cwd: args.cwd });
18178
- }
18179
-
18180
18224
  // scripts/git/branch-tag.ts
18181
18225
  init_esm_shims();
18182
18226
  var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["production", "main", "master"]);
@@ -18581,6 +18625,7 @@ export {
18581
18625
  createWorktree,
18582
18626
  cutBackup,
18583
18627
  databricksAuthPrereqMessage,
18628
+ defaultRefreshPromoteWorkflows,
18584
18629
  delay,
18585
18630
  deleteAppEndpoint,
18586
18631
  deleteBranch,