From be0d711828e15bc83996f1e45fc28a49ab937efc Mon Sep 17 00:00:00 2001 From: Keys <70819367+keys-i@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:34:43 +1000 Subject: [PATCH] feat: consume standalone Krad Wasm --- README.md | 13 ++++- docs/CHANGELOG.md | 4 ++ javascripts/browser.fixture.html | 8 ++- javascripts/index.js | 1 + javascripts/shell.min.js | 92 ++++++++++++++++--------------- javascripts/test.js | 40 ++++++++++++++ javascripts/wasm.js | 66 +++++++++++++++++++++- package.json | 3 + wasm/krad-add.SOURCE | 7 +++ wasm/krad-add.wasm | Bin 0 -> 70 bytes 10 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 wasm/krad-add.SOURCE create mode 100644 wasm/krad-add.wasm diff --git a/README.md b/README.md index d6f1e59..b447bd9 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,17 @@ A small, safe shell engine that can sit behind any web terminal UI. ES modules expose the same headless API: ```js -import { createShell, createManuals, mountShell, profiles } from "./javascripts/index.js"; +import { createKradAdd, createManuals, createShell, mountShell, profiles } from "./javascripts/index.js"; const shell = createShell({ profile: profiles.linux, files: { "/etc/motd": "recovery ready\n" }, manuals: createManuals({ base: "/manuals/", profile: "linux" }), + commands: { "krad-add": createKradAdd() }, wasm: "auto", }); -const { code, stdout, stderr } = await shell.exec("cat /etc/motd"); +const { code, stdout, stderr } = await shell.exec("krad-add 20 22"); ``` The engine provides quotes, escapes, variables, assignments, `;`, `&&`, `||`, @@ -54,6 +55,14 @@ The optional Rust WebAssembly module accelerates large literal line filters. It is lazy, import-free, and has a JavaScript fallback. Short commands do not fetch or instantiate Wasm. `await shell.prepare("wasm")` warms it explicitly. +`createKradAdd()` registers Krad's pinned `krad-add.wasm` through the existing +custom-command boundary. The 70-byte module is fetched lazily, digest-checked, +limited to 4 KiB, and rejected if it imports host capabilities or lacks the +expected function. It is built by the standalone Rust/LLVM/WASI SDK Krad +workspace without a third-party transpiler dependency. This fixture proves the +package boundary; it is not a compiler, libc, native-ISA translator, or +Linux/BSD distribution. + ## Manuals `npm run manuals` downloads commit-pinned roff from the official FreeBSD cgit diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3988104..be2582c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- add a verified standalone Krad WebAssembly command + ## [0.3.0](https://github.com/keys-i/shell.js/compare/v0.2.0...v0.3.0) - 2026-08-01 ### Added diff --git a/javascripts/browser.fixture.html b/javascripts/browser.fixture.html index 1a26a63..a70ee2a 100644 --- a/javascripts/browser.fixture.html +++ b/javascripts/browser.fixture.html @@ -33,8 +33,10 @@ window.browserReport = (async () => { assert(typeof ShellJS?.createShell === "function", "classic global missing"); + assert(typeof ShellJS.createKradAdd === "function", "Krad command missing"); const esm = await import("./index.js"); assert(typeof esm.createShell === "function", "ES module missing"); + assert(typeof esm.createKradAdd === "function", "ES module Krad command missing"); assert((await esm.createShell().exec("echo esm")).stdout === "esm\n", "ES module command failed"); const coldMs = performance.now() - window.fixtureStart; @@ -46,7 +48,10 @@ const shell = ShellJS.createShell({ profile: "freebsd", limits: { maxRuntimeMs: 2_000 }, - commands: { hang: () => new Promise(() => {}) }, + commands: { + hang: () => new Promise(() => {}), + "krad-add": ShellJS.createKradAdd(), + }, manuals: { base: "/missing/" }, }); const ui = ShellJS.mountShell(root, shell, { @@ -72,6 +77,7 @@ assert(input.value === "echo koala", "history up failed"); key(input, "ArrowDown"); assert(input.value === "", "history down failed"); + assert((await submit("krad-add 20 22")).stdout === "42\n", "Krad command failed"); input.value = "unam"; key(input, "Tab"); assert(input.value === "uname", "completion failed"); diff --git a/javascripts/index.js b/javascripts/index.js index 9325549..b3ea3c3 100644 --- a/javascripts/index.js +++ b/javascripts/index.js @@ -1,3 +1,4 @@ export { createShell, MemoryFS, profiles } from "./shell.js"; export { createManuals } from "./man.js"; export { mountShell } from "./ui.js"; +export { createKradAdd } from "./wasm.js"; diff --git a/javascripts/shell.min.js b/javascripts/shell.min.js index f6b6121..f1eab21 100644 --- a/javascripts/shell.min.js +++ b/javascripts/shell.min.js @@ -1,55 +1,57 @@ -var ShellJS=(()=>{var Y=Object.defineProperty;var ve=Object.getOwnPropertyDescriptor;var Ee=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var Oe=(t,e)=>{for(var n in e)Y(t,n,{get:e[n],enumerable:!0})},je=(t,e,n,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Ee(e))!xe.call(t,r)&&r!==n&&Y(t,r,{get:()=>e[r],enumerable:!(s=ve(e,r))||s.enumerable});return t};var ke=t=>je(Y({},"__esModule",{value:!0}),t);var bt={};Oe(bt,{MemoryFS:()=>W,createManuals:()=>Z,createShell:()=>ye,mountShell:()=>ge,profiles:()=>D});var Se=new TextEncoder,se=/^[A-Za-z_][A-Za-z0-9_]*$/,J=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s,f=(t="",e=0,n="")=>({code:e,stdout:t,stderr:n}),y=(t,e,n=1)=>f("",n,`${t}: ${e} -`),B=(t,e)=>(t.length?t:["-"]).map(n=>({path:n,text:n==="-"?e.stdin:e.fs.read(n,e.cwd)})),Te=(t,e)=>f(B(t,e).map(({text:n})=>n).join("")),ze=(t,e)=>{if(t.length>1)return y("cd","too many arguments");let n=t[0]==="-"?e.env.OLDPWD:t[0]??e.env.HOME??"/";return n?(e.chdir(n),f(t[0]==="-"?`${e.cwd} -`:"")):y("cd","OLDPWD not set")},Ae=t=>{let e=t[0]!=="-n";return e||(t=t.slice(1)),f(`${t.join(" ")}${e?` -`:""}`)},oe=async(t,e)=>{let n={...e.env};for(;t[0]?.match(J);){let[,s,r]=t.shift().match(J);n[s]=r}return t.length?e.invoke(t[0],t.slice(1),{env:n,stdin:e.stdin}):f(Object.keys(n).sort().map(s=>`${s}=${n[s]} -`).join(""))},Ne=(t,e)=>{if(!t.length)return oe(t,e);let n="",s=0;for(let r of t)Object.hasOwn(e.env,r)?n+=`${e.env[r]} -`:s=1;return f(n,s)},Le=(t,e)=>{if(!t.length)return f(Object.keys(e.env).sort().map(n=>`export ${n}=${JSON.stringify(e.env[n])} -`).join(""));for(let n of t){let s=n.match(J);if(s)e.setenv(s[1],s[2]);else if(se.test(n))e.setenv(n,e.env[n]??"");else return y("export",`${n}: invalid name`,2)}return f()},_e=(t,e)=>{for(let n of t){if(!se.test(n))return y("unset",`${n}: invalid name`,2);e.unsetenv(n)}return f()},We=(t,e)=>{let n=!1,s=!1,r=!1,o=!1;for(;/^-[ivnq]+$/.test(t[0]??"");)for(let u of t.shift().slice(1))u==="i"&&(n=!0),u==="v"&&(s=!0),u==="n"&&(r=!0),u==="q"&&(o=!0);if(!t.length)return y("grep","missing pattern",2);let i=t.shift();n&&(i=i.toLocaleLowerCase());let c="",m=!1,a=B(t,e);if(!n&&!r&&!o&&a.length===1){let u=e.wasm?.filter(a[0].text,i,s);if(u!=null)return f(u,u?0:1)}for(let{path:u,text:d}of a){let l=d.split(` -`);l.at(-1)===""&&l.pop(),l.forEach((v,b)=>{(n?v.toLocaleLowerCase():v).includes(i)!==s&&(m=!0,o||(c+=`${a.length>1?`${u}:`:""}${r?`${b+1}:`:""}${v} -`))})}return f(c,m?0:1)},De=(t,e)=>{let n=10;if(t[0]==="-n"?(n=Number(t[1]),t=t.slice(2)):/^-\d+$/.test(t[0]??"")&&(n=Number(t.shift().slice(1))),!Number.isSafeInteger(n)||n<0)return y("head","invalid line count",2);let s=B(t,e);return f(s.map(({path:r,text:o})=>{let i=n?(o.match(/[^\n]*\n|[^\n]+$/g)??[]).slice(0,n).join(""):"";return`${s.length>1?`==> ${r} <== +var ShellJS=(()=>{var Q=Object.defineProperty;var xe=Object.getOwnPropertyDescriptor;var ke=Object.getOwnPropertyNames;var Oe=Object.prototype.hasOwnProperty;var je=(t,e)=>{for(var r in e)Q(t,r,{get:e[r],enumerable:!0})},Se=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of ke(e))!Oe.call(t,n)&&n!==r&&Q(t,n,{get:()=>e[n],enumerable:!(s=xe(e,n))||s.enumerable});return t};var Te=t=>Se(Q({},"__esModule",{value:!0}),t);var Et={};je(Et,{MemoryFS:()=>_,createKradAdd:()=>ye,createManuals:()=>Z,createShell:()=>be,mountShell:()=>$e,profiles:()=>D});var Ae=new TextEncoder,se=/^[A-Za-z_][A-Za-z0-9_]*$/,V=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s,f=(t="",e=0,r="")=>({code:e,stdout:t,stderr:r}),g=(t,e,r=1)=>f("",r,`${t}: ${e} +`),U=(t,e)=>(t.length?t:["-"]).map(r=>({path:r,text:r==="-"?e.stdin:e.fs.read(r,e.cwd)})),Ne=(t,e)=>f(U(t,e).map(({text:r})=>r).join("")),ze=(t,e)=>{if(t.length>1)return g("cd","too many arguments");let r=t[0]==="-"?e.env.OLDPWD:t[0]??e.env.HOME??"/";return r?(e.chdir(r),f(t[0]==="-"?`${e.cwd} +`:"")):g("cd","OLDPWD not set")},Le=t=>{let e=t[0]!=="-n";return e||(t=t.slice(1)),f(`${t.join(" ")}${e?` +`:""}`)},oe=async(t,e)=>{let r={...e.env};for(;t[0]?.match(V);){let[,s,n]=t.shift().match(V);r[s]=n}return t.length?e.invoke(t[0],t.slice(1),{env:r,stdin:e.stdin}):f(Object.keys(r).sort().map(s=>`${s}=${r[s]} +`).join(""))},We=(t,e)=>{if(!t.length)return oe(t,e);let r="",s=0;for(let n of t)Object.hasOwn(e.env,n)?r+=`${e.env[n]} +`:s=1;return f(r,s)},_e=(t,e)=>{if(!t.length)return f(Object.keys(e.env).sort().map(r=>`export ${r}=${JSON.stringify(e.env[r])} +`).join(""));for(let r of t){let s=r.match(V);if(s)e.setenv(s[1],s[2]);else if(se.test(r))e.setenv(r,e.env[r]??"");else return g("export",`${r}: invalid name`,2)}return f()},De=(t,e)=>{for(let r of t){if(!se.test(r))return g("unset",`${r}: invalid name`,2);e.unsetenv(r)}return f()},Re=(t,e)=>{let r=!1,s=!1,n=!1,o=!1;for(;/^-[ivnq]+$/.test(t[0]??"");)for(let d of t.shift().slice(1))d==="i"&&(r=!0),d==="v"&&(s=!0),d==="n"&&(n=!0),d==="q"&&(o=!0);if(!t.length)return g("grep","missing pattern",2);let i=t.shift();r&&(i=i.toLocaleLowerCase());let c="",u=!1,a=U(t,e);if(!r&&!n&&!o&&a.length===1){let d=e.wasm?.filter(a[0].text,i,s);if(d!=null)return f(d,d?0:1)}for(let{path:d,text:m}of a){let l=m.split(` +`);l.at(-1)===""&&l.pop(),l.forEach(($,y)=>{(r?$.toLocaleLowerCase():$).includes(i)!==s&&(u=!0,o||(c+=`${a.length>1?`${d}:`:""}${n?`${y+1}:`:""}${$} +`))})}return f(c,u?0:1)},Me=(t,e)=>{let r=10;if(t[0]==="-n"?(r=Number(t[1]),t=t.slice(2)):/^-\d+$/.test(t[0]??"")&&(r=Number(t.shift().slice(1))),!Number.isSafeInteger(r)||r<0)return g("head","invalid line count",2);let s=U(t,e);return f(s.map(({path:n,text:o})=>{let i=r?(o.match(/[^\n]*\n|[^\n]+$/g)??[]).slice(0,r).join(""):"";return`${s.length>1?`==> ${n} <== `:""}${i}`}).join(s.length>1?` -`:""))},Re=(t,e)=>t[0]==="-c"?(e.clearHistory(),f()):t.length?y("history","usage: history [-c]",2):f(e.history.map((n,s)=>`${String(s+1).padStart(5)} ${n} -`).join("")),Me=(t,e)=>{let n=!1,s=!1;for(;/^-[al]+$/.test(t[0]??"");)for(let o of t.shift().slice(1))o==="a"&&(n=!0),o==="l"&&(s=!0);t=t.length?t:["."];let r="";return t.forEach((o,i)=>{let c=e.fs.stat(o,e.cwd),m=c.type==="directory"?e.fs.list(o,e.cwd):[{...c,name:e.fs.basename(c.path)}];n||(m=m.filter(({name:a})=>!a.startsWith("."))),t.length>1&&(r+=`${i?` +`:""))},Ie=(t,e)=>t[0]==="-c"?(e.clearHistory(),f()):t.length?g("history","usage: history [-c]",2):f(e.history.map((r,s)=>`${String(s+1).padStart(5)} ${r} +`).join("")),Pe=(t,e)=>{let r=!1,s=!1;for(;/^-[al]+$/.test(t[0]??"");)for(let o of t.shift().slice(1))o==="a"&&(r=!0),o==="l"&&(s=!0);t=t.length?t:["."];let n="";return t.forEach((o,i)=>{let c=e.fs.stat(o,e.cwd),u=c.type==="directory"?e.fs.list(o,e.cwd):[{...c,name:e.fs.basename(c.path)}];r||(u=u.filter(({name:a})=>!a.startsWith("."))),t.length>1&&(n+=`${i?` `:""}${o}: -`),n&&c.type==="directory"&&m.unshift({name:".",type:"directory",size:0},{name:"..",type:"directory",size:0}),r+=s?m.map(a=>`${a.type==="directory"?"d":"-"}rw------- ${String(a.size).padStart(8)} ${a.name} -`).join(""):`${m.map(({name:a})=>a).join(" ")}${m.length?` -`:""}`}),f(r)},Pe=(t,e)=>{let n=t[0]==="-p";if(n&&(t=t.slice(1)),!t.length)return y("mkdir","missing operand",2);for(let s of t)e.fs.mkdir(s,{cwd:e.cwd,parents:n});return f()},Ce=t=>{if(!t.length)return y("printf","missing format",2);let e=t.shift().replace(/\\([nrt\\])/g,(i,c)=>({n:` -`,r:"\r",t:" ","\\":"\\"})[c]),n=0,s=!1,r=()=>e.replace(/%([%sdi])/g,(i,c)=>{if(c==="%")return"%";let m=t[n++]??"";if(c==="s")return m;let a=Number.parseInt(m,10);return Number.isNaN(a)&&(s=!0),Number.isNaN(a)?"0":String(a)}),o=r();for(;nt.length?y("pwd","too many arguments",2):f(`${e.cwd} -`),Be=(t,e)=>{let n=!1,s=!1;for(;/^-[fRr]+$/.test(t[0]??"");)for(let r of t.shift().slice(1))r==="f"&&(n=!0),(r==="r"||r==="R")&&(s=!0);if(!t.length)return n?f():y("rm","missing operand",2);for(let r of t)try{if(!s&&e.fs.stat(r,e.cwd).type==="directory")return y("rm",`${r}: is a directory`);e.fs.remove(r,{cwd:e.cwd,recursive:s})}catch(o){if(!n||o.code!=="ENOENT")throw o}return f()},Ue=(t,e)=>{if(!t.length)return y("rmdir","missing operand",2);for(let n of t){if(e.fs.stat(n,e.cwd).type!=="directory")return y("rmdir",`${n}: Not a directory`);e.fs.remove(n,{cwd:e.cwd})}return f()},qe=(t,e)=>{if(!t.length)return y("touch","missing operand",2);for(let n of t)e.fs.touch(n,e.cwd);return f()},Ze=async(t,e)=>{if(!e.manuals)return y("man","manual provider unavailable");let n="";if(/^[1-9][A-Za-z0-9]*$/.test(t[0]??"")&&(n=t.shift()),!t.length)return y("man","what manual page do you want?",2);let s=[],r=[];for(let o of t){let i=await e.manuals.read(o,n,e.signal);i?s.push(i):r.push(`No manual entry for ${o}${n?` in section ${n}`:""} +`),r&&c.type==="directory"&&u.unshift({name:".",type:"directory",size:0},{name:"..",type:"directory",size:0}),n+=s?u.map(a=>`${a.type==="directory"?"d":"-"}rw------- ${String(a.size).padStart(8)} ${a.name} +`).join(""):`${u.map(({name:a})=>a).join(" ")}${u.length?` +`:""}`}),f(n)},Ce=(t,e)=>{let r=t[0]==="-p";if(r&&(t=t.slice(1)),!t.length)return g("mkdir","missing operand",2);for(let s of t)e.fs.mkdir(s,{cwd:e.cwd,parents:r});return f()},Ue=t=>{if(!t.length)return g("printf","missing format",2);let e=t.shift().replace(/\\([nrt\\])/g,(i,c)=>({n:` +`,r:"\r",t:" ","\\":"\\"})[c]),r=0,s=!1,n=()=>e.replace(/%([%sdi])/g,(i,c)=>{if(c==="%")return"%";let u=t[r++]??"";if(c==="s")return u;let a=Number.parseInt(u,10);return Number.isNaN(a)&&(s=!0),Number.isNaN(a)?"0":String(a)}),o=n();for(;rt.length?g("pwd","too many arguments",2):f(`${e.cwd} +`),Be=(t,e)=>{let r=!1,s=!1;for(;/^-[fRr]+$/.test(t[0]??"");)for(let n of t.shift().slice(1))n==="f"&&(r=!0),(n==="r"||n==="R")&&(s=!0);if(!t.length)return r?f():g("rm","missing operand",2);for(let n of t)try{if(!s&&e.fs.stat(n,e.cwd).type==="directory")return g("rm",`${n}: is a directory`);e.fs.remove(n,{cwd:e.cwd,recursive:s})}catch(o){if(!r||o.code!=="ENOENT")throw o}return f()},Ze=(t,e)=>{if(!t.length)return g("rmdir","missing operand",2);for(let r of t){if(e.fs.stat(r,e.cwd).type!=="directory")return g("rmdir",`${r}: Not a directory`);e.fs.remove(r,{cwd:e.cwd})}return f()},Fe=(t,e)=>{if(!t.length)return g("touch","missing operand",2);for(let r of t)e.fs.touch(r,e.cwd);return f()},He=async(t,e)=>{if(!e.manuals)return g("man","manual provider unavailable");let r="";if(/^[1-9][A-Za-z0-9]*$/.test(t[0]??"")&&(r=t.shift()),!t.length)return g("man","what manual page do you want?",2);let s=[],n=[];for(let o of t){let i=await e.manuals.read(o,r,e.signal);i?s.push(i):n.push(`No manual entry for ${o}${r?` in section ${r}`:""} `)}return f(s.join(` -`),r.length?1:0,r.join(""))},ne=async(t,e)=>{if(!e.manuals)return y("apropos","manual provider unavailable");if(!t.length)return y("apropos","missing keyword",2);let n=await e.manuals.search(t.join(" "),e.signal);return f(n.length?n.map(({name:s,section:r,description:o=""})=>`${s}(${r})${o?` - ${o}`:""} +`),n.length?1:0,n.join(""))},re=async(t,e)=>{if(!e.manuals)return g("apropos","manual provider unavailable");if(!t.length)return g("apropos","missing keyword",2);let r=await e.manuals.search(t.join(" "),e.signal);return f(r.length?r.map(({name:s,section:n,description:o=""})=>`${s}(${n})${o?` - ${o}`:""} `).join(""):`${t.join(" ")}: nothing appropriate -`,n.length?0:1)},re=(t,e)=>{let n=e.kernel.sysctls;if(!t.length||t[0]==="-a")return f(Object.keys(n).sort().map(r=>`${r}: ${n[r]} -`).join(""));let s="";for(let r of t){let o=r.indexOf("="),i=o<0?r:r.slice(0,o);if(!Object.hasOwn(n,i))return y("sysctl",`unknown oid '${i}'`);o>=0&&(n[i]=r.slice(o+1)),s+=`${i}: ${n[i]} -`}return f(s)},K=t=>(t=t.split("/").pop(),/^[A-Za-z0-9_.-]+$/.test(t??"")?t==="kernel"||t.endsWith(".ko")?t:`${t}.ko`:""),Fe=(t,e)=>t.length?y("kldstat","usage: kldstat"):f(`${["Id Refs Address Size Name",...[...e.kernel.modules].map((n,s)=>`${String(s+1).padStart(2)} 1 0xffffffff${(2164260864+s*1048576).toString(16)} 1000 ${n}`)].join(` +`,r.length?0:1)},ne=(t,e)=>{let r=e.kernel.sysctls;if(!t.length||t[0]==="-a")return f(Object.keys(r).sort().map(n=>`${n}: ${r[n]} +`).join(""));let s="";for(let n of t){let o=n.indexOf("="),i=o<0?n:n.slice(0,o);if(!Object.hasOwn(r,i))return g("sysctl",`unknown oid '${i}'`);o>=0&&(r[i]=n.slice(o+1)),s+=`${i}: ${r[i]} +`}return f(s)},Y=t=>(t=t.split("/").pop(),/^[A-Za-z0-9_.-]+$/.test(t??"")?t==="kernel"||t.endsWith(".ko")?t:`${t}.ko`:""),Ke=(t,e)=>t.length?g("kldstat","usage: kldstat"):f(`${["Id Refs Address Size Name",...[...e.kernel.modules].map((r,s)=>`${String(s+1).padStart(2)} 1 0xffffffff${(2164260864+s*1048576).toString(16)} 1000 ${r}`)].join(` `)} -`),He=(t,e)=>{if(!t.length)return y("kldload","missing module",2);for(let n of t){let s=K(n);if(!s)return y("kldload",`${n}: invalid module`,2);e.kernel.modules.add(s)}return f()},Qe=(t,e)=>{if(!t.length)return y("kldunload","missing module",2);for(let n of t){let s=K(n);if(!s)return y("kldunload",`${n}: invalid module`,2);if(s==="kernel")return y("kldunload","can't unload kernel");if(!e.kernel.modules.delete(s))return y("kldunload",`${s}: module not loaded`)}return f()},Ve=(t,e)=>t.length?y("lsmod","usage: lsmod"):f(`${["Module Size Used by",...[...e.kernel.modules].map(n=>`${n.replace(/\.ko$/,"").padEnd(20)} 4096 0`)].join(` +`),Ge=(t,e)=>{if(!t.length)return g("kldload","missing module",2);for(let r of t){let s=Y(r);if(!s)return g("kldload",`${r}: invalid module`,2);e.kernel.modules.add(s)}return f()},Qe=(t,e)=>{if(!t.length)return g("kldunload","missing module",2);for(let r of t){let s=Y(r);if(!s)return g("kldunload",`${r}: invalid module`,2);if(s==="kernel")return g("kldunload","can't unload kernel");if(!e.kernel.modules.delete(s))return g("kldunload",`${s}: module not loaded`)}return f()},Ve=(t,e)=>t.length?g("lsmod","usage: lsmod"):f(`${["Module Size Used by",...[...e.kernel.modules].map(r=>`${r.replace(/\.ko$/,"").padEnd(20)} 4096 0`)].join(` `)} -`),Ye=(t,e)=>{let n=t[0]==="-r";if(n&&t.shift(),!t.length)return y("modprobe","missing module",2);for(let s of t){let r=K(s);if(!r)return y("modprobe",`${s}: invalid module`,2);n?e.kernel.modules.delete(r):e.kernel.modules.add(r)}return f()},Je=(t,{profile:e})=>{let n={s:e.sysname,n:e.hostname,r:e.release,v:`${e.sysname} ${e.release}`,m:e.machine};if(!t.length)return f(`${n.s} -`);let s=t.join("").replaceAll("-","");return s.includes("a")?f(`${n.s} ${n.n} ${n.r} ${n.v} ${n.m} -`):[...s].some(r=>!(r in n))?y("uname","invalid option",2):f(`${[...s].map(r=>n[r]).join(" ")} -`)},Ke=(t,e)=>{let n="";for(;/^-[lwc]+$/.test(t[0]??"");)n+=t.shift().slice(1);n=[...new Set(n||"lwc")].join("");let s=B(t,e),r={l:0,w:0,c:0},o=s.map(({path:i,text:c})=>{let m={l:(c.match(/\n/g)??[]).length,w:(c.match(/\S+/g)??[]).length,c:Se.encode(c).length};for(let a of n)r[a]+=m[a];return`${[...n].map(a=>String(m[a]).padStart(7)).join("")}${s.length>1||i!=="-"?` ${i}`:""} -`});return s.length>1&&o.push(`${[...n].map(i=>String(r[i]).padStart(7)).join("")} total -`),f(o.join(""))},Xe=(t,e)=>{if(!t.length)return y("which","missing command",2);let n=t.filter(e.hasCommand);return f(n.map(s=>`/bin/${s} -`).join(""),n.length===t.length?0:1)},ie=t=>{let e={messages:[...t.messages],modules:new Set(t.modules),sysctls:Object.assign(Object.create(null),t.sysctls)},n={apropos:ne,cat:Te,cd:ze,clear:()=>f(),date:()=>f(`${new Date().toString()} +`),Ye=(t,e)=>{let r=t[0]==="-r";if(r&&t.shift(),!t.length)return g("modprobe","missing module",2);for(let s of t){let n=Y(s);if(!n)return g("modprobe",`${s}: invalid module`,2);r?e.kernel.modules.delete(n):e.kernel.modules.add(n)}return f()},Je=(t,{profile:e})=>{let r={s:e.sysname,n:e.hostname,r:e.release,v:`${e.sysname} ${e.release}`,m:e.machine};if(!t.length)return f(`${r.s} +`);let s=t.join("").replaceAll("-","");return s.includes("a")?f(`${r.s} ${r.n} ${r.r} ${r.v} ${r.m} +`):[...s].some(n=>!(n in r))?g("uname","invalid option",2):f(`${[...s].map(n=>r[n]).join(" ")} +`)},Xe=(t,e)=>{let r="";for(;/^-[lwc]+$/.test(t[0]??"");)r+=t.shift().slice(1);r=[...new Set(r||"lwc")].join("");let s=U(t,e),n={l:0,w:0,c:0},o=s.map(({path:i,text:c})=>{let u={l:(c.match(/\n/g)??[]).length,w:(c.match(/\S+/g)??[]).length,c:Ae.encode(c).length};for(let a of r)n[a]+=u[a];return`${[...r].map(a=>String(u[a]).padStart(7)).join("")}${s.length>1||i!=="-"?` ${i}`:""} +`});return s.length>1&&o.push(`${[...r].map(i=>String(n[i]).padStart(7)).join("")} total +`),f(o.join(""))},et=(t,e)=>{if(!t.length)return g("which","missing command",2);let r=t.filter(e.hasCommand);return f(r.map(s=>`/bin/${s} +`).join(""),r.length===t.length?0:1)},ie=t=>{let e={messages:[...t.messages],modules:new Set(t.modules),sysctls:Object.assign(Object.create(null),t.sysctls)},r={apropos:re,cat:Ne,cd:ze,clear:()=>f(),date:()=>f(`${new Date().toString()} `),dmesg:()=>f(`${e.messages.join(` `)} -`),echo:Ae,env:oe,export:Le,false:()=>f("",1),grep:We,head:De,help:(s,r)=>f(`${r.commands().join(" ")} -`),history:Re,hostname:()=>f(`${t.hostname} +`),echo:Le,env:oe,export:_e,false:()=>f("",1),grep:Re,head:Me,help:(s,n)=>f(`${n.commands().join(" ")} +`),history:Ie,hostname:()=>f(`${t.hostname} `),id:()=>f(`uid=${t.uid}(${t.user}) gid=${t.gid}(${t.group}) -`),ls:Me,man:Ze,mkdir:Pe,printenv:Ne,printf:Ce,pwd:Ie,rm:Be,rmdir:Ue,touch:qe,true:()=>f(),uname:Je,unset:_e,wc:Ke,whatis:ne,which:Xe,whoami:()=>f(`${t.user} -`)};return t.name==="freebsd"&&Object.assign(n,{"freebsd-version":()=>f(`${t.release} -`),kldload:He,kldstat:Fe,kldunload:Qe,sysctl:re}),t.name==="linux"&&Object.assign(n,{"linux-version":()=>f(`${t.release} -`),lsmod:Ve,modprobe:Ye,sysctl:re}),{commands:n,kernel:e}};var Ge=new TextEncoder,M=t=>Ge.encode(t).length,z=(t,e,n="EINVAL")=>{let s=new Error(`${t}: ${e}`);throw s.code=n,s},et=t=>({maxFileBytes:t.maxFileBytes??1048576,maxFiles:t.maxFiles??2048,maxTotalBytes:t.maxTotalBytes??8388608}),W=class{#t=new Set(["/"]);#e=new Map;#n=new Map([["/",Date.now()]]);#r;#s=0;constructor(e={},n={}){this.#r=et(n);let s=e instanceof Map?e:Object.entries(e);for(let[r,o]of s)o===null||r.endsWith("/")?this.mkdir(r,{parents:!0}):(this.mkdir(this.parent(this.resolve("/",r)),{parents:!0}),this.write(r,String(o)))}resolve(e="/",n="."){(typeof e!="string"||typeof n!="string"||n.includes("\0"))&&z(String(n),"invalid path");let s=n.startsWith("/")?[]:e.split("/").filter(Boolean);for(let r of n.split("/"))!r||r==="."||(r===".."?(s.length||z(n,"path escapes root","EACCES"),s.pop()):s.push(r));return`/${s.join("/")}`}parent(e){let n=e.split("/");return n.pop(),n.join("/")||"/"}basename(e){return e==="/"?"/":e.slice(e.lastIndexOf("/")+1)}exists(e,n="/"){return e=this.resolve(n,e),this.#t.has(e)||this.#e.has(e)}stat(e,n="/"){if(e=this.resolve(n,e),this.#t.has(e))return{path:e,type:"directory",size:0,mtime:this.#n.get(e)};if(this.#e.has(e))return{path:e,type:"file",size:M(this.#e.get(e)),mtime:this.#n.get(e)};z(e,"No such file or directory","ENOENT")}read(e,n="/"){return e=this.resolve(n,e),this.#t.has(e)&&z(e,"Is a directory","EISDIR"),this.#e.has(e)||z(e,"No such file or directory","ENOENT"),this.#e.get(e)}write(e,n,s="/"){e=this.resolve(s,e),n=String(n);let r=M(n);r>this.#r.maxFileBytes&&z(e,"file quota exceeded","EDQUOT"),this.#t.has(e)&&z(e,"Is a directory","EISDIR"),this.#t.has(this.parent(e))||z(e,"No such directory","ENOENT");let o=this.#e.has(e)?M(this.#e.get(e)):0;return this.#e.has(e)||this.#o(1),this.#s-o+r>this.#r.maxTotalBytes&&z(e,"filesystem quota exceeded","EDQUOT"),this.#e.set(e,n),this.#n.set(e,Date.now()),this.#s+=r-o,e}append(e,n,s="/"){return e=this.resolve(s,e),this.write(e,(this.#e.get(e)??"")+n)}mkdir(e,{cwd:n="/",parents:s=!1}={}){if(e=this.resolve(n,e),this.#e.has(e)&&z(e,"Not a directory","ENOTDIR"),this.#t.has(e)){if(s)return e;z(e,"File exists","EEXIST")}let r=[];for(let o=e;!this.#t.has(o)&&(r.push(o),o!=="/");o=this.parent(o));!s&&r.length>1&&z(e,"No such directory","ENOENT"),this.#o(r.length);for(let o of r.reverse())this.#t.add(o),this.#n.set(o,Date.now());return e}list(e=".",n="/"){e=this.resolve(n,e);let s=this.stat(e);if(s.type==="file")return[{...s,name:this.basename(e)}];let r=e==="/"?"/":`${e}/`,o=new Map;for(let i of[...this.#t,...this.#e.keys()]){if(i===e||!i.startsWith(r))continue;let c=i.slice(r.length).split("/")[0],m=`${r}${c}`;o.has(c)||o.set(c,{...this.stat(m),name:c})}return[...o.values()].sort((i,c)=>i.name.localeCompare(c.name))}remove(e,{cwd:n="/",recursive:s=!1}={}){if(n=this.resolve("/",n),e=this.resolve(n,e),e==="/"&&z(e,"cannot remove root","EPERM"),(e===n||n.startsWith(`${e}/`))&&z(e,"Device busy","EBUSY"),this.#e.has(e)){this.#s-=M(this.#e.get(e)),this.#e.delete(e),this.#n.delete(e);return}this.#t.has(e)||z(e,"No such file or directory","ENOENT");let r=`${e}/`;[...this.#t,...this.#e.keys()].filter(i=>i.startsWith(r)).length&&!s&&z(e,"Directory not empty","ENOTEMPTY");for(let i of[...this.#e.keys()])i.startsWith(r)&&(this.#s-=M(this.#e.get(i)),this.#e.delete(i),this.#n.delete(i));for(let i of[...this.#t])(i===e||i.startsWith(r))&&(this.#t.delete(i),this.#n.delete(i))}touch(e,n="/"){return e=this.resolve(n,e),this.exists(e)?(this.#n.set(e,Date.now()),e):this.write(e,"")}complete(e,n){let s=n.lastIndexOf("/"),r=s<0?".":n.slice(0,s)||"/",o=s<0?n:n.slice(s+1),i=s<0?"":n.slice(0,s+1);try{return this.list(r,e).filter(({name:c})=>c.startsWith(o)).map(({name:c,type:m})=>`${i}${c}${m==="directory"?"/":""}`)}catch{return[]}}#o(e){this.#t.size+this.#e.size-1+e>this.#r.maxFiles&&z("/","file count quota exceeded","EDQUOT")}},ae=(t,e)=>new W(t,e);var U=/^[a-z0-9_][a-z0-9_.+-]*$/,X=/^[1-9][A-Za-z0-9]*$/,tt=/^[a-f0-9]{64}$/,q=t=>Array.isArray(t)?t:t?[t]:[],G=t=>{if(typeof t!="string"||t.startsWith("/")||t.split("/").some(e=>!/^[A-Za-z0-9._+-]+$/.test(e)||e==="."||e===".."))throw new TypeError("invalid manual path");return t},nt=async(t,e)=>{let n=Number(t.headers.get("content-length")||0);if(Number.isFinite(n)&&n>e)throw new RangeError("manual response is too large");if(!t.body)return"";let s=t.body.getReader(),r=new TextDecoder,o=[],i=0;try{for(;;){let{done:c,value:m}=await s.read();if(c)break;if(i+=m.byteLength,i>e)throw await s.cancel().catch(()=>{}),new RangeError("manual response is too large");o.push(r.decode(m,{stream:!0}))}return o.push(r.decode()),o.join("")}finally{s.releaseLock()}},ce=async(t,e,n,s=!1,r)=>{let o=await t(e,{signal:r});if(!o.ok)throw new Error(`manual request failed: ${o.status}`);let i=await nt(o,n);return s?JSON.parse(i):i},rt=t=>{if(!t||typeof t!="object"||!t.pages||typeof t.pages!="object")throw new TypeError("invalid manual index");let e=Object.create(null);for(let[s,r]of Object.entries(t.pages)){if(!U.test(s))throw new TypeError("invalid manual index");let o=Array.isArray(r)?r:[r];if(!o.length||o.some(c=>!c||typeof c!="object"||!X.test(c.section)||typeof c.path!="string"||!tt.test(c.sha256)||c.description!=null&&typeof c.description!="string"))throw new TypeError("invalid manual index");if(new Set(o.map(c=>c.section)).size!==o.length)throw new TypeError("invalid manual index");let i=Object.freeze(o.map(c=>Object.freeze({...c,path:G(c.path)})));e[s]=Array.isArray(r)?i:i[0]}let n=Object.create(null);for(let[s,r]of Object.entries(t.aliases??{})){let o=r&&Object.hasOwn(e,r.name)?e[r.name]:null;if(!U.test(s)||Object.hasOwn(e,s)||!U.test(r?.name??"")||!X.test(r?.section??"")||!q(o).some(i=>i.section===r.section))throw new TypeError("invalid manual index");n[s]=Object.freeze({name:r.name,section:r.section})}return Object.freeze({...t,aliases:Object.freeze(n),pages:Object.freeze(e)})},Z=({base:t="./manuals/",profile:e="freebsd",fetch:n=globalThis.fetch,crypto:s=globalThis.crypto,maxPage:r=1048576}={})=>{if(typeof n!="function")throw new TypeError("manuals require fetch");if(!s?.subtle)throw new TypeError("manuals require Web Crypto");if(!Number.isSafeInteger(r)||r<1)throw new TypeError("maxPage must be a positive integer");let o=new URL(`${G(e)}/`,new URL(t,globalThis.location?.href??"http://localhost/")),i,c=async d=>(i||(i=ce(n,new URL("index.json",o),r,!0,d).then(rt).catch(l=>{throw i=null,l})),i),m=async(d,l,v)=>{if(d=d.toLowerCase(),!U.test(d)||l&&!X.test(l))return null;let b=await c(v);if(Object.hasOwn(b.aliases,d)){let O=b.aliases[d];if(l&&l!==O.section)return null;d=O.name,l=O.section}let x=Object.hasOwn(b.pages,d)?b.pages[d]:null;return q(x).find(O=>!l||O.section===l)??null};return Object.freeze({index:c,find:m,read:async(d,l,v)=>{let b=await m(d,l,v);if(!b)return null;let x=await ce(n,new URL(G(b.path),o),r,!1,v),O=new Uint8Array(await s.subtle.digest("SHA-256",new TextEncoder().encode(x))),h="";for(let A of O)h+=A.toString(16).padStart(2,"0");if(h!==b.sha256)throw new Error("manual integrity check failed");return x},search:async(d,l)=>{if(d=d.trim().toLowerCase(),!d)return[];let v=await c(l),b=Object.entries(v.aliases).map(([x,O])=>[x,q(v.pages[O.name]).find(h=>h.section===O.section)]);return[...Object.entries(v.pages),...b].flatMap(([x,O])=>q(O).filter(h=>`${x} ${h.description??""}`.toLowerCase().includes(d)).map(h=>({name:x,...h})))}})};var st=/^[A-Za-z_][A-Za-z0-9_]*$/,ot=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s,it=new Set([";","&&","||","|","<",">",">>"]),ue=new Set(["<",">",">>","0<","1>","1>>","2>","2>>"]),at=(t,e,n)=>{let s=t[e+1];if(s==="?")return[String(n["?"]??0),e+2];if(s==="{"){let o=t.indexOf("}",e+2);if(o<0)throw new SyntaxError("missing } in variable expansion");let i=t.slice(e+2,o);if(!st.test(i))throw new SyntaxError(`invalid variable name: ${i}`);return[String(n[i]??""),o+1]}let r=t.slice(e+1).match(/^[A-Za-z_][A-Za-z0-9_]*/);return r?[String(n[r[0]]??""),e+r[0].length+1]:["$",e+1]},P=(t,e,n)=>{e&&(t.at(-1)?.expandable===n?t.at(-1).text+=e:t.push({text:e,expandable:n}))},ct=t=>t.parts.map(({text:e})=>e).join(""),F=t=>{let e=t.length;for(;e&&t[e-1].trim()&&!";|&".includes(t[e-1]);)e--;return e},H=(t,e={})=>t.parts.map(({text:n,expandable:s})=>{if(!s)return n;let r="";for(let o=0;o{if(typeof t!="string")throw new TypeError("command must be a string");if(t.length>(n.maxSource??16384))throw new RangeError("command is too long");let s=[],r=[],o=!1,i="",c=()=>{o&&(s.push({type:"word",parts:r}),r=[],o=!1)},m=a=>{c(),!(a===";"&&(!s.length||s.at(-1).value===";"))&&s.push({type:"operator",value:a})};for(let a=0;a=t.length)throw new SyntaxError("trailing backslash");if(i==='"'&&!["$",'"',"\\",` -`].includes(t[a+1])){P(r,u,!0),o=!0,a++;continue}a++,t[a]!==` -`&&(P(r,t[a],!1),o=!0),a++;continue}if(u==="$"){P(r,u,!0),o=!0,a++;continue}if(!i&&u==="#"&&!o){for(;a"].includes(t[a+1])){let v=t.slice(a+1,a+3)===">>"?3:2,b=t.slice(a,a+v);if(!ue.has(b))throw new SyntaxError(`unsupported redirect: ${b}`);m(b),a+=v;continue}let d=t.slice(a,a+2),l=["&&","||",">>"].includes(d)?d:it.has(u)?u:"";if(l){m(l),a+=l.length;continue}if(u==="&")throw new SyntaxError("unsupported operator: &")}P(r,u,!0),o=!0,a++}if(i)throw new SyntaxError(`unterminated ${i} quote`);if(c(),s.at(-1)?.value===";"&&s.pop(),s.length>(n.maxTokens??1024))throw new RangeError("too many tokens");return s},le=(t,e,n)=>{let s=[],r=[];for(;e.i(n.maxArgs??128))throw new RangeError("too many arguments");let o=[];for(;s.length;){let i=ct(s[0]).match(ot);if(!i)break;o.push([i[1],s.shift()])}if(!s.length&&!o.length&&!r.length)throw new SyntaxError("empty command");return{argv:s,assignments:o,redirects:r}},fe=(t,e={},n={})=>{let s=lt(t,e,n),r={i:0},o=[],i=null,c=0;for(;r.i=s.length)throw new SyntaxError("missing command after |");m.push(le(s,r,n)),c++}o.push({link:i,pipeline:m});let a=s[r.i++];if(!a)break;if(![";","&&","||"].includes(a.value))throw new SyntaxError(`unexpected operator: ${a.value}`);if(i=a.value,r.i>=s.length&&i!==";")throw new SyntaxError(`missing command after ${i}`);for(;s[r.i]?.value===";";)r.i++}if(c>(n.maxCommands??128))throw new RangeError("too many commands");if(o.length>(n.maxPipelines??64))throw new RangeError("too many pipelines");return o};var de=t=>Object.freeze({...t,messages:Object.freeze([...t.messages??[]]),modules:Object.freeze([...t.modules??[]]),sysctls:Object.freeze({...t.sysctls??{}})}),ee=(t,e,n,s,r={})=>de({name:t,sysname:e,release:n,machine:s,hostname:"shell",user:"guest",group:"guest",uid:1e3,gid:1e3,home:"/home/guest",shell:"/bin/sh",path:"/bin:/usr/bin",messages:[],modules:[],sysctls:{},...r}),D=Object.freeze({posix:ee("posix","POSIX","1","wasm32"),freebsd:ee("freebsd","FreeBSD","14.2-RELEASE","amd64",{hostname:"recv",user:"rad",group:"wheel",uid:0,gid:0,home:"/home/rad",ostype:"freebsd14.2",modules:["kernel","zfs.ko"],messages:["FreeBSD 14.2-RELEASE","Trying to mount root from ufs:/dev/ada0p2"],sysctls:{"kern.hostname":"recv","kern.osrelease":"14.2-RELEASE","kern.ostype":"FreeBSD"}}),linux:ee("linux","Linux","6.12.0-web","wasm32",{ostype:"linux-gnu",modules:["loop","overlay"],messages:["Linux version 6.12.0-web","VFS: Mounted root (tmpfs filesystem) readonly"],sysctls:{"kernel.hostname":"shell","kernel.osrelease":"6.12.0-web","kernel.ostype":"Linux"}})}),me=(t="posix")=>{if(typeof t=="string"){if(!D[t])throw new TypeError(`unknown profile: ${t}`);return D[t]}if(!t||typeof t!="object")throw new TypeError("profile must be a name or object");return de({...D.posix,...t})};var he=new TextEncoder,ut=new TextDecoder,ft=typeof document>"u"?globalThis.location?.href:document.currentScript?.src??globalThis.location.href,dt=()=>{let t=ft||globalThis.location?.href;if(!t)throw new TypeError("WebAssembly URL is required outside a browser");return new URL("../wasm/shell.wasm",t)},mt=async(t,e)=>{let n=await e(t);if(!n.ok)throw new Error(`WebAssembly request failed: ${n.status}`);let r=(n.headers.get("content-type")?.includes("application/wasm")?await WebAssembly.instantiateStreaming(n,{}):await WebAssembly.instantiate(await n.arrayBuffer(),{})).instance.exports;if(r.abi?.()!==1||!(r.memory instanceof WebAssembly.Memory))throw new Error("unsupported shell.js WebAssembly ABI");return r},pe=(t,e={})=>{if(!t)return null;typeof t=="object"&&!(t instanceof URL)&&(e=t);let{fetch:n=globalThis.fetch,threshold:s=262144}=e,r=t===!0||t==="auto"?dt():e.url??t;if(typeof n!="function")throw new TypeError("WebAssembly requires fetch");let o,i,c=!1,m=()=>i?Promise.resolve(!0):c?Promise.resolve(!1):(o||(o=mt(r,n).then(u=>(i=u,!0)).catch(()=>(c=!0,!1))),o);return Object.freeze({filter:(u,d,l=!1)=>{if(u.lengthi.capacity())return null;let O=new Uint8Array(i.memory.buffer);O.set(v,x),O.set(b,x+v.length);let h=ut.decode(O.subarray(x,x+i.filter_lines(v.length,v.length,b.length,l)));return h&&!h.endsWith(` +`),ls:Pe,man:He,mkdir:Ce,printenv:We,printf:Ue,pwd:qe,rm:Be,rmdir:Ze,touch:Fe,true:()=>f(),uname:Je,unset:De,wc:Xe,whatis:re,which:et,whoami:()=>f(`${t.user} +`)};return t.name==="freebsd"&&Object.assign(r,{"freebsd-version":()=>f(`${t.release} +`),kldload:Ge,kldstat:Ke,kldunload:Qe,sysctl:ne}),t.name==="linux"&&Object.assign(r,{"linux-version":()=>f(`${t.release} +`),lsmod:Ve,modprobe:Ye,sysctl:ne}),{commands:r,kernel:e}};var tt=new TextEncoder,M=t=>tt.encode(t).length,A=(t,e,r="EINVAL")=>{let s=new Error(`${t}: ${e}`);throw s.code=r,s},rt=t=>({maxFileBytes:t.maxFileBytes??1048576,maxFiles:t.maxFiles??2048,maxTotalBytes:t.maxTotalBytes??8388608}),_=class{#t=new Set(["/"]);#e=new Map;#r=new Map([["/",Date.now()]]);#n;#s=0;constructor(e={},r={}){this.#n=rt(r);let s=e instanceof Map?e:Object.entries(e);for(let[n,o]of s)o===null||n.endsWith("/")?this.mkdir(n,{parents:!0}):(this.mkdir(this.parent(this.resolve("/",n)),{parents:!0}),this.write(n,String(o)))}resolve(e="/",r="."){(typeof e!="string"||typeof r!="string"||r.includes("\0"))&&A(String(r),"invalid path");let s=r.startsWith("/")?[]:e.split("/").filter(Boolean);for(let n of r.split("/"))!n||n==="."||(n===".."?(s.length||A(r,"path escapes root","EACCES"),s.pop()):s.push(n));return`/${s.join("/")}`}parent(e){let r=e.split("/");return r.pop(),r.join("/")||"/"}basename(e){return e==="/"?"/":e.slice(e.lastIndexOf("/")+1)}exists(e,r="/"){return e=this.resolve(r,e),this.#t.has(e)||this.#e.has(e)}stat(e,r="/"){if(e=this.resolve(r,e),this.#t.has(e))return{path:e,type:"directory",size:0,mtime:this.#r.get(e)};if(this.#e.has(e))return{path:e,type:"file",size:M(this.#e.get(e)),mtime:this.#r.get(e)};A(e,"No such file or directory","ENOENT")}read(e,r="/"){return e=this.resolve(r,e),this.#t.has(e)&&A(e,"Is a directory","EISDIR"),this.#e.has(e)||A(e,"No such file or directory","ENOENT"),this.#e.get(e)}write(e,r,s="/"){e=this.resolve(s,e),r=String(r);let n=M(r);n>this.#n.maxFileBytes&&A(e,"file quota exceeded","EDQUOT"),this.#t.has(e)&&A(e,"Is a directory","EISDIR"),this.#t.has(this.parent(e))||A(e,"No such directory","ENOENT");let o=this.#e.has(e)?M(this.#e.get(e)):0;return this.#e.has(e)||this.#o(1),this.#s-o+n>this.#n.maxTotalBytes&&A(e,"filesystem quota exceeded","EDQUOT"),this.#e.set(e,r),this.#r.set(e,Date.now()),this.#s+=n-o,e}append(e,r,s="/"){return e=this.resolve(s,e),this.write(e,(this.#e.get(e)??"")+r)}mkdir(e,{cwd:r="/",parents:s=!1}={}){if(e=this.resolve(r,e),this.#e.has(e)&&A(e,"Not a directory","ENOTDIR"),this.#t.has(e)){if(s)return e;A(e,"File exists","EEXIST")}let n=[];for(let o=e;!this.#t.has(o)&&(n.push(o),o!=="/");o=this.parent(o));!s&&n.length>1&&A(e,"No such directory","ENOENT"),this.#o(n.length);for(let o of n.reverse())this.#t.add(o),this.#r.set(o,Date.now());return e}list(e=".",r="/"){e=this.resolve(r,e);let s=this.stat(e);if(s.type==="file")return[{...s,name:this.basename(e)}];let n=e==="/"?"/":`${e}/`,o=new Map;for(let i of[...this.#t,...this.#e.keys()]){if(i===e||!i.startsWith(n))continue;let c=i.slice(n.length).split("/")[0],u=`${n}${c}`;o.has(c)||o.set(c,{...this.stat(u),name:c})}return[...o.values()].sort((i,c)=>i.name.localeCompare(c.name))}remove(e,{cwd:r="/",recursive:s=!1}={}){if(r=this.resolve("/",r),e=this.resolve(r,e),e==="/"&&A(e,"cannot remove root","EPERM"),(e===r||r.startsWith(`${e}/`))&&A(e,"Device busy","EBUSY"),this.#e.has(e)){this.#s-=M(this.#e.get(e)),this.#e.delete(e),this.#r.delete(e);return}this.#t.has(e)||A(e,"No such file or directory","ENOENT");let n=`${e}/`;[...this.#t,...this.#e.keys()].filter(i=>i.startsWith(n)).length&&!s&&A(e,"Directory not empty","ENOTEMPTY");for(let i of[...this.#e.keys()])i.startsWith(n)&&(this.#s-=M(this.#e.get(i)),this.#e.delete(i),this.#r.delete(i));for(let i of[...this.#t])(i===e||i.startsWith(n))&&(this.#t.delete(i),this.#r.delete(i))}touch(e,r="/"){return e=this.resolve(r,e),this.exists(e)?(this.#r.set(e,Date.now()),e):this.write(e,"")}complete(e,r){let s=r.lastIndexOf("/"),n=s<0?".":r.slice(0,s)||"/",o=s<0?r:r.slice(s+1),i=s<0?"":r.slice(0,s+1);try{return this.list(n,e).filter(({name:c})=>c.startsWith(o)).map(({name:c,type:u})=>`${i}${c}${u==="directory"?"/":""}`)}catch{return[]}}#o(e){this.#t.size+this.#e.size-1+e>this.#n.maxFiles&&A("/","file count quota exceeded","EDQUOT")}},ae=(t,e)=>new _(t,e);var q=/^[a-z0-9_][a-z0-9_.+-]*$/,J=/^[1-9][A-Za-z0-9]*$/,nt=/^[a-f0-9]{64}$/,B=t=>Array.isArray(t)?t:t?[t]:[],X=t=>{if(typeof t!="string"||t.startsWith("/")||t.split("/").some(e=>!/^[A-Za-z0-9._+-]+$/.test(e)||e==="."||e===".."))throw new TypeError("invalid manual path");return t},st=async(t,e)=>{let r=Number(t.headers.get("content-length")||0);if(Number.isFinite(r)&&r>e)throw new RangeError("manual response is too large");if(!t.body)return"";let s=t.body.getReader(),n=new TextDecoder,o=[],i=0;try{for(;;){let{done:c,value:u}=await s.read();if(c)break;if(i+=u.byteLength,i>e)throw await s.cancel().catch(()=>{}),new RangeError("manual response is too large");o.push(n.decode(u,{stream:!0}))}return o.push(n.decode()),o.join("")}finally{s.releaseLock()}},ce=async(t,e,r,s=!1,n)=>{let o=await t(e,{signal:n});if(!o.ok)throw new Error(`manual request failed: ${o.status}`);let i=await st(o,r);return s?JSON.parse(i):i},ot=t=>{if(!t||typeof t!="object"||!t.pages||typeof t.pages!="object")throw new TypeError("invalid manual index");let e=Object.create(null);for(let[s,n]of Object.entries(t.pages)){if(!q.test(s))throw new TypeError("invalid manual index");let o=Array.isArray(n)?n:[n];if(!o.length||o.some(c=>!c||typeof c!="object"||!J.test(c.section)||typeof c.path!="string"||!nt.test(c.sha256)||c.description!=null&&typeof c.description!="string"))throw new TypeError("invalid manual index");if(new Set(o.map(c=>c.section)).size!==o.length)throw new TypeError("invalid manual index");let i=Object.freeze(o.map(c=>Object.freeze({...c,path:X(c.path)})));e[s]=Array.isArray(n)?i:i[0]}let r=Object.create(null);for(let[s,n]of Object.entries(t.aliases??{})){let o=n&&Object.hasOwn(e,n.name)?e[n.name]:null;if(!q.test(s)||Object.hasOwn(e,s)||!q.test(n?.name??"")||!J.test(n?.section??"")||!B(o).some(i=>i.section===n.section))throw new TypeError("invalid manual index");r[s]=Object.freeze({name:n.name,section:n.section})}return Object.freeze({...t,aliases:Object.freeze(r),pages:Object.freeze(e)})},Z=({base:t="./manuals/",profile:e="freebsd",fetch:r=globalThis.fetch,crypto:s=globalThis.crypto,maxPage:n=1048576}={})=>{if(typeof r!="function")throw new TypeError("manuals require fetch");if(!s?.subtle)throw new TypeError("manuals require Web Crypto");if(!Number.isSafeInteger(n)||n<1)throw new TypeError("maxPage must be a positive integer");let o=new URL(`${X(e)}/`,new URL(t,globalThis.location?.href??"http://localhost/")),i,c=async m=>(i||(i=ce(r,new URL("index.json",o),n,!0,m).then(ot).catch(l=>{throw i=null,l})),i),u=async(m,l,$)=>{if(m=m.toLowerCase(),!q.test(m)||l&&!J.test(l))return null;let y=await c($);if(Object.hasOwn(y.aliases,m)){let k=y.aliases[m];if(l&&l!==k.section)return null;m=k.name,l=k.section}let v=Object.hasOwn(y.pages,m)?y.pages[m]:null;return B(v).find(k=>!l||k.section===l)??null};return Object.freeze({index:c,find:u,read:async(m,l,$)=>{let y=await u(m,l,$);if(!y)return null;let v=await ce(r,new URL(X(y.path),o),n,!1,$),k=new Uint8Array(await s.subtle.digest("SHA-256",new TextEncoder().encode(v))),h="";for(let N of k)h+=N.toString(16).padStart(2,"0");if(h!==y.sha256)throw new Error("manual integrity check failed");return v},search:async(m,l)=>{if(m=m.trim().toLowerCase(),!m)return[];let $=await c(l),y=Object.entries($.aliases).map(([v,k])=>[v,B($.pages[k.name]).find(h=>h.section===k.section)]);return[...Object.entries($.pages),...y].flatMap(([v,k])=>B(k).filter(h=>`${v} ${h.description??""}`.toLowerCase().includes(m)).map(h=>({name:v,...h})))}})};var it=/^[A-Za-z_][A-Za-z0-9_]*$/,at=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s,ct=new Set([";","&&","||","|","<",">",">>"]),de=new Set(["<",">",">>","0<","1>","1>>","2>","2>>"]),lt=(t,e,r)=>{let s=t[e+1];if(s==="?")return[String(r["?"]??0),e+2];if(s==="{"){let o=t.indexOf("}",e+2);if(o<0)throw new SyntaxError("missing } in variable expansion");let i=t.slice(e+2,o);if(!it.test(i))throw new SyntaxError(`invalid variable name: ${i}`);return[String(r[i]??""),o+1]}let n=t.slice(e+1).match(/^[A-Za-z_][A-Za-z0-9_]*/);return n?[String(r[n[0]]??""),e+n[0].length+1]:["$",e+1]},I=(t,e,r)=>{e&&(t.at(-1)?.expandable===r?t.at(-1).text+=e:t.push({text:e,expandable:r}))},dt=t=>t.parts.map(({text:e})=>e).join(""),F=t=>{let e=t.length;for(;e&&t[e-1].trim()&&!";|&".includes(t[e-1]);)e--;return e},H=(t,e={})=>t.parts.map(({text:r,expandable:s})=>{if(!s)return r;let n="";for(let o=0;o{if(typeof t!="string")throw new TypeError("command must be a string");if(t.length>(r.maxSource??16384))throw new RangeError("command is too long");let s=[],n=[],o=!1,i="",c=()=>{o&&(s.push({type:"word",parts:n}),n=[],o=!1)},u=a=>{c(),!(a===";"&&(!s.length||s.at(-1).value===";"))&&s.push({type:"operator",value:a})};for(let a=0;a=t.length)throw new SyntaxError("trailing backslash");if(i==='"'&&!["$",'"',"\\",` +`].includes(t[a+1])){I(n,d,!0),o=!0,a++;continue}a++,t[a]!==` +`&&(I(n,t[a],!1),o=!0),a++;continue}if(d==="$"){I(n,d,!0),o=!0,a++;continue}if(!i&&d==="#"&&!o){for(;a"].includes(t[a+1])){let $=t.slice(a+1,a+3)===">>"?3:2,y=t.slice(a,a+$);if(!de.has(y))throw new SyntaxError(`unsupported redirect: ${y}`);u(y),a+=$;continue}let m=t.slice(a,a+2),l=["&&","||",">>"].includes(m)?m:ct.has(d)?d:"";if(l){u(l),a+=l.length;continue}if(d==="&")throw new SyntaxError("unsupported operator: &")}I(n,d,!0),o=!0,a++}if(i)throw new SyntaxError(`unterminated ${i} quote`);if(c(),s.at(-1)?.value===";"&&s.pop(),s.length>(r.maxTokens??1024))throw new RangeError("too many tokens");return s},le=(t,e,r)=>{let s=[],n=[];for(;e.i(r.maxArgs??128))throw new RangeError("too many arguments");let o=[];for(;s.length;){let i=dt(s[0]).match(at);if(!i)break;o.push([i[1],s.shift()])}if(!s.length&&!o.length&&!n.length)throw new SyntaxError("empty command");return{argv:s,assignments:o,redirects:n}},ue=(t,e={},r={})=>{let s=ut(t,e,r),n={i:0},o=[],i=null,c=0;for(;n.i=s.length)throw new SyntaxError("missing command after |");u.push(le(s,n,r)),c++}o.push({link:i,pipeline:u});let a=s[n.i++];if(!a)break;if(![";","&&","||"].includes(a.value))throw new SyntaxError(`unexpected operator: ${a.value}`);if(i=a.value,n.i>=s.length&&i!==";")throw new SyntaxError(`missing command after ${i}`);for(;s[n.i]?.value===";";)n.i++}if(c>(r.maxCommands??128))throw new RangeError("too many commands");if(o.length>(r.maxPipelines??64))throw new RangeError("too many pipelines");return o};var fe=t=>Object.freeze({...t,messages:Object.freeze([...t.messages??[]]),modules:Object.freeze([...t.modules??[]]),sysctls:Object.freeze({...t.sysctls??{}})}),ee=(t,e,r,s,n={})=>fe({name:t,sysname:e,release:r,machine:s,hostname:"shell",user:"guest",group:"guest",uid:1e3,gid:1e3,home:"/home/guest",shell:"/bin/sh",path:"/bin:/usr/bin",messages:[],modules:[],sysctls:{},...n}),D=Object.freeze({posix:ee("posix","POSIX","1","wasm32"),freebsd:ee("freebsd","FreeBSD","14.2-RELEASE","amd64",{hostname:"recv",user:"rad",group:"wheel",uid:0,gid:0,home:"/home/rad",ostype:"freebsd14.2",modules:["kernel","zfs.ko"],messages:["FreeBSD 14.2-RELEASE","Trying to mount root from ufs:/dev/ada0p2"],sysctls:{"kern.hostname":"recv","kern.osrelease":"14.2-RELEASE","kern.ostype":"FreeBSD"}}),linux:ee("linux","Linux","6.12.0-web","wasm32",{ostype:"linux-gnu",modules:["loop","overlay"],messages:["Linux version 6.12.0-web","VFS: Mounted root (tmpfs filesystem) readonly"],sysctls:{"kernel.hostname":"shell","kernel.osrelease":"6.12.0-web","kernel.ostype":"Linux"}})}),me=(t="posix")=>{if(typeof t=="string"){if(!D[t])throw new TypeError(`unknown profile: ${t}`);return D[t]}if(!t||typeof t!="object")throw new TypeError("profile must be a name or object");return fe({...D.posix,...t})};var he=new TextEncoder,ft=new TextDecoder,mt="d6f47a8df8691ada08f49fac71b77f3b8dbb061c92041acf00988d34e25d8bcf",ht=typeof document>"u"?globalThis.location?.href:document.currentScript?.src??globalThis.location.href,pe=(t="shell.wasm")=>{let e=ht||globalThis.location?.href;if(!e)throw new TypeError("WebAssembly URL is required outside a browser");return new URL(`../wasm/${t}`,e)},pt=async(t,e)=>{let r=await e(t);if(!r.ok)throw new Error(`WebAssembly request failed: ${r.status}`);let n=(r.headers.get("content-type")?.includes("application/wasm")?await WebAssembly.instantiateStreaming(r,{}):await WebAssembly.instantiate(await r.arrayBuffer(),{})).instance.exports;if(n.abi?.()!==1||!(n.memory instanceof WebAssembly.Memory))throw new Error("unsupported shell.js WebAssembly ABI");return n},we=(t,e={})=>{if(!t)return null;typeof t=="object"&&!(t instanceof URL)&&(e=t);let{fetch:r=globalThis.fetch,threshold:s=262144}=e,n=t===!0||t==="auto"?pe():e.url??t;if(typeof r!="function")throw new TypeError("WebAssembly requires fetch");let o,i,c=!1,u=()=>i?Promise.resolve(!0):c?Promise.resolve(!1):(o||(o=pt(n,r).then(d=>(i=d,!0)).catch(()=>(c=!0,!1))),o);return Object.freeze({filter:(d,m,l=!1)=>{if(d.lengthi.capacity())return null;let k=new Uint8Array(i.memory.buffer);k.set($,v),k.set(y,v+$.length);let h=ft.decode(k.subarray(v,v+i.filter_lines($.length,$.length,y.length,l)));return h&&!h.endsWith(` `)?`${h} -`:h},prepare:m,get ready(){return!!i}})};var ht=/^[A-Za-z0-9][A-Za-z0-9._+-]*$/,C=new TextEncoder,pt=Object.freeze({maxArgs:128,maxCommands:128,maxFileBytes:1048576,maxFiles:2048,maxHistory:500,maxOutput:1048576,maxPipelines:64,maxRuntimeMs:5e3,maxSource:16384,maxTokens:1024,maxTotalBytes:8388608}),wt=(t={})=>{let e={...pt,...t};for(let[n,s]of Object.entries(e))if(!Number.isSafeInteger(s)||s<1)throw new TypeError(`${n} must be a positive integer`);return Object.freeze(e)},yt=t=>{if(t==null)return{code:0,stdout:"",stderr:""};if(typeof t=="string")return{code:0,stdout:t,stderr:""};if(typeof t=="number"&&(t={code:t}),typeof t!="object")throw new TypeError("command returned an invalid result");let e={code:t.code??0,stdout:t.stdout??"",stderr:t.stderr??""};if(!Number.isInteger(e.code)||e.code<0||e.code>255)throw new TypeError("command status must be an integer from 0 to 255");if(typeof e.stdout!="string"||typeof e.stderr!="string")throw new TypeError("command output must be a string");return e},we=(t,e,n=1)=>{let s=e.aborted&&e.reason?.name==="TimeoutError";return{code:s?124:e.aborted?130:n,stdout:"",stderr:`shell: ${s?"timed out":e.aborted?"aborted":t?.message??t} -`}},gt=(t,e)=>{if(e.aborted)return Promise.reject(e.reason);let n,s=new Promise((r,o)=>{n=()=>o(e.reason),e.addEventListener("abort",n,{once:!0})});return Promise.race([t,s]).finally(()=>e.removeEventListener("abort",n))},ye=({profile:t="posix",cwd:e,env:n={},files:s={},commands:r={},manuals:o,wasm:i,limits:c,signal:m}={})=>{let a=me(t),u=wt(c),d=s instanceof W?s:ae(s,u),l={cwd:"/",env:{HOME:a.home,HOSTNAME:a.hostname,LOGNAME:a.user,OSTYPE:a.ostype??a.name,PATH:a.path,SHELL:a.shell,USER:a.user,...n},history:[],status:0};d.mkdir(l.env.HOME,{parents:!0}),d.mkdir(e??l.env.HOME,{parents:!0}),l.cwd=d.resolve("/",e??l.env.HOME),l.env.PWD=l.cwd;let v=ie(a),b=new Map(Object.entries(v.commands));o=o?.read&&o?.search?o:o?Z({profile:a.name,...o}):null,i=i?.filter&&i?.prepare?i:pe(i);let x,O=(p,E)=>{if(!ht.test(p)||typeof E!="function")throw new TypeError("register requires a valid name and function");return b.set(p,E),x};for(let[p,E]of r instanceof Map?r:Object.entries(r))O(p,E);let h=({stdout:p,stderr:E})=>{if(C.encode(p).length+C.encode(E).length>u.maxOutput)throw new RangeError("output limit exceeded")},A=(p,E=l.env)=>{let g=d.resolve(l.cwd,p);if(d.stat(g).type!=="directory")throw new Error(`${p}: Not a directory`);let S=l.cwd;l.cwd=g,Object.assign(l.env,{OLDPWD:S,PWD:g}),E!==l.env&&Object.assign(E,{OLDPWD:S,PWD:g})},I=async(p,E,g,S,j)=>{let L=b.get(p);if(!L)return{code:127,stdout:"",stderr:`${p}: command not found -`};let $={fs:d,profile:a,kernel:v.kernel,manuals:o,wasm:i,signal:j,stdin:S,env:g,get cwd(){return l.cwd},chdir:w=>A(w,g),setenv:(w,T)=>{if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(w))throw new TypeError(`invalid variable: ${w}`);g[w]=String(T)},unsetenv:w=>delete g[w],history:l.history,clearHistory:()=>l.history.splice(0),hasCommand:w=>b.has(w),commands:()=>[...b.keys()].sort(),invoke:(w,T=[],N={})=>I(w,T,N.env??g,N.stdin??"",j)};try{let w=yt(await gt(Promise.resolve().then(()=>L(E,$)),j));return h(w),w}catch(w){if(j.aborted)throw w;return{code:1,stdout:"",stderr:`${p}: ${w?.message??w} -`}}},V=async(p,E,g)=>{let S={...l.env,"?":l.status},j=p.argv.map(k=>H(k,S)),L=p.assignments.map(([k,R])=>[k,H(R,S).slice(k.length+1)]),$=p.redirects.map(({type:k,path:R})=>({type:k,path:H(R,S)})),w=j.length&&L.length?{...l.env,...Object.fromEntries(L)}:l.env;if(!j.length)for(let[k,R]of L)l.env[k]=R;let T,N;for(let k of $)(k.type==="<"||k.type==="0<")&&(E=d.read(k.path,l.cwd)),[">","1>","2>"].includes(k.type)&&d.write(k.path,"",l.cwd),k.type.endsWith(">>")&&!d.exists(k.path,l.cwd)&&d.write(k.path,"",l.cwd),[">",">>","1>","1>>"].includes(k.type)&&(T=k),["2>","2>>"].includes(k.type)&&(N=k);let _=j.length?await I(j[0],j.slice(1),w,E,g):{code:0,stdout:"",stderr:""};return T&&(T.type.endsWith(">>")?d.append(T.path,_.stdout,l.cwd):d.write(T.path,_.stdout,l.cwd),_={..._,stdout:""}),N&&(N.type.endsWith(">>")?d.append(N.path,_.stderr,l.cwd):d.write(N.path,_.stderr,l.cwd),_={..._,stderr:""}),_},be=async(p,E)=>{let g="",S="",j=0;try{for(let L of p){let $=await V(L,g,E);g=$.stdout,S+=$.stderr,j=$.code,h({stdout:g,stderr:S})}return{code:j,stdout:g,stderr:S}}catch(L){let $=we(L,E);try{h({stdout:"",stderr:S+$.stderr}),$.stderr=S+$.stderr}catch{}return $}},$e=async(p,E={})=>{if(typeof p!="string")return{code:2,stdout:"",stderr:`shell: command must be a string -`};p.trim()&&(l.history.push(p),l.history.length>u.maxHistory&&l.history.splice(0,l.history.length-u.maxHistory));let g=new AbortController,S=[m,E.signal].filter(Boolean),j=w=>g.abort(w.target.reason);for(let w of S)w.aborted?g.abort(w.reason):w.addEventListener("abort",j,{once:!0});let L=setTimeout(()=>{let w=new Error("timed out");w.name="TimeoutError",g.abort(w)},u.maxRuntimeMs),$={code:0,stdout:"",stderr:""};try{let w=fe(p,{},u);for(let T of w){if(T.link==="&&"&&$.code!==0||T.link==="||"&&$.code===0)continue;let N=await be(T.pipeline,g.signal);l.status=N.code,$={code:N.code,stdout:$.stdout+N.stdout,stderr:$.stderr+N.stderr},h($)}l.status=$.code}catch(w){let T=we(w,g.signal,2);C.encode($.stdout).length+C.encode($.stderr).length+C.encode(T.stderr).length>u.maxOutput?$=T:$={code:T.code,stdout:$.stdout,stderr:$.stderr+T.stderr},l.status=$.code}finally{clearTimeout(L);for(let w of S)w.removeEventListener("abort",j)}return $},te=Promise.resolve();return x={exec:(p,E)=>{let g=te.then(()=>$e(p,E));return te=g.catch(()=>{}),g},complete:(p="")=>{if(typeof p!="string"||p.length>u.maxSource)return[];let E=F(p),g=p.slice(E);if(g.startsWith("$"))return Object.keys(l.env).filter(j=>j.startsWith(g.slice(1))).sort().map(j=>`$${j}`);let S=p.slice(0,E);return!S.trim()||";|&".includes(S.trimEnd().at(-1))?[...b.keys()].filter(j=>j.startsWith(g)).sort():d.complete(l.cwd,g)},register:O,prepare:(p="wasm")=>p==="wasm"?i?.prepare()??Promise.resolve(!1):Promise.reject(new TypeError(`unknown feature: ${p}`)),fs:d,profile:a,kernel:v.kernel,env:l.env,get cwd(){return l.cwd}},x};var Q=(t,e)=>t.matches?.(e)?t:t.querySelector(e),ge=(t,e,n={})=>{if(!t||typeof t.querySelector!="function"||!e?.exec)throw new TypeError("mountShell requires a root element and shell");let s=Q(t,n.form??"form"),r=Q(t,n.input??"input"),o=Q(t,n.output??"output, [role=log]"),i=Q(t,n.promptElement??"[data-shell-prompt]");if(!s||!r||!o)throw new TypeError("shell UI requires form, input, and output elements");let c=n.maxEntries??500;if(!Number.isSafeInteger(c)||c<1)throw new TypeError("maxEntries must be a positive integer");let m=[],a=0,u,d=!1,l,v=n.render??(h=>{let A=t.ownerDocument.createElement("pre");A.textContent=`${h.prompt}${h.command} -${h.stdout}${h.stderr}`,o.append(A),o.scrollTop=o.scrollHeight}),b=()=>{let h=typeof n.prompt=="function"?n.prompt(e):n.promptText??"$ ";return i&&(i.textContent=h),h},x=async h=>{if(h.preventDefault(),u)return;let A=r.value;if(!A.trim())return;m.push(A),m.length>c&&m.shift(),a=m.length,r.value="";let I=b();l=r.readOnly,u=new AbortController,r.readOnly=!0;try{let V=await e.exec(A,{signal:u.signal});if(d)return;for(A.trim()==="clear"?o.replaceChildren():v({...V,command:A,prompt:I},{root:t,form:s,input:r,output:o,shell:e});o.childNodes.length>c;)o.firstChild.remove()}finally{u=null,d||(r.readOnly=l,b(),r.focus()),l=void 0}},O=h=>{if(h.ctrlKey&&h.key.toLowerCase()==="c"&&u)h.preventDefault(),u.abort();else if(h.ctrlKey&&h.key.toLowerCase()==="l")h.preventDefault(),o.replaceChildren();else if(h.key==="ArrowUp"||h.key==="ArrowDown")h.preventDefault(),a=Math.max(0,Math.min(m.length,a+(h.key==="ArrowUp"?-1:1))),r.value=m[a]??"";else if(h.key==="Tab"){let A=e.complete(r.value);A.length===1&&(h.preventDefault(),r.value=`${r.value.slice(0,F(r.value))}${A[0]}`)}};return s.addEventListener("submit",x),r.addEventListener("keydown",O),b(),Object.freeze({destroy(){d=!0,l!==void 0&&(r.readOnly=l),u?.abort(),s.removeEventListener("submit",x),r.removeEventListener("keydown",O)},focus:()=>r.focus()})};return ke(bt);})(); +`:h},prepare:u,get ready(){return!!i}})},ye=({url:t,fetch:e=globalThis.fetch}={})=>{if(typeof e!="function")throw new TypeError("Krad requires fetch");let r=t??pe("krad-add.wasm"),s,n=o=>(s||(s=(async()=>{let i=await e(r,{credentials:"omit",redirect:"error",referrerPolicy:"no-referrer",signal:o});if(!i.ok)throw new Error(`Krad request failed: ${i.status}`);let c=i.body?.getReader();if(!c)throw new Error("Krad response has no body");let u=new Uint8Array(4096),a=0;try{for(;;){let{done:y,value:v}=await c.read();if(y)break;if(a+v.byteLength>u.byteLength)throw await c.cancel().catch(()=>{}),new RangeError("Krad module is too large");u.set(v,a),a+=v.byteLength}}finally{c.releaseLock()}let d=u.subarray(0,a);if([...new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256",d))].map(y=>y.toString(16).padStart(2,"0")).join("")!==mt)throw new Error("Krad module failed integrity verification");let l=await WebAssembly.compile(d);if(WebAssembly.Module.imports(l).length)throw new Error("Krad module imports are not allowed");let $=await WebAssembly.instantiate(l);if(typeof $.exports.krad_add!="function")throw new Error("unsupported Krad ABI");return $.exports.krad_add})().catch(i=>{throw s=null,i})),s);return async(o,{signal:i}={})=>!Array.isArray(o)||o.length!==2||o.some(c=>!/^[+-]?\d+$/.test(c)||Number(c)<-2147483648||Number(c)>2147483647)?{code:2,stderr:`usage: krad-add INTEGER INTEGER +`}:`${(await n(i))(Number(o[0]),Number(o[1]))} +`};var wt=/^[A-Za-z0-9][A-Za-z0-9._+-]*$/,P=new TextEncoder,yt=Object.freeze({maxArgs:128,maxCommands:128,maxFileBytes:1048576,maxFiles:2048,maxHistory:500,maxOutput:1048576,maxPipelines:64,maxRuntimeMs:5e3,maxSource:16384,maxTokens:1024,maxTotalBytes:8388608}),gt=(t={})=>{let e={...yt,...t};for(let[r,s]of Object.entries(e))if(!Number.isSafeInteger(s)||s<1)throw new TypeError(`${r} must be a positive integer`);return Object.freeze(e)},bt=t=>{if(t==null)return{code:0,stdout:"",stderr:""};if(typeof t=="string")return{code:0,stdout:t,stderr:""};if(typeof t=="number"&&(t={code:t}),typeof t!="object")throw new TypeError("command returned an invalid result");let e={code:t.code??0,stdout:t.stdout??"",stderr:t.stderr??""};if(!Number.isInteger(e.code)||e.code<0||e.code>255)throw new TypeError("command status must be an integer from 0 to 255");if(typeof e.stdout!="string"||typeof e.stderr!="string")throw new TypeError("command output must be a string");return e},ge=(t,e,r=1)=>{let s=e.aborted&&e.reason?.name==="TimeoutError";return{code:s?124:e.aborted?130:r,stdout:"",stderr:`shell: ${s?"timed out":e.aborted?"aborted":t?.message??t} +`}},$t=(t,e)=>{if(e.aborted)return Promise.reject(e.reason);let r,s=new Promise((n,o)=>{r=()=>o(e.reason),e.addEventListener("abort",r,{once:!0})});return Promise.race([t,s]).finally(()=>e.removeEventListener("abort",r))},be=({profile:t="posix",cwd:e,env:r={},files:s={},commands:n={},manuals:o,wasm:i,limits:c,signal:u}={})=>{let a=me(t),d=gt(c),m=s instanceof _?s:ae(s,d),l={cwd:"/",env:{HOME:a.home,HOSTNAME:a.hostname,LOGNAME:a.user,OSTYPE:a.ostype??a.name,PATH:a.path,SHELL:a.shell,USER:a.user,...r},history:[],status:0};m.mkdir(l.env.HOME,{parents:!0}),m.mkdir(e??l.env.HOME,{parents:!0}),l.cwd=m.resolve("/",e??l.env.HOME),l.env.PWD=l.cwd;let $=ie(a),y=new Map(Object.entries($.commands));o=o?.read&&o?.search?o:o?Z({profile:a.name,...o}):null,i=i?.filter&&i?.prepare?i:we(i);let v,k=(p,x)=>{if(!wt.test(p)||typeof x!="function")throw new TypeError("register requires a valid name and function");return y.set(p,x),v};for(let[p,x]of n instanceof Map?n:Object.entries(n))k(p,x);let h=({stdout:p,stderr:x})=>{if(P.encode(p).length+P.encode(x).length>d.maxOutput)throw new RangeError("output limit exceeded")},N=(p,x=l.env)=>{let b=m.resolve(l.cwd,p);if(m.stat(b).type!=="directory")throw new Error(`${p}: Not a directory`);let S=l.cwd;l.cwd=b,Object.assign(l.env,{OLDPWD:S,PWD:b}),x!==l.env&&Object.assign(x,{OLDPWD:S,PWD:b})},C=async(p,x,b,S,O)=>{let L=y.get(p);if(!L)return{code:127,stdout:"",stderr:`${p}: command not found +`};let E={fs:m,profile:a,kernel:$.kernel,manuals:o,wasm:i,signal:O,stdin:S,env:b,get cwd(){return l.cwd},chdir:w=>N(w,b),setenv:(w,T)=>{if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(w))throw new TypeError(`invalid variable: ${w}`);b[w]=String(T)},unsetenv:w=>delete b[w],history:l.history,clearHistory:()=>l.history.splice(0),hasCommand:w=>y.has(w),commands:()=>[...y.keys()].sort(),invoke:(w,T=[],z={})=>C(w,T,z.env??b,z.stdin??"",O)};try{let w=bt(await $t(Promise.resolve().then(()=>L(x,E)),O));return h(w),w}catch(w){if(O.aborted)throw w;return{code:1,stdout:"",stderr:`${p}: ${w?.message??w} +`}}},G=async(p,x,b)=>{let S={...l.env,"?":l.status},O=p.argv.map(j=>H(j,S)),L=p.assignments.map(([j,R])=>[j,H(R,S).slice(j.length+1)]),E=p.redirects.map(({type:j,path:R})=>({type:j,path:H(R,S)})),w=O.length&&L.length?{...l.env,...Object.fromEntries(L)}:l.env;if(!O.length)for(let[j,R]of L)l.env[j]=R;let T,z;for(let j of E)(j.type==="<"||j.type==="0<")&&(x=m.read(j.path,l.cwd)),[">","1>","2>"].includes(j.type)&&m.write(j.path,"",l.cwd),j.type.endsWith(">>")&&!m.exists(j.path,l.cwd)&&m.write(j.path,"",l.cwd),[">",">>","1>","1>>"].includes(j.type)&&(T=j),["2>","2>>"].includes(j.type)&&(z=j);let W=O.length?await C(O[0],O.slice(1),w,x,b):{code:0,stdout:"",stderr:""};return T&&(T.type.endsWith(">>")?m.append(T.path,W.stdout,l.cwd):m.write(T.path,W.stdout,l.cwd),W={...W,stdout:""}),z&&(z.type.endsWith(">>")?m.append(z.path,W.stderr,l.cwd):m.write(z.path,W.stderr,l.cwd),W={...W,stderr:""}),W},Ee=async(p,x)=>{let b="",S="",O=0;try{for(let L of p){let E=await G(L,b,x);b=E.stdout,S+=E.stderr,O=E.code,h({stdout:b,stderr:S})}return{code:O,stdout:b,stderr:S}}catch(L){let E=ge(L,x);try{h({stdout:"",stderr:S+E.stderr}),E.stderr=S+E.stderr}catch{}return E}},ve=async(p,x={})=>{if(typeof p!="string")return{code:2,stdout:"",stderr:`shell: command must be a string +`};p.trim()&&(l.history.push(p),l.history.length>d.maxHistory&&l.history.splice(0,l.history.length-d.maxHistory));let b=new AbortController,S=[u,x.signal].filter(Boolean),O=w=>b.abort(w.target.reason);for(let w of S)w.aborted?b.abort(w.reason):w.addEventListener("abort",O,{once:!0});let L=setTimeout(()=>{let w=new Error("timed out");w.name="TimeoutError",b.abort(w)},d.maxRuntimeMs),E={code:0,stdout:"",stderr:""};try{let w=ue(p,{},d);for(let T of w){if(T.link==="&&"&&E.code!==0||T.link==="||"&&E.code===0)continue;let z=await Ee(T.pipeline,b.signal);l.status=z.code,E={code:z.code,stdout:E.stdout+z.stdout,stderr:E.stderr+z.stderr},h(E)}l.status=E.code}catch(w){let T=ge(w,b.signal,2);P.encode(E.stdout).length+P.encode(E.stderr).length+P.encode(T.stderr).length>d.maxOutput?E=T:E={code:T.code,stdout:E.stdout,stderr:E.stderr+T.stderr},l.status=E.code}finally{clearTimeout(L);for(let w of S)w.removeEventListener("abort",O)}return E},te=Promise.resolve();return v={exec:(p,x)=>{let b=te.then(()=>ve(p,x));return te=b.catch(()=>{}),b},complete:(p="")=>{if(typeof p!="string"||p.length>d.maxSource)return[];let x=F(p),b=p.slice(x);if(b.startsWith("$"))return Object.keys(l.env).filter(O=>O.startsWith(b.slice(1))).sort().map(O=>`$${O}`);let S=p.slice(0,x);return!S.trim()||";|&".includes(S.trimEnd().at(-1))?[...y.keys()].filter(O=>O.startsWith(b)).sort():m.complete(l.cwd,b)},register:k,prepare:(p="wasm")=>p==="wasm"?i?.prepare()??Promise.resolve(!1):Promise.reject(new TypeError(`unknown feature: ${p}`)),fs:m,profile:a,kernel:$.kernel,env:l.env,get cwd(){return l.cwd}},v};var K=(t,e)=>t.matches?.(e)?t:t.querySelector(e),$e=(t,e,r={})=>{if(!t||typeof t.querySelector!="function"||!e?.exec)throw new TypeError("mountShell requires a root element and shell");let s=K(t,r.form??"form"),n=K(t,r.input??"input"),o=K(t,r.output??"output, [role=log]"),i=K(t,r.promptElement??"[data-shell-prompt]");if(!s||!n||!o)throw new TypeError("shell UI requires form, input, and output elements");let c=r.maxEntries??500;if(!Number.isSafeInteger(c)||c<1)throw new TypeError("maxEntries must be a positive integer");let u=[],a=0,d,m=!1,l,$=r.render??(h=>{let N=t.ownerDocument.createElement("pre");N.textContent=`${h.prompt}${h.command} +${h.stdout}${h.stderr}`,o.append(N),o.scrollTop=o.scrollHeight}),y=()=>{let h=typeof r.prompt=="function"?r.prompt(e):r.promptText??"$ ";return i&&(i.textContent=h),h},v=async h=>{if(h.preventDefault(),d)return;let N=n.value;if(!N.trim())return;u.push(N),u.length>c&&u.shift(),a=u.length,n.value="";let C=y();l=n.readOnly,d=new AbortController,n.readOnly=!0;try{let G=await e.exec(N,{signal:d.signal});if(m)return;for(N.trim()==="clear"?o.replaceChildren():$({...G,command:N,prompt:C},{root:t,form:s,input:n,output:o,shell:e});o.childNodes.length>c;)o.firstChild.remove()}finally{d=null,m||(n.readOnly=l,y(),n.focus()),l=void 0}},k=h=>{if(h.ctrlKey&&h.key.toLowerCase()==="c"&&d)h.preventDefault(),d.abort();else if(h.ctrlKey&&h.key.toLowerCase()==="l")h.preventDefault(),o.replaceChildren();else if(h.key==="ArrowUp"||h.key==="ArrowDown")h.preventDefault(),a=Math.max(0,Math.min(u.length,a+(h.key==="ArrowUp"?-1:1))),n.value=u[a]??"";else if(h.key==="Tab"){let N=e.complete(n.value);N.length===1&&(h.preventDefault(),n.value=`${n.value.slice(0,F(n.value))}${N[0]}`)}};return s.addEventListener("submit",v),n.addEventListener("keydown",k),y(),Object.freeze({destroy(){m=!0,l!==void 0&&(n.readOnly=l),d?.abort(),s.removeEventListener("submit",v),n.removeEventListener("keydown",k)},focus:()=>n.focus()})};return Te(Et);})(); diff --git a/javascripts/test.js b/javascripts/test.js index 857604e..46dd553 100644 --- a/javascripts/test.js +++ b/javascripts/test.js @@ -14,6 +14,7 @@ import { import { tmpdir } from "node:os"; import { join, relative } from "node:path"; import { runInNewContext } from "node:vm"; +import { createKradAdd } from "./index.js"; import { createManuals, readLimited } from "./man.js"; import { buildManuals, licenseHeader, validateManual } from "./manuals.js"; import { MemoryFS, createShell, profiles } from "./shell.js"; @@ -385,6 +386,44 @@ if (existsSync("wasm/shell.wasm")) { assert.equal(wasm.filter("koala", "koa"), "koala\n"); } +if (existsSync("wasm/krad-add.wasm")) { + const bytes = readFileSync("wasm/krad-add.wasm"); + let requests = 0; + const krad = createShell({ + commands: { + "krad-add": createKradAdd({ + url: "https://example.test/krad-add.wasm", + fetch: async () => { + requests++; + return new Response(bytes); + }, + }), + }, + }); + assert.equal((await krad.exec("krad-add nope 22")).code, 2); + assert.equal(requests, 0); + assert.equal((await run(krad, "krad-add 20 22")).stdout, "42\n"); + assert.equal((await run(krad, "krad-add -1 2")).stdout, "1\n"); + assert.equal(requests, 1); + + const corrupted = Uint8Array.from(bytes); + corrupted[corrupted.length - 1] ^= 1; + for (const [body, error] of [ + [corrupted, /integrity verification/], + [new Uint8Array(4097), /module is too large/], + ]) { + const unsafe = createShell({ + commands: { + "krad-add": createKradAdd({ + url: "https://example.test/krad-add.wasm", + fetch: async () => new Response(body), + }), + }, + }); + assert.match((await unsafe.exec("krad-add 20 22")).stderr, error); + } +} + if (existsSync("javascripts/shell.min.js")) { const context = { AbortController, @@ -397,6 +436,7 @@ if (existsSync("javascripts/shell.min.js")) { }; runInNewContext(readFileSync("javascripts/shell.min.js", "utf8"), context); assert.equal(typeof context.ShellJS.createShell, "function"); + assert.equal(typeof context.ShellJS.createKradAdd, "function"); } console.log("shell.js core: ok"); diff --git a/javascripts/wasm.js b/javascripts/wasm.js index 5c9c1ac..bb4e061 100644 --- a/javascripts/wasm.js +++ b/javascripts/wasm.js @@ -1,11 +1,12 @@ const encoder = new TextEncoder(); const decoder = new TextDecoder(); +const kradAddDigest = "d6f47a8df8691ada08f49fac71b77f3b8dbb061c92041acf00988d34e25d8bcf"; const script = typeof document === "undefined" ? globalThis.location?.href : (document.currentScript?.src ?? import.meta.url); -const defaultURL = () => { +const defaultURL = (name = "shell.wasm") => { const base = script || globalThis.location?.href; if (!base) throw new TypeError("WebAssembly URL is required outside a browser"); - return new URL("../wasm/shell.wasm", base); + return new URL(`../wasm/${name}`, base); }; const instantiate = async (source, fetcher) => { @@ -72,3 +73,64 @@ export const createWasm = (setting, options = {}) => { }, }); }; + +export const createKradAdd = ({ url, fetch: fetcher = globalThis.fetch } = {}) => { + if (typeof fetcher !== "function") throw new TypeError("Krad requires fetch"); + const source = url ?? defaultURL("krad-add.wasm"); + let loading; + const load = (signal) => { + if (!loading) { + loading = (async () => { + const response = await fetcher(source, { + credentials: "omit", + redirect: "error", + referrerPolicy: "no-referrer", + signal, + }); + if (!response.ok) throw new Error(`Krad request failed: ${response.status}`); + const reader = response.body?.getReader(); + if (!reader) throw new Error("Krad response has no body"); + const buffer = new Uint8Array(4096); + let length = 0; + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + if (length + value.byteLength > buffer.byteLength) { + await reader.cancel().catch(() => {}); + throw new RangeError("Krad module is too large"); + } + buffer.set(value, length); + length += value.byteLength; + } + } finally { + reader.releaseLock(); + } + const bytes = buffer.subarray(0, length); + const digest = [...new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes))] + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + if (digest !== kradAddDigest) throw new Error("Krad module failed integrity verification"); + const module = await WebAssembly.compile(bytes); + if (WebAssembly.Module.imports(module).length) throw new Error("Krad module imports are not allowed"); + const instance = await WebAssembly.instantiate(module); + if (typeof instance.exports.krad_add !== "function") throw new Error("unsupported Krad ABI"); + return instance.exports.krad_add; + })().catch((error) => { + loading = null; + throw error; + }); + } + return loading; + }; + return async (args, { signal } = {}) => { + if ( + !Array.isArray(args) || + args.length !== 2 || + args.some((value) => !/^[+-]?\d+$/.test(value) || Number(value) < -2147483648 || Number(value) > 2147483647) + ) { + return { code: 2, stderr: "usage: krad-add INTEGER INTEGER\n" }; + } + return `${(await load(signal))(Number(args[0]), Number(args[1]))}\n`; + }; +}; diff --git a/package.json b/package.json index 453f007..23ace8b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "sideEffects": false, "exports": { ".": "./javascripts/index.js", + "./krad-add.wasm": "./wasm/krad-add.wasm", "./wasm": "./wasm/shell.wasm", "./manuals/*": "./manuals/*" }, @@ -24,6 +25,8 @@ "kernels/lib.rs", "manuals", "docs/MANUAL_SOURCES.md", + "wasm/krad-add.SOURCE", + "wasm/krad-add.wasm", "wasm/shell.wasm", "Cargo.lock", "Cargo.toml", diff --git a/wasm/krad-add.SOURCE b/wasm/krad-add.SOURCE new file mode 100644 index 0000000..df701de --- /dev/null +++ b/wasm/krad-add.SOURCE @@ -0,0 +1,7 @@ +Repository: https://github.com/keys-i/krad +Merged as: 145b9affc6ae259e258237b3e2b20acca910c870 +Source commit: 55367f8ad006e18ac6c4c8c760641569975b1bba +Source path: dist/krad-add.wasm +SHA-256: d6f47a8df8691ada08f49fac71b77f3b8dbb061c92041acf00988d34e25d8bcf +Compiler: WASI SDK 33 / Clang 22.1.0, wasm32-unknown-unknown, -nostdlib +License: MIT diff --git a/wasm/krad-add.wasm b/wasm/krad-add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5e40d7975a2a259447554a322d663fed266c09ac GIT binary patch literal 70 zcmV~$F%Ezr5Cp*8LphY%p0D^4B*8{QOt7*#p6(3F+X#TlL(FJSh}jYJfyRA|De5M^ Vo$cwLAl$vwTxtc@g^I+4EB=(Y3*7(! literal 0 HcmV?d00001