@biffo/cli 0.73.2 → 0.74.1

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 (2) hide show
  1. package/dist/index.js +467 -434
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -432,8 +432,8 @@ async function runCoreStatus(options) {
432
432
 
433
433
  // src/commands/core-upgrade.ts
434
434
  import { execSync as execSync2 } from "child_process";
435
- import { existsSync as existsSync10, rmSync as rmSync5 } from "fs";
436
- import { join as join11, resolve as resolve3 } from "path";
435
+ import { existsSync as existsSync11, rmSync as rmSync5 } from "fs";
436
+ import { join as join12, resolve as resolve3 } from "path";
437
437
  import chalk4 from "chalk";
438
438
  import { execa as execa3 } from "execa";
439
439
  import { Command as Command3 } from "commander";
@@ -1585,23 +1585,107 @@ function applyMigrationCarry(instanceDir, plan) {
1585
1585
  // src/lib/core-upgrade.ts
1586
1586
  import {
1587
1587
  chmodSync,
1588
- existsSync as existsSync6,
1588
+ existsSync as existsSync7,
1589
1589
  mkdirSync as mkdirSync2,
1590
1590
  mkdtempSync as mkdtempSync2,
1591
- readFileSync as readFileSync4,
1591
+ readFileSync as readFileSync5,
1592
1592
  rmSync as rmSync2,
1593
1593
  statSync,
1594
1594
  writeFileSync as writeFileSync3
1595
1595
  } from "fs";
1596
1596
  import { tmpdir as tmpdir2 } from "os";
1597
- import { dirname as dirname4, join as join6 } from "path";
1597
+ import { dirname as dirname4, join as join7 } from "path";
1598
1598
  import { execa as execa2 } from "execa";
1599
+
1600
+ // src/lib/core-ownership-guard.ts
1601
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1602
+ import { join as join6 } from "path";
1603
+ import { z as z3 } from "zod";
1604
+ var DIVERGENCE_FILE = "biffo.divergence.json";
1605
+ var DivergenceEntrySchema = z3.object({
1606
+ prefix: z3.string().min(1),
1607
+ reason: z3.string().min(1),
1608
+ upstream: z3.string().min(1)
1609
+ });
1610
+ var DivergenceConfigSchema = z3.object({
1611
+ note: z3.string().optional(),
1612
+ warnOnly: z3.array(DivergenceEntrySchema).default([])
1613
+ });
1614
+ function readDivergenceConfig(repoRoot) {
1615
+ const path = join6(repoRoot, DIVERGENCE_FILE);
1616
+ if (!existsSync6(path)) return { warnOnly: [] };
1617
+ let raw;
1618
+ try {
1619
+ raw = JSON.parse(readFileSync4(path, "utf8"));
1620
+ } catch (err) {
1621
+ throw new Error(`${DIVERGENCE_FILE} is not valid JSON: ${err.message}`);
1622
+ }
1623
+ const parsed = DivergenceConfigSchema.safeParse(raw);
1624
+ if (!parsed.success) {
1625
+ const issues = parsed.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1626
+ throw new Error(`${DIVERGENCE_FILE} is invalid:
1627
+ ${issues}`);
1628
+ }
1629
+ return parsed.data;
1630
+ }
1631
+ function parseDivergenceTrailer(commitMessage) {
1632
+ const body = commitMessage.split("\n").filter((line) => !line.startsWith("#")).join("\n");
1633
+ const match = /^Core-Divergence:[ \t]*(\S.*?)[ \t]*$/m.exec(body);
1634
+ return match?.[1] ?? null;
1635
+ }
1636
+ function resolveBranch(env, gitBranch) {
1637
+ return (env["GITHUB_HEAD_REF"] || env["GITHUB_REF_NAME"] || gitBranch).trim();
1638
+ }
1639
+ function parseNameStatus(stdout) {
1640
+ const changed = [];
1641
+ const deleted = [];
1642
+ for (const line of stdout.split("\n")) {
1643
+ const parts = line.split(" ").filter(Boolean);
1644
+ const status = parts[0];
1645
+ const path = parts[parts.length - 1];
1646
+ if (!status || !path || parts.length < 2) continue;
1647
+ changed.push(path);
1648
+ if (status.startsWith("D")) deleted.push(path);
1649
+ }
1650
+ return { changed, deleted };
1651
+ }
1652
+ function checkCoreOwnership({
1653
+ changedFiles,
1654
+ manifest,
1655
+ isInstance,
1656
+ branch = "",
1657
+ commitMessage = "",
1658
+ warnOnly = []
1659
+ }) {
1660
+ const empty = { blocked: [], warned: [], divergenceReason: null };
1661
+ if (!isInstance) return { skipped: "template", ...empty };
1662
+ if (branch.startsWith(UPGRADE_BRANCH_PREFIX)) return { skipped: "upgrade-branch", ...empty };
1663
+ const templateOwned = changedFiles.filter((f) => isTemplateOwned(f, manifest));
1664
+ const acknowledged = (path) => warnOnly.filter((entry) => path.startsWith(entry.prefix)).reduce(
1665
+ (best, entry) => !best || entry.prefix.length > best.prefix.length ? entry : best,
1666
+ void 0
1667
+ );
1668
+ const warned = [];
1669
+ const offending = [];
1670
+ for (const path of templateOwned) {
1671
+ const entry = acknowledged(path);
1672
+ if (entry) warned.push({ path, entry });
1673
+ else offending.push(path);
1674
+ }
1675
+ const divergenceReason = parseDivergenceTrailer(commitMessage);
1676
+ if (offending.length > 0 && divergenceReason !== null) {
1677
+ return { skipped: "divergence-trailer", blocked: [], warned, divergenceReason };
1678
+ }
1679
+ return { skipped: null, blocked: offending, warned, divergenceReason: null };
1680
+ }
1681
+
1682
+ // src/lib/core-upgrade.ts
1599
1683
  var gitMergeFile = async (base, ours, theirs) => {
1600
- const dir = mkdtempSync2(join6(tmpdir2(), "biffo-merge-"));
1684
+ const dir = mkdtempSync2(join7(tmpdir2(), "biffo-merge-"));
1601
1685
  try {
1602
- const b = join6(dir, "base");
1603
- const o = join6(dir, "ours");
1604
- const t = join6(dir, "theirs");
1686
+ const b = join7(dir, "base");
1687
+ const o = join7(dir, "ours");
1688
+ const t = join7(dir, "theirs");
1605
1689
  writeFileSync3(b, base);
1606
1690
  writeFileSync3(o, ours);
1607
1691
  writeFileSync3(t, theirs);
@@ -1618,7 +1702,7 @@ var gitMergeFile = async (base, ours, theirs) => {
1618
1702
  }
1619
1703
  };
1620
1704
  function read(root, rel) {
1621
- return readFileSync4(join6(root, rel), "utf8");
1705
+ return readFileSync5(join7(root, rel), "utf8");
1622
1706
  }
1623
1707
  var EMPTY_SUMMARY = () => ({
1624
1708
  unchanged: 0,
@@ -1628,6 +1712,7 @@ var EMPTY_SUMMARY = () => ({
1628
1712
  conflict: 0,
1629
1713
  added: 0,
1630
1714
  "add-conflict": 0,
1715
+ restored: 0,
1631
1716
  removed: 0,
1632
1717
  "remove-conflict": 0
1633
1718
  });
@@ -1636,18 +1721,32 @@ async function planCoreUpgrade(options) {
1636
1721
  const base = new Set(listTemplateOwnedFiles(options.baseDir, options.manifest));
1637
1722
  const ours = new Set(listTemplateOwnedFiles(options.oursDir, options.manifest));
1638
1723
  const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest));
1724
+ const divergentPrefixes = readDivergenceConfig(options.oursDir).warnOnly.map((e) => e.prefix);
1725
+ const isDeclaredDivergent = (path) => divergentPrefixes.some((prefix) => path.startsWith(prefix));
1639
1726
  const paths = [.../* @__PURE__ */ new Set([...base, ...ours, ...theirs])].sort();
1640
1727
  const entries = [];
1728
+ const divergenceSkips = [];
1641
1729
  for (const path of paths) {
1642
- entries.push(await classify(path, base, ours, theirs, options, mergeFile));
1730
+ entries.push(
1731
+ await classify(
1732
+ path,
1733
+ base,
1734
+ ours,
1735
+ theirs,
1736
+ options,
1737
+ mergeFile,
1738
+ isDeclaredDivergent,
1739
+ (p) => divergenceSkips.push(p)
1740
+ )
1741
+ );
1643
1742
  }
1644
1743
  const summary = EMPTY_SUMMARY();
1645
1744
  for (const e of entries) summary[e.status]++;
1646
1745
  const changes = entries.filter((e) => e.status !== "unchanged" && e.status !== "keep-ours");
1647
1746
  const conflicts = entries.filter((e) => e.conflicted);
1648
- return { entries, changes, conflicts, summary };
1747
+ return { entries, changes, conflicts, summary, divergenceSkips };
1649
1748
  }
1650
- async function classify(path, base, ours, theirs, opts, mergeFile) {
1749
+ async function classify(path, base, ours, theirs, opts, mergeFile, isDeclaredDivergent, noteDivergenceSkip) {
1651
1750
  const inBase = base.has(path);
1652
1751
  const inOurs = ours.has(path);
1653
1752
  const inTheirs = theirs.has(path);
@@ -1671,8 +1770,11 @@ async function classify(path, base, ours, theirs, opts, mergeFile) {
1671
1770
  const baseContent = read(opts.baseDir, path);
1672
1771
  const theirsContent = read(opts.theirsDir, path);
1673
1772
  if (!inOurs) {
1674
- if (baseContent === theirsContent) return { path, status: "removed", conflicted: false };
1675
- return { path, status: "added", conflicted: false, content: theirsContent };
1773
+ if (isDeclaredDivergent(path)) {
1774
+ noteDivergenceSkip(path);
1775
+ return { path, status: "removed", conflicted: false };
1776
+ }
1777
+ return { path, status: "restored", conflicted: false, content: theirsContent };
1676
1778
  }
1677
1779
  const oursContent = read(opts.oursDir, path);
1678
1780
  const oursChanged = oursContent !== baseContent;
@@ -1693,9 +1795,9 @@ function applyUpgradePlan(instanceDir, plan, theirsDir) {
1693
1795
  const written = [];
1694
1796
  const deleted = [];
1695
1797
  for (const e of plan.entries) {
1696
- const abs = join6(instanceDir, e.path);
1798
+ const abs = join7(instanceDir, e.path);
1697
1799
  if (e.status === "removed") {
1698
- if (existsSync6(abs)) {
1800
+ if (existsSync7(abs)) {
1699
1801
  rmSync2(abs);
1700
1802
  deleted.push(e.path);
1701
1803
  }
@@ -1705,8 +1807,8 @@ function applyUpgradePlan(instanceDir, plan, theirsDir) {
1705
1807
  mkdirSync2(dirname4(abs), { recursive: true });
1706
1808
  writeFileSync3(abs, e.content);
1707
1809
  if (theirsDir !== void 0) {
1708
- const source = join6(theirsDir, e.path);
1709
- if (existsSync6(source) && (statSync(source).mode & 73) !== 0) {
1810
+ const source = join7(theirsDir, e.path);
1811
+ if (existsSync7(source) && (statSync(source).mode & 73) !== 0) {
1710
1812
  chmodSync(abs, 493);
1711
1813
  }
1712
1814
  }
@@ -1731,7 +1833,7 @@ function upgradeBranchName(from, to) {
1731
1833
  import { execFileSync as execFileSync2 } from "child_process";
1732
1834
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "fs";
1733
1835
  import { tmpdir as tmpdir3 } from "os";
1734
- import { join as join7 } from "path";
1836
+ import { join as join8 } from "path";
1735
1837
  function coreTag(version) {
1736
1838
  parseCoreVersion(version);
1737
1839
  return `core-v${version}`;
@@ -1770,8 +1872,8 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1770
1872
  `No git tag ${tag} in ${repo}. The template tree at core version ${version} is unavailable, so it can't be used as a merge base. Ensure the tag exists (it is pushed by the Core Version Tag workflow on release), or pass an explicit --from-template / --to-template checkout.`
1771
1873
  );
1772
1874
  }
1773
- const dir = mkdtempSync3(join7(tmpdir3(), `biffo-core-${version}-`));
1774
- const tarball = join7(dir, ".tree.tar");
1875
+ const dir = mkdtempSync3(join8(tmpdir3(), `biffo-core-${version}-`));
1876
+ const tarball = join8(dir, ".tree.tar");
1775
1877
  try {
1776
1878
  git(["-C", repo, "archive", "--format=tar", "-o", tarball, tag]);
1777
1879
  execFileSync2("tar", ["-x", "-f", tarball, "-C", dir]);
@@ -1784,8 +1886,8 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1784
1886
  }
1785
1887
 
1786
1888
  // src/lib/breaking-changes.ts
1787
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1788
- import { join as join8 } from "path";
1889
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1890
+ import { join as join9 } from "path";
1789
1891
  var UPGRADE_GUIDE_PATH = "docs/guides/core-upgrade.md";
1790
1892
  var SECTION_HEADING = "## Breaking changes by version";
1791
1893
  var ENTRY_HEADING = /^###\s+(\d+\.\d+\.\d+)\s*[—-]\s*(.+?)\s*$/;
@@ -1810,9 +1912,9 @@ function parseBreakingChanges(guide) {
1810
1912
  return entries;
1811
1913
  }
1812
1914
  function readBreakingChanges(templateRoot) {
1813
- const path = join8(templateRoot, UPGRADE_GUIDE_PATH);
1814
- if (!existsSync7(path)) return [];
1815
- return parseBreakingChanges(readFileSync5(path, "utf8"));
1915
+ const path = join9(templateRoot, UPGRADE_GUIDE_PATH);
1916
+ if (!existsSync8(path)) return [];
1917
+ return parseBreakingChanges(readFileSync6(path, "utf8"));
1816
1918
  }
1817
1919
  function breakingChangesBetween(from, to, entries) {
1818
1920
  parseCoreVersion(from);
@@ -1829,8 +1931,8 @@ var GLOBAL_DISPATCH_WORKFLOW_PATHS = [
1829
1931
  ];
1830
1932
 
1831
1933
  // src/lib/plugin-terraform-wiring.ts
1832
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1833
- import { join as join9 } from "path";
1934
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1935
+ import { join as join10 } from "path";
1834
1936
  var TEMPLATE_MODULE_DIR = "_template";
1835
1937
  var DEFAULT_PLUGIN_HANDLER = "src.lambda.main.handler";
1836
1938
  var GENERATED_TF_FILE = "plugins.generated.tf";
@@ -1848,7 +1950,7 @@ function standardArguments(pluginName, handler) {
1848
1950
  ];
1849
1951
  }
1850
1952
  function listPluginModules(cwd) {
1851
- const dir = join9(cwd, "modules", "plugins");
1953
+ const dir = join10(cwd, "modules", "plugins");
1852
1954
  let entries;
1853
1955
  try {
1854
1956
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1860,7 +1962,7 @@ function listPluginModules(cwd) {
1860
1962
  var FIRST_PARTY_TERRAFORM = (name) => `../../../services/_plugins/${name}/terraform`;
1861
1963
  var THIRD_PARTY_TERRAFORM = (name) => `../../../modules/plugins/${name}`;
1862
1964
  function isFirstPartyPlugin(cwd, name) {
1863
- return existsSync8(join9(cwd, "services", "_plugins", name, "terraform", "main.tf"));
1965
+ return existsSync9(join10(cwd, "services", "_plugins", name, "terraform", "main.tf"));
1864
1966
  }
1865
1967
  function pluginModuleSource(cwd, name) {
1866
1968
  return isFirstPartyPlugin(cwd, name) ? FIRST_PARTY_TERRAFORM(name) : THIRD_PARTY_TERRAFORM(name);
@@ -1871,7 +1973,7 @@ function listWireablePlugins(cwd) {
1871
1973
  return [.../* @__PURE__ */ new Set([...copied, ...firstParty])].sort();
1872
1974
  }
1873
1975
  function firstPartyPluginNames(cwd) {
1874
- const dir = join9(cwd, "services", "_plugins");
1976
+ const dir = join10(cwd, "services", "_plugins");
1875
1977
  let entries;
1876
1978
  try {
1877
1979
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1885,7 +1987,7 @@ function staleFirstPartyCopies(cwd) {
1885
1987
  return firstPartyPluginNames(cwd).filter((name) => copied.has(name));
1886
1988
  }
1887
1989
  function listEnvironments(cwd) {
1888
- const dir = join9(cwd, "infra", "environments");
1990
+ const dir = join10(cwd, "infra", "environments");
1889
1991
  let entries;
1890
1992
  try {
1891
1993
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1893,12 +1995,12 @@ function listEnvironments(cwd) {
1893
1995
  return [];
1894
1996
  }
1895
1997
  return entries.filter((e) => {
1896
- if (!e.isDirectory() || !existsSync8(join9(dir, e.name, "main.tf"))) return false;
1897
- return declaredVariables(join9(dir, e.name)).has("enabled_plugins");
1998
+ if (!e.isDirectory() || !existsSync9(join10(dir, e.name, "main.tf"))) return false;
1999
+ return declaredVariables(join10(dir, e.name)).has("enabled_plugins");
1898
2000
  }).map((e) => e.name).sort();
1899
2001
  }
1900
2002
  function listUnwirableEnvironments(cwd) {
1901
- const dir = join9(cwd, "infra", "environments");
2003
+ const dir = join10(cwd, "infra", "environments");
1902
2004
  let entries;
1903
2005
  try {
1904
2006
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1906,7 +2008,7 @@ function listUnwirableEnvironments(cwd) {
1906
2008
  return [];
1907
2009
  }
1908
2010
  return entries.filter(
1909
- (e) => e.isDirectory() && existsSync8(join9(dir, e.name, "main.tf")) && !declaredVariables(join9(dir, e.name)).has("enabled_plugins")
2011
+ (e) => e.isDirectory() && existsSync9(join10(dir, e.name, "main.tf")) && !declaredVariables(join10(dir, e.name)).has("enabled_plugins")
1910
2012
  ).map((e) => e.name).sort();
1911
2013
  }
1912
2014
  function declaredVariables(moduleDir) {
@@ -1921,7 +2023,7 @@ function declaredVariables(moduleDir) {
1921
2023
  if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
1922
2024
  let contents;
1923
2025
  try {
1924
- contents = readFileSync6(join9(moduleDir, entry.name), "utf8");
2026
+ contents = readFileSync7(join10(moduleDir, entry.name), "utf8");
1925
2027
  } catch {
1926
2028
  continue;
1927
2029
  }
@@ -1998,7 +2100,7 @@ function syncPluginTerraform(cwd) {
1998
2100
  const changedPaths = [];
1999
2101
  const rendered = plugins.map((name) => {
2000
2102
  const firstParty = isFirstPartyPlugin(cwd, name);
2001
- const moduleDir = firstParty ? join9(cwd, "services", "_plugins", name, "terraform") : join9(cwd, "modules", "plugins", name);
2103
+ const moduleDir = firstParty ? join10(cwd, "services", "_plugins", name, "terraform") : join10(cwd, "modules", "plugins", name);
2002
2104
  return {
2003
2105
  name,
2004
2106
  declaredVariables: declaredVariables(moduleDir),
@@ -2006,16 +2108,16 @@ function syncPluginTerraform(cwd) {
2006
2108
  };
2007
2109
  });
2008
2110
  for (const env of environments) {
2009
- const envDir = join9(cwd, "infra", "environments", env);
2010
- const tfPath = join9(envDir, GENERATED_TF_FILE);
2011
- const tfvarsPath = join9(envDir, GENERATED_TFVARS_FILE);
2111
+ const envDir = join10(cwd, "infra", "environments", env);
2112
+ const tfPath = join10(envDir, GENERATED_TF_FILE);
2113
+ const tfvarsPath = join10(envDir, GENERATED_TFVARS_FILE);
2012
2114
  const relBase = `infra/environments/${env}`;
2013
2115
  if (plugins.length === 0) {
2014
2116
  for (const [abs, rel] of [
2015
2117
  [tfPath, `${relBase}/${GENERATED_TF_FILE}`],
2016
2118
  [tfvarsPath, `${relBase}/${GENERATED_TFVARS_FILE}`]
2017
2119
  ]) {
2018
- if (existsSync8(abs)) {
2120
+ if (existsSync9(abs)) {
2019
2121
  rmSync4(abs);
2020
2122
  changedPaths.push(rel);
2021
2123
  }
@@ -2031,8 +2133,8 @@ function syncPluginTerraform(cwd) {
2031
2133
  }
2032
2134
 
2033
2135
  // src/lib/lockfile-refresh.ts
2034
- import { existsSync as existsSync9 } from "fs";
2035
- import { join as join10 } from "path";
2136
+ import { existsSync as existsSync10 } from "fs";
2137
+ import { join as join11 } from "path";
2036
2138
  var LOCKFILE_TRIGGERS = [
2037
2139
  {
2038
2140
  manifest: "package.json",
@@ -2055,7 +2157,7 @@ function lockfilesNeedingRefresh(changedPaths, instanceDir, triggers = LOCKFILE_
2055
2157
  const locked = changedPaths.filter((p) => !isForeignManifest(p));
2056
2158
  return triggers.filter((t) => {
2057
2159
  const touched = locked.some((p) => p === t.manifest || p.endsWith(`/${t.manifest}`));
2058
- return touched && existsSync9(join10(instanceDir, t.lockfile));
2160
+ return touched && existsSync10(join11(instanceDir, t.lockfile));
2059
2161
  });
2060
2162
  }
2061
2163
  async function refreshLockfiles(instanceDir, triggers, run) {
@@ -2242,6 +2344,13 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
2242
2344
  `
2243
2345
  ${chalk4.bold(String(plan.changes.length))} change(s), ${chalk4.bold(String(migrations.entries.length))} new core migration(s), ${plan.conflicts.length > 0 ? chalk4.red(`${plan.conflicts.length} conflict(s)`) : chalk4.green("0 conflicts")}.`
2244
2346
  );
2347
+ if (plan.divergenceSkips && plan.divergenceSkips.length > 0) {
2348
+ console.log(
2349
+ chalk4.dim(
2350
+ ` ${plan.divergenceSkips.length} deleted template-owned file(s) left absent \u2014 declared divergent in biffo.divergence.json.`
2351
+ )
2352
+ );
2353
+ }
2245
2354
  if (!options.apply) {
2246
2355
  if (plan.conflicts.length > 0) {
2247
2356
  log.warn("Some core files changed on both sides and need manual resolution.");
@@ -2286,7 +2395,7 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
2286
2395
  const carried = applyMigrationCarry(options.cwd, migrations);
2287
2396
  writeInstanceCoreVersion(options.cwd, toVersion);
2288
2397
  const cleanedCoreVersion = coreVersionCleanup?.action === "delete";
2289
- if (cleanedCoreVersion && existsSync10(coreVersionCleanup.path)) {
2398
+ if (cleanedCoreVersion && existsSync11(coreVersionCleanup.path)) {
2290
2399
  rmSync5(coreVersionCleanup.path);
2291
2400
  log.info(
2292
2401
  `Deleted orphaned ${CORE_VERSION_FILE} (inherited copy recording ${coreVersionCleanup.found}, superseded by biffo.core.json) \u2014 nothing reads it as an authority (#434).`
@@ -2355,8 +2464,15 @@ function buildPrBody(from, to, plan, migrations, base = GLOBAL_DISPATCH_REF, loc
2355
2464
  `- merged: ${plan.summary.merged}`,
2356
2465
  `- take-theirs: ${plan.summary["take-theirs"]}`,
2357
2466
  `- added: ${plan.summary.added}`,
2467
+ `- restored: ${plan.summary.restored}`,
2358
2468
  `- removed: ${plan.summary.removed}`
2359
2469
  );
2470
+ if (plan.divergenceSkips && plan.divergenceSkips.length > 0) {
2471
+ lines.push(
2472
+ "",
2473
+ `> ${plan.divergenceSkips.length} template-owned file(s) the instance deleted were left absent because \`biffo.divergence.json\` declares the path an intentional divergence (not restored).`
2474
+ );
2475
+ }
2360
2476
  if (migrations.entries.length > 0) {
2361
2477
  lines.push(
2362
2478
  "",
@@ -2433,6 +2549,7 @@ var STATUS_COLOR = {
2433
2549
  merged: chalk4.yellow,
2434
2550
  "take-theirs": chalk4.green,
2435
2551
  added: chalk4.green,
2552
+ restored: chalk4.green,
2436
2553
  removed: chalk4.red,
2437
2554
  "keep-ours": chalk4.dim
2438
2555
  };
@@ -2525,8 +2642,8 @@ function printBreakingChanges(breaking, applying) {
2525
2642
  }
2526
2643
  function versionOfCheckout(dir, explicit) {
2527
2644
  if (explicit) return explicit;
2528
- const file = join11(dir, CORE_VERSION_FILE);
2529
- if (existsSync10(file)) return readCoreVersionFile(file);
2645
+ const file = join12(dir, CORE_VERSION_FILE);
2646
+ if (existsSync11(file)) return readCoreVersionFile(file);
2530
2647
  throw new Error(
2531
2648
  `Cannot determine the core version of ${dir}: it has no ${CORE_VERSION_FILE}, and a checkout supplied explicitly is not resolved from a tag. Pass --to to state which version this tree is.`
2532
2649
  );
@@ -2534,8 +2651,8 @@ function versionOfCheckout(dir, explicit) {
2534
2651
  function latestCoreVersion(repo) {
2535
2652
  const fromTags = latestCoreVersionFromTags(repo);
2536
2653
  if (fromTags) return fromTags;
2537
- const file = join11(repo, CORE_VERSION_FILE);
2538
- if (existsSync10(file)) return readCoreVersionFile(file);
2654
+ const file = join12(repo, CORE_VERSION_FILE);
2655
+ if (existsSync11(file)) return readCoreVersionFile(file);
2539
2656
  throw new Error(
2540
2657
  `Cannot determine the template's core version: ${repo} has no core-v* tags and no ${CORE_VERSION_FILE}. Fetch tags (\`git fetch --tags\`) or pass --to explicitly.`
2541
2658
  );
@@ -2553,7 +2670,7 @@ coreCommand.addCommand(coreUpgradeCommand);
2553
2670
  import { Command as Command8 } from "commander";
2554
2671
 
2555
2672
  // src/commands/data-apply.ts
2556
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2673
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
2557
2674
  import { resolve as resolve4 } from "path";
2558
2675
  import chalk5 from "chalk";
2559
2676
  import { Command as Command5 } from "commander";
@@ -2913,62 +3030,62 @@ var AwsAdapter = class {
2913
3030
  };
2914
3031
 
2915
3032
  // src/config/schema.ts
2916
- import { z as z3 } from "zod";
2917
- var AwsConfigSchema = z3.object({
2918
- account_id: z3.string().regex(/^\d{12}$/, "AWS account ID must be 12 digits").describe("12-digit AWS account ID"),
2919
- region: z3.string().default("us-east-1"),
2920
- profile: z3.string().optional(),
2921
- oidc_role_arn: z3.string().regex(/^arn:aws:iam::\d{12}:role\/.+/, "Must be a valid IAM role ARN").optional(),
2922
- tf_state_bucket: z3.string().optional()
3033
+ import { z as z4 } from "zod";
3034
+ var AwsConfigSchema = z4.object({
3035
+ account_id: z4.string().regex(/^\d{12}$/, "AWS account ID must be 12 digits").describe("12-digit AWS account ID"),
3036
+ region: z4.string().default("us-east-1"),
3037
+ profile: z4.string().optional(),
3038
+ oidc_role_arn: z4.string().regex(/^arn:aws:iam::\d{12}:role\/.+/, "Must be a valid IAM role ARN").optional(),
3039
+ tf_state_bucket: z4.string().optional()
2923
3040
  });
2924
- var GitHubConfigSchema = z3.object({
2925
- org: z3.string().min(1).describe("GitHub organisation or username"),
2926
- repo: z3.string().min(1).describe("Repository name (will be created)")
3041
+ var GitHubConfigSchema = z4.object({
3042
+ org: z4.string().min(1).describe("GitHub organisation or username"),
3043
+ repo: z4.string().min(1).describe("Repository name (will be created)")
2927
3044
  });
2928
- var SourceControlConfigSchema = z3.discriminatedUnion("provider", [
2929
- z3.object({ provider: z3.literal("github"), config: GitHubConfigSchema })
3045
+ var SourceControlConfigSchema = z4.discriminatedUnion("provider", [
3046
+ z4.object({ provider: z4.literal("github"), config: GitHubConfigSchema })
2930
3047
  ]);
2931
- var CloudConfigSchema = z3.discriminatedUnion("provider", [
2932
- z3.object({ provider: z3.literal("aws"), config: AwsConfigSchema })
3048
+ var CloudConfigSchema = z4.discriminatedUnion("provider", [
3049
+ z4.object({ provider: z4.literal("aws"), config: AwsConfigSchema })
2933
3050
  ]);
2934
- var ModulesSchema = z3.object({
2935
- auth: z3.enum(["cognito"]).default("cognito"),
2936
- events: z3.enum(["eventbridge"]).default("eventbridge"),
2937
- storage: z3.enum(["s3"]).default("s3"),
2938
- database: z3.enum(["postgresql"]).default("postgresql"),
2939
- compute: z3.enum(["lambda"]).default("lambda"),
2940
- cdn: z3.enum(["cloudfront"]).default("cloudfront")
3051
+ var ModulesSchema = z4.object({
3052
+ auth: z4.enum(["cognito"]).default("cognito"),
3053
+ events: z4.enum(["eventbridge"]).default("eventbridge"),
3054
+ storage: z4.enum(["s3"]).default("s3"),
3055
+ database: z4.enum(["postgresql"]).default("postgresql"),
3056
+ compute: z4.enum(["lambda"]).default("lambda"),
3057
+ cdn: z4.enum(["cloudfront"]).default("cloudfront")
2941
3058
  });
2942
- var DnsSchema = z3.object({
2943
- mode: z3.enum(["managed-route53", "external", "none"]).default("managed-route53"),
2944
- domain: z3.string().min(1).optional()
3059
+ var DnsSchema = z4.object({
3060
+ mode: z4.enum(["managed-route53", "external", "none"]).default("managed-route53"),
3061
+ domain: z4.string().min(1).optional()
2945
3062
  });
2946
- var BiffoConfigSchema = z3.object({
2947
- $schema: z3.string().optional(),
2948
- project: z3.object({
2949
- name: z3.string().min(1).regex(/^[a-z0-9-]+$/, "Must be lowercase kebab-case"),
2950
- description: z3.string().default(""),
3063
+ var BiffoConfigSchema = z4.object({
3064
+ $schema: z4.string().optional(),
3065
+ project: z4.object({
3066
+ name: z4.string().min(1).regex(/^[a-z0-9-]+$/, "Must be lowercase kebab-case"),
3067
+ description: z4.string().default(""),
2951
3068
  // Backward compatibility for existing configs. New configs should use dns.domain.
2952
- domain: z3.string().min(1).optional().describe("Primary domain, e.g. myapp.com")
3069
+ domain: z4.string().min(1).optional().describe("Primary domain, e.g. myapp.com")
2953
3070
  }),
2954
3071
  dns: DnsSchema.optional(),
2955
3072
  source_control: SourceControlConfigSchema,
2956
3073
  cloud: CloudConfigSchema,
2957
- environments: z3.array(z3.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
2958
- admin: z3.object({
2959
- email: z3.string().email(),
2960
- username: z3.string().min(1)
3074
+ environments: z4.array(z4.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
3075
+ admin: z4.object({
3076
+ email: z4.string().email(),
3077
+ username: z4.string().min(1)
2961
3078
  }),
2962
- database: z3.object({
2963
- schema_path: z3.string().nullable().default(null),
2964
- migrations_path: z3.string().default("services/api/migrations")
3079
+ database: z4.object({
3080
+ schema_path: z4.string().nullable().default(null),
3081
+ migrations_path: z4.string().default("services/api/migrations")
2965
3082
  }).default({}),
2966
3083
  modules: ModulesSchema.default({})
2967
3084
  }).superRefine((config, ctx) => {
2968
3085
  const dns = resolveDnsConfig(config);
2969
3086
  if (dns.mode !== "none" && !dns.domain) {
2970
3087
  ctx.addIssue({
2971
- code: z3.ZodIssueCode.custom,
3088
+ code: z4.ZodIssueCode.custom,
2972
3089
  path: ["dns", "domain"],
2973
3090
  message: 'DNS domain is required unless dns.mode is "none"'
2974
3091
  });
@@ -3004,16 +3121,16 @@ function isTemplatePlaceholderConfig(raw) {
3004
3121
 
3005
3122
  // src/lib/session.ts
3006
3123
  import {
3007
- existsSync as existsSync11,
3124
+ existsSync as existsSync12,
3008
3125
  mkdirSync as mkdirSync4,
3009
3126
  readdirSync as readdirSync4,
3010
- readFileSync as readFileSync7,
3127
+ readFileSync as readFileSync8,
3011
3128
  rmSync as rmSync6,
3012
3129
  statSync as statSync2,
3013
3130
  writeFileSync as writeFileSync5
3014
3131
  } from "fs";
3015
3132
  import { homedir } from "os";
3016
- import { join as join12 } from "path";
3133
+ import { join as join13 } from "path";
3017
3134
  var LEGACY_STEP_ALIASES = {
3018
3135
  github_config: ["github_branches", "github_instance_files", "github_settings"]
3019
3136
  };
@@ -3022,39 +3139,39 @@ function hasCompleted(session, step) {
3022
3139
  return session.completedSteps.some((done) => LEGACY_STEP_ALIASES[done]?.includes(step) ?? false);
3023
3140
  }
3024
3141
  function sessionsDir() {
3025
- return process.env["BIFFO_SESSIONS_DIR"] ?? join12(homedir(), ".biffo", "sessions");
3142
+ return process.env["BIFFO_SESSIONS_DIR"] ?? join13(homedir(), ".biffo", "sessions");
3026
3143
  }
3027
3144
  function sessionPath(projectName) {
3028
- return join12(sessionsDir(), `${projectName}.json`);
3145
+ return join13(sessionsDir(), `${projectName}.json`);
3029
3146
  }
3030
3147
  function loadSession(projectName) {
3031
3148
  const path = sessionPath(projectName);
3032
- if (!existsSync11(path)) return null;
3149
+ if (!existsSync12(path)) return null;
3033
3150
  try {
3034
- return JSON.parse(readFileSync7(path, "utf8"));
3151
+ return JSON.parse(readFileSync8(path, "utf8"));
3035
3152
  } catch {
3036
3153
  return null;
3037
3154
  }
3038
3155
  }
3039
3156
  function findLatestSession() {
3040
3157
  const dir = sessionsDir();
3041
- if (!existsSync11(dir)) return null;
3158
+ if (!existsSync12(dir)) return null;
3042
3159
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
3043
3160
  if (files.length === 0) return null;
3044
3161
  const sorted = files.map((f) => {
3045
- const fullPath = join12(dir, f);
3046
- const mtime = existsSync11(fullPath) ? statSync2(fullPath).mtimeMs : -1;
3162
+ const fullPath = join13(dir, f);
3163
+ const mtime = existsSync12(fullPath) ? statSync2(fullPath).mtimeMs : -1;
3047
3164
  return { f, mtime };
3048
3165
  }).sort((a, b) => b.mtime - a.mtime);
3049
3166
  try {
3050
- return JSON.parse(readFileSync7(join12(dir, sorted[0].f), "utf8"));
3167
+ return JSON.parse(readFileSync8(join13(dir, sorted[0].f), "utf8"));
3051
3168
  } catch {
3052
3169
  return null;
3053
3170
  }
3054
3171
  }
3055
3172
  function saveSession(session) {
3056
3173
  const dir = sessionsDir();
3057
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
3174
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
3058
3175
  const name = session.config.project?.name ?? "unknown";
3059
3176
  const prior = loadSession(name);
3060
3177
  if (prior) {
@@ -3076,36 +3193,36 @@ function markStepComplete(session, step) {
3076
3193
  }
3077
3194
  function deleteSession(projectName) {
3078
3195
  const path = sessionPath(projectName);
3079
- if (existsSync11(path)) rmSync6(path);
3196
+ if (existsSync12(path)) rmSync6(path);
3080
3197
  }
3081
3198
  function projectsDir() {
3082
- return process.env["BIFFO_PROJECTS_DIR"] ?? join12(homedir(), ".biffo", "projects");
3199
+ return process.env["BIFFO_PROJECTS_DIR"] ?? join13(homedir(), ".biffo", "projects");
3083
3200
  }
3084
3201
  function saveProjectConfig(config) {
3085
3202
  const dir = projectsDir();
3086
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
3087
- writeFileSync5(join12(dir, `${config.project.name}.json`), JSON.stringify(config, null, 2));
3203
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
3204
+ writeFileSync5(join13(dir, `${config.project.name}.json`), JSON.stringify(config, null, 2));
3088
3205
  }
3089
3206
  function loadProjectConfig(name) {
3090
- const path = join12(projectsDir(), `${name}.json`);
3091
- if (!existsSync11(path)) return null;
3207
+ const path = join13(projectsDir(), `${name}.json`);
3208
+ if (!existsSync12(path)) return null;
3092
3209
  try {
3093
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync7(path, "utf8")));
3210
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync8(path, "utf8")));
3094
3211
  return result.success ? result.data : null;
3095
3212
  } catch {
3096
3213
  return null;
3097
3214
  }
3098
3215
  }
3099
3216
  function deleteProjectConfig(name) {
3100
- const path = join12(projectsDir(), `${name}.json`);
3101
- if (existsSync11(path)) rmSync6(path);
3217
+ const path = join13(projectsDir(), `${name}.json`);
3218
+ if (existsSync12(path)) rmSync6(path);
3102
3219
  }
3103
3220
  function listProjectConfigs() {
3104
3221
  const dir = projectsDir();
3105
- if (!existsSync11(dir)) return [];
3222
+ if (!existsSync12(dir)) return [];
3106
3223
  return readdirSync4(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
3107
3224
  try {
3108
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync7(join12(dir, f), "utf8")));
3225
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync8(join13(dir, f), "utf8")));
3109
3226
  return result.success ? [result.data] : [];
3110
3227
  } catch {
3111
3228
  return [];
@@ -3174,7 +3291,7 @@ async function runDataApply(name, environment, config, aws) {
3174
3291
  }
3175
3292
  async function resolveConfig(options) {
3176
3293
  if (options.config) {
3177
- const raw = JSON.parse(readFileSync8(resolve4(options.config), "utf8"));
3294
+ const raw = JSON.parse(readFileSync9(resolve4(options.config), "utf8"));
3178
3295
  const result = BiffoConfigSchema.safeParse(raw);
3179
3296
  if (!result.success) {
3180
3297
  log.error(`Invalid config at ${options.config}:`);
@@ -3194,8 +3311,8 @@ async function resolveConfig(options) {
3194
3311
  return cfg;
3195
3312
  }
3196
3313
  const localConfigPath = resolve4(process.cwd(), "biffo.config.json");
3197
- if (existsSync12(localConfigPath)) {
3198
- const raw = JSON.parse(readFileSync8(localConfigPath, "utf8"));
3314
+ if (existsSync13(localConfigPath)) {
3315
+ const raw = JSON.parse(readFileSync9(localConfigPath, "utf8"));
3199
3316
  const result = BiffoConfigSchema.safeParse(raw);
3200
3317
  if (result.success) return result.data;
3201
3318
  if (isTemplatePlaceholderConfig(raw)) {
@@ -3241,8 +3358,8 @@ async function resolveConfig(options) {
3241
3358
 
3242
3359
  // src/commands/data-import.ts
3243
3360
  import { execSync as execSync3 } from "child_process";
3244
- import { cpSync, existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3245
- import { join as join13, resolve as resolve5 } from "path";
3361
+ import { cpSync, existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3362
+ import { join as join14, resolve as resolve5 } from "path";
3246
3363
  import chalk6 from "chalk";
3247
3364
  import { Command as Command6 } from "commander";
3248
3365
  import inquirer2 from "inquirer";
@@ -3282,23 +3399,23 @@ async function runDataImport(name, options, deps) {
3282
3399
  `Invalid import name '${name}'. Use lowercase letters, numbers, and hyphens, starting with a letter.`
3283
3400
  );
3284
3401
  }
3285
- const servicesDir = join13(options.cwd, "services");
3286
- if (!existsSync13(servicesDir)) {
3402
+ const servicesDir = join14(options.cwd, "services");
3403
+ if (!existsSync14(servicesDir)) {
3287
3404
  throw new Error(
3288
3405
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
3289
3406
  );
3290
3407
  }
3291
- const targetDir = join13(options.cwd, "db", "imports", name);
3292
- if (existsSync13(targetDir)) {
3408
+ const targetDir = join14(options.cwd, "db", "imports", name);
3409
+ if (existsSync14(targetDir)) {
3293
3410
  throw new Error(
3294
3411
  `DDL import '${name}' is already present at db/imports/${name}/. Remove it first to re-import.`
3295
3412
  );
3296
3413
  }
3297
- const isLocalDir = existsSync13(options.source) && statSync3(options.source).isDirectory();
3414
+ const isLocalDir = existsSync14(options.source) && statSync3(options.source).isDirectory();
3298
3415
  let sourceDir;
3299
3416
  let cleanupClone = null;
3300
3417
  if (isLocalDir) {
3301
- sourceDir = options.path ? join13(options.source, options.path) : options.source;
3418
+ sourceDir = options.path ? join14(options.source, options.path) : options.source;
3302
3419
  } else {
3303
3420
  const token = options.token ?? await resolveDdlImportToken();
3304
3421
  log.info(`Cloning ${options.source}...`);
@@ -3306,10 +3423,10 @@ async function runDataImport(name, options, deps) {
3306
3423
  cleanupClone = () => {
3307
3424
  deps.git.cleanup(tmpDir);
3308
3425
  };
3309
- sourceDir = options.path ? join13(tmpDir, options.path) : tmpDir;
3426
+ sourceDir = options.path ? join14(tmpDir, options.path) : tmpDir;
3310
3427
  }
3311
3428
  try {
3312
- if (!existsSync13(sourceDir)) {
3429
+ if (!existsSync14(sourceDir)) {
3313
3430
  throw new Error(`Source directory does not exist: ${sourceDir}`);
3314
3431
  }
3315
3432
  const sqlFiles = readdirSync5(sourceDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".sql")).map((entry) => entry.name).sort();
@@ -3334,7 +3451,7 @@ async function runDataImport(name, options, deps) {
3334
3451
  }
3335
3452
  mkdirSync5(targetDir, { recursive: true });
3336
3453
  for (const file of sqlFiles) {
3337
- cpSync(join13(sourceDir, file), join13(targetDir, file));
3454
+ cpSync(join14(sourceDir, file), join14(targetDir, file));
3338
3455
  }
3339
3456
  log.success(`Imported ${String(sqlFiles.length)} .sql file(s) to db/imports/${name}/`);
3340
3457
  const commitMessage = `feat(data): import ${name} (${String(sqlFiles.length)} SQL file(s))`;
@@ -3386,8 +3503,8 @@ function printDryRun(name, sqlFiles) {
3386
3503
  }
3387
3504
 
3388
3505
  // src/commands/data-list.ts
3389
- import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
3390
- import { join as join14, resolve as resolve6 } from "path";
3506
+ import { existsSync as existsSync15, readdirSync as readdirSync6 } from "fs";
3507
+ import { join as join15, resolve as resolve6 } from "path";
3391
3508
  import chalk7 from "chalk";
3392
3509
  import { Command as Command7 } from "commander";
3393
3510
  var dataListCommand = new Command7("list").description("List DDL imports vendored in this project checkout").option("--cwd <path>", "Project root to scan (defaults to the current directory)").action(async (options) => {
@@ -3400,15 +3517,15 @@ var dataListCommand = new Command7("list").description("List DDL imports vendore
3400
3517
  }
3401
3518
  });
3402
3519
  async function runDataList(options) {
3403
- const importsDir = join14(options.cwd, "db", "imports");
3404
- if (!existsSync14(importsDir)) {
3520
+ const importsDir = join15(options.cwd, "db", "imports");
3521
+ if (!existsSync15(importsDir)) {
3405
3522
  console.log(chalk7.dim("\n No DDL imports in this checkout.\n"));
3406
3523
  return;
3407
3524
  }
3408
3525
  const candidates = readdirSync6(importsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
3409
3526
  const imports = [];
3410
3527
  for (const name of candidates) {
3411
- const fileCount = readdirSync6(join14(importsDir, name)).filter((f) => f.endsWith(".sql")).length;
3528
+ const fileCount = readdirSync6(join15(importsDir, name)).filter((f) => f.endsWith(".sql")).length;
3412
3529
  if (fileCount > 0) imports.push({ name, fileCount });
3413
3530
  }
3414
3531
  if (imports.length === 0) {
@@ -3438,7 +3555,7 @@ dataCommand.addCommand(dataListCommand);
3438
3555
 
3439
3556
  // src/commands/deploy.ts
3440
3557
  import { execSync as execSync4 } from "child_process";
3441
- import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
3558
+ import { existsSync as existsSync16, readFileSync as readFileSync10 } from "fs";
3442
3559
  import { resolve as resolve7 } from "path";
3443
3560
  import chalk8 from "chalk";
3444
3561
  import { Command as Command9 } from "commander";
@@ -3792,7 +3909,7 @@ var deployCommand = new Command9("deploy").description("Deploy infrastructure an
3792
3909
  );
3793
3910
  async function resolveConfig2(options) {
3794
3911
  if (options.config) {
3795
- const raw = JSON.parse(readFileSync9(resolve7(options.config), "utf8"));
3912
+ const raw = JSON.parse(readFileSync10(resolve7(options.config), "utf8"));
3796
3913
  const result = BiffoConfigSchema.safeParse(raw);
3797
3914
  if (!result.success) {
3798
3915
  log.error(`Invalid config at ${options.config}:`);
@@ -3812,8 +3929,8 @@ async function resolveConfig2(options) {
3812
3929
  return cfg;
3813
3930
  }
3814
3931
  const localConfigPath = resolve7(process.cwd(), "biffo.config.json");
3815
- if (existsSync15(localConfigPath)) {
3816
- const raw = JSON.parse(readFileSync9(localConfigPath, "utf8"));
3932
+ if (existsSync16(localConfigPath)) {
3933
+ const raw = JSON.parse(readFileSync10(localConfigPath, "utf8"));
3817
3934
  const result = BiffoConfigSchema.safeParse(raw);
3818
3935
  if (result.success) return result.data;
3819
3936
  if (isTemplatePlaceholderConfig(raw)) {
@@ -4197,7 +4314,7 @@ function resolveGithubToken() {
4197
4314
 
4198
4315
  // src/commands/destroy.ts
4199
4316
  import { execSync as execSync5 } from "child_process";
4200
- import { readFileSync as readFileSync10 } from "fs";
4317
+ import { readFileSync as readFileSync11 } from "fs";
4201
4318
  import { resolve as resolve8 } from "path";
4202
4319
  import chalk9 from "chalk";
4203
4320
  import { Command as Command10 } from "commander";
@@ -4287,7 +4404,7 @@ var destroyCommand = new Command10("destroy").description("Destroy infrastructur
4287
4404
  });
4288
4405
  async function resolveConfig3(options) {
4289
4406
  if (options.config) {
4290
- const raw = JSON.parse(readFileSync10(resolve8(options.config), "utf8"));
4407
+ const raw = JSON.parse(readFileSync11(resolve8(options.config), "utf8"));
4291
4408
  const result = BiffoConfigSchema.safeParse(raw);
4292
4409
  if (!result.success) {
4293
4410
  log.error(`Invalid config at ${options.config}:`);
@@ -4307,7 +4424,7 @@ async function resolveConfig3(options) {
4307
4424
  return cfg;
4308
4425
  }
4309
4426
  try {
4310
- const raw = JSON.parse(readFileSync10(resolve8(process.cwd(), "biffo.config.json"), "utf8"));
4427
+ const raw = JSON.parse(readFileSync11(resolve8(process.cwd(), "biffo.config.json"), "utf8"));
4311
4428
  const result = BiffoConfigSchema.safeParse(raw);
4312
4429
  if (result.success) return result.data;
4313
4430
  } catch {
@@ -4357,15 +4474,15 @@ function resolveGithubToken2() {
4357
4474
  }
4358
4475
 
4359
4476
  // src/commands/init.ts
4360
- import { readFileSync as readFileSync14 } from "fs";
4477
+ import { readFileSync as readFileSync15 } from "fs";
4361
4478
  import { resolve as resolve10 } from "path";
4362
4479
  import chalk12 from "chalk";
4363
4480
  import { Command as Command12 } from "commander";
4364
4481
  import inquirer5 from "inquirer";
4365
4482
 
4366
4483
  // src/lib/build-freshness.ts
4367
- import { existsSync as existsSync16, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
4368
- import { dirname as dirname5, join as join15, relative as relative2, sep as sep2 } from "path";
4484
+ import { existsSync as existsSync17, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
4485
+ import { dirname as dirname5, join as join16, relative as relative2, sep as sep2 } from "path";
4369
4486
  import { fileURLToPath as fileURLToPath3 } from "url";
4370
4487
  var SKIP_ENV_VAR = "BIFFO_SKIP_BUILD_FRESHNESS_CHECK";
4371
4488
  function checkBuildFreshness(options = {}) {
@@ -4379,7 +4496,7 @@ function checkBuildFreshness(options = {}) {
4379
4496
  if (!packageRoot) {
4380
4497
  return { status: "skipped", reason: `no package.json above ${moduleDir}`, newerSources: [] };
4381
4498
  }
4382
- const distDir = join15(packageRoot, "dist");
4499
+ const distDir = join16(packageRoot, "dist");
4383
4500
  if (!isInside(distDir, moduleDir)) {
4384
4501
  return {
4385
4502
  status: "skipped",
@@ -4387,16 +4504,16 @@ function checkBuildFreshness(options = {}) {
4387
4504
  newerSources: []
4388
4505
  };
4389
4506
  }
4390
- const srcDir = join15(packageRoot, "src");
4391
- if (!existsSync16(srcDir)) {
4507
+ const srcDir = join16(packageRoot, "src");
4508
+ if (!existsSync17(srcDir)) {
4392
4509
  return {
4393
4510
  status: "skipped",
4394
4511
  reason: "no src/ alongside dist/ \u2014 this is a shipped package",
4395
4512
  newerSources: []
4396
4513
  };
4397
4514
  }
4398
- const entry = join15(distDir, "index.js");
4399
- if (!existsSync16(entry)) {
4515
+ const entry = join16(distDir, "index.js");
4516
+ if (!existsSync17(entry)) {
4400
4517
  return { status: "skipped", reason: `${entry} not found`, newerSources: [] };
4401
4518
  }
4402
4519
  const builtAt = statSync4(entry).mtimeMs;
@@ -4440,7 +4557,7 @@ function collectSourceFiles(srcDir) {
4440
4557
  const found = [];
4441
4558
  const walk = (dir) => {
4442
4559
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
4443
- const full = join15(dir, entry.name);
4560
+ const full = join16(dir, entry.name);
4444
4561
  if (entry.isDirectory()) {
4445
4562
  if (entry.name === "node_modules") continue;
4446
4563
  walk(full);
@@ -4459,7 +4576,7 @@ function collectSourceFiles(srcDir) {
4459
4576
  function findPackageRoot(from) {
4460
4577
  let dir = from;
4461
4578
  for (; ; ) {
4462
- if (existsSync16(join15(dir, "package.json"))) return dir;
4579
+ if (existsSync17(join16(dir, "package.json"))) return dir;
4463
4580
  const parent = dirname5(dir);
4464
4581
  if (parent === dir) return null;
4465
4582
  dir = parent;
@@ -4473,9 +4590,9 @@ function isInside(parent, child) {
4473
4590
 
4474
4591
  // src/lib/credentials.ts
4475
4592
  import { execSync as execSync6 } from "child_process";
4476
- import { existsSync as existsSync17, readFileSync as readFileSync11 } from "fs";
4593
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
4477
4594
  import { homedir as homedir2 } from "os";
4478
- import { join as join16 } from "path";
4595
+ import { join as join17 } from "path";
4479
4596
  import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
4480
4597
  import chalk10 from "chalk";
4481
4598
  import inquirer4 from "inquirer";
@@ -4654,11 +4771,11 @@ async function verifySelectedAwsCredentials(profile, region) {
4654
4771
  return sts.send(new GetCallerIdentityCommand2({}));
4655
4772
  }
4656
4773
  function discoverAwsProfiles() {
4657
- const files = [join16(homedir2(), ".aws", "credentials"), join16(homedir2(), ".aws", "config")];
4774
+ const files = [join17(homedir2(), ".aws", "credentials"), join17(homedir2(), ".aws", "config")];
4658
4775
  const profiles = /* @__PURE__ */ new Set();
4659
4776
  for (const file of files) {
4660
- if (!existsSync17(file)) continue;
4661
- const content = readFileSync11(file, "utf8");
4777
+ if (!existsSync18(file)) continue;
4778
+ const content = readFileSync12(file, "utf8");
4662
4779
  for (const match of content.matchAll(/^\s*\[([^\]]+)\]\s*$/gm)) {
4663
4780
  const section = match[1]?.trim();
4664
4781
  if (!section) continue;
@@ -4683,15 +4800,15 @@ async function resolveRepoIds(github, config) {
4683
4800
  }
4684
4801
 
4685
4802
  // src/config/sibling-schema.ts
4686
- import { z as z4 } from "zod";
4687
- var SiblingConfigSchema = z4.object({
4688
- $schema: z4.string().optional(),
4689
- project: z4.object({
4690
- name: z4.string().min(1).regex(
4803
+ import { z as z5 } from "zod";
4804
+ var SiblingConfigSchema = z5.object({
4805
+ $schema: z5.string().optional(),
4806
+ project: z5.object({
4807
+ name: z5.string().min(1).regex(
4691
4808
  /^[a-z][a-z0-9-]*$/,
4692
4809
  "Must be lowercase kebab-case, starting with a letter (it becomes a URL path segment)"
4693
4810
  ),
4694
- description: z4.string().default(""),
4811
+ description: z5.string().default(""),
4695
4812
  // Notable routes this sibling exposes, shown as labelled links on the
4696
4813
  // core project's Microservices tab (ADR-0007). Each `path` is relative to
4697
4814
  // the sibling's own path_prefix (so "demo" renders as /<prefix>/demo), and
@@ -4699,26 +4816,26 @@ var SiblingConfigSchema = z4.object({
4699
4816
  // routes just shows its single root link. Declare real routes here as you
4700
4817
  // build the sibling's pages; the values flow to the core's
4701
4818
  // siblings.auto.tfvars.json at registration and into siblings.json at deploy.
4702
- routes: z4.array(
4703
- z4.object({
4704
- path: z4.string().min(1).regex(
4819
+ routes: z5.array(
4820
+ z5.object({
4821
+ path: z5.string().min(1).regex(
4705
4822
  /^[a-z0-9][a-z0-9/-]*$/,
4706
4823
  'Sub-path relative to the sibling prefix, no leading slash (e.g. "demo" or "apply")'
4707
4824
  ),
4708
- label: z4.string().min(1)
4825
+ label: z5.string().min(1)
4709
4826
  })
4710
4827
  ).default([])
4711
4828
  }),
4712
4829
  source_control: SourceControlConfigSchema,
4713
4830
  cloud: CloudConfigSchema,
4714
- environments: z4.array(z4.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
4831
+ environments: z5.array(z5.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
4715
4832
  // The core project this sibling is paired with (ADR-0007) — never
4716
4833
  // provisions its own Cognito pool or CloudFront distribution, always
4717
4834
  // plugs into the core project's.
4718
- core: z4.object({
4835
+ core: z5.object({
4719
4836
  // Exactly one of these two must be set — see the superRefine below.
4720
- project_name: z4.string().min(1).optional().describe("Name of a project previously scaffolded with `biffo init` on this machine"),
4721
- config_path: z4.string().min(1).optional().describe(
4837
+ project_name: z5.string().min(1).optional().describe("Name of a project previously scaffolded with `biffo init` on this machine"),
4838
+ config_path: z5.string().min(1).optional().describe(
4722
4839
  "Path to the core project's biffo.config.json, for when it wasn't scaffolded here"
4723
4840
  ),
4724
4841
  // Defaults to project.name at parse time by the caller (sibling-create.ts),
@@ -4731,7 +4848,7 @@ var SiblingConfigSchema = z4.object({
4731
4848
  // CDN's default_cache_behavior instead of a pair of ordered behaviours.
4732
4849
  // It still registers under a non-empty reserved name ("app") — see
4733
4850
  // lib/root-sibling.ts for why the two must not be conflated.
4734
- path_prefix: z4.string().regex(
4851
+ path_prefix: z5.string().regex(
4735
4852
  /^$|^[a-z][a-z0-9-]*$/,
4736
4853
  "Must be lowercase kebab-case, or empty for the root sibling"
4737
4854
  ).optional()
@@ -4739,7 +4856,7 @@ var SiblingConfigSchema = z4.object({
4739
4856
  }).superRefine((config, ctx) => {
4740
4857
  if (!config.core.project_name && !config.core.config_path) {
4741
4858
  ctx.addIssue({
4742
- code: z4.ZodIssueCode.custom,
4859
+ code: z5.ZodIssueCode.custom,
4743
4860
  path: ["core"],
4744
4861
  message: "Either core.project_name or core.config_path is required"
4745
4862
  });
@@ -4748,34 +4865,34 @@ var SiblingConfigSchema = z4.object({
4748
4865
 
4749
4866
  // src/lib/sibling-session.ts
4750
4867
  import {
4751
- existsSync as existsSync18,
4868
+ existsSync as existsSync19,
4752
4869
  mkdirSync as mkdirSync6,
4753
4870
  readdirSync as readdirSync8,
4754
- readFileSync as readFileSync12,
4871
+ readFileSync as readFileSync13,
4755
4872
  rmSync as rmSync7,
4756
4873
  statSync as statSync5,
4757
4874
  writeFileSync as writeFileSync6
4758
4875
  } from "fs";
4759
4876
  import { homedir as homedir3 } from "os";
4760
- import { join as join17 } from "path";
4877
+ import { join as join18 } from "path";
4761
4878
  function sessionsDir2() {
4762
- return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join17(homedir3(), ".biffo", "sibling-sessions");
4879
+ return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join18(homedir3(), ".biffo", "sibling-sessions");
4763
4880
  }
4764
4881
  function sessionPath2(projectName) {
4765
- return join17(sessionsDir2(), `${projectName}.json`);
4882
+ return join18(sessionsDir2(), `${projectName}.json`);
4766
4883
  }
4767
4884
  function loadSiblingSession(projectName) {
4768
4885
  const path = sessionPath2(projectName);
4769
- if (!existsSync18(path)) return null;
4886
+ if (!existsSync19(path)) return null;
4770
4887
  try {
4771
- return JSON.parse(readFileSync12(path, "utf8"));
4888
+ return JSON.parse(readFileSync13(path, "utf8"));
4772
4889
  } catch {
4773
4890
  return null;
4774
4891
  }
4775
4892
  }
4776
4893
  function saveSiblingSession(session) {
4777
4894
  const dir = sessionsDir2();
4778
- if (!existsSync18(dir)) mkdirSync6(dir, { recursive: true });
4895
+ if (!existsSync19(dir)) mkdirSync6(dir, { recursive: true });
4779
4896
  const name = session.config.project?.name ?? "unknown";
4780
4897
  const prior = loadSiblingSession(name);
4781
4898
  if (prior) {
@@ -4797,30 +4914,30 @@ function markSiblingStepComplete(session, step) {
4797
4914
  }
4798
4915
  function deleteSiblingSession(projectName) {
4799
4916
  const path = sessionPath2(projectName);
4800
- if (existsSync18(path)) rmSync7(path);
4917
+ if (existsSync19(path)) rmSync7(path);
4801
4918
  }
4802
4919
 
4803
4920
  // src/commands/sibling-create.ts
4804
- import { cpSync as cpSync2, existsSync as existsSync19, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
4921
+ import { cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
4805
4922
  import { tmpdir as tmpdir4 } from "os";
4806
- import { dirname as dirname6, join as join19, resolve as resolve9 } from "path";
4923
+ import { dirname as dirname6, join as join20, resolve as resolve9 } from "path";
4807
4924
  import { fileURLToPath as fileURLToPath4 } from "url";
4808
4925
  import chalk11 from "chalk";
4809
4926
  import { Command as Command11 } from "commander";
4810
4927
 
4811
4928
  // src/lib/skeleton-dotfiles.ts
4812
4929
  import { readdirSync as readdirSync9, renameSync } from "fs";
4813
- import { join as join18 } from "path";
4930
+ import { join as join19 } from "path";
4814
4931
  var PACKAGED_GITIGNORE = "_gitignore";
4815
4932
  var REAL_GITIGNORE = ".gitignore";
4816
4933
  function restorePackagedDotfiles(dir) {
4817
4934
  const restored = [];
4818
4935
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
4819
- const full = join18(dir, entry.name);
4936
+ const full = join19(dir, entry.name);
4820
4937
  if (entry.isDirectory()) {
4821
4938
  restored.push(...restorePackagedDotfiles(full));
4822
4939
  } else if (entry.name === PACKAGED_GITIGNORE) {
4823
- const target = join18(dir, REAL_GITIGNORE);
4940
+ const target = join19(dir, REAL_GITIGNORE);
4824
4941
  renameSync(full, target);
4825
4942
  restored.push(target);
4826
4943
  }
@@ -4865,7 +4982,7 @@ async function runSiblingCreateCommand(name, options) {
4865
4982
  printDryRun2(config, coreConfig, options.templateRoot);
4866
4983
  return;
4867
4984
  }
4868
- if (!existsSync19(options.templateRoot)) {
4985
+ if (!existsSync20(options.templateRoot)) {
4869
4986
  throw new Error(`Sibling template not found at ${options.templateRoot}`);
4870
4987
  }
4871
4988
  let session = null;
@@ -5050,7 +5167,7 @@ function assertPathPrefixIsAllowed(pathPrefix) {
5050
5167
  }
5051
5168
  }
5052
5169
  function readSiblingConfig(path, root = false) {
5053
- const raw = JSON.parse(readFileSync13(path, "utf8"));
5170
+ const raw = JSON.parse(readFileSync14(path, "utf8"));
5054
5171
  const withDefaults = raw && typeof raw === "object" && "project" in raw && "core" in raw ? {
5055
5172
  ...raw,
5056
5173
  core: {
@@ -5084,7 +5201,7 @@ function resolveCoreConfig(config, configPath) {
5084
5201
  throw new Error("Either core.project_name or core.config_path is required.");
5085
5202
  }
5086
5203
  function parseCoreConfig(path) {
5087
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync13(path, "utf8")));
5204
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
5088
5205
  if (!result.success) {
5089
5206
  throw new Error(
5090
5207
  `Invalid core configuration at ${path}:
@@ -5123,7 +5240,7 @@ async function resolveCoreIdentity(coreAws, coreConfig, environments) {
5123
5240
  return coreIdentity;
5124
5241
  }
5125
5242
  async function pushSkeleton(git, skeletonRoot, cloneUrl, config, coreConfig, githubToken) {
5126
- const workDir = mkdtempSync4(join19(tmpdir4(), `biffo-sibling-${config.project.name}-`));
5243
+ const workDir = mkdtempSync4(join20(tmpdir4(), `biffo-sibling-${config.project.name}-`));
5127
5244
  try {
5128
5245
  writeSiblingTemplate(skeletonRoot, workDir, config, {
5129
5246
  coreProjectName: coreConfig.project.name,
@@ -5139,13 +5256,13 @@ async function pushSkeleton(git, skeletonRoot, cloneUrl, config, coreConfig, git
5139
5256
  }
5140
5257
  }
5141
5258
  function writeSiblingTemplate(templateRoot, targetDir, config, context) {
5142
- if (!existsSync19(templateRoot)) {
5259
+ if (!existsSync20(templateRoot)) {
5143
5260
  throw new Error(`Sibling template not found at ${templateRoot}`);
5144
5261
  }
5145
5262
  cpSync2(templateRoot, targetDir, { recursive: true });
5146
5263
  restorePackagedDotfiles(targetDir);
5147
5264
  writeFileSync7(
5148
- join19(targetDir, "biffo.sibling.json"),
5265
+ join20(targetDir, "biffo.sibling.json"),
5149
5266
  JSON.stringify(
5150
5267
  {
5151
5268
  name: config.project.name,
@@ -5161,10 +5278,10 @@ function writeSiblingTemplate(templateRoot, targetDir, config, context) {
5161
5278
  2
5162
5279
  ) + "\n"
5163
5280
  );
5164
- const envPath = join19(targetDir, "apps", "frontend", ".env.example");
5281
+ const envPath = join20(targetDir, "apps", "frontend", ".env.example");
5165
5282
  try {
5166
5283
  const path = basePathFor(context.pathPrefix);
5167
- const content = readFileSync13(envPath, "utf8").replace(/^NEXT_PUBLIC_SIBLING_NAME=.*$/m, `NEXT_PUBLIC_SIBLING_NAME=${config.project.name}`).replace(/^NEXT_PUBLIC_SIBLING_PATH_PREFIX=.*$/m, `NEXT_PUBLIC_SIBLING_PATH_PREFIX=${path}`).replace(/^NEXT_PUBLIC_BASE_PATH=.*$/m, `NEXT_PUBLIC_BASE_PATH=${path}`);
5284
+ const content = readFileSync14(envPath, "utf8").replace(/^NEXT_PUBLIC_SIBLING_NAME=.*$/m, `NEXT_PUBLIC_SIBLING_NAME=${config.project.name}`).replace(/^NEXT_PUBLIC_SIBLING_PATH_PREFIX=.*$/m, `NEXT_PUBLIC_SIBLING_PATH_PREFIX=${path}`).replace(/^NEXT_PUBLIC_BASE_PATH=.*$/m, `NEXT_PUBLIC_BASE_PATH=${path}`);
5168
5285
  writeFileSync7(envPath, content);
5169
5286
  } catch (err) {
5170
5287
  if (err.code !== "ENOENT") throw err;
@@ -5210,17 +5327,17 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
5210
5327
  }
5211
5328
  function readExistingSiblingOrigins(filePath) {
5212
5329
  try {
5213
- return JSON.parse(readFileSync13(filePath, "utf8"));
5330
+ return JSON.parse(readFileSync14(filePath, "utf8"));
5214
5331
  } catch (err) {
5215
5332
  if (err.code === "ENOENT") return {};
5216
5333
  throw err;
5217
5334
  }
5218
5335
  }
5219
5336
  function assertCoreSupportsSiblingRouting(cloneDir, coreRepo, pathPrefix = "x") {
5220
- const cdnVarsPath = join19(cloneDir, "modules", "cloud", "aws", "cdn", "variables.tf");
5337
+ const cdnVarsPath = join20(cloneDir, "modules", "cloud", "aws", "cdn", "variables.tf");
5221
5338
  let declaresSiblingOrigins = false;
5222
5339
  try {
5223
- declaresSiblingOrigins = /variable\s+"sibling_origins"/.test(readFileSync13(cdnVarsPath, "utf8"));
5340
+ declaresSiblingOrigins = /variable\s+"sibling_origins"/.test(readFileSync14(cdnVarsPath, "utf8"));
5224
5341
  } catch {
5225
5342
  declaresSiblingOrigins = false;
5226
5343
  }
@@ -5230,10 +5347,10 @@ function assertCoreSupportsSiblingRouting(cloneDir, coreRepo, pathPrefix = "x")
5230
5347
  );
5231
5348
  }
5232
5349
  if (!isRootPathPrefix(pathPrefix)) return;
5233
- const cdnMainPath = join19(cloneDir, "modules", "cloud", "aws", "cdn", "main.tf");
5350
+ const cdnMainPath = join20(cloneDir, "modules", "cloud", "aws", "cdn", "main.tf");
5234
5351
  let supportsRoot = false;
5235
5352
  try {
5236
- supportsRoot = /root_sibling_registered/.test(readFileSync13(cdnMainPath, "utf8"));
5353
+ supportsRoot = /root_sibling_registered/.test(readFileSync14(cdnMainPath, "utf8"));
5237
5354
  } catch {
5238
5355
  supportsRoot = false;
5239
5356
  }
@@ -5263,8 +5380,8 @@ async function registerWithCore(git, github, config, coreConfig, pathPrefix, git
5263
5380
  for (const env of config.environments) {
5264
5381
  const bucketName = siteBucketName(config.project.name, env, siblingAccountId);
5265
5382
  const domain = bucketRegionalDomain(bucketName, coreAwsRegion);
5266
- const relativePath = join19("infra", "environments", env, "siblings.auto.tfvars.json");
5267
- const filePath = join19(cloneDir, relativePath);
5383
+ const relativePath = join20("infra", "environments", env, "siblings.auto.tfvars.json");
5384
+ const filePath = join20(cloneDir, relativePath);
5268
5385
  const existing = readExistingSiblingOrigins(filePath);
5269
5386
  const siblings = upsertSiblingOrigin(existing.sibling_origins ?? [], {
5270
5387
  name,
@@ -5344,8 +5461,8 @@ function defaultSiblingTemplateRoot() {
5344
5461
  const start = dirname6(fileURLToPath4(import.meta.url));
5345
5462
  let dir = start;
5346
5463
  for (; ; ) {
5347
- const candidate = join19(dir, "_skeletons", "sibling-template");
5348
- if (existsSync19(candidate)) return candidate;
5464
+ const candidate = join20(dir, "_skeletons", "sibling-template");
5465
+ if (existsSync20(candidate)) return candidate;
5349
5466
  const parent = dirname6(dir);
5350
5467
  if (parent === dir) break;
5351
5468
  dir = parent;
@@ -5369,7 +5486,7 @@ var initCommand = new Command12("init").description("Scaffold a new project from
5369
5486
  let config;
5370
5487
  let githubToken;
5371
5488
  if (options.config) {
5372
- const rawConfig = JSON.parse(readFileSync14(resolve10(options.config), "utf8"));
5489
+ const rawConfig = JSON.parse(readFileSync15(resolve10(options.config), "utf8"));
5373
5490
  config = parseConfig(rawConfig);
5374
5491
  const { account_id: accountId, region } = config.cloud.config;
5375
5492
  session = resolveConfigFileSession(config, accountId, region, options.fresh === true);
@@ -5802,28 +5919,28 @@ async function promptForConfig(awsAccountId, awsRegion, awsProfile) {
5802
5919
  import { Command as Command20 } from "commander";
5803
5920
 
5804
5921
  // src/commands/plugin-create.ts
5805
- import { existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
5806
- import { dirname as dirname8, join as join22, resolve as resolve11 } from "path";
5922
+ import { existsSync as existsSync23, readFileSync as readFileSync17 } from "fs";
5923
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "path";
5807
5924
  import { fileURLToPath as fileURLToPath5 } from "url";
5808
5925
  import chalk13 from "chalk";
5809
5926
  import { Command as Command13 } from "commander";
5810
5927
 
5811
5928
  // src/lib/plugin-locations.ts
5812
- import { existsSync as existsSync20, readdirSync as readdirSync10 } from "fs";
5813
- import { join as join20 } from "path";
5929
+ import { existsSync as existsSync21, readdirSync as readdirSync10 } from "fs";
5930
+ import { join as join21 } from "path";
5814
5931
  var FIRST_PARTY_PLUGINS_DIR = "_plugins";
5815
5932
  var PLUGIN_MANIFEST_FILE = "biffo.plugin.json";
5816
5933
  function pluginDir(name, channel) {
5817
5934
  return channel === "first-party" ? `services/${FIRST_PARTY_PLUGINS_DIR}/${name}` : `services/${name}`;
5818
5935
  }
5819
5936
  function scanDir(absDir, relDir, channel) {
5820
- if (!existsSync20(absDir)) return [];
5937
+ if (!existsSync21(absDir)) return [];
5821
5938
  const found = [];
5822
5939
  for (const entry of readdirSync10(absDir, { withFileTypes: true })) {
5823
5940
  if (!entry.isDirectory()) continue;
5824
5941
  if (channel === "third-party" && entry.name === FIRST_PARTY_PLUGINS_DIR) continue;
5825
- const manifestPath = join20(absDir, entry.name, PLUGIN_MANIFEST_FILE);
5826
- if (!existsSync20(manifestPath)) continue;
5942
+ const manifestPath = join21(absDir, entry.name, PLUGIN_MANIFEST_FILE);
5943
+ if (!existsSync21(manifestPath)) continue;
5827
5944
  found.push({
5828
5945
  dirName: entry.name,
5829
5946
  relDir: `${relDir}/${entry.name}`,
@@ -5834,11 +5951,11 @@ function scanDir(absDir, relDir, channel) {
5834
5951
  return found;
5835
5952
  }
5836
5953
  function findInstalledPlugins(cwd) {
5837
- const servicesDir = join20(cwd, "services");
5954
+ const servicesDir = join21(cwd, "services");
5838
5955
  return [
5839
5956
  ...scanDir(servicesDir, "services", "third-party"),
5840
5957
  ...scanDir(
5841
- join20(servicesDir, FIRST_PARTY_PLUGINS_DIR),
5958
+ join21(servicesDir, FIRST_PARTY_PLUGINS_DIR),
5842
5959
  `services/${FIRST_PARTY_PLUGINS_DIR}`,
5843
5960
  "first-party"
5844
5961
  )
@@ -5846,46 +5963,46 @@ function findInstalledPlugins(cwd) {
5846
5963
  }
5847
5964
 
5848
5965
  // src/lib/plugin-manifest.ts
5849
- import { z as z5 } from "zod";
5966
+ import { z as z6 } from "zod";
5850
5967
  var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["id", "tenant_id", "created_at", "updated_at"]);
5851
5968
  var COLUMN_TYPE_PATTERN = /^(String|Integer|Text|Boolean|Float|DateTime)(\(.*\))?$/;
5852
- var ColumnDefinitionSchema = z5.object({
5853
- name: z5.string().refine(
5969
+ var ColumnDefinitionSchema = z6.object({
5970
+ name: z6.string().refine(
5854
5971
  (n) => !RESERVED_COLUMN_NAMES.has(n),
5855
5972
  (n) => ({
5856
5973
  message: `Column '${n}' is reserved and added automatically; it must not be declared in the manifest.`
5857
5974
  })
5858
5975
  ),
5859
- type: z5.string().regex(
5976
+ type: z6.string().regex(
5860
5977
  COLUMN_TYPE_PATTERN,
5861
5978
  "must be one of String, Integer, Text, Boolean, Float, DateTime (e.g. 'String(255)')"
5862
5979
  ),
5863
- primary_key: z5.boolean().default(false),
5864
- nullable: z5.boolean().default(false),
5865
- index: z5.boolean().default(false),
5866
- default: z5.string().optional(),
5867
- description: z5.string().default("")
5980
+ primary_key: z6.boolean().default(false),
5981
+ nullable: z6.boolean().default(false),
5982
+ index: z6.boolean().default(false),
5983
+ default: z6.string().optional(),
5984
+ description: z6.string().default("")
5868
5985
  });
5869
- var IndexDefinitionSchema = z5.object({
5870
- name: z5.string(),
5871
- columns: z5.array(z5.string()).min(1),
5872
- unique: z5.boolean().default(false)
5986
+ var IndexDefinitionSchema = z6.object({
5987
+ name: z6.string(),
5988
+ columns: z6.array(z6.string()).min(1),
5989
+ unique: z6.boolean().default(false)
5873
5990
  });
5874
- var PermissionRuleSchema = z5.object({
5875
- allowed: z5.boolean().default(false),
5876
- required_role: z5.array(z5.string()).default([])
5991
+ var PermissionRuleSchema = z6.object({
5992
+ allowed: z6.boolean().default(false),
5993
+ required_role: z6.array(z6.string()).default([])
5877
5994
  }).strict();
5878
- var TablePermissionsSchema = z5.object({
5995
+ var TablePermissionsSchema = z6.object({
5879
5996
  list: PermissionRuleSchema.default({}),
5880
5997
  read: PermissionRuleSchema.default({}),
5881
5998
  create: PermissionRuleSchema.default({}),
5882
5999
  update: PermissionRuleSchema.default({}),
5883
6000
  delete: PermissionRuleSchema.default({})
5884
6001
  }).strict();
5885
- var TableDefinitionSchema = z5.object({
5886
- name: z5.string().regex(/^[a-z][a-z0-9_]*$/, "table name must be snake_case, e.g. rbac_roles"),
5887
- columns: z5.array(ColumnDefinitionSchema).default([]),
5888
- indexes: z5.array(IndexDefinitionSchema).default([]),
6002
+ var TableDefinitionSchema = z6.object({
6003
+ name: z6.string().regex(/^[a-z][a-z0-9_]*$/, "table name must be snake_case, e.g. rbac_roles"),
6004
+ columns: z6.array(ColumnDefinitionSchema).default([]),
6005
+ indexes: z6.array(IndexDefinitionSchema).default([]),
5889
6006
  permissions: TablePermissionsSchema.default({})
5890
6007
  }).superRefine((table, ctx) => {
5891
6008
  const colCounts = /* @__PURE__ */ new Map();
@@ -5893,7 +6010,7 @@ var TableDefinitionSchema = z5.object({
5893
6010
  for (const [name, count] of colCounts) {
5894
6011
  if (count > 1) {
5895
6012
  ctx.addIssue({
5896
- code: z5.ZodIssueCode.custom,
6013
+ code: z6.ZodIssueCode.custom,
5897
6014
  message: `Duplicate column name '${name}' in table '${table.name}'`
5898
6015
  });
5899
6016
  }
@@ -5903,7 +6020,7 @@ var TableDefinitionSchema = z5.object({
5903
6020
  for (const [name, count] of idxCounts) {
5904
6021
  if (count > 1) {
5905
6022
  ctx.addIssue({
5906
- code: z5.ZodIssueCode.custom,
6023
+ code: z6.ZodIssueCode.custom,
5907
6024
  message: `Duplicate index name '${name}' in table '${table.name}'`
5908
6025
  });
5909
6026
  }
@@ -5913,7 +6030,7 @@ var TableDefinitionSchema = z5.object({
5913
6030
  for (const col of idx.columns) {
5914
6031
  if (!validColumns.has(col)) {
5915
6032
  ctx.addIssue({
5916
- code: z5.ZodIssueCode.custom,
6033
+ code: z6.ZodIssueCode.custom,
5917
6034
  message: `Index '${idx.name}' on table '${table.name}' references unknown column '${col}'`
5918
6035
  });
5919
6036
  }
@@ -5928,17 +6045,17 @@ var OPERATION_METHODS = {
5928
6045
  delete: /* @__PURE__ */ new Set(["DELETE"])
5929
6046
  };
5930
6047
  var SINGLE_ROW_OPERATIONS = /* @__PURE__ */ new Set(["read", "update", "delete"]);
5931
- var RouteDefSchema = z5.object({
5932
- method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
5933
- path: z5.string().startsWith("/", "path must start with '/'"),
5934
- table: z5.string(),
5935
- operation: z5.enum(["list", "read", "create", "update", "delete"]),
5936
- description: z5.string().default("")
6048
+ var RouteDefSchema = z6.object({
6049
+ method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
6050
+ path: z6.string().startsWith("/", "path must start with '/'"),
6051
+ table: z6.string(),
6052
+ operation: z6.enum(["list", "read", "create", "update", "delete"]),
6053
+ description: z6.string().default("")
5937
6054
  }).superRefine((route, ctx) => {
5938
6055
  const allowed = OPERATION_METHODS[route.operation];
5939
6056
  if (allowed && !allowed.has(route.method)) {
5940
6057
  ctx.addIssue({
5941
- code: z5.ZodIssueCode.custom,
6058
+ code: z6.ZodIssueCode.custom,
5942
6059
  message: `operation '${route.operation}' requires method in [${[...allowed].sort().join(", ")}], got '${route.method}'`
5943
6060
  });
5944
6061
  }
@@ -5946,38 +6063,38 @@ var RouteDefSchema = z5.object({
5946
6063
  const needsId = SINGLE_ROW_OPERATIONS.has(route.operation);
5947
6064
  if (needsId && !hasId) {
5948
6065
  ctx.addIssue({
5949
- code: z5.ZodIssueCode.custom,
6066
+ code: z6.ZodIssueCode.custom,
5950
6067
  message: `operation '${route.operation}' addresses a single row and requires an '{id}' path parameter: ${route.path}`
5951
6068
  });
5952
6069
  }
5953
6070
  if (!needsId && hasId) {
5954
6071
  ctx.addIssue({
5955
- code: z5.ZodIssueCode.custom,
6072
+ code: z6.ZodIssueCode.custom,
5956
6073
  message: `operation '${route.operation}' is collection-level and must not have an '{id}' path parameter: ${route.path}`
5957
6074
  });
5958
6075
  }
5959
6076
  });
5960
- var PluginManifestSchema = z5.object({
5961
- name: z5.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
5962
- version: z5.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
5963
- description: z5.string().default(""),
5964
- author: z5.string().default("Biffo Team"),
5965
- tags: z5.array(z5.string()).default([]),
5966
- tables: z5.array(TableDefinitionSchema).default([]),
5967
- api_routes: z5.array(RouteDefSchema).default([]),
6077
+ var PluginManifestSchema = z6.object({
6078
+ name: z6.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
6079
+ version: z6.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
6080
+ description: z6.string().default(""),
6081
+ author: z6.string().default("Biffo Team"),
6082
+ tags: z6.array(z6.string()).default([]),
6083
+ tables: z6.array(TableDefinitionSchema).default([]),
6084
+ api_routes: z6.array(RouteDefSchema).default([]),
5968
6085
  // Events the plugin reacts to. Parsed (rather than dropped as an unknown
5969
6086
  // key) so `biffo plugin install` can warn when a plugin declares
5970
6087
  // subscriptions but ships no terraform/ to route them — see #194 and
5971
6088
  // lib/plugin-terraform-guard.ts. Kept loose deliberately: the authoritative
5972
6089
  // schema is the registry's, and this consumer only needs to count them.
5973
- event_subscriptions: z5.array(z5.object({ source: z5.string(), detail_type: z5.string() }).passthrough()).default([]),
5974
- required_core_version: z5.string().default(">=0.0.0")
6090
+ event_subscriptions: z6.array(z6.object({ source: z6.string(), detail_type: z6.string() }).passthrough()).default([]),
6091
+ required_core_version: z6.string().default(">=0.0.0")
5975
6092
  }).superRefine((manifest, ctx) => {
5976
6093
  const tableNames = new Set(manifest.tables.map((t) => t.name));
5977
6094
  for (const route of manifest.api_routes) {
5978
6095
  if (!tableNames.has(route.table)) {
5979
6096
  ctx.addIssue({
5980
- code: z5.ZodIssueCode.custom,
6097
+ code: z6.ZodIssueCode.custom,
5981
6098
  message: `Route ${route.method} ${route.path} references table '${route.table}', which is not declared in this manifest's 'tables' (${[...tableNames].sort().join(", ") || "none"})`
5982
6099
  });
5983
6100
  }
@@ -5998,13 +6115,13 @@ function validateManifest(raw) {
5998
6115
  // src/lib/plugin-scaffold.ts
5999
6116
  import {
6000
6117
  copyFileSync,
6001
- existsSync as existsSync21,
6118
+ existsSync as existsSync22,
6002
6119
  mkdirSync as mkdirSync8,
6003
- readFileSync as readFileSync15,
6120
+ readFileSync as readFileSync16,
6004
6121
  readdirSync as readdirSync11,
6005
6122
  writeFileSync as writeFileSync8
6006
6123
  } from "fs";
6007
- import { dirname as dirname7, join as join21 } from "path";
6124
+ import { dirname as dirname7, join as join22 } from "path";
6008
6125
  var STANDALONE_ONLY_ENTRIES = {
6009
6126
  ".github": "standalone-repo CI/release workflows \u2014 the host monorepo already runs lint/type/test/security over services/",
6010
6127
  "registry-schema.json": "the plugin-registry publishing schema, used when submitting a *published* plugin to the registry repo, not by an in-tree plugin"
@@ -6057,10 +6174,10 @@ function applySubstitutions(text, names) {
6057
6174
  }
6058
6175
  var BINARY_EXTENSIONS = /\.(png|jpe?g|gif|ico|woff2?|ttf|zip|gz)$/i;
6059
6176
  function scaffoldPlugin(skeletonRoot, destDir, names) {
6060
- if (!existsSync21(skeletonRoot)) {
6177
+ if (!existsSync22(skeletonRoot)) {
6061
6178
  throw new Error(`Plugin skeleton not found at ${skeletonRoot}`);
6062
6179
  }
6063
- if (!existsSync21(join21(skeletonRoot, "terraform"))) {
6180
+ if (!existsSync22(join22(skeletonRoot, "terraform"))) {
6064
6181
  throw new Error(
6065
6182
  `Plugin skeleton at ${skeletonRoot} has no terraform/ directory. Refusing to scaffold a plugin that cannot receive events (issue #194) \u2014 the skeleton is broken.`
6066
6183
  );
@@ -6068,7 +6185,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
6068
6185
  const skipped = [];
6069
6186
  const files = [];
6070
6187
  const walk = (relDir) => {
6071
- const absDir = join21(skeletonRoot, relDir);
6188
+ const absDir = join22(skeletonRoot, relDir);
6072
6189
  for (const entry of readdirSync11(absDir, { withFileTypes: true }).sort(
6073
6190
  (a, b) => a.name.localeCompare(b.name)
6074
6191
  )) {
@@ -6083,14 +6200,14 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
6083
6200
  continue;
6084
6201
  }
6085
6202
  const destRel = applySubstitutions(relPath, names);
6086
- const destPath = join21(destDir, destRel);
6203
+ const destPath = join22(destDir, destRel);
6087
6204
  mkdirSync8(dirname7(destPath), { recursive: true });
6088
6205
  if (BINARY_EXTENSIONS.test(entry.name)) {
6089
- copyFileSync(join21(skeletonRoot, relPath), destPath);
6206
+ copyFileSync(join22(skeletonRoot, relPath), destPath);
6090
6207
  } else {
6091
6208
  writeFileSync8(
6092
6209
  destPath,
6093
- applySubstitutions(readFileSync15(join21(skeletonRoot, relPath), "utf8"), names)
6210
+ applySubstitutions(readFileSync16(join22(skeletonRoot, relPath), "utf8"), names)
6094
6211
  );
6095
6212
  }
6096
6213
  files.push(destRel);
@@ -6107,8 +6224,8 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
6107
6224
  function findSkeletonRoot(startDir, skeleton) {
6108
6225
  let dir = startDir;
6109
6226
  for (; ; ) {
6110
- const candidate = join21(dir, "_skeletons", skeleton);
6111
- if (existsSync21(candidate)) return candidate;
6227
+ const candidate = join22(dir, "_skeletons", skeleton);
6228
+ if (existsSync22(candidate)) return candidate;
6112
6229
  const parent = dirname7(dir);
6113
6230
  if (parent === dir) return null;
6114
6231
  dir = parent;
@@ -6145,7 +6262,7 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
6145
6262
  );
6146
6263
  async function runPluginCreate(name, options, deps) {
6147
6264
  const names = deriveNames(name);
6148
- const isInstance = existsSync22(join22(options.cwd, INSTANCE_CORE_FILE));
6265
+ const isInstance = existsSync23(join23(options.cwd, INSTANCE_CORE_FILE));
6149
6266
  if (options.firstParty && isInstance) {
6150
6267
  throw new Error(
6151
6268
  `--first-party scaffolds into services/_plugins/, which is template-owned: \`biffo core upgrade\` three-way-merges it against the template on every upgrade, and the template has no '${names.slug}'. This checkout is a Biffo instance (${INSTANCE_CORE_FILE} is present), so your plugin belongs in the user-owned ${pluginDir(names.slug, "third-party")}/ \u2014 re-run without --first-party.`
@@ -6153,19 +6270,19 @@ async function runPluginCreate(name, options, deps) {
6153
6270
  }
6154
6271
  const channel = options.firstParty ? "first-party" : "third-party";
6155
6272
  const relDir = pluginDir(names.slug, channel);
6156
- const destDir = join22(options.cwd, relDir);
6157
- const servicesDir = join22(options.cwd, "services");
6158
- if (!existsSync22(servicesDir)) {
6273
+ const destDir = join23(options.cwd, relDir);
6274
+ const servicesDir = join23(options.cwd, "services");
6275
+ if (!existsSync23(servicesDir)) {
6159
6276
  throw new Error(
6160
6277
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6161
6278
  );
6162
6279
  }
6163
- if (existsSync22(destDir)) {
6280
+ if (existsSync23(destDir)) {
6164
6281
  throw new Error(`${relDir}/ already exists. Choose a different name, or remove it first.`);
6165
6282
  }
6166
6283
  const here = dirname8(fileURLToPath5(import.meta.url));
6167
- const skeletonRoot = options.skeletonRoot ?? findSkeletonRoot(here, "plugin-template") ?? join22(options.cwd, "_skeletons", "plugin-template");
6168
- if (!existsSync22(skeletonRoot)) {
6284
+ const skeletonRoot = options.skeletonRoot ?? findSkeletonRoot(here, "plugin-template") ?? join23(options.cwd, "_skeletons", "plugin-template");
6285
+ if (!existsSync23(skeletonRoot)) {
6169
6286
  throw new Error(
6170
6287
  `Could not find the plugin skeleton (_skeletons/plugin-template/). Pass --skeleton <path> to point at it explicitly.`
6171
6288
  );
@@ -6180,8 +6297,8 @@ async function runPluginCreate(name, options, deps) {
6180
6297
  for (const { entry, reason } of skipped) {
6181
6298
  log.info(`Skipped ${entry} \u2014 ${reason}`);
6182
6299
  }
6183
- const manifestPath = join22(destDir, "biffo.plugin.json");
6184
- const manifest = validateManifest(JSON.parse(readFileSync16(manifestPath, "utf8")));
6300
+ const manifestPath = join23(destDir, "biffo.plugin.json");
6301
+ const manifest = validateManifest(JSON.parse(readFileSync17(manifestPath, "utf8")));
6185
6302
  if (manifest.name !== names.slug) {
6186
6303
  throw new Error(
6187
6304
  `Scaffolded manifest declares name '${manifest.name}', expected '${names.slug}'. The skeleton's manifest name may have diverged from 'example-plugin'.`
@@ -6237,25 +6354,25 @@ import chalk14 from "chalk";
6237
6354
  import { Command as Command14 } from "commander";
6238
6355
 
6239
6356
  // src/adapters/registry/index.ts
6240
- import { z as z6 } from "zod";
6241
- var RegistryPluginEntrySchema = z6.object({
6242
- name: z6.string().regex(/^[a-z][a-z0-9-]*$/),
6243
- version: z6.string().regex(/^\d+\.\d+\.\d+$/),
6244
- minor_version: z6.string().regex(/^\d+\.\d+$/),
6245
- repo: z6.string().url(),
6246
- description: z6.string().optional(),
6247
- author: z6.string().optional(),
6248
- tags: z6.array(z6.string()).optional(),
6249
- required_core_version: z6.string().optional(),
6250
- infra_modules: z6.array(z6.string()).optional(),
6251
- api_routes: z6.array(z6.string()).optional(),
6252
- ui_components: z6.array(z6.string()).optional(),
6253
- status: z6.enum(["active", "disabled"])
6357
+ import { z as z7 } from "zod";
6358
+ var RegistryPluginEntrySchema = z7.object({
6359
+ name: z7.string().regex(/^[a-z][a-z0-9-]*$/),
6360
+ version: z7.string().regex(/^\d+\.\d+\.\d+$/),
6361
+ minor_version: z7.string().regex(/^\d+\.\d+$/),
6362
+ repo: z7.string().url(),
6363
+ description: z7.string().optional(),
6364
+ author: z7.string().optional(),
6365
+ tags: z7.array(z7.string()).optional(),
6366
+ required_core_version: z7.string().optional(),
6367
+ infra_modules: z7.array(z7.string()).optional(),
6368
+ api_routes: z7.array(z7.string()).optional(),
6369
+ ui_components: z7.array(z7.string()).optional(),
6370
+ status: z7.enum(["active", "disabled"])
6254
6371
  });
6255
- var PluginRegistrySchema = z6.object({
6256
- schema_version: z6.string(),
6257
- last_updated: z6.string(),
6258
- plugins: z6.array(RegistryPluginEntrySchema)
6372
+ var PluginRegistrySchema = z7.object({
6373
+ schema_version: z7.string(),
6374
+ last_updated: z7.string(),
6375
+ plugins: z7.array(RegistryPluginEntrySchema)
6259
6376
  });
6260
6377
  var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/keiranholloway/biffo-plugins-registry/main/plugins.json";
6261
6378
  var RegistryAdapter = class {
@@ -6371,14 +6488,14 @@ function printEntry(entry) {
6371
6488
  }
6372
6489
 
6373
6490
  // src/commands/plugin-install.ts
6374
- import { cpSync as cpSync3, existsSync as existsSync23, mkdirSync as mkdirSync9, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
6375
- import { basename, join as join24, relative as relative3, resolve as resolve12 } from "path";
6491
+ import { cpSync as cpSync3, existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
6492
+ import { basename, join as join25, relative as relative3, resolve as resolve12 } from "path";
6376
6493
  import chalk15 from "chalk";
6377
6494
  import { Command as Command15 } from "commander";
6378
6495
 
6379
6496
  // src/adapters/plugin-migrations/index.ts
6380
6497
  import { execa as execa4 } from "execa";
6381
- import { join as join23 } from "path";
6498
+ import { join as join24 } from "path";
6382
6499
  var PluginMigrationsAdapter = class {
6383
6500
  /**
6384
6501
  * Generates migration file(s) for `pluginNames` (every discovered
@@ -6387,22 +6504,22 @@ var PluginMigrationsAdapter = class {
6387
6504
  * or declared no tables.
6388
6505
  */
6389
6506
  async generate(cwd, pluginNames) {
6390
- const scriptPath = join23(cwd, "services", "api", "scripts", "generate_plugin_migrations.py");
6507
+ const scriptPath = join24(cwd, "services", "api", "scripts", "generate_plugin_migrations.py");
6391
6508
  const args = [
6392
6509
  "run",
6393
6510
  "python",
6394
6511
  scriptPath,
6395
6512
  "--services-root",
6396
- join23(cwd, "services"),
6513
+ join24(cwd, "services"),
6397
6514
  "--versions-dir",
6398
- join23(cwd, "services", "api", "migrations", "versions")
6515
+ join24(cwd, "services", "api", "migrations", "versions")
6399
6516
  ];
6400
6517
  for (const name of pluginNames ?? []) {
6401
6518
  args.push("--plugin", name);
6402
6519
  }
6403
6520
  let result;
6404
6521
  try {
6405
- result = await execa4("uv", args, { cwd: join23(cwd, "services", "api") });
6522
+ result = await execa4("uv", args, { cwd: join24(cwd, "services", "api") });
6406
6523
  } catch (err) {
6407
6524
  const cause = err;
6408
6525
  if (cause.code === "ENOENT") {
@@ -6460,14 +6577,14 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
6460
6577
  ".terraform"
6461
6578
  ]);
6462
6579
  function resolveLocalPlugin(localPath) {
6463
- if (!existsSync23(localPath)) {
6580
+ if (!existsSync24(localPath)) {
6464
6581
  throw new Error(`--local path does not exist: ${localPath}`);
6465
6582
  }
6466
6583
  if (!statSync6(localPath).isDirectory()) {
6467
6584
  throw new Error(`--local path is not a directory: ${localPath}`);
6468
6585
  }
6469
- const manifestPath = join24(localPath, "biffo.plugin.json");
6470
- if (!existsSync23(manifestPath)) {
6586
+ const manifestPath = join25(localPath, "biffo.plugin.json");
6587
+ if (!existsSync24(manifestPath)) {
6471
6588
  throw new Error(
6472
6589
  `${localPath} does not contain a biffo.plugin.json manifest at its root \u2014 is it a plugin directory? (Scaffold one with \`biffo plugin create <name>\`.)`
6473
6590
  );
@@ -6493,8 +6610,8 @@ function parsePluginTarget(target) {
6493
6610
  async function cloneAndValidatePlugin(entry, git) {
6494
6611
  const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
6495
6612
  try {
6496
- const manifestPath = join24(tmpDir, "biffo.plugin.json");
6497
- if (!existsSync23(manifestPath)) {
6613
+ const manifestPath = join25(tmpDir, "biffo.plugin.json");
6614
+ if (!existsSync24(manifestPath)) {
6498
6615
  throw new Error(
6499
6616
  `Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
6500
6617
  );
@@ -6522,8 +6639,8 @@ async function runPluginInstall(target, options, deps) {
6522
6639
  `Nothing to install. Pass a registry target (e.g. \`biffo plugin install acme-crm@1.0\`) or a local plugin directory (\`biffo plugin install --local services/acme-crm\`).`
6523
6640
  );
6524
6641
  }
6525
- const servicesDir = join24(options.cwd, "services");
6526
- if (!existsSync23(servicesDir)) {
6642
+ const servicesDir = join25(options.cwd, "services");
6643
+ if (!existsSync24(servicesDir)) {
6527
6644
  throw new Error(
6528
6645
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6529
6646
  );
@@ -6541,10 +6658,10 @@ async function runPluginInstall(target, options, deps) {
6541
6658
  }
6542
6659
  const pluginName = entry ? entry.name : source.name;
6543
6660
  const relTargetDir = pluginDir(pluginName, "third-party");
6544
- const targetDir = join24(options.cwd, relTargetDir);
6545
- const modulesDir = join24(options.cwd, "modules", "plugins", pluginName);
6661
+ const targetDir = join25(options.cwd, relTargetDir);
6662
+ const modulesDir = join25(options.cwd, "modules", "plugins", pluginName);
6546
6663
  const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
6547
- if (existsSync23(targetDir) && !inTreeSource) {
6664
+ if (existsSync24(targetDir) && !inTreeSource) {
6548
6665
  throw new Error(
6549
6666
  `Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
6550
6667
  );
@@ -6587,8 +6704,8 @@ async function runPluginInstall(target, options, deps) {
6587
6704
  log.success(`Installed plugin source at ${relTargetDir}/`);
6588
6705
  }
6589
6706
  const stagePaths = [relTargetDir];
6590
- const tfSourceDir = join24(targetDir, "terraform");
6591
- if (existsSync23(tfSourceDir)) {
6707
+ const tfSourceDir = join25(targetDir, "terraform");
6708
+ if (existsSync24(tfSourceDir)) {
6592
6709
  mkdirSync9(modulesDir, { recursive: true });
6593
6710
  cpSync3(tfSourceDir, modulesDir, { recursive: true });
6594
6711
  stagePaths.push(`modules/plugins/${pluginName}`);
@@ -6645,7 +6762,7 @@ async function runPluginInstall(target, options, deps) {
6645
6762
  }
6646
6763
  function parseManifestFile(path) {
6647
6764
  try {
6648
- return JSON.parse(readFileSync17(path, "utf8"));
6765
+ return JSON.parse(readFileSync18(path, "utf8"));
6649
6766
  } catch (err) {
6650
6767
  throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
6651
6768
  }
@@ -6682,8 +6799,8 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
6682
6799
  }
6683
6800
 
6684
6801
  // src/commands/plugin-list.ts
6685
- import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
6686
- import { join as join25, resolve as resolve13 } from "path";
6802
+ import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
6803
+ import { join as join26, resolve as resolve13 } from "path";
6687
6804
  import chalk16 from "chalk";
6688
6805
  import { Command as Command16 } from "commander";
6689
6806
  var pluginListCommand = new Command16("list").description("List plugins installed in this project checkout").option("--cwd <path>", "Project root to scan (defaults to the current directory)").action(async (options) => {
@@ -6696,8 +6813,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
6696
6813
  }
6697
6814
  });
6698
6815
  async function runPluginList(options) {
6699
- const servicesDir = join25(options.cwd, "services");
6700
- if (!existsSync24(servicesDir)) {
6816
+ const servicesDir = join26(options.cwd, "services");
6817
+ if (!existsSync25(servicesDir)) {
6701
6818
  throw new Error(
6702
6819
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6703
6820
  );
@@ -6705,7 +6822,7 @@ async function runPluginList(options) {
6705
6822
  const plugins = [];
6706
6823
  for (const location of findInstalledPlugins(options.cwd)) {
6707
6824
  try {
6708
- const manifest = validateManifest(JSON.parse(readFileSync18(location.manifestPath, "utf8")));
6825
+ const manifest = validateManifest(JSON.parse(readFileSync19(location.manifestPath, "utf8")));
6709
6826
  plugins.push({
6710
6827
  name: manifest.name,
6711
6828
  version: manifest.version,
@@ -6742,8 +6859,8 @@ async function runPluginList(options) {
6742
6859
  }
6743
6860
 
6744
6861
  // src/commands/plugin-sync-migrations.ts
6745
- import { existsSync as existsSync25 } from "fs";
6746
- import { join as join26, relative as relative4, resolve as resolve14 } from "path";
6862
+ import { existsSync as existsSync26 } from "fs";
6863
+ import { join as join27, relative as relative4, resolve as resolve14 } from "path";
6747
6864
  import chalk17 from "chalk";
6748
6865
  import { Command as Command17 } from "commander";
6749
6866
  var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
@@ -6764,11 +6881,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
6764
6881
  }
6765
6882
  );
6766
6883
  async function runPluginSyncMigrations(name, options, deps) {
6767
- const servicesDir = join26(options.cwd, "services");
6768
- if (!existsSync25(servicesDir)) {
6884
+ const servicesDir = join27(options.cwd, "services");
6885
+ if (!existsSync26(servicesDir)) {
6769
6886
  throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
6770
6887
  }
6771
- if (name && !existsSync25(join26(servicesDir, name, "biffo.plugin.json"))) {
6888
+ if (name && !existsSync26(join27(servicesDir, name, "biffo.plugin.json"))) {
6772
6889
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
6773
6890
  }
6774
6891
  if (options.dryRun) {
@@ -6804,8 +6921,8 @@ async function runPluginSyncMigrations(name, options, deps) {
6804
6921
  }
6805
6922
 
6806
6923
  // src/commands/plugin-uninstall.ts
6807
- import { existsSync as existsSync26, readFileSync as readFileSync19, rmSync as rmSync8 } from "fs";
6808
- import { join as join27, resolve as resolve15 } from "path";
6924
+ import { existsSync as existsSync27, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
6925
+ import { join as join28, resolve as resolve15 } from "path";
6809
6926
  import chalk18 from "chalk";
6810
6927
  import { Command as Command18 } from "commander";
6811
6928
  import inquirer6 from "inquirer";
@@ -6837,16 +6954,16 @@ async function runPluginUninstall(name, options, deps) {
6837
6954
  if (!NAME_PATTERN2.test(name)) {
6838
6955
  throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
6839
6956
  }
6840
- const servicesDir = join27(options.cwd, "services");
6841
- if (!existsSync26(servicesDir)) {
6957
+ const servicesDir = join28(options.cwd, "services");
6958
+ if (!existsSync27(servicesDir)) {
6842
6959
  throw new Error(
6843
6960
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6844
6961
  );
6845
6962
  }
6846
- const targetDir = join27(servicesDir, name);
6847
- if (!existsSync26(targetDir)) {
6848
- const firstParty = join27(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
6849
- if (existsSync26(firstParty)) {
6963
+ const targetDir = join28(servicesDir, name);
6964
+ if (!existsSync27(targetDir)) {
6965
+ const firstParty = join28(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
6966
+ if (existsSync27(firstParty)) {
6850
6967
  throw new Error(
6851
6968
  `Plugin '${name}' is a first-party plugin at ${pluginDir(name, "first-party")}/, which is template-owned \u2014 \`biffo core upgrade\` would restore it on the next upgrade. Disable it instead by removing '${name}' from \`enabled_plugins\` in infra/environments/<env>/main.tf and re-applying.`
6852
6969
  );
@@ -6854,9 +6971,9 @@ async function runPluginUninstall(name, options, deps) {
6854
6971
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
6855
6972
  }
6856
6973
  const version = readInstalledVersion(targetDir);
6857
- const modulesDir = join27(options.cwd, "modules", "plugins", name);
6974
+ const modulesDir = join28(options.cwd, "modules", "plugins", name);
6858
6975
  const stagePaths = [`services/${name}`];
6859
- if (existsSync26(modulesDir)) {
6976
+ if (existsSync27(modulesDir)) {
6860
6977
  stagePaths.push(`modules/plugins/${name}`);
6861
6978
  }
6862
6979
  if (options.dryRun) {
@@ -6878,7 +6995,7 @@ async function runPluginUninstall(name, options, deps) {
6878
6995
  }
6879
6996
  rmSync8(targetDir, { recursive: true, force: true });
6880
6997
  log.success(`Removed services/${name}/`);
6881
- if (existsSync26(modulesDir)) {
6998
+ if (existsSync27(modulesDir)) {
6882
6999
  rmSync8(modulesDir, { recursive: true, force: true });
6883
7000
  log.success(`Removed modules/plugins/${name}/`);
6884
7001
  const wiring = syncPluginTerraform(options.cwd);
@@ -6915,10 +7032,10 @@ async function runPluginUninstall(name, options, deps) {
6915
7032
  }
6916
7033
  }
6917
7034
  function readInstalledVersion(targetDir) {
6918
- const manifestPath = join27(targetDir, "biffo.plugin.json");
6919
- if (!existsSync26(manifestPath)) return void 0;
7035
+ const manifestPath = join28(targetDir, "biffo.plugin.json");
7036
+ if (!existsSync27(manifestPath)) return void 0;
6920
7037
  try {
6921
- return validateManifest(JSON.parse(readFileSync19(manifestPath, "utf8"))).version;
7038
+ return validateManifest(JSON.parse(readFileSync20(manifestPath, "utf8"))).version;
6922
7039
  } catch {
6923
7040
  return void 0;
6924
7041
  }
@@ -6951,8 +7068,8 @@ function printDryRun5(name, version, stagePaths, keepData) {
6951
7068
  }
6952
7069
 
6953
7070
  // src/commands/plugin-upgrade.ts
6954
- import { cpSync as cpSync4, existsSync as existsSync27, mkdirSync as mkdirSync10, readFileSync as readFileSync20, rmSync as rmSync9 } from "fs";
6955
- import { join as join28, relative as relative5, resolve as resolve16 } from "path";
7071
+ import { cpSync as cpSync4, existsSync as existsSync28, mkdirSync as mkdirSync10, readFileSync as readFileSync21, rmSync as rmSync9 } from "fs";
7072
+ import { join as join29, relative as relative5, resolve as resolve16 } from "path";
6956
7073
  import chalk19 from "chalk";
6957
7074
  import { Command as Command19 } from "commander";
6958
7075
  import inquirer7 from "inquirer";
@@ -6977,14 +7094,14 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
6977
7094
  });
6978
7095
  async function runPluginUpgrade(target, options, deps) {
6979
7096
  const { name, minor } = parsePluginTarget(target);
6980
- const servicesDir = join28(options.cwd, "services");
6981
- if (!existsSync27(servicesDir)) {
7097
+ const servicesDir = join29(options.cwd, "services");
7098
+ if (!existsSync28(servicesDir)) {
6982
7099
  throw new Error(
6983
7100
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6984
7101
  );
6985
7102
  }
6986
- const targetDir = join28(servicesDir, name);
6987
- if (!existsSync27(targetDir)) {
7103
+ const targetDir = join29(servicesDir, name);
7104
+ if (!existsSync28(targetDir)) {
6988
7105
  throw new Error(
6989
7106
  `Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
6990
7107
  );
@@ -6998,7 +7115,7 @@ async function runPluginUpgrade(target, options, deps) {
6998
7115
  `Plugin declares required_core_version '${entry.required_core_version}'. The CLI cannot verify this against your deployment \u2014 the Core API exposes no version endpoint and services/api/pyproject.toml's version is a static placeholder, not a real release marker. Confirm compatibility yourself before deploying.`
6999
7116
  );
7000
7117
  }
7001
- const modulesDir = join28(options.cwd, "modules", "plugins", entry.name);
7118
+ const modulesDir = join29(options.cwd, "modules", "plugins", entry.name);
7002
7119
  if (options.dryRun) {
7003
7120
  printDryRun6(entry, currentVersion);
7004
7121
  return;
@@ -7031,11 +7148,11 @@ async function runPluginUpgrade(target, options, deps) {
7031
7148
  cpSync4(tmpDir, targetDir, { recursive: true });
7032
7149
  log.success(`Upgraded plugin source at services/${entry.name}/`);
7033
7150
  const stagePaths = [`services/${entry.name}`];
7034
- if (existsSync27(modulesDir)) {
7151
+ if (existsSync28(modulesDir)) {
7035
7152
  rmSync9(modulesDir, { recursive: true, force: true });
7036
7153
  }
7037
- const tfSourceDir = join28(targetDir, "terraform");
7038
- if (existsSync27(tfSourceDir)) {
7154
+ const tfSourceDir = join29(targetDir, "terraform");
7155
+ if (existsSync28(tfSourceDir)) {
7039
7156
  mkdirSync10(modulesDir, { recursive: true });
7040
7157
  cpSync4(tfSourceDir, modulesDir, { recursive: true });
7041
7158
  stagePaths.push(`modules/plugins/${entry.name}`);
@@ -7069,10 +7186,10 @@ async function runPluginUpgrade(target, options, deps) {
7069
7186
  }
7070
7187
  }
7071
7188
  function readInstalledVersion2(targetDir) {
7072
- const manifestPath = join28(targetDir, "biffo.plugin.json");
7073
- if (!existsSync27(manifestPath)) return void 0;
7189
+ const manifestPath = join29(targetDir, "biffo.plugin.json");
7190
+ if (!existsSync28(manifestPath)) return void 0;
7074
7191
  try {
7075
- return validateManifest(JSON.parse(readFileSync20(manifestPath, "utf8"))).version;
7192
+ return validateManifest(JSON.parse(readFileSync21(manifestPath, "utf8"))).version;
7076
7193
  } catch {
7077
7194
  return void 0;
7078
7195
  }
@@ -7122,90 +7239,6 @@ import { Command as Command22 } from "commander";
7122
7239
 
7123
7240
  // src/scripts/check-core-ownership.ts
7124
7241
  import { execa as execa5 } from "execa";
7125
-
7126
- // src/lib/core-ownership-guard.ts
7127
- import { existsSync as existsSync28, readFileSync as readFileSync21 } from "fs";
7128
- import { join as join29 } from "path";
7129
- import { z as z7 } from "zod";
7130
- var DIVERGENCE_FILE = "biffo.divergence.json";
7131
- var DivergenceEntrySchema = z7.object({
7132
- prefix: z7.string().min(1),
7133
- reason: z7.string().min(1),
7134
- upstream: z7.string().min(1)
7135
- });
7136
- var DivergenceConfigSchema = z7.object({
7137
- note: z7.string().optional(),
7138
- warnOnly: z7.array(DivergenceEntrySchema).default([])
7139
- });
7140
- function readDivergenceConfig(repoRoot) {
7141
- const path = join29(repoRoot, DIVERGENCE_FILE);
7142
- if (!existsSync28(path)) return { warnOnly: [] };
7143
- let raw;
7144
- try {
7145
- raw = JSON.parse(readFileSync21(path, "utf8"));
7146
- } catch (err) {
7147
- throw new Error(`${DIVERGENCE_FILE} is not valid JSON: ${err.message}`);
7148
- }
7149
- const parsed = DivergenceConfigSchema.safeParse(raw);
7150
- if (!parsed.success) {
7151
- const issues = parsed.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
7152
- throw new Error(`${DIVERGENCE_FILE} is invalid:
7153
- ${issues}`);
7154
- }
7155
- return parsed.data;
7156
- }
7157
- function parseDivergenceTrailer(commitMessage) {
7158
- const body = commitMessage.split("\n").filter((line) => !line.startsWith("#")).join("\n");
7159
- const match = /^Core-Divergence:[ \t]*(\S.*?)[ \t]*$/m.exec(body);
7160
- return match?.[1] ?? null;
7161
- }
7162
- function resolveBranch(env, gitBranch) {
7163
- return (env["GITHUB_HEAD_REF"] || env["GITHUB_REF_NAME"] || gitBranch).trim();
7164
- }
7165
- function parseNameStatus(stdout) {
7166
- const changed = [];
7167
- const deleted = [];
7168
- for (const line of stdout.split("\n")) {
7169
- const parts = line.split(" ").filter(Boolean);
7170
- const status = parts[0];
7171
- const path = parts[parts.length - 1];
7172
- if (!status || !path || parts.length < 2) continue;
7173
- changed.push(path);
7174
- if (status.startsWith("D")) deleted.push(path);
7175
- }
7176
- return { changed, deleted };
7177
- }
7178
- function checkCoreOwnership({
7179
- changedFiles,
7180
- manifest,
7181
- isInstance,
7182
- branch = "",
7183
- commitMessage = "",
7184
- warnOnly = []
7185
- }) {
7186
- const empty = { blocked: [], warned: [], divergenceReason: null };
7187
- if (!isInstance) return { skipped: "template", ...empty };
7188
- if (branch.startsWith(UPGRADE_BRANCH_PREFIX)) return { skipped: "upgrade-branch", ...empty };
7189
- const templateOwned = changedFiles.filter((f) => isTemplateOwned(f, manifest));
7190
- const acknowledged = (path) => warnOnly.filter((entry) => path.startsWith(entry.prefix)).reduce(
7191
- (best, entry) => !best || entry.prefix.length > best.prefix.length ? entry : best,
7192
- void 0
7193
- );
7194
- const warned = [];
7195
- const offending = [];
7196
- for (const path of templateOwned) {
7197
- const entry = acknowledged(path);
7198
- if (entry) warned.push({ path, entry });
7199
- else offending.push(path);
7200
- }
7201
- const divergenceReason = parseDivergenceTrailer(commitMessage);
7202
- if (offending.length > 0 && divergenceReason !== null) {
7203
- return { skipped: "divergence-trailer", blocked: [], warned, divergenceReason };
7204
- }
7205
- return { skipped: null, blocked: offending, warned, divergenceReason: null };
7206
- }
7207
-
7208
- // src/scripts/check-core-ownership.ts
7209
7242
  var BOLD = "\x1B[1m";
7210
7243
  var DIM = "\x1B[2m";
7211
7244
  var RED = "\x1B[31m";