Skip to content

Commit 5b82d24

Browse files
authored
Read-Write FS (#13)
* Read-Write FS * knip
1 parent 08c21ff commit 5b82d24

26 files changed

+1521
-205
lines changed

README.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Supports optional network access via `curl` with secure-by-default URL filtering
1616
- [Basic API](#basic-api)
1717
- [Configuration](#configuration)
1818
- [Custom Commands](#custom-commands)
19-
- [OverlayFs (Copy-on-Write)](#overlayfs-copy-on-write)
19+
- [Filesystem Options](#filesystem-options)
2020
- [AI SDK Tool](#ai-sdk-tool)
2121
- [Vercel Sandbox Compatible API](#vercel-sandbox-compatible-api)
2222
- [CLI Binary](#cli-binary)
@@ -91,31 +91,46 @@ const upper = defineCommand("upper", async (args, ctx) => {
9191

9292
const bash = new Bash({ customCommands: [hello, upper] });
9393

94-
await bash.exec("hello Alice"); // "Hello, Alice!\n"
95-
await bash.exec("echo 'test' | upper"); // "TEST\n"
94+
await bash.exec("hello Alice"); // "Hello, Alice!\n"
95+
await bash.exec("echo 'test' | upper"); // "TEST\n"
9696
```
9797

9898
Custom commands receive the full `CommandContext` with access to `fs`, `cwd`, `env`, `stdin`, and `exec` for running subcommands.
9999

100-
### OverlayFs (Copy-on-Write)
100+
### Filesystem Options
101101

102-
Seed the bash environment with files from a real directory. The agent can read but not write to the real filesystem - all changes stay in memory.
102+
Three filesystem implementations are available:
103+
104+
**InMemoryFs** (default) - Pure in-memory filesystem, no disk access:
105+
106+
```typescript
107+
import { Bash } from "just-bash";
108+
const env = new Bash(); // Uses InMemoryFs by default
109+
```
110+
111+
**OverlayFs** - Copy-on-write over a real directory. Reads come from disk, writes stay in memory:
103112

104113
```typescript
105-
import { Bash, OverlayFs } from "just-bash";
114+
import { Bash } from "just-bash";
115+
import { OverlayFs } from "just-bash/fs/overlay-fs";
106116

107-
// Files are mounted at /home/user/project by default
108117
const overlay = new OverlayFs({ root: "/path/to/project" });
109118
const env = new Bash({ fs: overlay, cwd: overlay.getMountPoint() });
110119

111-
// Reads come from the real filesystem
112-
await env.exec("cat package.json"); // reads /path/to/project/package.json
120+
await env.exec("cat package.json"); // reads from disk
121+
await env.exec('echo "modified" > package.json'); // stays in memory
122+
```
123+
124+
**ReadWriteFs** - Direct read-write access to a real directory. Use this if you want the agent to be agle to write to your disk:
125+
126+
```typescript
127+
import { Bash } from "just-bash";
128+
import { ReadWriteFs } from "just-bash/fs/read-write-fs";
113129

114-
// Writes stay in memory (real files unchanged)
115-
await env.exec('echo "modified" > package.json');
130+
const rwfs = new ReadWriteFs({ root: "/path/to/sandbox" });
131+
const env = new Bash({ fs: rwfs });
116132

117-
// Custom mount point
118-
const overlay2 = new OverlayFs({ root: "/path/to/project", mountPoint: "/" });
133+
await env.exec('echo "hello" > file.txt'); // writes to real filesystem
119134
```
120135

121136
### AI SDK Tool

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"build:cli": "esbuild dist/cli/just-bash.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bin --entry-names=[name] --chunk-names=chunks/[name]-[hash] --banner:js='#!/usr/bin/env node'",
6767
"build:shell": "esbuild dist/cli/shell.js --bundle --splitting --platform=node --format=esm --minify --outdir=dist/bin/shell --entry-names=[name] --chunk-names=chunks/[name]-[hash] --banner:js='#!/usr/bin/env node'",
6868
"prepublishOnly": "pnpm validate",
69-
"validate": "pnpm build && pnpm typecheck && pnpm lint && pnpm knip && pnpm test:run && pnpm test:dist",
69+
"validate": "pnpm lint && pnpm knip && pnpm typecheck && pnpm build && pnpm test:run && pnpm test:dist",
7070
"typecheck": "tsc --noEmit",
7171
"lint": "biome check .",
7272
"lint:fix": "biome check --write .",

src/Bash.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import {
1919
createLazyCustomCommand,
2020
isLazyCommand,
2121
} from "./custom-commands.js";
22-
import { type IFileSystem, VirtualFs } from "./fs.js";
23-
import type { InitialFiles } from "./fs-interface.js";
22+
import { InMemoryFs } from "./fs/in-memory-fs/in-memory-fs.js";
23+
import type { IFileSystem, InitialFiles } from "./fs/interface.js";
2424
import {
2525
ArithmeticError,
2626
ExecutionLimitError,
@@ -132,7 +132,7 @@ export class Bash {
132132
private state: InterpreterState;
133133

134134
constructor(options: BashOptions = {}) {
135-
const fs = options.fs ?? new VirtualFs(options.files);
135+
const fs = options.fs ?? new InMemoryFs(options.files);
136136
this.fs = fs;
137137

138138
this.useDefaultLayout = !options.cwd && !options.files;
@@ -195,8 +195,8 @@ export class Bash {
195195
loopDepth: 0,
196196
};
197197

198-
// Create essential directories for VirtualFs
199-
if (fs instanceof VirtualFs) {
198+
// Create essential directories for InMemoryFs
199+
if (fs instanceof InMemoryFs) {
200200
try {
201201
// Always create /bin for PATH-based command resolution
202202
fs.mkdirSync("/bin", { recursive: true });
@@ -211,7 +211,7 @@ export class Bash {
211211
}
212212
}
213213

214-
if (cwd !== "/" && fs instanceof VirtualFs) {
214+
if (cwd !== "/" && fs instanceof InMemoryFs) {
215215
try {
216216
fs.mkdirSync(cwd, { recursive: true });
217217
} catch {
@@ -245,7 +245,7 @@ export class Bash {
245245
registerCommand(command: Command): void {
246246
this.commands.set(command.name, command);
247247
// Always create command stubs in /bin for PATH-based resolution
248-
if (this.fs instanceof VirtualFs) {
248+
if (this.fs instanceof InMemoryFs) {
249249
try {
250250
this.fs.writeFileSync(
251251
`/bin/${command.name}`,

src/ai/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type Tool, tool, zodSchema } from "ai";
22
import { z } from "zod";
33
import { Bash, type BashOptions } from "../Bash.js";
44
import type { CommandName } from "../commands/registry.js";
5-
import type { IFileSystem, InitialFiles } from "../fs-interface.js";
5+
import type { IFileSystem, InitialFiles } from "../fs/interface.js";
66

77
type BashToolInput = { command: string };
88
type BashToolOutput = { stdout: string; stderr: string; exitCode: number };

src/cli/just-bash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
import { resolve } from "node:path";
4141
import { Bash } from "../Bash.js";
42-
import { OverlayFs } from "../overlay-fs/index.js";
42+
import { OverlayFs } from "../fs/overlay-fs/index.js";
4343

4444
interface CliOptions {
4545
script?: string;
Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { describe, expect, it } from "vitest";
2-
import { VirtualFs } from "./fs.js";
2+
import { InMemoryFs } from "./in-memory-fs.js";
33

4-
describe("VirtualFs Buffer and Encoding Support", () => {
4+
describe("InMemoryFs Buffer and Encoding Support", () => {
55
describe("basic Buffer operations", () => {
66
it("should write and read Uint8Array", async () => {
7-
const fs = new VirtualFs();
7+
const fs = new InMemoryFs();
88
const data = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
99

1010
await fs.writeFile("/binary.bin", data);
@@ -14,7 +14,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
1414
});
1515

1616
it("should write Uint8Array and read as string", async () => {
17-
const fs = new VirtualFs();
17+
const fs = new InMemoryFs();
1818
const data = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
1919

2020
await fs.writeFile("/test.txt", data);
@@ -24,7 +24,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
2424
});
2525

2626
it("should write string and read as Uint8Array", async () => {
27-
const fs = new VirtualFs();
27+
const fs = new InMemoryFs();
2828

2929
await fs.writeFile("/test.txt", "Hello");
3030
const result = await fs.readFileBuffer("/test.txt");
@@ -33,7 +33,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
3333
});
3434

3535
it("should handle binary data with null bytes", async () => {
36-
const fs = new VirtualFs();
36+
const fs = new InMemoryFs();
3737
const data = new Uint8Array([0x00, 0x01, 0x00, 0xff, 0x00]);
3838

3939
await fs.writeFile("/binary.bin", data);
@@ -43,7 +43,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
4343
});
4444

4545
it("should calculate correct size for binary files", async () => {
46-
const fs = new VirtualFs();
46+
const fs = new InMemoryFs();
4747
const data = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04]);
4848

4949
await fs.writeFile("/binary.bin", data);
@@ -55,7 +55,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
5555

5656
describe("encoding support", () => {
5757
it("should write and read with utf8 encoding", async () => {
58-
const fs = new VirtualFs();
58+
const fs = new InMemoryFs();
5959

6060
await fs.writeFile("/test.txt", "Hello 世界", "utf8");
6161
const result = await fs.readFile("/test.txt", "utf8");
@@ -64,7 +64,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
6464
});
6565

6666
it("should write and read with base64 encoding", async () => {
67-
const fs = new VirtualFs();
67+
const fs = new InMemoryFs();
6868

6969
// "Hello" in base64 is "SGVsbG8="
7070
await fs.writeFile("/test.txt", "SGVsbG8=", "base64");
@@ -74,7 +74,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
7474
});
7575

7676
it("should read as base64", async () => {
77-
const fs = new VirtualFs();
77+
const fs = new InMemoryFs();
7878

7979
await fs.writeFile("/test.txt", "Hello");
8080
const result = await fs.readFile("/test.txt", "base64");
@@ -83,7 +83,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
8383
});
8484

8585
it("should write and read with hex encoding", async () => {
86-
const fs = new VirtualFs();
86+
const fs = new InMemoryFs();
8787

8888
// "Hello" in hex is "48656c6c6f"
8989
await fs.writeFile("/test.txt", "48656c6c6f", "hex");
@@ -93,7 +93,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
9393
});
9494

9595
it("should read as hex", async () => {
96-
const fs = new VirtualFs();
96+
const fs = new InMemoryFs();
9797

9898
await fs.writeFile("/test.txt", "Hello");
9999
const result = await fs.readFile("/test.txt", "hex");
@@ -102,7 +102,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
102102
});
103103

104104
it("should write with latin1 encoding", async () => {
105-
const fs = new VirtualFs();
105+
const fs = new InMemoryFs();
106106

107107
// Latin1 character é is 0xe9
108108
await fs.writeFile("/test.txt", "café", "latin1");
@@ -112,7 +112,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
112112
});
113113

114114
it("should support encoding in options object", async () => {
115-
const fs = new VirtualFs();
115+
const fs = new InMemoryFs();
116116

117117
await fs.writeFile("/test.txt", "SGVsbG8=", { encoding: "base64" });
118118
const result = await fs.readFile("/test.txt", { encoding: "utf8" });
@@ -123,7 +123,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
123123

124124
describe("appendFile with Buffer", () => {
125125
it("should append Uint8Array to existing file", async () => {
126-
const fs = new VirtualFs();
126+
const fs = new InMemoryFs();
127127

128128
await fs.writeFile("/test.txt", "Hello");
129129
await fs.appendFile(
@@ -136,7 +136,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
136136
});
137137

138138
it("should append string to file with Buffer content", async () => {
139-
const fs = new VirtualFs();
139+
const fs = new InMemoryFs();
140140
const initial = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
141141

142142
await fs.writeFile("/test.txt", initial);
@@ -147,7 +147,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
147147
});
148148

149149
it("should append with encoding", async () => {
150-
const fs = new VirtualFs();
150+
const fs = new InMemoryFs();
151151

152152
await fs.writeFile("/test.txt", "Hello");
153153
// " World" in base64 is "IFdvcmxk"
@@ -160,7 +160,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
160160

161161
describe("constructor with Buffer content", () => {
162162
it("should initialize files with Uint8Array content", async () => {
163-
const fs = new VirtualFs({
163+
const fs = new InMemoryFs({
164164
"/binary.bin": new Uint8Array([0x00, 0x01, 0x02]),
165165
"/text.txt": "Hello",
166166
});
@@ -175,7 +175,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
175175

176176
describe("edge cases", () => {
177177
it("should handle empty Uint8Array", async () => {
178-
const fs = new VirtualFs();
178+
const fs = new InMemoryFs();
179179

180180
await fs.writeFile("/empty.bin", new Uint8Array(0));
181181
const result = await fs.readFileBuffer("/empty.bin");
@@ -185,7 +185,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
185185
});
186186

187187
it("should handle large binary files", async () => {
188-
const fs = new VirtualFs();
188+
const fs = new InMemoryFs();
189189
const size = 1024 * 1024; // 1MB
190190
const data = new Uint8Array(size);
191191
for (let i = 0; i < size; i++) {
@@ -202,7 +202,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
202202
});
203203

204204
it("should preserve binary content through copy", async () => {
205-
const fs = new VirtualFs();
205+
const fs = new InMemoryFs();
206206
const data = new Uint8Array([0x00, 0xff, 0x00, 0xff]);
207207

208208
await fs.writeFile("/src.bin", data);
@@ -213,7 +213,7 @@ describe("VirtualFs Buffer and Encoding Support", () => {
213213
});
214214

215215
it("should follow symlinks for binary files", async () => {
216-
const fs = new VirtualFs();
216+
const fs = new InMemoryFs();
217217
const data = new Uint8Array([0x48, 0x69]);
218218

219219
await fs.writeFile("/real.bin", data);

0 commit comments

Comments
 (0)