@biffo/cli 0.56.2 → 0.57.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 (3) hide show
  1. package/core.version +1 -1
  2. package/dist/index.js +304 -214
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -337,7 +337,7 @@ async function runCoreStatus(options) {
337
337
 
338
338
  // src/commands/core-upgrade.ts
339
339
  import { execSync as execSync2 } from "child_process";
340
- import { join as join10, resolve as resolve3 } from "path";
340
+ import { join as join11, resolve as resolve3 } from "path";
341
341
  import chalk4 from "chalk";
342
342
  import { execa as execa3 } from "execa";
343
343
  import { Command as Command3 } from "commander";
@@ -1628,6 +1628,45 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1628
1628
  return { dir, cleanup: () => rmSync3(dir, { recursive: true, force: true }) };
1629
1629
  }
1630
1630
 
1631
+ // src/lib/breaking-changes.ts
1632
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1633
+ import { join as join8 } from "path";
1634
+ var UPGRADE_GUIDE_PATH = "docs/guides/core-upgrade.md";
1635
+ var SECTION_HEADING = "## Breaking changes by version";
1636
+ var ENTRY_HEADING = /^###\s+(\d+\.\d+\.\d+)\s*[—-]\s*(.+?)\s*$/;
1637
+ function parseBreakingChanges(guide) {
1638
+ const lines = guide.split("\n");
1639
+ const start = lines.findIndex((l) => l.trim() === SECTION_HEADING);
1640
+ if (start === -1) return [];
1641
+ const entries = [];
1642
+ let current = null;
1643
+ for (const line of lines.slice(start + 1)) {
1644
+ if (/^##\s/.test(line) && !/^###/.test(line)) break;
1645
+ const match = ENTRY_HEADING.exec(line);
1646
+ if (match?.[1] && match[2]) {
1647
+ if (current) entries.push({ ...current, body: current.body.trim() });
1648
+ current = { version: match[1], title: match[2], body: "" };
1649
+ continue;
1650
+ }
1651
+ if (current) current.body += `${line}
1652
+ `;
1653
+ }
1654
+ if (current) entries.push({ ...current, body: current.body.trim() });
1655
+ return entries;
1656
+ }
1657
+ function readBreakingChanges(templateRoot) {
1658
+ const path = join8(templateRoot, UPGRADE_GUIDE_PATH);
1659
+ if (!existsSync6(path)) return [];
1660
+ return parseBreakingChanges(readFileSync5(path, "utf8"));
1661
+ }
1662
+ function breakingChangesBetween(from, to, entries) {
1663
+ parseCoreVersion(from);
1664
+ parseCoreVersion(to);
1665
+ return entries.filter(
1666
+ (e) => compareCoreVersions(e.version, from) > 0 && compareCoreVersions(e.version, to) <= 0
1667
+ ).sort((a, b) => compareCoreVersions(a.version, b.version));
1668
+ }
1669
+
1631
1670
  // src/lib/global-workflows.ts
1632
1671
  var GLOBAL_DISPATCH_REF = "main";
1633
1672
  var GLOBAL_DISPATCH_WORKFLOW_PATHS = [
@@ -1635,8 +1674,8 @@ var GLOBAL_DISPATCH_WORKFLOW_PATHS = [
1635
1674
  ];
1636
1675
 
1637
1676
  // src/lib/plugin-terraform-wiring.ts
1638
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1639
- import { join as join8 } from "path";
1677
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1678
+ import { join as join9 } from "path";
1640
1679
  var TEMPLATE_MODULE_DIR = "_template";
1641
1680
  var DEFAULT_PLUGIN_HANDLER = "src.lambda.main.handler";
1642
1681
  var GENERATED_TF_FILE = "plugins.generated.tf";
@@ -1654,7 +1693,7 @@ function standardArguments(pluginName, handler) {
1654
1693
  ];
1655
1694
  }
1656
1695
  function listPluginModules(cwd) {
1657
- const dir = join8(cwd, "modules", "plugins");
1696
+ const dir = join9(cwd, "modules", "plugins");
1658
1697
  let entries;
1659
1698
  try {
1660
1699
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1666,7 +1705,7 @@ function listPluginModules(cwd) {
1666
1705
  var FIRST_PARTY_TERRAFORM = (name) => `../../../services/_plugins/${name}/terraform`;
1667
1706
  var THIRD_PARTY_TERRAFORM = (name) => `../../../modules/plugins/${name}`;
1668
1707
  function isFirstPartyPlugin(cwd, name) {
1669
- return existsSync6(join8(cwd, "services", "_plugins", name, "terraform", "main.tf"));
1708
+ return existsSync7(join9(cwd, "services", "_plugins", name, "terraform", "main.tf"));
1670
1709
  }
1671
1710
  function pluginModuleSource(cwd, name) {
1672
1711
  return isFirstPartyPlugin(cwd, name) ? FIRST_PARTY_TERRAFORM(name) : THIRD_PARTY_TERRAFORM(name);
@@ -1677,7 +1716,7 @@ function listWireablePlugins(cwd) {
1677
1716
  return [.../* @__PURE__ */ new Set([...copied, ...firstParty])].sort();
1678
1717
  }
1679
1718
  function firstPartyPluginNames(cwd) {
1680
- const dir = join8(cwd, "services", "_plugins");
1719
+ const dir = join9(cwd, "services", "_plugins");
1681
1720
  let entries;
1682
1721
  try {
1683
1722
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1691,7 +1730,7 @@ function staleFirstPartyCopies(cwd) {
1691
1730
  return firstPartyPluginNames(cwd).filter((name) => copied.has(name));
1692
1731
  }
1693
1732
  function listEnvironments(cwd) {
1694
- const dir = join8(cwd, "infra", "environments");
1733
+ const dir = join9(cwd, "infra", "environments");
1695
1734
  let entries;
1696
1735
  try {
1697
1736
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1699,12 +1738,12 @@ function listEnvironments(cwd) {
1699
1738
  return [];
1700
1739
  }
1701
1740
  return entries.filter((e) => {
1702
- if (!e.isDirectory() || !existsSync6(join8(dir, e.name, "main.tf"))) return false;
1703
- return declaredVariables(join8(dir, e.name)).has("enabled_plugins");
1741
+ if (!e.isDirectory() || !existsSync7(join9(dir, e.name, "main.tf"))) return false;
1742
+ return declaredVariables(join9(dir, e.name)).has("enabled_plugins");
1704
1743
  }).map((e) => e.name).sort();
1705
1744
  }
1706
1745
  function listUnwirableEnvironments(cwd) {
1707
- const dir = join8(cwd, "infra", "environments");
1746
+ const dir = join9(cwd, "infra", "environments");
1708
1747
  let entries;
1709
1748
  try {
1710
1749
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1712,7 +1751,7 @@ function listUnwirableEnvironments(cwd) {
1712
1751
  return [];
1713
1752
  }
1714
1753
  return entries.filter(
1715
- (e) => e.isDirectory() && existsSync6(join8(dir, e.name, "main.tf")) && !declaredVariables(join8(dir, e.name)).has("enabled_plugins")
1754
+ (e) => e.isDirectory() && existsSync7(join9(dir, e.name, "main.tf")) && !declaredVariables(join9(dir, e.name)).has("enabled_plugins")
1716
1755
  ).map((e) => e.name).sort();
1717
1756
  }
1718
1757
  function declaredVariables(moduleDir) {
@@ -1727,7 +1766,7 @@ function declaredVariables(moduleDir) {
1727
1766
  if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
1728
1767
  let contents;
1729
1768
  try {
1730
- contents = readFileSync5(join8(moduleDir, entry.name), "utf8");
1769
+ contents = readFileSync6(join9(moduleDir, entry.name), "utf8");
1731
1770
  } catch {
1732
1771
  continue;
1733
1772
  }
@@ -1804,7 +1843,7 @@ function syncPluginTerraform(cwd) {
1804
1843
  const changedPaths = [];
1805
1844
  const rendered = plugins.map((name) => {
1806
1845
  const firstParty = isFirstPartyPlugin(cwd, name);
1807
- const moduleDir = firstParty ? join8(cwd, "services", "_plugins", name, "terraform") : join8(cwd, "modules", "plugins", name);
1846
+ const moduleDir = firstParty ? join9(cwd, "services", "_plugins", name, "terraform") : join9(cwd, "modules", "plugins", name);
1808
1847
  return {
1809
1848
  name,
1810
1849
  declaredVariables: declaredVariables(moduleDir),
@@ -1812,16 +1851,16 @@ function syncPluginTerraform(cwd) {
1812
1851
  };
1813
1852
  });
1814
1853
  for (const env of environments) {
1815
- const envDir = join8(cwd, "infra", "environments", env);
1816
- const tfPath = join8(envDir, GENERATED_TF_FILE);
1817
- const tfvarsPath = join8(envDir, GENERATED_TFVARS_FILE);
1854
+ const envDir = join9(cwd, "infra", "environments", env);
1855
+ const tfPath = join9(envDir, GENERATED_TF_FILE);
1856
+ const tfvarsPath = join9(envDir, GENERATED_TFVARS_FILE);
1818
1857
  const relBase = `infra/environments/${env}`;
1819
1858
  if (plugins.length === 0) {
1820
1859
  for (const [abs, rel] of [
1821
1860
  [tfPath, `${relBase}/${GENERATED_TF_FILE}`],
1822
1861
  [tfvarsPath, `${relBase}/${GENERATED_TFVARS_FILE}`]
1823
1862
  ]) {
1824
- if (existsSync6(abs)) {
1863
+ if (existsSync7(abs)) {
1825
1864
  rmSync4(abs);
1826
1865
  changedPaths.push(rel);
1827
1866
  }
@@ -1837,8 +1876,8 @@ function syncPluginTerraform(cwd) {
1837
1876
  }
1838
1877
 
1839
1878
  // src/lib/lockfile-refresh.ts
1840
- import { existsSync as existsSync7 } from "fs";
1841
- import { join as join9 } from "path";
1879
+ import { existsSync as existsSync8 } from "fs";
1880
+ import { join as join10 } from "path";
1842
1881
  var LOCKFILE_TRIGGERS = [
1843
1882
  {
1844
1883
  manifest: "package.json",
@@ -1861,7 +1900,7 @@ function lockfilesNeedingRefresh(changedPaths, instanceDir, triggers = LOCKFILE_
1861
1900
  const locked = changedPaths.filter((p) => !isForeignManifest(p));
1862
1901
  return triggers.filter((t) => {
1863
1902
  const touched = locked.some((p) => p === t.manifest || p.endsWith(`/${t.manifest}`));
1864
- return touched && existsSync7(join9(instanceDir, t.lockfile));
1903
+ return touched && existsSync8(join10(instanceDir, t.lockfile));
1865
1904
  });
1866
1905
  }
1867
1906
  async function refreshLockfiles(instanceDir, triggers, run) {
@@ -1891,13 +1930,17 @@ var coreUpgradeCommand = new Command3("upgrade").description("Three-way-merge te
1891
1930
  ).option(
1892
1931
  "--to-template <path>",
1893
1932
  "Override: path to a template checkout at the TARGET version. Normally auto-resolved."
1894
- ).option("--apply", "Apply the plan on a new branch and open a PR (default: dry run)").option("--allow-conflicts", "With --apply, open the PR even if some files conflict").option("--base <branch>", "Base branch for the PR (defaults to the current branch)").option("--remote <name>", "Git remote to push to and open the PR on (default: origin)").action(
1933
+ ).option("--apply", "Apply the plan on a new branch and open a PR (default: dry run)").option("--allow-conflicts", "With --apply, open the PR even if some files conflict").option(
1934
+ "--acknowledge-breaking",
1935
+ "With --apply, proceed even though this upgrade crosses a documented breaking change"
1936
+ ).option("--base <branch>", "Base branch for the PR (defaults to the current branch)").option("--remote <name>", "Git remote to push to and open the PR on (default: origin)").action(
1895
1937
  async (options) => {
1896
1938
  const cwd = options.cwd ? resolve3(options.cwd) : process.cwd();
1897
1939
  const runOptions = {
1898
1940
  cwd,
1899
1941
  apply: options.apply ?? false,
1900
- allowConflicts: options.allowConflicts ?? false
1942
+ allowConflicts: options.allowConflicts ?? false,
1943
+ acknowledgeBreaking: options.acknowledgeBreaking ?? false
1901
1944
  };
1902
1945
  if (options.templateRepo) runOptions.templateRepo = resolve3(options.templateRepo);
1903
1946
  if (options.to) runOptions.toVersion = options.to;
@@ -1950,9 +1993,9 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
1950
1993
  let toVersion;
1951
1994
  if (options.theirsDir) {
1952
1995
  theirsDir = options.theirsDir;
1953
- toVersion = readCoreVersionFile(join10(theirsDir, "core.version"));
1996
+ toVersion = readCoreVersionFile(join11(theirsDir, "core.version"));
1954
1997
  } else {
1955
- const workingVersion = readCoreVersionFile(join10(templateRepo, "core.version"));
1998
+ const workingVersion = readCoreVersionFile(join11(templateRepo, "core.version"));
1956
1999
  toVersion = options.toVersion ?? workingVersion;
1957
2000
  if (toVersion === workingVersion) {
1958
2001
  theirsDir = templateRepo;
@@ -1966,7 +2009,7 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
1966
2009
  let fromVersion;
1967
2010
  if (options.baseDir) {
1968
2011
  baseDir = options.baseDir;
1969
- fromVersion = readCoreVersionFile(join10(baseDir, "core.version"));
2012
+ fromVersion = readCoreVersionFile(join11(baseDir, "core.version"));
1970
2013
  } else {
1971
2014
  if (instanceVersion === null) {
1972
2015
  throw new Error(
@@ -1998,6 +2041,14 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
1998
2041
  log.success("Nothing to upgrade \u2014 the instance already matches the target for all core files.");
1999
2042
  return;
2000
2043
  }
2044
+ const breaking = breakingChangesBetween(
2045
+ fromVersion,
2046
+ toVersion,
2047
+ // From the TEMPLATE at the target version, never the instance's own copy —
2048
+ // docs/ is user-owned, so an instance's guide may be stale or edited.
2049
+ readBreakingChanges(theirsDir)
2050
+ );
2051
+ printBreakingChanges(breaking, options.apply === true);
2001
2052
  printPlan(plan);
2002
2053
  printMigrationCarry(migrations);
2003
2054
  warnStaleFirstPartyCopies(options.cwd);
@@ -2014,9 +2065,14 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
2014
2065
  );
2015
2066
  return;
2016
2067
  }
2017
- await applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion);
2068
+ await applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking);
2018
2069
  }
2019
- async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion) {
2070
+ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking) {
2071
+ if (breaking.length > 0 && !options.acknowledgeBreaking) {
2072
+ throw new Error(
2073
+ `This upgrade crosses ${breaking.length} documented breaking change(s): ${breaking.map((b) => b.version).join(", ")}. They are printed above and in ${UPGRADE_GUIDE_PATH}. Read what each one requires \u2014 some destroy data or need manual work after the deploy \u2014 then re-run with --acknowledge-breaking.`
2074
+ );
2075
+ }
2020
2076
  if (plan.conflicts.length > 0 && !options.allowConflicts) {
2021
2077
  throw new Error(
2022
2078
  `${plan.conflicts.length} file(s) conflict. Resolve them upstream, or re-run with --allow-conflicts to open a PR that includes the conflict markers for manual resolution.`
@@ -2058,7 +2114,7 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
2058
2114
  head: branch,
2059
2115
  base,
2060
2116
  title: `Upgrade Biffo core ${fromVersion} \u2192 ${toVersion}`,
2061
- body: buildPrBody(fromVersion, toVersion, plan, migrations, base, lockfiles)
2117
+ body: buildPrBody(fromVersion, toVersion, plan, migrations, base, lockfiles, breaking)
2062
2118
  });
2063
2119
  if (plan.conflicts.length > 0) {
2064
2120
  log.warn(`PR opened with ${plan.conflicts.length} conflict(s) to resolve: ${pr.url}`);
@@ -2066,8 +2122,20 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
2066
2122
  log.success(`Opened PR #${pr.number}: ${pr.url}`);
2067
2123
  }
2068
2124
  }
2069
- function buildPrBody(from, to, plan, migrations, base = GLOBAL_DISPATCH_REF, lockfiles = []) {
2070
- const lines = [
2125
+ function buildPrBody(from, to, plan, migrations, base = GLOBAL_DISPATCH_REF, lockfiles = [], breaking = []) {
2126
+ const lines = [];
2127
+ if (breaking.length > 0) {
2128
+ lines.push(
2129
+ `## \u26A0 This upgrade crosses ${breaking.length} breaking change(s)`,
2130
+ "",
2131
+ "Read these before merging \u2014 some destroy data or require manual work afterwards.",
2132
+ "",
2133
+ ...breaking.flatMap((b) => [`### ${b.version} \u2014 ${b.title}`, "", b.body, ""]),
2134
+ "---",
2135
+ ""
2136
+ );
2137
+ }
2138
+ lines.push(
2071
2139
  "Automated core upgrade generated by `biffo core upgrade` (ADR-0006).",
2072
2140
  "",
2073
2141
  `Bumps the Biffo template core from **${from}** to **${to}** and updates \`biffo.core.json\`. Only template-owned paths (see \`core-manifest.json\`) were touched \u2014 product, plugin, and infra files were left untouched.`,
@@ -2078,7 +2146,7 @@ function buildPrBody(from, to, plan, migrations, base = GLOBAL_DISPATCH_REF, loc
2078
2146
  `- take-theirs: ${plan.summary["take-theirs"]}`,
2079
2147
  `- added: ${plan.summary.added}`,
2080
2148
  `- removed: ${plan.summary.removed}`
2081
- ];
2149
+ );
2082
2150
  if (migrations.entries.length > 0) {
2083
2151
  lines.push(
2084
2152
  "",
@@ -2203,6 +2271,28 @@ function warnStaleFirstPartyCopies(cwd) {
2203
2271
  `${stale.length} first-party plugin(s) still have a copy under modules/plugins/: ${stale.join(", ")}. An upgrade never updates those copies, so if infra/environments/*/ still sources them, this plugin's infrastructure changes are NOT deployed. Point the module source at services/_plugins/<name>/terraform and delete the copy \u2014 see docs/guides/core-upgrade.md.`
2204
2272
  );
2205
2273
  }
2274
+ function printBreakingChanges(breaking, applying) {
2275
+ if (breaking.length === 0) return;
2276
+ console.log(
2277
+ chalk4.red.bold(
2278
+ `
2279
+ \u26A0 This upgrade crosses ${breaking.length} documented breaking change(s):
2280
+ `
2281
+ )
2282
+ );
2283
+ for (const b of breaking) {
2284
+ console.log(` ${chalk4.bold(b.version)} \u2014 ${b.title}`);
2285
+ for (const line of b.body.split("\n").slice(0, 6)) console.log(chalk4.dim(` ${line}`));
2286
+ console.log();
2287
+ }
2288
+ console.log(chalk4.dim(` Full detail: ${UPGRADE_GUIDE_PATH}
2289
+ `));
2290
+ if (!applying) {
2291
+ console.log(
2292
+ chalk4.dim(" Re-run with --apply --acknowledge-breaking once you have read them.\n")
2293
+ );
2294
+ }
2295
+ }
2206
2296
 
2207
2297
  // src/commands/core.ts
2208
2298
  var coreCommand = new Command4("core").description(
@@ -2216,7 +2306,7 @@ coreCommand.addCommand(coreUpgradeCommand);
2216
2306
  import { Command as Command8 } from "commander";
2217
2307
 
2218
2308
  // src/commands/data-apply.ts
2219
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
2309
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
2220
2310
  import { resolve as resolve4 } from "path";
2221
2311
  import chalk5 from "chalk";
2222
2312
  import { Command as Command5 } from "commander";
@@ -2667,16 +2757,16 @@ function isTemplatePlaceholderConfig(raw) {
2667
2757
 
2668
2758
  // src/lib/session.ts
2669
2759
  import {
2670
- existsSync as existsSync8,
2760
+ existsSync as existsSync9,
2671
2761
  mkdirSync as mkdirSync4,
2672
2762
  readdirSync as readdirSync4,
2673
- readFileSync as readFileSync6,
2763
+ readFileSync as readFileSync7,
2674
2764
  rmSync as rmSync5,
2675
2765
  statSync,
2676
2766
  writeFileSync as writeFileSync5
2677
2767
  } from "fs";
2678
2768
  import { homedir } from "os";
2679
- import { join as join11 } from "path";
2769
+ import { join as join12 } from "path";
2680
2770
  var LEGACY_STEP_ALIASES = {
2681
2771
  github_config: ["github_branches", "github_instance_files", "github_settings"]
2682
2772
  };
@@ -2685,39 +2775,39 @@ function hasCompleted(session, step) {
2685
2775
  return session.completedSteps.some((done) => LEGACY_STEP_ALIASES[done]?.includes(step) ?? false);
2686
2776
  }
2687
2777
  function sessionsDir() {
2688
- return process.env["BIFFO_SESSIONS_DIR"] ?? join11(homedir(), ".biffo", "sessions");
2778
+ return process.env["BIFFO_SESSIONS_DIR"] ?? join12(homedir(), ".biffo", "sessions");
2689
2779
  }
2690
2780
  function sessionPath(projectName) {
2691
- return join11(sessionsDir(), `${projectName}.json`);
2781
+ return join12(sessionsDir(), `${projectName}.json`);
2692
2782
  }
2693
2783
  function loadSession(projectName) {
2694
2784
  const path = sessionPath(projectName);
2695
- if (!existsSync8(path)) return null;
2785
+ if (!existsSync9(path)) return null;
2696
2786
  try {
2697
- return JSON.parse(readFileSync6(path, "utf8"));
2787
+ return JSON.parse(readFileSync7(path, "utf8"));
2698
2788
  } catch {
2699
2789
  return null;
2700
2790
  }
2701
2791
  }
2702
2792
  function findLatestSession() {
2703
2793
  const dir = sessionsDir();
2704
- if (!existsSync8(dir)) return null;
2794
+ if (!existsSync9(dir)) return null;
2705
2795
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
2706
2796
  if (files.length === 0) return null;
2707
2797
  const sorted = files.map((f) => {
2708
- const fullPath = join11(dir, f);
2709
- const mtime = existsSync8(fullPath) ? statSync(fullPath).mtimeMs : -1;
2798
+ const fullPath = join12(dir, f);
2799
+ const mtime = existsSync9(fullPath) ? statSync(fullPath).mtimeMs : -1;
2710
2800
  return { f, mtime };
2711
2801
  }).sort((a, b) => b.mtime - a.mtime);
2712
2802
  try {
2713
- return JSON.parse(readFileSync6(join11(dir, sorted[0].f), "utf8"));
2803
+ return JSON.parse(readFileSync7(join12(dir, sorted[0].f), "utf8"));
2714
2804
  } catch {
2715
2805
  return null;
2716
2806
  }
2717
2807
  }
2718
2808
  function saveSession(session) {
2719
2809
  const dir = sessionsDir();
2720
- if (!existsSync8(dir)) mkdirSync4(dir, { recursive: true });
2810
+ if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
2721
2811
  const name = session.config.project?.name ?? "unknown";
2722
2812
  const prior = loadSession(name);
2723
2813
  if (prior) {
@@ -2739,36 +2829,36 @@ function markStepComplete(session, step) {
2739
2829
  }
2740
2830
  function deleteSession(projectName) {
2741
2831
  const path = sessionPath(projectName);
2742
- if (existsSync8(path)) rmSync5(path);
2832
+ if (existsSync9(path)) rmSync5(path);
2743
2833
  }
2744
2834
  function projectsDir() {
2745
- return process.env["BIFFO_PROJECTS_DIR"] ?? join11(homedir(), ".biffo", "projects");
2835
+ return process.env["BIFFO_PROJECTS_DIR"] ?? join12(homedir(), ".biffo", "projects");
2746
2836
  }
2747
2837
  function saveProjectConfig(config) {
2748
2838
  const dir = projectsDir();
2749
- if (!existsSync8(dir)) mkdirSync4(dir, { recursive: true });
2750
- writeFileSync5(join11(dir, `${config.project.name}.json`), JSON.stringify(config, null, 2));
2839
+ if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
2840
+ writeFileSync5(join12(dir, `${config.project.name}.json`), JSON.stringify(config, null, 2));
2751
2841
  }
2752
2842
  function loadProjectConfig(name) {
2753
- const path = join11(projectsDir(), `${name}.json`);
2754
- if (!existsSync8(path)) return null;
2843
+ const path = join12(projectsDir(), `${name}.json`);
2844
+ if (!existsSync9(path)) return null;
2755
2845
  try {
2756
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync6(path, "utf8")));
2846
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync7(path, "utf8")));
2757
2847
  return result.success ? result.data : null;
2758
2848
  } catch {
2759
2849
  return null;
2760
2850
  }
2761
2851
  }
2762
2852
  function deleteProjectConfig(name) {
2763
- const path = join11(projectsDir(), `${name}.json`);
2764
- if (existsSync8(path)) rmSync5(path);
2853
+ const path = join12(projectsDir(), `${name}.json`);
2854
+ if (existsSync9(path)) rmSync5(path);
2765
2855
  }
2766
2856
  function listProjectConfigs() {
2767
2857
  const dir = projectsDir();
2768
- if (!existsSync8(dir)) return [];
2858
+ if (!existsSync9(dir)) return [];
2769
2859
  return readdirSync4(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
2770
2860
  try {
2771
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync6(join11(dir, f), "utf8")));
2861
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync7(join12(dir, f), "utf8")));
2772
2862
  return result.success ? [result.data] : [];
2773
2863
  } catch {
2774
2864
  return [];
@@ -2837,7 +2927,7 @@ async function runDataApply(name, environment, config, aws) {
2837
2927
  }
2838
2928
  async function resolveConfig(options) {
2839
2929
  if (options.config) {
2840
- const raw = JSON.parse(readFileSync7(resolve4(options.config), "utf8"));
2930
+ const raw = JSON.parse(readFileSync8(resolve4(options.config), "utf8"));
2841
2931
  const result = BiffoConfigSchema.safeParse(raw);
2842
2932
  if (!result.success) {
2843
2933
  log.error(`Invalid config at ${options.config}:`);
@@ -2857,8 +2947,8 @@ async function resolveConfig(options) {
2857
2947
  return cfg;
2858
2948
  }
2859
2949
  const localConfigPath = resolve4(process.cwd(), "biffo.config.json");
2860
- if (existsSync9(localConfigPath)) {
2861
- const raw = JSON.parse(readFileSync7(localConfigPath, "utf8"));
2950
+ if (existsSync10(localConfigPath)) {
2951
+ const raw = JSON.parse(readFileSync8(localConfigPath, "utf8"));
2862
2952
  const result = BiffoConfigSchema.safeParse(raw);
2863
2953
  if (result.success) return result.data;
2864
2954
  if (isTemplatePlaceholderConfig(raw)) {
@@ -2904,8 +2994,8 @@ async function resolveConfig(options) {
2904
2994
 
2905
2995
  // src/commands/data-import.ts
2906
2996
  import { execSync as execSync3 } from "child_process";
2907
- import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
2908
- import { join as join12, resolve as resolve5 } from "path";
2997
+ import { cpSync, existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
2998
+ import { join as join13, resolve as resolve5 } from "path";
2909
2999
  import chalk6 from "chalk";
2910
3000
  import { Command as Command6 } from "commander";
2911
3001
  import inquirer2 from "inquirer";
@@ -2945,23 +3035,23 @@ async function runDataImport(name, options, deps) {
2945
3035
  `Invalid import name '${name}'. Use lowercase letters, numbers, and hyphens, starting with a letter.`
2946
3036
  );
2947
3037
  }
2948
- const servicesDir = join12(options.cwd, "services");
2949
- if (!existsSync10(servicesDir)) {
3038
+ const servicesDir = join13(options.cwd, "services");
3039
+ if (!existsSync11(servicesDir)) {
2950
3040
  throw new Error(
2951
3041
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
2952
3042
  );
2953
3043
  }
2954
- const targetDir = join12(options.cwd, "db", "imports", name);
2955
- if (existsSync10(targetDir)) {
3044
+ const targetDir = join13(options.cwd, "db", "imports", name);
3045
+ if (existsSync11(targetDir)) {
2956
3046
  throw new Error(
2957
3047
  `DDL import '${name}' is already present at db/imports/${name}/. Remove it first to re-import.`
2958
3048
  );
2959
3049
  }
2960
- const isLocalDir = existsSync10(options.source) && statSync2(options.source).isDirectory();
3050
+ const isLocalDir = existsSync11(options.source) && statSync2(options.source).isDirectory();
2961
3051
  let sourceDir;
2962
3052
  let cleanupClone = null;
2963
3053
  if (isLocalDir) {
2964
- sourceDir = options.path ? join12(options.source, options.path) : options.source;
3054
+ sourceDir = options.path ? join13(options.source, options.path) : options.source;
2965
3055
  } else {
2966
3056
  const token = options.token ?? await resolveDdlImportToken();
2967
3057
  log.info(`Cloning ${options.source}...`);
@@ -2969,10 +3059,10 @@ async function runDataImport(name, options, deps) {
2969
3059
  cleanupClone = () => {
2970
3060
  deps.git.cleanup(tmpDir);
2971
3061
  };
2972
- sourceDir = options.path ? join12(tmpDir, options.path) : tmpDir;
3062
+ sourceDir = options.path ? join13(tmpDir, options.path) : tmpDir;
2973
3063
  }
2974
3064
  try {
2975
- if (!existsSync10(sourceDir)) {
3065
+ if (!existsSync11(sourceDir)) {
2976
3066
  throw new Error(`Source directory does not exist: ${sourceDir}`);
2977
3067
  }
2978
3068
  const sqlFiles = readdirSync5(sourceDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".sql")).map((entry) => entry.name).sort();
@@ -2997,7 +3087,7 @@ async function runDataImport(name, options, deps) {
2997
3087
  }
2998
3088
  mkdirSync5(targetDir, { recursive: true });
2999
3089
  for (const file of sqlFiles) {
3000
- cpSync(join12(sourceDir, file), join12(targetDir, file));
3090
+ cpSync(join13(sourceDir, file), join13(targetDir, file));
3001
3091
  }
3002
3092
  log.success(`Imported ${String(sqlFiles.length)} .sql file(s) to db/imports/${name}/`);
3003
3093
  const commitMessage = `feat(data): import ${name} (${String(sqlFiles.length)} SQL file(s))`;
@@ -3049,8 +3139,8 @@ function printDryRun(name, sqlFiles) {
3049
3139
  }
3050
3140
 
3051
3141
  // src/commands/data-list.ts
3052
- import { existsSync as existsSync11, readdirSync as readdirSync6 } from "fs";
3053
- import { join as join13, resolve as resolve6 } from "path";
3142
+ import { existsSync as existsSync12, readdirSync as readdirSync6 } from "fs";
3143
+ import { join as join14, resolve as resolve6 } from "path";
3054
3144
  import chalk7 from "chalk";
3055
3145
  import { Command as Command7 } from "commander";
3056
3146
  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) => {
@@ -3063,15 +3153,15 @@ var dataListCommand = new Command7("list").description("List DDL imports vendore
3063
3153
  }
3064
3154
  });
3065
3155
  async function runDataList(options) {
3066
- const importsDir = join13(options.cwd, "db", "imports");
3067
- if (!existsSync11(importsDir)) {
3156
+ const importsDir = join14(options.cwd, "db", "imports");
3157
+ if (!existsSync12(importsDir)) {
3068
3158
  console.log(chalk7.dim("\n No DDL imports in this checkout.\n"));
3069
3159
  return;
3070
3160
  }
3071
3161
  const candidates = readdirSync6(importsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
3072
3162
  const imports = [];
3073
3163
  for (const name of candidates) {
3074
- const fileCount = readdirSync6(join13(importsDir, name)).filter((f) => f.endsWith(".sql")).length;
3164
+ const fileCount = readdirSync6(join14(importsDir, name)).filter((f) => f.endsWith(".sql")).length;
3075
3165
  if (fileCount > 0) imports.push({ name, fileCount });
3076
3166
  }
3077
3167
  if (imports.length === 0) {
@@ -3101,7 +3191,7 @@ dataCommand.addCommand(dataListCommand);
3101
3191
 
3102
3192
  // src/commands/deploy.ts
3103
3193
  import { execSync as execSync4 } from "child_process";
3104
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
3194
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
3105
3195
  import { resolve as resolve7 } from "path";
3106
3196
  import chalk8 from "chalk";
3107
3197
  import { Command as Command9 } from "commander";
@@ -3455,7 +3545,7 @@ var deployCommand = new Command9("deploy").description("Deploy infrastructure an
3455
3545
  );
3456
3546
  async function resolveConfig2(options) {
3457
3547
  if (options.config) {
3458
- const raw = JSON.parse(readFileSync8(resolve7(options.config), "utf8"));
3548
+ const raw = JSON.parse(readFileSync9(resolve7(options.config), "utf8"));
3459
3549
  const result = BiffoConfigSchema.safeParse(raw);
3460
3550
  if (!result.success) {
3461
3551
  log.error(`Invalid config at ${options.config}:`);
@@ -3475,8 +3565,8 @@ async function resolveConfig2(options) {
3475
3565
  return cfg;
3476
3566
  }
3477
3567
  const localConfigPath = resolve7(process.cwd(), "biffo.config.json");
3478
- if (existsSync12(localConfigPath)) {
3479
- const raw = JSON.parse(readFileSync8(localConfigPath, "utf8"));
3568
+ if (existsSync13(localConfigPath)) {
3569
+ const raw = JSON.parse(readFileSync9(localConfigPath, "utf8"));
3480
3570
  const result = BiffoConfigSchema.safeParse(raw);
3481
3571
  if (result.success) return result.data;
3482
3572
  if (isTemplatePlaceholderConfig(raw)) {
@@ -3860,7 +3950,7 @@ function resolveGithubToken() {
3860
3950
 
3861
3951
  // src/commands/destroy.ts
3862
3952
  import { execSync as execSync5 } from "child_process";
3863
- import { readFileSync as readFileSync9 } from "fs";
3953
+ import { readFileSync as readFileSync10 } from "fs";
3864
3954
  import { resolve as resolve8 } from "path";
3865
3955
  import chalk9 from "chalk";
3866
3956
  import { Command as Command10 } from "commander";
@@ -3950,7 +4040,7 @@ var destroyCommand = new Command10("destroy").description("Destroy infrastructur
3950
4040
  });
3951
4041
  async function resolveConfig3(options) {
3952
4042
  if (options.config) {
3953
- const raw = JSON.parse(readFileSync9(resolve8(options.config), "utf8"));
4043
+ const raw = JSON.parse(readFileSync10(resolve8(options.config), "utf8"));
3954
4044
  const result = BiffoConfigSchema.safeParse(raw);
3955
4045
  if (!result.success) {
3956
4046
  log.error(`Invalid config at ${options.config}:`);
@@ -3970,7 +4060,7 @@ async function resolveConfig3(options) {
3970
4060
  return cfg;
3971
4061
  }
3972
4062
  try {
3973
- const raw = JSON.parse(readFileSync9(resolve8(process.cwd(), "biffo.config.json"), "utf8"));
4063
+ const raw = JSON.parse(readFileSync10(resolve8(process.cwd(), "biffo.config.json"), "utf8"));
3974
4064
  const result = BiffoConfigSchema.safeParse(raw);
3975
4065
  if (result.success) return result.data;
3976
4066
  } catch {
@@ -4020,15 +4110,15 @@ function resolveGithubToken2() {
4020
4110
  }
4021
4111
 
4022
4112
  // src/commands/init.ts
4023
- import { readFileSync as readFileSync13 } from "fs";
4113
+ import { readFileSync as readFileSync14 } from "fs";
4024
4114
  import { resolve as resolve10 } from "path";
4025
4115
  import chalk12 from "chalk";
4026
4116
  import { Command as Command12 } from "commander";
4027
4117
  import inquirer5 from "inquirer";
4028
4118
 
4029
4119
  // src/lib/build-freshness.ts
4030
- import { existsSync as existsSync13, readdirSync as readdirSync7, statSync as statSync3 } from "fs";
4031
- import { dirname as dirname5, join as join14, relative as relative2, sep as sep2 } from "path";
4120
+ import { existsSync as existsSync14, readdirSync as readdirSync7, statSync as statSync3 } from "fs";
4121
+ import { dirname as dirname5, join as join15, relative as relative2, sep as sep2 } from "path";
4032
4122
  import { fileURLToPath as fileURLToPath3 } from "url";
4033
4123
  var SKIP_ENV_VAR = "BIFFO_SKIP_BUILD_FRESHNESS_CHECK";
4034
4124
  function checkBuildFreshness(options = {}) {
@@ -4042,7 +4132,7 @@ function checkBuildFreshness(options = {}) {
4042
4132
  if (!packageRoot) {
4043
4133
  return { status: "skipped", reason: `no package.json above ${moduleDir}`, newerSources: [] };
4044
4134
  }
4045
- const distDir = join14(packageRoot, "dist");
4135
+ const distDir = join15(packageRoot, "dist");
4046
4136
  if (!isInside(distDir, moduleDir)) {
4047
4137
  return {
4048
4138
  status: "skipped",
@@ -4050,16 +4140,16 @@ function checkBuildFreshness(options = {}) {
4050
4140
  newerSources: []
4051
4141
  };
4052
4142
  }
4053
- const srcDir = join14(packageRoot, "src");
4054
- if (!existsSync13(srcDir)) {
4143
+ const srcDir = join15(packageRoot, "src");
4144
+ if (!existsSync14(srcDir)) {
4055
4145
  return {
4056
4146
  status: "skipped",
4057
4147
  reason: "no src/ alongside dist/ \u2014 this is a shipped package",
4058
4148
  newerSources: []
4059
4149
  };
4060
4150
  }
4061
- const entry = join14(distDir, "index.js");
4062
- if (!existsSync13(entry)) {
4151
+ const entry = join15(distDir, "index.js");
4152
+ if (!existsSync14(entry)) {
4063
4153
  return { status: "skipped", reason: `${entry} not found`, newerSources: [] };
4064
4154
  }
4065
4155
  const builtAt = statSync3(entry).mtimeMs;
@@ -4103,7 +4193,7 @@ function collectSourceFiles(srcDir) {
4103
4193
  const found = [];
4104
4194
  const walk = (dir) => {
4105
4195
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
4106
- const full = join14(dir, entry.name);
4196
+ const full = join15(dir, entry.name);
4107
4197
  if (entry.isDirectory()) {
4108
4198
  if (entry.name === "node_modules") continue;
4109
4199
  walk(full);
@@ -4122,7 +4212,7 @@ function collectSourceFiles(srcDir) {
4122
4212
  function findPackageRoot(from) {
4123
4213
  let dir = from;
4124
4214
  for (; ; ) {
4125
- if (existsSync13(join14(dir, "package.json"))) return dir;
4215
+ if (existsSync14(join15(dir, "package.json"))) return dir;
4126
4216
  const parent = dirname5(dir);
4127
4217
  if (parent === dir) return null;
4128
4218
  dir = parent;
@@ -4136,9 +4226,9 @@ function isInside(parent, child) {
4136
4226
 
4137
4227
  // src/lib/credentials.ts
4138
4228
  import { execSync as execSync6 } from "child_process";
4139
- import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
4229
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
4140
4230
  import { homedir as homedir2 } from "os";
4141
- import { join as join15 } from "path";
4231
+ import { join as join16 } from "path";
4142
4232
  import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
4143
4233
  import chalk10 from "chalk";
4144
4234
  import inquirer4 from "inquirer";
@@ -4317,11 +4407,11 @@ async function verifySelectedAwsCredentials(profile, region) {
4317
4407
  return sts.send(new GetCallerIdentityCommand2({}));
4318
4408
  }
4319
4409
  function discoverAwsProfiles() {
4320
- const files = [join15(homedir2(), ".aws", "credentials"), join15(homedir2(), ".aws", "config")];
4410
+ const files = [join16(homedir2(), ".aws", "credentials"), join16(homedir2(), ".aws", "config")];
4321
4411
  const profiles = /* @__PURE__ */ new Set();
4322
4412
  for (const file of files) {
4323
- if (!existsSync14(file)) continue;
4324
- const content = readFileSync10(file, "utf8");
4413
+ if (!existsSync15(file)) continue;
4414
+ const content = readFileSync11(file, "utf8");
4325
4415
  for (const match of content.matchAll(/^\s*\[([^\]]+)\]\s*$/gm)) {
4326
4416
  const section = match[1]?.trim();
4327
4417
  if (!section) continue;
@@ -4411,34 +4501,34 @@ var SiblingConfigSchema = z4.object({
4411
4501
 
4412
4502
  // src/lib/sibling-session.ts
4413
4503
  import {
4414
- existsSync as existsSync15,
4504
+ existsSync as existsSync16,
4415
4505
  mkdirSync as mkdirSync6,
4416
4506
  readdirSync as readdirSync8,
4417
- readFileSync as readFileSync11,
4507
+ readFileSync as readFileSync12,
4418
4508
  rmSync as rmSync6,
4419
4509
  statSync as statSync4,
4420
4510
  writeFileSync as writeFileSync6
4421
4511
  } from "fs";
4422
4512
  import { homedir as homedir3 } from "os";
4423
- import { join as join16 } from "path";
4513
+ import { join as join17 } from "path";
4424
4514
  function sessionsDir2() {
4425
- return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join16(homedir3(), ".biffo", "sibling-sessions");
4515
+ return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join17(homedir3(), ".biffo", "sibling-sessions");
4426
4516
  }
4427
4517
  function sessionPath2(projectName) {
4428
- return join16(sessionsDir2(), `${projectName}.json`);
4518
+ return join17(sessionsDir2(), `${projectName}.json`);
4429
4519
  }
4430
4520
  function loadSiblingSession(projectName) {
4431
4521
  const path = sessionPath2(projectName);
4432
- if (!existsSync15(path)) return null;
4522
+ if (!existsSync16(path)) return null;
4433
4523
  try {
4434
- return JSON.parse(readFileSync11(path, "utf8"));
4524
+ return JSON.parse(readFileSync12(path, "utf8"));
4435
4525
  } catch {
4436
4526
  return null;
4437
4527
  }
4438
4528
  }
4439
4529
  function saveSiblingSession(session) {
4440
4530
  const dir = sessionsDir2();
4441
- if (!existsSync15(dir)) mkdirSync6(dir, { recursive: true });
4531
+ if (!existsSync16(dir)) mkdirSync6(dir, { recursive: true });
4442
4532
  const name = session.config.project?.name ?? "unknown";
4443
4533
  const prior = loadSiblingSession(name);
4444
4534
  if (prior) {
@@ -4460,30 +4550,30 @@ function markSiblingStepComplete(session, step) {
4460
4550
  }
4461
4551
  function deleteSiblingSession(projectName) {
4462
4552
  const path = sessionPath2(projectName);
4463
- if (existsSync15(path)) rmSync6(path);
4553
+ if (existsSync16(path)) rmSync6(path);
4464
4554
  }
4465
4555
 
4466
4556
  // src/commands/sibling-create.ts
4467
- import { cpSync as cpSync2, existsSync as existsSync16, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
4557
+ import { cpSync as cpSync2, existsSync as existsSync17, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
4468
4558
  import { tmpdir as tmpdir4 } from "os";
4469
- import { dirname as dirname6, join as join18, resolve as resolve9 } from "path";
4559
+ import { dirname as dirname6, join as join19, resolve as resolve9 } from "path";
4470
4560
  import { fileURLToPath as fileURLToPath4 } from "url";
4471
4561
  import chalk11 from "chalk";
4472
4562
  import { Command as Command11 } from "commander";
4473
4563
 
4474
4564
  // src/lib/skeleton-dotfiles.ts
4475
4565
  import { readdirSync as readdirSync9, renameSync } from "fs";
4476
- import { join as join17 } from "path";
4566
+ import { join as join18 } from "path";
4477
4567
  var PACKAGED_GITIGNORE = "_gitignore";
4478
4568
  var REAL_GITIGNORE = ".gitignore";
4479
4569
  function restorePackagedDotfiles(dir) {
4480
4570
  const restored = [];
4481
4571
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
4482
- const full = join17(dir, entry.name);
4572
+ const full = join18(dir, entry.name);
4483
4573
  if (entry.isDirectory()) {
4484
4574
  restored.push(...restorePackagedDotfiles(full));
4485
4575
  } else if (entry.name === PACKAGED_GITIGNORE) {
4486
- const target = join17(dir, REAL_GITIGNORE);
4576
+ const target = join18(dir, REAL_GITIGNORE);
4487
4577
  renameSync(full, target);
4488
4578
  restored.push(target);
4489
4579
  }
@@ -4528,7 +4618,7 @@ async function runSiblingCreateCommand(name, options) {
4528
4618
  printDryRun2(config, coreConfig, options.templateRoot);
4529
4619
  return;
4530
4620
  }
4531
- if (!existsSync16(options.templateRoot)) {
4621
+ if (!existsSync17(options.templateRoot)) {
4532
4622
  throw new Error(`Sibling template not found at ${options.templateRoot}`);
4533
4623
  }
4534
4624
  let session = null;
@@ -4713,7 +4803,7 @@ function assertPathPrefixIsAllowed(pathPrefix) {
4713
4803
  }
4714
4804
  }
4715
4805
  function readSiblingConfig(path, root = false) {
4716
- const raw = JSON.parse(readFileSync12(path, "utf8"));
4806
+ const raw = JSON.parse(readFileSync13(path, "utf8"));
4717
4807
  const withDefaults = raw && typeof raw === "object" && "project" in raw && "core" in raw ? {
4718
4808
  ...raw,
4719
4809
  core: {
@@ -4747,7 +4837,7 @@ function resolveCoreConfig(config, configPath) {
4747
4837
  throw new Error("Either core.project_name or core.config_path is required.");
4748
4838
  }
4749
4839
  function parseCoreConfig(path) {
4750
- const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync12(path, "utf8")));
4840
+ const result = BiffoConfigSchema.safeParse(JSON.parse(readFileSync13(path, "utf8")));
4751
4841
  if (!result.success) {
4752
4842
  throw new Error(
4753
4843
  `Invalid core configuration at ${path}:
@@ -4786,7 +4876,7 @@ async function resolveCoreIdentity(coreAws, coreConfig, environments) {
4786
4876
  return coreIdentity;
4787
4877
  }
4788
4878
  async function pushSkeleton(git, skeletonRoot, cloneUrl, config, coreConfig, githubToken) {
4789
- const workDir = mkdtempSync4(join18(tmpdir4(), `biffo-sibling-${config.project.name}-`));
4879
+ const workDir = mkdtempSync4(join19(tmpdir4(), `biffo-sibling-${config.project.name}-`));
4790
4880
  try {
4791
4881
  writeSiblingTemplate(skeletonRoot, workDir, config, {
4792
4882
  coreProjectName: coreConfig.project.name,
@@ -4802,13 +4892,13 @@ async function pushSkeleton(git, skeletonRoot, cloneUrl, config, coreConfig, git
4802
4892
  }
4803
4893
  }
4804
4894
  function writeSiblingTemplate(templateRoot, targetDir, config, context) {
4805
- if (!existsSync16(templateRoot)) {
4895
+ if (!existsSync17(templateRoot)) {
4806
4896
  throw new Error(`Sibling template not found at ${templateRoot}`);
4807
4897
  }
4808
4898
  cpSync2(templateRoot, targetDir, { recursive: true });
4809
4899
  restorePackagedDotfiles(targetDir);
4810
4900
  writeFileSync7(
4811
- join18(targetDir, "biffo.sibling.json"),
4901
+ join19(targetDir, "biffo.sibling.json"),
4812
4902
  JSON.stringify(
4813
4903
  {
4814
4904
  name: config.project.name,
@@ -4824,10 +4914,10 @@ function writeSiblingTemplate(templateRoot, targetDir, config, context) {
4824
4914
  2
4825
4915
  ) + "\n"
4826
4916
  );
4827
- const envPath = join18(targetDir, "apps", "frontend", ".env.example");
4917
+ const envPath = join19(targetDir, "apps", "frontend", ".env.example");
4828
4918
  try {
4829
4919
  const path = basePathFor(context.pathPrefix);
4830
- const content = readFileSync12(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}`);
4920
+ 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}`);
4831
4921
  writeFileSync7(envPath, content);
4832
4922
  } catch (err) {
4833
4923
  if (err.code !== "ENOENT") throw err;
@@ -4873,17 +4963,17 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
4873
4963
  }
4874
4964
  function readExistingSiblingOrigins(filePath) {
4875
4965
  try {
4876
- return JSON.parse(readFileSync12(filePath, "utf8"));
4966
+ return JSON.parse(readFileSync13(filePath, "utf8"));
4877
4967
  } catch (err) {
4878
4968
  if (err.code === "ENOENT") return {};
4879
4969
  throw err;
4880
4970
  }
4881
4971
  }
4882
4972
  function assertCoreSupportsSiblingRouting(cloneDir, coreRepo, pathPrefix = "x") {
4883
- const cdnVarsPath = join18(cloneDir, "modules", "cloud", "aws", "cdn", "variables.tf");
4973
+ const cdnVarsPath = join19(cloneDir, "modules", "cloud", "aws", "cdn", "variables.tf");
4884
4974
  let declaresSiblingOrigins = false;
4885
4975
  try {
4886
- declaresSiblingOrigins = /variable\s+"sibling_origins"/.test(readFileSync12(cdnVarsPath, "utf8"));
4976
+ declaresSiblingOrigins = /variable\s+"sibling_origins"/.test(readFileSync13(cdnVarsPath, "utf8"));
4887
4977
  } catch {
4888
4978
  declaresSiblingOrigins = false;
4889
4979
  }
@@ -4893,10 +4983,10 @@ function assertCoreSupportsSiblingRouting(cloneDir, coreRepo, pathPrefix = "x")
4893
4983
  );
4894
4984
  }
4895
4985
  if (!isRootPathPrefix(pathPrefix)) return;
4896
- const cdnMainPath = join18(cloneDir, "modules", "cloud", "aws", "cdn", "main.tf");
4986
+ const cdnMainPath = join19(cloneDir, "modules", "cloud", "aws", "cdn", "main.tf");
4897
4987
  let supportsRoot = false;
4898
4988
  try {
4899
- supportsRoot = /root_sibling_registered/.test(readFileSync12(cdnMainPath, "utf8"));
4989
+ supportsRoot = /root_sibling_registered/.test(readFileSync13(cdnMainPath, "utf8"));
4900
4990
  } catch {
4901
4991
  supportsRoot = false;
4902
4992
  }
@@ -4926,8 +5016,8 @@ async function registerWithCore(git, github, config, coreConfig, pathPrefix, git
4926
5016
  for (const env of config.environments) {
4927
5017
  const bucketName = siteBucketName(config.project.name, env, siblingAccountId);
4928
5018
  const domain = bucketRegionalDomain(bucketName, coreAwsRegion);
4929
- const relativePath = join18("infra", "environments", env, "siblings.auto.tfvars.json");
4930
- const filePath = join18(cloneDir, relativePath);
5019
+ const relativePath = join19("infra", "environments", env, "siblings.auto.tfvars.json");
5020
+ const filePath = join19(cloneDir, relativePath);
4931
5021
  const existing = readExistingSiblingOrigins(filePath);
4932
5022
  const siblings = upsertSiblingOrigin(existing.sibling_origins ?? [], {
4933
5023
  name,
@@ -5007,8 +5097,8 @@ function defaultSiblingTemplateRoot() {
5007
5097
  const start = dirname6(fileURLToPath4(import.meta.url));
5008
5098
  let dir = start;
5009
5099
  for (; ; ) {
5010
- const candidate = join18(dir, "_skeletons", "sibling-template");
5011
- if (existsSync16(candidate)) return candidate;
5100
+ const candidate = join19(dir, "_skeletons", "sibling-template");
5101
+ if (existsSync17(candidate)) return candidate;
5012
5102
  const parent = dirname6(dir);
5013
5103
  if (parent === dir) break;
5014
5104
  dir = parent;
@@ -5032,7 +5122,7 @@ var initCommand = new Command12("init").description("Scaffold a new project from
5032
5122
  let config;
5033
5123
  let githubToken;
5034
5124
  if (options.config) {
5035
- const rawConfig = JSON.parse(readFileSync13(resolve10(options.config), "utf8"));
5125
+ const rawConfig = JSON.parse(readFileSync14(resolve10(options.config), "utf8"));
5036
5126
  config = parseConfig(rawConfig);
5037
5127
  const { account_id: accountId, region } = config.cloud.config;
5038
5128
  session = resolveConfigFileSession(config, accountId, region, options.fresh === true);
@@ -5465,28 +5555,28 @@ async function promptForConfig(awsAccountId, awsRegion, awsProfile) {
5465
5555
  import { Command as Command20 } from "commander";
5466
5556
 
5467
5557
  // src/commands/plugin-create.ts
5468
- import { existsSync as existsSync19, readFileSync as readFileSync15 } from "fs";
5469
- import { dirname as dirname8, join as join21, resolve as resolve11 } from "path";
5558
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
5559
+ import { dirname as dirname8, join as join22, resolve as resolve11 } from "path";
5470
5560
  import { fileURLToPath as fileURLToPath5 } from "url";
5471
5561
  import chalk13 from "chalk";
5472
5562
  import { Command as Command13 } from "commander";
5473
5563
 
5474
5564
  // src/lib/plugin-locations.ts
5475
- import { existsSync as existsSync17, readdirSync as readdirSync10 } from "fs";
5476
- import { join as join19 } from "path";
5565
+ import { existsSync as existsSync18, readdirSync as readdirSync10 } from "fs";
5566
+ import { join as join20 } from "path";
5477
5567
  var FIRST_PARTY_PLUGINS_DIR = "_plugins";
5478
5568
  var PLUGIN_MANIFEST_FILE = "biffo.plugin.json";
5479
5569
  function pluginDir(name, channel) {
5480
5570
  return channel === "first-party" ? `services/${FIRST_PARTY_PLUGINS_DIR}/${name}` : `services/${name}`;
5481
5571
  }
5482
5572
  function scanDir(absDir, relDir, channel) {
5483
- if (!existsSync17(absDir)) return [];
5573
+ if (!existsSync18(absDir)) return [];
5484
5574
  const found = [];
5485
5575
  for (const entry of readdirSync10(absDir, { withFileTypes: true })) {
5486
5576
  if (!entry.isDirectory()) continue;
5487
5577
  if (channel === "third-party" && entry.name === FIRST_PARTY_PLUGINS_DIR) continue;
5488
- const manifestPath = join19(absDir, entry.name, PLUGIN_MANIFEST_FILE);
5489
- if (!existsSync17(manifestPath)) continue;
5578
+ const manifestPath = join20(absDir, entry.name, PLUGIN_MANIFEST_FILE);
5579
+ if (!existsSync18(manifestPath)) continue;
5490
5580
  found.push({
5491
5581
  dirName: entry.name,
5492
5582
  relDir: `${relDir}/${entry.name}`,
@@ -5497,11 +5587,11 @@ function scanDir(absDir, relDir, channel) {
5497
5587
  return found;
5498
5588
  }
5499
5589
  function findInstalledPlugins(cwd) {
5500
- const servicesDir = join19(cwd, "services");
5590
+ const servicesDir = join20(cwd, "services");
5501
5591
  return [
5502
5592
  ...scanDir(servicesDir, "services", "third-party"),
5503
5593
  ...scanDir(
5504
- join19(servicesDir, FIRST_PARTY_PLUGINS_DIR),
5594
+ join20(servicesDir, FIRST_PARTY_PLUGINS_DIR),
5505
5595
  `services/${FIRST_PARTY_PLUGINS_DIR}`,
5506
5596
  "first-party"
5507
5597
  )
@@ -5661,13 +5751,13 @@ function validateManifest(raw) {
5661
5751
  // src/lib/plugin-scaffold.ts
5662
5752
  import {
5663
5753
  copyFileSync,
5664
- existsSync as existsSync18,
5754
+ existsSync as existsSync19,
5665
5755
  mkdirSync as mkdirSync8,
5666
- readFileSync as readFileSync14,
5756
+ readFileSync as readFileSync15,
5667
5757
  readdirSync as readdirSync11,
5668
5758
  writeFileSync as writeFileSync8
5669
5759
  } from "fs";
5670
- import { dirname as dirname7, join as join20 } from "path";
5760
+ import { dirname as dirname7, join as join21 } from "path";
5671
5761
  var STANDALONE_ONLY_ENTRIES = {
5672
5762
  ".github": "standalone-repo CI/release workflows \u2014 the host monorepo already runs lint/type/test/security over services/",
5673
5763
  "registry-schema.json": "the plugin-registry publishing schema, used when submitting a *published* plugin to the registry repo, not by an in-tree plugin"
@@ -5720,10 +5810,10 @@ function applySubstitutions(text, names) {
5720
5810
  }
5721
5811
  var BINARY_EXTENSIONS = /\.(png|jpe?g|gif|ico|woff2?|ttf|zip|gz)$/i;
5722
5812
  function scaffoldPlugin(skeletonRoot, destDir, names) {
5723
- if (!existsSync18(skeletonRoot)) {
5813
+ if (!existsSync19(skeletonRoot)) {
5724
5814
  throw new Error(`Plugin skeleton not found at ${skeletonRoot}`);
5725
5815
  }
5726
- if (!existsSync18(join20(skeletonRoot, "terraform"))) {
5816
+ if (!existsSync19(join21(skeletonRoot, "terraform"))) {
5727
5817
  throw new Error(
5728
5818
  `Plugin skeleton at ${skeletonRoot} has no terraform/ directory. Refusing to scaffold a plugin that cannot receive events (issue #194) \u2014 the skeleton is broken.`
5729
5819
  );
@@ -5731,7 +5821,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
5731
5821
  const skipped = [];
5732
5822
  const files = [];
5733
5823
  const walk = (relDir) => {
5734
- const absDir = join20(skeletonRoot, relDir);
5824
+ const absDir = join21(skeletonRoot, relDir);
5735
5825
  for (const entry of readdirSync11(absDir, { withFileTypes: true }).sort(
5736
5826
  (a, b) => a.name.localeCompare(b.name)
5737
5827
  )) {
@@ -5746,14 +5836,14 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
5746
5836
  continue;
5747
5837
  }
5748
5838
  const destRel = applySubstitutions(relPath, names);
5749
- const destPath = join20(destDir, destRel);
5839
+ const destPath = join21(destDir, destRel);
5750
5840
  mkdirSync8(dirname7(destPath), { recursive: true });
5751
5841
  if (BINARY_EXTENSIONS.test(entry.name)) {
5752
- copyFileSync(join20(skeletonRoot, relPath), destPath);
5842
+ copyFileSync(join21(skeletonRoot, relPath), destPath);
5753
5843
  } else {
5754
5844
  writeFileSync8(
5755
5845
  destPath,
5756
- applySubstitutions(readFileSync14(join20(skeletonRoot, relPath), "utf8"), names)
5846
+ applySubstitutions(readFileSync15(join21(skeletonRoot, relPath), "utf8"), names)
5757
5847
  );
5758
5848
  }
5759
5849
  files.push(destRel);
@@ -5770,8 +5860,8 @@ function scaffoldPlugin(skeletonRoot, destDir, names) {
5770
5860
  function findSkeletonRoot(startDir, skeleton) {
5771
5861
  let dir = startDir;
5772
5862
  for (; ; ) {
5773
- const candidate = join20(dir, "_skeletons", skeleton);
5774
- if (existsSync18(candidate)) return candidate;
5863
+ const candidate = join21(dir, "_skeletons", skeleton);
5864
+ if (existsSync19(candidate)) return candidate;
5775
5865
  const parent = dirname7(dir);
5776
5866
  if (parent === dir) return null;
5777
5867
  dir = parent;
@@ -5808,7 +5898,7 @@ var pluginCreateCommand = new Command13("create").description("Scaffold a new pl
5808
5898
  );
5809
5899
  async function runPluginCreate(name, options, deps) {
5810
5900
  const names = deriveNames(name);
5811
- const isInstance = existsSync19(join21(options.cwd, INSTANCE_CORE_FILE));
5901
+ const isInstance = existsSync20(join22(options.cwd, INSTANCE_CORE_FILE));
5812
5902
  if (options.firstParty && isInstance) {
5813
5903
  throw new Error(
5814
5904
  `--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.`
@@ -5816,19 +5906,19 @@ async function runPluginCreate(name, options, deps) {
5816
5906
  }
5817
5907
  const channel = options.firstParty ? "first-party" : "third-party";
5818
5908
  const relDir = pluginDir(names.slug, channel);
5819
- const destDir = join21(options.cwd, relDir);
5820
- const servicesDir = join21(options.cwd, "services");
5821
- if (!existsSync19(servicesDir)) {
5909
+ const destDir = join22(options.cwd, relDir);
5910
+ const servicesDir = join22(options.cwd, "services");
5911
+ if (!existsSync20(servicesDir)) {
5822
5912
  throw new Error(
5823
5913
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
5824
5914
  );
5825
5915
  }
5826
- if (existsSync19(destDir)) {
5916
+ if (existsSync20(destDir)) {
5827
5917
  throw new Error(`${relDir}/ already exists. Choose a different name, or remove it first.`);
5828
5918
  }
5829
5919
  const here = dirname8(fileURLToPath5(import.meta.url));
5830
- const skeletonRoot = options.skeletonRoot ?? findSkeletonRoot(here, "plugin-template") ?? join21(options.cwd, "_skeletons", "plugin-template");
5831
- if (!existsSync19(skeletonRoot)) {
5920
+ const skeletonRoot = options.skeletonRoot ?? findSkeletonRoot(here, "plugin-template") ?? join22(options.cwd, "_skeletons", "plugin-template");
5921
+ if (!existsSync20(skeletonRoot)) {
5832
5922
  throw new Error(
5833
5923
  `Could not find the plugin skeleton (_skeletons/plugin-template/). Pass --skeleton <path> to point at it explicitly.`
5834
5924
  );
@@ -5843,8 +5933,8 @@ async function runPluginCreate(name, options, deps) {
5843
5933
  for (const { entry, reason } of skipped) {
5844
5934
  log.info(`Skipped ${entry} \u2014 ${reason}`);
5845
5935
  }
5846
- const manifestPath = join21(destDir, "biffo.plugin.json");
5847
- const manifest = validateManifest(JSON.parse(readFileSync15(manifestPath, "utf8")));
5936
+ const manifestPath = join22(destDir, "biffo.plugin.json");
5937
+ const manifest = validateManifest(JSON.parse(readFileSync16(manifestPath, "utf8")));
5848
5938
  if (manifest.name !== names.slug) {
5849
5939
  throw new Error(
5850
5940
  `Scaffolded manifest declares name '${manifest.name}', expected '${names.slug}'. The skeleton's manifest name may have diverged from 'example-plugin'.`
@@ -6034,14 +6124,14 @@ function printEntry(entry) {
6034
6124
  }
6035
6125
 
6036
6126
  // src/commands/plugin-install.ts
6037
- import { cpSync as cpSync3, existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
6038
- import { basename, join as join23, relative as relative3, resolve as resolve12 } from "path";
6127
+ import { cpSync as cpSync3, existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync17, statSync as statSync5 } from "fs";
6128
+ import { basename, join as join24, relative as relative3, resolve as resolve12 } from "path";
6039
6129
  import chalk15 from "chalk";
6040
6130
  import { Command as Command15 } from "commander";
6041
6131
 
6042
6132
  // src/adapters/plugin-migrations/index.ts
6043
6133
  import { execa as execa4 } from "execa";
6044
- import { join as join22 } from "path";
6134
+ import { join as join23 } from "path";
6045
6135
  var PluginMigrationsAdapter = class {
6046
6136
  /**
6047
6137
  * Generates migration file(s) for `pluginNames` (every discovered
@@ -6050,22 +6140,22 @@ var PluginMigrationsAdapter = class {
6050
6140
  * or declared no tables.
6051
6141
  */
6052
6142
  async generate(cwd, pluginNames) {
6053
- const scriptPath = join22(cwd, "services", "api", "scripts", "generate_plugin_migrations.py");
6143
+ const scriptPath = join23(cwd, "services", "api", "scripts", "generate_plugin_migrations.py");
6054
6144
  const args = [
6055
6145
  "run",
6056
6146
  "python",
6057
6147
  scriptPath,
6058
6148
  "--services-root",
6059
- join22(cwd, "services"),
6149
+ join23(cwd, "services"),
6060
6150
  "--versions-dir",
6061
- join22(cwd, "services", "api", "migrations", "versions")
6151
+ join23(cwd, "services", "api", "migrations", "versions")
6062
6152
  ];
6063
6153
  for (const name of pluginNames ?? []) {
6064
6154
  args.push("--plugin", name);
6065
6155
  }
6066
6156
  let result;
6067
6157
  try {
6068
- result = await execa4("uv", args, { cwd: join22(cwd, "services", "api") });
6158
+ result = await execa4("uv", args, { cwd: join23(cwd, "services", "api") });
6069
6159
  } catch (err) {
6070
6160
  const cause = err;
6071
6161
  if (cause.code === "ENOENT") {
@@ -6123,14 +6213,14 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
6123
6213
  ".terraform"
6124
6214
  ]);
6125
6215
  function resolveLocalPlugin(localPath) {
6126
- if (!existsSync20(localPath)) {
6216
+ if (!existsSync21(localPath)) {
6127
6217
  throw new Error(`--local path does not exist: ${localPath}`);
6128
6218
  }
6129
6219
  if (!statSync5(localPath).isDirectory()) {
6130
6220
  throw new Error(`--local path is not a directory: ${localPath}`);
6131
6221
  }
6132
- const manifestPath = join23(localPath, "biffo.plugin.json");
6133
- if (!existsSync20(manifestPath)) {
6222
+ const manifestPath = join24(localPath, "biffo.plugin.json");
6223
+ if (!existsSync21(manifestPath)) {
6134
6224
  throw new Error(
6135
6225
  `${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>\`.)`
6136
6226
  );
@@ -6156,8 +6246,8 @@ function parsePluginTarget(target) {
6156
6246
  async function cloneAndValidatePlugin(entry, git) {
6157
6247
  const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
6158
6248
  try {
6159
- const manifestPath = join23(tmpDir, "biffo.plugin.json");
6160
- if (!existsSync20(manifestPath)) {
6249
+ const manifestPath = join24(tmpDir, "biffo.plugin.json");
6250
+ if (!existsSync21(manifestPath)) {
6161
6251
  throw new Error(
6162
6252
  `Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
6163
6253
  );
@@ -6185,8 +6275,8 @@ async function runPluginInstall(target, options, deps) {
6185
6275
  `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\`).`
6186
6276
  );
6187
6277
  }
6188
- const servicesDir = join23(options.cwd, "services");
6189
- if (!existsSync20(servicesDir)) {
6278
+ const servicesDir = join24(options.cwd, "services");
6279
+ if (!existsSync21(servicesDir)) {
6190
6280
  throw new Error(
6191
6281
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6192
6282
  );
@@ -6204,10 +6294,10 @@ async function runPluginInstall(target, options, deps) {
6204
6294
  }
6205
6295
  const pluginName = entry ? entry.name : source.name;
6206
6296
  const relTargetDir = pluginDir(pluginName, "third-party");
6207
- const targetDir = join23(options.cwd, relTargetDir);
6208
- const modulesDir = join23(options.cwd, "modules", "plugins", pluginName);
6297
+ const targetDir = join24(options.cwd, relTargetDir);
6298
+ const modulesDir = join24(options.cwd, "modules", "plugins", pluginName);
6209
6299
  const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
6210
- if (existsSync20(targetDir) && !inTreeSource) {
6300
+ if (existsSync21(targetDir) && !inTreeSource) {
6211
6301
  throw new Error(
6212
6302
  `Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
6213
6303
  );
@@ -6250,8 +6340,8 @@ async function runPluginInstall(target, options, deps) {
6250
6340
  log.success(`Installed plugin source at ${relTargetDir}/`);
6251
6341
  }
6252
6342
  const stagePaths = [relTargetDir];
6253
- const tfSourceDir = join23(targetDir, "terraform");
6254
- if (existsSync20(tfSourceDir)) {
6343
+ const tfSourceDir = join24(targetDir, "terraform");
6344
+ if (existsSync21(tfSourceDir)) {
6255
6345
  mkdirSync9(modulesDir, { recursive: true });
6256
6346
  cpSync3(tfSourceDir, modulesDir, { recursive: true });
6257
6347
  stagePaths.push(`modules/plugins/${pluginName}`);
@@ -6308,7 +6398,7 @@ async function runPluginInstall(target, options, deps) {
6308
6398
  }
6309
6399
  function parseManifestFile(path) {
6310
6400
  try {
6311
- return JSON.parse(readFileSync16(path, "utf8"));
6401
+ return JSON.parse(readFileSync17(path, "utf8"));
6312
6402
  } catch (err) {
6313
6403
  throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
6314
6404
  }
@@ -6345,8 +6435,8 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
6345
6435
  }
6346
6436
 
6347
6437
  // src/commands/plugin-list.ts
6348
- import { existsSync as existsSync21, readFileSync as readFileSync17 } from "fs";
6349
- import { join as join24, resolve as resolve13 } from "path";
6438
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
6439
+ import { join as join25, resolve as resolve13 } from "path";
6350
6440
  import chalk16 from "chalk";
6351
6441
  import { Command as Command16 } from "commander";
6352
6442
  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) => {
@@ -6359,8 +6449,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
6359
6449
  }
6360
6450
  });
6361
6451
  async function runPluginList(options) {
6362
- const servicesDir = join24(options.cwd, "services");
6363
- if (!existsSync21(servicesDir)) {
6452
+ const servicesDir = join25(options.cwd, "services");
6453
+ if (!existsSync22(servicesDir)) {
6364
6454
  throw new Error(
6365
6455
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6366
6456
  );
@@ -6368,7 +6458,7 @@ async function runPluginList(options) {
6368
6458
  const plugins = [];
6369
6459
  for (const location of findInstalledPlugins(options.cwd)) {
6370
6460
  try {
6371
- const manifest = validateManifest(JSON.parse(readFileSync17(location.manifestPath, "utf8")));
6461
+ const manifest = validateManifest(JSON.parse(readFileSync18(location.manifestPath, "utf8")));
6372
6462
  plugins.push({
6373
6463
  name: manifest.name,
6374
6464
  version: manifest.version,
@@ -6405,8 +6495,8 @@ async function runPluginList(options) {
6405
6495
  }
6406
6496
 
6407
6497
  // src/commands/plugin-sync-migrations.ts
6408
- import { existsSync as existsSync22 } from "fs";
6409
- import { join as join25, relative as relative4, resolve as resolve14 } from "path";
6498
+ import { existsSync as existsSync23 } from "fs";
6499
+ import { join as join26, relative as relative4, resolve as resolve14 } from "path";
6410
6500
  import chalk17 from "chalk";
6411
6501
  import { Command as Command17 } from "commander";
6412
6502
  var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
@@ -6427,11 +6517,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
6427
6517
  }
6428
6518
  );
6429
6519
  async function runPluginSyncMigrations(name, options, deps) {
6430
- const servicesDir = join25(options.cwd, "services");
6431
- if (!existsSync22(servicesDir)) {
6520
+ const servicesDir = join26(options.cwd, "services");
6521
+ if (!existsSync23(servicesDir)) {
6432
6522
  throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
6433
6523
  }
6434
- if (name && !existsSync22(join25(servicesDir, name, "biffo.plugin.json"))) {
6524
+ if (name && !existsSync23(join26(servicesDir, name, "biffo.plugin.json"))) {
6435
6525
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
6436
6526
  }
6437
6527
  if (options.dryRun) {
@@ -6467,8 +6557,8 @@ async function runPluginSyncMigrations(name, options, deps) {
6467
6557
  }
6468
6558
 
6469
6559
  // src/commands/plugin-uninstall.ts
6470
- import { existsSync as existsSync23, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
6471
- import { join as join26, resolve as resolve15 } from "path";
6560
+ import { existsSync as existsSync24, readFileSync as readFileSync19, rmSync as rmSync7 } from "fs";
6561
+ import { join as join27, resolve as resolve15 } from "path";
6472
6562
  import chalk18 from "chalk";
6473
6563
  import { Command as Command18 } from "commander";
6474
6564
  import inquirer6 from "inquirer";
@@ -6500,16 +6590,16 @@ async function runPluginUninstall(name, options, deps) {
6500
6590
  if (!NAME_PATTERN2.test(name)) {
6501
6591
  throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
6502
6592
  }
6503
- const servicesDir = join26(options.cwd, "services");
6504
- if (!existsSync23(servicesDir)) {
6593
+ const servicesDir = join27(options.cwd, "services");
6594
+ if (!existsSync24(servicesDir)) {
6505
6595
  throw new Error(
6506
6596
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6507
6597
  );
6508
6598
  }
6509
- const targetDir = join26(servicesDir, name);
6510
- if (!existsSync23(targetDir)) {
6511
- const firstParty = join26(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
6512
- if (existsSync23(firstParty)) {
6599
+ const targetDir = join27(servicesDir, name);
6600
+ if (!existsSync24(targetDir)) {
6601
+ const firstParty = join27(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
6602
+ if (existsSync24(firstParty)) {
6513
6603
  throw new Error(
6514
6604
  `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.`
6515
6605
  );
@@ -6517,9 +6607,9 @@ async function runPluginUninstall(name, options, deps) {
6517
6607
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
6518
6608
  }
6519
6609
  const version = readInstalledVersion(targetDir);
6520
- const modulesDir = join26(options.cwd, "modules", "plugins", name);
6610
+ const modulesDir = join27(options.cwd, "modules", "plugins", name);
6521
6611
  const stagePaths = [`services/${name}`];
6522
- if (existsSync23(modulesDir)) {
6612
+ if (existsSync24(modulesDir)) {
6523
6613
  stagePaths.push(`modules/plugins/${name}`);
6524
6614
  }
6525
6615
  if (options.dryRun) {
@@ -6541,7 +6631,7 @@ async function runPluginUninstall(name, options, deps) {
6541
6631
  }
6542
6632
  rmSync7(targetDir, { recursive: true, force: true });
6543
6633
  log.success(`Removed services/${name}/`);
6544
- if (existsSync23(modulesDir)) {
6634
+ if (existsSync24(modulesDir)) {
6545
6635
  rmSync7(modulesDir, { recursive: true, force: true });
6546
6636
  log.success(`Removed modules/plugins/${name}/`);
6547
6637
  const wiring = syncPluginTerraform(options.cwd);
@@ -6578,10 +6668,10 @@ async function runPluginUninstall(name, options, deps) {
6578
6668
  }
6579
6669
  }
6580
6670
  function readInstalledVersion(targetDir) {
6581
- const manifestPath = join26(targetDir, "biffo.plugin.json");
6582
- if (!existsSync23(manifestPath)) return void 0;
6671
+ const manifestPath = join27(targetDir, "biffo.plugin.json");
6672
+ if (!existsSync24(manifestPath)) return void 0;
6583
6673
  try {
6584
- return validateManifest(JSON.parse(readFileSync18(manifestPath, "utf8"))).version;
6674
+ return validateManifest(JSON.parse(readFileSync19(manifestPath, "utf8"))).version;
6585
6675
  } catch {
6586
6676
  return void 0;
6587
6677
  }
@@ -6614,8 +6704,8 @@ function printDryRun5(name, version, stagePaths, keepData) {
6614
6704
  }
6615
6705
 
6616
6706
  // src/commands/plugin-upgrade.ts
6617
- import { cpSync as cpSync4, existsSync as existsSync24, mkdirSync as mkdirSync10, readFileSync as readFileSync19, rmSync as rmSync8 } from "fs";
6618
- import { join as join27, relative as relative5, resolve as resolve16 } from "path";
6707
+ import { cpSync as cpSync4, existsSync as existsSync25, mkdirSync as mkdirSync10, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
6708
+ import { join as join28, relative as relative5, resolve as resolve16 } from "path";
6619
6709
  import chalk19 from "chalk";
6620
6710
  import { Command as Command19 } from "commander";
6621
6711
  import inquirer7 from "inquirer";
@@ -6640,14 +6730,14 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
6640
6730
  });
6641
6731
  async function runPluginUpgrade(target, options, deps) {
6642
6732
  const { name, minor } = parsePluginTarget(target);
6643
- const servicesDir = join27(options.cwd, "services");
6644
- if (!existsSync24(servicesDir)) {
6733
+ const servicesDir = join28(options.cwd, "services");
6734
+ if (!existsSync25(servicesDir)) {
6645
6735
  throw new Error(
6646
6736
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
6647
6737
  );
6648
6738
  }
6649
- const targetDir = join27(servicesDir, name);
6650
- if (!existsSync24(targetDir)) {
6739
+ const targetDir = join28(servicesDir, name);
6740
+ if (!existsSync25(targetDir)) {
6651
6741
  throw new Error(
6652
6742
  `Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
6653
6743
  );
@@ -6661,7 +6751,7 @@ async function runPluginUpgrade(target, options, deps) {
6661
6751
  `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.`
6662
6752
  );
6663
6753
  }
6664
- const modulesDir = join27(options.cwd, "modules", "plugins", entry.name);
6754
+ const modulesDir = join28(options.cwd, "modules", "plugins", entry.name);
6665
6755
  if (options.dryRun) {
6666
6756
  printDryRun6(entry, currentVersion);
6667
6757
  return;
@@ -6694,11 +6784,11 @@ async function runPluginUpgrade(target, options, deps) {
6694
6784
  cpSync4(tmpDir, targetDir, { recursive: true });
6695
6785
  log.success(`Upgraded plugin source at services/${entry.name}/`);
6696
6786
  const stagePaths = [`services/${entry.name}`];
6697
- if (existsSync24(modulesDir)) {
6787
+ if (existsSync25(modulesDir)) {
6698
6788
  rmSync8(modulesDir, { recursive: true, force: true });
6699
6789
  }
6700
- const tfSourceDir = join27(targetDir, "terraform");
6701
- if (existsSync24(tfSourceDir)) {
6790
+ const tfSourceDir = join28(targetDir, "terraform");
6791
+ if (existsSync25(tfSourceDir)) {
6702
6792
  mkdirSync10(modulesDir, { recursive: true });
6703
6793
  cpSync4(tfSourceDir, modulesDir, { recursive: true });
6704
6794
  stagePaths.push(`modules/plugins/${entry.name}`);
@@ -6732,10 +6822,10 @@ async function runPluginUpgrade(target, options, deps) {
6732
6822
  }
6733
6823
  }
6734
6824
  function readInstalledVersion2(targetDir) {
6735
- const manifestPath = join27(targetDir, "biffo.plugin.json");
6736
- if (!existsSync24(manifestPath)) return void 0;
6825
+ const manifestPath = join28(targetDir, "biffo.plugin.json");
6826
+ if (!existsSync25(manifestPath)) return void 0;
6737
6827
  try {
6738
- return validateManifest(JSON.parse(readFileSync19(manifestPath, "utf8"))).version;
6828
+ return validateManifest(JSON.parse(readFileSync20(manifestPath, "utf8"))).version;
6739
6829
  } catch {
6740
6830
  return void 0;
6741
6831
  }