@bneb/b4mal 0.1.1 → 0.1.2
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/README.md +15 -0
- package/dist/index.js +973 -699
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -875,6 +875,34 @@ class ArtifactVault {
|
|
|
875
875
|
if (decompressCode !== 0) {
|
|
876
876
|
throw new Error(`Unpack failed: zstd exited ${decompressCode} (${decompressErr.trim()})`);
|
|
877
877
|
}
|
|
878
|
+
const typedListProc = Bun.spawn(["tar", "-tvf", tarPath], {
|
|
879
|
+
stdout: "pipe",
|
|
880
|
+
stderr: "pipe"
|
|
881
|
+
});
|
|
882
|
+
const typedList = await new Response(typedListProc.stdout).text();
|
|
883
|
+
const typedCode = await typedListProc.exited;
|
|
884
|
+
if (typedCode !== 0) {
|
|
885
|
+
const typedErr = await new Response(typedListProc.stderr).text();
|
|
886
|
+
throw new Error(`Unpack failed: could not list archive types (${typedErr.trim()})`);
|
|
887
|
+
}
|
|
888
|
+
const TYPE_LABELS = {
|
|
889
|
+
l: "symlink",
|
|
890
|
+
h: "hard link",
|
|
891
|
+
b: "block device",
|
|
892
|
+
c: "character device",
|
|
893
|
+
p: "FIFO",
|
|
894
|
+
s: "socket"
|
|
895
|
+
};
|
|
896
|
+
for (const line of typedList.split(`
|
|
897
|
+
`)) {
|
|
898
|
+
if (!line.trim())
|
|
899
|
+
continue;
|
|
900
|
+
const type = line[0];
|
|
901
|
+
if (type === "-" || type === "d")
|
|
902
|
+
continue;
|
|
903
|
+
const kind = TYPE_LABELS[type] ?? `unrecognised (${JSON.stringify(type)})`;
|
|
904
|
+
throw new Error(`Unpack rejected: archive contains a ${kind} entry: ${line.slice(0, 120)}`);
|
|
905
|
+
}
|
|
878
906
|
const listProc = Bun.spawn(["tar", "-tf", tarPath], {
|
|
879
907
|
stdout: "pipe",
|
|
880
908
|
stderr: "pipe"
|
|
@@ -1621,279 +1649,301 @@ var init_sqlite_ledger = () => {};
|
|
|
1621
1649
|
import { join as join8 } from "path";
|
|
1622
1650
|
import { existsSync as existsSync5 } from "fs";
|
|
1623
1651
|
import { cpus } from "os";
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (resolved || settled.size < totalTasks)
|
|
1646
|
-
return;
|
|
1647
|
-
resolved = true;
|
|
1652
|
+
var DynamicExecutor;
|
|
1653
|
+
var init_executor = __esm(() => {
|
|
1654
|
+
init_stream_engine();
|
|
1655
|
+
init_env_sanitizer();
|
|
1656
|
+
init_artifact_vault();
|
|
1657
|
+
init_cache_key();
|
|
1658
|
+
init_sqlite_ledger();
|
|
1659
|
+
DynamicExecutor = class DynamicExecutor {
|
|
1660
|
+
static warnedAboutWhenIf = new Set;
|
|
1661
|
+
static async run(dag, config) {
|
|
1662
|
+
const ledger = config?.projectRoot ? new SQLiteLedger(join8(config.projectRoot, ".b4mal", "cache.db")) : undefined;
|
|
1663
|
+
const concurrency = config?.concurrency ?? cpus().length;
|
|
1664
|
+
const inDegree = new Map(dag.inDegree);
|
|
1665
|
+
const readyQueue = [];
|
|
1666
|
+
for (const [id, count] of inDegree.entries()) {
|
|
1667
|
+
if (count === 0)
|
|
1668
|
+
readyQueue.push(id);
|
|
1669
|
+
}
|
|
1670
|
+
let activeCount = 0;
|
|
1671
|
+
const totalTasks = dag.tasks.size;
|
|
1672
|
+
if (totalTasks === 0) {
|
|
1648
1673
|
ledger?.close();
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1674
|
+
return [];
|
|
1675
|
+
}
|
|
1676
|
+
return new Promise((resolve2) => {
|
|
1677
|
+
const settled = new Map;
|
|
1678
|
+
let resolved = false;
|
|
1679
|
+
const settleIfComplete = () => {
|
|
1680
|
+
if (resolved || settled.size < totalTasks)
|
|
1681
|
+
return;
|
|
1682
|
+
resolved = true;
|
|
1683
|
+
ledger?.close();
|
|
1684
|
+
resolve2([...settled.values()]);
|
|
1685
|
+
};
|
|
1686
|
+
const skipDependentsOf = (failedId) => {
|
|
1687
|
+
const stack = [failedId];
|
|
1688
|
+
while (stack.length > 0) {
|
|
1689
|
+
const current = stack.pop();
|
|
1690
|
+
for (const dependent of dag.dependents.get(current) ?? []) {
|
|
1691
|
+
if (settled.has(dependent))
|
|
1692
|
+
continue;
|
|
1693
|
+
settled.set(dependent, {
|
|
1694
|
+
taskId: dependent,
|
|
1695
|
+
exitCode: 1,
|
|
1696
|
+
stdout: "",
|
|
1697
|
+
stderr: `Skipped: dependency '${current}' failed.`,
|
|
1698
|
+
durationMs: 0,
|
|
1699
|
+
cached: false,
|
|
1700
|
+
skipped: true
|
|
1701
|
+
});
|
|
1702
|
+
stack.push(dependent);
|
|
1703
|
+
}
|
|
1679
1704
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1688
|
-
settled.set(taskId, {
|
|
1689
|
-
taskId,
|
|
1690
|
-
exitCode: 1,
|
|
1691
|
-
stdout: "",
|
|
1692
|
-
stderr: message,
|
|
1693
|
-
durationMs: 0,
|
|
1694
|
-
cached: false
|
|
1695
|
-
});
|
|
1696
|
-
}).finally(() => {
|
|
1697
|
-
activeCount--;
|
|
1698
|
-
const result = settled.get(taskId);
|
|
1699
|
-
const failed = !result || result.exitCode !== 0;
|
|
1700
|
-
if (failed) {
|
|
1701
|
-
skipDependentsOf(taskId);
|
|
1705
|
+
};
|
|
1706
|
+
const dispatch = async () => {
|
|
1707
|
+
while (readyQueue.length > 0 && activeCount < concurrency) {
|
|
1708
|
+
let taskId;
|
|
1709
|
+
if (config?.chaos) {
|
|
1710
|
+
const randomIndex = Math.floor(Math.random() * readyQueue.length);
|
|
1711
|
+
taskId = readyQueue.splice(randomIndex, 1)[0];
|
|
1702
1712
|
} else {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1713
|
+
taskId = readyQueue.shift();
|
|
1714
|
+
}
|
|
1715
|
+
if (settled.has(taskId))
|
|
1716
|
+
continue;
|
|
1717
|
+
const task = dag.tasks.get(taskId);
|
|
1718
|
+
activeCount++;
|
|
1719
|
+
this.executeTask(task, config, ledger).then((result) => {
|
|
1720
|
+
settled.set(taskId, result);
|
|
1721
|
+
}).catch((err) => {
|
|
1722
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1723
|
+
settled.set(taskId, {
|
|
1724
|
+
taskId,
|
|
1725
|
+
exitCode: 1,
|
|
1726
|
+
stdout: "",
|
|
1727
|
+
stderr: message,
|
|
1728
|
+
durationMs: 0,
|
|
1729
|
+
cached: false
|
|
1730
|
+
});
|
|
1731
|
+
}).finally(() => {
|
|
1732
|
+
activeCount--;
|
|
1733
|
+
const result = settled.get(taskId);
|
|
1734
|
+
const failed = !result || result.exitCode !== 0;
|
|
1735
|
+
if (failed) {
|
|
1736
|
+
skipDependentsOf(taskId);
|
|
1737
|
+
} else {
|
|
1738
|
+
for (const dependent of dag.dependents.get(taskId) || []) {
|
|
1739
|
+
const currentCount = inDegree.get(dependent) - 1;
|
|
1740
|
+
inDegree.set(dependent, currentCount);
|
|
1741
|
+
if (currentCount === 0) {
|
|
1742
|
+
readyQueue.push(dependent);
|
|
1743
|
+
}
|
|
1708
1744
|
}
|
|
1709
1745
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1746
|
+
StreamEngine.broadcast("wave_complete", {
|
|
1747
|
+
depth: 0,
|
|
1748
|
+
tasks: 1,
|
|
1749
|
+
durationMs: 0,
|
|
1750
|
+
taskIds: [taskId]
|
|
1751
|
+
});
|
|
1752
|
+
settleIfComplete();
|
|
1753
|
+
if (!resolved)
|
|
1754
|
+
dispatch();
|
|
1716
1755
|
});
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
dispatch();
|
|
1725
|
-
} catch (err) {
|
|
1726
|
-
ledger?.close();
|
|
1727
|
-
throw err;
|
|
1728
|
-
}
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
static async executeTask(task, config, ledger) {
|
|
1732
|
-
const projectRoot = config?.projectRoot;
|
|
1733
|
-
const envClaims = task.claims.filter((c) => c.startsWith("env:")).map((c) => c.replace(/^env:/, ""));
|
|
1734
|
-
const requestedEnv = [...new Set([...envClaims, ...task.envReads ?? []])];
|
|
1735
|
-
const writes = task.writes ?? [];
|
|
1736
|
-
const producesArtifact = writes.length > 0;
|
|
1737
|
-
const logicHash = projectRoot ? await computeCacheKey({
|
|
1738
|
-
id: task.id,
|
|
1739
|
-
cmd: task.cmd,
|
|
1740
|
-
reads: task.reads,
|
|
1741
|
-
writes: task.writes,
|
|
1742
|
-
claims: task.claims,
|
|
1743
|
-
envReads: task.envReads
|
|
1744
|
-
}, projectRoot) : undefined;
|
|
1745
|
-
const skipCache = config?.force === true;
|
|
1746
|
-
if (!skipCache && logicHash && config?.remoteVault && projectRoot) {
|
|
1747
|
-
try {
|
|
1748
|
-
const l2Result = await config.remoteVault.checkAndPull(logicHash, projectRoot);
|
|
1749
|
-
if (l2Result) {
|
|
1750
|
-
return {
|
|
1751
|
-
taskId: task.id,
|
|
1752
|
-
exitCode: l2Result.exitCode ?? 0,
|
|
1753
|
-
stdout: "[L2 cache hit \u2014 restored from remote vault]",
|
|
1754
|
-
stderr: "",
|
|
1755
|
-
durationMs: l2Result.durationMs ?? 0,
|
|
1756
|
-
cached: true
|
|
1757
|
-
};
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
try {
|
|
1759
|
+
dispatch();
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
ledger?.close();
|
|
1762
|
+
throw err;
|
|
1758
1763
|
}
|
|
1759
|
-
}
|
|
1760
|
-
process.stderr.write(`\x1B[2m[L2] pull failed: ${err?.message || err}\x1B[0m
|
|
1761
|
-
`);
|
|
1762
|
-
}
|
|
1764
|
+
});
|
|
1763
1765
|
}
|
|
1764
|
-
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1766
|
+
static async executeTask(task, config, ledger) {
|
|
1767
|
+
const projectRoot = config?.projectRoot;
|
|
1768
|
+
const envClaims = task.claims.filter((c) => c.startsWith("env:")).map((c) => c.replace(/^env:/, ""));
|
|
1769
|
+
const requestedEnv = [...new Set([...envClaims, ...task.envReads ?? []])];
|
|
1770
|
+
const writes = task.writes ?? [];
|
|
1771
|
+
const producesArtifact = writes.length > 0;
|
|
1772
|
+
const logicHash = projectRoot ? await computeCacheKey({
|
|
1773
|
+
id: task.id,
|
|
1774
|
+
cmd: task.cmd,
|
|
1775
|
+
reads: task.reads,
|
|
1776
|
+
writes: task.writes,
|
|
1777
|
+
claims: task.claims,
|
|
1778
|
+
envReads: task.envReads
|
|
1779
|
+
}, projectRoot) : undefined;
|
|
1780
|
+
const skipCache = config?.force === true;
|
|
1781
|
+
if (task.when) {
|
|
1782
|
+
const w = task.when;
|
|
1783
|
+
if (w.if) {
|
|
1784
|
+
if (!DynamicExecutor.warnedAboutWhenIf.has(task.id)) {
|
|
1785
|
+
DynamicExecutor.warnedAboutWhenIf.add(task.id);
|
|
1786
|
+
process.stderr.write(`\x1B[2m[when] '${task.id}' sets when.if, which is not implemented \u2014 ` + `the condition is ignored and the task will run.\x1B[0m
|
|
1787
|
+
`);
|
|
1773
1788
|
}
|
|
1774
|
-
return {
|
|
1775
|
-
taskId: task.id,
|
|
1776
|
-
exitCode: 0,
|
|
1777
|
-
stdout: entry.stdout ?? "[L1 cache hit \u2014 restored from local vault]",
|
|
1778
|
-
stderr: entry.stderr ?? "",
|
|
1779
|
-
durationMs: entry.durationMs ?? performance.now() - start2,
|
|
1780
|
-
cached: true
|
|
1781
|
-
};
|
|
1782
1789
|
}
|
|
1783
|
-
|
|
1784
|
-
}
|
|
1785
|
-
if (task.when) {
|
|
1786
|
-
const w = task.when;
|
|
1787
|
-
if (w.platform && !w.platform.includes(process.platform)) {
|
|
1788
|
-
return {
|
|
1789
|
-
taskId: task.id,
|
|
1790
|
-
exitCode: 0,
|
|
1791
|
-
stdout: `[skipped \u2014 platform ${process.platform} not in ${w.platform}]`,
|
|
1792
|
-
stderr: "",
|
|
1793
|
-
durationMs: 0,
|
|
1794
|
-
cached: true
|
|
1795
|
-
};
|
|
1796
|
-
}
|
|
1797
|
-
if (w.branch) {
|
|
1798
|
-
const branch = process.env.GIT_BRANCH || process.env.CI_COMMIT_BRANCH || "";
|
|
1799
|
-
const matches = branch === w.branch || new RegExp(`^${w.branch.replace(/\*/g, ".*")}$`).test(branch);
|
|
1800
|
-
if (branch && !matches) {
|
|
1790
|
+
if (w.platform && !w.platform.includes(process.platform)) {
|
|
1801
1791
|
return {
|
|
1802
1792
|
taskId: task.id,
|
|
1803
1793
|
exitCode: 0,
|
|
1804
|
-
stdout: `[skipped \u2014
|
|
1794
|
+
stdout: `[skipped \u2014 platform ${process.platform} not in ${w.platform}]`,
|
|
1805
1795
|
stderr: "",
|
|
1806
1796
|
durationMs: 0,
|
|
1807
|
-
cached:
|
|
1797
|
+
cached: false,
|
|
1798
|
+
skippedByCondition: true
|
|
1808
1799
|
};
|
|
1809
1800
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1801
|
+
if (w.branch) {
|
|
1802
|
+
const branch = process.env.GIT_BRANCH || process.env.CI_COMMIT_BRANCH || "";
|
|
1803
|
+
const matches = branch === w.branch || new RegExp(`^${w.branch.replace(/\*/g, ".*")}$`).test(branch);
|
|
1804
|
+
if (branch && !matches) {
|
|
1805
|
+
return {
|
|
1806
|
+
taskId: task.id,
|
|
1807
|
+
exitCode: 0,
|
|
1808
|
+
stdout: `[skipped \u2014 branch "${branch}" doesn't match "${w.branch}"]`,
|
|
1809
|
+
stderr: "",
|
|
1810
|
+
durationMs: 0,
|
|
1811
|
+
cached: false,
|
|
1812
|
+
skippedByCondition: true
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1820
1815
|
}
|
|
1821
1816
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
process.stderr.write(`\x1B[2m[Cache] pack failed for '${task.id}': ${err instanceof Error ? err.message : err}\x1B[0m
|
|
1817
|
+
if (!skipCache && logicHash && config?.remoteVault && projectRoot) {
|
|
1818
|
+
try {
|
|
1819
|
+
const l2Result = await config.remoteVault.checkAndPull(logicHash, projectRoot);
|
|
1820
|
+
if (l2Result) {
|
|
1821
|
+
if (producesArtifact) {
|
|
1822
|
+
await ArtifactVault.unpack(logicHash, projectRoot);
|
|
1823
|
+
}
|
|
1824
|
+
ledger?.recordEntry({
|
|
1825
|
+
logicHash,
|
|
1826
|
+
taskId: task.id,
|
|
1827
|
+
action: "l2-hit",
|
|
1828
|
+
timestamp: Date.now(),
|
|
1829
|
+
stdout: "[L2 cache hit \u2014 restored from remote vault]",
|
|
1830
|
+
stderr: "",
|
|
1831
|
+
durationMs: l2Result.durationMs ?? 0,
|
|
1832
|
+
exitCode: l2Result.exitCode ?? 0
|
|
1833
|
+
});
|
|
1834
|
+
return {
|
|
1835
|
+
taskId: task.id,
|
|
1836
|
+
exitCode: l2Result.exitCode ?? 0,
|
|
1837
|
+
stdout: "[L2 cache hit \u2014 restored from remote vault]",
|
|
1838
|
+
stderr: "",
|
|
1839
|
+
durationMs: l2Result.durationMs ?? 0,
|
|
1840
|
+
cached: true
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
process.stderr.write(`\x1B[2m[L2] pull failed: ${err?.message || err}\x1B[0m
|
|
1851
1845
|
`);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (!skipCache && logicHash && ledger) {
|
|
1849
|
+
const entry = ledger.getEntry(logicHash);
|
|
1850
|
+
if (entry) {
|
|
1851
|
+
const needsArtifact = producesArtifact;
|
|
1852
|
+
const hasArtifact = !needsArtifact || ArtifactVault.hasArtifact(logicHash, projectRoot);
|
|
1853
|
+
if (hasArtifact) {
|
|
1854
|
+
const start2 = performance.now();
|
|
1855
|
+
if (needsArtifact && projectRoot) {
|
|
1856
|
+
await ArtifactVault.unpack(logicHash, projectRoot);
|
|
1857
|
+
}
|
|
1858
|
+
return {
|
|
1859
|
+
taskId: task.id,
|
|
1860
|
+
exitCode: 0,
|
|
1861
|
+
stdout: entry.stdout ?? "[L1 cache hit \u2014 restored from local vault]",
|
|
1862
|
+
stderr: entry.stderr ?? "",
|
|
1863
|
+
durationMs: entry.durationMs ?? performance.now() - start2,
|
|
1864
|
+
cached: true
|
|
1865
|
+
};
|
|
1852
1866
|
}
|
|
1853
1867
|
}
|
|
1854
1868
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
}
|
|
1869
|
+
const start = performance.now();
|
|
1870
|
+
const sanitizedEnv = EnvSanitizer.sanitize(requestedEnv, process.env);
|
|
1871
|
+
const secrets = task.secrets;
|
|
1872
|
+
if (secrets && secrets.length > 0) {
|
|
1873
|
+
for (const name of secrets) {
|
|
1874
|
+
const val = process.env[name];
|
|
1875
|
+
if (val !== undefined) {
|
|
1876
|
+
sanitizedEnv[name] = val;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1865
1879
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1880
|
+
let finalCmd = task.cmd;
|
|
1881
|
+
const proc = Bun.spawn(finalCmd, {
|
|
1882
|
+
cwd: projectRoot,
|
|
1883
|
+
stdout: "pipe",
|
|
1884
|
+
stderr: "pipe",
|
|
1885
|
+
env: sanitizedEnv
|
|
1886
|
+
});
|
|
1887
|
+
const exitCode = await proc.exited;
|
|
1888
|
+
const durationMs = performance.now() - start;
|
|
1889
|
+
const maxLogSize = 100 * 1024;
|
|
1890
|
+
const rawStdout = await new Response(proc.stdout).text();
|
|
1891
|
+
const rawStderr = await new Response(proc.stderr).text();
|
|
1892
|
+
const stdout = rawStdout.length > maxLogSize ? rawStdout.slice(-maxLogSize) : rawStdout;
|
|
1893
|
+
const stderr = rawStderr.length > maxLogSize ? rawStderr.slice(-maxLogSize) : rawStderr;
|
|
1894
|
+
if (exitCode === 0 && logicHash) {
|
|
1895
|
+
let cacheable = true;
|
|
1896
|
+
if (projectRoot && producesArtifact) {
|
|
1897
|
+
const missing = writes.filter((w) => !existsSync5(join8(projectRoot, w)));
|
|
1898
|
+
if (missing.length > 0) {
|
|
1899
|
+
cacheable = false;
|
|
1900
|
+
process.stderr.write(`\x1B[2m[Cache] '${task.id}' declared output(s) not produced: ` + `${missing.join(", ")} \u2014 task will not be cached.\x1B[0m
|
|
1901
|
+
`);
|
|
1902
|
+
} else {
|
|
1903
|
+
try {
|
|
1904
|
+
await ArtifactVault.pack(logicHash, projectRoot, writes);
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
cacheable = false;
|
|
1907
|
+
process.stderr.write(`\x1B[2m[Cache] pack failed for '${task.id}': ${err instanceof Error ? err.message : err}\x1B[0m
|
|
1908
|
+
`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (cacheable) {
|
|
1913
|
+
ledger?.recordEntry({
|
|
1869
1914
|
logicHash,
|
|
1870
1915
|
taskId: task.id,
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1916
|
+
action: "execute",
|
|
1917
|
+
timestamp: Date.now(),
|
|
1918
|
+
stdout: stdout.trim(),
|
|
1919
|
+
stderr: stderr.trim(),
|
|
1920
|
+
durationMs
|
|
1874
1921
|
});
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1922
|
+
}
|
|
1923
|
+
if (projectRoot && config?.remoteVault) {
|
|
1924
|
+
try {
|
|
1925
|
+
await config.remoteVault.pushWithMetadata(logicHash, projectRoot, {
|
|
1926
|
+
logicHash,
|
|
1927
|
+
taskId: task.id,
|
|
1928
|
+
exitCode,
|
|
1929
|
+
durationMs
|
|
1930
|
+
});
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
process.stderr.write(`\x1B[2m[L2] push failed: ${err?.message || err}\x1B[0m
|
|
1877
1933
|
`);
|
|
1934
|
+
}
|
|
1878
1935
|
}
|
|
1879
1936
|
}
|
|
1937
|
+
return {
|
|
1938
|
+
taskId: task.id,
|
|
1939
|
+
exitCode,
|
|
1940
|
+
stdout: stdout.trim(),
|
|
1941
|
+
stderr: stderr.trim(),
|
|
1942
|
+
durationMs,
|
|
1943
|
+
cached: false
|
|
1944
|
+
};
|
|
1880
1945
|
}
|
|
1881
|
-
|
|
1882
|
-
taskId: task.id,
|
|
1883
|
-
exitCode,
|
|
1884
|
-
stdout: stdout.trim(),
|
|
1885
|
-
stderr: stderr.trim(),
|
|
1886
|
-
durationMs,
|
|
1887
|
-
cached: false
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
var init_executor = __esm(() => {
|
|
1892
|
-
init_stream_engine();
|
|
1893
|
-
init_env_sanitizer();
|
|
1894
|
-
init_artifact_vault();
|
|
1895
|
-
init_cache_key();
|
|
1896
|
-
init_sqlite_ledger();
|
|
1946
|
+
};
|
|
1897
1947
|
});
|
|
1898
1948
|
|
|
1899
1949
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -6340,9 +6390,55 @@ var init_s3_adapter = __esm(() => {
|
|
|
6340
6390
|
HASH_RE = /^[a-zA-Z0-9_.-]+$/;
|
|
6341
6391
|
});
|
|
6342
6392
|
|
|
6393
|
+
// src/core/crypto.ts
|
|
6394
|
+
import { timingSafeEqual } from "crypto";
|
|
6395
|
+
|
|
6396
|
+
class ArtifactCrypto {
|
|
6397
|
+
secretKey;
|
|
6398
|
+
constructor(secretKey = Bun.env.B4MAL_CACHE_SECRET ?? null) {
|
|
6399
|
+
this.secretKey = secretKey && secretKey.length > 0 ? secretKey : null;
|
|
6400
|
+
}
|
|
6401
|
+
get isEnabled() {
|
|
6402
|
+
return this.secretKey !== null;
|
|
6403
|
+
}
|
|
6404
|
+
sign(contentHash) {
|
|
6405
|
+
if (!this.secretKey)
|
|
6406
|
+
return null;
|
|
6407
|
+
const hasher = new Bun.CryptoHasher("sha256", this.secretKey);
|
|
6408
|
+
hasher.update(contentHash);
|
|
6409
|
+
return hasher.digest("hex");
|
|
6410
|
+
}
|
|
6411
|
+
verify(contentHash, signature) {
|
|
6412
|
+
if (!this.secretKey)
|
|
6413
|
+
return true;
|
|
6414
|
+
if (!signature)
|
|
6415
|
+
return false;
|
|
6416
|
+
const expected = this.sign(contentHash);
|
|
6417
|
+
if (!expected)
|
|
6418
|
+
return false;
|
|
6419
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
6420
|
+
const actualBuf = Buffer.from(signature, "hex");
|
|
6421
|
+
if (expectedBuf.length !== actualBuf.length)
|
|
6422
|
+
return false;
|
|
6423
|
+
return timingSafeEqual(expectedBuf, actualBuf);
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
var init_crypto = () => {};
|
|
6427
|
+
|
|
6343
6428
|
// src/core/remote_vault.ts
|
|
6344
6429
|
import { join as join9 } from "path";
|
|
6345
6430
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync, readFileSync as readFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
6431
|
+
function sha256Hex(data) {
|
|
6432
|
+
const buf = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
6433
|
+
return new Bun.CryptoHasher("sha256").update(buf).digest("hex");
|
|
6434
|
+
}
|
|
6435
|
+
function stableMetadataJson(metadata) {
|
|
6436
|
+
const { signature: _signature, ...rest } = metadata;
|
|
6437
|
+
return JSON.stringify(rest, Object.keys(rest).sort());
|
|
6438
|
+
}
|
|
6439
|
+
function signedInput(logicHash, payload, metadata) {
|
|
6440
|
+
return `${logicHash}:${sha256Hex(payload)}:${sha256Hex(stableMetadataJson(metadata))}`;
|
|
6441
|
+
}
|
|
6346
6442
|
function embedMetadata(data, metadata) {
|
|
6347
6443
|
const jsonStr = JSON.stringify(metadata);
|
|
6348
6444
|
const jsonBytes = Buffer.from(jsonStr, "utf-8");
|
|
@@ -6366,10 +6462,13 @@ function parseEmbeddedMetadata(data) {
|
|
|
6366
6462
|
|
|
6367
6463
|
class RemoteVault {
|
|
6368
6464
|
adapter;
|
|
6465
|
+
crypto;
|
|
6369
6466
|
lastPromoted = null;
|
|
6467
|
+
rejected = 0;
|
|
6370
6468
|
stats = { pushes: 0, pulls: 0, hits: 0, bytesUp: 0, bytesDown: 0 };
|
|
6371
|
-
constructor(adapter) {
|
|
6469
|
+
constructor(adapter, crypto = new ArtifactCrypto) {
|
|
6372
6470
|
this.adapter = adapter;
|
|
6471
|
+
this.crypto = crypto;
|
|
6373
6472
|
}
|
|
6374
6473
|
async checkAndPull(logicHash, projectRoot) {
|
|
6375
6474
|
if (!this.adapter)
|
|
@@ -6387,10 +6486,20 @@ class RemoteVault {
|
|
|
6387
6486
|
if (!pulled)
|
|
6388
6487
|
return null;
|
|
6389
6488
|
const rawData = await Bun.file(tmpPath).arrayBuffer();
|
|
6390
|
-
const
|
|
6489
|
+
const payload = Buffer.from(rawData);
|
|
6490
|
+
const metadata = parseEmbeddedMetadata(payload);
|
|
6391
6491
|
if (!metadata) {
|
|
6392
6492
|
return null;
|
|
6393
6493
|
}
|
|
6494
|
+
const headerLen = payload.readUInt32LE(0);
|
|
6495
|
+
const archiveBytes = payload.subarray(4 + headerLen);
|
|
6496
|
+
const signatureValid = this.crypto.verify(signedInput(logicHash, archiveBytes, metadata), metadata.signature ?? undefined);
|
|
6497
|
+
if (!signatureValid) {
|
|
6498
|
+
this.rejected++;
|
|
6499
|
+
process.stderr.write(`\x1B[2m[L2] rejected artifact for ${logicHash.slice(0, 12)}\u2026: ` + `signature missing or invalid \u2014 re-executing\x1B[0m
|
|
6500
|
+
`);
|
|
6501
|
+
return null;
|
|
6502
|
+
}
|
|
6394
6503
|
this.promoteToL1(logicHash, tmpPath, projectRoot);
|
|
6395
6504
|
this.lastPromoted = logicHash;
|
|
6396
6505
|
this.stats.hits++;
|
|
@@ -6415,8 +6524,10 @@ class RemoteVault {
|
|
|
6415
6524
|
const l1Path = ArtifactVault.getArchivePath(logicHash, projectRoot);
|
|
6416
6525
|
if (!existsSync6(l1Path))
|
|
6417
6526
|
return false;
|
|
6418
|
-
const rawData = await Bun.file(l1Path).arrayBuffer();
|
|
6419
|
-
const
|
|
6527
|
+
const rawData = Buffer.from(await Bun.file(l1Path).arrayBuffer());
|
|
6528
|
+
const metaBase = { ...metadata, logicHash };
|
|
6529
|
+
const signature = this.crypto.sign(signedInput(logicHash, rawData, metaBase));
|
|
6530
|
+
const archiveWithMeta = embedMetadata(rawData, { ...metaBase, signature });
|
|
6420
6531
|
const tmpPath = join9(projectRoot, ".b4mal", `l2-push-${logicHash}.tmp`);
|
|
6421
6532
|
writeFileSync(tmpPath, new Uint8Array(archiveWithMeta));
|
|
6422
6533
|
const result = await this.adapter.push(logicHash, tmpPath);
|
|
@@ -6452,6 +6563,7 @@ class RemoteVault {
|
|
|
6452
6563
|
}
|
|
6453
6564
|
var init_remote_vault = __esm(() => {
|
|
6454
6565
|
init_artifact_vault();
|
|
6566
|
+
init_crypto();
|
|
6455
6567
|
});
|
|
6456
6568
|
|
|
6457
6569
|
// src/core/engine.ts
|
|
@@ -6530,6 +6642,14 @@ class B4malEngine {
|
|
|
6530
6642
|
}));
|
|
6531
6643
|
writeFileSync2(this.lockPath, JSON.stringify(tasks, null, 2), "utf-8");
|
|
6532
6644
|
}
|
|
6645
|
+
readLockfile() {
|
|
6646
|
+
const text = readFileSync3(this.lockPath, "utf-8");
|
|
6647
|
+
try {
|
|
6648
|
+
return JSON.parse(text);
|
|
6649
|
+
} catch (e) {
|
|
6650
|
+
throw new Error(`Failed to parse b4mal.lock: ${e instanceof Error ? e.message : String(e)}`);
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6533
6653
|
normalizeLockTasks(raw) {
|
|
6534
6654
|
let entries;
|
|
6535
6655
|
if (Array.isArray(raw)) {
|
|
@@ -6537,21 +6657,32 @@ class B4malEngine {
|
|
|
6537
6657
|
} else {
|
|
6538
6658
|
entries = raw.tasks ?? [];
|
|
6539
6659
|
}
|
|
6540
|
-
return entries.map((t) =>
|
|
6541
|
-
id
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6660
|
+
return entries.map((t, index) => {
|
|
6661
|
+
const id = String(t?.id ?? "").trim();
|
|
6662
|
+
if (!id) {
|
|
6663
|
+
throw new Error(`Invalid b4mal.lock: task at index ${index} has no "id".`);
|
|
6664
|
+
}
|
|
6665
|
+
const cmd = t.cmd;
|
|
6666
|
+
if (!Array.isArray(cmd) || cmd.length === 0 || cmd.some((c) => typeof c !== "string")) {
|
|
6667
|
+
throw new Error(`Invalid b4mal.lock: task "${id}" must have a non-empty "cmd" array of strings.`);
|
|
6668
|
+
}
|
|
6669
|
+
return {
|
|
6670
|
+
id,
|
|
6671
|
+
cmd,
|
|
6672
|
+
dependencies: t.deps ?? t.dependencies ?? [],
|
|
6673
|
+
inputs: t.reads ?? t.inputs ?? [],
|
|
6674
|
+
outputs: t.writes ?? t.outputs ?? [],
|
|
6675
|
+
claims: t.claims ?? [],
|
|
6676
|
+
needsEnv: t.envReads ?? t.needsEnv ?? [],
|
|
6677
|
+
providesEnv: t.envWrites ?? t.providesEnv ?? [],
|
|
6678
|
+
secrets: t.secrets ?? [],
|
|
6679
|
+
env: t.env ?? {},
|
|
6680
|
+
cwd: t.cwd,
|
|
6681
|
+
timeout: t.timeout ?? 300000,
|
|
6682
|
+
cache: t.cache ?? true,
|
|
6683
|
+
when: t.when
|
|
6684
|
+
};
|
|
6685
|
+
});
|
|
6555
6686
|
}
|
|
6556
6687
|
createRemoteVault() {
|
|
6557
6688
|
const bucket = process.env.B4MAL_CACHE_BUCKET;
|
|
@@ -6587,7 +6718,7 @@ class B4malEngine {
|
|
|
6587
6718
|
throw new Error(`No b4mal.lock found. Run 'b4mal init' first.
|
|
6588
6719
|
Docs: https://github.com/bneb/b4mal/blob/main/docs/guide/getting-started.md`);
|
|
6589
6720
|
}
|
|
6590
|
-
const raw =
|
|
6721
|
+
const raw = this.readLockfile();
|
|
6591
6722
|
const lockTasks = this.normalizeLockTasks(raw);
|
|
6592
6723
|
const tasks = lockTasks.map((t) => ({
|
|
6593
6724
|
id: t.id,
|
|
@@ -6625,7 +6756,7 @@ class B4malEngine {
|
|
|
6625
6756
|
throw new Error(`No b4mal.lock found. Run 'b4mal init' first. (Looked for: ${this.lockPath})
|
|
6626
6757
|
Docs: https://github.com/bneb/b4mal/blob/main/docs/guide/getting-started.md`);
|
|
6627
6758
|
}
|
|
6628
|
-
const raw =
|
|
6759
|
+
const raw = this.readLockfile();
|
|
6629
6760
|
const lockTasks = this.normalizeLockTasks(raw);
|
|
6630
6761
|
const tasks = lockTasks.map((t) => ({
|
|
6631
6762
|
id: t.id,
|
|
@@ -6696,7 +6827,7 @@ class B4malEngine {
|
|
|
6696
6827
|
if (!existsSync7(this.lockPath)) {
|
|
6697
6828
|
throw new Error(`No b4mal.lock found.`);
|
|
6698
6829
|
}
|
|
6699
|
-
const raw =
|
|
6830
|
+
const raw = this.readLockfile();
|
|
6700
6831
|
const lockTasks = this.normalizeLockTasks(raw);
|
|
6701
6832
|
const claims = lockTasks.map((t) => ({
|
|
6702
6833
|
id: t.id,
|
|
@@ -6715,7 +6846,7 @@ class B4malEngine {
|
|
|
6715
6846
|
throw new Error(`No b4mal.lock found. Run 'b4mal init' first.
|
|
6716
6847
|
Docs: https://github.com/bneb/b4mal/blob/main/docs/guide/getting-started.md`);
|
|
6717
6848
|
}
|
|
6718
|
-
const raw =
|
|
6849
|
+
const raw = this.readLockfile();
|
|
6719
6850
|
const lockTasks = this.normalizeLockTasks(raw);
|
|
6720
6851
|
const tasks = lockTasks.map((t) => ({
|
|
6721
6852
|
id: t.id,
|
|
@@ -10508,24 +10639,35 @@ function findLineForConflict(text, conflict) {
|
|
|
10508
10639
|
}
|
|
10509
10640
|
async function validateDocument(uri, text) {
|
|
10510
10641
|
try {
|
|
10511
|
-
const
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
const tasks =
|
|
10515
|
-
id
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10642
|
+
const parsed = JSON.parse(text);
|
|
10643
|
+
const raw = Array.isArray(parsed) ? parsed : Object.entries(parsed?.tasks ?? {}).map(([id, t]) => ({ id, ...t }));
|
|
10644
|
+
const deps = new Map;
|
|
10645
|
+
const tasks = raw.map((t) => {
|
|
10646
|
+
const id = String(t.id ?? "unknown");
|
|
10647
|
+
deps.set(id, t.dependencies ?? t.deps ?? []);
|
|
10648
|
+
return {
|
|
10649
|
+
id,
|
|
10650
|
+
reads: t.reads ?? t.inputs ?? [],
|
|
10651
|
+
writes: t.writes ?? t.outputs ?? [],
|
|
10652
|
+
envReads: t.envReads ?? t.needsEnv ?? [],
|
|
10653
|
+
envWrites: t.envWrites ?? t.providesEnv ?? [],
|
|
10654
|
+
claims: t.claims ?? []
|
|
10655
|
+
};
|
|
10656
|
+
});
|
|
10657
|
+
const shadows = await FormalShadow.detectShadowing(tasks, deps);
|
|
10658
|
+
const diagnostics = shadows.map((shadow) => {
|
|
10659
|
+
const resource = shadow.resources?.[0] ?? shadow.counterexample;
|
|
10660
|
+
const message = shadow.kind === "implicit-dependency" ? `${shadow.taskB} reads ${resource} produced by ${shadow.taskA} \u2014 no dependency edge is declared` : `${shadow.taskB} masks ${shadow.taskA} at ${resource} \u2014 the overwrite is ${shadow.ordering === "implicit" ? "not declared" : "declared"}`;
|
|
10661
|
+
return {
|
|
10662
|
+
range: {
|
|
10663
|
+
start: { line: findLineForConflict(text, shadow), character: 0 },
|
|
10664
|
+
end: { line: findLineForConflict(text, shadow), character: 100 }
|
|
10665
|
+
},
|
|
10666
|
+
severity: 2,
|
|
10667
|
+
source: "b4mal",
|
|
10668
|
+
message
|
|
10669
|
+
};
|
|
10670
|
+
});
|
|
10529
10671
|
sendMessage({ jsonrpc: "2.0", method: "textDocument/publishDiagnostics", params: { uri, diagnostics } });
|
|
10530
10672
|
} catch {}
|
|
10531
10673
|
}
|
|
@@ -10584,332 +10726,38 @@ var init_server = __esm(() => {
|
|
|
10584
10726
|
MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
10585
10727
|
});
|
|
10586
10728
|
|
|
10587
|
-
// src/
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
}
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
for (const task of Object.values(tasks)) {
|
|
10603
|
-
for (const dep of task.dependencies) {
|
|
10604
|
-
if (!ids.has(dep))
|
|
10605
|
-
return false;
|
|
10729
|
+
// src/cli/remote.ts
|
|
10730
|
+
var exports_remote = {};
|
|
10731
|
+
__export(exports_remote, {
|
|
10732
|
+
RemoteCommand: () => RemoteCommand
|
|
10733
|
+
});
|
|
10734
|
+
|
|
10735
|
+
class RemoteCommand {
|
|
10736
|
+
static async execute(args) {
|
|
10737
|
+
const sub = args[0];
|
|
10738
|
+
if (sub === "status" || !sub) {
|
|
10739
|
+
await this.status();
|
|
10740
|
+
} else {
|
|
10741
|
+
process.stderr.write(`${c3.red}[FAIL] Unknown remote command: ${sub}. Try: b4mal remote status${c3.reset}
|
|
10742
|
+
`);
|
|
10743
|
+
process.exit(1);
|
|
10606
10744
|
}
|
|
10607
10745
|
}
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
const c3 = color.get(id);
|
|
10624
|
-
if (c3 === GRAY)
|
|
10625
|
-
return false;
|
|
10626
|
-
if (c3 === BLACK)
|
|
10627
|
-
return true;
|
|
10628
|
-
color.set(id, GRAY);
|
|
10629
|
-
for (const dep of tasks[id]?.dependencies ?? []) {
|
|
10630
|
-
if (!dfs(dep))
|
|
10631
|
-
return false;
|
|
10632
|
-
}
|
|
10633
|
-
color.set(id, BLACK);
|
|
10634
|
-
return true;
|
|
10635
|
-
}
|
|
10636
|
-
for (const id of color.keys()) {
|
|
10637
|
-
if (color.get(id) === WHITE && !dfs(id))
|
|
10638
|
-
return false;
|
|
10639
|
-
}
|
|
10640
|
-
return true;
|
|
10641
|
-
}
|
|
10642
|
-
var TaskConfigSchema, VALID_TASK_ID, B4malConfigSchema, TaskSchema, PipelineSchema;
|
|
10643
|
-
var init_schema = __esm(() => {
|
|
10644
|
-
init_zod();
|
|
10645
|
-
TaskConfigSchema = exports_external.object({
|
|
10646
|
-
cmd: exports_external.array(exports_external.string()).min(1, "cmd must contain at least one element"),
|
|
10647
|
-
dependencies: exports_external.array(exports_external.string()).default([]),
|
|
10648
|
-
inputs: exports_external.array(exports_external.string()).default([]).transform((ps) => ps.map(sanitizePath)),
|
|
10649
|
-
outputs: exports_external.array(exports_external.string()).default([]).transform((ps) => ps.map(sanitizePath)),
|
|
10650
|
-
claims: exports_external.array(exports_external.string()).default([]).transform((ps) => ps.map((c3) => c3.startsWith("fs:") ? `fs:${sanitizePath(c3.slice(3))}` : c3)),
|
|
10651
|
-
needsEnv: exports_external.array(exports_external.string()).default([]),
|
|
10652
|
-
providesEnv: exports_external.array(exports_external.string()).default([]),
|
|
10653
|
-
secrets: exports_external.array(exports_external.string()).default([]),
|
|
10654
|
-
when: exports_external.object({
|
|
10655
|
-
branch: exports_external.string().optional().describe("Glob pattern for branch name"),
|
|
10656
|
-
platform: exports_external.array(exports_external.string()).optional().describe("OS platforms to run on"),
|
|
10657
|
-
if: exports_external.string().optional().describe("Arbitrary condition expression")
|
|
10658
|
-
}).optional(),
|
|
10659
|
-
matrix: exports_external.record(exports_external.array(exports_external.string()).max(10)).optional().refine((m) => !m || Object.keys(m).length <= 4, "Max 4 matrix axes").refine((m) => !m || cartesianSize(m) <= 256, "Max 256 matrix combinations"),
|
|
10660
|
-
env: exports_external.record(exports_external.string()).default({}),
|
|
10661
|
-
cwd: exports_external.string().optional().transform((p) => p ? p.replace(/\\/g, "/") : p).refine((p) => !p || !p.startsWith("/") && !p.split("/").some((seg) => seg === ".."), "cwd must be a relative path within the project root"),
|
|
10662
|
-
timeout: exports_external.number().int().nonnegative().max(3600000).transform((t) => t === 0 ? 300000 : t).default(300000),
|
|
10663
|
-
cache: exports_external.boolean().default(true)
|
|
10664
|
-
});
|
|
10665
|
-
VALID_TASK_ID = /^[a-zA-Z0-9_-]+$/;
|
|
10666
|
-
B4malConfigSchema = exports_external.object({
|
|
10667
|
-
name: exports_external.string().optional(),
|
|
10668
|
-
tasks: exports_external.record(TaskConfigSchema).refine((tasks) => Object.keys(tasks).length > 0, "At least one task is required").refine(validTaskIds, { message: "Task IDs must contain only alphanumeric characters, dashes, and underscores" }).refine(allDepsResolve, { message: "All dependency references must resolve to existing task IDs" }).refine(hasNoCycles, { message: "Circular dependency detected in task dependencies" }),
|
|
10669
|
-
concurrency: exports_external.number().int().nonnegative().default(0),
|
|
10670
|
-
env: exports_external.record(exports_external.string()).default({})
|
|
10671
|
-
});
|
|
10672
|
-
TaskSchema = exports_external.object({
|
|
10673
|
-
id: exports_external.string().min(1),
|
|
10674
|
-
cmd: exports_external.array(exports_external.string()).min(1),
|
|
10675
|
-
env: exports_external.record(exports_external.string()).default({}),
|
|
10676
|
-
dependencies: exports_external.array(exports_external.string()).default([]),
|
|
10677
|
-
cwd: exports_external.string().optional(),
|
|
10678
|
-
timeout: exports_external.number().int().nonnegative().default(0)
|
|
10679
|
-
});
|
|
10680
|
-
PipelineSchema = exports_external.object({
|
|
10681
|
-
name: exports_external.string().min(1),
|
|
10682
|
-
tasks: exports_external.array(TaskSchema).min(1),
|
|
10683
|
-
concurrency: exports_external.number().int().nonnegative().default(0),
|
|
10684
|
-
env: exports_external.record(exports_external.string()).default({})
|
|
10685
|
-
});
|
|
10686
|
-
});
|
|
10687
|
-
|
|
10688
|
-
// src/config_loader.ts
|
|
10689
|
-
var exports_config_loader = {};
|
|
10690
|
-
__export(exports_config_loader, {
|
|
10691
|
-
writeLockfileAtomic: () => writeLockfileAtomic,
|
|
10692
|
-
loadConfig: () => loadConfig,
|
|
10693
|
-
isConfigStale: () => isConfigStale,
|
|
10694
|
-
configToTasks: () => configToTasks
|
|
10695
|
-
});
|
|
10696
|
-
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync4, renameSync, realpathSync, rmSync } from "fs";
|
|
10697
|
-
import { join as join17, dirname as dirname7 } from "path";
|
|
10698
|
-
import { randomBytes, createHash as createHash2 } from "crypto";
|
|
10699
|
-
function sortStrings(a, b) {
|
|
10700
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
10701
|
-
}
|
|
10702
|
-
function sortedSet(arr) {
|
|
10703
|
-
return [...new Set(arr)].sort(sortStrings);
|
|
10704
|
-
}
|
|
10705
|
-
function sortedRecordKeys(rec) {
|
|
10706
|
-
if (!rec)
|
|
10707
|
-
return {};
|
|
10708
|
-
const out2 = {};
|
|
10709
|
-
for (const k of Object.keys(rec).sort(sortStrings)) {
|
|
10710
|
-
out2[k] = rec[k];
|
|
10711
|
-
}
|
|
10712
|
-
return out2;
|
|
10713
|
-
}
|
|
10714
|
-
function resolveConfigPath(projectRoot) {
|
|
10715
|
-
const configPath = join17(projectRoot, "b4mal.config.json");
|
|
10716
|
-
if (!existsSync14(configPath)) {
|
|
10717
|
-
throw new Error(`Configuration file not found: b4mal.config.json (searched in ${projectRoot}). Run 'b4mal init' to create one.`);
|
|
10718
|
-
}
|
|
10719
|
-
return configPath;
|
|
10720
|
-
}
|
|
10721
|
-
function verifyPathBoundary(configPath, projectRoot) {
|
|
10722
|
-
let realConfigPath;
|
|
10723
|
-
let realRoot;
|
|
10724
|
-
try {
|
|
10725
|
-
realConfigPath = realpathSync(configPath);
|
|
10726
|
-
realRoot = realpathSync(projectRoot);
|
|
10727
|
-
} catch (e) {
|
|
10728
|
-
throw new Error(`Cannot resolve config file path: ${e.message}. Check that b4mal.config.json exists and is accessible.`);
|
|
10729
|
-
}
|
|
10730
|
-
const rootPrefix = realRoot.replace(/\\/g, "/") + "/";
|
|
10731
|
-
const normConfig = realConfigPath.replace(/\\/g, "/");
|
|
10732
|
-
if (!normConfig.startsWith(rootPrefix) && normConfig !== realRoot.replace(/\\/g, "/")) {
|
|
10733
|
-
throw new Error(`Config file resolves outside project root: ${realConfigPath}. Symlinks to external files are not allowed.`);
|
|
10734
|
-
}
|
|
10735
|
-
}
|
|
10736
|
-
function readAndValidate(configPath) {
|
|
10737
|
-
let raw;
|
|
10738
|
-
try {
|
|
10739
|
-
raw = JSON.parse(readFileSync10(configPath, "utf-8"));
|
|
10740
|
-
} catch (e) {
|
|
10741
|
-
throw new Error(`Failed to parse b4mal.config.json: ${e.message}`);
|
|
10742
|
-
}
|
|
10743
|
-
const result = B4malConfigSchema.safeParse(raw);
|
|
10744
|
-
if (!result.success) {
|
|
10745
|
-
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join(`
|
|
10746
|
-
`);
|
|
10747
|
-
throw new Error(`Invalid configuration in b4mal.config.json:
|
|
10748
|
-
${issues}`);
|
|
10749
|
-
}
|
|
10750
|
-
return result.data;
|
|
10751
|
-
}
|
|
10752
|
-
function loadConfig(projectRoot) {
|
|
10753
|
-
const configPath = resolveConfigPath(projectRoot);
|
|
10754
|
-
verifyPathBoundary(configPath, projectRoot);
|
|
10755
|
-
return readAndValidate(configPath);
|
|
10756
|
-
}
|
|
10757
|
-
function configToTasks(config) {
|
|
10758
|
-
const ids = Object.keys(config.tasks).sort(sortStrings);
|
|
10759
|
-
const tasks = [];
|
|
10760
|
-
for (const id of ids) {
|
|
10761
|
-
const t = config.tasks[id];
|
|
10762
|
-
const matrix = t.matrix;
|
|
10763
|
-
if (matrix) {
|
|
10764
|
-
const expanded = expandMatrix(id, t, matrix);
|
|
10765
|
-
tasks.push(...expanded);
|
|
10766
|
-
} else {
|
|
10767
|
-
tasks.push(buildTask(id, t));
|
|
10768
|
-
}
|
|
10769
|
-
}
|
|
10770
|
-
const seen = new Set;
|
|
10771
|
-
for (const task of tasks) {
|
|
10772
|
-
if (seen.has(task.id)) {
|
|
10773
|
-
throw new Error(`Duplicate task ID after matrix expansion: "${task.id}". Check for collisions between manual task names and matrix-generated names.`);
|
|
10774
|
-
}
|
|
10775
|
-
seen.add(task.id);
|
|
10776
|
-
}
|
|
10777
|
-
return tasks;
|
|
10778
|
-
}
|
|
10779
|
-
function validateMatrixAxes(baseId, axes, matrix) {
|
|
10780
|
-
for (const axis of axes) {
|
|
10781
|
-
if (!VALID_MATRIX_VALUE.test(axis)) {
|
|
10782
|
-
throw new Error(`Matrix axis name "${axis}" contains invalid characters. Use [a-zA-Z0-9_.-]`);
|
|
10783
|
-
}
|
|
10784
|
-
if (matrix[axis].length === 0) {
|
|
10785
|
-
throw new Error(`Matrix axis "${axis}" for task "${baseId}" has zero values`);
|
|
10786
|
-
}
|
|
10787
|
-
const invalidVal = matrix[axis].find((v) => !VALID_MATRIX_VALUE.test(v));
|
|
10788
|
-
if (invalidVal) {
|
|
10789
|
-
throw new Error(`Matrix value "${invalidVal}" in axis "${axis}" contains invalid characters. Use [a-zA-Z0-9_.-]`);
|
|
10790
|
-
}
|
|
10791
|
-
}
|
|
10792
|
-
}
|
|
10793
|
-
function cartesianProduct(axes, matrix) {
|
|
10794
|
-
return axes.reduce((combos, axis) => {
|
|
10795
|
-
const sortedVals = matrix[axis].slice().sort(sortStrings);
|
|
10796
|
-
return combos.flatMap((combo) => sortedVals.map((val) => ({ ...combo, [axis]: val })));
|
|
10797
|
-
}, [{}]);
|
|
10798
|
-
}
|
|
10799
|
-
function expandMatrix(baseId, t, matrix) {
|
|
10800
|
-
const axes = Object.keys(matrix).sort(sortStrings);
|
|
10801
|
-
if (axes.length === 0)
|
|
10802
|
-
return [buildTask(baseId, t)];
|
|
10803
|
-
validateMatrixAxes(baseId, axes, matrix);
|
|
10804
|
-
const combinations = cartesianProduct(axes, matrix);
|
|
10805
|
-
return combinations.map((combo) => {
|
|
10806
|
-
const suffix = axes.map((a) => `${a}=${combo[a]}`).join("-");
|
|
10807
|
-
return buildTask(`${baseId}-${suffix}`, t, combo);
|
|
10808
|
-
});
|
|
10809
|
-
}
|
|
10810
|
-
function buildTask(id, t, matrixVars) {
|
|
10811
|
-
const norm = (arr) => sortedSet((arr ?? []).map((p) => p.replace(/\\/g, "/")));
|
|
10812
|
-
const env = sortedRecordKeys(t.env ?? {});
|
|
10813
|
-
if (matrixVars) {
|
|
10814
|
-
for (const [k, v] of Object.entries(matrixVars)) {
|
|
10815
|
-
env[`MATRIX_${k.toUpperCase()}`] = v;
|
|
10816
|
-
}
|
|
10817
|
-
}
|
|
10818
|
-
const task = {
|
|
10819
|
-
id,
|
|
10820
|
-
cmd: t.cmd ?? [],
|
|
10821
|
-
dependencies: sortedSet(t.dependencies ?? []),
|
|
10822
|
-
inputs: norm(t.inputs),
|
|
10823
|
-
outputs: norm(t.outputs),
|
|
10824
|
-
claims: norm(t.claims),
|
|
10825
|
-
needsEnv: sortedSet(t.needsEnv ?? []),
|
|
10826
|
-
providesEnv: sortedSet(t.providesEnv ?? []),
|
|
10827
|
-
secrets: sortedSet(t.secrets ?? []),
|
|
10828
|
-
env,
|
|
10829
|
-
timeout: t.timeout ?? 300000,
|
|
10830
|
-
cache: t.cache ?? true,
|
|
10831
|
-
when: t.when
|
|
10832
|
-
};
|
|
10833
|
-
if (t.cwd)
|
|
10834
|
-
task.cwd = t.cwd.replace(/\\/g, "/");
|
|
10835
|
-
return task;
|
|
10836
|
-
}
|
|
10837
|
-
function writeLockfileAtomic(tasks, lockPath) {
|
|
10838
|
-
const configHash = computeConfigHash(tasks);
|
|
10839
|
-
const envelope = {
|
|
10840
|
-
version: 2,
|
|
10841
|
-
_meta: { configHash },
|
|
10842
|
-
tasks
|
|
10843
|
-
};
|
|
10844
|
-
const json = JSON.stringify(envelope, null, 2);
|
|
10845
|
-
const dir = dirname7(lockPath);
|
|
10846
|
-
const tmpName = `.b4mal-lock-${randomBytes(8).toString("hex")}.tmp`;
|
|
10847
|
-
const tmpPath = join17(dir, tmpName);
|
|
10848
|
-
try {
|
|
10849
|
-
writeFileSync4(tmpPath, json, "utf-8");
|
|
10850
|
-
renameSync(tmpPath, lockPath);
|
|
10851
|
-
} catch (e) {
|
|
10852
|
-
try {
|
|
10853
|
-
if (existsSync14(tmpPath))
|
|
10854
|
-
rmSync(tmpPath);
|
|
10855
|
-
} catch {}
|
|
10856
|
-
throw e;
|
|
10857
|
-
}
|
|
10858
|
-
}
|
|
10859
|
-
function computeConfigHash(tasks) {
|
|
10860
|
-
const hash = createHash2("sha256");
|
|
10861
|
-
hash.update(JSON.stringify(tasks));
|
|
10862
|
-
return `sha256:${hash.digest("hex")}`;
|
|
10863
|
-
}
|
|
10864
|
-
function isConfigStale(projectRoot) {
|
|
10865
|
-
const configPath = join17(projectRoot, "b4mal.config.json");
|
|
10866
|
-
const lockPath = join17(projectRoot, "b4mal.lock");
|
|
10867
|
-
if (!existsSync14(configPath))
|
|
10868
|
-
return false;
|
|
10869
|
-
if (!existsSync14(lockPath))
|
|
10870
|
-
return true;
|
|
10871
|
-
const configMtime = statSync(configPath).mtimeMs;
|
|
10872
|
-
const lockMtime = statSync(lockPath).mtimeMs;
|
|
10873
|
-
return configMtime > lockMtime;
|
|
10874
|
-
}
|
|
10875
|
-
var VALID_MATRIX_VALUE;
|
|
10876
|
-
var init_config_loader = __esm(() => {
|
|
10877
|
-
init_schema();
|
|
10878
|
-
VALID_MATRIX_VALUE = /^[a-zA-Z0-9_.-]+$/;
|
|
10879
|
-
});
|
|
10880
|
-
|
|
10881
|
-
// src/cli/remote.ts
|
|
10882
|
-
var exports_remote = {};
|
|
10883
|
-
__export(exports_remote, {
|
|
10884
|
-
RemoteCommand: () => RemoteCommand
|
|
10885
|
-
});
|
|
10886
|
-
|
|
10887
|
-
class RemoteCommand {
|
|
10888
|
-
static async execute(args) {
|
|
10889
|
-
const sub = args[0];
|
|
10890
|
-
if (sub === "status" || !sub) {
|
|
10891
|
-
await this.status();
|
|
10892
|
-
} else {
|
|
10893
|
-
process.stderr.write(`${c3.red}[FAIL] Unknown remote command: ${sub}. Try: b4mal remote status${c3.reset}
|
|
10894
|
-
`);
|
|
10895
|
-
process.exit(1);
|
|
10896
|
-
}
|
|
10897
|
-
}
|
|
10898
|
-
static async status() {
|
|
10899
|
-
const bucket = process.env.B4MAL_CACHE_BUCKET;
|
|
10900
|
-
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
10901
|
-
const endpoint = process.env.AWS_S3_ENDPOINT;
|
|
10902
|
-
process.stdout.write(`
|
|
10903
|
-
${c3.bold}L2 Remote Cache Status${c3.reset}
|
|
10904
|
-
|
|
10905
|
-
`);
|
|
10906
|
-
if (!bucket || !accessKeyId) {
|
|
10907
|
-
process.stdout.write(`${c3.dim} Not configured.${c3.reset}
|
|
10908
|
-
`);
|
|
10909
|
-
process.stdout.write(`${c3.dim} Set B4MAL_CACHE_BUCKET + AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY to enable.${c3.reset}
|
|
10910
|
-
|
|
10911
|
-
`);
|
|
10912
|
-
process.exit(0);
|
|
10746
|
+
static async status() {
|
|
10747
|
+
const bucket = process.env.B4MAL_CACHE_BUCKET;
|
|
10748
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
10749
|
+
const endpoint = process.env.AWS_S3_ENDPOINT;
|
|
10750
|
+
process.stdout.write(`
|
|
10751
|
+
${c3.bold}L2 Remote Cache Status${c3.reset}
|
|
10752
|
+
|
|
10753
|
+
`);
|
|
10754
|
+
if (!bucket || !accessKeyId) {
|
|
10755
|
+
process.stdout.write(`${c3.dim} Not configured.${c3.reset}
|
|
10756
|
+
`);
|
|
10757
|
+
process.stdout.write(`${c3.dim} Set B4MAL_CACHE_BUCKET + AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY to enable.${c3.reset}
|
|
10758
|
+
|
|
10759
|
+
`);
|
|
10760
|
+
process.exit(0);
|
|
10913
10761
|
}
|
|
10914
10762
|
process.stdout.write(`${c3.dim} Endpoint:${c3.reset} ${endpoint || "AWS S3 (default)"}
|
|
10915
10763
|
`);
|
|
@@ -10959,21 +10807,21 @@ var exports_watch = {};
|
|
|
10959
10807
|
__export(exports_watch, {
|
|
10960
10808
|
WatchCommand: () => WatchCommand
|
|
10961
10809
|
});
|
|
10962
|
-
import { watch, readFileSync as
|
|
10963
|
-
import { join as
|
|
10810
|
+
import { watch, readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
10811
|
+
import { join as join17 } from "path";
|
|
10964
10812
|
function collectWatchDirs(projectRoot) {
|
|
10965
|
-
const lockPath =
|
|
10966
|
-
if (!
|
|
10813
|
+
const lockPath = join17(projectRoot, "b4mal.lock");
|
|
10814
|
+
if (!existsSync14(lockPath))
|
|
10967
10815
|
return new Set;
|
|
10968
|
-
const raw = JSON.parse(
|
|
10816
|
+
const raw = JSON.parse(readFileSync10(lockPath, "utf-8"));
|
|
10969
10817
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
10970
10818
|
const dirs = new Set;
|
|
10971
10819
|
for (const t of tasks) {
|
|
10972
10820
|
for (const input of t.inputs ?? []) {
|
|
10973
|
-
dirs.add(
|
|
10821
|
+
dirs.add(join17(projectRoot, input));
|
|
10974
10822
|
}
|
|
10975
10823
|
}
|
|
10976
|
-
dirs.add(
|
|
10824
|
+
dirs.add(join17(projectRoot, "b4mal.config.json"));
|
|
10977
10825
|
dirs.add(lockPath);
|
|
10978
10826
|
return dirs;
|
|
10979
10827
|
}
|
|
@@ -11033,6 +10881,85 @@ var init_watch = __esm(() => {
|
|
|
11033
10881
|
init_engine();
|
|
11034
10882
|
});
|
|
11035
10883
|
|
|
10884
|
+
// src/cli/attest.ts
|
|
10885
|
+
var exports_attest = {};
|
|
10886
|
+
__export(exports_attest, {
|
|
10887
|
+
AttestHandler: () => AttestHandler
|
|
10888
|
+
});
|
|
10889
|
+
|
|
10890
|
+
class AttestHandler {
|
|
10891
|
+
static parseArgs(args) {
|
|
10892
|
+
const taskName = args[0] ?? "";
|
|
10893
|
+
const reads = [];
|
|
10894
|
+
const writes = [];
|
|
10895
|
+
const envReads = [];
|
|
10896
|
+
const envWrites = [];
|
|
10897
|
+
const ports = [];
|
|
10898
|
+
for (let i = 1;i < args.length; i++) {
|
|
10899
|
+
const arg = args[i];
|
|
10900
|
+
if (arg.startsWith("fs:write:")) {
|
|
10901
|
+
writes.push(arg.slice(9));
|
|
10902
|
+
} else if (arg.startsWith("fs:read:")) {
|
|
10903
|
+
reads.push(arg.slice(8));
|
|
10904
|
+
} else if (arg.startsWith("fs:")) {
|
|
10905
|
+
reads.push(arg.slice(3));
|
|
10906
|
+
} else if (arg.startsWith("env:write:")) {
|
|
10907
|
+
envWrites.push(arg.slice(10));
|
|
10908
|
+
} else if (arg.startsWith("env:read:")) {
|
|
10909
|
+
envReads.push(arg.slice(9));
|
|
10910
|
+
} else if (arg.startsWith("env:")) {
|
|
10911
|
+
envReads.push(arg.slice(4));
|
|
10912
|
+
} else if (arg.startsWith("port:")) {
|
|
10913
|
+
ports.push(arg.slice(5));
|
|
10914
|
+
}
|
|
10915
|
+
}
|
|
10916
|
+
return { taskName, reads, writes, envReads, envWrites, ports };
|
|
10917
|
+
}
|
|
10918
|
+
static identifyCaller(env) {
|
|
10919
|
+
const caller = env.B4MAL_CALLER;
|
|
10920
|
+
if (!caller)
|
|
10921
|
+
return { name: "unknown", version: "unknown" };
|
|
10922
|
+
const vIdx = caller.lastIndexOf("-v");
|
|
10923
|
+
if (vIdx === -1)
|
|
10924
|
+
return { name: caller, version: "unknown" };
|
|
10925
|
+
return {
|
|
10926
|
+
name: caller.slice(0, vIdx),
|
|
10927
|
+
version: caller.slice(vIdx + 1)
|
|
10928
|
+
};
|
|
10929
|
+
}
|
|
10930
|
+
static toClaim(args) {
|
|
10931
|
+
return {
|
|
10932
|
+
id: args.taskName,
|
|
10933
|
+
reads: [...args.reads],
|
|
10934
|
+
writes: [...args.writes, ...args.ports.map((p) => `port:${p}`)],
|
|
10935
|
+
envReads: [...args.envReads],
|
|
10936
|
+
envWrites: [...args.envWrites]
|
|
10937
|
+
};
|
|
10938
|
+
}
|
|
10939
|
+
static async execute(args, env) {
|
|
10940
|
+
try {
|
|
10941
|
+
if (args.length === 0 || !args[0]) {
|
|
10942
|
+
return { accepted: false, taskName: "", error: "Missing task name" };
|
|
10943
|
+
}
|
|
10944
|
+
const parsed = this.parseArgs(args);
|
|
10945
|
+
const caller = this.identifyCaller(env);
|
|
10946
|
+
const claim = this.toClaim(parsed);
|
|
10947
|
+
return {
|
|
10948
|
+
accepted: true,
|
|
10949
|
+
taskName: parsed.taskName,
|
|
10950
|
+
caller,
|
|
10951
|
+
claim
|
|
10952
|
+
};
|
|
10953
|
+
} catch (e) {
|
|
10954
|
+
return {
|
|
10955
|
+
accepted: false,
|
|
10956
|
+
taskName: args[0] ?? "",
|
|
10957
|
+
error: `Attestation failed: ${e instanceof Error ? e.message : String(e)}`
|
|
10958
|
+
};
|
|
10959
|
+
}
|
|
10960
|
+
}
|
|
10961
|
+
}
|
|
10962
|
+
|
|
11036
10963
|
// src/plugin/wasm_registry.ts
|
|
11037
10964
|
var exports_wasm_registry = {};
|
|
11038
10965
|
__export(exports_wasm_registry, {
|
|
@@ -11051,7 +10978,7 @@ class WasmRegistry {
|
|
|
11051
10978
|
}
|
|
11052
10979
|
}
|
|
11053
10980
|
async install(url, name) {
|
|
11054
|
-
if (name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
10981
|
+
if (!name || name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
11055
10982
|
throw new Error("Invalid plugin name");
|
|
11056
10983
|
}
|
|
11057
10984
|
let res;
|
|
@@ -11077,7 +11004,7 @@ class WasmRegistry {
|
|
|
11077
11004
|
return pluginPath;
|
|
11078
11005
|
}
|
|
11079
11006
|
async run(name) {
|
|
11080
|
-
if (name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
11007
|
+
if (!name || name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
11081
11008
|
throw new Error("Invalid plugin name");
|
|
11082
11009
|
}
|
|
11083
11010
|
const pluginPath = path11.join(this.registryDir, `${name}.wasm`);
|
|
@@ -14763,10 +14690,10 @@ var require_resolve_block_map = __commonJS((exports) => {
|
|
|
14763
14690
|
let offset = bm.offset;
|
|
14764
14691
|
let commentEnd = null;
|
|
14765
14692
|
for (const collItem of bm.items) {
|
|
14766
|
-
const { start, key, sep
|
|
14693
|
+
const { start, key, sep, value } = collItem;
|
|
14767
14694
|
const keyProps = resolveProps.resolveProps(start, {
|
|
14768
14695
|
indicator: "explicit-key-ind",
|
|
14769
|
-
next: key ??
|
|
14696
|
+
next: key ?? sep?.[0],
|
|
14770
14697
|
offset,
|
|
14771
14698
|
onError,
|
|
14772
14699
|
parentIndent: bm.indent,
|
|
@@ -14780,7 +14707,7 @@ var require_resolve_block_map = __commonJS((exports) => {
|
|
|
14780
14707
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
14781
14708
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
14782
14709
|
}
|
|
14783
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
14710
|
+
if (!keyProps.anchor && !keyProps.tag && !sep) {
|
|
14784
14711
|
commentEnd = keyProps.end;
|
|
14785
14712
|
if (keyProps.comment) {
|
|
14786
14713
|
if (map.comment)
|
|
@@ -14805,7 +14732,7 @@ var require_resolve_block_map = __commonJS((exports) => {
|
|
|
14805
14732
|
ctx.atKey = false;
|
|
14806
14733
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
14807
14734
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
14808
|
-
const valueProps = resolveProps.resolveProps(
|
|
14735
|
+
const valueProps = resolveProps.resolveProps(sep ?? [], {
|
|
14809
14736
|
indicator: "map-value-ind",
|
|
14810
14737
|
next: value,
|
|
14811
14738
|
offset: keyNode.range[2],
|
|
@@ -14821,7 +14748,7 @@ var require_resolve_block_map = __commonJS((exports) => {
|
|
|
14821
14748
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
14822
14749
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
14823
14750
|
}
|
|
14824
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
14751
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep, null, valueProps, onError);
|
|
14825
14752
|
if (ctx.schema.compat)
|
|
14826
14753
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
14827
14754
|
offset = valueNode.range[2];
|
|
@@ -14907,7 +14834,7 @@ var require_resolve_end = __commonJS((exports) => {
|
|
|
14907
14834
|
let comment = "";
|
|
14908
14835
|
if (end) {
|
|
14909
14836
|
let hasSpace = false;
|
|
14910
|
-
let
|
|
14837
|
+
let sep = "";
|
|
14911
14838
|
for (const token of end) {
|
|
14912
14839
|
const { source, type } = token;
|
|
14913
14840
|
switch (type) {
|
|
@@ -14921,13 +14848,13 @@ var require_resolve_end = __commonJS((exports) => {
|
|
|
14921
14848
|
if (!comment)
|
|
14922
14849
|
comment = cb;
|
|
14923
14850
|
else
|
|
14924
|
-
comment +=
|
|
14925
|
-
|
|
14851
|
+
comment += sep + cb;
|
|
14852
|
+
sep = "";
|
|
14926
14853
|
break;
|
|
14927
14854
|
}
|
|
14928
14855
|
case "newline":
|
|
14929
14856
|
if (comment)
|
|
14930
|
-
|
|
14857
|
+
sep += source;
|
|
14931
14858
|
hasSpace = true;
|
|
14932
14859
|
break;
|
|
14933
14860
|
default:
|
|
@@ -14967,18 +14894,18 @@ var require_resolve_flow_collection = __commonJS((exports) => {
|
|
|
14967
14894
|
let offset = fc.offset + fc.start.source.length;
|
|
14968
14895
|
for (let i = 0;i < fc.items.length; ++i) {
|
|
14969
14896
|
const collItem = fc.items[i];
|
|
14970
|
-
const { start, key, sep
|
|
14897
|
+
const { start, key, sep, value } = collItem;
|
|
14971
14898
|
const props = resolveProps.resolveProps(start, {
|
|
14972
14899
|
flow: fcName,
|
|
14973
14900
|
indicator: "explicit-key-ind",
|
|
14974
|
-
next: key ??
|
|
14901
|
+
next: key ?? sep?.[0],
|
|
14975
14902
|
offset,
|
|
14976
14903
|
onError,
|
|
14977
14904
|
parentIndent: fc.indent,
|
|
14978
14905
|
startOnNewline: false
|
|
14979
14906
|
});
|
|
14980
14907
|
if (!props.found) {
|
|
14981
|
-
if (!props.anchor && !props.tag && !
|
|
14908
|
+
if (!props.anchor && !props.tag && !sep && !value) {
|
|
14982
14909
|
if (i === 0 && props.comma)
|
|
14983
14910
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
14984
14911
|
else if (i < fc.items.length - 1)
|
|
@@ -15030,8 +14957,8 @@ var require_resolve_flow_collection = __commonJS((exports) => {
|
|
|
15030
14957
|
}
|
|
15031
14958
|
}
|
|
15032
14959
|
}
|
|
15033
|
-
if (!isMap && !
|
|
15034
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
14960
|
+
if (!isMap && !sep && !props.found) {
|
|
14961
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
|
|
15035
14962
|
coll.items.push(valueNode);
|
|
15036
14963
|
offset = valueNode.range[2];
|
|
15037
14964
|
if (isBlock(value))
|
|
@@ -15043,7 +14970,7 @@ var require_resolve_flow_collection = __commonJS((exports) => {
|
|
|
15043
14970
|
if (isBlock(key))
|
|
15044
14971
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
15045
14972
|
ctx.atKey = false;
|
|
15046
|
-
const valueProps = resolveProps.resolveProps(
|
|
14973
|
+
const valueProps = resolveProps.resolveProps(sep ?? [], {
|
|
15047
14974
|
flow: fcName,
|
|
15048
14975
|
indicator: "map-value-ind",
|
|
15049
14976
|
next: value,
|
|
@@ -15054,8 +14981,8 @@ var require_resolve_flow_collection = __commonJS((exports) => {
|
|
|
15054
14981
|
});
|
|
15055
14982
|
if (valueProps.found) {
|
|
15056
14983
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
15057
|
-
if (
|
|
15058
|
-
for (const st of
|
|
14984
|
+
if (sep)
|
|
14985
|
+
for (const st of sep) {
|
|
15059
14986
|
if (st === valueProps.found)
|
|
15060
14987
|
break;
|
|
15061
14988
|
if (st.type === "newline") {
|
|
@@ -15072,7 +14999,7 @@ var require_resolve_flow_collection = __commonJS((exports) => {
|
|
|
15072
14999
|
else
|
|
15073
15000
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
15074
15001
|
}
|
|
15075
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
15002
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep, null, valueProps, onError) : null;
|
|
15076
15003
|
if (valueNode) {
|
|
15077
15004
|
if (isBlock(value))
|
|
15078
15005
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -15249,7 +15176,7 @@ var require_resolve_block_scalar = __commonJS((exports) => {
|
|
|
15249
15176
|
chompStart = i + 1;
|
|
15250
15177
|
}
|
|
15251
15178
|
let value = "";
|
|
15252
|
-
let
|
|
15179
|
+
let sep = "";
|
|
15253
15180
|
let prevMoreIndented = false;
|
|
15254
15181
|
for (let i = 0;i < contentStart; ++i)
|
|
15255
15182
|
value += lines[i][0].slice(trimIndent) + `
|
|
@@ -15267,33 +15194,33 @@ var require_resolve_block_scalar = __commonJS((exports) => {
|
|
|
15267
15194
|
indent = "";
|
|
15268
15195
|
}
|
|
15269
15196
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
15270
|
-
value +=
|
|
15271
|
-
|
|
15197
|
+
value += sep + indent.slice(trimIndent) + content;
|
|
15198
|
+
sep = `
|
|
15272
15199
|
`;
|
|
15273
15200
|
} else if (indent.length > trimIndent || content[0] === "\t") {
|
|
15274
|
-
if (
|
|
15275
|
-
|
|
15201
|
+
if (sep === " ")
|
|
15202
|
+
sep = `
|
|
15276
15203
|
`;
|
|
15277
|
-
else if (!prevMoreIndented &&
|
|
15204
|
+
else if (!prevMoreIndented && sep === `
|
|
15278
15205
|
`)
|
|
15279
|
-
|
|
15206
|
+
sep = `
|
|
15280
15207
|
|
|
15281
15208
|
`;
|
|
15282
|
-
value +=
|
|
15283
|
-
|
|
15209
|
+
value += sep + indent.slice(trimIndent) + content;
|
|
15210
|
+
sep = `
|
|
15284
15211
|
`;
|
|
15285
15212
|
prevMoreIndented = true;
|
|
15286
15213
|
} else if (content === "") {
|
|
15287
|
-
if (
|
|
15214
|
+
if (sep === `
|
|
15288
15215
|
`)
|
|
15289
15216
|
value += `
|
|
15290
15217
|
`;
|
|
15291
15218
|
else
|
|
15292
|
-
|
|
15219
|
+
sep = `
|
|
15293
15220
|
`;
|
|
15294
15221
|
} else {
|
|
15295
|
-
value +=
|
|
15296
|
-
|
|
15222
|
+
value += sep + content;
|
|
15223
|
+
sep = " ";
|
|
15297
15224
|
prevMoreIndented = false;
|
|
15298
15225
|
}
|
|
15299
15226
|
}
|
|
@@ -15474,27 +15401,27 @@ var require_resolve_flow_scalar = __commonJS((exports) => {
|
|
|
15474
15401
|
if (!match)
|
|
15475
15402
|
return source;
|
|
15476
15403
|
let res = match[1];
|
|
15477
|
-
let
|
|
15404
|
+
let sep = " ";
|
|
15478
15405
|
let pos = first.lastIndex;
|
|
15479
15406
|
line.lastIndex = pos;
|
|
15480
15407
|
while (match = line.exec(source)) {
|
|
15481
15408
|
if (match[1] === "") {
|
|
15482
|
-
if (
|
|
15409
|
+
if (sep === `
|
|
15483
15410
|
`)
|
|
15484
|
-
res +=
|
|
15411
|
+
res += sep;
|
|
15485
15412
|
else
|
|
15486
|
-
|
|
15413
|
+
sep = `
|
|
15487
15414
|
`;
|
|
15488
15415
|
} else {
|
|
15489
|
-
res +=
|
|
15490
|
-
|
|
15416
|
+
res += sep + match[1];
|
|
15417
|
+
sep = " ";
|
|
15491
15418
|
}
|
|
15492
15419
|
pos = line.lastIndex;
|
|
15493
15420
|
}
|
|
15494
15421
|
const last = /[ \t]*(.*)/sy;
|
|
15495
15422
|
last.lastIndex = pos;
|
|
15496
15423
|
match = last.exec(source);
|
|
15497
|
-
return res +
|
|
15424
|
+
return res + sep + (match?.[1] ?? "");
|
|
15498
15425
|
}
|
|
15499
15426
|
function doubleQuotedValue(source, onError) {
|
|
15500
15427
|
let res = "";
|
|
@@ -16267,14 +16194,14 @@ var require_cst_stringify = __commonJS((exports) => {
|
|
|
16267
16194
|
}
|
|
16268
16195
|
}
|
|
16269
16196
|
}
|
|
16270
|
-
function stringifyItem({ start, key, sep
|
|
16197
|
+
function stringifyItem({ start, key, sep, value }) {
|
|
16271
16198
|
let res = "";
|
|
16272
16199
|
for (const st of start)
|
|
16273
16200
|
res += st.source;
|
|
16274
16201
|
if (key)
|
|
16275
16202
|
res += stringifyToken(key);
|
|
16276
|
-
if (
|
|
16277
|
-
for (const st of
|
|
16203
|
+
if (sep)
|
|
16204
|
+
for (const st of sep)
|
|
16278
16205
|
res += st.source;
|
|
16279
16206
|
if (value)
|
|
16280
16207
|
res += stringifyToken(value);
|
|
@@ -17404,18 +17331,18 @@ var require_parser = __commonJS((exports) => {
|
|
|
17404
17331
|
if (this.type === "map-value-ind") {
|
|
17405
17332
|
const prev = getPrevProps(this.peek(2));
|
|
17406
17333
|
const start = getFirstKeyStartProps(prev);
|
|
17407
|
-
let
|
|
17334
|
+
let sep;
|
|
17408
17335
|
if (scalar.end) {
|
|
17409
|
-
|
|
17410
|
-
|
|
17336
|
+
sep = scalar.end;
|
|
17337
|
+
sep.push(this.sourceToken);
|
|
17411
17338
|
delete scalar.end;
|
|
17412
17339
|
} else
|
|
17413
|
-
|
|
17340
|
+
sep = [this.sourceToken];
|
|
17414
17341
|
const map = {
|
|
17415
17342
|
type: "block-map",
|
|
17416
17343
|
offset: scalar.offset,
|
|
17417
17344
|
indent: scalar.indent,
|
|
17418
|
-
items: [{ start, key: scalar, sep
|
|
17345
|
+
items: [{ start, key: scalar, sep }]
|
|
17419
17346
|
};
|
|
17420
17347
|
this.onKeyLine = true;
|
|
17421
17348
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -17569,15 +17496,15 @@ var require_parser = __commonJS((exports) => {
|
|
|
17569
17496
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
17570
17497
|
const start2 = getFirstKeyStartProps(it.start);
|
|
17571
17498
|
const key = it.key;
|
|
17572
|
-
const
|
|
17573
|
-
|
|
17499
|
+
const sep = it.sep;
|
|
17500
|
+
sep.push(this.sourceToken);
|
|
17574
17501
|
delete it.key;
|
|
17575
17502
|
delete it.sep;
|
|
17576
17503
|
this.stack.push({
|
|
17577
17504
|
type: "block-map",
|
|
17578
17505
|
offset: this.offset,
|
|
17579
17506
|
indent: this.indent,
|
|
17580
|
-
items: [{ start: start2, key, sep
|
|
17507
|
+
items: [{ start: start2, key, sep }]
|
|
17581
17508
|
});
|
|
17582
17509
|
} else if (start.length > 0) {
|
|
17583
17510
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -17771,13 +17698,13 @@ var require_parser = __commonJS((exports) => {
|
|
|
17771
17698
|
const prev = getPrevProps(parent);
|
|
17772
17699
|
const start = getFirstKeyStartProps(prev);
|
|
17773
17700
|
fixFlowSeqItems(fc);
|
|
17774
|
-
const
|
|
17775
|
-
|
|
17701
|
+
const sep = fc.end.splice(1, fc.end.length);
|
|
17702
|
+
sep.push(this.sourceToken);
|
|
17776
17703
|
const map = {
|
|
17777
17704
|
type: "block-map",
|
|
17778
17705
|
offset: fc.offset,
|
|
17779
17706
|
indent: fc.indent,
|
|
17780
|
-
items: [{ start, key: fc, sep
|
|
17707
|
+
items: [{ start, key: fc, sep }]
|
|
17781
17708
|
};
|
|
17782
17709
|
this.onKeyLine = true;
|
|
17783
17710
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -18049,6 +17976,107 @@ var init_dist = __esm(() => {
|
|
|
18049
17976
|
$visitAsync = visit.visitAsync;
|
|
18050
17977
|
});
|
|
18051
17978
|
|
|
17979
|
+
// src/schema.ts
|
|
17980
|
+
function sanitizePath(p) {
|
|
17981
|
+
if (p.startsWith("/")) {
|
|
17982
|
+
throw new Error(`Absolute paths not allowed: "${p}"`);
|
|
17983
|
+
}
|
|
17984
|
+
if (p.split("/").some((seg) => seg === "..")) {
|
|
17985
|
+
throw new Error(`Path traversal not allowed: "${p}"`);
|
|
17986
|
+
}
|
|
17987
|
+
return p.replace(/\\/g, "/");
|
|
17988
|
+
}
|
|
17989
|
+
function validTaskIds(tasks) {
|
|
17990
|
+
return Object.keys(tasks).every((k) => VALID_TASK_ID.test(k));
|
|
17991
|
+
}
|
|
17992
|
+
function allDepsResolve(tasks) {
|
|
17993
|
+
const ids = new Set(Object.keys(tasks));
|
|
17994
|
+
for (const task of Object.values(tasks)) {
|
|
17995
|
+
for (const dep of task.dependencies) {
|
|
17996
|
+
if (!ids.has(dep))
|
|
17997
|
+
return false;
|
|
17998
|
+
}
|
|
17999
|
+
}
|
|
18000
|
+
return true;
|
|
18001
|
+
}
|
|
18002
|
+
function cartesianSize(matrix) {
|
|
18003
|
+
let size = 1;
|
|
18004
|
+
for (const vals of Object.values(matrix)) {
|
|
18005
|
+
size *= vals.length;
|
|
18006
|
+
}
|
|
18007
|
+
return size;
|
|
18008
|
+
}
|
|
18009
|
+
function hasNoCycles(tasks) {
|
|
18010
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
18011
|
+
const color = new Map;
|
|
18012
|
+
for (const id of Object.keys(tasks))
|
|
18013
|
+
color.set(id, WHITE);
|
|
18014
|
+
function dfs(id) {
|
|
18015
|
+
const c4 = color.get(id);
|
|
18016
|
+
if (c4 === GRAY)
|
|
18017
|
+
return false;
|
|
18018
|
+
if (c4 === BLACK)
|
|
18019
|
+
return true;
|
|
18020
|
+
color.set(id, GRAY);
|
|
18021
|
+
for (const dep of tasks[id]?.dependencies ?? []) {
|
|
18022
|
+
if (!dfs(dep))
|
|
18023
|
+
return false;
|
|
18024
|
+
}
|
|
18025
|
+
color.set(id, BLACK);
|
|
18026
|
+
return true;
|
|
18027
|
+
}
|
|
18028
|
+
for (const id of color.keys()) {
|
|
18029
|
+
if (color.get(id) === WHITE && !dfs(id))
|
|
18030
|
+
return false;
|
|
18031
|
+
}
|
|
18032
|
+
return true;
|
|
18033
|
+
}
|
|
18034
|
+
var TaskConfigSchema, VALID_TASK_ID, B4malConfigSchema, TaskSchema, PipelineSchema;
|
|
18035
|
+
var init_schema = __esm(() => {
|
|
18036
|
+
init_zod();
|
|
18037
|
+
TaskConfigSchema = exports_external.object({
|
|
18038
|
+
cmd: exports_external.array(exports_external.string()).min(1, "cmd must contain at least one element"),
|
|
18039
|
+
dependencies: exports_external.array(exports_external.string()).default([]),
|
|
18040
|
+
inputs: exports_external.array(exports_external.string()).default([]).transform((ps) => ps.map(sanitizePath)),
|
|
18041
|
+
outputs: exports_external.array(exports_external.string()).default([]).transform((ps) => ps.map(sanitizePath)),
|
|
18042
|
+
claims: exports_external.array(exports_external.string()).default([]).transform((ps) => ps.map((c4) => c4.startsWith("fs:") ? `fs:${sanitizePath(c4.slice(3))}` : c4)),
|
|
18043
|
+
needsEnv: exports_external.array(exports_external.string()).default([]),
|
|
18044
|
+
providesEnv: exports_external.array(exports_external.string()).default([]),
|
|
18045
|
+
secrets: exports_external.array(exports_external.string()).default([]),
|
|
18046
|
+
when: exports_external.object({
|
|
18047
|
+
branch: exports_external.string().optional().describe("Glob pattern for branch name"),
|
|
18048
|
+
platform: exports_external.array(exports_external.string()).optional().describe("OS platforms to run on"),
|
|
18049
|
+
if: exports_external.string().optional().describe("Arbitrary condition expression")
|
|
18050
|
+
}).optional(),
|
|
18051
|
+
matrix: exports_external.record(exports_external.array(exports_external.string()).max(10)).optional().refine((m) => !m || Object.keys(m).length <= 4, "Max 4 matrix axes").refine((m) => !m || cartesianSize(m) <= 256, "Max 256 matrix combinations"),
|
|
18052
|
+
env: exports_external.record(exports_external.string()).default({}),
|
|
18053
|
+
cwd: exports_external.string().optional().transform((p) => p ? p.replace(/\\/g, "/") : p).refine((p) => !p || !p.startsWith("/") && !p.split("/").some((seg) => seg === ".."), "cwd must be a relative path within the project root"),
|
|
18054
|
+
timeout: exports_external.number().int().nonnegative().max(3600000).transform((t) => t === 0 ? 300000 : t).default(300000),
|
|
18055
|
+
cache: exports_external.boolean().default(true)
|
|
18056
|
+
});
|
|
18057
|
+
VALID_TASK_ID = /^[a-zA-Z0-9_-]+$/;
|
|
18058
|
+
B4malConfigSchema = exports_external.object({
|
|
18059
|
+
name: exports_external.string().optional(),
|
|
18060
|
+
tasks: exports_external.record(TaskConfigSchema).refine((tasks) => Object.keys(tasks).length > 0, "At least one task is required").refine(validTaskIds, { message: "Task IDs must contain only alphanumeric characters, dashes, and underscores" }).refine(allDepsResolve, { message: "All dependency references must resolve to existing task IDs" }).refine(hasNoCycles, { message: "Circular dependency detected in task dependencies" }),
|
|
18061
|
+
concurrency: exports_external.number().int().nonnegative().default(0),
|
|
18062
|
+
env: exports_external.record(exports_external.string()).default({})
|
|
18063
|
+
});
|
|
18064
|
+
TaskSchema = exports_external.object({
|
|
18065
|
+
id: exports_external.string().min(1),
|
|
18066
|
+
cmd: exports_external.array(exports_external.string()).min(1),
|
|
18067
|
+
env: exports_external.record(exports_external.string()).default({}),
|
|
18068
|
+
dependencies: exports_external.array(exports_external.string()).default([]),
|
|
18069
|
+
cwd: exports_external.string().optional(),
|
|
18070
|
+
timeout: exports_external.number().int().nonnegative().default(0)
|
|
18071
|
+
});
|
|
18072
|
+
PipelineSchema = exports_external.object({
|
|
18073
|
+
name: exports_external.string().min(1),
|
|
18074
|
+
tasks: exports_external.array(TaskSchema).min(1),
|
|
18075
|
+
concurrency: exports_external.number().int().nonnegative().default(0),
|
|
18076
|
+
env: exports_external.record(exports_external.string()).default({})
|
|
18077
|
+
});
|
|
18078
|
+
});
|
|
18079
|
+
|
|
18052
18080
|
// src/shim/forecaster.ts
|
|
18053
18081
|
function measureVolatility(source) {
|
|
18054
18082
|
if (source.trim().length === 0)
|
|
@@ -18221,6 +18249,199 @@ var init_mint_transpiler = __esm(() => {
|
|
|
18221
18249
|
init_forecaster();
|
|
18222
18250
|
});
|
|
18223
18251
|
|
|
18252
|
+
// src/config_loader.ts
|
|
18253
|
+
var exports_config_loader = {};
|
|
18254
|
+
__export(exports_config_loader, {
|
|
18255
|
+
writeLockfileAtomic: () => writeLockfileAtomic,
|
|
18256
|
+
loadConfig: () => loadConfig,
|
|
18257
|
+
isConfigStale: () => isConfigStale,
|
|
18258
|
+
configToTasks: () => configToTasks
|
|
18259
|
+
});
|
|
18260
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, statSync, writeFileSync as writeFileSync5, renameSync, realpathSync, rmSync } from "fs";
|
|
18261
|
+
import { join as join19, dirname as dirname7 } from "path";
|
|
18262
|
+
import { randomBytes, createHash as createHash2 } from "crypto";
|
|
18263
|
+
function sortStrings(a, b) {
|
|
18264
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
18265
|
+
}
|
|
18266
|
+
function sortedSet(arr) {
|
|
18267
|
+
return [...new Set(arr)].sort(sortStrings);
|
|
18268
|
+
}
|
|
18269
|
+
function sortedRecordKeys(rec) {
|
|
18270
|
+
if (!rec)
|
|
18271
|
+
return {};
|
|
18272
|
+
const out2 = {};
|
|
18273
|
+
for (const k of Object.keys(rec).sort(sortStrings)) {
|
|
18274
|
+
out2[k] = rec[k];
|
|
18275
|
+
}
|
|
18276
|
+
return out2;
|
|
18277
|
+
}
|
|
18278
|
+
function resolveConfigPath(projectRoot) {
|
|
18279
|
+
const configPath = join19(projectRoot, "b4mal.config.json");
|
|
18280
|
+
if (!existsSync16(configPath)) {
|
|
18281
|
+
throw new Error(`Configuration file not found: b4mal.config.json (searched in ${projectRoot}). Run 'b4mal init' to create one.`);
|
|
18282
|
+
}
|
|
18283
|
+
return configPath;
|
|
18284
|
+
}
|
|
18285
|
+
function verifyPathBoundary(configPath, projectRoot) {
|
|
18286
|
+
let realConfigPath;
|
|
18287
|
+
let realRoot;
|
|
18288
|
+
try {
|
|
18289
|
+
realConfigPath = realpathSync(configPath);
|
|
18290
|
+
realRoot = realpathSync(projectRoot);
|
|
18291
|
+
} catch (e) {
|
|
18292
|
+
throw new Error(`Cannot resolve config file path: ${e.message}. Check that b4mal.config.json exists and is accessible.`);
|
|
18293
|
+
}
|
|
18294
|
+
const rootPrefix = realRoot.replace(/\\/g, "/") + "/";
|
|
18295
|
+
const normConfig = realConfigPath.replace(/\\/g, "/");
|
|
18296
|
+
if (!normConfig.startsWith(rootPrefix) && normConfig !== realRoot.replace(/\\/g, "/")) {
|
|
18297
|
+
throw new Error(`Config file resolves outside project root: ${realConfigPath}. Symlinks to external files are not allowed.`);
|
|
18298
|
+
}
|
|
18299
|
+
}
|
|
18300
|
+
function readAndValidate(configPath) {
|
|
18301
|
+
let raw;
|
|
18302
|
+
try {
|
|
18303
|
+
raw = JSON.parse(readFileSync12(configPath, "utf-8"));
|
|
18304
|
+
} catch (e) {
|
|
18305
|
+
throw new Error(`Failed to parse b4mal.config.json: ${e.message}`);
|
|
18306
|
+
}
|
|
18307
|
+
const result = B4malConfigSchema.safeParse(raw);
|
|
18308
|
+
if (!result.success) {
|
|
18309
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join(`
|
|
18310
|
+
`);
|
|
18311
|
+
throw new Error(`Invalid configuration in b4mal.config.json:
|
|
18312
|
+
${issues}`);
|
|
18313
|
+
}
|
|
18314
|
+
return result.data;
|
|
18315
|
+
}
|
|
18316
|
+
function loadConfig(projectRoot) {
|
|
18317
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
18318
|
+
verifyPathBoundary(configPath, projectRoot);
|
|
18319
|
+
return readAndValidate(configPath);
|
|
18320
|
+
}
|
|
18321
|
+
function configToTasks(config) {
|
|
18322
|
+
const ids = Object.keys(config.tasks).sort(sortStrings);
|
|
18323
|
+
const tasks = [];
|
|
18324
|
+
for (const id of ids) {
|
|
18325
|
+
const t = config.tasks[id];
|
|
18326
|
+
const matrix = t.matrix;
|
|
18327
|
+
if (matrix) {
|
|
18328
|
+
const expanded = expandMatrix(id, t, matrix);
|
|
18329
|
+
tasks.push(...expanded);
|
|
18330
|
+
} else {
|
|
18331
|
+
tasks.push(buildTask(id, t));
|
|
18332
|
+
}
|
|
18333
|
+
}
|
|
18334
|
+
const seen = new Set;
|
|
18335
|
+
for (const task of tasks) {
|
|
18336
|
+
if (seen.has(task.id)) {
|
|
18337
|
+
throw new Error(`Duplicate task ID after matrix expansion: "${task.id}". Check for collisions between manual task names and matrix-generated names.`);
|
|
18338
|
+
}
|
|
18339
|
+
seen.add(task.id);
|
|
18340
|
+
}
|
|
18341
|
+
return tasks;
|
|
18342
|
+
}
|
|
18343
|
+
function validateMatrixAxes(baseId, axes, matrix) {
|
|
18344
|
+
for (const axis of axes) {
|
|
18345
|
+
if (!VALID_MATRIX_VALUE.test(axis)) {
|
|
18346
|
+
throw new Error(`Matrix axis name "${axis}" contains invalid characters. Use [a-zA-Z0-9_.-]`);
|
|
18347
|
+
}
|
|
18348
|
+
if (matrix[axis].length === 0) {
|
|
18349
|
+
throw new Error(`Matrix axis "${axis}" for task "${baseId}" has zero values`);
|
|
18350
|
+
}
|
|
18351
|
+
const invalidVal = matrix[axis].find((v) => !VALID_MATRIX_VALUE.test(v));
|
|
18352
|
+
if (invalidVal) {
|
|
18353
|
+
throw new Error(`Matrix value "${invalidVal}" in axis "${axis}" contains invalid characters. Use [a-zA-Z0-9_.-]`);
|
|
18354
|
+
}
|
|
18355
|
+
}
|
|
18356
|
+
}
|
|
18357
|
+
function cartesianProduct(axes, matrix) {
|
|
18358
|
+
return axes.reduce((combos, axis) => {
|
|
18359
|
+
const sortedVals = matrix[axis].slice().sort(sortStrings);
|
|
18360
|
+
return combos.flatMap((combo) => sortedVals.map((val) => ({ ...combo, [axis]: val })));
|
|
18361
|
+
}, [{}]);
|
|
18362
|
+
}
|
|
18363
|
+
function expandMatrix(baseId, t, matrix) {
|
|
18364
|
+
const axes = Object.keys(matrix).sort(sortStrings);
|
|
18365
|
+
if (axes.length === 0)
|
|
18366
|
+
return [buildTask(baseId, t)];
|
|
18367
|
+
validateMatrixAxes(baseId, axes, matrix);
|
|
18368
|
+
const combinations = cartesianProduct(axes, matrix);
|
|
18369
|
+
return combinations.map((combo) => {
|
|
18370
|
+
const suffix = axes.map((a) => `${a}=${combo[a]}`).join("-");
|
|
18371
|
+
return buildTask(`${baseId}-${suffix}`, t, combo);
|
|
18372
|
+
});
|
|
18373
|
+
}
|
|
18374
|
+
function buildTask(id, t, matrixVars) {
|
|
18375
|
+
const norm = (arr) => sortedSet((arr ?? []).map((p) => p.replace(/\\/g, "/")));
|
|
18376
|
+
const env = sortedRecordKeys(t.env ?? {});
|
|
18377
|
+
if (matrixVars) {
|
|
18378
|
+
for (const [k, v] of Object.entries(matrixVars)) {
|
|
18379
|
+
env[`MATRIX_${k.toUpperCase()}`] = v;
|
|
18380
|
+
}
|
|
18381
|
+
}
|
|
18382
|
+
const task = {
|
|
18383
|
+
id,
|
|
18384
|
+
cmd: t.cmd ?? [],
|
|
18385
|
+
dependencies: sortedSet(t.dependencies ?? []),
|
|
18386
|
+
inputs: norm(t.inputs),
|
|
18387
|
+
outputs: norm(t.outputs),
|
|
18388
|
+
claims: norm(t.claims),
|
|
18389
|
+
needsEnv: sortedSet(t.needsEnv ?? []),
|
|
18390
|
+
providesEnv: sortedSet(t.providesEnv ?? []),
|
|
18391
|
+
secrets: sortedSet(t.secrets ?? []),
|
|
18392
|
+
env,
|
|
18393
|
+
timeout: t.timeout ?? 300000,
|
|
18394
|
+
cache: t.cache ?? true,
|
|
18395
|
+
when: t.when
|
|
18396
|
+
};
|
|
18397
|
+
if (t.cwd)
|
|
18398
|
+
task.cwd = t.cwd.replace(/\\/g, "/");
|
|
18399
|
+
return task;
|
|
18400
|
+
}
|
|
18401
|
+
function writeLockfileAtomic(tasks, lockPath) {
|
|
18402
|
+
const configHash = computeConfigHash(tasks);
|
|
18403
|
+
const envelope = {
|
|
18404
|
+
version: 2,
|
|
18405
|
+
_meta: { configHash },
|
|
18406
|
+
tasks
|
|
18407
|
+
};
|
|
18408
|
+
const json = JSON.stringify(envelope, null, 2);
|
|
18409
|
+
const dir = dirname7(lockPath);
|
|
18410
|
+
const tmpName = `.b4mal-lock-${randomBytes(8).toString("hex")}.tmp`;
|
|
18411
|
+
const tmpPath = join19(dir, tmpName);
|
|
18412
|
+
try {
|
|
18413
|
+
writeFileSync5(tmpPath, json, "utf-8");
|
|
18414
|
+
renameSync(tmpPath, lockPath);
|
|
18415
|
+
} catch (e) {
|
|
18416
|
+
try {
|
|
18417
|
+
if (existsSync16(tmpPath))
|
|
18418
|
+
rmSync(tmpPath);
|
|
18419
|
+
} catch {}
|
|
18420
|
+
throw e;
|
|
18421
|
+
}
|
|
18422
|
+
}
|
|
18423
|
+
function computeConfigHash(tasks) {
|
|
18424
|
+
const hash = createHash2("sha256");
|
|
18425
|
+
hash.update(JSON.stringify(tasks));
|
|
18426
|
+
return `sha256:${hash.digest("hex")}`;
|
|
18427
|
+
}
|
|
18428
|
+
function isConfigStale(projectRoot) {
|
|
18429
|
+
const configPath = join19(projectRoot, "b4mal.config.json");
|
|
18430
|
+
const lockPath = join19(projectRoot, "b4mal.lock");
|
|
18431
|
+
if (!existsSync16(configPath))
|
|
18432
|
+
return false;
|
|
18433
|
+
if (!existsSync16(lockPath))
|
|
18434
|
+
return true;
|
|
18435
|
+
const configMtime = statSync(configPath).mtimeMs;
|
|
18436
|
+
const lockMtime = statSync(lockPath).mtimeMs;
|
|
18437
|
+
return configMtime > lockMtime;
|
|
18438
|
+
}
|
|
18439
|
+
var VALID_MATRIX_VALUE;
|
|
18440
|
+
var init_config_loader = __esm(() => {
|
|
18441
|
+
init_schema();
|
|
18442
|
+
VALID_MATRIX_VALUE = /^[a-zA-Z0-9_.-]+$/;
|
|
18443
|
+
});
|
|
18444
|
+
|
|
18224
18445
|
// src/cli/index.ts
|
|
18225
18446
|
init_engine();
|
|
18226
18447
|
import { parseArgs } from "util";
|
|
@@ -18397,6 +18618,11 @@ ${c.reset}
|
|
|
18397
18618
|
`);
|
|
18398
18619
|
out(` ${c.bold}b4mal: prove \u2192 halt \u2192 fix \u2192 run${c.reset}
|
|
18399
18620
|
|
|
18621
|
+
`);
|
|
18622
|
+
out(` ${c.dim}Exit code 1 \u2014 the collision above is fatal, so this mirrors what a${c.reset}
|
|
18623
|
+
`);
|
|
18624
|
+
out(` ${c.dim}real build would do. See docs/reference/cli.md for the rationale.${c.reset}
|
|
18625
|
+
|
|
18400
18626
|
`);
|
|
18401
18627
|
}
|
|
18402
18628
|
async function runDemo() {
|
|
@@ -18429,7 +18655,7 @@ ${c.reset}`);
|
|
|
18429
18655
|
import { readFileSync as readFileSync4 } from "fs";
|
|
18430
18656
|
import { dirname as dirname4, join as join11 } from "path";
|
|
18431
18657
|
var MAX_ASCENT = 5;
|
|
18432
|
-
var FALLBACK_VERSION = "0.1.
|
|
18658
|
+
var FALLBACK_VERSION = "0.1.2";
|
|
18433
18659
|
var PKG_NAME = "b4mal";
|
|
18434
18660
|
function barePackageName(name) {
|
|
18435
18661
|
if (typeof name !== "string" || name.length === 0)
|
|
@@ -18598,6 +18824,7 @@ async function main() {
|
|
|
18598
18824
|
await CICommand2.execute(Bun.argv);
|
|
18599
18825
|
} else {
|
|
18600
18826
|
fail("Unknown setup command. Try: b4mal setup ci");
|
|
18827
|
+
process.exit(1);
|
|
18601
18828
|
}
|
|
18602
18829
|
break;
|
|
18603
18830
|
}
|
|
@@ -18608,19 +18835,7 @@ async function main() {
|
|
|
18608
18835
|
}
|
|
18609
18836
|
case "build": {
|
|
18610
18837
|
banner2("Engaging Wave Orchestrator\u2026");
|
|
18611
|
-
|
|
18612
|
-
const configPath = join20(process.cwd(), "b4mal.config.json");
|
|
18613
|
-
if (syncFlag || existsSync17(configPath)) {
|
|
18614
|
-
const { loadConfig: loadConfig2, configToTasks: configToTasks2, writeLockfileAtomic: writeLockfileAtomic2, isConfigStale: isConfigStale2 } = await Promise.resolve().then(() => (init_config_loader(), exports_config_loader));
|
|
18615
|
-
if (syncFlag || isConfigStale2(process.cwd())) {
|
|
18616
|
-
info2("Loading b4mal.config.json...");
|
|
18617
|
-
const config = loadConfig2(process.cwd());
|
|
18618
|
-
const tasks = configToTasks2(config);
|
|
18619
|
-
writeLockfileAtomic2(tasks, join20(process.cwd(), "b4mal.lock"));
|
|
18620
|
-
if (syncFlag)
|
|
18621
|
-
info2("Lockfile regenerated from config (--sync).");
|
|
18622
|
-
}
|
|
18623
|
-
}
|
|
18838
|
+
await syncLockFromConfig(process.cwd(), values.sync || values["from-config"]);
|
|
18624
18839
|
const dryRun = values["dry-run"];
|
|
18625
18840
|
const opts = { force: values.force, strict: values.strict };
|
|
18626
18841
|
if (dryRun) {
|
|
@@ -18657,11 +18872,15 @@ async function main() {
|
|
|
18657
18872
|
}
|
|
18658
18873
|
process.exit(1);
|
|
18659
18874
|
}
|
|
18660
|
-
const
|
|
18661
|
-
const
|
|
18875
|
+
const conditionSkipped = result.results.filter((r) => r.skippedByCondition).length;
|
|
18876
|
+
const hits = result.results.filter((r) => r.cached && !r.skippedByCondition).length;
|
|
18877
|
+
const misses = result.results.filter((r) => !r.cached && !r.skippedByCondition).length;
|
|
18662
18878
|
for (const r of result.results) {
|
|
18663
|
-
if (r.cached) {
|
|
18879
|
+
if (r.cached && !r.skippedByCondition) {
|
|
18664
18880
|
process.stdout.write(`${c4.cyan}${c4.dim} \u21A9 ${r.taskId} (cached)${c4.reset}
|
|
18881
|
+
`);
|
|
18882
|
+
} else if (r.skippedByCondition) {
|
|
18883
|
+
process.stdout.write(`${c4.dim} \u2298 ${r.taskId} (skipped \u2014 when condition not met)${c4.reset}
|
|
18665
18884
|
`);
|
|
18666
18885
|
} else if (r.skipped) {
|
|
18667
18886
|
process.stdout.write(`${c4.yellow} \u2298 ${r.taskId} (skipped \u2014 dependency failed)${c4.reset}
|
|
@@ -18680,8 +18899,12 @@ async function main() {
|
|
|
18680
18899
|
`);
|
|
18681
18900
|
}
|
|
18682
18901
|
}
|
|
18683
|
-
if (hits > 0)
|
|
18684
|
-
|
|
18902
|
+
if (hits > 0) {
|
|
18903
|
+
const skippedNote = conditionSkipped > 0 ? `, ${conditionSkipped} skipped by condition` : "";
|
|
18904
|
+
info2(`${hits} task(s) restored from cache \u2014 ${misses} executed${skippedNote}.`);
|
|
18905
|
+
} else if (conditionSkipped > 0) {
|
|
18906
|
+
info2(`${conditionSkipped} task(s) skipped by condition \u2014 ${misses} executed.`);
|
|
18907
|
+
}
|
|
18685
18908
|
if (!result.success) {
|
|
18686
18909
|
const skippedCount = result.results.filter((r) => r.skipped).length;
|
|
18687
18910
|
fail(skippedCount > 0 ? `One or more tasks failed \u2014 ${skippedCount} downstream task(s) skipped.` : "One or more tasks exited non-zero.");
|
|
@@ -18710,6 +18933,7 @@ async function main() {
|
|
|
18710
18933
|
case "check": {
|
|
18711
18934
|
const jsonMode = values.json;
|
|
18712
18935
|
const isGHA = process.env.GITHUB_ACTIONS === "true";
|
|
18936
|
+
await syncLockFromConfig(process.cwd(), false);
|
|
18713
18937
|
if (!jsonMode) {
|
|
18714
18938
|
banner2("Verifying DAG Correctness\u2026");
|
|
18715
18939
|
info2("Checking resource isolation and shadowing without executing tasks.");
|
|
@@ -18746,8 +18970,10 @@ async function main() {
|
|
|
18746
18970
|
if (jsonMode) {
|
|
18747
18971
|
process.stdout.write(JSON.stringify({
|
|
18748
18972
|
verified: issues === 0,
|
|
18973
|
+
taskCount: plan.totalTasks,
|
|
18749
18974
|
collisions: plan.conflicts.length,
|
|
18750
18975
|
shadows: shadows.length,
|
|
18976
|
+
note: plan.totalTasks === 0 ? "no tasks in the lockfile \u2014 nothing was verified" : undefined,
|
|
18751
18977
|
findings
|
|
18752
18978
|
}, null, 2) + `
|
|
18753
18979
|
`);
|
|
@@ -18758,7 +18984,8 @@ async function main() {
|
|
|
18758
18984
|
`);
|
|
18759
18985
|
}
|
|
18760
18986
|
if (issues === 0) {
|
|
18761
|
-
process.stdout.write(`::
|
|
18987
|
+
process.stdout.write(plan.totalTasks === 0 ? `::warning::b4mal check found no tasks in b4mal.lock \u2014 nothing was verified.
|
|
18988
|
+
` : `::notice::DAG verified \u2014 no collisions, no shadowing.
|
|
18762
18989
|
`);
|
|
18763
18990
|
}
|
|
18764
18991
|
} else {
|
|
@@ -18775,9 +19002,7 @@ async function main() {
|
|
|
18775
19002
|
`);
|
|
18776
19003
|
}
|
|
18777
19004
|
}
|
|
18778
|
-
if (issues
|
|
18779
|
-
ok2("DAG verified \u2014 no collisions, no shadowing.");
|
|
18780
|
-
} else {
|
|
19005
|
+
if (issues > 0) {
|
|
18781
19006
|
process.stdout.write(`
|
|
18782
19007
|
${c4.red}${issues} issue(s) found.${c4.reset}
|
|
18783
19008
|
`);
|
|
@@ -18786,6 +19011,11 @@ async function main() {
|
|
|
18786
19011
|
process.stdout.write(` ${c4.dim}Run 'b4mal build' to execute after fixing the above.${c4.reset}
|
|
18787
19012
|
|
|
18788
19013
|
`);
|
|
19014
|
+
} else if (plan.totalTasks === 0) {
|
|
19015
|
+
warn2("b4mal.lock contains no tasks \u2014 nothing was verified.");
|
|
19016
|
+
info2("Declare tasks in b4mal.config.json, then run: b4mal build --sync");
|
|
19017
|
+
} else {
|
|
19018
|
+
ok2("DAG verified \u2014 no collisions, no shadowing.");
|
|
18789
19019
|
}
|
|
18790
19020
|
}
|
|
18791
19021
|
if (issues > 0)
|
|
@@ -18818,6 +19048,13 @@ ${c4.reset}`);
|
|
|
18818
19048
|
}
|
|
18819
19049
|
break;
|
|
18820
19050
|
}
|
|
19051
|
+
case "attest": {
|
|
19052
|
+
const { AttestHandler: AttestHandler2 } = await Promise.resolve().then(() => exports_attest);
|
|
19053
|
+
const result = await AttestHandler2.execute(positionals.slice(3), process.env);
|
|
19054
|
+
process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
19055
|
+
`);
|
|
19056
|
+
process.exit(result.accepted ? 0 : 1);
|
|
19057
|
+
}
|
|
18821
19058
|
case "analyze": {
|
|
18822
19059
|
banner2("Generating Visual Observability Dashboard\u2026");
|
|
18823
19060
|
const outPath = await engine.analyze();
|
|
@@ -18826,31 +19063,52 @@ ${c4.reset}`);
|
|
|
18826
19063
|
break;
|
|
18827
19064
|
}
|
|
18828
19065
|
case "plugin": {
|
|
18829
|
-
const sub = positionals[
|
|
19066
|
+
const sub = positionals[3];
|
|
18830
19067
|
const { WasmRegistry: WasmRegistry2 } = await Promise.resolve().then(() => (init_wasm_registry(), exports_wasm_registry));
|
|
18831
19068
|
const path12 = await import("path");
|
|
18832
19069
|
const registry = new WasmRegistry2;
|
|
18833
19070
|
if (sub === "install") {
|
|
18834
|
-
const url = positionals[
|
|
18835
|
-
const name = positionals[3] || path12.basename(url, ".wasm");
|
|
19071
|
+
const url = positionals[4];
|
|
18836
19072
|
if (!url) {
|
|
18837
19073
|
fail("Usage: b4mal plugin install <url> [name]");
|
|
19074
|
+
info2("Example: b4mal plugin install https://example.com/my-plugin.wasm");
|
|
18838
19075
|
process.exit(1);
|
|
18839
19076
|
}
|
|
19077
|
+
let parsedUrl;
|
|
19078
|
+
try {
|
|
19079
|
+
parsedUrl = new URL(url);
|
|
19080
|
+
} catch {
|
|
19081
|
+
fail(`"${url}" is not a valid URL.`);
|
|
19082
|
+
info2("Expected an absolute URL, e.g. https://example.com/my-plugin.wasm");
|
|
19083
|
+
process.exit(1);
|
|
19084
|
+
}
|
|
19085
|
+
const derived = path12.basename(parsedUrl.pathname, ".wasm");
|
|
19086
|
+
const name = positionals[5] || derived || parsedUrl.hostname || "plugin";
|
|
18840
19087
|
banner2(`Installing Plugin: ${name}`);
|
|
18841
|
-
|
|
18842
|
-
|
|
19088
|
+
try {
|
|
19089
|
+
const outPath = await registry.install(url, name);
|
|
19090
|
+
ok2(`Plugin successfully installed to ${outPath}`);
|
|
19091
|
+
} catch (err2) {
|
|
19092
|
+
fail(err2?.message ?? String(err2));
|
|
19093
|
+
process.exit(1);
|
|
19094
|
+
}
|
|
18843
19095
|
} else if (sub === "run") {
|
|
18844
|
-
const name = positionals[
|
|
19096
|
+
const name = positionals[4];
|
|
18845
19097
|
if (!name) {
|
|
18846
19098
|
fail("Usage: b4mal plugin run <name>");
|
|
18847
19099
|
process.exit(1);
|
|
18848
19100
|
}
|
|
18849
19101
|
banner2(`Running Plugin: ${name}`);
|
|
18850
|
-
|
|
18851
|
-
|
|
19102
|
+
try {
|
|
19103
|
+
const code = await registry.run(name);
|
|
19104
|
+
ok2(`Plugin exited with code ${code}`);
|
|
19105
|
+
} catch (err2) {
|
|
19106
|
+
fail(err2?.message ?? String(err2));
|
|
19107
|
+
process.exit(1);
|
|
19108
|
+
}
|
|
18852
19109
|
} else {
|
|
18853
19110
|
fail("Unknown plugin command. Try: install, run");
|
|
19111
|
+
process.exit(1);
|
|
18854
19112
|
}
|
|
18855
19113
|
break;
|
|
18856
19114
|
}
|
|
@@ -18890,6 +19148,21 @@ ${c4.dim}${error.stack}${c4.reset}
|
|
|
18890
19148
|
engine.close();
|
|
18891
19149
|
}
|
|
18892
19150
|
}
|
|
19151
|
+
async function syncLockFromConfig(projectRoot, force) {
|
|
19152
|
+
const configPath = join20(projectRoot, "b4mal.config.json");
|
|
19153
|
+
if (!force && !existsSync17(configPath))
|
|
19154
|
+
return;
|
|
19155
|
+
const { loadConfig: loadConfig2, configToTasks: configToTasks2, writeLockfileAtomic: writeLockfileAtomic2, isConfigStale: isConfigStale2 } = await Promise.resolve().then(() => (init_config_loader(), exports_config_loader));
|
|
19156
|
+
if (!force && !isConfigStale2(projectRoot))
|
|
19157
|
+
return;
|
|
19158
|
+
process.stderr.write(`${c4.dim} Loading b4mal.config.json...${c4.reset}
|
|
19159
|
+
`);
|
|
19160
|
+
const tasks = configToTasks2(loadConfig2(projectRoot));
|
|
19161
|
+
writeLockfileAtomic2(tasks, join20(projectRoot, "b4mal.lock"));
|
|
19162
|
+
if (force)
|
|
19163
|
+
process.stderr.write(`${c4.dim} Lockfile regenerated from config (--sync).${c4.reset}
|
|
19164
|
+
`);
|
|
19165
|
+
}
|
|
18893
19166
|
function printUsage() {
|
|
18894
19167
|
process.stdout.write(`
|
|
18895
19168
|
${c4.bold}b4mal${c4.reset} \u2014 Core Build Engine v${getVersion()}
|
|
@@ -18906,6 +19179,7 @@ function printUsage() {
|
|
|
18906
19179
|
b4mal clean Purge artifact vault + ledger
|
|
18907
19180
|
b4mal trace "cmd" Synthesize a DAG automatically via eBPF
|
|
18908
19181
|
b4mal plugin Manage and execute decentralized WASM plugins
|
|
19182
|
+
b4mal attest Declare a task's resources and get a normalized claim (JSON)
|
|
18909
19183
|
|
|
18910
19184
|
${c4.bold}Flags:${c4.reset}
|
|
18911
19185
|
-f, --force Bypass cache (force re-execution)
|