explainshell compiled to WebAssembly, with a typed TypeScript API for Node.js and browsers.
The module is built with Rust (wasm32-unknown-unknown) via wasm-bindgen, exposing the Explainshell class (new(data), explain(command), manpage_count()). The manpage corpus is exported from explainshell's SQLite database to a MessagePack bundle at build time; both runtimes load the same artifacts.
- Node.js >= 20
- Rust via rvm-windows (
rvm use stable), with thewasm32-unknown-unknowntarget - wasm-bindgen-cli matching the
wasm-bindgencrate version (see Cargo.lock) - Python 3 +
msgpack(data export only) - The prebuilt
dist/artifacts (see Building), or build them yourself
npm install explainshell-wasmExplaining commands requires the manpage database. Since v0.1.1, the build automatically downloads the full database from explainshell's GitHub releases (the db-latest tag, ~20-30 MB MessagePack bundle covering thousands of manpages from Ubuntu/Arch). No manual setup required.
Data source priority (first match wins):
$EXPLAINSHELL_DBenvironment variable — explicit path to a localexplainshell.db./explainshell.dbin the repo root — local database file.explainshell-cache/— cached auto-downloaded database (gitignored)- Auto-download from GitHub releases — fetches newest
explainshell-*.db.zst, verifies SHA256, decompresses with zstd-wasm - CI fixture fallback (
test/fixtures/test.db) — 4 manpages only, with loud warning
To force the CI fixture (e.g., for minimal test builds):
EXPLAINSHELL_DB= npm run build # empty value skips auto-downloadLicense note: The database contains upstream manpage text (GPL, BSD, MIT, etc.). See LICENSE-DATABASE.md for redistribution terms. The source field in each entry identifies the originating package.
One-step release:
npm run release-patch # 0.1.0 -> 0.1.1
npm run release-minor # 0.1.0 -> 0.2.0
npm run release-major # 0.1.0 -> 1.0.0This runs tests, bumps the version, rebuilds dist/, pushes the tag, and publishes to npm.
For manual control:
npm run build
npm publishimport { createExplainshell, explain } from 'explainshell-wasm'
// One-shot
const result = await explain('tar -xvf archive.tar')
// Reusable instance (WASM module is instantiated once and reused)
const explainshell = await createExplainshell()
console.log('manpages:', explainshell.manpageCount())
const result2 = await explainshell.explain('sudo tar -xvf archive.tar')By default the loader resolves dist/wasm-node/explainshell.js and
dist/explainshell.data.msgpack relative to the package. Pass explicit paths when needed:
const explainshell = await createExplainshell({
dataPath: '/path/to/explainshell.data.msgpack',
})Serve dist/wasm-web/ and dist/explainshell.data.msgpack from the same origin with correct MIME types (application/wasm for .wasm, text/javascript for .js, application/octet-stream for .msgpack), then:
import { createExplainshell } from 'explainshell-wasm'
const explainshell = await createExplainshell({
runtime: 'browser',
glueUrl: '/wasm-web/explainshell.js',
wasmUrl: '/wasm-web/explainshell_bg.wasm',
dataUrl: '/explainshell.data.msgpack',
})
const result = await explainshell.explain(editorValue)
console.log(result)Creates (or returns the cached) explainshell instance, instantiating the WASM module on first call. Pass forceNew: true to bypass the cache.
await createExplainshell(options?: {
gluePath?: string; // node: path of the wasm-bindgen glue
dataPath?: string; // node: path of the .data.msgpack bundle
glueUrl?: string; // browser: URL of the wasm-bindgen glue
wasmUrl?: string; // browser: URL of the _bg.wasm module
dataUrl?: string; // browser: URL of the .data.msgpack bundle
runtime?: 'node' | 'browser' | 'auto'; // default 'auto'
forceNew?: boolean; // bypass cache (default false)
});await explain('git commit -m "msg"') // ExplainResultTerminates the cached instance. Mainly useful in tests.
interface ExplainOptions {
distro?: string // reserved for multi-distro bundles
release?: string // reserved for multi-distro bundles
}interface ExplainResult {
groups: MatchGroup[] // one "shell" group plus one per command
expansions: Expansion[]
}
interface MatchGroup {
name: string // e.g. "shell", "command1"
results: MatchResult[]
manpage?: ParsedManpage | null
suggestions?: ParsedManpage[]
error?: string | null
positional_index: number
}
interface MatchResult {
start: number
end: number
text?: string | null // help text, null when unknown
match_text?: string | null // matched input span
debug_info?: Record<string, unknown> | null
}explainshell.db (SQLite, built by the explainshell pipeline)
│ scripts/export_wasm_data.py (msgpack)
▼
dist/explainshell.data.msgpack (versioned bundle: manpages + mappings)
│ crates/{core,data,parse,match} (Rust)
│ cargo build --target wasm32-unknown-unknown
│ wasm-bindgen --target {nodejs,web}
▼
dist/wasm-{node,web}/explainshell.js (+ _bg.wasm)
│ src/runtime/{node,browser}.ts
▼
explain(command) → Promise<ExplainResult>
Key design points:
- Single entry point.
Explainshell.explain()takes a command string and returnsExplainResultJSON. The shell parser (bashlex port) and matcher run entirely inside the module. - In-memory data. Manpage lookup resolves against the bundled MessagePack data; there is no host filesystem or network access from WASM. Repeated calls share no mutable state.
- No WASI. The module is pure computation on
wasm32-unknown-unknown, so both runtimes use the stock wasm-bindgen glue with no WASI shim. - Cache discipline.
createExplainshellcaches by artifact path;forceNew: truebypasses it.
Requires Rust (see Requirements). Python is needed only for the data export step.
# Toolchain setup (Windows, one time)
# install https://github.com/MemoryClear/rvm-windows, then:
rvm use stable
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.128 --locked
pip install msgpack
npm run build:wasm # Rust -> wasm-bindgen -> data bundle, all in dist/
npm run build # build:wasm + TypeScript (via unbuild)scripts/build-wasm.mjs is cross-platform (Windows/Linux/macOS). The
wasm32-wasi target name from older docs is now wasm32-wasip1 in Rust;
this project does not use it (see "No WASI" above).
Auto-download: On first build (or when the upstream db-latest release updates), the script downloads ~100 MB compressed database, verifies SHA256, decompresses to .explainshell-cache/, and exports the MessagePack bundle. Subsequent builds use the cache. Set GITHUB_TOKEN env var for higher API rate limits (60/hr unauthenticated).
npm install # install JS dependencies
npm run build # builds WASM + TypeScript (via unbuild)
npm test # Node suites: unit + integration over HTTP
npm run test:browser # real headless Chromium via @vitest/browser + Playwright
npm run lint # Biome check
npm run lint:fix # Biome check --writeGit hooks (Husky + lint-staged) run Biome on staged JS/TS files at commit and the test suite on push.
| Suite | Environment | What it covers |
|---|---|---|
api.test.ts |
Node | Type shapes, response envelopes |
matcher.test.ts |
Node + built .wasm |
explain() groups, unknown-program errors, empty input |
browser-serve.test.ts |
Node + local HTTP | BrowserExplainshell fetching glue/wasm/data over HTTP |
browser.test.ts |
Real headless Chromium | Full browser path: fetch, instantiate, explain |
Integration suites skip automatically when dist/ is not built.
explainshell-wasm/
├── Cargo.toml # Rust workspace
├── crates/
│ ├── core/ # domain types (CliOption, ParsedManpage, ...)
│ ├── data/ # msgpack bundle loading + lookup
│ ├── parse/ # shell parser (bashlex port)
│ ├── match/ # matcher algorithm
│ └── wasm/ # wasm-bindgen exports (Explainshell)
├── src/
│ ├── index.ts # public entry point
│ ├── api.ts # createExplainshell / explain / resetExplainshell
│ ├── types.ts # ExplainOptions, ExplainResult, ...
│ ├── runtime/
│ │ ├── node.ts # Node loader (reads dist/ from disk)
│ │ ├── browser.ts # browser loader (fetch)
│ │ └── utils.ts # glue shape guards, response validation
│ └── __tests__/ # Vitest suites
├── test/
│ ├── fixtures/ # test.db + test.data.msgpack (CI fixture)
│ └── serve-dist.ts # globalSetup: serves dist/ to browser tests
├── scripts/ # build-wasm.mjs, export_wasm_data.py, ...
├── build.config.ts # unbuild configuration
├── vitest.config.ts # Node suites
└── vitest.browser.config.ts # Chromium suite
dist/ (compiled JS, .wasm, glue, data bundle, generated .d.ts) and target/ are gitignored build outputs.
- The default bundle is the 4-manpage CI fixture; real coverage needs a full
explainshell.db(see Data notice). - The data bundle for a full distro is ~20-30 MB; reuse the instance returned by
createExplainshell. - The matcher currently resolves commands to manpages; full token-level option matching lands with the bashlex port.
- Browser testing covers headless Chromium. Other engines should work (the module only needs post-MVP features all modern browsers ship), but they are not in the matrix.