@everruns/bashkit-wasm

中文文档
0.21.0发布于 2026/11/11MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 4.4万

Sandboxed bash interpreter for the browser (WebAssembly). Single-threaded — no SharedArrayBuffer, no COOP/COEP headers.

原始使用文档
文档目录18 节
  1. Live demo
  2. Install
  3. Quick start
  4. Plain <script type="module"> (no bundler)
  5. Async custom builtins
  6. Sync vs async
  7. Interactive terminal
  8. Options
  9. Virtual filesystem
  10. Analysis, cancellation, and persistence
  11. Host-backed filesystem
  12. What's included
  13. Limitations
  14. Examples
  15. Development
  16. License
  17. Deployment and cold start
  18. Optional Python/TypeScript and host functions

@everruns/bashkit-wasm

Sandboxed bash interpreter compiled to WebAssembly, for the browser and any other JavaScript runtime: edge/serverless workers (Cloudflare Workers, Vercel Edge, Deno Deploy), Node, Deno, and Bun.

Unlike a WASI-threads build, this package is single-threaded: it needs no SharedArrayBuffer and no cross-origin isolation (COOP/COEP) headers. That makes it a drop-in for any web app, including embedded and third-party iframe contexts where those headers can't be set, and for the constrained edge runtimes that can't use threads either.

It's a wasm-bindgen module, so it runs in any JS host but not a non-JS/WASI wasm runtime (wasmtime, wasmer). For a native Node.js / Bun / Deno addon (faster, no wasm), use @everruns/bashkit instead; reach for this package when a native addon can't load, browsers and edge runtimes.

Live demo

A full interactive terminal built on this package, examples/browser. It's a single index.html on Vite: pnpm install && pnpm start, no build step and no special headers.

Bashkit browser terminal

Install

npm install @everruns/bashkit-wasm

Quick start

import { initBashkit, Bash } from "@everruns/bashkit-wasm";

// Load the .wasm once before constructing Bash.
await initBashkit();

const bash = new Bash();
const result = bash.executeSync('echo "Hello, browser!" | tr a-z A-Z');
console.log(result.stdout); // HELLO, BROWSER!

Plain <script type="module"> (no bundler)

<script type="module">
  import { initBashkit, Bash } from "https://esm.sh/@everruns/bashkit-wasm";
  await initBashkit();
  const bash = new Bash();
  document.body.textContent = bash.executeSync("seq 1 5 | paste -sd+ | bc").stdout;
</script>

Async custom builtins

Register JS callbacks as bash commands. Async callbacks (e.g. issuing a fetch / GraphQL request) are awaited by execute(), the async API:

const bash = new Bash({
  customBuiltins: {
    graphql: async (ctx) => {
      const res = await fetch("/graphql", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: ctx.stdin ?? "{}",
      });
      return await res.text();
    },
  },
});

const out = await bash.execute('echo "{ me { id } }" | graphql | jq .data');
console.log(out.stdout);

ctx is { name, argv, stdin, env, cwd, fs }. Return the builtin's stdout as a string (or a Promise<string>); throwing becomes stderr with exit code 1.

ctx.fs is a live handle to the same virtual filesystem the script sees, so a builtin can read inputs and write outputs that later commands pick up:

const bash = new Bash({
  customBuiltins: {
    "uppercase-file": (ctx) => {
      const text = ctx.fs.readFile(ctx.argv[0]);
      ctx.fs.writeFile("/out.txt", text.toUpperCase());
      return "done\n";
    },
  },
});
bash.writeFile("/in.txt", "hello\n");
await bash.execute("uppercase-file /in.txt && cat /out.txt"); // -> HELLO

ctx.fs has readFile, writeFile, appendFile, their binary-safe *FileBytes variants, exists, mkdir, remove, and ls, the same surface as the Bash VFS helpers below.

Sync vs async

  • executeSync(cmd), for plain bash and jq. Fast, returns an ExecResult directly. Throws if the script suspends, for example sleep or an async custom builtin.
  • execute(cmd), returns Promise<ExecResult>. Required whenever an async custom builtin or wall-clock operation may run.
  • executeWithOutput(cmd, callback), async execution plus incremental (stdout, stderr) chunks. The returned ExecResult remains authoritative.

Interactive terminal

Terminal runs an interactive shell session on an in-memory terminal: you forward keystrokes, it returns escape-sequence bytes for a renderer such as xterm.js. vi, less and more run full-screen. Try it on the bashkit.sh playground.

