
# just-bash

`justBash` contributes a shell to the [terminal](/extensions/terminal):
[just-bash](https://www.npmjs.com/package/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](/extensions/webcontainer) instead.

```ts
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](#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](#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.

::codelet-playground{mode="workbench" extensions="terminal" view="terminal" active="greet.ts" height="480"}

```ts [greet.ts]
export const greet = (name: string) => `hi ${name}`;

console.log(greet("codelet"));
```

```md [README.md]
# just-bash demo

A shell over these files.
```

::

## 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](#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).

## 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:

```ts
import { justBash, typescript } from "codelet/extensions/terminal/just-bash";

justBash({ node: { transform: typescript() } });
```

`typescript()` is a type stripper built on [sucrase](https://github.com/alangpierce/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{to="/extensions/terminal"}
