@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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +973 -699
  3. 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
- class DynamicExecutor {
1626
- static async run(dag, config) {
1627
- const ledger = config?.projectRoot ? new SQLiteLedger(join8(config.projectRoot, ".b4mal", "cache.db")) : undefined;
1628
- const concurrency = config?.concurrency ?? cpus().length;
1629
- const inDegree = new Map(dag.inDegree);
1630
- const readyQueue = [];
1631
- for (const [id, count] of inDegree.entries()) {
1632
- if (count === 0)
1633
- readyQueue.push(id);
1634
- }
1635
- let activeCount = 0;
1636
- const totalTasks = dag.tasks.size;
1637
- if (totalTasks === 0) {
1638
- ledger?.close();
1639
- return [];
1640
- }
1641
- return new Promise((resolve2) => {
1642
- const settled = new Map;
1643
- let resolved = false;
1644
- const settleIfComplete = () => {
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
- resolve2([...settled.values()]);
1650
- };
1651
- const skipDependentsOf = (failedId) => {
1652
- const stack = [failedId];
1653
- while (stack.length > 0) {
1654
- const current = stack.pop();
1655
- for (const dependent of dag.dependents.get(current) ?? []) {
1656
- if (settled.has(dependent))
1657
- continue;
1658
- settled.set(dependent, {
1659
- taskId: dependent,
1660
- exitCode: 1,
1661
- stdout: "",
1662
- stderr: `Skipped: dependency '${current}' failed.`,
1663
- durationMs: 0,
1664
- cached: false,
1665
- skipped: true
1666
- });
1667
- stack.push(dependent);
1668
- }
1669
- }
1670
- };
1671
- const dispatch = async () => {
1672
- while (readyQueue.length > 0 && activeCount < concurrency) {
1673
- let taskId;
1674
- if (config?.chaos) {
1675
- const randomIndex = Math.floor(Math.random() * readyQueue.length);
1676
- taskId = readyQueue.splice(randomIndex, 1)[0];
1677
- } else {
1678
- taskId = readyQueue.shift();
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
- if (settled.has(taskId))
1681
- continue;
1682
- const task = dag.tasks.get(taskId);
1683
- activeCount++;
1684
- this.executeTask(task, config, ledger).then((result) => {
1685
- settled.set(taskId, result);
1686
- }).catch((err) => {
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
- for (const dependent of dag.dependents.get(taskId) || []) {
1704
- const currentCount = inDegree.get(dependent) - 1;
1705
- inDegree.set(dependent, currentCount);
1706
- if (currentCount === 0) {
1707
- readyQueue.push(dependent);
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
- StreamEngine.broadcast("wave_complete", {
1712
- depth: 0,
1713
- tasks: 1,
1714
- durationMs: 0,
1715
- taskIds: [taskId]
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
- settleIfComplete();
1718
- if (!resolved)
1719
- dispatch();
1720
- });
1721
- }
1722
- };
1723
- try {
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
- } catch (err) {
1760
- process.stderr.write(`\x1B[2m[L2] pull failed: ${err?.message || err}\x1B[0m
1761
- `);
1762
- }
1764
+ });
1763
1765
  }
1764
- if (!skipCache && logicHash && ledger) {
1765
- const entry = ledger.getEntry(logicHash);
1766
- if (entry) {
1767
- const needsArtifact = producesArtifact;
1768
- const hasArtifact = !needsArtifact || ArtifactVault.hasArtifact(logicHash, projectRoot);
1769
- if (hasArtifact) {
1770
- const start2 = performance.now();
1771
- if (needsArtifact && projectRoot) {
1772
- await ArtifactVault.unpack(logicHash, projectRoot);
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 branch "${branch}" doesn't match "${w.branch}"]`,
1794
+ stdout: `[skipped \u2014 platform ${process.platform} not in ${w.platform}]`,
1805
1795
  stderr: "",
1806
1796
  durationMs: 0,
1807
- cached: true
1797
+ cached: false,
1798
+ skippedByCondition: true
1808
1799
  };
1809
1800
  }
1810
- }
1811
- }
1812
- const start = performance.now();
1813
- const sanitizedEnv = EnvSanitizer.sanitize(requestedEnv, process.env);
1814
- const secrets = task.secrets;
1815
- if (secrets && secrets.length > 0) {
1816
- for (const name of secrets) {
1817
- const val = process.env[name];
1818
- if (val !== undefined) {
1819
- sanitizedEnv[name] = val;
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
- let finalCmd = task.cmd;
1824
- const proc = Bun.spawn(finalCmd, {
1825
- cwd: projectRoot,
1826
- stdout: "pipe",
1827
- stderr: "pipe",
1828
- env: sanitizedEnv
1829
- });
1830
- const exitCode = await proc.exited;
1831
- const durationMs = performance.now() - start;
1832
- const maxLogSize = 100 * 1024;
1833
- const rawStdout = await new Response(proc.stdout).text();
1834
- const rawStderr = await new Response(proc.stderr).text();
1835
- const stdout = rawStdout.length > maxLogSize ? rawStdout.slice(-maxLogSize) : rawStdout;
1836
- const stderr = rawStderr.length > maxLogSize ? rawStderr.slice(-maxLogSize) : rawStderr;
1837
- if (exitCode === 0 && logicHash) {
1838
- let cacheable = true;
1839
- if (projectRoot && producesArtifact) {
1840
- const missing = writes.filter((w) => !existsSync5(join8(projectRoot, w)));
1841
- if (missing.length > 0) {
1842
- cacheable = false;
1843
- process.stderr.write(`\x1B[2m[Cache] '${task.id}' declared output(s) not produced: ` + `${missing.join(", ")} \u2014 task will not be cached.\x1B[0m
1844
- `);
1845
- } else {
1846
- try {
1847
- await ArtifactVault.pack(logicHash, projectRoot, writes);
1848
- } catch (err) {
1849
- cacheable = false;
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
- if (cacheable) {
1856
- ledger?.recordEntry({
1857
- logicHash,
1858
- taskId: task.id,
1859
- action: "execute",
1860
- timestamp: Date.now(),
1861
- stdout: stdout.trim(),
1862
- stderr: stderr.trim(),
1863
- durationMs
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
- if (projectRoot && config?.remoteVault) {
1867
- try {
1868
- await config.remoteVault.pushWithMetadata(logicHash, projectRoot, {
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
- exitCode,
1872
- durationMs,
1873
- signature: null
1916
+ action: "execute",
1917
+ timestamp: Date.now(),
1918
+ stdout: stdout.trim(),
1919
+ stderr: stderr.trim(),
1920
+ durationMs
1874
1921
  });
1875
- } catch (err) {
1876
- process.stderr.write(`\x1B[2m[L2] push failed: ${err?.message || err}\x1B[0m
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
- return {
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 metadata = parseEmbeddedMetadata(Buffer.from(rawData));
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 archiveWithMeta = embedMetadata(Buffer.from(rawData), metadata);
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: String(t.id ?? ""),
6542
- cmd: t.cmd ?? [],
6543
- dependencies: t.deps ?? t.dependencies ?? [],
6544
- inputs: t.reads ?? t.inputs ?? [],
6545
- outputs: t.writes ?? t.outputs ?? [],
6546
- claims: t.claims ?? [],
6547
- needsEnv: t.envReads ?? t.needsEnv ?? [],
6548
- providesEnv: t.envWrites ?? t.providesEnv ?? [],
6549
- secrets: t.secrets ?? [],
6550
- env: t.env ?? {},
6551
- cwd: t.cwd,
6552
- timeout: t.timeout ?? 300000,
6553
- cache: t.cache ?? true
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 = JSON.parse(readFileSync3(this.lockPath, "utf-8"));
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 = JSON.parse(readFileSync3(this.lockPath, "utf-8"));
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 = JSON.parse(readFileSync3(this.lockPath, "utf-8"));
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 = JSON.parse(readFileSync3(this.lockPath, "utf-8"));
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 tasksRaw = JSON.parse(text);
10512
- if (!Array.isArray(tasksRaw))
10513
- return;
10514
- const tasks = tasksRaw.map((t) => ({
10515
- id: t.id || "unknown",
10516
- reads: t.reads || [],
10517
- writes: t.writes || [],
10518
- envReads: t.envReads || [],
10519
- envWrites: t.envWrites || [],
10520
- claims: t.claims || []
10521
- }));
10522
- const result = await FormalShadow.verifyWave(tasks);
10523
- const diagnostics = result.conflicts.map((conflict) => ({
10524
- range: { start: { line: findLineForConflict(text, conflict), character: 0 }, end: { line: findLineForConflict(text, conflict), character: 100 } },
10525
- severity: 1,
10526
- source: "b4mal",
10527
- message: `Resource Collision: Task ${conflict.taskA} and ${conflict.taskB} concurrently claim ${conflict.counterexample}`
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/schema.ts
10588
- function sanitizePath(p) {
10589
- if (p.startsWith("/")) {
10590
- throw new Error(`Absolute paths not allowed: "${p}"`);
10591
- }
10592
- if (p.split("/").some((seg) => seg === "..")) {
10593
- throw new Error(`Path traversal not allowed: "${p}"`);
10594
- }
10595
- return p.replace(/\\/g, "/");
10596
- }
10597
- function validTaskIds(tasks) {
10598
- return Object.keys(tasks).every((k) => VALID_TASK_ID.test(k));
10599
- }
10600
- function allDepsResolve(tasks) {
10601
- const ids = new Set(Object.keys(tasks));
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
- return true;
10609
- }
10610
- function cartesianSize(matrix) {
10611
- let size = 1;
10612
- for (const vals of Object.values(matrix)) {
10613
- size *= vals.length;
10614
- }
10615
- return size;
10616
- }
10617
- function hasNoCycles(tasks) {
10618
- const WHITE = 0, GRAY = 1, BLACK = 2;
10619
- const color = new Map;
10620
- for (const id of Object.keys(tasks))
10621
- color.set(id, WHITE);
10622
- function dfs(id) {
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 readFileSync11, existsSync as existsSync15 } from "fs";
10963
- import { join as join18 } from "path";
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 = join18(projectRoot, "b4mal.lock");
10966
- if (!existsSync15(lockPath))
10813
+ const lockPath = join17(projectRoot, "b4mal.lock");
10814
+ if (!existsSync14(lockPath))
10967
10815
  return new Set;
10968
- const raw = JSON.parse(readFileSync11(lockPath, "utf-8"));
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(join18(projectRoot, input));
10821
+ dirs.add(join17(projectRoot, input));
10974
10822
  }
10975
10823
  }
10976
- dirs.add(join18(projectRoot, "b4mal.config.json"));
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: sep2, value } = collItem;
14693
+ const { start, key, sep, value } = collItem;
14767
14694
  const keyProps = resolveProps.resolveProps(start, {
14768
14695
  indicator: "explicit-key-ind",
14769
- next: key ?? sep2?.[0],
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 && !sep2) {
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(sep2 ?? [], {
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, sep2, null, valueProps, onError);
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 sep2 = "";
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 += sep2 + cb;
14925
- sep2 = "";
14851
+ comment += sep + cb;
14852
+ sep = "";
14926
14853
  break;
14927
14854
  }
14928
14855
  case "newline":
14929
14856
  if (comment)
14930
- sep2 += source;
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: sep2, value } = collItem;
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 ?? sep2?.[0],
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 && !sep2 && !value) {
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 && !sep2 && !props.found) {
15034
- const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
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(sep2 ?? [], {
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 (sep2)
15058
- for (const st of sep2) {
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, sep2, null, valueProps, onError) : null;
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 sep2 = "";
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 += sep2 + indent.slice(trimIndent) + content;
15271
- sep2 = `
15197
+ value += sep + indent.slice(trimIndent) + content;
15198
+ sep = `
15272
15199
  `;
15273
15200
  } else if (indent.length > trimIndent || content[0] === "\t") {
15274
- if (sep2 === " ")
15275
- sep2 = `
15201
+ if (sep === " ")
15202
+ sep = `
15276
15203
  `;
15277
- else if (!prevMoreIndented && sep2 === `
15204
+ else if (!prevMoreIndented && sep === `
15278
15205
  `)
15279
- sep2 = `
15206
+ sep = `
15280
15207
 
15281
15208
  `;
15282
- value += sep2 + indent.slice(trimIndent) + content;
15283
- sep2 = `
15209
+ value += sep + indent.slice(trimIndent) + content;
15210
+ sep = `
15284
15211
  `;
15285
15212
  prevMoreIndented = true;
15286
15213
  } else if (content === "") {
15287
- if (sep2 === `
15214
+ if (sep === `
15288
15215
  `)
15289
15216
  value += `
15290
15217
  `;
15291
15218
  else
15292
- sep2 = `
15219
+ sep = `
15293
15220
  `;
15294
15221
  } else {
15295
- value += sep2 + content;
15296
- sep2 = " ";
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 sep2 = " ";
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 (sep2 === `
15409
+ if (sep === `
15483
15410
  `)
15484
- res += sep2;
15411
+ res += sep;
15485
15412
  else
15486
- sep2 = `
15413
+ sep = `
15487
15414
  `;
15488
15415
  } else {
15489
- res += sep2 + match[1];
15490
- sep2 = " ";
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 + sep2 + (match?.[1] ?? "");
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: sep2, value }) {
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 (sep2)
16277
- for (const st of sep2)
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 sep2;
17334
+ let sep;
17408
17335
  if (scalar.end) {
17409
- sep2 = scalar.end;
17410
- sep2.push(this.sourceToken);
17336
+ sep = scalar.end;
17337
+ sep.push(this.sourceToken);
17411
17338
  delete scalar.end;
17412
17339
  } else
17413
- sep2 = [this.sourceToken];
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: sep2 }]
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 sep2 = it.sep;
17573
- sep2.push(this.sourceToken);
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: sep2 }]
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 sep2 = fc.end.splice(1, fc.end.length);
17775
- sep2.push(this.sourceToken);
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: sep2 }]
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.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
- const syncFlag = values.sync || values["from-config"];
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 hits = result.results.filter((r) => r.cached).length;
18661
- const misses = result.results.filter((r) => !r.cached).length;
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
- info2(`${hits} task(s) restored from cache \u2014 ${misses} executed.`);
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(`::notice::DAG verified \u2014 no collisions, no shadowing.
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 === 0) {
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[1];
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[2];
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
- const outPath = await registry.install(url, name);
18842
- ok2(`Plugin successfully installed to ${outPath}`);
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[2];
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
- const code = await registry.run(name);
18851
- ok2(`Plugin exited with code ${code}`);
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)