import { Terminal as XTerm } from "@xterm/xterm";
import { initBashkit, Terminal } from "@everruns/bashkit-wasm";

await initBashkit();
const xterm = new XTerm();
xterm.open(document.getElementById("term"));

const term = new Terminal({ rows: xterm.rows, cols: xterm.cols, cwd: "/home/user" });
let running = false;
let again = false;
async function pump() {
  if (running) return void (again = true);
  running = true;
  do {
    again = false;
    await term.runUntilIdle(); // { status: "idle" } or { status: "exited", exitCode }
    xterm.write(term.takeOutput());
  } while (again);
  running = false;
}
xterm.onData((keys) => { term.send(keys); pump(); });
xterm.onResize(({ rows, cols }) => term.resize(rows, cols));
pump();

runUntilIdle() resolves once the session waits for input; keystrokes sent while a command runs are queued. screenText() returns the visible screen as plain text and fs() reads what vi saved. Terminal takes every Bash option plus rows and cols. It is undefined in custom builds made with BASHKIT_WASM_FEATURES="". See the terminal guide for the line editor, vi keys and limits.

Options

new Bash({
  username, hostname, cwd,
  env: { KEY: "value" },
  maxCommands, maxLoopIterations, maxMemory,
  files: { "/config.json": '{"debug":true}' },
  customBuiltins: { name: (ctx) => "..." },
  fs: hostFileSystem, // host-backed filesystem; see below
});

Virtual filesystem

Files created via the helpers are visible to scripts and vice versa:

bash.mkdir("/data");
bash.writeFile("/data/x.txt", "hi\n");
bash.appendFile("/data/x.txt", "there\n");
bash.readFile("/data/x.txt"); // "hi\nthere\n"
bash.exists("/data/x.txt");   // true
bash.ls("/data");             // ["x.txt"]
bash.executeSync("cat /data/x.txt").stdout; // "hi\nthere\n"
bash.remove("/data/x.txt");

// Binary-safe I/O avoids UTF-8 conversion.
bash.writeFileBytes("/data/blob", Uint8Array.from([0, 255]));
bash.readFileBytes("/data/blob");

// bash.fs() returns the same live handle passed to builtins as ctx.fs
const fs = bash.fs();

Analysis, cancellation, and persistence

analyze(script) provides the same advisory Gatekeeper projection as the native bindings. Treat parse errors or isOpaque as deny/prompt, not safe:

const analysis = bash.analyze("cat input.txt | jq . > output.json");
if (analysis.isOpaque) throw new Error("permission prompt required");

cancel() sets a sticky cooperative flag checked at command boundaries; clearCancel() makes the instance reusable. commit() returns a content-addressed object set and checkout(id, objects, policy?) restores it:

const saved = bash.commit();
const resumed = new Bash();
resumed.checkout(saved.id, saved.objects); // policy defaults to "superset"

Host-backed filesystem

Pass fs to run scripts directly against storage you own, a Durable Object, an OPFS handle, IndexedDB, instead of the in-memory VFS. Nothing is copied in or diffed back out: every read and write during the run is a call into your object.

const bash = new Bash({ cwd: "/workspace", fs: myHost });
const r = await bash.execute("grep -rl TODO . | head -5");

Implement seven required methods; each may return its value directly or as a Promise:

read(path)                 // -> Uint8Array | string   (throw ENOENT when absent)
write(path, bytes)         // -> void
mkdir(path, recursive)     // -> void
remove(path, recursive)    // -> void
stat(path)                 // -> { type: "file" | "dir" | "symlink", size?, mode?, mtimeMs? }
readDir(path)              // -> [{ name, type, size?, mode?, mtimeMs? }]
exists(path)               // -> boolean

append, copy, rename, and chmod are optional: omit them and they are synthesized from the required primitives (chmod is accepted and ignored, so chmod +x still works). symlink and readLink are optional too, but scripts that reach for them fail with ENOSYS when the host omits them.

Your host implements raw storage only, POSIX semantics (parent-directory checks, "is a directory", symlink resolution) are enforced above it. Throw an Error carrying a code (ENOENT, EEXIST, EACCES, EPERM, EISDIR, ENOTDIR, ENOTEMPTY, EXDEV, ENOSYS) so bash reports the failure the way a real shell does.

