Skip to content
Open
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
38 changes: 36 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,15 @@ Input Script → Parser (src/parser/) → AST (src/ast/) → Interpreter (src/in
- Each command in its own directory with implementation + tests
- Registry pattern via `registry.ts`

**Filesystem** (`src/fs.ts`, `src/overlay-fs/`): In-memory VFS with optional overlay on real filesystem

**Filesystem** (`src/fs/`): In-memory VFS with pluggable backends

- `interface.ts` - `IFileSystem` interface all backends implement
- `in-memory-fs/` - Pure in-memory filesystem (default)
- `overlay-fs/` - Copy-on-write over a real directory (reads from disk, writes to memory)
- `read-write-fs/` - Direct read-write to a real directory
- `http-fs/` - Read-only filesystem backed by HTTP `fetch()`. Manifest-driven (file tree declared up front), lazy-fetches on first read, caches in memory. Zero dependencies.
- `mountable-fs/` - Compose multiple `IFileSystem` backends at different mount points
- `mount.ts` - `mount()` helper for concise filesystem composition
- `real-fs-utils.ts` - Shared security helpers for real-FS-backed implementations
- `OverlayFs` / `ReadWriteFs` - Both default to `allowSymlinks: false` (symlinks blocked)
- Symlink policy is enforced at central gate functions (`resolveAndValidate`, `validateRealPath_`) so new methods get protection automatically
Expand Down Expand Up @@ -181,6 +188,27 @@ When adding comparison tests:
3. Commit both the test file and the generated fixture JSON
4. If manually adjusting for Linux behavior, add `"locked": true` to the fixture

## Composing Filesystems with `mount()` and `HttpFs`

Use `mount()` to compose multiple `IFileSystem` backends into a unified namespace:

```typescript
import { Bash, mount, HttpFs } from "just-bash";

const fs = mount({
"/data": new HttpFs("https://cdn.example.com/dataset", [
"train.csv",
"test.csv",
]),
});
const bash = new Bash({ fs });
await bash.exec("cat /data/train.csv | wc -l");
```

`HttpFs` accepts a file list (array of paths or `Record<string, { size?: number }>`), an optional `fetch` function, and optional `headers`. Files are fetched lazily and cached. All write operations throw `EROFS`. Use `prefetch()` to eagerly load all files in parallel.

When `mount()` doesn't receive a `"/"` entry, it creates an `InMemoryFs` base pre-initialised with `/dev`, `/proc`, `/bin` so the shell works out of the box.

## Filesystem Security: Default-Deny Symlinks

`OverlayFs` and `ReadWriteFs` default to `allowSymlinks: false`. This means:
Expand All @@ -196,6 +224,12 @@ When adding comparison tests:

**In tests**: Pass `allowSymlinks: true` to the constructor when testing symlink behavior. The `cross-fs-no-symlinks.test.ts` file tests the default-deny behavior.

## Redirect Error Handling

All filesystem writes in the redirection system (`src/interpreter/redirections.ts`) go through `redirectWrite()` and `redirectAppend()` helpers. These catch FS exceptions and convert them to bash-style error messages (e.g. `bash: /path: Read-only file system`). This ensures read-only backends like `HttpFs` don't crash the interpreter when scripts attempt writes via redirections.

**When adding new redirect operators**: Use `redirectWrite()` / `redirectAppend()` for all FS writes. Never call `ctx.fs.writeFile()` or `ctx.fs.appendFile()` directly in redirection code.

## Development Guidelines

- Read AGENTS.md
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ const env = new Bash({ fs: rwfs });
await env.exec('echo "hello" > file.txt'); // writes to real filesystem
```

**HttpFs** - Read-only filesystem backed by HTTP. Files are declared up front and fetched lazily on first read, then cached in memory. No dependencies beyond `fetch`:

```typescript
import { Bash, HttpFs, mount } from "just-bash";

const fs = mount({
"/data": new HttpFs("https://cdn.example.com/dataset", [
"train.csv",
"test.csv",
"metadata.json",
]),
});

const bash = new Bash({ fs });

await bash.exec("wc -l /data/train.csv"); // fetches once, then cached
await bash.exec("cat /data/metadata.json | jq .name"); // reads from cache
await bash.exec("ls /data"); // from manifest, no network
await bash.exec("echo x > /data/new.txt"); // EROFS: read-only file system
```

You can pass custom headers (e.g. for auth) and a custom fetch function:

```typescript
const fs = new HttpFs("https://api.example.com/files", ["secret.json"], {
headers: { Authorization: "Bearer tok123" },
});
```

**MountableFs** - Mount multiple filesystems at different paths. Combines read-only and read-write filesystems into a unified namespace:

```typescript
Expand Down Expand Up @@ -188,6 +217,21 @@ const fs = new MountableFs({
});
```

**`mount()` helper** - Shorthand for composing filesystems. Automatically creates an initialised base `InMemoryFs` (with `/dev`, `/proc`, `/bin`) when you don't provide `"/"`:

```typescript
import { Bash, mount, HttpFs } from "just-bash";
import { OverlayFs } from "just-bash/fs/overlay-fs";

const fs = mount({
"/project": new OverlayFs({ root: "./my-project", readOnly: true }),
"/data": new HttpFs("https://cdn.example.com/dataset", ["train.csv", "test.csv"]),
});

const bash = new Bash({ fs });
await bash.exec("cat /data/train.csv | wc -l");
```

### AI SDK Tool

For AI agents, use [`bash-tool`](https://github.com/vercel-labs/bash-tool) which is optimized for just-bash and provides a ready-to-use [AI SDK](https://ai-sdk.dev/) tool:
Expand Down
2 changes: 1 addition & 1 deletion src/cli/just-bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("just-bash CLI", () => {
tempDir,
]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("EROFS");
expect(result.stderr).toContain("Read-only file system");
});

it("should block mkdir by default", () => {
Expand Down
Loading