Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `;`, `&&`, `||`,
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion javascripts/browser.fixture.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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, {
Expand All @@ -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");
Expand Down
1 change: 1 addition & 0 deletions javascripts/index.js
Original file line number Diff line number Diff line change
@@ -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";
92 changes: 47 additions & 45 deletions javascripts/shell.min.js

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions javascripts/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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");
66 changes: 64 additions & 2 deletions javascripts/wasm.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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`;
};
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"sideEffects": false,
"exports": {
".": "./javascripts/index.js",
"./krad-add.wasm": "./wasm/krad-add.wasm",
"./wasm": "./wasm/shell.wasm",
"./manuals/*": "./manuals/*"
},
Expand All @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions wasm/krad-add.SOURCE
Original file line number Diff line number Diff line change
@@ -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
Binary file added wasm/krad-add.wasm
Binary file not shown.