Two contract notes:

  • execute() only. A host call can suspend the interpreter, and executeSync cannot await, it reports the suspension instead of blocking. The synchronous bash.readFile(...) helpers behave the same way.
  • files is rejected alongside fs. Seeding writes through the VFS synchronously, which a promise-returning host can never satisfy. Write seed data through the host directly.

Provide /dev/null in the host if your scripts redirect to it; with a host filesystem there is no built-in VFS underneath to supply it.

What's included

Plain bash plus the built-in text tooling (grep, sed, awk, jq, find, …) and jq. Not included in the browser build: outbound HTTP (curl/wget), ssh and sqlite, these need sockets, threads, or a host filesystem the browser sandbox doesn't provide. Bridge to the network through a custom builtin (see above) so requests go through your app's own fetch.

Limitations

  • Cancellation is cooperative. Host timers drive sleep, timeout, and configured execution deadlines. A synchronous CPU loop cannot observe a JS callback or cancel() until control returns to the event loop, so command, loop, parser-fuel, and memory limits remain the hard bound for such work.
  • executeSync can't run async builtins. The single-threaded event loop can't settle a Promise without yielding; an async builtin under executeSync fails fast with a clear message. Use execute().

Examples

  • examples/browser , the full interactive terminal shown above, on Vite (no build step, no headers).
  • Minimal, dependency-free demos in example/, an interactive terminal and an async-builtin/ctx.fs demo, served by any static file server. See example/README.md.

Development

# Build the bundle and run the headless integration tests:
bash scripts/build.sh
node --test "__test__/*.test.mjs"
# or, from the repo root:
just build-wasm

License

MIT, part of the Bashkit project.

Deployment and cold start

Serve bashkit_wasm_bg.wasm as application/wasm with Brotli or gzip HTTP compression. Raw npm file size is not transfer size. Use content-hashed asset URLs with long-lived immutable caching; keep the JS glue and WASM from the same build together. initBashkit() uses wasm-bindgen's streaming loader when the server sends the correct MIME type. A slow mobile connection can take more than 15 seconds to download a multi-megabyte bundle, so choose startup deadlines from your application's measured network conditions.

The package includes build-info.json: source commit, dirty-checkout status, compiler/tools, Cargo profile/features, and the SHA-256 of the shipped WASM. Source archives without Git metadata record a null source identity. A source checkout between releases can have the same package version as an older npm release; compare the source commit as well as the version.

Maintainers can run the bounded compatibility and compressed-size gates:

node crates/bashkit-wasm/bench/compat.mjs
node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check

The browser benchmark methodology describes reproducible cold-start and execution experiments and the measured profile tradeoff.

Optional Python/TypeScript and host functions

Import @everruns/bashkit-wasm/runtimes for the runtime-enabled single-threaded bundle; the default import stays slim. Enable python/typescript explicitly and pass externalFunctions/externalHandler (Python) or typescriptExternalFunctions/typescriptExternalHandler (TypeScript). Callbacks support JSON values and an execution-local AbortSignal. Use async Bash.execute, with one active execution per instance. See the host function guide.

Build both variants with scripts/build.sh release, followed by BASHKIT_WASM_VARIANT=runtimes BASHKIT_WASM_FEATURES=terminal,python,typescript scripts/build.sh release.

版本与发布记录

@everruns/bashkit-wasm 共发布 15 个版本,首次发布于 2026/07/19;过去 12 个月发布了 15 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.21.0latest2026/11/113 天前
0.20.32026/11/059 天前
0.20.22026/10/3114 天前
0.20.12026/10/3015 天前
0.19.02026/10/1629 天前
0.18.22026/09/231 个月前

关于 @everruns/bashkit-wasm 的常见问题

@everruns/bashkit-wasm 是什么?

Sandboxed bash interpreter for the browser (WebAssembly). Single-threaded — no SharedArrayBuffer, no COOP/COEP headers.

如何安装 @everruns/bashkit-wasm?

在项目目录运行 npm install @everruns/bashkit-wasm,或使用 pnpm add / yarn add / bun add。

@everruns/bashkit-wasm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@everruns/bashkit-wasm 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@everruns/bashkit-wasm 可以商用吗?

@everruns/bashkit-wasm 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@everruns/bashkit-wasm 的下载量有多少?

根据 npm 官方下载统计,@everruns/bashkit-wasm 在 2026/10/14 – 2026/11/12 这 30 天内被下载 44,321 次,日均约 1,477 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@everruns/bashkit-wasm 还在维护吗?

最新版本 0.21.0 发布于 2026/11/11(3 天前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/07/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。