@nimbus-sh/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/runtime/bash-runner.d.ts +63 -0
  5. package/dist/runtime/bash-runner.d.ts.map +1 -0
  6. package/dist/runtime/bash-runner.generated.d.ts +14 -0
  7. package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
  8. package/dist/runtime/bash-runner.generated.js +13 -0
  9. package/dist/runtime/bash-runner.js +290 -0
  10. package/dist/runtime/cpython-runner.d.ts +86 -0
  11. package/dist/runtime/cpython-runner.d.ts.map +1 -0
  12. package/dist/runtime/cpython-runner.js +425 -0
  13. package/dist/runtime/facet-host.d.ts +159 -0
  14. package/dist/runtime/facet-host.d.ts.map +1 -0
  15. package/dist/runtime/facet-host.js +22 -0
  16. package/dist/runtime/installed-runtimes.d.ts +99 -0
  17. package/dist/runtime/installed-runtimes.d.ts.map +1 -0
  18. package/dist/runtime/installed-runtimes.js +162 -0
  19. package/dist/runtime/local-facet-host.d.ts +38 -0
  20. package/dist/runtime/local-facet-host.d.ts.map +1 -0
  21. package/dist/runtime/local-facet-host.js +171 -0
  22. package/dist/runtime/python-pip.d.ts +38 -0
  23. package/dist/runtime/python-pip.d.ts.map +1 -0
  24. package/dist/runtime/python-pip.js +1063 -0
  25. package/dist/runtime/runtime-manifest.d.ts +86 -0
  26. package/dist/runtime/runtime-manifest.d.ts.map +1 -0
  27. package/dist/runtime/runtime-manifest.js +72 -0
  28. package/dist/runtime/runtime-package.d.ts +63 -0
  29. package/dist/runtime/runtime-package.d.ts.map +1 -0
  30. package/dist/runtime/runtime-package.js +66 -0
  31. package/dist/runtime/runtime-registry.d.ts +162 -0
  32. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  33. package/dist/runtime/runtime-registry.js +363 -0
  34. package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
  35. package/dist/runtime/vfs-snapshot.js +15 -1
  36. package/dist/runtime/vfs-supervisor.d.ts +22 -0
  37. package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
  38. package/dist/runtime/vfs-supervisor.js +65 -0
  39. package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
  40. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
  41. package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
  42. package/dist/runtime/wasm-runner.d.ts +80 -0
  43. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  44. package/dist/runtime/wasm-runner.js +686 -0
  45. package/dist/workspace/nimbus-workspace.d.ts +116 -20
  46. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  47. package/dist/workspace/nimbus-workspace.js +238 -45
  48. package/package.json +4 -2
  49. package/src/index.ts +16 -0
  50. package/src/runtime/bash-runner.generated.ts +14 -0
  51. package/src/runtime/bash-runner.ts +347 -0
  52. package/src/runtime/cpython-runner.ts +504 -0
  53. package/src/runtime/facet-host.ts +170 -0
  54. package/src/runtime/installed-runtimes.ts +235 -0
  55. package/src/runtime/local-facet-host.ts +205 -0
  56. package/src/runtime/python-pip.ts +1211 -0
  57. package/src/runtime/runtime-manifest.ts +155 -0
  58. package/src/runtime/runtime-package.ts +114 -0
  59. package/src/runtime/runtime-registry.ts +511 -0
  60. package/src/runtime/vfs-snapshot.ts +15 -1
  61. package/src/runtime/vfs-supervisor.ts +67 -0
  62. package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
  63. package/src/runtime/wasm-runner.ts +835 -0
  64. package/src/workspace/nimbus-workspace.ts +349 -54
