@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.
- package/dist/index.js +516 -438
- 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
|
|
436
|
-
import { join as
|
|
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
|
|
1588
|
+
existsSync as existsSync7,
|
|
1589
1589
|
mkdirSync as mkdirSync2,
|
|
1590
1590
|
mkdtempSync as mkdtempSync2,
|
|
1591
|
-
readFileSync as
|
|
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
|
|
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(
|
|
1714
|
+
const dir = mkdtempSync2(join7(tmpdir2(), "biffo-merge-"));
|
|
1601
1715
|
try {
|
|
1602
|
-
const b =
|
|
1603
|
-
const o =
|
|
1604
|
-
const t =
|
|
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
|
|
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(
|
|
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 (
|
|
1675
|
-
|
|
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 =
|
|
1828
|
+
const abs = join7(instanceDir, e.path);
|
|
1697
1829
|
if (e.status === "removed") {
|
|
1698
|
-
if (
|
|
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 =
|
|
1709
|
-
if (
|
|
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
|
|
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(
|
|
1774
|
-
const tarball =
|
|
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
|
|
1788
|
-
import { join as
|
|
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 =
|
|
1814
|
-
if (!
|
|
1815
|
-
return parseBreakingChanges(
|
|
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
|
|
1833
|
-
import { join as
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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() || !
|
|
1897
|
-
return declaredVariables(
|
|
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 =
|
|
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() &&
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
2010
|
-
const tfPath =
|
|
2011
|
-
const tfvarsPath =
|
|
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 (
|
|
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
|
|
2035
|
-
import { join as
|
|
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 &&
|
|
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 &&
|
|
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 =
|
|
2529
|
-
if (
|
|
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 =
|
|
2538
|
-
if (
|
|
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
|
|
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
|
|
2917
|
-
var AwsConfigSchema =
|
|
2918
|
-
account_id:
|
|
2919
|
-
region:
|
|
2920
|
-
profile:
|
|
2921
|
-
oidc_role_arn:
|
|
2922
|
-
tf_state_bucket:
|
|
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 =
|
|
2925
|
-
org:
|
|
2926
|
-
repo:
|
|
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 =
|
|
2929
|
-
|
|
3075
|
+
var SourceControlConfigSchema = z4.discriminatedUnion("provider", [
|
|
3076
|
+
z4.object({ provider: z4.literal("github"), config: GitHubConfigSchema })
|
|
2930
3077
|
]);
|
|
2931
|
-
var CloudConfigSchema =
|
|
2932
|
-
|
|
3078
|
+
var CloudConfigSchema = z4.discriminatedUnion("provider", [
|
|
3079
|
+
z4.object({ provider: z4.literal("aws"), config: AwsConfigSchema })
|
|
2933
3080
|
]);
|
|
2934
|
-
var ModulesSchema =
|
|
2935
|
-
auth:
|
|
2936
|
-
events:
|
|
2937
|
-
storage:
|
|
2938
|
-
database:
|
|
2939
|
-
compute:
|
|
2940
|
-
cdn:
|
|
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 =
|
|
2943
|
-
mode:
|
|
2944
|
-
domain:
|
|
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 =
|
|
2947
|
-
$schema:
|
|
2948
|
-
project:
|
|
2949
|
-
name:
|
|
2950
|
-
description:
|
|
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:
|
|
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:
|
|
2958
|
-
admin:
|
|
2959
|
-
email:
|
|
2960
|
-
username:
|
|
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:
|
|
2963
|
-
schema_path:
|
|
2964
|
-
migrations_path:
|
|
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:
|
|
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
|
|
3154
|
+
existsSync as existsSync12,
|
|
3008
3155
|
mkdirSync as mkdirSync4,
|
|
3009
3156
|
readdirSync as readdirSync4,
|
|
3010
|
-
readFileSync as
|
|
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
|
|
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"] ??
|
|
3172
|
+
return process.env["BIFFO_SESSIONS_DIR"] ?? join13(homedir(), ".biffo", "sessions");
|
|
3026
3173
|
}
|
|
3027
3174
|
function sessionPath(projectName) {
|
|
3028
|
-
return
|
|
3175
|
+
return join13(sessionsDir(), `${projectName}.json`);
|
|
3029
3176
|
}
|
|
3030
3177
|
function loadSession(projectName) {
|
|
3031
3178
|
const path = sessionPath(projectName);
|
|
3032
|
-
if (!
|
|
3179
|
+
if (!existsSync12(path)) return null;
|
|
3033
3180
|
try {
|
|
3034
|
-
return JSON.parse(
|
|
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 (!
|
|
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 =
|
|
3046
|
-
const mtime =
|
|
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(
|
|
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 (!
|
|
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 (
|
|
3226
|
+
if (existsSync12(path)) rmSync6(path);
|
|
3080
3227
|
}
|
|
3081
3228
|
function projectsDir() {
|
|
3082
|
-
return process.env["BIFFO_PROJECTS_DIR"] ??
|
|
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 (!
|
|
3087
|
-
writeFileSync5(
|
|
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 =
|
|
3091
|
-
if (!
|
|
3237
|
+
const path = join13(projectsDir(), `${name}.json`);
|
|
3238
|
+
if (!existsSync12(path)) return null;
|
|
3092
3239
|
try {
|
|
3093
|
-
const result = BiffoConfigSchema.safeParse(JSON.parse(
|
|
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 =
|
|
3101
|
-
if (
|
|
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 (!
|
|
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(
|
|
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(
|
|
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 (
|
|
3198
|
-
const raw = JSON.parse(
|
|
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
|
|
3245
|
-
import { join as
|
|
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 =
|
|
3286
|
-
if (!
|
|
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 =
|
|
3292
|
-
if (
|
|
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 =
|
|
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 ?
|
|
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 ?
|
|
3456
|
+
sourceDir = options.path ? join14(tmpDir, options.path) : tmpDir;
|
|
3310
3457
|
}
|
|
3311
3458
|
try {
|
|
3312
|
-
if (!
|
|
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(
|
|
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
|
|
3390
|
-
import { join as
|
|
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 =
|
|
3404
|
-
if (!
|
|
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(
|
|
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
|
|
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(
|
|
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 (
|
|
3816
|
-
const raw = JSON.parse(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
4368
|
-
import { dirname as dirname5, join as
|
|
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 =
|
|
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 =
|
|
4391
|
-
if (!
|
|
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 =
|
|
4399
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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
|
|
4623
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
|
|
4477
4624
|
import { homedir as homedir2 } from "os";
|
|
4478
|
-
import { join as
|
|
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 = [
|
|
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 (!
|
|
4661
|
-
const content =
|
|
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
|
|
4687
|
-
var SiblingConfigSchema =
|
|
4688
|
-
$schema:
|
|
4689
|
-
project:
|
|
4690
|
-
name:
|
|
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:
|
|
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:
|
|
4703
|
-
|
|
4704
|
-
path:
|
|
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:
|
|
4855
|
+
label: z5.string().min(1)
|
|
4709
4856
|
})
|
|
4710
4857
|
).default([])
|
|
4711
4858
|
}),
|
|
4712
4859
|
source_control: SourceControlConfigSchema,
|
|
4713
4860
|
cloud: CloudConfigSchema,
|
|
4714
|
-
environments:
|
|
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:
|
|
4865
|
+
core: z5.object({
|
|
4719
4866
|
// Exactly one of these two must be set — see the superRefine below.
|
|
4720
|
-
project_name:
|
|
4721
|
-
config_path:
|
|
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:
|
|
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:
|
|
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
|
|
4898
|
+
existsSync as existsSync19,
|
|
4752
4899
|
mkdirSync as mkdirSync6,
|
|
4753
4900
|
readdirSync as readdirSync8,
|
|
4754
|
-
readFileSync as
|
|
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
|
|
4907
|
+
import { join as join18 } from "path";
|
|
4761
4908
|
function sessionsDir2() {
|
|
4762
|
-
return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ??
|
|
4909
|
+
return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join18(homedir3(), ".biffo", "sibling-sessions");
|
|
4763
4910
|
}
|
|
4764
4911
|
function sessionPath2(projectName) {
|
|
4765
|
-
return
|
|
4912
|
+
return join18(sessionsDir2(), `${projectName}.json`);
|
|
4766
4913
|
}
|
|
4767
4914
|
function loadSiblingSession(projectName) {
|
|
4768
4915
|
const path = sessionPath2(projectName);
|
|
4769
|
-
if (!
|
|
4916
|
+
if (!existsSync19(path)) return null;
|
|
4770
4917
|
try {
|
|
4771
|
-
return JSON.parse(
|
|
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 (!
|
|
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 (
|
|
4947
|
+
if (existsSync19(path)) rmSync7(path);
|
|
4801
4948
|
}
|
|
4802
4949
|
|
|
4803
4950
|
// src/commands/sibling-create.ts
|
|
4804
|
-
import { cpSync as cpSync2, existsSync as
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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(
|
|
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(
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
5311
|
+
const envPath = join20(targetDir, "apps", "frontend", ".env.example");
|
|
5165
5312
|
try {
|
|
5166
5313
|
const path = basePathFor(context.pathPrefix);
|
|
5167
|
-
const content =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
5267
|
-
const filePath =
|
|
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 =
|
|
5348
|
-
if (
|
|
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(
|
|
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
|
|
5806
|
-
import { dirname as dirname8, join as
|
|
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
|
|
5813
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
5826
|
-
if (!
|
|
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 =
|
|
5984
|
+
const servicesDir = join21(cwd, "services");
|
|
5838
5985
|
return [
|
|
5839
5986
|
...scanDir(servicesDir, "services", "third-party"),
|
|
5840
5987
|
...scanDir(
|
|
5841
|
-
|
|
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
|
|
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 =
|
|
5853
|
-
name:
|
|
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:
|
|
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:
|
|
5864
|
-
nullable:
|
|
5865
|
-
index:
|
|
5866
|
-
default:
|
|
5867
|
-
description:
|
|
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 =
|
|
5870
|
-
name:
|
|
5871
|
-
columns:
|
|
5872
|
-
unique:
|
|
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 =
|
|
5875
|
-
allowed:
|
|
5876
|
-
required_role:
|
|
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 =
|
|
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 =
|
|
5886
|
-
name:
|
|
5887
|
-
columns:
|
|
5888
|
-
indexes:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
5932
|
-
method:
|
|
5933
|
-
path:
|
|
5934
|
-
table:
|
|
5935
|
-
operation:
|
|
5936
|
-
description:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
5961
|
-
name:
|
|
5962
|
-
version:
|
|
5963
|
-
description:
|
|
5964
|
-
author:
|
|
5965
|
-
tags:
|
|
5966
|
-
tables:
|
|
5967
|
-
api_routes:
|
|
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:
|
|
5974
|
-
required_core_version:
|
|
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:
|
|
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
|
|
6148
|
+
existsSync as existsSync22,
|
|
6002
6149
|
mkdirSync as mkdirSync8,
|
|
6003
|
-
readFileSync as
|
|
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
|
|
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 (!
|
|
6207
|
+
if (!existsSync22(skeletonRoot)) {
|
|
6061
6208
|
throw new Error(`Plugin skeleton not found at ${skeletonRoot}`);
|
|
6062
6209
|
}
|
|
6063
|
-
if (!
|
|
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 =
|
|
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 =
|
|
6233
|
+
const destPath = join22(destDir, destRel);
|
|
6087
6234
|
mkdirSync8(dirname7(destPath), { recursive: true });
|
|
6088
6235
|
if (BINARY_EXTENSIONS.test(entry.name)) {
|
|
6089
|
-
copyFileSync(
|
|
6236
|
+
copyFileSync(join22(skeletonRoot, relPath), destPath);
|
|
6090
6237
|
} else {
|
|
6091
6238
|
writeFileSync8(
|
|
6092
6239
|
destPath,
|
|
6093
|
-
applySubstitutions(
|
|
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 =
|
|
6111
|
-
if (
|
|
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 =
|
|
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 =
|
|
6157
|
-
const servicesDir =
|
|
6158
|
-
if (!
|
|
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 (
|
|
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") ??
|
|
6168
|
-
if (!
|
|
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 =
|
|
6184
|
-
const manifest = validateManifest(JSON.parse(
|
|
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
|
|
6241
|
-
var RegistryPluginEntrySchema =
|
|
6242
|
-
name:
|
|
6243
|
-
version:
|
|
6244
|
-
minor_version:
|
|
6245
|
-
repo:
|
|
6246
|
-
description:
|
|
6247
|
-
author:
|
|
6248
|
-
tags:
|
|
6249
|
-
required_core_version:
|
|
6250
|
-
infra_modules:
|
|
6251
|
-
api_routes:
|
|
6252
|
-
ui_components:
|
|
6253
|
-
status:
|
|
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 =
|
|
6256
|
-
schema_version:
|
|
6257
|
-
last_updated:
|
|
6258
|
-
plugins:
|
|
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
|
|
6375
|
-
import { basename, join as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6543
|
+
join24(cwd, "services"),
|
|
6397
6544
|
"--versions-dir",
|
|
6398
|
-
|
|
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:
|
|
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 (!
|
|
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 =
|
|
6470
|
-
if (!
|
|
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 =
|
|
6497
|
-
if (!
|
|
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 =
|
|
6526
|
-
if (!
|
|
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 =
|
|
6545
|
-
const modulesDir =
|
|
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 (
|
|
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 =
|
|
6591
|
-
if (
|
|
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(
|
|
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
|
|
6686
|
-
import { join as
|
|
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 =
|
|
6700
|
-
if (!
|
|
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(
|
|
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
|
|
6746
|
-
import { join as
|
|
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 =
|
|
6768
|
-
if (!
|
|
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 && !
|
|
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
|
|
6808
|
-
import { join as
|
|
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 =
|
|
6841
|
-
if (!
|
|
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 =
|
|
6847
|
-
if (!
|
|
6848
|
-
const firstParty =
|
|
6849
|
-
if (
|
|
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 =
|
|
7004
|
+
const modulesDir = join28(options.cwd, "modules", "plugins", name);
|
|
6858
7005
|
const stagePaths = [`services/${name}`];
|
|
6859
|
-
if (
|
|
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 (
|
|
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 =
|
|
6919
|
-
if (!
|
|
7065
|
+
const manifestPath = join28(targetDir, "biffo.plugin.json");
|
|
7066
|
+
if (!existsSync27(manifestPath)) return void 0;
|
|
6920
7067
|
try {
|
|
6921
|
-
return validateManifest(JSON.parse(
|
|
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
|
|
6955
|
-
import { join as
|
|
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 =
|
|
6981
|
-
if (!
|
|
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 =
|
|
6987
|
-
if (!
|
|
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 =
|
|
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 (
|
|
7181
|
+
if (existsSync28(modulesDir)) {
|
|
7035
7182
|
rmSync9(modulesDir, { recursive: true, force: true });
|
|
7036
7183
|
}
|
|
7037
|
-
const tfSourceDir =
|
|
7038
|
-
if (
|
|
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 =
|
|
7073
|
-
if (!
|
|
7219
|
+
const manifestPath = join29(targetDir, "biffo.plugin.json");
|
|
7220
|
+
if (!existsSync28(manifestPath)) return void 0;
|
|
7074
7221
|
try {
|
|
7075
|
-
return validateManifest(JSON.parse(
|
|
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
|
|
7303
|
-
core upgrade will
|
|
7304
|
-
|
|
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
|
-
|
|
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}
|