@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
@@ -79021,6 +79021,7 @@ __export(scripts_exports, {
79021
79021
  createWorktree: () => createWorktree,
79022
79022
  cutBackup: () => cutBackup,
79023
79023
  databricksAuthPrereqMessage: () => databricksAuthPrereqMessage,
79024
+ defaultRefreshPromoteWorkflows: () => defaultRefreshPromoteWorkflows,
79024
79025
  delay: () => delay,
79025
79026
  deleteAppEndpoint: () => deleteAppEndpoint,
79026
79027
  deleteBranch: () => deleteBranch,
@@ -88905,6 +88906,7 @@ var RUNTIME_ARTIFACT_IGNORE = [
88905
88906
  var DEFAULT_ENDPOINT = "primary";
88906
88907
  var CONSORT_APPLICATION_NAME = "consort";
88907
88908
  var SCM_UTILS_APPLICATION_NAME = "scm-utils";
88909
+ var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
88908
88910
  var CONSORT_VERSION_ENV = "CONSORT_VERSION";
88909
88911
 
88910
88912
  // scripts/lakebase/self-version.ts
@@ -88916,8 +88918,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88916
88918
  var cached;
88917
88919
  function substrateSelfVersion() {
88918
88920
  if (cached !== void 0) return cached;
88919
- if ("0.2.44".length > 0) {
88920
- cached = "0.2.44";
88921
+ if ("0.2.46".length > 0) {
88922
+ cached = "0.2.46";
88921
88923
  return cached;
88922
88924
  }
88923
88925
  cached = "unknown";
@@ -88945,9 +88947,17 @@ function substrateSelfVersion() {
88945
88947
  }
88946
88948
 
88947
88949
  // scripts/lakebase/get-connection.ts
88950
+ function sanitizeApplicationName(raw) {
88951
+ const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
88952
+ if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
88953
+ return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
88954
+ }
88948
88955
  function connectionApplicationName() {
88956
+ const client = process.env[SCM_CLIENT_ENV]?.trim();
88957
+ if (client) return sanitizeApplicationName(client);
88949
88958
  const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
88950
- return consortVersion ? `${CONSORT_APPLICATION_NAME}/${consortVersion}` : `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
88959
+ if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
88960
+ return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
88951
88961
  }
88952
88962
  async function getConnection(args) {
88953
88963
  const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
@@ -95218,122 +95228,499 @@ function pickRunUrl(pr) {
95218
95228
 
95219
95229
  // scripts/lakebase/scm-merge.ts
95220
95230
  init_cjs_shims();
95221
- async function reconcileTierToOrigin(args) {
95222
- const { cwd, tier } = args;
95231
+
95232
+ // scripts/git/sync.ts
95233
+ init_cjs_shims();
95234
+ async function currentBranchName(cwd) {
95223
95235
  try {
95224
- const head = await getCurrentBranch({ cwd });
95225
- if (head === tier) {
95226
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
95227
- } else {
95228
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
95229
- }
95230
- return null;
95231
- } catch (err) {
95232
- 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).`;
95236
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
95237
+ } catch {
95238
+ return "";
95233
95239
  }
95234
95240
  }
95235
- var ScmMergeError = class extends Error {
95236
- constructor(message, code) {
95237
- super(message);
95238
- this.code = code;
95239
- this.name = "ScmMergeError";
95240
- }
95241
- code;
95242
- };
95243
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
95244
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
95245
- function defaultMigratePredicate(run, mergedAt) {
95246
- if (!run.createdAt) return false;
95247
- const created = Date.parse(run.createdAt);
95248
- if (!Number.isFinite(created)) return false;
95249
- if (run.event && run.event !== "push") return false;
95250
- return created >= mergedAt.getTime() - 5e3;
95241
+ async function push(args) {
95242
+ await exec2("git push", { cwd: args.cwd });
95251
95243
  }
95252
- function shaMigratePredicate(mergeCommitSha) {
95253
- return (run) => {
95254
- if (run.event && run.event !== "push") return false;
95255
- return !!run.headSha && run.headSha === mergeCommitSha;
95256
- };
95244
+ async function pull(args) {
95245
+ await exec2("git pull", { cwd: args.cwd });
95257
95246
  }
95258
- async function mergeFeature(args) {
95259
- const current = readWorkflowState(args.projectDir);
95260
- if (!current) {
95261
- throw new ScmMergeError(
95262
- "No SCM workflow state. wait-ci first.",
95263
- "no-state-file"
95264
- );
95265
- }
95266
- if (current.state !== "ci-green") {
95267
- throw new ScmMergeError(
95268
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
95269
- "bad-precondition"
95270
- );
95247
+ async function publishBranch(args) {
95248
+ const remote = args.remote ?? "origin";
95249
+ const branch = await currentBranchName(args.cwd);
95250
+ if (!branch) throw new Error("No current branch");
95251
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
95252
+ cwd: args.cwd
95253
+ });
95254
+ }
95255
+ async function pushCurrentBranchForPr(args) {
95256
+ const remote = args.remote ?? "origin";
95257
+ const branch = await currentBranchName(args.cwd);
95258
+ if (!branch) throw new Error("No current branch");
95259
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
95260
+ if (!upstreamSet) {
95261
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
95262
+ cwd: args.cwd
95263
+ });
95264
+ } else {
95265
+ await exec2("git push", { cwd: args.cwd });
95271
95266
  }
95272
- if (!current.pr_url) {
95273
- throw new ScmMergeError(
95274
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
95275
- "no-pr-url"
95267
+ }
95268
+ async function fetch3(args) {
95269
+ const parts = ["git fetch"];
95270
+ if (args.prune) parts.push("--prune");
95271
+ if (args.all) parts.push("--all");
95272
+ await exec2(parts.join(" "), { cwd: args.cwd });
95273
+ }
95274
+ async function pullFrom(args) {
95275
+ await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
95276
+ cwd: args.cwd
95277
+ });
95278
+ }
95279
+ async function pushTo(args) {
95280
+ await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
95281
+ cwd: args.cwd
95282
+ });
95283
+ }
95284
+ async function sync(args) {
95285
+ await exec2("git pull", { cwd: args.cwd });
95286
+ await exec2("git push", { cwd: args.cwd });
95287
+ }
95288
+
95289
+ // scripts/lakebase/workflow-drift.ts
95290
+ init_cjs_shims();
95291
+ var fs30 = __toESM(require("fs"), 1);
95292
+ var path28 = __toESM(require("path"), 1);
95293
+ function findKitTemplatesDir(start) {
95294
+ let dir = start;
95295
+ for (let i2 = 0; i2 < 6; i2++) {
95296
+ const candidate = path28.join(
95297
+ dir,
95298
+ "templates",
95299
+ "project",
95300
+ "common",
95301
+ ".github",
95302
+ "workflows"
95276
95303
  );
95304
+ if (fs30.existsSync(candidate)) return candidate;
95305
+ const parent = path28.dirname(dir);
95306
+ if (parent === dir) break;
95307
+ dir = parent;
95277
95308
  }
95278
- if (!current.branch || !current.parent_branch) {
95279
- throw new ScmMergeError(
95280
- "ci-green row missing branch / parent_branch; refusing to merge.",
95281
- "bad-precondition"
95282
- );
95309
+ throw new Error(
95310
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
95311
+ );
95312
+ }
95313
+ function unifiedDiff(name, projectContent, templateContent) {
95314
+ if (projectContent === templateContent) return "";
95315
+ const a = projectContent.split("\n");
95316
+ const b = templateContent.split("\n");
95317
+ const out = [`--- project/${name}`, `+++ template/${name}`];
95318
+ const max = Math.max(a.length, b.length);
95319
+ for (let i2 = 0; i2 < max; i2++) {
95320
+ const av = a[i2];
95321
+ const bv = b[i2];
95322
+ if (av === bv) continue;
95323
+ if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
95324
+ if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
95283
95325
  }
95284
- const ownerRepo = await getOwnerRepo(args.projectDir);
95285
- if (!ownerRepo) {
95286
- throw new ScmMergeError(
95287
- "No GitHub remote found at origin.",
95288
- "no-github-remote"
95289
- );
95326
+ return out.join("\n");
95327
+ }
95328
+ function detectWorkflowDrift(args) {
95329
+ const projectWorkflowsDir = path28.join(
95330
+ args.projectDir,
95331
+ ".github",
95332
+ "workflows"
95333
+ );
95334
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95335
+ const kitWorkflowsDir = args.kitDir ? path28.join(
95336
+ args.kitDir,
95337
+ "templates",
95338
+ "project",
95339
+ "common",
95340
+ ".github",
95341
+ "workflows"
95342
+ ) : findKitTemplatesDir(here);
95343
+ const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95344
+ const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95345
+ const version = readKitVersion(kitWorkflowsDir);
95346
+ const seen = /* @__PURE__ */ new Set();
95347
+ const files = [];
95348
+ for (const name of templateFiles) {
95349
+ seen.add(name);
95350
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95351
+ const templatePath = path28.join(kitWorkflowsDir, name);
95352
+ if (!fs30.existsSync(projectPath2)) {
95353
+ files.push({ name, status: "missing" });
95354
+ continue;
95355
+ }
95356
+ const projectContent = fs30.readFileSync(projectPath2, "utf8");
95357
+ const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
95358
+ if (projectContent === templateContent) {
95359
+ files.push({ name, status: "unchanged" });
95360
+ } else {
95361
+ files.push({
95362
+ name,
95363
+ status: "drifted",
95364
+ diff: unifiedDiff(name, projectContent, templateContent)
95365
+ });
95366
+ }
95290
95367
  }
95291
- const pullNumber = extractPullNumber(current.pr_url);
95292
- if (!pullNumber) {
95293
- throw new ScmMergeError(
95294
- `Could not extract PR number from URL: ${current.pr_url}`,
95295
- "bad-pr-url"
95296
- );
95368
+ for (const name of projectFiles) {
95369
+ if (seen.has(name)) continue;
95370
+ files.push({ name, status: "extra" });
95297
95371
  }
95298
- const instance = args.instance ?? current.project_id;
95299
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
95300
- const wantMigrate = args.waitMigrate !== false;
95301
- let authVerified = false;
95302
- if (wantMigrate && args.verifyMigrateAuth) {
95303
- const probe = await args.verifyMigrateAuth();
95304
- if (!probe.ok) {
95305
- throw new ScmMergeError(
95306
- `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.`,
95307
- "migrate-auth"
95308
- );
95309
- }
95310
- authVerified = true;
95372
+ const order = {
95373
+ drifted: 0,
95374
+ missing: 1,
95375
+ extra: 2,
95376
+ unchanged: 3
95377
+ };
95378
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95379
+ const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95380
+ return {
95381
+ overall: hasDrift ? "drift" : "ok",
95382
+ files
95383
+ };
95384
+ }
95385
+ function readKitVersion(kitWorkflowsDir) {
95386
+ let dir = kitWorkflowsDir;
95387
+ for (let i2 = 0; i2 < 5; i2++) {
95388
+ dir = path28.dirname(dir);
95311
95389
  }
95312
- let paired;
95313
95390
  try {
95314
- paired = await mergePairedPullRequest({
95315
- ownerRepo,
95316
- pullNumber,
95317
- lakebaseInstance: instance,
95318
- method: args.method ?? "squash"
95319
- });
95320
- } catch (err) {
95321
- throw new ScmMergeError(
95322
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
95323
- "merge-failed"
95391
+ const raw = fs30.readFileSync(path28.join(dir, "package.json"), "utf-8");
95392
+ const pkg = JSON.parse(raw);
95393
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
95394
+ } catch {
95395
+ return "unknown";
95396
+ }
95397
+ }
95398
+ function applyPlaceholders(content, version) {
95399
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
95400
+ }
95401
+ function applyCommandPlaceholders(content, version) {
95402
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
95403
+ }
95404
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
95405
+ function findKitCommandsDir(start) {
95406
+ let dir = start;
95407
+ for (let i2 = 0; i2 < 6; i2++) {
95408
+ const candidate = path28.join(
95409
+ dir,
95410
+ "templates",
95411
+ "project",
95412
+ "common",
95413
+ ".claude",
95414
+ "commands"
95324
95415
  );
95416
+ if (fs30.existsSync(candidate)) return candidate;
95417
+ const parent = path28.dirname(dir);
95418
+ if (parent === dir) break;
95419
+ dir = parent;
95325
95420
  }
95326
- const warnings = [...paired.warnings];
95327
- let localBranchDeleted = false;
95328
- let headAfter = current.branch;
95329
- if (!args.skipLocalCleanup) {
95330
- const switchTo = args.switchTo ?? gitBase;
95331
- const head = await getCurrentBranch({ cwd: args.projectDir });
95332
- if (head === current.branch) {
95333
- try {
95334
- await exec2(`git checkout ${shellEscape2(switchTo)}`, {
95335
- cwd: args.projectDir,
95336
- timeout: 1e4
95421
+ throw new Error(
95422
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
95423
+ );
95424
+ }
95425
+ function parsePinnedVersion(content) {
95426
+ const m2 = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
95427
+ return m2 ? m2[1] : void 0;
95428
+ }
95429
+ function detectCommandDrift(args) {
95430
+ const projectCommandsDir = path28.join(args.projectDir, ".claude", "commands");
95431
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95432
+ const kitCommandsDir = args.kitDir ? path28.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
95433
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
95434
+ const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95435
+ const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95436
+ const seen = /* @__PURE__ */ new Set();
95437
+ const files = [];
95438
+ for (const name of templateFiles) {
95439
+ seen.add(name);
95440
+ const projectPath2 = path28.join(projectCommandsDir, name);
95441
+ const templatePath = path28.join(kitCommandsDir, name);
95442
+ const templateRaw = fs30.readFileSync(templatePath, "utf8");
95443
+ if (!fs30.existsSync(projectPath2)) {
95444
+ files.push({ name, status: "missing", kit_version: kitVersion });
95445
+ continue;
95446
+ }
95447
+ const projectContent = fs30.readFileSync(projectPath2, "utf8");
95448
+ const pinned = parsePinnedVersion(projectContent);
95449
+ const versionForCompare = pinned ?? kitVersion;
95450
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
95451
+ if (projectContent === templateContent) {
95452
+ files.push({
95453
+ name,
95454
+ status: "unchanged",
95455
+ pinned_version: pinned,
95456
+ kit_version: kitVersion
95457
+ });
95458
+ } else {
95459
+ files.push({
95460
+ name,
95461
+ status: "drifted",
95462
+ pinned_version: pinned,
95463
+ kit_version: kitVersion,
95464
+ diff: unifiedDiff(name, projectContent, templateContent)
95465
+ });
95466
+ }
95467
+ }
95468
+ for (const name of projectFiles) {
95469
+ if (seen.has(name)) continue;
95470
+ files.push({ name, status: "extra", kit_version: kitVersion });
95471
+ }
95472
+ const order = {
95473
+ drifted: 0,
95474
+ missing: 1,
95475
+ extra: 2,
95476
+ unchanged: 3
95477
+ };
95478
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95479
+ const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95480
+ return { overall: hasDrift ? "drift" : "ok", files };
95481
+ }
95482
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
95483
+ let dir = kitCommandsDir;
95484
+ for (let i2 = 0; i2 < 5; i2++) {
95485
+ dir = path28.dirname(dir);
95486
+ }
95487
+ try {
95488
+ const raw = fs30.readFileSync(path28.join(dir, "package.json"), "utf-8");
95489
+ const pkg = JSON.parse(raw);
95490
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
95491
+ } catch {
95492
+ return "unknown";
95493
+ }
95494
+ }
95495
+ function detectScaffoldedDrift(args) {
95496
+ const workflows = detectWorkflowDrift(args);
95497
+ const commands = detectCommandDrift(args);
95498
+ return {
95499
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
95500
+ workflows,
95501
+ commands
95502
+ };
95503
+ }
95504
+ function updateWorkflows(args) {
95505
+ const projectWorkflowsDir = path28.join(
95506
+ args.projectDir,
95507
+ ".github",
95508
+ "workflows"
95509
+ );
95510
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95511
+ const kitWorkflowsDir = args.kitDir ? path28.join(
95512
+ args.kitDir,
95513
+ "templates",
95514
+ "project",
95515
+ "common",
95516
+ ".github",
95517
+ "workflows"
95518
+ ) : findKitTemplatesDir(here);
95519
+ const substitute = args.substitute !== false;
95520
+ const dryRun = args.dryRun === true;
95521
+ const pruneExtras = args.pruneExtras === true;
95522
+ const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95523
+ const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95524
+ if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
95525
+ fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
95526
+ }
95527
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
95528
+ const seen = /* @__PURE__ */ new Set();
95529
+ const files = [];
95530
+ for (const name of templateFiles) {
95531
+ seen.add(name);
95532
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95533
+ const templatePath = path28.join(kitWorkflowsDir, name);
95534
+ const templateRaw = fs30.readFileSync(templatePath, "utf-8");
95535
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
95536
+ const existed = fs30.existsSync(projectPath2);
95537
+ const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
95538
+ let outcome;
95539
+ if (!existed) {
95540
+ outcome = "added";
95541
+ } else if (current === desired) {
95542
+ outcome = "unchanged";
95543
+ } else {
95544
+ outcome = "updated";
95545
+ }
95546
+ if (!dryRun && outcome !== "unchanged") {
95547
+ fs30.writeFileSync(projectPath2, desired);
95548
+ }
95549
+ files.push({ name, outcome });
95550
+ }
95551
+ if (pruneExtras) {
95552
+ for (const name of projectFiles) {
95553
+ if (seen.has(name)) continue;
95554
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95555
+ if (!dryRun) {
95556
+ fs30.unlinkSync(projectPath2);
95557
+ }
95558
+ files.push({ name, outcome: "removed" });
95559
+ }
95560
+ }
95561
+ const order = {
95562
+ added: 0,
95563
+ updated: 1,
95564
+ removed: 2,
95565
+ unchanged: 3
95566
+ };
95567
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
95568
+ const changed = files.some((f3) => f3.outcome !== "unchanged");
95569
+ return { files, changed };
95570
+ }
95571
+
95572
+ // scripts/lakebase/scm-merge.ts
95573
+ async function reconcileTierToOrigin(args) {
95574
+ const { cwd, tier } = args;
95575
+ try {
95576
+ const head = await getCurrentBranch({ cwd });
95577
+ if (head === tier) {
95578
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
95579
+ } else {
95580
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
95581
+ }
95582
+ return null;
95583
+ } catch (err) {
95584
+ 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).`;
95585
+ }
95586
+ }
95587
+ var ScmMergeError = class extends Error {
95588
+ constructor(message, code) {
95589
+ super(message);
95590
+ this.code = code;
95591
+ this.name = "ScmMergeError";
95592
+ }
95593
+ code;
95594
+ };
95595
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
95596
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
95597
+ function defaultMigratePredicate(run, mergedAt) {
95598
+ if (!run.createdAt) return false;
95599
+ const created = Date.parse(run.createdAt);
95600
+ if (!Number.isFinite(created)) return false;
95601
+ if (run.event && run.event !== "push") return false;
95602
+ return created >= mergedAt.getTime() - 5e3;
95603
+ }
95604
+ function shaMigratePredicate(mergeCommitSha) {
95605
+ return (run) => {
95606
+ if (run.event && run.event !== "push") return false;
95607
+ return !!run.headSha && run.headSha === mergeCommitSha;
95608
+ };
95609
+ }
95610
+ async function defaultRefreshPromoteWorkflows(projectDir) {
95611
+ const drift = detectWorkflowDrift({ projectDir });
95612
+ const touched = drift.files.filter((f3) => f3.status === "drifted").map((f3) => f3.name);
95613
+ if (touched.length === 0) return { refreshed: false };
95614
+ const result = updateWorkflows({ projectDir });
95615
+ if (!result.changed) return { refreshed: false };
95616
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
95617
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
95618
+ cwd: projectDir,
95619
+ timeout: 1e4
95620
+ });
95621
+ if (!staged.trim()) return { refreshed: false };
95622
+ await exec2(
95623
+ `git commit -m ${shellEscape2(
95624
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
95625
+ )} -- .github/workflows`,
95626
+ { cwd: projectDir, timeout: 15e3 }
95627
+ );
95628
+ await pushCurrentBranchForPr({ cwd: projectDir });
95629
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
95630
+ }
95631
+ async function mergeFeature(args) {
95632
+ const current = readWorkflowState(args.projectDir);
95633
+ if (!current) {
95634
+ throw new ScmMergeError(
95635
+ "No SCM workflow state. wait-ci first.",
95636
+ "no-state-file"
95637
+ );
95638
+ }
95639
+ if (current.state !== "ci-green") {
95640
+ throw new ScmMergeError(
95641
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
95642
+ "bad-precondition"
95643
+ );
95644
+ }
95645
+ if (!current.pr_url) {
95646
+ throw new ScmMergeError(
95647
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
95648
+ "no-pr-url"
95649
+ );
95650
+ }
95651
+ if (!current.branch || !current.parent_branch) {
95652
+ throw new ScmMergeError(
95653
+ "ci-green row missing branch / parent_branch; refusing to merge.",
95654
+ "bad-precondition"
95655
+ );
95656
+ }
95657
+ const ownerRepo = await getOwnerRepo(args.projectDir);
95658
+ if (!ownerRepo) {
95659
+ throw new ScmMergeError(
95660
+ "No GitHub remote found at origin.",
95661
+ "no-github-remote"
95662
+ );
95663
+ }
95664
+ const pullNumber = extractPullNumber(current.pr_url);
95665
+ if (!pullNumber) {
95666
+ throw new ScmMergeError(
95667
+ `Could not extract PR number from URL: ${current.pr_url}`,
95668
+ "bad-pr-url"
95669
+ );
95670
+ }
95671
+ const instance = args.instance ?? current.project_id;
95672
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
95673
+ const wantMigrate = args.waitMigrate !== false;
95674
+ let authVerified = false;
95675
+ if (wantMigrate && args.verifyMigrateAuth) {
95676
+ const probe = await args.verifyMigrateAuth();
95677
+ if (!probe.ok) {
95678
+ throw new ScmMergeError(
95679
+ `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.`,
95680
+ "migrate-auth"
95681
+ );
95682
+ }
95683
+ authVerified = true;
95684
+ }
95685
+ const preflightNotes = [];
95686
+ {
95687
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
95688
+ try {
95689
+ const r2 = await refresh();
95690
+ if (r2.refreshed) {
95691
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r2.detail ? ` (${r2.detail})` : ""}.`);
95692
+ }
95693
+ } catch (err) {
95694
+ preflightNotes.push(
95695
+ `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').`
95696
+ );
95697
+ }
95698
+ }
95699
+ let paired;
95700
+ try {
95701
+ paired = await mergePairedPullRequest({
95702
+ ownerRepo,
95703
+ pullNumber,
95704
+ lakebaseInstance: instance,
95705
+ method: args.method ?? "squash"
95706
+ });
95707
+ } catch (err) {
95708
+ throw new ScmMergeError(
95709
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
95710
+ "merge-failed"
95711
+ );
95712
+ }
95713
+ const warnings = [...preflightNotes, ...paired.warnings];
95714
+ let localBranchDeleted = false;
95715
+ let headAfter = current.branch;
95716
+ if (!args.skipLocalCleanup) {
95717
+ const switchTo = args.switchTo ?? gitBase;
95718
+ const head = await getCurrentBranch({ cwd: args.projectDir });
95719
+ if (head === current.branch) {
95720
+ try {
95721
+ await exec2(`git checkout ${shellEscape2(switchTo)}`, {
95722
+ cwd: args.projectDir,
95723
+ timeout: 1e4
95337
95724
  });
95338
95725
  headAfter = switchTo;
95339
95726
  try {
@@ -95520,7 +95907,7 @@ init_cjs_shims();
95520
95907
 
95521
95908
  // scripts/git/branches.ts
95522
95909
  init_cjs_shims();
95523
- async function currentBranchName(cwd) {
95910
+ async function currentBranchName2(cwd) {
95524
95911
  try {
95525
95912
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
95526
95913
  } catch {
@@ -95539,7 +95926,7 @@ async function listLocalBranches(args) {
95539
95926
  return [];
95540
95927
  }
95541
95928
  if (!raw) return [];
95542
- const current = await currentBranchName(cwd);
95929
+ const current = await currentBranchName2(cwd);
95543
95930
  return raw.split("\n").filter(Boolean).map((line) => {
95544
95931
  const [name, tracking, trackInfo] = line.split("|");
95545
95932
  let ahead = 0;
@@ -95739,15 +96126,15 @@ function parentForTopology(t2, defaultLeaf) {
95739
96126
 
95740
96127
  // scripts/lakebase/scm-doctor.ts
95741
96128
  init_cjs_shims();
95742
- var fs30 = __toESM(require("fs"), 1);
95743
- var path28 = __toESM(require("path"), 1);
96129
+ var fs31 = __toESM(require("fs"), 1);
96130
+ var path29 = __toESM(require("path"), 1);
95744
96131
  var FEATURE_PREFIX = "feature/";
95745
96132
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
95746
96133
  function readEnv(projectDir) {
95747
- const envPath = path28.join(projectDir, ".env");
96134
+ const envPath = path29.join(projectDir, ".env");
95748
96135
  const out = /* @__PURE__ */ new Map();
95749
- if (!fs30.existsSync(envPath)) return out;
95750
- const lines = fs30.readFileSync(envPath, "utf8").split("\n");
96136
+ if (!fs31.existsSync(envPath)) return out;
96137
+ const lines = fs31.readFileSync(envPath, "utf8").split("\n");
95751
96138
  for (const line of lines) {
95752
96139
  const m2 = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
95753
96140
  if (m2) out.set(m2[1], m2[2].replace(/^["']|["']$/g, ""));
@@ -95802,9 +96189,9 @@ async function runDoctor(args, deps = {}) {
95802
96189
  }
95803
96190
  for (const wf of ["pr.yml", "merge.yml"]) {
95804
96191
  try {
95805
- const p = path28.join(projectDir, ".github", "workflows", wf);
95806
- if (!fs30.existsSync(p)) continue;
95807
- const body = fs30.readFileSync(p, "utf8");
96192
+ const p = path29.join(projectDir, ".github", "workflows", wf);
96193
+ if (!fs31.existsSync(p)) continue;
96194
+ const body = fs31.readFileSync(p, "utf8");
95808
96195
  if (/lakebase-scm-utils#v\d/.test(body)) {
95809
96196
  findings.push({
95810
96197
  id: "ci-workflow-substrate-pin",
@@ -95966,449 +96353,164 @@ async function runDoctor(args, deps = {}) {
95966
96353
  workflowStatePresent,
95967
96354
  state: state ?? void 0,
95968
96355
  inferredTierTopology: inferredTopology,
95969
- findings
95970
- });
95971
- }
95972
- function finalize(report) {
95973
- let worst = "ok";
95974
- for (const f3 of report.findings) {
95975
- worst = worstOf(worst, f3.severity);
95976
- }
95977
- return { ...report, worstSeverity: worst };
95978
- }
95979
- var ScmDoctorFixError = class extends Error {
95980
- constructor(message, code) {
95981
- super(message);
95982
- this.code = code;
95983
- this.name = "ScmDoctorFixError";
95984
- }
95985
- code;
95986
- };
95987
- var FIXABLE_FINDING_IDS = [
95988
- "env-branch-drift",
95989
- "head-branch-drift",
95990
- "tier-topology-mismatch",
95991
- "orphan-current-branch",
95992
- "multiple-migration-heads",
95993
- "db-ahead-of-code"
95994
- ];
95995
- async function fixFinding(args) {
95996
- if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
95997
- throw new ScmDoctorFixError(
95998
- `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
95999
- "unsupported-finding"
96000
- );
96001
- }
96002
- const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
96003
- const present = report.findings.find((f3) => f3.id === args.findingId);
96004
- if (!present) {
96005
- throw new ScmDoctorFixError(
96006
- `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
96007
- "finding-not-present"
96008
- );
96009
- }
96010
- let action = "";
96011
- try {
96012
- switch (args.findingId) {
96013
- case "env-branch-drift": {
96014
- const branch = report.state?.branch;
96015
- if (!branch) {
96016
- throw new ScmDoctorFixError(
96017
- "Cannot fix: workflow state has no branch field.",
96018
- "fix-failed"
96019
- );
96020
- }
96021
- const sanitized = sanitizeBranchName(branch);
96022
- const envFile = path28.join(args.projectDir, ".env");
96023
- updateEnvConnection({
96024
- envPath: envFile,
96025
- projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
96026
- branchId: sanitized,
96027
- username: readEnvVar(envFile, "DB_USERNAME") ?? "",
96028
- endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
96029
- });
96030
- action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
96031
- break;
96032
- }
96033
- case "head-branch-drift": {
96034
- const branch = report.state?.branch;
96035
- if (!branch) {
96036
- throw new ScmDoctorFixError(
96037
- "Cannot fix: workflow state has no branch field.",
96038
- "fix-failed"
96039
- );
96040
- }
96041
- await exec2(`git checkout ${shellEscape3(branch)}`, {
96042
- cwd: args.projectDir,
96043
- timeout: 15e3
96044
- });
96045
- action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
96046
- break;
96047
- }
96048
- case "tier-topology-mismatch": {
96049
- const instance = args.instance ?? report.state?.project_id;
96050
- if (!instance) {
96051
- throw new ScmDoctorFixError(
96052
- "Cannot fix: missing Lakebase project id.",
96053
- "fix-failed"
96054
- );
96055
- }
96056
- await adoptScmState({
96057
- projectDir: args.projectDir,
96058
- instance,
96059
- force: true
96060
- });
96061
- action = `adopted state with --force to re-infer tier_topology`;
96062
- break;
96063
- }
96064
- case "orphan-current-branch": {
96065
- const instance = args.instance ?? report.state?.project_id;
96066
- if (!instance) {
96067
- throw new ScmDoctorFixError(
96068
- "Cannot fix: missing Lakebase project id.",
96069
- "fix-failed"
96070
- );
96071
- }
96072
- const headBranch = await getCurrentBranch({ cwd: args.projectDir });
96073
- if (!headBranch) {
96074
- throw new ScmDoctorFixError(
96075
- "Cannot fix: detached HEAD or no current branch.",
96076
- "fix-failed"
96077
- );
96078
- }
96079
- await recoverOrphans({
96080
- projectDir: args.projectDir,
96081
- instance,
96082
- claim: true,
96083
- onlyBranch: headBranch
96084
- });
96085
- action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
96086
- break;
96087
- }
96088
- case "multiple-migration-heads": {
96089
- const r2 = await collapseMigrationHeads({ projectDir: args.projectDir });
96090
- if (r2.status !== "ok" || !r2.mergeRevision) {
96091
- throw new ScmDoctorFixError(
96092
- `Expected to create a merge revision but got status="${r2.status}".`,
96093
- "fix-failed"
96094
- );
96095
- }
96096
- action = `collapsed ${r2.headsBefore.length} heads into merge revision ${r2.mergeRevision} (commit it)`;
96097
- break;
96098
- }
96099
- case "db-ahead-of-code": {
96100
- const r2 = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
96101
- action = `abandoned the feature (deleted the polluted paired branch${r2.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r2.state.state}'; re-claim to re-fork a clean branch from the tier`;
96102
- break;
96103
- }
96104
- }
96105
- } catch (err) {
96106
- if (err instanceof ScmDoctorFixError) throw err;
96107
- throw new ScmDoctorFixError(
96108
- `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
96109
- "fix-failed"
96110
- );
96111
- }
96112
- const postReport = await runDoctor({
96113
- projectDir: args.projectDir,
96114
- instance: args.instance
96115
- });
96116
- return { findingId: args.findingId, action, postReport };
96117
- }
96118
- function shellEscape3(s2) {
96119
- return `'${s2.replace(/'/g, "'\\''")}'`;
96120
- }
96121
-
96122
- // scripts/lakebase/doctor.ts
96123
- init_cjs_shims();
96124
- var fs32 = __toESM(require("fs"), 1);
96125
- var path30 = __toESM(require("path"), 1);
96126
- var cp6 = __toESM(require("child_process"), 1);
96127
-
96128
- // scripts/lakebase/workflow-drift.ts
96129
- init_cjs_shims();
96130
- var fs31 = __toESM(require("fs"), 1);
96131
- var path29 = __toESM(require("path"), 1);
96132
- function findKitTemplatesDir(start) {
96133
- let dir = start;
96134
- for (let i2 = 0; i2 < 6; i2++) {
96135
- const candidate = path29.join(
96136
- dir,
96137
- "templates",
96138
- "project",
96139
- "common",
96140
- ".github",
96141
- "workflows"
96142
- );
96143
- if (fs31.existsSync(candidate)) return candidate;
96144
- const parent = path29.dirname(dir);
96145
- if (parent === dir) break;
96146
- dir = parent;
96147
- }
96148
- throw new Error(
96149
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
96150
- );
96151
- }
96152
- function unifiedDiff(name, projectContent, templateContent) {
96153
- if (projectContent === templateContent) return "";
96154
- const a = projectContent.split("\n");
96155
- const b = templateContent.split("\n");
96156
- const out = [`--- project/${name}`, `+++ template/${name}`];
96157
- const max = Math.max(a.length, b.length);
96158
- for (let i2 = 0; i2 < max; i2++) {
96159
- const av = a[i2];
96160
- const bv = b[i2];
96161
- if (av === bv) continue;
96162
- if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
96163
- if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
96164
- }
96165
- return out.join("\n");
96166
- }
96167
- function detectWorkflowDrift(args) {
96168
- const projectWorkflowsDir = path29.join(
96169
- args.projectDir,
96170
- ".github",
96171
- "workflows"
96172
- );
96173
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96174
- const kitWorkflowsDir = args.kitDir ? path29.join(
96175
- args.kitDir,
96176
- "templates",
96177
- "project",
96178
- "common",
96179
- ".github",
96180
- "workflows"
96181
- ) : findKitTemplatesDir(here);
96182
- const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96183
- const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96184
- const version = readKitVersion(kitWorkflowsDir);
96185
- const seen = /* @__PURE__ */ new Set();
96186
- const files = [];
96187
- for (const name of templateFiles) {
96188
- seen.add(name);
96189
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96190
- const templatePath = path29.join(kitWorkflowsDir, name);
96191
- if (!fs31.existsSync(projectPath2)) {
96192
- files.push({ name, status: "missing" });
96193
- continue;
96194
- }
96195
- const projectContent = fs31.readFileSync(projectPath2, "utf8");
96196
- const templateContent = applyPlaceholders(fs31.readFileSync(templatePath, "utf8"), version);
96197
- if (projectContent === templateContent) {
96198
- files.push({ name, status: "unchanged" });
96199
- } else {
96200
- files.push({
96201
- name,
96202
- status: "drifted",
96203
- diff: unifiedDiff(name, projectContent, templateContent)
96204
- });
96205
- }
96206
- }
96207
- for (const name of projectFiles) {
96208
- if (seen.has(name)) continue;
96209
- files.push({ name, status: "extra" });
96210
- }
96211
- const order = {
96212
- drifted: 0,
96213
- missing: 1,
96214
- extra: 2,
96215
- unchanged: 3
96216
- };
96217
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
96218
- const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
96219
- return {
96220
- overall: hasDrift ? "drift" : "ok",
96221
- files
96222
- };
96223
- }
96224
- function readKitVersion(kitWorkflowsDir) {
96225
- let dir = kitWorkflowsDir;
96226
- for (let i2 = 0; i2 < 5; i2++) {
96227
- dir = path29.dirname(dir);
96228
- }
96229
- try {
96230
- const raw = fs31.readFileSync(path29.join(dir, "package.json"), "utf-8");
96231
- const pkg = JSON.parse(raw);
96232
- return typeof pkg.version === "string" ? pkg.version : "unknown";
96233
- } catch {
96234
- return "unknown";
96235
- }
96236
- }
96237
- function applyPlaceholders(content, version) {
96238
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
96239
- }
96240
- function applyCommandPlaceholders(content, version) {
96241
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
96242
- }
96243
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
96244
- function findKitCommandsDir(start) {
96245
- let dir = start;
96246
- for (let i2 = 0; i2 < 6; i2++) {
96247
- const candidate = path29.join(
96248
- dir,
96249
- "templates",
96250
- "project",
96251
- "common",
96252
- ".claude",
96253
- "commands"
96254
- );
96255
- if (fs31.existsSync(candidate)) return candidate;
96256
- const parent = path29.dirname(dir);
96257
- if (parent === dir) break;
96258
- dir = parent;
96259
- }
96260
- throw new Error(
96261
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
96262
- );
96263
- }
96264
- function parsePinnedVersion(content) {
96265
- const m2 = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
96266
- return m2 ? m2[1] : void 0;
96267
- }
96268
- function detectCommandDrift(args) {
96269
- const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
96270
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96271
- const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
96272
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
96273
- const templateFiles = fs31.existsSync(kitCommandsDir) ? fs31.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
96274
- const projectFiles = fs31.existsSync(projectCommandsDir) ? fs31.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
96275
- const seen = /* @__PURE__ */ new Set();
96276
- const files = [];
96277
- for (const name of templateFiles) {
96278
- seen.add(name);
96279
- const projectPath2 = path29.join(projectCommandsDir, name);
96280
- const templatePath = path29.join(kitCommandsDir, name);
96281
- const templateRaw = fs31.readFileSync(templatePath, "utf8");
96282
- if (!fs31.existsSync(projectPath2)) {
96283
- files.push({ name, status: "missing", kit_version: kitVersion });
96284
- continue;
96285
- }
96286
- const projectContent = fs31.readFileSync(projectPath2, "utf8");
96287
- const pinned = parsePinnedVersion(projectContent);
96288
- const versionForCompare = pinned ?? kitVersion;
96289
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
96290
- if (projectContent === templateContent) {
96291
- files.push({
96292
- name,
96293
- status: "unchanged",
96294
- pinned_version: pinned,
96295
- kit_version: kitVersion
96296
- });
96297
- } else {
96298
- files.push({
96299
- name,
96300
- status: "drifted",
96301
- pinned_version: pinned,
96302
- kit_version: kitVersion,
96303
- diff: unifiedDiff(name, projectContent, templateContent)
96304
- });
96305
- }
96306
- }
96307
- for (const name of projectFiles) {
96308
- if (seen.has(name)) continue;
96309
- files.push({ name, status: "extra", kit_version: kitVersion });
96310
- }
96311
- const order = {
96312
- drifted: 0,
96313
- missing: 1,
96314
- extra: 2,
96315
- unchanged: 3
96316
- };
96317
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
96318
- const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
96319
- return { overall: hasDrift ? "drift" : "ok", files };
96320
- }
96321
- function readKitVersionFromCommandsDir(kitCommandsDir) {
96322
- let dir = kitCommandsDir;
96323
- for (let i2 = 0; i2 < 5; i2++) {
96324
- dir = path29.dirname(dir);
96325
- }
96326
- try {
96327
- const raw = fs31.readFileSync(path29.join(dir, "package.json"), "utf-8");
96328
- const pkg = JSON.parse(raw);
96329
- return typeof pkg.version === "string" ? pkg.version : "unknown";
96330
- } catch {
96331
- return "unknown";
96332
- }
96356
+ findings
96357
+ });
96333
96358
  }
96334
- function detectScaffoldedDrift(args) {
96335
- const workflows = detectWorkflowDrift(args);
96336
- const commands = detectCommandDrift(args);
96337
- return {
96338
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
96339
- workflows,
96340
- commands
96341
- };
96359
+ function finalize(report) {
96360
+ let worst = "ok";
96361
+ for (const f3 of report.findings) {
96362
+ worst = worstOf(worst, f3.severity);
96363
+ }
96364
+ return { ...report, worstSeverity: worst };
96342
96365
  }
96343
- function updateWorkflows(args) {
96344
- const projectWorkflowsDir = path29.join(
96345
- args.projectDir,
96346
- ".github",
96347
- "workflows"
96348
- );
96349
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96350
- const kitWorkflowsDir = args.kitDir ? path29.join(
96351
- args.kitDir,
96352
- "templates",
96353
- "project",
96354
- "common",
96355
- ".github",
96356
- "workflows"
96357
- ) : findKitTemplatesDir(here);
96358
- const substitute = args.substitute !== false;
96359
- const dryRun = args.dryRun === true;
96360
- const pruneExtras = args.pruneExtras === true;
96361
- const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96362
- const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96363
- if (!dryRun && templateFiles.length > 0 && !fs31.existsSync(projectWorkflowsDir)) {
96364
- fs31.mkdirSync(projectWorkflowsDir, { recursive: true });
96366
+ var ScmDoctorFixError = class extends Error {
96367
+ constructor(message, code) {
96368
+ super(message);
96369
+ this.code = code;
96370
+ this.name = "ScmDoctorFixError";
96365
96371
  }
96366
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
96367
- const seen = /* @__PURE__ */ new Set();
96368
- const files = [];
96369
- for (const name of templateFiles) {
96370
- seen.add(name);
96371
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96372
- const templatePath = path29.join(kitWorkflowsDir, name);
96373
- const templateRaw = fs31.readFileSync(templatePath, "utf-8");
96374
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
96375
- const existed = fs31.existsSync(projectPath2);
96376
- const current = existed ? fs31.readFileSync(projectPath2, "utf-8") : "";
96377
- let outcome;
96378
- if (!existed) {
96379
- outcome = "added";
96380
- } else if (current === desired) {
96381
- outcome = "unchanged";
96382
- } else {
96383
- outcome = "updated";
96384
- }
96385
- if (!dryRun && outcome !== "unchanged") {
96386
- fs31.writeFileSync(projectPath2, desired);
96387
- }
96388
- files.push({ name, outcome });
96372
+ code;
96373
+ };
96374
+ var FIXABLE_FINDING_IDS = [
96375
+ "env-branch-drift",
96376
+ "head-branch-drift",
96377
+ "tier-topology-mismatch",
96378
+ "orphan-current-branch",
96379
+ "multiple-migration-heads",
96380
+ "db-ahead-of-code"
96381
+ ];
96382
+ async function fixFinding(args) {
96383
+ if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
96384
+ throw new ScmDoctorFixError(
96385
+ `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
96386
+ "unsupported-finding"
96387
+ );
96389
96388
  }
96390
- if (pruneExtras) {
96391
- for (const name of projectFiles) {
96392
- if (seen.has(name)) continue;
96393
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96394
- if (!dryRun) {
96395
- fs31.unlinkSync(projectPath2);
96389
+ const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
96390
+ const present = report.findings.find((f3) => f3.id === args.findingId);
96391
+ if (!present) {
96392
+ throw new ScmDoctorFixError(
96393
+ `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
96394
+ "finding-not-present"
96395
+ );
96396
+ }
96397
+ let action = "";
96398
+ try {
96399
+ switch (args.findingId) {
96400
+ case "env-branch-drift": {
96401
+ const branch = report.state?.branch;
96402
+ if (!branch) {
96403
+ throw new ScmDoctorFixError(
96404
+ "Cannot fix: workflow state has no branch field.",
96405
+ "fix-failed"
96406
+ );
96407
+ }
96408
+ const sanitized = sanitizeBranchName(branch);
96409
+ const envFile = path29.join(args.projectDir, ".env");
96410
+ updateEnvConnection({
96411
+ envPath: envFile,
96412
+ projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
96413
+ branchId: sanitized,
96414
+ username: readEnvVar(envFile, "DB_USERNAME") ?? "",
96415
+ endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
96416
+ });
96417
+ action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
96418
+ break;
96419
+ }
96420
+ case "head-branch-drift": {
96421
+ const branch = report.state?.branch;
96422
+ if (!branch) {
96423
+ throw new ScmDoctorFixError(
96424
+ "Cannot fix: workflow state has no branch field.",
96425
+ "fix-failed"
96426
+ );
96427
+ }
96428
+ await exec2(`git checkout ${shellEscape3(branch)}`, {
96429
+ cwd: args.projectDir,
96430
+ timeout: 15e3
96431
+ });
96432
+ action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
96433
+ break;
96434
+ }
96435
+ case "tier-topology-mismatch": {
96436
+ const instance = args.instance ?? report.state?.project_id;
96437
+ if (!instance) {
96438
+ throw new ScmDoctorFixError(
96439
+ "Cannot fix: missing Lakebase project id.",
96440
+ "fix-failed"
96441
+ );
96442
+ }
96443
+ await adoptScmState({
96444
+ projectDir: args.projectDir,
96445
+ instance,
96446
+ force: true
96447
+ });
96448
+ action = `adopted state with --force to re-infer tier_topology`;
96449
+ break;
96450
+ }
96451
+ case "orphan-current-branch": {
96452
+ const instance = args.instance ?? report.state?.project_id;
96453
+ if (!instance) {
96454
+ throw new ScmDoctorFixError(
96455
+ "Cannot fix: missing Lakebase project id.",
96456
+ "fix-failed"
96457
+ );
96458
+ }
96459
+ const headBranch = await getCurrentBranch({ cwd: args.projectDir });
96460
+ if (!headBranch) {
96461
+ throw new ScmDoctorFixError(
96462
+ "Cannot fix: detached HEAD or no current branch.",
96463
+ "fix-failed"
96464
+ );
96465
+ }
96466
+ await recoverOrphans({
96467
+ projectDir: args.projectDir,
96468
+ instance,
96469
+ claim: true,
96470
+ onlyBranch: headBranch
96471
+ });
96472
+ action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
96473
+ break;
96474
+ }
96475
+ case "multiple-migration-heads": {
96476
+ const r2 = await collapseMigrationHeads({ projectDir: args.projectDir });
96477
+ if (r2.status !== "ok" || !r2.mergeRevision) {
96478
+ throw new ScmDoctorFixError(
96479
+ `Expected to create a merge revision but got status="${r2.status}".`,
96480
+ "fix-failed"
96481
+ );
96482
+ }
96483
+ action = `collapsed ${r2.headsBefore.length} heads into merge revision ${r2.mergeRevision} (commit it)`;
96484
+ break;
96485
+ }
96486
+ case "db-ahead-of-code": {
96487
+ const r2 = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
96488
+ action = `abandoned the feature (deleted the polluted paired branch${r2.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r2.state.state}'; re-claim to re-fork a clean branch from the tier`;
96489
+ break;
96396
96490
  }
96397
- files.push({ name, outcome: "removed" });
96398
96491
  }
96492
+ } catch (err) {
96493
+ if (err instanceof ScmDoctorFixError) throw err;
96494
+ throw new ScmDoctorFixError(
96495
+ `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
96496
+ "fix-failed"
96497
+ );
96399
96498
  }
96400
- const order = {
96401
- added: 0,
96402
- updated: 1,
96403
- removed: 2,
96404
- unchanged: 3
96405
- };
96406
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
96407
- const changed = files.some((f3) => f3.outcome !== "unchanged");
96408
- return { files, changed };
96499
+ const postReport = await runDoctor({
96500
+ projectDir: args.projectDir,
96501
+ instance: args.instance
96502
+ });
96503
+ return { findingId: args.findingId, action, postReport };
96504
+ }
96505
+ function shellEscape3(s2) {
96506
+ return `'${s2.replace(/'/g, "'\\''")}'`;
96409
96507
  }
96410
96508
 
96411
96509
  // scripts/lakebase/doctor.ts
96510
+ init_cjs_shims();
96511
+ var fs32 = __toESM(require("fs"), 1);
96512
+ var path30 = __toESM(require("path"), 1);
96513
+ var cp6 = __toESM(require("child_process"), 1);
96412
96514
  function readEnvFile(projectDir) {
96413
96515
  const envPath = path30.join(projectDir, ".env");
96414
96516
  if (!fs32.existsSync(envPath)) return {};
@@ -96841,7 +96943,7 @@ function checkWorkflowDrift(projectDir) {
96841
96943
  status: "warn",
96842
96944
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
96843
96945
  detail: { files: report.files.map((f3) => ({ name: f3.name, status: f3.status })) },
96844
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
96946
+ 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."
96845
96947
  };
96846
96948
  } catch (err) {
96847
96949
  return {
@@ -97317,63 +97419,6 @@ async function discardAllChanges(args) {
97317
97419
  await exec2("git clean -fd", { cwd: args.cwd });
97318
97420
  }
97319
97421
 
97320
- // scripts/git/sync.ts
97321
- init_cjs_shims();
97322
- async function currentBranchName2(cwd) {
97323
- try {
97324
- return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
97325
- } catch {
97326
- return "";
97327
- }
97328
- }
97329
- async function push(args) {
97330
- await exec2("git push", { cwd: args.cwd });
97331
- }
97332
- async function pull(args) {
97333
- await exec2("git pull", { cwd: args.cwd });
97334
- }
97335
- async function publishBranch(args) {
97336
- const remote = args.remote ?? "origin";
97337
- const branch = await currentBranchName2(args.cwd);
97338
- if (!branch) throw new Error("No current branch");
97339
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
97340
- cwd: args.cwd
97341
- });
97342
- }
97343
- async function pushCurrentBranchForPr(args) {
97344
- const remote = args.remote ?? "origin";
97345
- const branch = await currentBranchName2(args.cwd);
97346
- if (!branch) throw new Error("No current branch");
97347
- const upstreamSet = await hasUpstream({ cwd: args.cwd });
97348
- if (!upstreamSet) {
97349
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
97350
- cwd: args.cwd
97351
- });
97352
- } else {
97353
- await exec2("git push", { cwd: args.cwd });
97354
- }
97355
- }
97356
- async function fetch3(args) {
97357
- const parts = ["git fetch"];
97358
- if (args.prune) parts.push("--prune");
97359
- if (args.all) parts.push("--all");
97360
- await exec2(parts.join(" "), { cwd: args.cwd });
97361
- }
97362
- async function pullFrom(args) {
97363
- await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
97364
- cwd: args.cwd
97365
- });
97366
- }
97367
- async function pushTo(args) {
97368
- await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
97369
- cwd: args.cwd
97370
- });
97371
- }
97372
- async function sync(args) {
97373
- await exec2("git pull", { cwd: args.cwd });
97374
- await exec2("git push", { cwd: args.cwd });
97375
- }
97376
-
97377
97422
  // scripts/git/branch-tag.ts
97378
97423
  init_cjs_shims();
97379
97424
  var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["production", "main", "master"]);
@@ -97779,6 +97824,7 @@ function withProxyEnv(base = {}) {
97779
97824
  createWorktree,
97780
97825
  cutBackup,
97781
97826
  databricksAuthPrereqMessage,
97827
+ defaultRefreshPromoteWorkflows,
97782
97828
  delay,
97783
97829
  deleteAppEndpoint,
97784
97830
  deleteBranch,