@@ -0,0 +1,14 @@
1
+ /**
2
+ * bash-runner.generated.ts — AUTO-GENERATED. DO NOT EDIT.
3
+ *
4
+ * Produced by scripts/bundle-facet-workers.mjs from:
5
+ * - @nimbus-sh/core src/runtime/bash/preamble.ts
6
+ *
7
+ * The facet-side bash scheduler as a self-contained IIFE that installs
8
+ * globalThis.__bashBoot / globalThis.__bashFeed. bash-runner.ts re-exports it
9
+ * as BASH_RUNNER_PREAMBLE and passes it as the facet preamble.
10
+ *
11
+ * Size: 58.56 KiB
12
+ */
13
+
14
+ export const BASH_RUNNER_BODY_SRC: string = "\"use strict\";\n(() => {\n // ../core/src/runtime/bash/preamble.ts\n var PAGE = 65536, te = new TextEncoder(), td = new TextDecoder();\n var MAIN_SIZE = 8 << 20, SLOT_SIZE = 256 << 10, NSLOT = 32;\n var E = { ACCES: 2, BADF: 8, EXIST: 20, INVAL: 28, ISDIR: 31, LOOP: 32, NOENT: 44, NOSYS: 52, NOTDIR: 54, NOTEMPTY: 55, PERM: 63, SPIPE: 70 };\n var CLOCK_REALTIME = 0, CLOCK_MONOTONIC = 1, CLOCK_PROCESS_CPUTIME = 2, CLOCK_THREAD_CPUTIME = 3;\n function realtimeNs() {\n return BigInt(Date.now()) * 1000000n;\n }\n function monotonicNs() {\n const ms = typeof performance !== \"undefined\" && performance.now ? performance.now() : Date.now();\n return BigInt(Math.floor(ms * 1e3)) * 1000n;\n }\n function clockNs(id) {\n if (id === CLOCK_REALTIME) return realtimeNs();\n if (id === CLOCK_MONOTONIC || id === CLOCK_PROCESS_CPUTIME || id === CLOCK_THREAD_CPUTIME) return monotonicNs();\n return null;\n }\n var Exit = class {\n constructor(c) {\n this.code = c;\n }\n };\n var S = null;\n function norm(p) {\n const parts = [];\n for (const seg of String(p).split(\"/\")) {\n if (!seg || seg === \".\") continue;\n if (seg === \"..\") {\n parts.pop();\n continue;\n }\n parts.push(seg);\n }\n return parts.join(\"/\");\n }\n function b64ToBytes(b64) {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n function bytesToB64(bytes) {\n let bin = \"\";\n const CH = 32768;\n for (let i = 0; i < bytes.length; i += CH) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CH));\n return btoa(bin);\n }\n function newSession(args) {\n const wasmTable = globalThis.__NIMBUS_WASM || {};\n const mod = wasmTable[\"bash.async.wasm\"];\n if (!mod) throw new Error(\"bash.async.wasm missing from __NIMBUS_WASM\");\n const coreutils = /* @__PURE__ */ new Map();\n for (const key of Object.keys(wasmTable)) {\n if (key.startsWith(\"cu_\") && key.endsWith(\".wasm\")) coreutils.set(key.slice(3, -5), wasmTable[key]);\n }\n const busybox = wasmTable[\"cu_busybox.wasm\"];\n if (busybox) for (const name of args.busyboxApplets || []) coreutils.set(name, busybox);\n const fs = {\n files: /* @__PURE__ */ new Map(),\n dirs: /* @__PURE__ */ new Set(),\n modes: /* @__PURE__ */ new Map(),\n written: /* @__PURE__ */ new Set(),\n deleted: /* @__PURE__ */ new Set(),\n dirsCreated: /* @__PURE__ */ new Set(),\n dirsDeleted: /* @__PURE__ */ new Set(),\n modesChanged: /* @__PURE__ */ new Map(),\n // vfsPath → mtime ns. The snapshot carries no times, so an inode the\n // session did not touch reports sessionNs: older than anything written\n // here, which is what makes 'is the target newer than its source' — the\n // question make(1) and every incremental build asks — answerable.\n times: /* @__PURE__ */ new Map(),\n sessionNs: realtimeNs(),\n // vfsPath → target string, stored verbatim per POSIX: a symlink is a dumb\n // string resolved at lookup time, not a pointer to an inode.\n symlinks: /* @__PURE__ */ new Map(),\n symlinksCreated: /* @__PURE__ */ new Map()\n };\n const snap = args.fsSnapshot || { files: {}, dirs: [], modes: {} };\n for (const [path, b64] of Object.entries(snap.files || {})) fs.files.set(norm(path), { bytes: b64ToBytes(b64) });\n for (const d of snap.dirs || []) fs.dirs.add(norm(d));\n for (const [path, m] of Object.entries(snap.modes || {})) fs.modes.set(norm(path), Number(m) & 7);\n for (const seed of [\"etc\", \"dev\", \"bin\", \"usr\", \"usr/bin\"]) {\n fs.dirs.add(seed);\n if (!fs.modes.has(seed)) fs.modes.set(seed, 5);\n }\n if (!fs.files.has(\"etc/nimbus.bashrc\")) {\n fs.files.set(\"etc/nimbus.bashrc\", { bytes: te.encode('command cd \"${NIMBUS_PWD:-/}\" 2>/dev/null\\n') });\n fs.modes.set(\"etc/nimbus.bashrc\", 4);\n }\n const home = norm((args.environ.find((e) => e.startsWith(\"HOME=\")) || \"HOME=/home/user\").slice(5));\n if (args.stdinTty && home && !fs.files.has(home + \"/.bashrc\")) {\n fs.files.set(home + \"/.bashrc\", { bytes: te.encode(\". /etc/nimbus.bashrc\\n\") });\n if (!fs.modes.has(home + \"/.bashrc\")) fs.modes.set(home + \"/.bashrc\", 6);\n }\n const cwd = norm((args.environ.find((e) => e.startsWith(\"NIMBUS_PWD=\")) || \"NIMBUS_PWD=/\").slice(11));\n return {\n mod,\n coreutils,\n fs,\n cwd,\n argv: args.argv,\n environ: args.environ,\n stdinTty: !!args.stdinTty,\n stdin: { chunks: args.stdinData ? [te.encode(args.stdinData)] : [], queued: 0, closed: !!args.stdinClosed, waiters: [] },\n procs: /* @__PURE__ */ new Map(),\n pipes: /* @__PURE__ */ new Map(),\n runnable: [],\n exitStatus: /* @__PURE__ */ new Map(),\n waiters: [],\n pidNext: 100,\n pipeNext: 1,\n rootPid: 0,\n rootExit: null,\n steps: 0,\n out: \"\",\n err: \"\",\n missingWasi: /* @__PURE__ */ new Set(),\n stats: { instances: 0, memPeak: 0, mainHi: 0, slotHi: 0 },\n error: null\n };\n }\n function initStdinQueued(s) {\n s.stdin.queued = s.stdin.chunks.reduce((a, c) => a + c.length, 0);\n }\n function newPipe(s) {\n const id = s.pipeNext++;\n s.pipes.set(id, { chunks: [], queued: 0, readers: 1, writers: 1, readW: [] });\n return id;\n }\n function lowestFd(proc) {\n let fd = 0;\n while (proc.fds.has(fd) || fd === 3) fd++;\n return fd;\n }\n function bumpPipe(s, e, d) {\n const pp = s.pipes.get(e.pipeId);\n if (e.end === \"r\") pp.readers += d;\n else pp.writers += d;\n }\n function closeFd(s, proc, fd) {\n const e = proc.fds.get(fd);\n if (!e) return;\n proc.fds.delete(fd);\n if (e.kind === \"pipe\") {\n bumpPipe(s, e, -1);\n wakePipe(s, s.pipes.get(e.pipeId));\n }\n }\n function takeUpTo(src, max) {\n let need = max;\n const parts = [];\n while (need > 0 && src.chunks.length) {\n const ch = src.chunks[0];\n if (ch.length <= need) {\n parts.push(ch);\n need -= ch.length;\n src.chunks.shift();\n } else {\n parts.push(ch.subarray(0, need));\n src.chunks[0] = ch.subarray(need);\n need = 0;\n }\n }\n const total = max - need;\n src.queued -= total;\n const o = new Uint8Array(total);\n let x = 0;\n for (const p of parts) {\n o.set(p, x);\n x += p.length;\n }\n return o;\n }\n var EMPTY_IOV = { list: [], total: 0 };\n function readIovs(dv, iovs, n) {\n const list = [];\n let total = 0;\n for (let i = 0; i < n; i++) {\n const ptr = dv.getUint32(iovs + i * 8, true), len = dv.getUint32(iovs + i * 8 + 4, true);\n if (len > 0) {\n list.push({ ptr, len });\n total += len;\n }\n }\n return { list, total };\n }\n function scatter(u8, iov, bytes) {\n let off = 0;\n for (const b of iov.list) {\n if (off >= bytes.length) break;\n const n = Math.min(b.len, bytes.length - off);\n u8.set(bytes.subarray(off, off + n), b.ptr);\n off += n;\n }\n return off;\n }\n function wakePipe(s, pp) {\n while (pp.readW.length && (pp.queued > 0 || pp.writers === 0)) {\n const w = pp.readW.shift();\n const proc = w.proc;\n const req = proc.ctx.pipeReq;\n const bytes = pp.queued > 0 ? takeUpTo(pp, req.iov.total) : new Uint8Array(0);\n proc.pendingRead = { iov: req.iov, bytes, nreadPtr: req.nreadPtr, pollUserdata: req.pollUserdata };\n resumeProc(proc);\n }\n }\n function wakeStdin(s) {\n const st = s.stdin;\n while (st.waiters.length && (st.queued > 0 || st.closed)) {\n const w = st.waiters.shift();\n const proc = w.proc;\n const req = proc.ctx.pipeReq;\n const bytes = st.queued > 0 ? takeUpTo(st, req.iov.total) : new Uint8Array(0);\n proc.pendingRead = { iov: req.iov, bytes, nreadPtr: req.nreadPtr, pollUserdata: req.pollUserdata };\n resumeProc(proc);\n }\n }\n function fileLookup(s, path) {\n return s.fs.files.get(path) || null;\n }\n function isDir(s, path) {\n return path === \"\" || s.fs.dirs.has(path);\n }\n function isCoreutil(s, path) {\n if (!path.startsWith(\"bin/\") && !path.startsWith(\"usr/bin/\")) return false;\n return s.coreutils.has(path.split(\"/\").pop());\n }\n function fileExists(s, path) {\n return s.fs.files.has(path) || s.fs.modes.has(path) && !s.fs.dirs.has(path);\n }\n function inodeExists(s, path) {\n return s.fs.files.has(path) || s.fs.dirs.has(path) || s.fs.modes.has(path);\n }\n function effMode(s, path) {\n if (path === \"\") return 7;\n const m = s.fs.modes.get(path);\n if (m !== void 0) return m;\n if (isCoreutil(s, path)) return 5;\n return inodeExists(s, path) ? 0 : 7;\n }\n function parentOf(path) {\n const i = path.lastIndexOf(\"/\");\n return i < 0 ? \"\" : path.slice(0, i);\n }\n function followPath(s, path) {\n let cur = path;\n for (let hops = 0; hops < 32; hops++) {\n if (!s.fs.symlinks.size) return { path: cur, errno: 0 };\n const parts = cur.split(\"/\");\n let prefix = \"\";\n let rewrote = false;\n for (let i = 0; i < parts.length; i++) {\n prefix = prefix ? prefix + \"/\" + parts[i] : parts[i];\n const target = s.fs.symlinks.get(prefix);\n if (target === void 0) continue;\n const base = target.startsWith(\"/\") ? norm(target) : norm(parentOf(prefix) + \"/\" + target);\n const rest = parts.slice(i + 1).join(\"/\");\n cur = rest ? norm(base + \"/\" + rest) : base;\n rewrote = true;\n break;\n }\n if (!rewrote) return { path: cur, errno: 0 };\n }\n return { path: cur, errno: E.LOOP };\n }\n function followParents(s, path) {\n const parent = parentOf(path);\n if (!parent) return { path, errno: 0 };\n const r = followPath(s, parent);\n if (r.errno) return r;\n const name = path.slice(parent.length + 1);\n return { path: r.path ? r.path + \"/\" + name : name, errno: 0 };\n }\n function checkTraversal(s, path) {\n const parts = path.split(\"/\");\n let anc = \"\";\n for (let i = 0; i < parts.length - 1; i++) {\n anc = anc ? anc + \"/\" + parts[i] : parts[i];\n if (!isDir(s, anc)) return inodeExists(s, anc) ? E.NOTDIR : E.NOENT;\n if ((effMode(s, anc) & 1) === 0) return E.ACCES;\n }\n return 0;\n }\n function checkParentWritable(s, path) {\n const parent = parentOf(path);\n if (parent && (effMode(s, parent) & 3) !== 3) return E.ACCES;\n return 0;\n }\n function recordDirAdded(s, path) {\n s.fs.dirs.add(path);\n s.fs.dirsDeleted.delete(path);\n s.fs.dirsCreated.add(path);\n if (!s.fs.modes.has(path)) s.fs.modes.set(path, 7);\n touchPath(s, path);\n }\n function recordDirRemoved(s, path) {\n s.fs.dirs.delete(path);\n s.fs.modes.delete(path);\n s.fs.times.delete(path);\n if (s.fs.dirsCreated.has(path)) s.fs.dirsCreated.delete(path);\n else s.fs.dirsDeleted.add(path);\n }\n function recordFileRemoved(s, path) {\n s.fs.files.delete(path);\n s.fs.written.delete(path);\n s.fs.modes.delete(path);\n s.fs.times.delete(path);\n s.fs.symlinks.delete(path);\n s.fs.symlinksCreated.delete(path);\n s.fs.deleted.add(path);\n }\n function markWritten(s, path) {\n s.fs.written.add(path);\n s.fs.deleted.delete(path);\n if (!s.fs.modes.has(path)) s.fs.modes.set(path, 6);\n touchPath(s, path);\n const parts = path.split(\"/\");\n for (let i = 1; i < parts.length; i++) {\n const dir = parts.slice(0, i).join(\"/\");\n if (!s.fs.dirs.has(dir)) recordDirAdded(s, dir);\n }\n }\n function fileWrite(s, entry, path, pos, bytes) {\n const need = pos + bytes.length;\n if (need > entry.bytes.length) {\n const grown = new Uint8Array(need);\n grown.set(entry.bytes);\n entry.bytes = grown;\n }\n entry.bytes.set(bytes, pos);\n markWritten(s, path);\n }\n function mtimeNs(s, path) {\n const t = s.fs.times.get(path);\n return t === void 0 ? s.fs.sessionNs : t;\n }\n function touchPath(s, path) {\n s.fs.times.set(path, realtimeNs());\n }\n function statBuf(dv, u8, buf, filetype, size, timeNs) {\n u8.fill(0, buf, buf + 64);\n dv.setUint8(buf + 16, filetype);\n dv.setBigUint64(buf + 24, 1n, true);\n dv.setBigUint64(buf + 32, BigInt(size), true);\n dv.setBigUint64(buf + 40, timeNs, true);\n dv.setBigUint64(buf + 48, timeNs, true);\n dv.setBigUint64(buf + 56, timeNs, true);\n }\n function isUnderAbsRoot(p, absRoots) {\n if (!absRoots) return false;\n for (const a of absRoots) {\n if (p === a) return true;\n if (a !== \"\" && p.startsWith(a + \"/\")) return true;\n }\n return false;\n }\n function makeResolve(s, pathBase, absRoots) {\n return (raw) => {\n const p = norm(raw);\n if (!pathBase) return p;\n if (isUnderAbsRoot(p, absRoots)) return p;\n const joined = norm(pathBase + \"/\" + p);\n if (inodeExists(s, joined)) return joined;\n if (inodeExists(s, p)) return p;\n if (isDir(s, parentOf(joined))) return joined;\n if (isDir(s, parentOf(p))) return p;\n return joined;\n };\n }\n function makeUnsupported(s) {\n const nosys = (name) => () => {\n s.missingWasi.add(name);\n return E.NOSYS;\n };\n return {\n // Every write is already durable in the session's own heap, so there is\n // nothing to flush and success is the honest answer.\n fd_sync: () => 0,\n fd_datasync: () => 0,\n fd_advise: () => 0,\n // One process runs at a time under this scheduler; a yield has nothing to\n // yield to that the caller has not already reached.\n sched_yield: () => 0,\n fd_pread: nosys(\"fd_pread\"),\n fd_pwrite: nosys(\"fd_pwrite\"),\n fd_allocate: nosys(\"fd_allocate\"),\n fd_filestat_set_size: nosys(\"fd_filestat_set_size\"),\n fd_filestat_set_times: nosys(\"fd_filestat_set_times\"),\n fd_fdstat_set_rights: nosys(\"fd_fdstat_set_rights\"),\n proc_raise: nosys(\"proc_raise\"),\n sock_send: nosys(\"sock_send\"),\n sock_recv: nosys(\"sock_recv\"),\n sock_shutdown: nosys(\"sock_shutdown\")\n };\n }\n function makeWasiFs(s, proc, DV, U8, io, pathBase, absRoots) {\n const resolve = makeResolve(s, pathBase, absRoots);\n return {\n ...makeUnsupported(s),\n fd_prestat_get: (fd, buf) => {\n if (fd === 3) {\n DV().setUint8(buf, 0);\n DV().setUint32(buf + 4, 1, true);\n return 0;\n }\n return E.BADF;\n },\n // The buffer length is a contract, not decoration: fd_prestat_get reports\n // the name is 1 byte, and a guest that offers less must not be written to.\n fd_prestat_dir_name: (fd, path, plen) => {\n if (fd !== 3) return E.BADF;\n if (plen < 1) return E.INVAL;\n U8()[path] = 47;\n return 0;\n },\n path_open: (dirfd, dirflags, pathPtr, pathLen, oflags, rightsBase, _ri, fdflags, retPtr) => {\n const raw = resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen)));\n const r = (dirflags & 1) !== 0 ? followPath(s, raw) : followParents(s, raw);\n if (r.errno) return r.errno;\n const path = r.path;\n const dv = DV();\n if (path === \"dev/null\" || path === \"dev/tty\") {\n const fd2 = lowestFd(proc);\n proc.fds.set(fd2, { kind: path === \"dev/tty\" ? \"tty\" : \"null\" });\n dv.setUint32(retPtr, fd2, true);\n return 0;\n }\n const trav = checkTraversal(s, path);\n if (trav) return trav;\n const wantDir = (oflags & 2) !== 0;\n if (isDir(s, path)) {\n if ((effMode(s, path) & 1) === 0) return E.ACCES;\n const fd2 = lowestFd(proc);\n proc.fds.set(fd2, { kind: \"dir\", path });\n dv.setUint32(retPtr, fd2, true);\n return 0;\n }\n if (wantDir) return fileExists(s, path) ? E.NOTDIR : E.NOENT;\n const rights = typeof rightsBase === \"bigint\" ? rightsBase : BigInt(rightsBase >>> 0);\n let need = 0;\n if ((rights & 2n) !== 0n) need |= 4;\n if ((rights & 64n) !== 0n) need |= 2;\n if ((oflags & 8) !== 0) need |= 2;\n let entry = fileLookup(s, path);\n const exists = entry !== null || fileExists(s, path);\n if (exists && oflags & 4) return E.EXIST;\n if (!exists) {\n if (!(oflags & 1)) return E.NOENT;\n const denied = checkParentWritable(s, path);\n if (denied) return denied;\n entry = { bytes: new Uint8Array(0) };\n s.fs.files.set(path, entry);\n markWritten(s, path);\n } else {\n if (need && (effMode(s, path) & need) !== need) return E.ACCES;\n if (!entry) {\n entry = { bytes: new Uint8Array(0) };\n s.fs.files.set(path, entry);\n }\n if (oflags & 8) {\n entry.bytes = new Uint8Array(0);\n markWritten(s, path);\n }\n }\n const fd = lowestFd(proc);\n proc.fds.set(fd, { kind: \"file\", path, pos: 0, append: (fdflags & 1) !== 0 });\n dv.setUint32(retPtr, fd, true);\n return 0;\n },\n fd_filestat_get: (fd, buf) => {\n const e = proc.fds.get(fd);\n if (!e) return E.BADF;\n if (e.kind === \"file\") {\n const f = fileLookup(s, e.path);\n statBuf(DV(), U8(), buf, 4, f ? f.bytes.length : 0, mtimeNs(s, e.path));\n return 0;\n }\n if (e && (e.kind === \"dir\" || e.kind === \"preopen\")) {\n statBuf(DV(), U8(), buf, 3, 0, mtimeNs(s, e.kind === \"preopen\" ? \"\" : e.path));\n return 0;\n }\n statBuf(DV(), U8(), buf, e && (e.kind === \"stdin\" || e.kind === \"stdout\" || e.kind === \"stderr\" || e.kind === \"tty\") ? 2 : 0, 0, realtimeNs());\n return 0;\n },\n path_filestat_get: (dirfd, flags, pathPtr, pathLen, buf) => {\n const raw = resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen)));\n if (raw === \"dev/null\" || raw === \"dev/tty\") {\n statBuf(DV(), U8(), buf, 2, 0, realtimeNs());\n return 0;\n }\n const follow = (flags & 1) !== 0;\n if (!follow && s.fs.symlinks.has(raw)) {\n statBuf(DV(), U8(), buf, 7, te.encode(s.fs.symlinks.get(raw)).length, mtimeNs(s, raw));\n return 0;\n }\n const r = followPath(s, raw);\n if (r.errno) return r.errno;\n const path = r.path;\n const trav = checkTraversal(s, path);\n if (trav) return trav;\n const f = fileLookup(s, path);\n if (f) {\n statBuf(DV(), U8(), buf, 4, f.bytes.length, mtimeNs(s, path));\n return 0;\n }\n if (isCoreutil(s, path)) {\n statBuf(DV(), U8(), buf, 4, 1024, mtimeNs(s, path));\n return 0;\n }\n if (isDir(s, path)) {\n statBuf(DV(), U8(), buf, 3, 0, mtimeNs(s, path));\n return 0;\n }\n if (fileExists(s, path)) {\n statBuf(DV(), U8(), buf, 4, 0, mtimeNs(s, path));\n return 0;\n }\n return E.NOENT;\n },\n // fstflags: ATIM=1, ATIM_NOW=2, MTIM=4, MTIM_NOW=8. Only the mtime half is\n // recorded — this layer keeps one timestamp per inode, and mtime is the\n // one every build tool reads.\n path_filestat_set_times: (dirfd, _flags, pathPtr, pathLen, _atim, mtim, fstflags) => {\n const path = resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen)));\n if (!inodeExists(s, path) && !s.fs.symlinks.has(path)) return E.NOENT;\n if (fstflags & 8) s.fs.times.set(path, realtimeNs());\n else if (fstflags & 4) s.fs.times.set(path, typeof mtim === \"bigint\" ? mtim : BigInt(mtim >>> 0));\n return 0;\n },\n path_unlink_file: (dirfd, pathPtr, pathLen) => {\n const r = followParents(s, resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen))));\n if (r.errno) return r.errno;\n const path = r.path;\n const trav = checkTraversal(s, path);\n if (trav) return trav;\n if (isDir(s, path)) return E.ISDIR;\n if (!fileExists(s, path) && !s.fs.symlinks.has(path)) return E.NOENT;\n const denied = checkParentWritable(s, path);\n if (denied) return denied;\n recordFileRemoved(s, path);\n return 0;\n },\n path_rename: (fd1, oldPtr, oldLen, fd2, newPtr, newLen) => {\n const from = resolve(td.decode(U8().subarray(oldPtr, oldPtr + oldLen)));\n const to = resolve(td.decode(U8().subarray(newPtr, newPtr + newLen)));\n let trav = checkTraversal(s, from);\n if (trav) return trav;\n trav = checkTraversal(s, to);\n if (trav) return trav;\n const fromIsDir = from !== \"\" && isDir(s, from);\n const f = fileLookup(s, from);\n if (!fromIsDir && !f && !fileExists(s, from)) return E.NOENT;\n if (from === to) return 0;\n let denied = checkParentWritable(s, from);\n if (!denied) denied = checkParentWritable(s, to);\n if (denied) return denied;\n const moveMode = (a, b) => {\n const m = s.fs.modes.get(a);\n s.fs.modes.delete(a);\n if (m !== void 0) s.fs.modes.set(b, m);\n };\n if (isDir(s, to)) {\n if (!fromIsDir) return E.ISDIR;\n const prefix = to + \"/\";\n for (const p of s.fs.files.keys()) if (p.startsWith(prefix)) return E.NOTEMPTY;\n for (const p of s.fs.dirs) if (p.startsWith(prefix)) return E.NOTEMPTY;\n recordDirRemoved(s, to);\n } else if (fileExists(s, to)) {\n if (fromIsDir) return E.NOTDIR;\n recordFileRemoved(s, to);\n }\n if (fromIsDir) {\n const mode2 = s.fs.modes.get(from);\n recordDirRemoved(s, from);\n recordDirAdded(s, to);\n if (mode2 !== void 0) s.fs.modes.set(to, mode2);\n const prefix = from + \"/\";\n const rebase = (key) => to + \"/\" + key.slice(prefix.length);\n for (const key of [...s.fs.files.keys()]) {\n if (!key.startsWith(prefix)) continue;\n const nk = rebase(key);\n const entry2 = s.fs.files.get(key);\n recordFileRemoved(s, key);\n moveMode(key, nk);\n s.fs.files.set(nk, entry2);\n s.fs.written.add(nk);\n s.fs.deleted.delete(nk);\n if (!s.fs.modes.has(nk)) s.fs.modes.set(nk, 6);\n }\n for (const key of [...s.fs.dirs]) {\n if (!key.startsWith(prefix)) continue;\n const nk = rebase(key);\n const mode22 = s.fs.modes.get(key);\n recordDirRemoved(s, key);\n recordDirAdded(s, nk);\n if (mode22 !== void 0) s.fs.modes.set(nk, mode22);\n }\n return 0;\n }\n const entry = f || { bytes: new Uint8Array(0) };\n const mode = s.fs.modes.get(from);\n recordFileRemoved(s, from);\n s.fs.files.set(to, entry);\n markWritten(s, to);\n if (mode !== void 0) s.fs.modes.set(to, mode);\n return 0;\n },\n path_create_directory: (dirfd, pathPtr, pathLen) => {\n const path = resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen)));\n const trav = checkTraversal(s, path);\n if (trav) return trav;\n if (isDir(s, path) || fileExists(s, path)) return E.EXIST;\n const denied = checkParentWritable(s, path);\n if (denied) return denied;\n recordDirAdded(s, path);\n return 0;\n },\n // path_readlink(fd, path, path_len, buf, buf_len, *bufused). Never follows\n // the last component — reading a link is the one op that is about the link.\n path_readlink: (dirfd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) => {\n const r = followParents(s, resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen))));\n if (r.errno) return r.errno;\n const target = s.fs.symlinks.get(r.path);\n if (target === void 0) return inodeExists(s, r.path) ? E.INVAL : E.NOENT;\n const bytes = te.encode(target);\n const n = Math.min(bytes.length, bufLen);\n U8().set(bytes.subarray(0, n), bufPtr);\n DV().setUint32(bufUsedPtr, n, true);\n return 0;\n },\n // path_symlink(old_path, old_path_len, fd, new_path, new_path_len) — the\n // target comes FIRST and is not a path in this filesystem: it is stored\n // verbatim and may dangle, exactly as ln -s allows.\n path_symlink: (oldPtr, oldLen, newFd, newPtr, newLen) => {\n const target = td.decode(U8().subarray(oldPtr, oldPtr + oldLen));\n const r = followParents(s, resolve(td.decode(U8().subarray(newPtr, newPtr + newLen))));\n if (r.errno) return r.errno;\n const link = r.path;\n const trav = checkTraversal(s, link);\n if (trav) return trav;\n if (inodeExists(s, link) || s.fs.symlinks.has(link)) return E.EXIST;\n const denied = checkParentWritable(s, link);\n if (denied) return denied;\n s.fs.symlinks.set(link, target);\n s.fs.symlinksCreated.set(link, target);\n s.fs.modes.set(link, 7);\n s.fs.deleted.delete(link);\n touchPath(s, link);\n return 0;\n },\n path_link: (fd1, _lookupFlags, oldPtr, oldLen, fd2, newPtr, newLen) => {\n const from = resolve(td.decode(U8().subarray(oldPtr, oldPtr + oldLen)));\n const to = resolve(td.decode(U8().subarray(newPtr, newPtr + newLen)));\n let trav = checkTraversal(s, from);\n if (trav) return trav;\n trav = checkTraversal(s, to);\n if (trav) return trav;\n if (isDir(s, from)) return E.PERM;\n const f = fileLookup(s, from);\n if (!f) return fileExists(s, from) ? E.ACCES : E.NOENT;\n if (fileExists(s, to) || isDir(s, to)) return E.EXIST;\n const denied = checkParentWritable(s, to);\n if (denied) return denied;\n s.fs.files.set(to, f);\n markWritten(s, to);\n s.fs.modes.set(to, effMode(s, from));\n return 0;\n },\n path_remove_directory: (dirfd, pathPtr, pathLen) => {\n const path = resolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen)));\n const trav = checkTraversal(s, path);\n if (trav) return trav;\n if (path === \"\" || path === \"etc\" || path === \"dev\" || path === \"bin\" || path === \"usr\" || path === \"usr/bin\") return E.ACCES;\n if (!isDir(s, path)) return fileExists(s, path) ? E.NOTDIR : E.NOENT;\n const denied = checkParentWritable(s, path);\n if (denied) return denied;\n const prefix = path + \"/\";\n for (const p of s.fs.files.keys()) if (p.startsWith(prefix)) return E.NOTEMPTY;\n for (const p of s.fs.dirs) if (p.startsWith(prefix)) return E.NOTEMPTY;\n for (const p of s.fs.modes.keys()) if (p.startsWith(prefix)) return E.NOTEMPTY;\n recordDirRemoved(s, path);\n return 0;\n },\n fd_renumber: (from, to) => {\n const e = proc.fds.get(from);\n if (!e) return E.BADF;\n if (from !== to) {\n closeFd(s, proc, to);\n proc.fds.set(to, e);\n proc.fds.delete(from);\n }\n return 0;\n },\n fd_readdir: (fd, buf, bufLen, cookie, retPtr) => {\n const e = proc.fds.get(fd);\n if (!e || e.kind !== \"dir\" && e.kind !== \"preopen\") return E.BADF;\n const dir = e.kind === \"preopen\" ? \"\" : e.path;\n if ((effMode(s, dir) & 4) === 0) return E.ACCES;\n const prefix = dir ? dir + \"/\" : \"\";\n const seen = /* @__PURE__ */ new Set([\".\", \"..\"]);\n const children = [];\n const push = (name, type) => {\n if (!seen.has(name)) {\n seen.add(name);\n children.push({ name, type });\n }\n };\n for (const p of s.fs.dirs) {\n if (p !== dir && p.startsWith(prefix) && !p.slice(prefix.length).includes(\"/\")) push(p.slice(prefix.length), 3);\n }\n for (const p of s.fs.files.keys()) {\n if (p.startsWith(prefix) && !p.slice(prefix.length).includes(\"/\")) push(p.slice(prefix.length), 4);\n }\n for (const p of s.fs.symlinks.keys()) {\n if (p.startsWith(prefix) && !p.slice(prefix.length).includes(\"/\")) push(p.slice(prefix.length), 7);\n }\n for (const p of s.fs.modes.keys()) {\n if (p !== dir && p.startsWith(prefix) && !p.slice(prefix.length).includes(\"/\") && !s.fs.dirs.has(p)) push(p.slice(prefix.length), 4);\n }\n if (dir === \"bin\" || dir === \"usr/bin\") for (const name of s.coreutils.keys()) push(name, 4);\n children.sort((a, b) => a.name < b.name ? -1 : 1);\n const entries = [{ name: \".\", type: 3 }, { name: \"..\", type: 3 }, ...children];\n const dv = DV(), u8 = U8();\n let off = 0;\n for (let i = Number(cookie); i < entries.length; i++) {\n const nb = te.encode(entries[i].name);\n const need = 24 + nb.length;\n const record = new Uint8Array(need);\n const rdv = new DataView(record.buffer);\n rdv.setBigUint64(0, BigInt(i + 1), true);\n rdv.setBigUint64(8, 1n, true);\n rdv.setUint32(16, nb.length, true);\n record[20] = entries[i].type;\n record.set(nb, 24);\n const room = bufLen - off;\n if (need > room) {\n u8.set(record.subarray(0, room), buf + off);\n off = bufLen;\n break;\n }\n u8.set(record, buf + off);\n off += need;\n }\n dv.setUint32(retPtr, off, true);\n return 0;\n },\n // An fd that was never opened is EBADF everywhere below. It used to be\n // ESPIPE from fd_seek and plain SUCCESS from fd_tell and fd_filestat_get,\n // while fd_close, fd_fdstat_get and fd_write in this same table already\n // answered EBADF — one descriptor table, five different opinions about what\n // a closed descriptor is. ESPIPE in particular is a yes: it means \"open, and\n // not seekable\", so a caller using lseek to test validity read it as valid.\n fd_seek: (fd, offset, whence, retPtr) => {\n const e = proc.fds.get(fd);\n if (!e) return E.BADF;\n if (e.kind === \"file\") {\n const f = fileLookup(s, e.path);\n const len = f ? f.bytes.length : 0;\n const off = Number(offset);\n const next = whence === 0 ? off : whence === 1 ? e.pos + off : len + off;\n if (next < 0) return E.INVAL;\n e.pos = next;\n DV().setBigUint64(retPtr, BigInt(next), true);\n return 0;\n }\n DV().setBigUint64(retPtr, 0n, true);\n return E.SPIPE;\n },\n fd_tell: (fd, retPtr) => {\n const e = proc.fds.get(fd);\n if (!e) return E.BADF;\n DV().setBigUint64(retPtr, BigInt(e.kind === \"file\" ? e.pos : 0), true);\n return 0;\n },\n fd_close: (fd) => {\n if (!proc.fds.has(fd)) return E.BADF;\n closeFd(s, proc, fd);\n return 0;\n },\n fd_fdstat_get: (fd, st) => {\n const dv = DV();\n const e = proc.fds.get(fd);\n let ft = 4;\n if (e && e.kind === \"pipe\") ft = 0;\n else if (e && (e.kind === \"stdin\" || e.kind === \"stdout\" || e.kind === \"stderr\")) ft = s.stdinTty || fd !== 0 ? 2 : 0;\n else if (e && e.kind === \"tty\") ft = 2;\n else if (e && (e.kind === \"dir\" || e.kind === \"preopen\")) ft = 3;\n else if (!e) return E.BADF;\n dv.setUint8(st, ft);\n dv.setUint16(st + 2, 0, true);\n dv.setBigUint64(st + 8, 0xffffffffffffffffn, true);\n dv.setBigUint64(st + 16, 0xffffffffffffffffn, true);\n return 0;\n },\n fd_fdstat_set_flags: () => 0,\n fd_read: (fd, iovs, n, nread) => io.read(fd, readIovs(DV(), iovs, n), nread),\n fd_write: (fd, iovs, n, nw) => {\n if (!proc.fds.has(fd)) return E.BADF;\n const dv = DV(), u8 = U8();\n let w = 0;\n for (let i = 0; i < n; i++) {\n const p = dv.getUint32(iovs + i * 8, true), l = dv.getUint32(iovs + i * 8 + 4, true);\n const n2 = io.write(fd, u8.subarray(p, p + l));\n if (n2 === null) return E.BADF;\n w += n2;\n }\n dv.setUint32(nw, w, true);\n return 0;\n },\n poll_oneoff: (inPtr, outPtr, nsubs, retPtr) => io.poll(inPtr, outPtr, nsubs, retPtr),\n clock_time_get: (id, _pr, t) => {\n const ns = clockNs(id);\n if (ns === null) return E.INVAL;\n DV().setBigUint64(t, ns, true);\n return 0;\n },\n clock_res_get: (id, r) => {\n if (clockNs(id) === null) return E.INVAL;\n DV().setBigUint64(r, id === CLOCK_REALTIME ? 1000000n : 1000n, true);\n return 0;\n },\n // getRandomValues caps at 65536 bytes per call; a guest asking for more\n // gets the same CSPRNG, chunked, never a weaker one.\n random_get: (b, l) => {\n const u = U8();\n for (let off = 0; off < l; off += 65536) {\n crypto.getRandomValues(u.subarray(b + off, b + off + Math.min(l - off, 65536)));\n }\n return 0;\n },\n proc_exit: (code) => {\n throw new Exit(code);\n }\n };\n }\n function writeThroughFd(s, proc, fd, bytes) {\n const e = proc.fds.get(fd);\n if (e && e.kind === \"pipe\") {\n if (e.end === \"r\") return null;\n const pp = s.pipes.get(e.pipeId);\n pp.chunks.push(bytes.slice());\n pp.queued += bytes.length;\n wakePipe(s, pp);\n return bytes.length;\n }\n if (e && e.kind === \"file\") {\n const f = fileLookup(s, e.path) || { bytes: new Uint8Array(0) };\n if (!s.fs.files.has(e.path)) s.fs.files.set(e.path, f);\n const pos = e.append ? f.bytes.length : e.pos;\n fileWrite(s, f, e.path, pos, bytes);\n e.pos = pos + bytes.length;\n return bytes.length;\n }\n if (e && (e.kind === \"null\" || e.kind === \"tty\")) return bytes.length;\n const text = td.decode(bytes);\n if (e.kind === \"stderr\") s.err += text;\n else s.out += text;\n return bytes.length;\n }\n function tryReadFd(s, proc, fd, dv, u8, iov, nreadPtr) {\n const e = proc.fds.get(fd);\n const deliver = (bytes) => {\n dv.setUint32(nreadPtr, scatter(u8, iov, bytes), true);\n return { errno: 0 };\n };\n if (e && e.kind === \"file\") {\n const f = fileLookup(s, e.path);\n const bytes = f ? f.bytes.subarray(e.pos, e.pos + iov.total) : new Uint8Array(0);\n e.pos += bytes.length;\n return deliver(bytes);\n }\n if (e && (e.kind === \"null\" || e.kind === \"tty\")) {\n dv.setUint32(nreadPtr, 0, true);\n return { errno: 0 };\n }\n if (e && e.kind === \"pipe\") {\n const pp = s.pipes.get(e.pipeId);\n if (pp.queued > 0) return deliver(takeUpTo(pp, iov.total));\n if (pp.writers === 0) {\n dv.setUint32(nreadPtr, 0, true);\n return { errno: 0 };\n }\n return null;\n }\n if (e && e.kind === \"stdin\") {\n const st = s.stdin;\n if (st.queued > 0) return deliver(takeUpTo(st, iov.total));\n if (st.closed) {\n dv.setUint32(nreadPtr, 0, true);\n return { errno: 0 };\n }\n return null;\n }\n if (e) {\n dv.setUint32(nreadPtr, 0, true);\n return { errno: 0 };\n }\n return { errno: E.BADF };\n }\n function readSubs(dv, inPtr, nsubs) {\n const subs = [];\n for (let i = 0; i < nsubs; i++) {\n const base = inPtr + i * 48;\n const userdata = dv.getBigUint64(base, true);\n const tag = dv.getUint8(base + 8);\n if (tag === 0) {\n const id = dv.getUint32(base + 16, true);\n const timeout = dv.getBigUint64(base + 24, true);\n const abs = (dv.getUint16(base + 40, true) & 1) !== 0;\n const now = clockNs(id);\n subs.push(now === null ? { tag, userdata, id, bad: true } : { tag, userdata, id, deadline: abs ? timeout : now + timeout });\n } else {\n subs.push({ tag, userdata, fd: dv.getUint32(base + 16, true) });\n }\n }\n return subs;\n }\n function clockExpired(sub) {\n const now = clockNs(sub.id);\n return now !== null && now >= sub.deadline;\n }\n function writeEvent(dv, outPtr, slot, sub, errno, nbytes) {\n const ev = outPtr + slot * 32;\n dv.setBigUint64(ev, sub.userdata, true);\n dv.setUint16(ev + 8, errno, true);\n dv.setUint8(ev + 10, sub.tag);\n dv.setBigUint64(ev + 16, BigInt(nbytes), true);\n dv.setUint16(ev + 24, 0, true);\n }\n function fdReadReady(s, proc, fd) {\n const e = proc.fds.get(fd);\n if (!e) return null;\n if (e.kind === \"pipe\") {\n const pp = s.pipes.get(e.pipeId);\n return { ready: pp.queued > 0 || pp.writers === 0, avail: pp.queued };\n }\n if (e.kind === \"stdin\") return { ready: s.stdin.queued > 0 || s.stdin.closed, avail: s.stdin.queued };\n return { ready: true, avail: 0 };\n }\n function emitReady(s, proc, dv, outPtr, subs) {\n let n = 0;\n for (const sub of subs) {\n if (sub.tag === 0) {\n if (sub.bad) {\n writeEvent(dv, outPtr, n++, sub, E.INVAL, 0);\n continue;\n }\n if (clockExpired(sub)) writeEvent(dv, outPtr, n++, sub, 0, 0);\n continue;\n }\n const st = fdReadReady(s, proc, sub.fd);\n if (!st) {\n writeEvent(dv, outPtr, n++, sub, E.BADF, 0);\n continue;\n }\n if (sub.tag === 2 || st.ready) writeEvent(dv, outPtr, n++, sub, 0, st.avail);\n }\n return n;\n }\n var FROZEN_PROBE = 2e5;\n function waitForDeadline(s, proc, subs) {\n const clocks = subs.filter((x) => x.tag === 0 && !x.bad);\n if (!clocks.length) return;\n const startedNs = realtimeNs();\n let spins = 0;\n while (!clocks.some(clockExpired)) {\n if (s.rootExit !== null) return;\n if (subs.some((x) => x.tag === 1 && (fdReadReady(s, proc, x.fd) || { ready: true }).ready)) return;\n if (s.runnable.length) {\n pumpOne(s);\n continue;\n }\n if (++spins > FROZEN_PROBE && realtimeNs() === startedNs) return;\n }\n }\n function emitClocks(dv, outPtr, subs) {\n let n = 0;\n for (const sub of subs) if (sub.tag === 0 && !sub.bad) writeEvent(dv, outPtr, n++, sub, 0, 0);\n return n;\n }\n function blockTarget(s, proc, fd) {\n const e = proc.fds.get(fd);\n if (e && e.kind === \"pipe\") return { list: s.pipes.get(e.pipeId).readW, wake: () => wakePipe(s, s.pipes.get(e.pipeId)) };\n if (e && e.kind === \"stdin\") return { list: s.stdin.waiters, wake: () => wakeStdin(s) };\n return null;\n }\n function makeProc(s, pid, ppid, fds) {\n const proc = {\n pid,\n ppid,\n fds,\n inst: null,\n __s: s,\n ctx: { reason: null, rewinding: false, captureEnv: 0, ljEnv: 0, ljVal: 0, nextSlot: 0, resume: 0 },\n MAIN_BUF: 0,\n SLOT0: 0,\n pendingRead: null,\n slotByEnv: /* @__PURE__ */ new Map(),\n freeSlots: []\n };\n const DV = () => new DataView(proc.inst.exports.memory.buffer);\n const U8 = () => new Uint8Array(proc.inst.exports.memory.buffer);\n proc.DV = DV;\n proc.U8 = U8;\n const slotAddr = (i) => proc.SLOT0 + i * SLOT_SIZE;\n proc.slotAddr = slotAddr;\n const initHdr = (a, sz) => {\n const dv = DV();\n dv.setUint32(a, a + 8, true);\n dv.setUint32(a + 4, a + sz, true);\n };\n proc.initHdr = initHdr;\n const wstr = (p, str) => {\n const b = te.encode(str);\n U8().set(b, p);\n return b.length;\n };\n const c = proc.ctx;\n const io = {\n read: (fd, iov, nread) => {\n if (proc.pendingRead) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n const pr = proc.pendingRead;\n proc.pendingRead = null;\n DV().setUint32(pr.nreadPtr, scatter(U8(), pr.iov, pr.bytes), true);\n return 0;\n }\n const dv = DV();\n const sync = tryReadFd(s, proc, fd, dv, U8(), iov, nread);\n if (sync) return sync.errno;\n dv.setUint32(nread, 0, true);\n c.reason = \"blockread\";\n c.pipeReq = { fd, iov, nreadPtr: nread };\n initHdr(proc.MAIN_BUF, MAIN_SIZE);\n proc.inst.exports.asyncify_start_unwind(proc.MAIN_BUF);\n return 0;\n },\n write: (fd, bytes) => writeThroughFd(s, proc, fd, bytes),\n poll: (inPtr, outPtr, nsubs, retPtr) => {\n if (proc.pendingRead) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n const pr = proc.pendingRead;\n proc.pendingRead = null;\n const dv2 = DV();\n dv2.setBigUint64(outPtr, pr.pollUserdata ?? 0n, true);\n dv2.setUint16(outPtr + 8, 0, true);\n dv2.setUint8(outPtr + 10, 1);\n dv2.setBigUint64(outPtr + 16, BigInt(pr.bytes.length), true);\n dv2.setUint16(outPtr + 24, 0, true);\n dv2.setUint32(retPtr, 1, true);\n return 0;\n }\n const dv = DV();\n const subs = readSubs(dv, inPtr, nsubs);\n let emitted = emitReady(s, proc, dv, outPtr, subs);\n if (emitted > 0) {\n dv.setUint32(retPtr, emitted, true);\n return 0;\n }\n const blockSub = subs.find((x) => x.tag === 1 && blockTarget(s, proc, x.fd));\n if (!blockSub) {\n waitForDeadline(s, proc, subs);\n emitted = emitReady(s, proc, dv, outPtr, subs) || emitClocks(dv, outPtr, subs);\n dv.setUint32(retPtr, emitted, true);\n return 0;\n }\n dv.setUint32(retPtr, 0, true);\n c.reason = \"blockread\";\n c.pipeReq = { fd: blockSub.fd, iov: EMPTY_IOV, nreadPtr: 0, pollUserdata: blockSub.userdata };\n initHdr(proc.MAIN_BUF, MAIN_SIZE);\n proc.inst.exports.asyncify_start_unwind(proc.MAIN_BUF);\n return 0;\n }\n };\n const wasiBase = makeWasiFs(s, proc, DV, U8, io);\n const wasi = {\n ...wasiBase,\n args_sizes_get: (a, b) => {\n const dv = DV();\n dv.setUint32(a, s.argv.length, true);\n dv.setUint32(b, s.argv.reduce((x, v) => x + te.encode(v).length + 1, 0), true);\n return 0;\n },\n args_get: (ptrs, buf) => {\n const dv = DV();\n let p = buf;\n for (const a of s.argv) {\n dv.setUint32(ptrs, p, true);\n ptrs += 4;\n p += wstr(p, a);\n U8()[p++] = 0;\n }\n return 0;\n },\n environ_sizes_get: (a, b) => {\n const dv = DV();\n dv.setUint32(a, s.environ.length, true);\n dv.setUint32(b, s.environ.reduce((x, v) => x + te.encode(v).length + 1, 0), true);\n return 0;\n },\n environ_get: (ptrs, buf) => {\n const dv = DV();\n let p = buf;\n for (const v of s.environ) {\n dv.setUint32(ptrs, p, true);\n ptrs += 4;\n p += wstr(p, v);\n U8()[p++] = 0;\n }\n return 0;\n }\n };\n const allocSlot = (env) => {\n const prev = proc.slotByEnv.get(env);\n if (prev !== void 0) {\n proc.slotByEnv.delete(env);\n proc.freeSlots.push(prev);\n }\n let idx;\n if (proc.freeSlots.length > 1) idx = proc.freeSlots.shift();\n else if (c.nextSlot < NSLOT) idx = c.nextSlot++;\n else idx = proc.freeSlots.shift();\n if (idx === void 0) throw new Error(\"bash-runner: setjmp slot budget exceeded (\" + NSLOT + \")\");\n proc.slotByEnv.set(env, idx);\n return idx;\n };\n const nimbus_proc = {\n setjmp: (env) => {\n if (c.rewinding) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n return;\n }\n c.reason = \"capture\";\n c.captureEnv = env;\n const idx = allocSlot(env);\n const dv = DV();\n dv.setInt32(env, idx, true);\n dv.setInt32(env + 4, 0, true);\n initHdr(slotAddr(idx), SLOT_SIZE);\n proc.inst.exports.asyncify_start_unwind(slotAddr(idx));\n },\n longjmp: (env, val) => {\n if (c.rewinding) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n return;\n }\n c.reason = \"longjmp\";\n c.ljEnv = env;\n c.ljVal = val;\n initHdr(proc.MAIN_BUF, MAIN_SIZE);\n proc.inst.exports.asyncify_start_unwind(proc.MAIN_BUF);\n },\n fork: () => {\n if (c.rewinding) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n return c.resume;\n }\n c.reason = \"fork\";\n initHdr(proc.MAIN_BUF, MAIN_SIZE);\n proc.inst.exports.asyncify_start_unwind(proc.MAIN_BUF);\n return 0;\n },\n vfork: () => nimbus_proc.fork(),\n waitpid: (pid2, statusPtr, _opt) => {\n if (c.rewinding) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n if (c.waitStatusPtr != null) DV().setInt32(c.waitStatusPtr, c.resumeStatus, true);\n return c.resume;\n }\n c.reason = \"waitpid\";\n c.waitTarget = pid2;\n c.waitStatusPtr = statusPtr;\n initHdr(proc.MAIN_BUF, MAIN_SIZE);\n proc.inst.exports.asyncify_start_unwind(proc.MAIN_BUF);\n return 0;\n },\n execve: (pathPtr, argvFlatPtr, argvLen, envFlatPtr, envLen) => {\n if (c.rewinding) {\n proc.inst.exports.asyncify_stop_rewind();\n c.rewinding = false;\n return c.resume;\n }\n const u8 = U8();\n let e = pathPtr;\n while (u8[e]) e++;\n c.reason = \"exec\";\n c.execPath = td.decode(u8.subarray(pathPtr, e));\n c.execArgv = td.decode(u8.subarray(argvFlatPtr, argvFlatPtr + argvLen)).split(\"\\0\").filter((x) => x.length);\n c.execEnv = td.decode(u8.subarray(envFlatPtr, envFlatPtr + envLen)).split(\"\\0\").filter((x) => x.length);\n initHdr(proc.MAIN_BUF, MAIN_SIZE);\n proc.inst.exports.asyncify_start_unwind(proc.MAIN_BUF);\n return 0;\n },\n pipe: (fdsPtr) => {\n const id = newPipe(s);\n const rfd = lowestFd(proc);\n proc.fds.set(rfd, { kind: \"pipe\", pipeId: id, end: \"r\" });\n const wfd = lowestFd(proc);\n proc.fds.set(wfd, { kind: \"pipe\", pipeId: id, end: \"w\" });\n const dv = DV();\n dv.setInt32(fdsPtr, rfd, true);\n dv.setInt32(fdsPtr + 4, wfd, true);\n return 0;\n },\n dup: (o) => {\n const e = proc.fds.get(o);\n if (!e) return -E.BADF;\n const nf = lowestFd(proc);\n proc.fds.set(nf, { ...e });\n if (e.kind === \"pipe\") bumpPipe(s, e, 1);\n return nf;\n },\n dup2: (o, n) => {\n const e = proc.fds.get(o);\n if (!e) return -E.BADF;\n if (o === n) return n;\n if (proc.fds.has(n)) closeFd(s, proc, n);\n proc.fds.set(n, { ...e });\n if (e.kind === \"pipe\") bumpPipe(s, e, 1);\n return n;\n },\n kill: () => 0,\n setpgid: () => 0,\n getpgid: () => proc.pid,\n getppid: () => proc.ppid,\n tcsetpgrp: () => 0,\n tcgetpgrp: () => proc.pid,\n tcgetattr: () => -1,\n tcsetattr: () => 0\n };\n const envImports = {\n getpid: () => proc.pid,\n getuid: () => 0,\n geteuid: () => 0,\n getgid: () => 0,\n getegid: () => 0,\n setuid: () => 0,\n setgid: () => 0,\n umask: () => 18,\n gethostname: (p, _l) => {\n U8().set(te.encode(\"nimbus\"), p);\n return 0;\n },\n dlopen: () => 0,\n dlsym: () => 0,\n dlclose: () => 0,\n dlerror: () => 0\n };\n proc.inst = new WebAssembly.Instance(s.mod, { wasi_snapshot_preview1: wasi, nimbus_proc, env: envImports });\n s.stats.instances++;\n s.procs.set(pid, proc);\n return proc;\n }\n function setupArena(proc) {\n const base = proc.inst.exports.memory.buffer.byteLength;\n const need = MAIN_SIZE + NSLOT * SLOT_SIZE;\n proc.inst.exports.memory.grow(Math.ceil(need / PAGE));\n proc.MAIN_BUF = base;\n proc.SLOT0 = proc.MAIN_BUF + MAIN_SIZE;\n }\n function resumeProc(proc) {\n proc.ctx.rewinding = true;\n proc.inst.exports.asyncify_start_rewind(proc.MAIN_BUF);\n proc.__s.runnable.push(proc);\n }\n function trackArena(s, proc, bufAddr, size, isSlot) {\n const used = proc.DV().getUint32(bufAddr, true) - (bufAddr + 8);\n if (isSlot) {\n if (used > s.stats.slotHi) s.stats.slotHi = used;\n } else if (used > s.stats.mainHi) s.stats.mainHi = used;\n }\n function step(s, proc) {\n const c = proc.ctx, ex = proc.inst.exports;\n try {\n ex._start();\n } catch (e) {\n if (e instanceof Exit) {\n finishProc(s, proc, e.code);\n return;\n }\n throw e;\n }\n if (c.reason === null) {\n finishProc(s, proc, 0);\n return;\n }\n ex.asyncify_stop_unwind();\n const r = c.reason;\n c.reason = null;\n const dv = proc.DV();\n const mem = proc.inst.exports.memory.buffer.byteLength;\n if (mem > s.stats.memPeak) s.stats.memPeak = mem;\n if (r === \"capture\") {\n const idx = dv.getInt32(c.captureEnv, true);\n trackArena(s, proc, proc.slotAddr(idx), SLOT_SIZE, true);\n dv.setUint32(c.captureEnv + 8, dv.getUint32(proc.slotAddr(idx), true), true);\n c.rewinding = true;\n ex.asyncify_start_rewind(proc.slotAddr(idx));\n s.runnable.push(proc);\n } else if (r === \"longjmp\") {\n trackArena(s, proc, proc.MAIN_BUF, MAIN_SIZE, false);\n const idx = dv.getInt32(c.ljEnv, true), hw = dv.getUint32(c.ljEnv + 8, true);\n dv.setInt32(c.ljEnv + 4, c.ljVal, true);\n dv.setUint32(proc.slotAddr(idx), hw, true);\n c.rewinding = true;\n ex.asyncify_start_rewind(proc.slotAddr(idx));\n s.runnable.push(proc);\n } else if (r === \"fork\") {\n trackArena(s, proc, proc.MAIN_BUF, MAIN_SIZE, false);\n doFork(s, proc);\n } else if (r === \"waitpid\") {\n trackArena(s, proc, proc.MAIN_BUF, MAIN_SIZE, false);\n doWait(s, proc);\n } else if (r === \"blockread\") {\n trackArena(s, proc, proc.MAIN_BUF, MAIN_SIZE, false);\n const target = blockTarget(s, proc, c.pipeReq.fd);\n if (!target) {\n proc.pendingRead = { iov: c.pipeReq.iov, bytes: new Uint8Array(0), nreadPtr: c.pipeReq.nreadPtr, pollUserdata: c.pipeReq.pollUserdata };\n resumeProc(proc);\n } else {\n target.list.push({ proc });\n target.wake();\n }\n } else if (r === \"exec\") {\n trackArena(s, proc, proc.MAIN_BUF, MAIN_SIZE, false);\n doExec(s, proc);\n } else {\n throw new Error(\"bash-runner: unknown unwind reason \" + r);\n }\n }\n function pumpOne(s) {\n if (!s.runnable.length) return false;\n step(s, s.runnable.shift());\n return true;\n }\n function doExec(s, proc) {\n const name = proc.ctx.execPath.split(\"/\").pop();\n const env = proc.ctx.execEnv && proc.ctx.execEnv.length ? proc.ctx.execEnv : s.environ;\n const pwdVar = env.find((x) => x.startsWith(\"PWD=\"));\n const childCwd = pwdVar ? norm(pwdVar.slice(4)) : s.cwd;\n const path = makeResolve(s, childCwd)(proc.ctx.execPath);\n const viaPath = path.startsWith(\"bin/\") || path.startsWith(\"usr/bin/\");\n const m = viaPath ? s.coreutils.get(name) : null;\n if (!m) {\n proc.ctx.resume = fileLookup(s, path) ? -45 : fileExists(s, path) ? -2 : -44;\n proc.ctx.rewinding = true;\n proc.inst.exports.asyncify_start_rewind(proc.MAIN_BUF);\n s.runnable.push(proc);\n return;\n }\n let inst2;\n const DV = () => new DataView(inst2.exports.memory.buffer);\n const U8 = () => new Uint8Array(inst2.exports.memory.buffer);\n const tv = proc.ctx.execArgv;\n const absRoots = /* @__PURE__ */ new Set();\n for (let i = 1; i < tv.length; i++) if (tv[i].startsWith(\"/\")) absRoots.add(norm(tv[i]));\n const wstr = (p, str) => {\n const b = te.encode(str);\n U8().set(b, p);\n return b.length;\n };\n let code = 0;\n const io = {\n read: (fd, iov, nread) => {\n const dv = DV();\n for (; ; ) {\n const sync = tryReadFd(s, proc, fd, dv, U8(), iov, nread);\n if (sync) return sync.errno;\n if (!pumpOne(s)) {\n dv.setUint32(nread, 0, true);\n return 0;\n }\n }\n },\n write: (fd, bytes) => writeThroughFd(s, proc, fd, bytes),\n // An exec'd tool cannot park, so a wait it cannot satisfy locally is spent\n // pumping the scheduler — the same discipline its blocking reads use.\n poll: (inPtr, outPtr, nsubs, retPtr) => {\n const dv = DV();\n const subs = readSubs(dv, inPtr, nsubs);\n let emitted = emitReady(s, proc, dv, outPtr, subs);\n if (emitted === 0) {\n waitForDeadline(s, proc, subs);\n emitted = emitReady(s, proc, dv, outPtr, subs) || emitClocks(dv, outPtr, subs);\n }\n dv.setUint32(retPtr, emitted, true);\n return 0;\n }\n };\n const base = makeWasiFs(s, proc, DV, U8, io, childCwd, absRoots);\n const w = {\n ...base,\n args_sizes_get: (a, b) => {\n const dv = DV();\n dv.setUint32(a, tv.length, true);\n dv.setUint32(b, tv.reduce((x, v) => x + te.encode(v).length + 1, 0), true);\n return 0;\n },\n args_get: (ptrs, buf) => {\n const dv = DV();\n let p = buf;\n for (const a of tv) {\n dv.setUint32(ptrs, p, true);\n ptrs += 4;\n p += wstr(p, a);\n U8()[p++] = 0;\n }\n return 0;\n },\n // POSIX environment inheritance: the child env bash passed to execve.\n environ_sizes_get: (a, b) => {\n const dv = DV();\n dv.setUint32(a, env.length, true);\n dv.setUint32(b, env.reduce((x, v) => x + te.encode(v).length + 1, 0), true);\n return 0;\n },\n environ_get: (ptrs, buf) => {\n const dv = DV();\n let p = buf;\n for (const v of env) {\n dv.setUint32(ptrs, p, true);\n ptrs += 4;\n p += wstr(p, v);\n U8()[p++] = 0;\n }\n return 0;\n },\n proc_exit: (ec) => {\n code = ec;\n throw new Exit(ec);\n }\n };\n const chmodResolve = makeResolve(s, childCwd, absRoots);\n const nimbus_proc = {\n chmod: (pathPtr, pathLen, mode) => {\n const p = chmodResolve(td.decode(U8().subarray(pathPtr, pathPtr + pathLen)));\n const trav = checkTraversal(s, p);\n if (trav) return trav;\n if (!inodeExists(s, p) && !(p !== \"\" && isDir(s, p))) return E.NOENT;\n const bits = mode & 511;\n s.fs.modes.set(p, bits >> 6 & 7);\n s.fs.modesChanged.set(p, bits);\n return 0;\n }\n };\n inst2 = new WebAssembly.Instance(m, { wasi_snapshot_preview1: w, nimbus_proc });\n s.stats.instances++;\n const mem2 = inst2.exports.memory.buffer.byteLength;\n if (mem2 > s.stats.memPeak) s.stats.memPeak = mem2;\n try {\n inst2.exports._start();\n } catch (e) {\n if (e instanceof Exit) code = e.code;\n else throw e;\n }\n finishProc(s, proc, code);\n }\n function doFork(s, parent) {\n const childPid = s.pidNext++;\n const childFds = /* @__PURE__ */ new Map();\n for (const [fd, e] of parent.fds) {\n childFds.set(fd, { ...e });\n if (e.kind === \"pipe\") bumpPipe(s, e, 1);\n }\n const child = makeProc(s, childPid, parent.pid, childFds);\n const pmem = parent.inst.exports.memory, cmem = child.inst.exports.memory;\n if (cmem.buffer.byteLength < pmem.buffer.byteLength) cmem.grow((pmem.buffer.byteLength - cmem.buffer.byteLength) / PAGE);\n new Uint8Array(cmem.buffer).set(new Uint8Array(pmem.buffer));\n for (const [k, v] of Object.entries(parent.inst.exports)) if (v instanceof WebAssembly.Global) child.inst.exports[k].value = v.value;\n child.MAIN_BUF = parent.MAIN_BUF;\n child.SLOT0 = parent.SLOT0;\n child.ctx.nextSlot = parent.ctx.nextSlot;\n child.slotByEnv = new Map(parent.slotByEnv);\n child.freeSlots = parent.freeSlots.slice();\n const total = s.procs.size;\n if (total * cmem.buffer.byteLength > s.stats.memPeak) s.stats.memPeak = total * cmem.buffer.byteLength;\n child.ctx.resume = 0;\n child.ctx.rewinding = true;\n child.inst.exports.asyncify_start_rewind(child.MAIN_BUF);\n s.runnable.push(child);\n parent.ctx.resume = childPid;\n parent.ctx.rewinding = true;\n parent.inst.exports.asyncify_start_rewind(parent.MAIN_BUF);\n s.runnable.push(parent);\n }\n function doWait(s, proc) {\n const t = proc.ctx.waitTarget;\n let pid = null;\n if (t > 0) {\n const e = s.exitStatus.get(t);\n if (e && e.ppid === proc.pid) pid = t;\n } else {\n for (const [p, e] of s.exitStatus) {\n if (e.ppid === proc.pid) {\n pid = p;\n break;\n }\n }\n }\n if (pid != null) {\n const st = s.exitStatus.get(pid).status;\n s.exitStatus.delete(pid);\n proc.ctx.resume = pid;\n proc.ctx.resumeStatus = st;\n resumeProc(proc);\n } else {\n s.waiters.push({ proc, targetPid: t });\n }\n }\n function finishProc(s, proc, code) {\n const st = (code & 255) << 8;\n s.procs.delete(proc.pid);\n for (const fd of [...proc.fds.keys()]) closeFd(s, proc, fd);\n if (proc.ppid === 0) s.rootExit = code;\n s.exitStatus.set(proc.pid, { status: st, ppid: proc.ppid });\n for (let i = 0; i < s.waiters.length; i++) {\n const w = s.waiters[i];\n if (w.proc.pid === proc.ppid && (w.targetPid === proc.pid || w.targetPid <= 0)) {\n s.waiters.splice(i, 1);\n w.proc.ctx.resume = proc.pid;\n w.proc.ctx.resumeStatus = st;\n s.exitStatus.delete(proc.pid);\n resumeProc(w.proc);\n break;\n }\n }\n }\n function composeFsDiff(s) {\n const filesWritten = {};\n for (const path of s.fs.written) {\n if (path.startsWith(\"etc/nimbus.bashrc\")) continue;\n const f = s.fs.files.get(path);\n if (f) filesWritten[path] = bytesToB64(f.bytes);\n }\n return {\n filesWritten,\n filesDeleted: [...s.fs.deleted],\n dirsCreated: [...s.fs.dirsCreated],\n // Deepest-first so the flush rmdirs children before their parents.\n dirsDeleted: [...s.fs.dirsDeleted].sort((a, b) => b.length - a.length),\n modesChanged: Object.fromEntries(s.fs.modesChanged),\n symlinksCreated: Object.fromEntries(s.fs.symlinksCreated)\n };\n }\n function pump(s) {\n try {\n while (s.runnable.length) {\n if (++s.steps > 5e6) throw new Error(\"bash-runner: runaway scheduler (>5M steps)\");\n step(s, s.runnable.shift());\n if (s.rootExit !== null) break;\n }\n } catch (e) {\n s.error = String(e && e.stack || e && e.message || e);\n }\n const out = s.out, err = s.err;\n s.out = \"\";\n s.err = \"\";\n const stats = { ...s.stats, steps: s.steps, missingWasi: [...s.missingWasi] };\n if (s.error) {\n S = null;\n return { state: \"error\", exitCode: 1, stdout: out, stderr: err, error: s.error, stats };\n }\n if (s.rootExit !== null || s.procs.size === 0) {\n const code = s.rootExit === null ? 0 : s.rootExit;\n const fsDiff = composeFsDiff(s);\n S = null;\n return { state: \"exited\", exitCode: code, stdout: out, stderr: err, fsDiff, stats };\n }\n if (s.stdin.waiters.length > 0) {\n return { state: \"need-input\", exitCode: 0, stdout: out, stderr: err, stats };\n }\n S = null;\n return { state: \"error\", exitCode: 1, stdout: out, stderr: err, error: \"bash-runner: deadlock \\u2014 live procs with empty run queue\", stats };\n }\n globalThis.__bashBoot = function __bashBoot(args) {\n try {\n S = newSession(args);\n initStdinQueued(S);\n const root = makeProc(S, S.pidNext++, 0, /* @__PURE__ */ new Map([\n [0, { kind: \"stdin\" }],\n [1, { kind: \"stdout\" }],\n [2, { kind: \"stderr\" }],\n [3, { kind: \"preopen\" }]\n ]));\n S.rootPid = root.pid;\n setupArena(root);\n S.runnable.push(root);\n return pump(S);\n } catch (e) {\n S = null;\n return { state: \"error\", exitCode: 1, stdout: \"\", stderr: \"\", error: \"boot failed: \" + String(e && e.message || e) };\n }\n };\n globalThis.__bashFeed = function __bashFeed(args) {\n if (!S) {\n return { state: \"error\", exitCode: 1, stdout: \"\", stderr: \"\", error: \"bash facet has no active session (warm isolate recycled?)\" };\n }\n try {\n if (args.data) {\n const b = te.encode(args.data);\n S.stdin.chunks.push(b);\n S.stdin.queued += b.length;\n }\n if (args.eof) S.stdin.closed = true;\n wakeStdin(S);\n return pump(S);\n } catch (e) {\n const s = S;\n S = null;\n return { state: \"error\", exitCode: 1, stdout: s ? s.out : \"\", stderr: s ? s.err : \"\", error: \"feed failed: \" + String(e && e.message || e) };\n }\n };\n})();\n";
@@ -0,0 +1,347 @@
1
+ /**
2
+ * bash-runner — real GNU bash 5.2.37 (wasm32-wasi, asyncified) in a
3
+ * dedicated facet.
4
+ *
5
+ * bash CANNOT run through the stock JSPI wasm-runner: the binary is
6
+ * asyncify-instrumented (fork/setjmp/blocking-pipe unwinds) and needs
7
+ * 15 `nimbus_proc` imports plus MULTIPLE instances per facet (fork).
8
+ * This runner embeds the proven multi-instance fork/pipe/exec/setjmp
9
+ * scheduler (packages/worker/wasm/bash/run-bash-fork.mjs — the local
10
+ * acid-test driver, itself a port of the PROVEN-LIVE fork M1/M2/M3
11
+ * mechanisms) as a facet preamble.
12
+ *
13
+ * Architecture (mirrors ruby-runner's facet dispatch):
14
+ * - bash.async.wasm + the coreutil exec targets ship through the facet
15
+ * host, which compiles them and exposes them on
16
+ * globalThis.__NIMBUS_WASM.
17
+ * - The preamble defines __bashBoot / __bashFeed. Boot instantiates
18
+ * bash, pumps the scheduler until the process tree exits or the
19
+ * root parks on a terminal stdin read; each feed delivers stdin
20
+ * bytes and pumps again. Facet state persists across submits on
21
+ * the warm isolate (same mechanism as __rubyInstance caching).
22
+ * - stdout/stderr accumulate per pump slice and stream back to the
23
+ * CommandContext; VFS writes come back as a WasiFsDiff on exit.
24
+ */
25
+ import type { RuntimeManifest } from './runtime-manifest.js';
26
+ import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
27
+ import type { Facet, FacetHost } from './facet-host.js';
28
+ import type { Command, CommandContext, CommandInputStream } from '../substrate/lifo/commands/types.js';
29
+ import { z } from 'zod';
30
+ import type { WasiFsDiff } from './vfs-snapshot.js';
31
+ import type { BashBootArgs, BashFeedArgs, BashSlice } from './bash/types.js';
32
+ import { BASH_RUNNER_BODY_SRC } from './bash-runner.generated.js';
33
+ import { flushVfsDiff, snapshotVfs } from './vfs-snapshot.js';
34
+ import { requireVfsCred } from './os-contracts.js';
35
+ import { resolveVfsPath } from '../vfs/path.js';
36
+
37
+ type BashRunnerFactory = (
38
+ manifest: RuntimeManifest,
39
+ installRoot: string,
40
+ binName: string,
41
+ binKind: string | undefined,
42
+ ) => Command;
43
+
44
+ type BashStepArgs = BashBootArgs | BashFeedArgs;
45
+
46
+ const BashSliceSchema = z.object({
47
+ state: z.enum(['need-input', 'exited', 'error']),
48
+ exitCode: z.number().optional(),
49
+ stdout: z.string().optional(),
50
+ stderr: z.string().optional(),
51
+ error: z.string().optional(),
52
+ fsDiff: z.custom<WasiFsDiff>().optional(),
53
+ stats: z.record(z.string(), z.unknown()).optional(),
54
+ }).passthrough();
55
+
56
+ function normalizeSlice(raw: unknown): BashSlice | null {
57
+ const parsed = BashSliceSchema.safeParse(raw);
58
+ if (!parsed.success) return null;
59
+ return {
60
+ state: parsed.data.state,
61
+ exitCode: Number(parsed.data.exitCode ?? 0),
62
+ stdout: parsed.data.stdout || '',
63
+ stderr: parsed.data.stderr || '',
64
+ error: parsed.data.error,
65
+ fsDiff: parsed.data.fsDiff,
66
+ stats: parsed.data.stats,
67
+ };
68
+ }
69
+
70
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
71
+ const out = new ArrayBuffer(bytes.byteLength);
72
+ new Uint8Array(out).set(bytes);
73
+ return out;
74
+ }
75
+
76
+ function errorMessage(error: unknown): string {
77
+ if (error instanceof Error) return error.message;
78
+ return String(error);
79
+ }
80
+
81
+ async function bashFacetStep(args: BashStepArgs): Promise<unknown> {
82
+ const boot = Reflect.get(globalThis, '__bashBoot');
83
+ const feed = Reflect.get(globalThis, '__bashFeed');
84
+ if (typeof boot !== 'function' || typeof feed !== 'function') {
85
+ return {
86
+ state: 'error',
87
+ exitCode: 127,
88
+ stdout: '',
89
+ stderr: '',
90
+ error: 'bash-runner preamble missing (__bashBoot/__bashFeed not in scope)',
91
+ };
92
+ }
93
+ return args.op === 'boot' ? boot(args) : feed(args);
94
+ }
95
+
96
+ export interface BashFacetSession {
97
+ readonly initial: BashSlice;
98
+ push(data: string, eof?: boolean): Promise<BashSlice>;
99
+ close(): Promise<void>;
100
+ }
101
+
102
+ export async function createBashFacetSession(deps: {
103
+ facets: FacetHost;
104
+ vfs: CredentialedVfs;
105
+ manifest: RuntimeManifest;
106
+ installRoot: string;
107
+ argv: string[];
108
+ env: Record<string, string>;
109
+ cwd: string;
110
+ stdinData?: string;
111
+ stdinClosed: boolean;
112
+ stdinTty: boolean;
113
+ extraRoots?: string[];
114
+ }): Promise<BashFacetSession> {
115
+ const findFile = (relativePath: string): string | null => {
116
+ const entry = deps.manifest.files.find((file) => file.path === relativePath);
117
+ return entry ? `${deps.installRoot}/${entry.path}` : null;
118
+ };
119
+ const bashWasmPath = findFile('share/bash/bash.async.wasm');
120
+ if (!bashWasmPath || !deps.vfs.exists(bashWasmPath)) {
121
+ throw new Error("bash.async.wasm missing (re-run 'nimbus install bash')");
122
+ }
123
+
124
+ const userEnv: Record<string, string> = { ...deps.env };
125
+ userEnv.HOME ||= '/home/user';
126
+ userEnv.PATH ||= '/bin:/usr/bin';
127
+ userEnv.TERM ||= 'dumb';
128
+ userEnv.NIMBUS_PWD = deps.cwd;
129
+ userEnv.BASH_ENV ||= '/etc/nimbus.bashrc';
130
+ userEnv.PWD = deps.cwd;
131
+
132
+ const extraRoots = [...(deps.extraRoots ?? [])];
133
+ if (userEnv.HOME !== '/home/user') extraRoots.push(userEnv.HOME);
134
+ const fsSnapshot = snapshotVfs(deps.vfs, deps.cwd, { extraRoots });
135
+ if ('error' in fsSnapshot) throw new Error(fsSnapshot.error);
136
+
137
+ const wasmModules: Record<string, ArrayBuffer> = {
138
+ 'bash.async.wasm': toArrayBuffer(deps.vfs.readFile(bashWasmPath)),
139
+ };
140
+ for (const file of deps.manifest.files) {
141
+ const prefix = 'share/bash/coreutils/';
142
+ if (!file.path.startsWith(prefix) || !file.path.endsWith('.wasm')) continue;
143
+ const name = file.path.slice(prefix.length, -'.wasm'.length);
144
+ const vfsPath = `${deps.installRoot}/${file.path}`;
145
+ if (deps.vfs.exists(vfsPath)) {
146
+ wasmModules[`cu_${name}.wasm`] = toArrayBuffer(deps.vfs.readFile(vfsPath));
147
+ }
148
+ }
149
+
150
+ const appletsPath = findFile('share/bash/coreutils/busybox.applets');
151
+ const busyboxApplets = appletsPath && deps.vfs.exists(appletsPath)
152
+ ? new TextDecoder().decode(deps.vfs.readFile(appletsPath))
153
+ .split('\n')
154
+ .map((line) => line.trim())
155
+ .filter(Boolean)
156
+ : [];
157
+
158
+ const facet: Facet = deps.facets.open({
159
+ tag: 'bash-runner',
160
+ concurrency: 1,
161
+ // No supervisor capability: bash is seeded with its subtree by value and
162
+ // hands the whole mutation back as a diff on exit, so it makes no syscall
163
+ // into the session while it runs.
164
+ preamble: BASH_RUNNER_PREAMBLE,
165
+ wasmModules,
166
+ });
167
+
168
+ let active = true;
169
+ let closed = false;
170
+ const submit = async (args: BashStepArgs): Promise<BashSlice> => {
171
+ const slice = normalizeSlice(
172
+ await facet.submit<BashStepArgs, unknown>(bashFacetStep, args, { timeoutMs: 300_000 }),
173
+ );
174
+ if (!slice) throw new Error('facet returned an invalid payload');
175
+ if (slice.state === 'exited') {
176
+ if (slice.fsDiff) flushVfsDiff(deps.vfs, slice.fsDiff);
177
+ active = false;
178
+ } else if (slice.state === 'error') {
179
+ active = false;
180
+ }
181
+ return slice;
182
+ };
183
+
184
+ try {
185
+ const initial = await submit({
186
+ op: 'boot',
187
+ argv: deps.argv,
188
+ environ: Object.entries(userEnv).map(([key, value]) => `${key}=${value}`),
189
+ cwd: deps.cwd,
190
+ fsSnapshot: fsSnapshot.snapshot,
191
+ stdinData: deps.stdinData ?? '',
192
+ stdinClosed: deps.stdinClosed,
193
+ stdinTty: deps.stdinTty,
194
+ busyboxApplets,
195
+ });
196
+ return {
197
+ initial,
198
+ push(data, eof = false) {
199
+ if (closed) throw new Error('bash facet session is closed');
200
+ return submit({ op: 'feed', data, eof });
201
+ },
202
+ async close() {
203
+ if (closed) return;
204
+ try {
205
+ if (active) await submit({ op: 'feed', data: '', eof: true });
206
+ } catch {
207
+ // Session teardown is best-effort; the owning command already
208
+ // reports dispatch failures from boot/push.
209
+ } finally {
210
+ closed = true;
211
+ facet.dispose();
212
+ }
213
+ },
214
+ };
215
+ } catch (error: unknown) {
216
+ facet.dispose();
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /** bash flags that consume the following argv element. */
222
+ const BASH_OPT_WITH_ARG = new Set(['-c', '-o', '+o', '--rcfile', '--init-file']);
223
+
224
+ /**
225
+ * Locate the script-path argv element (first non-flag arg when -c is
226
+ * absent) so the handler can resolve it against the session cwd —
227
+ * bash's own cwd inside the facet starts at '/' until the BASH_ENV
228
+ * chdir runs, so relative script paths must be made absolute host-side.
229
+ * Returns the argv index or -1 (interactive / -c / stdin modes).
230
+ */
231
+ function findScriptArgIndex(argv: string[]): number {
232
+ for (let i = 0; i < argv.length; i++) {
233
+ const a = argv[i];
234
+ if (a === '--') return i + 1 < argv.length ? i + 1 : -1;
235
+ if (a === '-' ) return -1; // read from stdin
236
+ if (a.startsWith('-') || a.startsWith('+')) {
237
+ if (BASH_OPT_WITH_ARG.has(a)) {
238
+ if (a === '-c') return -1; // command string mode
239
+ i++; // skip the option's argument
240
+ }
241
+ continue;
242
+ }
243
+ return i;
244
+ }
245
+ return -1;
246
+ }
247
+
248
+ export function makeBashRunnerFactory(deps: {
249
+ facets: FacetHost;
250
+ vfs: SqliteVFS;
251
+ }): BashRunnerFactory {
252
+ return function bashRunnerFactory(manifest, installRoot, binName, _binKind) {
253
+ return async function bashBinHandler(ctx: CommandContext): Promise<number> {
254
+ // All VFS access (runtime wasm reads, script probes, snapshot,
255
+ // fsDiff writeback) runs as the INVOKING process credential —
256
+ // S2a enforcement applies to bash exactly as to ruby/python.
257
+ const cred = requireVfsCred('cred' in ctx ? ctx.cred : undefined, binName);
258
+ const vfs = deps.vfs.as(cred);
259
+ const argv = [...(ctx.args ?? [])];
260
+ const cwd = ctx.cwd || '/home/user';
261
+
262
+ // Resolve a relative script path against the session cwd.
263
+ const scriptIdx = findScriptArgIndex(argv);
264
+ const extraRoots: string[] = [];
265
+ if (scriptIdx >= 0) {
266
+ const abs = resolveVfsPath(argv[scriptIdx], cwd);
267
+ if (!vfs.exists(abs)) {
268
+ ctx.stderr.write(`${binName}: ${argv[scriptIdx]}: No such file or directory\n`);
269
+ return 127;
270
+ }
271
+ // Pass bash an ABSOLUTE path: the facet chdir's to the session
272
+ // cwd via BASH_ENV before opening the script, so a relative arg
273
+ // would resolve against cwd twice. resolveVfsPath returns a
274
+ // slash-less canonical key; re-anchor it at root.
275
+ argv[scriptIdx] = '/' + abs;
276
+ const dir = abs.replace(/\/[^/]*$/, '');
277
+ if (dir) extraRoots.push(dir);
278
+ }
279
+
280
+ // stdin plumbing. A terminal-backed fd 0 feeds incrementally
281
+ // (interactive bash, `read` builtins); a piped stdin is drained
282
+ // upfront and closed so the scheduler never parks on it.
283
+ const stdinIsTty = typeof ctx.isFdTerminal === 'function' ? ctx.isFdTerminal(0) : !ctx.stdin;
284
+ const feedStream: CommandInputStream | undefined = ctx.terminalStdin ?? ctx.stdin;
285
+ let stdinData = '';
286
+ let stdinClosed = true;
287
+ if (stdinIsTty && feedStream) {
288
+ stdinClosed = false;
289
+ } else if (ctx.stdin) {
290
+ stdinData = await ctx.stdin.readAll();
291
+ }
292
+
293
+ let session: BashFacetSession | null = null;
294
+ try {
295
+ session = await createBashFacetSession({
296
+ facets: deps.facets,
297
+ vfs,
298
+ manifest,
299
+ installRoot,
300
+ argv: [binName, ...argv],
301
+ env: ctx.env || {},
302
+ cwd,
303
+ stdinData,
304
+ stdinClosed,
305
+ stdinTty: stdinIsTty,
306
+ extraRoots,
307
+ });
308
+ let slice = session.initial;
309
+
310
+ for (;;) {
311
+ if (slice.stdout) ctx.stdout.write(slice.stdout);
312
+ if (slice.stderr) ctx.stderr.write(slice.stderr);
313
+ if (slice.state === 'exited') {
314
+ return slice.exitCode;
315
+ }
316
+ if (slice.state === 'error') {
317
+ ctx.stderr.write(`${binName}: ${slice.error || 'bash facet error'}\n`);
318
+ return slice.exitCode || 1;
319
+ }
320
+ // need-input: pull the next chunk from the terminal.
321
+ let data = '';
322
+ let eof = true;
323
+ if (!ctx.signal.aborted && feedStream) {
324
+ const chunk = await feedStream.read();
325
+ if (!ctx.signal.aborted) { data = chunk === null ? '' : chunk.replace(/\r\n?/g, '\n'); eof = chunk === null; }
326
+ }
327
+ slice = await session.push(data, eof);
328
+ }
329
+ } catch (e: unknown) {
330
+ ctx.stderr.write(`${binName}: dispatch failed: ${errorMessage(e)}\n`);
331
+ return 1;
332
+ } finally {
333
+ await session?.close();
334
+ }
335
+ };
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Source string injected as the facet `preamble`. The facet's scope evaluates
341
+ * it verbatim so `__bashBoot` / `__bashFeed` are in scope when the user fn
342
+ * runs. Self-contained — no closure captures, no imports.
343
+ *
344
+ * The scheduler itself lives in `bash/preamble.ts` as real TypeScript; the build
345
+ * bundles it into `bash-runner.generated.ts`.
346
+ */
347
+ export const BASH_RUNNER_PREAMBLE: string = BASH_RUNNER_BODY_SRC;