@biffo/cli 0.74.0 → 0.75.0

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 +516 -438
  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,137 @@ 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 parseTrailer(commitMessage, key) {
1632
+ const body = commitMessage.split("\n").filter((line) => !line.startsWith("#")).join("\n");
1633
+ const match = new RegExp(String.raw`^Core-${key}:[ \t]*(\S.*?)[ \t]*$`, "m").exec(body);
1634
+ return match?.[1] ?? null;
1635
+ }
1636
+ function parseDivergenceTrailer(commitMessage) {
1637
+ return parseTrailer(commitMessage, "Divergence");
1638
+ }
1639
+ function parseConvergenceTrailer(commitMessage) {
1640
+ return parseTrailer(commitMessage, "Convergence");
1641
+ }
1642
+ function resolveBranch(env, gitBranch) {
1643
+ return (env["GITHUB_HEAD_REF"] || env["GITHUB_REF_NAME"] || gitBranch).trim();
1644
+ }
1645
+ function parseNameStatus(stdout) {
1646
+ const changed = [];
1647
+ const deleted = [];
1648
+ for (const line of stdout.split("\n")) {
1649
+ const parts = line.split(" ").filter(Boolean);
1650
+ const status = parts[0];
1651
+ const path = parts[parts.length - 1];
1652
+ if (!status || !path || parts.length < 2) continue;
1653
+ changed.push(path);
1654
+ if (status.startsWith("D")) deleted.push(path);
1655
+ }
1656
+ return { changed, deleted };
1657
+ }
1658
+ function checkCoreOwnership({
1659
+ changedFiles,
1660
+ manifest,
1661
+ isInstance,
1662
+ branch = "",
1663
+ commitMessage = "",
1664
+ warnOnly = []
1665
+ }) {
1666
+ const empty = { blocked: [], warned: [], divergenceReason: null, convergenceReason: null };
1667
+ if (!isInstance) return { skipped: "template", ...empty };
1668
+ if (branch.startsWith(UPGRADE_BRANCH_PREFIX)) return { skipped: "upgrade-branch", ...empty };
1669
+ const templateOwned = changedFiles.filter((f) => isTemplateOwned(f, manifest));
1670
+ const acknowledged = (path) => warnOnly.filter((entry) => path.startsWith(entry.prefix)).reduce(
1671
+ (best, entry) => !best || entry.prefix.length > best.prefix.length ? entry : best,
1672
+ void 0
1673
+ );
1674
+ const warned = [];
1675
+ const offending = [];
1676
+ for (const path of templateOwned) {
1677
+ const entry = acknowledged(path);
1678
+ if (entry) warned.push({ path, entry });
1679
+ else offending.push(path);
1680
+ }
1681
+ const divergenceReason = parseDivergenceTrailer(commitMessage);
1682
+ const convergenceReason = parseConvergenceTrailer(commitMessage);
1683
+ if (offending.length > 0) {
1684
+ if (divergenceReason !== null) {
1685
+ return {
1686
+ skipped: "divergence-trailer",
1687
+ blocked: [],
1688
+ warned,
1689
+ divergenceReason,
1690
+ convergenceReason: null
1691
+ };
1692
+ }
1693
+ if (convergenceReason !== null) {
1694
+ return {
1695
+ skipped: "convergence-trailer",
1696
+ blocked: [],
1697
+ warned,
1698
+ divergenceReason: null,
1699
+ convergenceReason
1700
+ };
1701
+ }
1702
+ }
1703
+ return {
1704
+ skipped: null,
1705
+ blocked: offending,
1706
+ warned,
1707
+ divergenceReason: null,
1708
+ convergenceReason: null
1709
+ };
1710
+ }
1711
+
1712
+ // src/lib/core-upgrade.ts
1599
1713
  var gitMergeFile = async (base, ours, theirs) => {
1600
- const dir = mkdtempSync2(join6(tmpdir2(), "biffo-merge-"));
1714
+ const dir = mkdtempSync2(join7(tmpdir2(), "biffo-merge-"));
1601
1715
  try {
1602
- const b = join6(dir, "base");
1603
- const o = join6(dir, "ours");
1604
- const t = join6(dir, "theirs");
1716
+ const b = join7(dir, "base");
1717
+ const o = join7(dir, "ours");
1718
+ const t = join7(dir, "theirs");
1605
1719
  writeFileSync3(b, base);
1606
1720
  writeFileSync3(o, ours);
1607
1721
  writeFileSync3(t, theirs);
@@ -1618,7 +1732,7 @@ var gitMergeFile = async (base, ours, theirs) => {
1618
1732
  }
1619
1733
  };
1620
1734
  function read(root, rel) {
1621
- return readFileSync4(join6(root, rel), "utf8");
1735
+ return readFileSync5(join7(root, rel), "utf8");
1622
1736
  }
1623
1737
  var EMPTY_SUMMARY = () => ({
1624
1738
  unchanged: 0,
@@ -1628,6 +1742,7 @@ var EMPTY_SUMMARY = () => ({
1628
1742
  conflict: 0,
1629
1743
  added: 0,
1630
1744
  "add-conflict": 0,
1745
+ restored: 0,
1631
1746
  removed: 0,
1632
1747
  "remove-conflict": 0
1633
1748
  });
@@ -1636,18 +1751,32 @@ async function planCoreUpgrade(options) {
1636
1751
  const base = new Set(listTemplateOwnedFiles(options.baseDir, options.manifest));
1637
1752
  const ours = new Set(listTemplateOwnedFiles(options.oursDir, options.manifest));
1638
1753
  const theirs = new Set(listTemplateOwnedFiles(options.theirsDir, options.manifest));
1754
+ const divergentPrefixes = readDivergenceConfig(options.oursDir).warnOnly.map((e) => e.prefix);
1755
+ const isDeclaredDivergent = (path) => divergentPrefixes.some((prefix) => path.startsWith(prefix));
1639
1756
  const paths = [.../* @__PURE__ */ new Set([...base, ...ours, ...theirs])].sort();
1640
1757
  const entries = [];
1758
+ const divergenceSkips = [];
1641
1759
  for (const path of paths) {
1642
- entries.push(await classify(path, base, ours, theirs, options, mergeFile));
1760
+ entries.push(
1761
+ await classify(
1762
+ path,
1763
+ base,
1764
+ ours,
1765
+ theirs,
1766
+ options,
1767
+ mergeFile,
1768
+ isDeclaredDivergent,
1769
+ (p) => divergenceSkips.push(p)
1770
+ )
1771
+ );
1643
1772
  }
1644
1773
  const summary = EMPTY_SUMMARY();
1645
1774
  for (const e of entries) summary[e.status]++;
1646
1775
  const changes = entries.filter((e) => e.status !== "unchanged" && e.status !== "keep-ours");
1647
1776
  const conflicts = entries.filter((e) => e.conflicted);
1648
- return { entries, changes, conflicts, summary };
1777
+ return { entries, changes, conflicts, summary, divergenceSkips };
1649
1778
  }
1650
- async function classify(path, base, ours, theirs, opts, mergeFile) {
1779
+ async function classify(path, base, ours, theirs, opts, mergeFile, isDeclaredDivergent, noteDivergenceSkip) {
1651
1780
  const inBase = base.has(path);
1652
1781
  const inOurs = ours.has(path);
1653
1782
  const inTheirs = theirs.has(path);
@@ -1671,8 +1800,11 @@ async function classify(path, base, ours, theirs, opts, mergeFile) {
1671
1800
  const baseContent = read(opts.baseDir, path);
1672
1801
  const theirsContent = read(opts.theirsDir, path);
1673
1802
  if (!inOurs) {
1674
- if (baseContent === theirsContent) return { path, status: "removed", conflicted: false };
1675
- return { path, status: "added", conflicted: false, content: theirsContent };
1803
+ if (isDeclaredDivergent(path)) {
1804
+ noteDivergenceSkip(path);
1805
+ return { path, status: "removed", conflicted: false };
1806
+ }
1807
+ return { path, status: "restored", conflicted: false, content: theirsContent };
1676
1808
  }
1677
1809
  const oursContent = read(opts.oursDir, path);
1678
1810
  const oursChanged = oursContent !== baseContent;
@@ -1693,9 +1825,9 @@ function applyUpgradePlan(instanceDir, plan, theirsDir) {
1693
1825
  const written = [];
1694
1826
  const deleted = [];
1695
1827
  for (const e of plan.entries) {
1696
- const abs = join6(instanceDir, e.path);
1828
+ const abs = join7(instanceDir, e.path);
1697
1829
  if (e.status === "removed") {
1698
- if (existsSync6(abs)) {
1830
+ if (existsSync7(abs)) {
1699
1831
  rmSync2(abs);
1700
1832
  deleted.push(e.path);
1701
1833
  }
@@ -1705,8 +1837,8 @@ function applyUpgradePlan(instanceDir, plan, theirsDir) {
1705
1837
  mkdirSync2(dirname4(abs), { recursive: true });
1706
1838
  writeFileSync3(abs, e.content);
1707
1839
  if (theirsDir !== void 0) {
1708
- const source = join6(theirsDir, e.path);
1709
- if (existsSync6(source) && (statSync(source).mode & 73) !== 0) {
1840
+ const source = join7(theirsDir, e.path);
1841
+ if (existsSync7(source) && (statSync(source).mode & 73) !== 0) {
1710
1842
  chmodSync(abs, 493);
1711
1843
  }
1712
1844
  }
@@ -1731,7 +1863,7 @@ function upgradeBranchName(from, to) {
1731
1863
  import { execFileSync as execFileSync2 } from "child_process";
1732
1864
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "fs";
1733
1865
  import { tmpdir as tmpdir3 } from "os";
1734
- import { join as join7 } from "path";
1866
+ import { join as join8 } from "path";
1735
1867
  function coreTag(version) {
1736
1868
  parseCoreVersion(version);
1737
1869
  return `core-v${version}`;
@@ -1770,8 +1902,8 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1770
1902
  `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
1903
  );
1772
1904
  }
1773
- const dir = mkdtempSync3(join7(tmpdir3(), `biffo-core-${version}-`));
1774
- const tarball = join7(dir, ".tree.tar");
1905
+ const dir = mkdtempSync3(join8(tmpdir3(), `biffo-core-${version}-`));
1906
+ const tarball = join8(dir, ".tree.tar");
1775
1907
  try {
1776
1908
  git(["-C", repo, "archive", "--format=tar", "-o", tarball, tag]);
1777
1909
  execFileSync2("tar", ["-x", "-f", tarball, "-C", dir]);
@@ -1784,8 +1916,8 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1784
1916
  }
1785
1917
 
1786
1918
  // src/lib/breaking-changes.ts
1787
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1788
- import { join as join8 } from "path";
1919
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1920
+ import { join as join9 } from "path";
1789
1921
  var UPGRADE_GUIDE_PATH = "docs/guides/core-upgrade.md";
1790
1922
  var SECTION_HEADING = "## Breaking changes by version";
1791
1923
  var ENTRY_HEADING = /^###\s+(\d+\.\d+\.\d+)\s*[—-]\s*(.+?)\s*$/;
@@ -1810,9 +1942,9 @@ function parseBreakingChanges(guide) {
1810
1942
  return entries;
1811
1943
  }
1812
1944
  function readBreakingChanges(templateRoot) {
1813
- const path = join8(templateRoot, UPGRADE_GUIDE_PATH);
1814
- if (!existsSync7(path)) return [];
1815
- return parseBreakingChanges(readFileSync5(path, "utf8"));
1945
+ const path = join9(templateRoot, UPGRADE_GUIDE_PATH);
1946
+ if (!existsSync8(path)) return [];
1947
+ return parseBreakingChanges(readFileSync6(path, "utf8"));
1816
1948
  }
1817
1949
  function breakingChangesBetween(from, to, entries) {
1818
1950
  parseCoreVersion(from);
@@ -1829,8 +1961,8 @@ var GLOBAL_DISPATCH_WORKFLOW_PATHS = [
1829
1961
  ];
1830
1962
 
1831
1963
  // 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";
1964
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1965
+ import { join as join10 } from "path";
1834
1966
  var TEMPLATE_MODULE_DIR = "_template";
1835
1967
  var DEFAULT_PLUGIN_HANDLER = "src.lambda.main.handler";
1836
1968
  var GENERATED_TF_FILE = "plugins.generated.tf";
@@ -1848,7 +1980,7 @@ function standardArguments(pluginName, handler) {
1848
1980
  ];
1849
1981
  }
1850
1982
  function listPluginModules(cwd) {
1851
- const dir = join9(cwd, "modules", "plugins");
1983
+ const dir = join10(cwd, "modules", "plugins");
1852
1984
  let entries;
1853
1985
  try {
1854
1986
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1860,7 +1992,7 @@ function listPluginModules(cwd) {
1860
1992
  var FIRST_PARTY_TERRAFORM = (name) => `../../../services/_plugins/${name}/terraform`;
1861
1993
  var THIRD_PARTY_TERRAFORM = (name) => `../../../modules/plugins/${name}`;
1862
1994
  function isFirstPartyPlugin(cwd, name) {
1863
- return existsSync8(join9(cwd, "services", "_plugins", name, "terraform", "main.tf"));
1995
+ return existsSync9(join10(cwd, "services", "_plugins", name, "terraform", "main.tf"));
1864
1996
  }
1865
1997
  function pluginModuleSource(cwd, name) {
1866
1998
  return isFirstPartyPlugin(cwd, name) ? FIRST_PARTY_TERRAFORM(name) : THIRD_PARTY_TERRAFORM(name);
@@ -1871,7 +2003,7 @@ function listWireablePlugins(cwd) {
1871
2003
  return [.../* @__PURE__ */ new Set([...copied, ...firstParty])].sort();
1872
2004
  }
1873
2005
  function firstPartyPluginNames(cwd) {
1874
- const dir = join9(cwd, "services", "_plugins");
2006
+ const dir = join10(cwd, "services", "_plugins");
1875
2007
  let entries;
1876
2008
  try {
1877
2009
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1885,7 +2017,7 @@ function staleFirstPartyCopies(cwd) {
1885
2017
  return firstPartyPluginNames(cwd).filter((name) => copied.has(name));
1886
2018
  }
1887
2019
  function listEnvironments(cwd) {
1888
- const dir = join9(cwd, "infra", "environments");
2020
+ const dir = join10(cwd, "infra", "environments");
1889
2021
  let entries;
1890
2022
  try {
1891
2023
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1893,12 +2025,12 @@ function listEnvironments(cwd) {
1893
2025
  return [];
1894
2026
  }
1895
2027
  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");
2028
+ if (!e.isDirectory() || !existsSync9(join10(dir, e.name, "main.tf"))) return false;
2029
+ return declaredVariables(join10(dir, e.name)).has("enabled_plugins");
1898
2030
  }).map((e) => e.name).sort();
1899
2031
  }
1900
2032
  function listUnwirableEnvironments(cwd) {
1901
- const dir = join9(cwd, "infra", "environments");
2033
+ const dir = join10(cwd, "infra", "environments");
1902
2034
  let entries;
1903
2035
  try {
1904
2036
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1906,7 +2038,7 @@ function listUnwirableEnvironments(cwd) {
1906
2038
  return [];
1907
2039
  }
1908
2040
  return entries.filter(
1909
- (e) => e.isDirectory() && existsSync8(join9(dir, e.name, "main.tf")) && !declaredVariables(join9(dir, e.name)).has("enabled_plugins")
2041
+ (e) => e.isDirectory() && existsSync9(join10(dir, e.name, "main.tf")) && !declaredVariables(join10(dir, e.name)).has("enabled_plugins")
1910
2042
  ).map((e) => e.name).sort();
1911
2043
  }
1912
2044
  function declaredVariables(moduleDir) {
@@ -1921,7 +2053,7 @@ function declaredVariables(moduleDir) {
1921
2053
  if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
1922
2054
  let contents;
1923
2055
  try {
1924
- contents = readFileSync6(join9(moduleDir, entry.name), "utf8");
2056
+ contents = readFileSync7(join10(moduleDir, entry.name), "utf8");
1925
2057
  } catch {
1926
2058
  continue;
1927
2059
  }
@@ -1998,7 +2130,7 @@ function syncPluginTerraform(cwd) {
1998
2130
  const changedPaths = [];
1999
2131
  const rendered = plugins.map((name) => {
2000
2132
  const firstParty = isFirstPartyPlugin(cwd, name);
2001
- const moduleDir = firstParty ? join9(cwd, "services", "_plugins", name, "terraform") : join9(cwd, "modules", "plugins", name);
2133
+ const moduleDir = firstParty ? join10(cwd, "services", "_plugins", name, "terraform") : join10(cwd, "modules", "plugins", name);
2002
2134
  return {
2003
2135
  name,
2004
2136
  declaredVariables: declaredVariables(moduleDir),
@@ -2006,16 +2138,16 @@ function syncPluginTerraform(cwd) {
2006
2138
  };
2007
2139
  });
2008
2140
  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);
2141
+ const envDir = join10(cwd, "infra", "environments", env);
2142
+ const tfPath = join10(envDir, GENERATED_TF_FILE);
2143
+ const tfvarsPath = join10(envDir, GENERATED_TFVARS_FILE);
2012
2144
  const relBase = `infra/environments/${env}`;
2013
2145
  if (plugins.length === 0) {
2014
2146
  for (const [abs, rel] of [
2015
2147
  [tfPath, `${relBase}/${GENERATED_TF_FILE}`],
2016
2148
  [tfvarsPath, `${relBase}/${GENERATED_TFVARS_FILE}`]
2017
2149
  ]) {
2018
- if (existsSync8(abs)) {
2150
+ if (existsSync9(abs)) {
2019
2151
  rmSync4(abs);
2020
2152
  changedPaths.push(rel);
2021
2153
  }
@@ -2031,8 +2163,8 @@ function syncPluginTerraform(cwd) {
2031
2163
  }
2032
2164
 
2033
2165
  // src/lib/lockfile-refresh.ts
2034
- import { existsSync as existsSync9 } from "fs";
2035
- import { join as join10 } from "path";
2166
+ import { existsSync as existsSync10 } from "fs";
2167
+ import { join as join11 } from "path";
2036
2168
  var LOCKFILE_TRIGGERS = [
2037
2169
  {
2038
2170
  manifest: "package.json",
@@ -2055,7 +2187,7 @@ function lockfilesNeedingRefresh(changedPaths, instanceDir, triggers = LOCKFILE_
2055
2187
  const locked = changedPaths.filter((p) => !isForeignManifest(p));
2056
2188
  return triggers.filter((t) => {
2057
2189
  const touched = locked.some((p) => p === t.manifest || p.endsWith(`/${t.manifest}`));
2058
- return touched && existsSync9(join10(instanceDir, t.lockfile));
2190
+ return touched && existsSync10(join11(instanceDir, t.lockfile));
2059
2191
  });
2060
2192
  }
2061
2193
  async function refreshLockfiles(instanceDir, triggers, run) {
@@ -2242,6 +2374,13 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
2242
2374
  `
2243
2375
  ${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
2376
  );
2377
+ if (plan.divergenceSkips && plan.divergenceSkips.length > 0) {
2378
+ console.log(
2379
+ chalk4.dim(
2380
+ ` ${plan.divergenceSkips.length} deleted template-owned file(s) left absent \u2014 declared divergent in biffo.divergence.json.`
2381
+ )
2382
+ );
2383
+ }
2245
2384
  if (!options.apply) {
2246
2385
  if (plan.conflicts.length > 0) {
2247
2386
  log.warn("Some core files changed on both sides and need manual resolution.");
@@ -2286,7 +2425,7 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
2286
2425
  const carried = applyMigrationCarry(options.cwd, migrations);
2287
2426
  writeInstanceCoreVersion(options.cwd, toVersion);
2288
2427
  const cleanedCoreVersion = coreVersionCleanup?.action === "delete";
2289
- if (cleanedCoreVersion && existsSync10(coreVersionCleanup.path)) {
2428
+ if (cleanedCoreVersion && existsSync11(coreVersionCleanup.path)) {
2290
2429
  rmSync5(coreVersionCleanup.path);
2291
2430
  log.info(
2292
2431
  `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 +2494,15 @@ function buildPrBody(from, to, plan, migrations, base = GLOBAL_DISPATCH_REF, loc
2355
2494
  `- merged: ${plan.summary.merged}`,
2356
2495
  `- take-theirs: ${plan.summary["take-theirs"]}`,
2357
2496
  `- added: ${plan.summary.added}`,
2497
+ `- restored: ${plan.summary.restored}`,
2358
2498
  `- removed: ${plan.summary.removed}`
2359
2499
  );
2500
+ if (plan.divergenceSkips && plan.divergenceSkips.length > 0) {
2501
+ lines.push(
2502
+ "",
2503
+ `> ${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).`
2504
+ );
2505
+ }
2360
2506
  if (migrations.entries.length > 0) {
2361
2507
  lines.push(
2362
2508
  "",
@@ -2433,6 +2579,7 @@ var STATUS_COLOR = {
2433
2579
  merged: chalk4.yellow,
2434
2580
  "take-theirs": chalk4.green,
2435
2581
  added: chalk4.green,
2582
+ restored: chalk4.green,
2436
2583
  removed: chalk4.red,
2437
2584
  "keep-ours": chalk4.dim
2438
2585
  };
@@ -2525,8 +2672,8 @@ function printBreakingChanges(breaking, applying) {
2525
2672
  }
2526
2673
  function versionOfCheckout(dir, explicit) {
2527
2674
  if (explicit) return explicit;
2528
- const file = join11(dir, CORE_VERSION_FILE);
2529
- if (existsSync10(file)) return readCoreVersionFile(file);
2675
+ const file = join12(dir, CORE_VERSION_FILE);
2676
+ if (existsSync11(file)) return readCoreVersionFile(file);
2530
2677
  throw new Error(
2531
2678
  `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
2679
  );
@@ -2534,8 +2681,8 @@ function versionOfCheckout(dir, explicit) {
2534
2681
  function latestCoreVersion(repo) {
2535
2682
  const fromTags = latestCoreVersionFromTags(repo);
2536
2683
  if (fromTags) return fromTags;
2537
- const file = join11(repo, CORE_VERSION_FILE);
2538
- if (existsSync10(file)) return readCoreVersionFile(file);
2684
+ const file = join12(repo, CORE_VERSION_FILE);
2685
+ if (existsSync11(file)) return readCoreVersionFile(file);
2539
2686
  throw new Error(
2540
2687
  `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
2688
  );
@@ -2553,7 +2700,7 @@ coreCommand.addCommand(coreUpgradeCommand);
2553
2700
  import { Command as Command8 } from "commander";
2554
2701
 
2555
2702
  // src/commands/data-apply.ts
2556
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2703
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
2557
2704
  import { resolve as resolve4 } from "path";
2558
2705
  import chalk5 from "chalk";
2559
2706
  import { Command as Command5 } from "commander";
@@ -2913,62 +3060,62 @@ var AwsAdapter = class {
2913
3060
  };
2914
3061
 
2915
3062
  // 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()
3063
+ import { z as z4 } from "zod";
3064
+ var AwsConfigSchema = z4.object({
3065
+ account_id: z4.string().regex(/^\d{12}$/, "AWS account ID must be 12 digits").describe("12-digit AWS account ID"),
3066
+ region: z4.string().default("us-east-1"),
3067
+ profile: z4.string().optional(),
3068
+ oidc_role_arn: z4.string().regex(/^arn:aws:iam::\d{12}:role\/.+/, "Must be a valid IAM role ARN").optional(),
3069
+ tf_state_bucket: z4.string().optional()
2923
3070
  });
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)")
3071
+ var GitHubConfigSchema = z4.object({
3072
+ org: z4.string().min(1).describe("GitHub organisation or username"),
3073
+ repo: z4.string().min(1).describe("Repository name (will be created)")
2927
3074
  });
2928
- var SourceControlConfigSchema = z3.discriminatedUnion("provider", [
2929
- z3.object({ provider: z3.literal("github"), config: GitHubConfigSchema })
3075
+ var SourceControlConfigSchema = z4.discriminatedUnion("provider", [
3076
+ z4.object({ provider: z4.literal("github"), config: GitHubConfigSchema })
2930
3077
  ]);
2931
- var CloudConfigSchema = z3.discriminatedUnion("provider", [
2932
- z3.object({ provider: z3.literal("aws"), config: AwsConfigSchema })
3078
+ var CloudConfigSchema = z4.discriminatedUnion("provider", [
3079
+ z4.object({ provider: z4.literal("aws"), config: AwsConfigSchema })
2933
3080
  ]);
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")
3081
+ var ModulesSchema = z4.object({
3082
+ auth: z4.enum(["cognito"]).default("cognito"),
3083
+ events: z4.enum(["eventbridge"]).default("eventbridge"),
3084
+ storage: z4.enum(["s3"]).default("s3"),
3085
+ database: z4.enum(["postgresql"]).default("postgresql"),
3086
+ compute: z4.enum(["lambda"]).default("lambda"),
3087
+ cdn: z4.enum(["cloudfront"]).default("cloudfront")
2941
3088
  });
2942
- var DnsSchema = z3.object({
2943
- mode: z3.enum(["managed-route53", "external", "none"]).default("managed-route53"),
2944
- domain: z3.string().min(1).optional()
3089
+ var DnsSchema = z4.object({
3090
+ mode: z4.enum(["managed-route53", "external", "none"]).default("managed-route53"),
3091
+ domain: z4.string().min(1).optional()
2945
3092
  });
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(""),
3093
+ var BiffoConfigSchema = z4.object({
3094
+ $schema: z4.string().optional(),
3095
+ project: z4.object({
3096
+ name: z4.string().min(1).regex(/^[a-z0-9-]+$/, "Must be lowercase kebab-case"),
3097
+ description: z4.string().default(""),
2951
3098
  // 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")
3099
+ domain: z4.string().min(1).optional().describe("Primary domain, e.g. myapp.com")
2953
3100
  }),
2954
3101
  dns: DnsSchema.optional(),
2955
3102
  source_control: SourceControlConfigSchema,
2956
3103
  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)
3104
+ environments: z4.array(z4.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
3105
+ admin: z4.object({
3106
+ email: z4.string().email(),
3107
+ username: z4.string().min(1)
2961
3108
  }),
2962
- database: z3.object({
2963
- schema_path: z3.string().nullable().default(null),
2964
- migrations_path: z3.string().default("services/api/migrations")
3109
+ database: z4.object({
3110
+ schema_path: z4.string().nullable().default(null),
3111
+ migrations_path: z4.string().default("services/api/migrations")
2965
3112
  }).default({}),
2966
3113
  modules: ModulesSchema.default({})
2967
3114
  }).superRefine((config, ctx) => {
2968
3115
  const dns = resolveDnsConfig(config);
2969
3116
  if (dns.mode !== "none" && !dns.domain) {
2970
3117
  ctx.addIssue({
2971
- code: z3.ZodIssueCode.custom,
3118
+ code: z4.ZodIssueCode.custom,
2972
3119
  path: ["dns", "domain"],
2973
3120
  message: 'DNS domain is required unless dns.mode is "none"'
2974
3121
  });
@@ -3004,16 +3151,16 @@ function isTemplatePlaceholderConfig(raw) {
3004
3151
 
3005
3152
  // src/lib/session.ts
3006
3153
  import {
3007
- existsSync as existsSync11,
3154
+ existsSync as existsSync12,
3008
3155
  mkdirSync as mkdirSync4,
3009
3156
  readdirSync as readdirSync4,
3010
- readFileSync as readFileSync7,
3157
+ readFileSync as readFileSync8,
3011
3158
  rmSync as rmSync6,
3012
3159
  statSync as statSync2,
3013
3160
  writeFileSync as writeFileSync5
3014
3161
  } from "fs";
3015
3162
  import { homedir } from "os";
3016
- import { join as join12 } from "path";
3163
+ import { join as join13 } from "path";
3017
3164
  var LEGACY_STEP_ALIASES = {
3018
3165
  github_config: ["github_branches", "github_instance_files", "github_settings"]
3019
3166
  };
@@ -3022,39 +3169,39 @@ function hasCompleted(session, step) {
3022
3169
  return session.completedSteps.some((done) => LEGACY_STEP_ALIASES[done]?.includes(step) ?? false);
3023
3170
  }
3024
3171
  function sessionsDir() {
3025
- return process.env["BIFFO_SESSIONS_DIR"] ?? join12(homedir(), ".biffo", "sessions");
3172
+ return process.env["BIFFO_SESSIONS_DIR"] ?? join13(homedir(), ".biffo", "sessions");
3026
3173
  }
3027
3174
  function sessionPath(projectName) {
3028
- return join12(sessionsDir(), `${projectName}.json`);
3175
+ return join13(sessionsDir(), `${projectName}.json`);
3029
3176
  }
3030
3177
  function loadSession(projectName) {
3031
3178
  const path = sessionPath(projectName);
3032
- if (!existsSync11(path)) return null;
3179
+ if (!existsSync12(path)) return null;
3033
3180
  try {
3034
- return JSON.parse(readFileSync7(path, "utf8"));
3181
+ return JSON.parse(readFileSync8(path, "utf8"));
3035
3182
  } catch {
3036
3183
  return null;
3037
3184
  }
3038
3185
  }
3039
3186
  function findLatestSession() {
3040
3187
  const dir = sessionsDir();
3041
- if (!existsSync11(dir)) return null;
3188
+ if (!existsSync12(dir)) return null;
3042
3189
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
3043
3190
  if (files.length === 0) return null;
3044
3191
  const sorted = files.map((f) => {
3045
- const fullPath = join12(dir, f);
3046
- const mtime = existsSync11(fullPath) ? statSync2(fullPath).mtimeMs : -1;
3192
+ const fullPath = join13(dir, f);
3193
+ const mtime = existsSync12(fullPath) ? statSync2(fullPath).mtimeMs : -1;
3047
3194
  return { f, mtime };
3048
3195
  }).sort((a, b) => b.mtime - a.mtime);
3049
3196
  try {
3050
- return JSON.parse(readFileSync7(join12(dir, sorted[0].f), "utf8"));
3197
+ return JSON.parse(readFileSync8(join13(dir, sorted[0].f), "utf8"));
3051
3198
  } catch {
3052
3199
  return null;
3053
3200
  }
3054
3201
  }
3055
3202
  function saveSession(session) {
3056
3203
  const dir = sessionsDir();
3057
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
3204
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
3058
3205
  const name = session.config.project?.name ?? "unknown";
3059
3206
  const prior = loadSession(name);
3060
3207
  if (prior) {
@@ -3076,36 +3223,36 @@ function markStepComplete(session, step) {
3076
3223
  }
3077
3224
  function deleteSession(projectName) {
3078
3225
  const path = sessionPath(projectName);
3079
- if (existsSync11(path)) rmSync6(path);
3226
+ if (existsSync12(path)) rmSync6(path);
3080
3227
  }
3081
3228
  function projectsDir() {
3082
- return process.env["BIFFO_PROJECTS_DIR"] ?? join12(homedir(), ".biffo", "projects");
3229
+ return process.env["BIFFO_PROJECTS_DIR"] ?? join13(homedir(), ".biffo", "projects");
3083
3230
  }
3084
3231
  function saveProjectConfig(config) {
3085
3232
  const dir = projectsDir();
3086
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
3087
- writeFileSync5(join12(dir, `${config.project.name}.json`), JSON.stringify(config, null, 2));
3233
+ if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
3234
+ writeFileSync5(join13(dir, `${config.project.name}.json`), JSON.stringify(config, null, 2));
3088
3235
  }
3089
3236
  function loadProjectConfig(name) {
3090
- const path = join12(projectsDir(), `${name}.json`);
3091
- if (!existsSync11(path)) return null;
3237
+ const path = join13(projectsDir(), `${name}.json`);
3238
+ if (!existsSync12(path)) return null;
3092
3239
  try {
3093
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync7(path, "utf8")));
3240
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync8(path, "utf8")));
3094
3241
  return result.success ? result.data : null;
3095
3242
  } catch {
3096
3243
  return null;
3097
3244
  }
3098
3245
  }
3099
3246
  function deleteProjectConfig(name) {
3100
- const path = join12(projectsDir(), `${name}.json`);
3101
- if (existsSync11(path)) rmSync6(path);
3247
+ const path = join13(projectsDir(), `${name}.json`);
3248
+ if (existsSync12(path)) rmSync6(path);
3102
3249
  }
3103
3250
  function listProjectConfigs() {
3104
3251
  const dir = projectsDir();
3105
- if (!existsSync11(dir)) return [];
3252
+ if (!existsSync12(dir)) return [];
3106
3253
  return readdirSync4(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
3107
3254
  try {
3108
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync7(join12(dir, f), "utf8")));
3255
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync8(join13(dir, f), "utf8")));
3109
3256
  return result.success ? [result.data] : [];
3110
3257
  } catch {
3111
3258
  return [];
@@ -3174,7 +3321,7 @@ async function runDataApply(name, environment, config, aws) {
3174
3321
  }
3175
3322
  async function resolveConfig(options) {
3176
3323
  if (options.config) {
3177
- const raw = JSON.parse(readFileSync8(resolve4(options.config), "utf8"));
3324
+ const raw = JSON.parse(readFileSync9(resolve4(options.config), "utf8"));
3178
3325
  const result = BiffoConfigSchema.safeParse(raw);
3179
3326
  if (!result.success) {
3180
3327
  log.error(`Invalid config at ${options.config}:`);
@@ -3194,8 +3341,8 @@ async function resolveConfig(options) {
3194
3341
  return cfg;
3195
3342
  }
3196
3343
  const localConfigPath = resolve4(process.cwd(), "biffo.config.json");
3197
- if (existsSync12(localConfigPath)) {
3198
- const raw = JSON.parse(readFileSync8(localConfigPath, "utf8"));
3344
+ if (existsSync13(localConfigPath)) {
3345
+ const raw = JSON.parse(readFileSync9(localConfigPath, "utf8"));
3199
3346
  const result = BiffoConfigSchema.safeParse(raw);
3200
3347
  if (result.success) return result.data;
3201
3348
  if (isTemplatePlaceholderConfig(raw)) {
@@ -3241,8 +3388,8 @@ async function resolveConfig(options) {
3241
3388
 
3242
3389
  // src/commands/data-import.ts
3243
3390
  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";
3391
+ import { cpSync, existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3392
+ import { join as join14, resolve as resolve5 } from "path";
3246
3393
  import chalk6 from "chalk";
3247
3394
  import { Command as Command6 } from "commander";
3248
3395
  import inquirer2 from "inquirer";
@@ -3282,23 +3429,23 @@ async function runDataImport(name, options, deps) {
3282
3429
  `Invalid import name '${name}'. Use lowercase letters, numbers, and hyphens, starting with a letter.`
3283
3430
  );
3284
3431
  }
3285
- const servicesDir = join13(options.cwd, "services");
3286
- if (!existsSync13(servicesDir)) {
3432
+ const servicesDir = join14(options.cwd, "services");
3433
+ if (!existsSync14(servicesDir)) {
3287
3434
  throw new Error(
3288
3435
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
3289
3436
  );
3290
3437
  }
3291
- const targetDir = join13(options.cwd, "db", "imports", name);
3292
- if (existsSync13(targetDir)) {
3438
+ const targetDir = join14(options.cwd, "db", "imports", name);
3439
+ if (existsSync14(targetDir)) {
3293
3440
  throw new Error(
3294
3441
  `DDL import '${name}' is already present at db/imports/${name}/. Remove it first to re-import.`
3295
3442
  );
3296
3443
  }
3297
- const isLocalDir = existsSync13(options.source) && statSync3(options.source).isDirectory();
3444
+ const isLocalDir = existsSync14(options.source) && statSync3(options.source).isDirectory();
3298
3445
  let sourceDir;
3299
3446
  let cleanupClone = null;
3300
3447
  if (isLocalDir) {
3301
- sourceDir = options.path ? join13(options.source, options.path) : options.source;
3448
+ sourceDir = options.path ? join14(options.source, options.path) : options.source;
3302
3449
  } else {
3303
3450
  const token = options.token ?? await resolveDdlImportToken();
3304
3451
  log.info(`Cloning ${options.source}...`);
@@ -3306,10 +3453,10 @@ async function runDataImport(name, options, deps) {
3306
3453
  cleanupClone = () => {
3307
3454
  deps.git.cleanup(tmpDir);
3308
3455
  };
3309
- sourceDir = options.path ? join13(tmpDir, options.path) : tmpDir;
3456
+ sourceDir = options.path ? join14(tmpDir, options.path) : tmpDir;
3310
3457
  }
3311
3458
  try {
3312
- if (!existsSync13(sourceDir)) {
3459
+ if (!existsSync14(sourceDir)) {
3313
3460
  throw new Error(`Source directory does not exist: ${sourceDir}`);
3314
3461
  }
3315
3462
  const sqlFiles = readdirSync5(sourceDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".sql")).map((entry) => entry.name).sort();
@@ -3334,7 +3481,7 @@ async function runDataImport(name, options, deps) {
3334
3481
  }
3335
3482
  mkdirSync5(targetDir, { recursive: true });
3336
3483
  for (const file of sqlFiles) {
3337
- cpSync(join13(sourceDir, file), join13(targetDir, file));
3484
+ cpSync(join14(sourceDir, file), join14(targetDir, file));
3338
3485
  }
3339
3486
  log.success(`Imported ${String(sqlFiles.length)} .sql file(s) to db/imports/${name}/`);
3340
3487
  const commitMessage = `feat(data): import ${name} (${String(sqlFiles.length)} SQL file(s))`;
@@ -3386,8 +3533,8 @@ function printDryRun(name, sqlFiles) {
3386
3533
  }
3387
3534
 
3388
3535
  // 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";
3536
+ import { existsSync as existsSync15, readdirSync as readdirSync6 } from "fs";
3537
+ import { join as join15, resolve as resolve6 } from "path";
3391
3538
  import chalk7 from "chalk";
3392
3539
  import { Command as Command7 } from "commander";
3393
3540
  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 +3547,15 @@ var dataListCommand = new Command7("list").description("List DDL imports vendore
3400
3547
  }
3401
3548
  });
3402
3549
  async function runDataList(options) {
3403
- const importsDir = join14(options.cwd, "db", "imports");
3404
- if (!existsSync14(importsDir)) {
3550
+ const importsDir = join15(options.cwd, "db", "imports");
3551
+ if (!existsSync15(importsDir)) {
3405
3552
  console.log(chalk7.dim("\n No DDL imports in this checkout.\n"));
3406
3553
  return;
3407
3554
  }
3408
3555
  const candidates = readdirSync6(importsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
3409
3556
  const imports = [];
3410
3557
  for (const name of candidates) {
3411
- const fileCount = readdirSync6(join14(importsDir, name)).filter((f) => f.endsWith(".sql")).length;
3558
+ const fileCount = readdirSync6(join15(importsDir, name)).filter((f) => f.endsWith(".sql")).length;
3412
3559
  if (fileCount > 0) imports.push({ name, fileCount });
3413
3560
  }
3414
3561
  if (imports.length === 0) {
@@ -3438,7 +3585,7 @@ dataCommand.addCommand(dataListCommand);
3438
3585
 
3439
3586
  // src/commands/deploy.ts
3440
3587
  import { execSync as execSync4 } from "child_process";
3441
- import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
3588
+ import { existsSync as existsSync16, readFileSync as readFileSync10 } from "fs";
3442
3589
  import { resolve as resolve7 } from "path";
3443
3590
  import chalk8 from "chalk";
3444
3591
  import { Command as Command9 } from "commander";
@@ -3792,7 +3939,7 @@ var deployCommand = new Command9("deploy").description("Deploy infrastructure an
3792
3939
  );
3793
3940
  async function resolveConfig2(options) {
3794
3941
  if (options.config) {
3795
- const raw = JSON.parse(readFileSync9(resolve7(options.config), "utf8"));
3942
+ const raw = JSON.parse(readFileSync10(resolve7(options.config), "utf8"));
3796
3943
  const result = BiffoConfigSchema.safeParse(raw);
3797
3944
  if (!result.success) {
3798
3945
  log.error(`Invalid config at ${options.config}:`);
@@ -3812,8 +3959,8 @@ async function resolveConfig2(options) {
3812
3959
  return cfg;
3813
3960
  }
3814
3961
  const localConfigPath = resolve7(process.cwd(), "biffo.config.json");
3815
- if (existsSync15(localConfigPath)) {
3816
- const raw = JSON.parse(readFileSync9(localConfigPath, "utf8"));
3962
+ if (existsSync16(localConfigPath)) {
3963
+ const raw = JSON.parse(readFileSync10(localConfigPath, "utf8"));
3817
3964
  const result = BiffoConfigSchema.safeParse(raw);
3818
3965
  if (result.success) return result.data;
3819
3966
  if (isTemplatePlaceholderConfig(raw)) {
@@ -4197,7 +4344,7 @@ function resolveGithubToken() {
4197
4344
 
4198
4345
  // src/commands/destroy.ts
4199
4346
  import { execSync as execSync5 } from "child_process";
4200
- import { readFileSync as readFileSync10 } from "fs";
4347
+ import { readFileSync as readFileSync11 } from "fs";
4201
4348
  import { resolve as resolve8 } from "path";
4202
4349
  import chalk9 from "chalk";
4203
4350
  import { Command as Command10 } from "commander";
@@ -4287,7 +4434,7 @@ var destroyCommand = new Command10("destroy").description("Destroy infrastructur
4287
4434
  });
4288
4435
  async function resolveConfig3(options) {
4289
4436
  if (options.config) {
4290
- const raw = JSON.parse(readFileSync10(resolve8(options.config), "utf8"));
4437
+ const raw = JSON.parse(readFileSync11(resolve8(options.config), "utf8"));
4291
4438
  const result = BiffoConfigSchema.safeParse(raw);
4292
4439
  if (!result.success) {
4293
4440
  log.error(`Invalid config at ${options.config}:`);
@@ -4307,7 +4454,7 @@ async function resolveConfig3(options) {
4307
4454
  return cfg;
4308
4455
  }
4309
4456
  try {
4310
- const raw = JSON.parse(readFileSync10(resolve8(process.cwd(), "biffo.config.json"), "utf8"));
4457
+ const raw = JSON.parse(readFileSync11(resolve8(process.cwd(), "biffo.config.json"), "utf8"));
4311
4458
  const result = BiffoConfigSchema.safeParse(raw);
4312
4459
  if (result.success) return result.data;
4313
4460
  } catch {
@@ -4357,15 +4504,15 @@ function resolveGithubToken2() {
4357
4504
  }
4358
4505
 
4359
4506
  // src/commands/init.ts
4360
- import { readFileSync as readFileSync14 } from "fs";
4507
+ import { readFileSync as readFileSync15 } from "fs";
4361
4508
  import { resolve as resolve10 } from "path";
4362
4509
  import chalk12 from "chalk";
4363
4510
  import { Command as Command12 } from "commander";
4364
4511
  import inquirer5 from "inquirer";
4365
4512
 
4366
4513
  // 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";
4514
+ import { existsSync as existsSync17, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
4515
+ import { dirname as dirname5, join as join16, relative as relative2, sep as sep2 } from "path";
4369
4516
  import { fileURLToPath as fileURLToPath3 } from "url";
4370
4517
  var SKIP_ENV_VAR = "BIFFO_SKIP_BUILD_FRESHNESS_CHECK";
4371
4518
  function checkBuildFreshness(options = {}) {
@@ -4379,7 +4526,7 @@ function checkBuildFreshness(options = {}) {
4379
4526
  if (!packageRoot) {
4380
4527
  return { status: "skipped", reason: `no package.json above ${moduleDir}`, newerSources: [] };
4381
4528
  }
4382
- const distDir = join15(packageRoot, "dist");
4529
+ const distDir = join16(packageRoot, "dist");
4383
4530
  if (!isInside(distDir, moduleDir)) {
4384
4531
  return {
4385
4532
  status: "skipped",
@@ -4387,16 +4534,16 @@ function checkBuildFreshness(options = {}) {
4387
4534
  newerSources: []
4388
4535
  };
4389
4536
  }
4390
- const srcDir = join15(packageRoot, "src");
4391
- if (!existsSync16(srcDir)) {
4537
+ const srcDir = join16(packageRoot, "src");
4538
+ if (!existsSync17(srcDir)) {
4392
4539
  return {
4393
4540
  status: "skipped",
4394
4541
  reason: "no src/ alongside dist/ \u2014 this is a shipped package",
4395
4542
  newerSources: []
4396
4543
  };
4397
4544
  }
4398
- const entry = join15(distDir, "index.js");
4399
- if (!existsSync16(entry)) {
4545
+ const entry = join16(distDir, "index.js");
4546
+ if (!existsSync17(entry)) {
4400
4547
  return { status: "skipped", reason: `${entry} not found`, newerSources: [] };
4401
4548
  }
4402
4549
  const builtAt = statSync4(entry).mtimeMs;
@@ -4440,7 +4587,7 @@ function collectSourceFiles(srcDir) {
4440
4587
  const found = [];
4441
4588
  const walk = (dir) => {
4442
4589
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
4443
- const full = join15(dir, entry.name);
4590
+ const full = join16(dir, entry.name);
4444
4591
  if (entry.isDirectory()) {
4445
4592
  if (entry.name === "node_modules") continue;
4446
4593
  walk(full);
@@ -4459,7 +4606,7 @@ function collectSourceFiles(srcDir) {
4459
4606
  function findPackageRoot(from) {
4460
4607
  let dir = from;
4461
4608
  for (; ; ) {
4462
- if (existsSync16(join15(dir, "package.json"))) return dir;
4609
+ if (existsSync17(join16(dir, "package.json"))) return dir;
4463
4610
  const parent = dirname5(dir);
4464
4611
  if (parent === dir) return null;
4465
4612
  dir = parent;
@@ -4473,9 +4620,9 @@ function isInside(parent, child) {
4473
4620
 
4474
4621
  // src/lib/credentials.ts
4475
4622
  import { execSync as execSync6 } from "child_process";
4476
- import { existsSync as existsSync17, readFileSync as readFileSync11 } from "fs";
4623
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
4477
4624
  import { homedir as homedir2 } from "os";
4478
- import { join as join16 } from "path";
4625
+ import { join as join17 } from "path";
4479
4626
  import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
4480
4627
  import chalk10 from "chalk";
4481
4628
  import inquirer4 from "inquirer";
@@ -4654,11 +4801,11 @@ async function verifySelectedAwsCredentials(profile, region) {
4654
4801
  return sts.send(new GetCallerIdentityCommand2({}));
4655
4802
  }
4656
4803
  function discoverAwsProfiles() {
4657
- const files = [join16(homedir2(), ".aws", "credentials"), join16(homedir2(), ".aws", "config")];
4804
+ const files = [join17(homedir2(), ".aws", "credentials"), join17(homedir2(), ".aws", "config")];
4658
4805
  const profiles = /* @__PURE__ */ new Set();
4659
4806
  for (const file of files) {
4660
- if (!existsSync17(file)) continue;
4661
- const content = readFileSync11(file, "utf8");
4807
+ if (!existsSync18(file)) continue;
4808
+ const content = readFileSync12(file, "utf8");
4662
4809
  for (const match of content.matchAll(/^\s*\[([^\]]+)\]\s*$/gm)) {
4663
4810
  const section = match[1]?.trim();
4664
4811
  if (!section) continue;
@@ -4683,15 +4830,15 @@ async function resolveRepoIds(github, config) {
4683
4830
  }
4684
4831
 
4685
4832
  // 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(
4833
+ import { z as z5 } from "zod";
4834
+ var SiblingConfigSchema = z5.object({
4835
+ $schema: z5.string().optional(),
4836
+ project: z5.object({
4837
+ name: z5.string().min(1).regex(
4691
4838
  /^[a-z][a-z0-9-]*$/,
4692
4839
  "Must be lowercase kebab-case, starting with a letter (it becomes a URL path segment)"
4693
4840
  ),
4694
- description: z4.string().default(""),
4841
+ description: z5.string().default(""),
4695
4842
  // Notable routes this sibling exposes, shown as labelled links on the
4696
4843
  // core project's Microservices tab (ADR-0007). Each `path` is relative to
4697
4844
  // the sibling's own path_prefix (so "demo" renders as /<prefix>/demo), and
@@ -4699,26 +4846,26 @@ var SiblingConfigSchema = z4.object({
4699
4846
  // routes just shows its single root link. Declare real routes here as you
4700
4847
  // build the sibling's pages; the values flow to the core's
4701
4848
  // 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(
4849
+ routes: z5.array(
4850
+ z5.object({
4851
+ path: z5.string().min(1).regex(
4705
4852
  /^[a-z0-9][a-z0-9/-]*$/,
4706
4853
  'Sub-path relative to the sibling prefix, no leading slash (e.g. "demo" or "apply")'
4707
4854
  ),
4708
- label: z4.string().min(1)
4855
+ label: z5.string().min(1)
4709
4856
  })
4710
4857
  ).default([])
4711
4858
  }),
4712
4859
  source_control: SourceControlConfigSchema,
4713
4860
  cloud: CloudConfigSchema,
4714
- environments: z4.array(z4.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
4861
+ environments: z5.array(z5.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
4715
4862
  // The core project this sibling is paired with (ADR-0007) — never
4716
4863
  // provisions its own Cognito pool or CloudFront distribution, always
4717
4864
  // plugs into the core project's.
4718
- core: z4.object({
4865
+ core: z5.object({
4719
4866
  // 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(
4867
+ project_name: z5.string().min(1).optional().describe("Name of a project previously scaffolded with `biffo init` on this machine"),
4868
+ config_path: z5.string().min(1).optional().describe(
4722
4869
  "Path to the core project's biffo.config.json, for when it wasn't scaffolded here"
4723
4870
  ),
4724
4871
  // Defaults to project.name at parse time by the caller (sibling-create.ts),
@@ -4731,7 +4878,7 @@ var SiblingConfigSchema = z4.object({
4731
4878
  // CDN's default_cache_behavior instead of a pair of ordered behaviours.
4732
4879
  // It still registers under a non-empty reserved name ("app") — see
4733
4880
  // lib/root-sibling.ts for why the two must not be conflated.
4734
- path_prefix: z4.string().regex(
4881
+ path_prefix: z5.string().regex(
4735
4882
  /^$|^[a-z][a-z0-9-]*$/,
4736
4883
  "Must be lowercase kebab-case, or empty for the root sibling"
4737
4884
  ).optional()
@@ -4739,7 +4886,7 @@ var SiblingConfigSchema = z4.object({
4739
4886
  }).superRefine((config, ctx) => {
4740
4887
  if (!config.core.project_name && !config.core.config_path) {
4741
4888
  ctx.addIssue({
4742
- code: z4.ZodIssueCode.custom,
4889
+ code: z5.ZodIssueCode.custom,
4743
4890
  path: ["core"],
4744
4891
  message: "Either core.project_name or core.config_path is required"
4745
4892
  });
@@ -4748,34 +4895,34 @@ var SiblingConfigSchema = z4.object({
4748
4895
 
4749
4896
  // src/lib/sibling-session.ts
4750
4897
  import {
4751
- existsSync as existsSync18,
4898
+ existsSync as existsSync19,
4752
4899
  mkdirSync as mkdirSync6,
4753
4900
  readdirSync as readdirSync8,
4754
- readFileSync as readFileSync12,
4901
+ readFileSync as readFileSync13,
4755
4902
  rmSync as rmSync7,
4756
4903
  statSync as statSync5,
4757
4904
  writeFileSync as writeFileSync6
4758
4905
  } from "fs";
4759
4906
  import { homedir as homedir3 } from "os";
4760
- import { join as join17 } from "path";
4907
+ import { join as join18 } from "path";
4761
4908
  function sessionsDir2() {
4762
- return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join17(homedir3(), ".biffo", "sibling-sessions");
4909
+ return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join18(homedir3(), ".biffo", "sibling-sessions");
4763
4910
  }
4764
4911
  function sessionPath2(projectName) {
4765
- return join17(sessionsDir2(), `${projectName}.json`);
4912
+ return join18(sessionsDir2(), `${projectName}.json`);
4766
4913
  }
4767
4914
  function loadSiblingSession(projectName) {
4768
4915
  const path = sessionPath2(projectName);
4769
- if (!existsSync18(path)) return null;
4916
+ if (!existsSync19(path)) return null;
4770
4917
  try {
4771
- return JSON.parse(readFileSync12(path, "utf8"));
4918
+ return JSON.parse(readFileSync13(path, "utf8"));
4772
4919
  } catch {
4773
4920
  return null;
4774
4921
  }
4775
4922
  }
4776
4923
  function saveSiblingSession(session) {
4777
4924
  const dir = sessionsDir2();
4778
- if (!existsSync18(dir)) mkdirSync6(dir, { recursive: true });
4925
+ if (!existsSync19(dir)) mkdirSync6(dir, { recursive: true });
4779
4926
  const name = session.config.project?.name ?? "unknown";
4780
4927
  const prior = loadSiblingSession(name);
4781
4928
  if (prior) {
@@ -4797,30 +4944,30 @@ function markSiblingStepComplete(session, step) {
4797
4944
  }
4798
4945
  function deleteSiblingSession(projectName) {
4799
4946
  const path = sessionPath2(projectName);
4800
- if (existsSync18(path)) rmSync7(path);
4947
+ if (existsSync19(path)) rmSync7(path);
4801
4948
  }
4802
4949
 
4803
4950
  // 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";
4951
+ import { cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
4805
4952
  import { tmpdir as tmpdir4 } from "os";
4806
- import { dirname as dirname6, join as join19, resolve as resolve9 } from "path";
4953
+ import { dirname as dirname6, join as join20, resolve as resolve9 } from "path";
4807
4954
  import { fileURLToPath as fileURLToPath4 } from "url";
4808
4955
  import chalk11 from "chalk";
4809
4956
  import { Command as Command11 } from "commander";
4810
4957
 
4811
4958
  // src/lib/skeleton-dotfiles.ts
4812
4959
  import { readdirSync as readdirSync9, renameSync } from "fs";
4813
- import { join as join18 } from "path";
4960
+ import { join as join19 } from "path";
4814
4961
  var PACKAGED_GITIGNORE = "_gitignore";
4815
4962
  var REAL_GITIGNORE = ".gitignore";
4816
4963
  function restorePackagedDotfiles(dir) {
4817
4964
  const restored = [];
4818
4965
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
4819
- const full = join18(dir, entry.name);
4966
+ const full = join19(dir, entry.name);
4820
4967
  if (entry.isDirectory()) {
4821
4968
  restored.push(...restorePackagedDotfiles(full));
4822
4969
  } else if (entry.name === PACKAGED_GITIGNORE) {
4823
- const target = join18(dir, REAL_GITIGNORE);
4970
+ const target = join19(dir, REAL_GITIGNORE);
4824
4971
  renameSync(full, target);
4825
4972
  restored.push(target);
4826
4973
  }
@@ -4865,7 +5012,7 @@ async function runSiblingCreateCommand(name, options) {
4865
5012
  printDryRun2(config, coreConfig, options.templateRoot);
4866
5013
  return;
4867
5014
  }
4868
- if (!existsSync19(options.templateRoot)) {
5015
+ if (!existsSync20(options.templateRoot)) {
4869
5016
  throw new Error(`Sibling template not found at ${options.templateRoot}`);
4870
5017
  }
4871
5018
  let session = null;
@@ -5050,7 +5197,7 @@ function assertPathPrefixIsAllowed(pathPrefix) {
5050
5197
  }
5051
5198
  }
5052
5199
  function readSiblingConfig(path, root = false) {
5053
- const raw = JSON.parse(readFileSync13(path, "utf8"));
5200
+ const raw = JSON.parse(readFileSync14(path, "utf8"));
5054
5201
  const withDefaults = raw && typeof raw === "object" && "project" in raw && "core" in raw ? {
5055
5202
  ...raw,
5056
5203
  core: {
@@ -5084,7 +5231,7 @@ function resolveCoreConfig(config, configPath) {
5084
5231
  throw new Error("Either core.project_name or core.config_path is required.");
5085
5232
  }
5086
5233
  function parseCoreConfig(path) {
5087
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync13(path, "utf8")));
5234
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
5088
5235
  if (!result.success) {
5089
5236
  throw new Error(
5090
5237
  `Invalid core configuration at ${path}:
@@ -5123,7 +5270,7 @@ async function resolveCoreIdentity(coreAws, coreConfig, environments) {
5123
5270
  return coreIdentity;
5124
5271
  }
5125
5272
  async function pushSkeleton(git, skeletonRoot, cloneUrl, config, coreConfig, githubToken) {
5126
- const workDir = mkdtempSync4(join19(tmpdir4(), `biffo-sibling-${config.project.name}-`));
5273
+ const workDir = mkdtempSync4(join20(tmpdir4(), `biffo-sibling-${config.project.name}-`));
5127
5274
  try {
5128
5275
  writeSiblingTemplate(skeletonRoot, workDir, config, {
5129
5276
  coreProjectName: coreConfig.project.name,
@@ -5139,13 +5286,13 @@ async function pushSkeleton(git, skeletonRoot, cloneUrl, config, coreConfig, git
5139
5286
  }
5140
5287
  }
5141
5288
  function writeSiblingTemplate(templateRoot, targetDir, config, context) {
5142
- if (!existsSync19(templateRoot)) {
5289
+ if (!existsSync20(templateRoot)) {
5143
5290
  throw new Error(`Sibling template not found at ${templateRoot}`);
5144
5291
  }
5145
5292
  cpSync2(templateRoot, targetDir, { recursive: true });
5146
5293
  restorePackagedDotfiles(targetDir);
5147
5294
  writeFileSync7(
5148
- join19(targetDir, "biffo.sibling.json"),
5295
+ join20(targetDir, "biffo.sibling.json"),
5149
5296
  JSON.stringify(
5150
5297
  {
5151
5298
  name: config.project.name,
@@ -5161,10 +5308,10 @@ function writeSiblingTemplate(templateRoot, targetDir, config, context) {
5161
5308
  2
5162
5309
  ) + "\n"
5163
5310
  );
5164
- const envPath = join19(targetDir, "apps", "frontend", ".env.example");
5311
+ const envPath = join20(targetDir, "apps", "frontend", ".env.example");
5165
5312
  try {
5166
5313
  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}`);
5314
+ 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
5315
  writeFileSync7(envPath, content);
5169
5316
  } catch (err) {
5170
5317
  if (err.code !== "ENOENT") throw err;
@@ -5210,17 +5357,17 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
5210
5357
  }
5211
5358
  function readExistingSiblingOrigins(filePath) {
5212
5359
  try {
5213
- return JSON.parse(readFileSync13(filePath, "utf8"));
5360
+ return JSON.parse(readFileSync14(filePath, "utf8"));
5214
5361
  } catch (err) {
5215
5362
  if (err.code === "ENOENT") return {};
5216
5363
  throw err;
5217
5364
  }
5218
5365
  }
5219
5366
  function assertCoreSupportsSiblingRouting(cloneDir, coreRepo, pathPrefix = "x") {
5220
- const cdnVarsPath = join19(cloneDir, "modules", "cloud", "aws", "cdn", "variables.tf");
5367
+ const cdnVarsPath = join20(cloneDir, "modules", "cloud", "aws", "cdn", "variables.tf");
5221
5368
  let declaresSiblingOrigins = false;
5222
5369
  try {
5223
- declaresSiblingOrigins = /variable\s+"sibling_origins"/.test(readFileSync13(cdnVarsPath, "utf8"));
5370
+ declaresSiblingOrigins = /variable\s+"sibling_origins"/.test(readFileSync14(cdnVarsPath, "utf8"));
5224
5371
  } catch {
5225
5372
  declaresSiblingOrigins = false;
5226
5373
  }
@@ -5230,10 +5377,10 @@ function assertCoreSupportsSiblingRouting(cloneDir, coreRepo, pathPrefix = "x")
5230
5377
  );
5231
5378
  }
5232
5379
  if (!isRootPathPrefix(pathPrefix)) return;
5233
- const cdnMainPath = join19(cloneDir, "modules", "cloud", "aws", "cdn", "main.tf");
5380
+ const cdnMainPath = join20(cloneDir, "modules", "cloud", "aws", "cdn", "main.tf");
5234
5381
  let supportsRoot = false;
5235
5382
  try {
5236
- supportsRoot = /root_sibling_registered/.test(readFileSync13(cdnMainPath, "utf8"));
5383
+ supportsRoot = /root_sibling_registered/.test(readFileSync14(cdnMainPath, "utf8"));
5237
5384
  } catch {
5238
5385
  supportsRoot = false;
5239
5386
  }
@@ -5263,8 +5410,8 @@ async function registerWithCore(git, github, config, coreConfig, pathPrefix, git
5263
5410
  for (const env of config.environments) {
5264
5411
  const bucketName = siteBucketName(config.project.name, env, siblingAccountId);
5265
5412
  const domain = bucketRegionalDomain(bucketName, coreAwsRegion);
5266
- const relativePath = join19("infra", "environments", env, "siblings.auto.tfvars.json");
5267
- const filePath = join19(cloneDir, relativePath);
5413
+ const relativePath = join20("infra", "environments", env, "siblings.auto.tfvars.json");
5414
+ const filePath = join20(cloneDir, relativePath);
5268
5415
  const existing = readExistingSiblingOrigins(filePath);
5269
5416
  const siblings = upsertSiblingOrigin(existing.sibling_origins ?? [], {
5270
5417
  name,
@@ -5344,8 +5491,8 @@ function defaultSiblingTemplateRoot() {
5344
5491
  const start = dirname6(fileURLToPath4(import.meta.url));
5345
5492
  let dir = start;
5346
5493
  for (; ; ) {
5347
- const candidate = join19(dir, "_skeletons", "sibling-template");
5348
- if (existsSync19(candidate)) return candidate;
5494
+ const candidate = join20(dir, "_skeletons", "sibling-template");
5495
+ if (existsSync20(candidate)) return candidate;
5349
5496
  const parent = dirname6(dir);
5350
5497
  if (parent === dir) break;
5351
5498
  dir = parent;
@@ -5369,7 +5516,7 @@ var initCommand = new Command12("init").description("Scaffold a new project from
5369
5516
  let config;
5370
5517
  let githubToken;
5371
5518
  if (options.config) {
5372
- const rawConfig = JSON.parse(readFileSync14(resolve10(options.config), "utf8"));
5519
+ const rawConfig = JSON.parse(readFileSync15(resolve10(options.config), "utf8"));
5373
5520
  config = parseConfig(rawConfig);
5374
5521
  const { account_id: accountId, region } = config.cloud.config;
5375
5522
  session = resolveConfigFileSession(config, accountId, region, options.fresh === true);
@@ -5802,28 +5949,28 @@ async function promptForConfig(awsAccountId, awsRegion, awsProfile) {
5802
5949
  import { Command as Command20 } from "commander";
5803
5950
 
5804
5951
  // 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";
5952
+ import { existsSync as existsSync23, readFileSync as readFileSync17 } from "fs";
5953
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "path";
5807
5954
  import { fileURLToPath as fileURLToPath5 } from "url";
5808
5955
  import chalk13 from "chalk";
5809
5956
  import { Command as Command13 } from "commander";
5810
5957
 
5811
5958
  // src/lib/plugin-locations.ts
5812
- import { existsSync as existsSync20, readdirSync as readdirSync10 } from "fs";
5813
- import { join as join20 } from "path";
5959
+ import { existsSync as existsSync21, readdirSync as readdirSync10 } from "fs";
5960
+ import { join as join21 } from "path";
5814
5961
  var FIRST_PARTY_PLUGINS_DIR = "_plugins";
5815
5962
  var PLUGIN_MANIFEST_FILE = "biffo.plugin.json";
5816
5963
  function pluginDir(name, channel) {
5817
5964
  return channel === "first-party" ? `services/${FIRST_PARTY_PLUGINS_DIR}/${name}` : `services/${name}`;
5818
5965
  }
5819
5966
  function scanDir(absDir, relDir, channel) {
5820
- if (!existsSync20(absDir)) return [];
5967
+ if (!existsSync21(absDir)) return [];
5821
5968
  const found = [];
5822
5969
  for (const entry of readdirSync10(absDir, { withFileTypes: true })) {
5823
5970
  if (!entry.isDirectory()) continue;
5824
5971
  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;
5972
+ const manifestPath = join21(absDir, entry.name, PLUGIN_MANIFEST_FILE);
5973
+ if (!existsSync21(manifestPath)) continue;
5827
5974
  found.push({
5828
5975
  dirName: entry.name,
5829
5976
  relDir: `${relDir}/${entry.name}`,
@@ -5834,11 +5981,11 @@ function scanDir(absDir, relDir, channel) {
5834
5981
  return found;
5835
5982
  }
5836
5983
  function findInstalledPlugins(cwd) {
5837
- const servicesDir = join20(cwd, "services");
5984
+ const servicesDir = join21(cwd, "services");
5838
5985
  return [
5839
5986
  ...scanDir(servicesDir, "services", "third-party"),
5840
5987
  ...scanDir(
5841
- join20(servicesDir, FIRST_PARTY_PLUGINS_DIR),
5988
+ join21(servicesDir, FIRST_PARTY_PLUGINS_DIR),
5842
5989
  `services/${FIRST_PARTY_PLUGINS_DIR}`,
5843
5990
  "first-party"
5844
5991
  )
@@ -5846,46 +5993,46 @@ function findInstalledPlugins(cwd) {
5846
5993
  }
5847
5994
 
5848
5995
  // src/lib/plugin-manifest.ts
5849
- import { z as z5 } from "zod";
5996
+ import { z as z6 } from "zod";
5850
5997
  var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["id", "tenant_id", "created_at", "updated_at"]);
5851
5998
  var COLUMN_TYPE_PATTERN = /^(String|Integer|Text|Boolean|Float|DateTime)(\(.*\))?$/;
5852
- var ColumnDefinitionSchema = z5.object({
5853
- name: z5.string().refine(
5999
+ var ColumnDefinitionSchema = z6.object({
6000
+ name: z6.string().refine(
5854
6001
  (n) => !RESERVED_COLUMN_NAMES.has(n),
5855
6002
  (n) => ({
5856
6003
  message: `Column '${n}' is reserved and added automatically; it must not be declared in the manifest.`
5857
6004
  })
5858
6005
  ),
5859
- type: z5.string().regex(
6006
+ type: z6.string().regex(
5860
6007
  COLUMN_TYPE_PATTERN,
5861
6008
  "must be one of String, Integer, Text, Boolean, Float, DateTime (e.g. 'String(255)')"
5862
6009
  ),
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("")
6010
+ primary_key: z6.boolean().default(false),
6011
+ nullable: z6.boolean().default(false),
6012
+ index: z6.boolean().default(false),
6013
+ default: z6.string().optional(),
6014
+ description: z6.string().default("")
5868
6015
  });
5869
- var IndexDefinitionSchema = z5.object({
5870
- name: z5.string(),
5871
- columns: z5.array(z5.string()).min(1),
5872
- unique: z5.boolean().default(false)
6016
+ var IndexDefinitionSchema = z6.object({
6017
+ name: z6.string(),
6018
+ columns: z6.array(z6.string()).min(1),
6019
+ unique: z6.boolean().default(false)
5873
6020
  });
5874
- var PermissionRuleSchema = z5.object({
5875
- allowed: z5.boolean().default(false),
5876
- required_role: z5.array(z5.string()).default([])
6021
+ var PermissionRuleSchema = z6.object({
6022
+ allowed: z6.boolean().default(false),
6023
+ required_role: z6.array(z6.string()).default([])
5877
6024
  }).strict();
5878
- var TablePermissionsSchema = z5.object({
6025
+ var TablePermissionsSchema = z6.object({
5879
6026
  list: PermissionRuleSchema.default({}),
5880
6027
  read: PermissionRuleSchema.default({}),
5881
6028
  create: PermissionRuleSchema.default({}),
5882
6029
  update: PermissionRuleSchema.default({}),
5883
6030
  delete: PermissionRuleSchema.default({})
5884
6031
  }).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([]),
6032
+ var TableDefinitionSchema = z6.object({
6033
+ name: z6.string().regex(/^[a-z][a-z0-9_]*$/, "table name must be snake_case, e.g. rbac_roles"),
6034
+ columns: z6.array(ColumnDefinitionSchema).default([]),
6035
+ indexes: z6.array(IndexDefinitionSchema).default([]),
5889
6036
  permissions: TablePermissionsSchema.default({})
5890
6037
  }).superRefine((table, ctx) => {
5891
6038
  const colCounts = /* @__PURE__ */ new Map();
@@ -5893,7 +6040,7 @@ var TableDefinitionSchema = z5.object({
5893
6040
  for (const [name, count] of colCounts) {
5894
6041
  if (count > 1) {
5895
6042
  ctx.addIssue({
5896
- code: z5.ZodIssueCode.custom,
6043
+ code: z6.ZodIssueCode.custom,
5897
6044
  message: `Duplicate column name '${name}' in table '${table.name}'`
5898
6045
  });
5899
6046
  }
@@ -5903,7 +6050,7 @@ var TableDefinitionSchema = z5.object({
5903
6050
  for (const [name, count] of idxCounts) {
5904
6051
  if (count > 1) {
5905
6052
  ctx.addIssue({
5906
- code: z5.ZodIssueCode.custom,
6053
+ code: z6.ZodIssueCode.custom,
5907
6054
  message: `Duplicate index name '${name}' in table '${table.name}'`
5908
6055
  });
5909
6056
  }
@@ -5913,7 +6060,7 @@ var TableDefinitionSchema = z5.object({
5913
6060
  for (const col of idx.columns) {
5914
6061
  if (!validColumns.has(col)) {
5915
6062
  ctx.addIssue({
5916
- code: z5.ZodIssueCode.custom,
6063
+ code: z6.ZodIssueCode.custom,
5917
6064
  message: `Index '${idx.name}' on table '${table.name}' references unknown column '${col}'`
5918
6065
  });
5919
6066
  }
@@ -5928,17 +6075,17 @@ var OPERATION_METHODS = {
5928
6075
  delete: /* @__PURE__ */ new Set(["DELETE"])
5929
6076
  };
5930
6077
  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("")
6078
+ var RouteDefSchema = z6.object({
6079
+ method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
6080
+ path: z6.string().startsWith("/", "path must start with '/'"),
6081
+ table: z6.string(),
6082
+ operation: z6.enum(["list", "read", "create", "update", "delete"]),
6083
+ description: z6.string().default("")
5937
6084
  }).superRefine((route, ctx) => {
5938
6085
  const allowed = OPERATION_METHODS[route.operation];
5939
6086
  if (allowed && !allowed.has(route.method)) {
5940
6087
  ctx.addIssue({
5941
- code: z5.ZodIssueCode.custom,
6088
+ code: z6.ZodIssueCode.custom,
5942
6089
  message: `operation '${route.operation}' requires method in [${[...allowed].sort().join(", ")}], got '${route.method}'`
5943
6090
  });
5944
6091
  }
@@ -5946,38 +6093,38 @@ var RouteDefSchema = z5.object({
5946
6093
  const needsId = SINGLE_ROW_OPERATIONS.has(route.operation);
5947
6094
  if (needsId && !hasId) {
5948
6095
  ctx.addIssue({
5949
- code: z5.ZodIssueCode.custom,
6096
+ code: z6.ZodIssueCode.custom,
5950
6097
  message: `operation '${route.operation}' addresses a single row and requires an '{id}' path parameter: ${route.path}`
5951
6098
  });
5952
6099
  }
5953
6100
  if (!needsId && hasId) {
5954
6101
  ctx.addIssue({
5955
- code: z5.ZodIssueCode.custom,
6102
+ code: z6.ZodIssueCode.custom,
5956
6103
  message: `operation '${route.operation}' is collection-level and must not have an '{id}' path parameter: ${route.path}`
5957
6104
  });
5958
6105
  }
5959
6106
  });
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([]),
6107
+ var PluginManifestSchema = z6.object({
6108
+ name: z6.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
6109
+ version: z6.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
6110
+ description: z6.string().default(""),
6111
+ author: z6.string().default("Biffo Team"),
6112
+ tags: z6.array(z6.string()).default([]),
6113
+ tables: z6.array(TableDefinitionSchema).default([]),
6114
+ api_routes: z6.array(RouteDefSchema).default([]),
5968
6115
  // Events the plugin reacts to. Parsed (rather than dropped as an unknown
5969
6116
  // key) so `biffo plugin install` can warn when a plugin declares
5970
6117
  // subscriptions but ships no terraform/ to route them — see #194 and
5971
6118
  // lib/plugin-terraform-guard.ts. Kept loose deliberately: the authoritative
5972
6119
  // 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")
6120
+ event_subscriptions: z6.array(z6.object({ source: z6.string(), detail_type: z6.string() }).passthrough()).default([]),
6121
+ required_core_version: z6.string().default(">=0.0.0")
5975
6122
  }).superRefine((manifest, ctx) => {
5976
6123
  const tableNames = new Set(manifest.tables.map((t) => t.name));
5977
6124
  for (const route of manifest.api_routes) {
5978
6125
  if (!tableNames.has(route.table)) {
5979
6126
  ctx.addIssue({
5980
- code: z5.ZodIssueCode.custom,
6127
+ code: z6.ZodIssueCode.custom,
5981
6128
  message: `Route ${route.method} ${route.path} references table '${route.table}', which is not declared in this manifest's 'tables' (${[...tableNames].sort().join(", ") || "none"})`
5982
6129
  });
5983
6130
  }
@@ -5998,13 +6145,13 @@ function validateManifest(raw) {
5998
6145
  // src/lib/plugin-scaffold.ts
5999
6146
  import {
6000
6147
  copyFileSync,
6001
- existsSync as existsSync21,
6148
+ existsSync as existsSync22,
6002
6149
  mkdirSync as mkdirSync8,
6003
- readFileSync as readFileSync15,
6150
+ readFileSync as readFileSync16,
6004
6151
  readdirSync as readdirSync11,
6005
6152
  writeFileSync as writeFileSync8
6006
6153
  } from "fs";
6007
- import { dirname as dirname7, join as join21 } from "path";
6154
+ import { dirname as dirname7, join as join22 } from "path";
6008
6155
  var STANDALONE_ONLY_ENTRIES = {
6009
6156
  ".github": "standalone-repo CI/release workflows \u2014 the host monorepo already runs lint/type/test/security over services/",
6010
6157
  "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 +6204,10 @@ function applySubstitutions(text, names) {
6057
6204
  }
6058
6205
  var BINARY_EXTENSIONS = /\.(png|jpe?g|gif|ico|woff2?|ttf|zip|gz)$/i;
6059
6206
  function scaffoldPlugin(skeletonRoot, destDir, names) {
6060
- if (!existsSync21(skeletonRoot)) {
6207
+ if (!existsSync22(skeletonRoot)) {
6061
6208
  throw new Error(`Plugin skeleton not found at ${skeletonRoot}`);
6062
6209
  }
6063
- if (!existsSync21(join21(skeletonRoot, "terraform"))) {
6210
+ if (!existsSync22(join22(skeletonRoot, "terraform"))) {
6064
6211
  throw new Error(
6065
6212
  `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
6213
  );
@@ -6068,7 +6215,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
6068
6215
  const skipped = [];
6069
6216
  const files = [];
6070
6217
  const walk = (relDir) => {
6071
- const absDir = join21(skeletonRoot, relDir);
6218
+ const absDir = join22(skeletonRoot, relDir);
6072
6219
  for (const entry of readdirSync11(absDir, { withFileTypes: true }).sort(
6073
6220
  (a, b) => a.name.localeCompare(b.name)
6074
6221
  )) {
@@ -6083,14 +6230,14 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
6083
6230
  continue;
6084
6231
  }
6085
6232
  const destRel = applySubstitutions(relPath, names);
6086
- const destPath = join21(destDir, destRel);
6233
+ const destPath = join22(destDir, destRel);
6087
6234
  mkdirSync8(dirname7(destPath), { recursive: true });
6088
6235
  if (BINARY_EXTENSIONS.test(entry.name)) {
6089
- copyFileSync(join21(skeletonRoot, relPath), destPath);
6236
+ copyFileSync(join22(skeletonRoot, relPath), destPath);
6090
6237
  } else {
6091
6238
  writeFileSync8(
6092
6239
  destPath,
6093
- applySubstitutions(readFileSync15(join21(skeletonRoot, relPath), "utf8"), names)
6240
+ applySubstitutions(readFileSync16(join22(skeletonRoot, relPath), "utf8"), names)
6094
6241
  );
6095
6242
  }
6096
6243
  files.push(destRel);
@@ -6107,8 +6254,8 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
6107
6254
  function findSkeletonRoot(startDir, skeleton) {
6108
6255
  let dir = startDir;
6109
6256
  for (; ; ) {
6110
- const candidate = join21(dir, "_skeletons", skeleton);
6111
- if (existsSync21(candidate)) return candidate;
6257
+ const candidate = join22(dir, "_skeletons", skeleton);
6258
+ if (existsSync22(candidate)) return candidate;
6112
6259
  const parent = dirname7(dir);
6113
6260
  if (parent === dir) return null;
6114
6261
  dir = parent;
@@ -6145,7 +6292,7 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
6145
6292
  );
6146
6293
  async function runPluginCreate(name, options, deps) {
6147
6294
  const names = deriveNames(name);
6148
- const isInstance = existsSync22(join22(options.cwd, INSTANCE_CORE_FILE));
6295
+ const isInstance = existsSync23(join23(options.cwd, INSTANCE_CORE_FILE));
6149
6296
  if (options.firstParty && isInstance) {
6150
6297
  throw new Error(
6151
6298
  `--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 +6300,19 @@ async function runPluginCreate(name, options, deps) {
6153
6300
  }
6154
6301
  const channel = options.firstParty ? "first-party" : "third-party";
6155
6302
  const relDir = pluginDir(names.slug, channel);
6156
- const destDir = join22(options.cwd, relDir);
6157
- const servicesDir = join22(options.cwd, "services");
6158
- if (!existsSync22(servicesDir)) {
6303
+ const destDir = join23(options.cwd, relDir);
6304
+ const servicesDir = join23(options.cwd, "services");
6305
+ if (!existsSync23(servicesDir)) {
6159
6306
  throw new Error(
6160
6307
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6161
6308
  );
6162
6309
  }
6163
- if (existsSync22(destDir)) {
6310
+ if (existsSync23(destDir)) {
6164
6311
  throw new Error(`${relDir}/ already exists. Choose a different name, or remove it first.`);
6165
6312
  }
6166
6313
  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)) {
6314
+ const skeletonRoot = options.skeletonRoot ?? findSkeletonRoot(here, "plugin-template") ?? join23(options.cwd, "_skeletons", "plugin-template");
6315
+ if (!existsSync23(skeletonRoot)) {
6169
6316
  throw new Error(
6170
6317
  `Could not find the plugin skeleton (_skeletons/plugin-template/). Pass --skeleton <path> to point at it explicitly.`
6171
6318
  );
@@ -6180,8 +6327,8 @@ async function runPluginCreate(name, options, deps) {
6180
6327
  for (const { entry, reason } of skipped) {
6181
6328
  log.info(`Skipped ${entry} \u2014 ${reason}`);
6182
6329
  }
6183
- const manifestPath = join22(destDir, "biffo.plugin.json");
6184
- const manifest = validateManifest(JSON.parse(readFileSync16(manifestPath, "utf8")));
6330
+ const manifestPath = join23(destDir, "biffo.plugin.json");
6331
+ const manifest = validateManifest(JSON.parse(readFileSync17(manifestPath, "utf8")));
6185
6332
  if (manifest.name !== names.slug) {
6186
6333
  throw new Error(
6187
6334
  `Scaffolded manifest declares name '${manifest.name}', expected '${names.slug}'. The skeleton's manifest name may have diverged from 'example-plugin'.`
@@ -6237,25 +6384,25 @@ import chalk14 from "chalk";
6237
6384
  import { Command as Command14 } from "commander";
6238
6385
 
6239
6386
  // 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"])
6387
+ import { z as z7 } from "zod";
6388
+ var RegistryPluginEntrySchema = z7.object({
6389
+ name: z7.string().regex(/^[a-z][a-z0-9-]*$/),
6390
+ version: z7.string().regex(/^\d+\.\d+\.\d+$/),
6391
+ minor_version: z7.string().regex(/^\d+\.\d+$/),
6392
+ repo: z7.string().url(),
6393
+ description: z7.string().optional(),
6394
+ author: z7.string().optional(),
6395
+ tags: z7.array(z7.string()).optional(),
6396
+ required_core_version: z7.string().optional(),
6397
+ infra_modules: z7.array(z7.string()).optional(),
6398
+ api_routes: z7.array(z7.string()).optional(),
6399
+ ui_components: z7.array(z7.string()).optional(),
6400
+ status: z7.enum(["active", "disabled"])
6254
6401
  });
6255
- var PluginRegistrySchema = z6.object({
6256
- schema_version: z6.string(),
6257
- last_updated: z6.string(),
6258
- plugins: z6.array(RegistryPluginEntrySchema)
6402
+ var PluginRegistrySchema = z7.object({
6403
+ schema_version: z7.string(),
6404
+ last_updated: z7.string(),
6405
+ plugins: z7.array(RegistryPluginEntrySchema)
6259
6406
  });
6260
6407
  var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/keiranholloway/biffo-plugins-registry/main/plugins.json";
6261
6408
  var RegistryAdapter = class {
@@ -6371,14 +6518,14 @@ function printEntry(entry) {
6371
6518
  }
6372
6519
 
6373
6520
  // 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";
6521
+ import { cpSync as cpSync3, existsSync as existsSync24, mkdirSync as mkdirSync9, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
6522
+ import { basename, join as join25, relative as relative3, resolve as resolve12 } from "path";
6376
6523
  import chalk15 from "chalk";
6377
6524
  import { Command as Command15 } from "commander";
6378
6525
 
6379
6526
  // src/adapters/plugin-migrations/index.ts
6380
6527
  import { execa as execa4 } from "execa";
6381
- import { join as join23 } from "path";
6528
+ import { join as join24 } from "path";
6382
6529
  var PluginMigrationsAdapter = class {
6383
6530
  /**
6384
6531
  * Generates migration file(s) for `pluginNames` (every discovered
@@ -6387,22 +6534,22 @@ var PluginMigrationsAdapter = class {
6387
6534
  * or declared no tables.
6388
6535
  */
6389
6536
  async generate(cwd, pluginNames) {
6390
- const scriptPath = join23(cwd, "services", "api", "scripts", "generate_plugin_migrations.py");
6537
+ const scriptPath = join24(cwd, "services", "api", "scripts", "generate_plugin_migrations.py");
6391
6538
  const args = [
6392
6539
  "run",
6393
6540
  "python",
6394
6541
  scriptPath,
6395
6542
  "--services-root",
6396
- join23(cwd, "services"),
6543
+ join24(cwd, "services"),
6397
6544
  "--versions-dir",
6398
- join23(cwd, "services", "api", "migrations", "versions")
6545
+ join24(cwd, "services", "api", "migrations", "versions")
6399
6546
  ];
6400
6547
  for (const name of pluginNames ?? []) {
6401
6548
  args.push("--plugin", name);
6402
6549
  }
6403
6550
  let result;
6404
6551
  try {
6405
- result = await execa4("uv", args, { cwd: join23(cwd, "services", "api") });
6552
+ result = await execa4("uv", args, { cwd: join24(cwd, "services", "api") });
6406
6553
  } catch (err) {
6407
6554
  const cause = err;
6408
6555
  if (cause.code === "ENOENT") {
@@ -6460,14 +6607,14 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
6460
6607
  ".terraform"
6461
6608
  ]);
6462
6609
  function resolveLocalPlugin(localPath) {
6463
- if (!existsSync23(localPath)) {
6610
+ if (!existsSync24(localPath)) {
6464
6611
  throw new Error(`--local path does not exist: ${localPath}`);
6465
6612
  }
6466
6613
  if (!statSync6(localPath).isDirectory()) {
6467
6614
  throw new Error(`--local path is not a directory: ${localPath}`);
6468
6615
  }
6469
- const manifestPath = join24(localPath, "biffo.plugin.json");
6470
- if (!existsSync23(manifestPath)) {
6616
+ const manifestPath = join25(localPath, "biffo.plugin.json");
6617
+ if (!existsSync24(manifestPath)) {
6471
6618
  throw new Error(
6472
6619
  `${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
6620
  );
@@ -6493,8 +6640,8 @@ function parsePluginTarget(target) {
6493
6640
  async function cloneAndValidatePlugin(entry, git) {
6494
6641
  const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
6495
6642
  try {
6496
- const manifestPath = join24(tmpDir, "biffo.plugin.json");
6497
- if (!existsSync23(manifestPath)) {
6643
+ const manifestPath = join25(tmpDir, "biffo.plugin.json");
6644
+ if (!existsSync24(manifestPath)) {
6498
6645
  throw new Error(
6499
6646
  `Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
6500
6647
  );
@@ -6522,8 +6669,8 @@ async function runPluginInstall(target, options, deps) {
6522
6669
  `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
6670
  );
6524
6671
  }
6525
- const servicesDir = join24(options.cwd, "services");
6526
- if (!existsSync23(servicesDir)) {
6672
+ const servicesDir = join25(options.cwd, "services");
6673
+ if (!existsSync24(servicesDir)) {
6527
6674
  throw new Error(
6528
6675
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6529
6676
  );
@@ -6541,10 +6688,10 @@ async function runPluginInstall(target, options, deps) {
6541
6688
  }
6542
6689
  const pluginName = entry ? entry.name : source.name;
6543
6690
  const relTargetDir = pluginDir(pluginName, "third-party");
6544
- const targetDir = join24(options.cwd, relTargetDir);
6545
- const modulesDir = join24(options.cwd, "modules", "plugins", pluginName);
6691
+ const targetDir = join25(options.cwd, relTargetDir);
6692
+ const modulesDir = join25(options.cwd, "modules", "plugins", pluginName);
6546
6693
  const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
6547
- if (existsSync23(targetDir) && !inTreeSource) {
6694
+ if (existsSync24(targetDir) && !inTreeSource) {
6548
6695
  throw new Error(
6549
6696
  `Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
6550
6697
  );
@@ -6587,8 +6734,8 @@ async function runPluginInstall(target, options, deps) {
6587
6734
  log.success(`Installed plugin source at ${relTargetDir}/`);
6588
6735
  }
6589
6736
  const stagePaths = [relTargetDir];
6590
- const tfSourceDir = join24(targetDir, "terraform");
6591
- if (existsSync23(tfSourceDir)) {
6737
+ const tfSourceDir = join25(targetDir, "terraform");
6738
+ if (existsSync24(tfSourceDir)) {
6592
6739
  mkdirSync9(modulesDir, { recursive: true });
6593
6740
  cpSync3(tfSourceDir, modulesDir, { recursive: true });
6594
6741
  stagePaths.push(`modules/plugins/${pluginName}`);
@@ -6645,7 +6792,7 @@ async function runPluginInstall(target, options, deps) {
6645
6792
  }
6646
6793
  function parseManifestFile(path) {
6647
6794
  try {
6648
- return JSON.parse(readFileSync17(path, "utf8"));
6795
+ return JSON.parse(readFileSync18(path, "utf8"));
6649
6796
  } catch (err) {
6650
6797
  throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
6651
6798
  }
@@ -6682,8 +6829,8 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
6682
6829
  }
6683
6830
 
6684
6831
  // 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";
6832
+ import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
6833
+ import { join as join26, resolve as resolve13 } from "path";
6687
6834
  import chalk16 from "chalk";
6688
6835
  import { Command as Command16 } from "commander";
6689
6836
  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 +6843,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
6696
6843
  }
6697
6844
  });
6698
6845
  async function runPluginList(options) {
6699
- const servicesDir = join25(options.cwd, "services");
6700
- if (!existsSync24(servicesDir)) {
6846
+ const servicesDir = join26(options.cwd, "services");
6847
+ if (!existsSync25(servicesDir)) {
6701
6848
  throw new Error(
6702
6849
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6703
6850
  );
@@ -6705,7 +6852,7 @@ async function runPluginList(options) {
6705
6852
  const plugins = [];
6706
6853
  for (const location of findInstalledPlugins(options.cwd)) {
6707
6854
  try {
6708
- const manifest = validateManifest(JSON.parse(readFileSync18(location.manifestPath, "utf8")));
6855
+ const manifest = validateManifest(JSON.parse(readFileSync19(location.manifestPath, "utf8")));
6709
6856
  plugins.push({
6710
6857
  name: manifest.name,
6711
6858
  version: manifest.version,
@@ -6742,8 +6889,8 @@ async function runPluginList(options) {
6742
6889
  }
6743
6890
 
6744
6891
  // 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";
6892
+ import { existsSync as existsSync26 } from "fs";
6893
+ import { join as join27, relative as relative4, resolve as resolve14 } from "path";
6747
6894
  import chalk17 from "chalk";
6748
6895
  import { Command as Command17 } from "commander";
6749
6896
  var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
@@ -6764,11 +6911,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
6764
6911
  }
6765
6912
  );
6766
6913
  async function runPluginSyncMigrations(name, options, deps) {
6767
- const servicesDir = join26(options.cwd, "services");
6768
- if (!existsSync25(servicesDir)) {
6914
+ const servicesDir = join27(options.cwd, "services");
6915
+ if (!existsSync26(servicesDir)) {
6769
6916
  throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
6770
6917
  }
6771
- if (name && !existsSync25(join26(servicesDir, name, "biffo.plugin.json"))) {
6918
+ if (name && !existsSync26(join27(servicesDir, name, "biffo.plugin.json"))) {
6772
6919
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
6773
6920
  }
6774
6921
  if (options.dryRun) {
@@ -6804,8 +6951,8 @@ async function runPluginSyncMigrations(name, options, deps) {
6804
6951
  }
6805
6952
 
6806
6953
  // 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";
6954
+ import { existsSync as existsSync27, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
6955
+ import { join as join28, resolve as resolve15 } from "path";
6809
6956
  import chalk18 from "chalk";
6810
6957
  import { Command as Command18 } from "commander";
6811
6958
  import inquirer6 from "inquirer";
@@ -6837,16 +6984,16 @@ async function runPluginUninstall(name, options, deps) {
6837
6984
  if (!NAME_PATTERN2.test(name)) {
6838
6985
  throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
6839
6986
  }
6840
- const servicesDir = join27(options.cwd, "services");
6841
- if (!existsSync26(servicesDir)) {
6987
+ const servicesDir = join28(options.cwd, "services");
6988
+ if (!existsSync27(servicesDir)) {
6842
6989
  throw new Error(
6843
6990
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6844
6991
  );
6845
6992
  }
6846
- const targetDir = join27(servicesDir, name);
6847
- if (!existsSync26(targetDir)) {
6848
- const firstParty = join27(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
6849
- if (existsSync26(firstParty)) {
6993
+ const targetDir = join28(servicesDir, name);
6994
+ if (!existsSync27(targetDir)) {
6995
+ const firstParty = join28(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
6996
+ if (existsSync27(firstParty)) {
6850
6997
  throw new Error(
6851
6998
  `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
6999
  );
@@ -6854,9 +7001,9 @@ async function runPluginUninstall(name, options, deps) {
6854
7001
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
6855
7002
  }
6856
7003
  const version = readInstalledVersion(targetDir);
6857
- const modulesDir = join27(options.cwd, "modules", "plugins", name);
7004
+ const modulesDir = join28(options.cwd, "modules", "plugins", name);
6858
7005
  const stagePaths = [`services/${name}`];
6859
- if (existsSync26(modulesDir)) {
7006
+ if (existsSync27(modulesDir)) {
6860
7007
  stagePaths.push(`modules/plugins/${name}`);
6861
7008
  }
6862
7009
  if (options.dryRun) {
@@ -6878,7 +7025,7 @@ async function runPluginUninstall(name, options, deps) {
6878
7025
  }
6879
7026
  rmSync8(targetDir, { recursive: true, force: true });
6880
7027
  log.success(`Removed services/${name}/`);
6881
- if (existsSync26(modulesDir)) {
7028
+ if (existsSync27(modulesDir)) {
6882
7029
  rmSync8(modulesDir, { recursive: true, force: true });
6883
7030
  log.success(`Removed modules/plugins/${name}/`);
6884
7031
  const wiring = syncPluginTerraform(options.cwd);
@@ -6915,10 +7062,10 @@ async function runPluginUninstall(name, options, deps) {
6915
7062
  }
6916
7063
  }
6917
7064
  function readInstalledVersion(targetDir) {
6918
- const manifestPath = join27(targetDir, "biffo.plugin.json");
6919
- if (!existsSync26(manifestPath)) return void 0;
7065
+ const manifestPath = join28(targetDir, "biffo.plugin.json");
7066
+ if (!existsSync27(manifestPath)) return void 0;
6920
7067
  try {
6921
- return validateManifest(JSON.parse(readFileSync19(manifestPath, "utf8"))).version;
7068
+ return validateManifest(JSON.parse(readFileSync20(manifestPath, "utf8"))).version;
6922
7069
  } catch {
6923
7070
  return void 0;
6924
7071
  }
@@ -6951,8 +7098,8 @@ function printDryRun5(name, version, stagePaths, keepData) {
6951
7098
  }
6952
7099
 
6953
7100
  // 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";
7101
+ import { cpSync as cpSync4, existsSync as existsSync28, mkdirSync as mkdirSync10, readFileSync as readFileSync21, rmSync as rmSync9 } from "fs";
7102
+ import { join as join29, relative as relative5, resolve as resolve16 } from "path";
6956
7103
  import chalk19 from "chalk";
6957
7104
  import { Command as Command19 } from "commander";
6958
7105
  import inquirer7 from "inquirer";
@@ -6977,14 +7124,14 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
6977
7124
  });
6978
7125
  async function runPluginUpgrade(target, options, deps) {
6979
7126
  const { name, minor } = parsePluginTarget(target);
6980
- const servicesDir = join28(options.cwd, "services");
6981
- if (!existsSync27(servicesDir)) {
7127
+ const servicesDir = join29(options.cwd, "services");
7128
+ if (!existsSync28(servicesDir)) {
6982
7129
  throw new Error(
6983
7130
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6984
7131
  );
6985
7132
  }
6986
- const targetDir = join28(servicesDir, name);
6987
- if (!existsSync27(targetDir)) {
7133
+ const targetDir = join29(servicesDir, name);
7134
+ if (!existsSync28(targetDir)) {
6988
7135
  throw new Error(
6989
7136
  `Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
6990
7137
  );
@@ -6998,7 +7145,7 @@ async function runPluginUpgrade(target, options, deps) {
6998
7145
  `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
7146
  );
7000
7147
  }
7001
- const modulesDir = join28(options.cwd, "modules", "plugins", entry.name);
7148
+ const modulesDir = join29(options.cwd, "modules", "plugins", entry.name);
7002
7149
  if (options.dryRun) {
7003
7150
  printDryRun6(entry, currentVersion);
7004
7151
  return;
@@ -7031,11 +7178,11 @@ async function runPluginUpgrade(target, options, deps) {
7031
7178
  cpSync4(tmpDir, targetDir, { recursive: true });
7032
7179
  log.success(`Upgraded plugin source at services/${entry.name}/`);
7033
7180
  const stagePaths = [`services/${entry.name}`];
7034
- if (existsSync27(modulesDir)) {
7181
+ if (existsSync28(modulesDir)) {
7035
7182
  rmSync9(modulesDir, { recursive: true, force: true });
7036
7183
  }
7037
- const tfSourceDir = join28(targetDir, "terraform");
7038
- if (existsSync27(tfSourceDir)) {
7184
+ const tfSourceDir = join29(targetDir, "terraform");
7185
+ if (existsSync28(tfSourceDir)) {
7039
7186
  mkdirSync10(modulesDir, { recursive: true });
7040
7187
  cpSync4(tfSourceDir, modulesDir, { recursive: true });
7041
7188
  stagePaths.push(`modules/plugins/${entry.name}`);
@@ -7069,10 +7216,10 @@ async function runPluginUpgrade(target, options, deps) {
7069
7216
  }
7070
7217
  }
7071
7218
  function readInstalledVersion2(targetDir) {
7072
- const manifestPath = join28(targetDir, "biffo.plugin.json");
7073
- if (!existsSync27(manifestPath)) return void 0;
7219
+ const manifestPath = join29(targetDir, "biffo.plugin.json");
7220
+ if (!existsSync28(manifestPath)) return void 0;
7074
7221
  try {
7075
- return validateManifest(JSON.parse(readFileSync20(manifestPath, "utf8"))).version;
7222
+ return validateManifest(JSON.parse(readFileSync21(manifestPath, "utf8"))).version;
7076
7223
  } catch {
7077
7224
  return void 0;
7078
7225
  }
@@ -7122,90 +7269,6 @@ import { Command as Command22 } from "commander";
7122
7269
 
7123
7270
  // src/scripts/check-core-ownership.ts
7124
7271
  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
7272
  var BOLD = "\x1B[1m";
7210
7273
  var DIM = "\x1B[2m";
7211
7274
  var RED = "\x1B[31m";
@@ -7276,6 +7339,12 @@ async function runOwnershipCheck(argv) {
7276
7339
  );
7277
7340
  return;
7278
7341
  }
7342
+ if (result.skipped === "convergence-trailer") {
7343
+ console.log(
7344
+ `\u2713 core ownership guard: allowed by Core-Convergence (reverting toward the template): ${result.convergenceReason ?? ""}`
7345
+ );
7346
+ return;
7347
+ }
7279
7348
  if (result.blocked.length === 0) {
7280
7349
  console.log("\u2713 core ownership guard: no template-owned paths changed.");
7281
7350
  return;
@@ -7299,11 +7368,20 @@ ${BOLD}What to do instead${OFF}
7299
7368
  path \u2014 see core-manifest.json for the split.
7300
7369
 
7301
7370
  ${result.blocked.some((p) => deletedFiles.includes(p)) ? `${BOLD}Some of these are deletions${OFF}
7302
- Deleting a template-owned file is not a smaller change than editing one \u2014 a
7303
- core upgrade will not restore it (#395), so the instance loses it silently and
7304
- for ever. If the file genuinely should not exist, delete it in biffo-template.
7371
+ Deleting a template-owned file is drift like any other: the next
7372
+ \`biffo core upgrade\` will restore it (#395) unless you declare the path an
7373
+ intentional divergence. If the file genuinely should not exist, delete it in
7374
+ biffo-template so every instance drops it.
7375
+
7376
+ ` : ""}${BOLD}If this REMOVES divergence (reverting toward the template)${OFF}
7377
+ Reverting a template-owned file to the template's own content, or deleting a
7378
+ file the template no longer ships, leaves the instance strictly closer to the
7379
+ template. Record that it converges \u2014 it is allowed, and kept distinct from a
7380
+ divergence so it never reads as drift to chase later (#385):
7381
+
7382
+ ${DIM}Core-Convergence: <what this reverts toward the template>${OFF}
7305
7383
 
7306
- ` : ""}${BOLD}If the divergence is deliberate${OFF}
7384
+ ${BOLD}If the divergence is deliberate${OFF}
7307
7385
  Record it in the commit message and it is allowed:
7308
7386
 
7309
7387
  ${DIM}Core-Divergence: <why this instance must differ from the template>${OFF}