Codelet logoCodelet

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

OptionTypeDefaultDescription
workspacebooleantrueWhether the shell is a shell over the workbench's files. false gives it only its own /tmp.
mountstring"/workspace"Where the workbench's files are mounted, and where the shell opens when it has a workspace.
filesRecord<string, string>What the in-memory filesystem around the mount starts with, by absolute path.
envRecord<string, string>Starting environment variables.
cwdstringmount, or just-bash's own default without a workspaceWhere the shell opens.
networkShellNetwork \| falseevery URL and every methodWhat curl, wget and ping may reach. See Network.
historystring \| false"codelet.just-bash.history"The localStorage key command history is kept under. false turns off persistence.
nodeboolean \| NodeOptionstrueWhether node is registered, and how it handles .ts/.tsx/.jsx. See Running scripts with node.
cdnstring"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 as open, 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 or y. 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, with bs, count, skip and seek.
  • link, unlink — a hard link and its removal. The workbench's files support neither linking nor symlinks, so link fails with the filesystem's own error.
  • uname — reports just-bash as the kernel name, the page's own hostname, the pinned just-bash version, and codelet as the OS. -m is navigator.platform, the one field a browser actually answers for the machine.
  • id — the one user there is, uid and gid 1000.
  • 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 when network is 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:

OptionTypeDefaultDescription
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, HEADMethods a request may use.
dangerouslyAllowFullInternetAccessbooleanEvery URL and every method.
maxRedirectsnumberHow many redirects a request follows.
timeoutMsnumberHow long a request waits before it's cut off.
maxResponseSizenumberThe largest response body allowed.
denyPrivateRangesbooleanfalsejust-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.

Read more in Extensions > Terminal.