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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
run: bash crates/bashkit-wasm/scripts/build.sh release

- name: Integration tests (headless Node)
run: node --test crates/bashkit-wasm/__test__/bashkit-wasm.test.mjs
run: node --test "crates/bashkit-wasm/__test__/*.test.mjs"

audit:
name: Audit
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
run: bash crates/bashkit-wasm/scripts/build.sh release

- name: Integration tests (headless Node)
run: node --test crates/bashkit-wasm/__test__/bashkit-wasm.test.mjs
run: node --test "crates/bashkit-wasm/__test__/*.test.mjs"

- name: Upload pkg
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/bashkit-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ serde_json = { workspace = true }
serde-wasm-bindgen = "0.6"
futures-util = { workspace = true }
send_wrapper = { version = "0.6", features = ["futures"] }
# Same clock shim bashkit uses internally, so `Metadata` timestamps built here
# unify with `bashkit::time_compat::SystemTime` on wasm32.
web-time = { workspace = true }
console_error_panic_hook = "0.1"
# getrandom's browser backend (matches the workspace pin). Enables `wasm_js`
# so the credential-placeholder RNG links on wasm32-unknown-unknown.
Expand Down
50 changes: 49 additions & 1 deletion crates/bashkit-wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ new Bash({
maxCommands, maxLoopIterations, maxMemory,
files: { "/config.json": '{"debug":true}' },
customBuiltins: { name: (ctx) => "..." },
fs: hostFileSystem, // host-backed filesystem; see below
});
```

Expand Down Expand Up @@ -167,6 +168,53 @@ 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.

```js
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`:

```ts
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`,
Expand Down Expand Up @@ -198,7 +246,7 @@ custom builtin (see above) so requests go through your app's own `fetch`.
```bash
# Build the bundle and run the headless integration tests:
bash scripts/build.sh
node --test __test__/bashkit-wasm.test.mjs
node --test "__test__/*.test.mjs"
# or, from the repo root:
just build-wasm
```
Expand Down
Loading