just-bash
A shell profile for the terminal, running in the page over the workbench's own files.
justBash contributes a shell to the terminal:
just-bash, a real but partial bash interpreter,
running in the page rather than on a server. It is just-bash and not the bash in /bin — a
profile named bash would be promising more than it delivers. It needs terminal alongside it
to have somewhere to appear.
There are no real processes behind it, no network unless you configure one, and no package
installs — it interprets a script over the workbench's own files, nothing more. For a real node
with npm install and a dev server, pair terminal with
WebContainer instead.
import { Workbench, FileSystem } from "codelet/workbench";
import { terminal } from "codelet/extensions/terminal";
import { justBash } from "codelet/extensions/terminal/just-bash";
const workbench = new Workbench({
parent: document.getElementById("app")!,
fs: new FileSystem({ "/README.md": "# Hello" }),
extensions: [terminal(), justBash()],
});#Options
| Option | Type | Default | Description |
|---|---|---|---|
workspace | boolean | true | Whether the shell is a shell over the workbench's files. false gives it only its own /tmp. |
mount | string | "/workspace" | Where the workbench's files are mounted, and where the shell opens when it has a workspace. |
files | Record<string, string> | — | What the in-memory filesystem around the mount starts with, by absolute path. |
env | Record<string, string> | — | Starting environment variables. |
cwd | string | mount, or just-bash's own default without a workspace | Where the shell opens. |
network | ShellNetwork \| false | every URL and every method | What curl, wget and ping may reach. See Network. |
history | string \| false | "codelet.just-bash.history" | The localStorage key command history is kept under. false turns off persistence. |
node | boolean \| NodeOptions | true | Whether node is registered, and how it handles .ts/.tsx/.jsx. See Running scripts with node. |
cdn | string | "https://esm.sh" | Where just-bash, and quickjs-wasi for node, are fetched from. |
#Try it
Type ls in the terminal: it lists the workbench's own files, mounted at /workspace. Run
cat greet.ts, then edit it from the shell with sed -i 's/hi/hey/' greet.ts — the greet.ts
tab above redraws with the change. Run node greet.ts to see it execute: node strips the
file's TypeScript before handing it to the engine. xterm, just-bash and sucrase (for node's
TypeScript) each load from a CDN on first use, so give it a moment.
#Files
By default the shell is a shell over the workbench's own files, mounted at /workspace (or
wherever mount says). ls is the tree the explorer draws, and sed -i is a file the editor
redraws — writes go through workspace.fs, the same as a save from the editor.
The mount sits inside a small in-memory filesystem rather than being the shell's whole /. A
/tmp the shell writes for itself (a heredoc, a pipeline's scratch file) stays there, outside
the mount, and never touches the workbench.
workspace: false gives the shell only that in-memory filesystem, with no mount at all: a demo
shell with nothing of the reader's in it.
#Commands
Beyond just-bash's own set, this registers:
open,code— open a file in a workbench tab. Given a directory, it reveals it in the explorer instead, since a directory has no tab. Given a URL, it opens a browser tab, which needs no host at all.vi,vim,nano— the same asopen, after creating the file first if it isn't there yet, since that's what opening an editor on a new path means.node— run a script; see Running scripts with node.realpath— resolve a path.yes— repeat a word ory. Bounded rather than infinite (-n, default 1000, capped at 100,000): a command here answers with its whole output at once, so an unbounded one would hang the tab.dd— copy bytes between a file, stdin and stdout, withbs,count,skipandseek.link,unlink— a hard link and its removal. The workbench's files support neither linking nor symlinks, solinkfails with the filesystem's own error.uname— reportsjust-bashas the kernel name, the page's own hostname, the pinned just-bash version, andcodeletas the OS.-misnavigator.platform, the one field a browser actually answers for the machine.id— the one user there is, uid and gid1000.uptime— how long the page has been open. No load averages: nothing in a page measures one.watch— reruns a command on an interval. Bounded passes (-c, default 3, capped at 20) rather than running until stopped, and no screen clearing between them.sudo— runs the rest of the line as the one user there is. No privileges to raise, so it exists only so a copied command line still runs.wget,ping— only registered whennetworkis configured; see Network.
#Network
curl (just-bash's own), plus wget and ping, reach whatever the page's CORS already lets it
reach. That's the whole answer to what a browser can fetch, so network only ever narrows it,
never widens it.
network: false removes all three commands: Tab completion doesn't offer them and they aren't in
the shell at all.
An object is an allow-list:
| Option | Type | Default | Description |
|---|---|---|---|
allowedUrlPrefixes | (string \| { url: string; transform?: RequestTransform[] })[] | — | Origins requests may reach, optionally with a path prefix, e.g. "https://api.example.com/v1/". Nothing else is reachable. |
allowedMethods | ("GET" \| "HEAD" \| "POST" \| "PUT" \| "DELETE" \| "PATCH" \| "OPTIONS")[] | GET, HEAD | Methods a request may use. |
dangerouslyAllowFullInternetAccess | boolean | — | Every URL and every method. |
maxRedirects | number | — | How many redirects a request follows. |
timeoutMs | number | — | How long a request waits before it's cut off. |
maxResponseSize | number | — | The largest response body allowed. |
denyPrivateRanges | boolean | false | just-bash's own SSRF guard. It works by resolving a host with node's dns, which doesn't exist in a browser, so it's off here regardless of just-bash's own default. |
Warning
dangerouslyAllowFullInternetAccess removes the allow-list entirely: every origin and every
method, limited only by what the page's own CORS already allows. This is what justBash()
defaults to when you don't pass network at all.
ping has no ICMP to work with in a browser. It's a timed HTTP GET through the same allow-list as
curl, and its output says so rather than claiming to send packets.
#Running scripts with node
node runs a .js file in QuickJS, in a worker of its own beside the page. A worker per run
rather than one kept alive is what makes ^C a terminate, and why a run leaves nothing behind
for the next one.
import and require resolve against the workbench's own files — the graph of everything a
script imports is read and resolved before the run starts, since QuickJS asks for a module
synchronously and reading the workbench's files is not.
By default node only reads .js. .ts, .tsx and .jsx need a transform to turn them into
something QuickJS can read:
import { justBash, typescript } from "codelet/extensions/terminal/just-bash";
justBash({ node: { transform: typescript() } });typescript() is a type stripper built on sucrase,
fetched off the same CDN the first time a .ts file is run. It strips types rather than
compiling: nothing is lowered, ESM is left as ESM, and JSX compiles to React.createElement by
default — a name, not a dependency, since a script under node has no node_modules to find a
real React in. A namespace is dropped rather than emitted, and a decorator is passed through
unchanged, which QuickJS then refuses.
Note
Both codelet/extensions/terminal/just-bash and codelet/extensions/lsp/typescript export a
function named typescript, for unrelated purposes — one strips types for node, the other runs
the TypeScript language service. Import both in the same file and alias one.
What node is not: there's no node:fs, no sockets, and no node_modules. It has console,
process, the timers, import and require, run over the workbench's own files and nothing
else.
#History
Lines typed into the shell are kept in localStorage, under the key history names — one
shell's own ("codelet.just-bash.history") unless you name another, or false for a shell whose
memory is only its own tab. Several tabs sharing a key share one history, the way several shells
on the same machine share a .bash_history: each loads it on open, and whichever tab wrote most
recently is what the next one sees.