@stablemodels/durable-bash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/final-review.md +20 -0
- package/.github/workflows/ci.yml +20 -0
- package/.github/workflows/publish.yml +28 -0
- package/CLAUDE.md +62 -0
- package/README.md +97 -0
- package/biome.json +25 -0
- package/bun.lock +442 -0
- package/bunfig.toml +2 -0
- package/dist/durable-fs.d.ts +88 -0
- package/dist/durable-fs.d.ts.map +1 -0
- package/dist/durable-fs.js +175 -0
- package/dist/durable-fs.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs-object.d.ts +68 -0
- package/dist/fs-object.d.ts.map +1 -0
- package/dist/fs-object.js +455 -0
- package/dist/fs-object.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -0
- package/src/durable-fs.ts +249 -0
- package/src/errors.ts +21 -0
- package/src/fs-object.ts +603 -0
- package/src/index.ts +12 -0
- package/src/types.ts +16 -0
- package/tests/durable-fs.test.ts +356 -0
- package/tests/fs-object.test.ts +435 -0
- package/tests/helpers.ts +105 -0
- package/tests/integration.test.ts +189 -0
- package/tests/just-bash.test.ts +85 -0
- package/tests/setup.ts +14 -0
- package/tsconfig.json +22 -0
- package/wrangler.toml +13 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Review all changes in the current branch compared to main and automatically bump the package version.
|
|
2
|
+
|
|
3
|
+
## Steps
|
|
4
|
+
|
|
5
|
+
1. **Diff analysis**: Run `git diff main...HEAD` to see all changes in this branch. Also run `git log --oneline main..HEAD` to understand the commit history.
|
|
6
|
+
|
|
7
|
+
2. **Determine version bump**:
|
|
8
|
+
- **patch** (e.g. 0.1.0 → 0.1.1): Bug fixes, documentation changes, refactors, test additions, CI changes — anything that doesn't change the public API.
|
|
9
|
+
- **minor** (e.g. 0.1.0 → 0.2.0): New features, new exports, new methods on public classes, new options added to existing methods — any additive change to the public API.
|
|
10
|
+
- Never bump major unless explicitly told to.
|
|
11
|
+
|
|
12
|
+
3. **Review the changes**: Provide a concise summary of what changed, organized by category (features, fixes, docs, tests, CI, etc.). Flag any concerns.
|
|
13
|
+
|
|
14
|
+
4. **Bump the version**: Run `npm version <patch|minor> --no-git-tag-version` to update package.json. Do NOT create a git tag — the version in package.json is what npm publish uses.
|
|
15
|
+
|
|
16
|
+
5. **Verify**: Run `bun test && bun run check && bun run build` to make sure everything still passes.
|
|
17
|
+
|
|
18
|
+
6. **Commit**: Stage the package.json change and commit with message: `chore: bump version to <new-version>`.
|
|
19
|
+
|
|
20
|
+
7. **Report**: Show the old version, new version, bump type, and the reasoning for the bump decision.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: oven-sh/setup-bun@v2
|
|
15
|
+
with:
|
|
16
|
+
bun-version: latest
|
|
17
|
+
- run: bun install --frozen-lockfile
|
|
18
|
+
- run: bun run check
|
|
19
|
+
- run: bun run build
|
|
20
|
+
- run: bun test
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: oven-sh/setup-bun@v2
|
|
16
|
+
with:
|
|
17
|
+
bun-version: latest
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 20
|
|
21
|
+
registry-url: https://registry.npmjs.org
|
|
22
|
+
- run: bun install --frozen-lockfile
|
|
23
|
+
- run: bun run check
|
|
24
|
+
- run: bun run build
|
|
25
|
+
- run: bun test
|
|
26
|
+
- run: npm publish --provenance --access public
|
|
27
|
+
env:
|
|
28
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# durable-bash
|
|
2
|
+
|
|
3
|
+
Cloudflare Durable Object-backed `IFileSystem` adapter for `just-bash`.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Two core classes, one adapter pattern:
|
|
8
|
+
|
|
9
|
+
- **`FsObject`** (`src/fs-object.ts`) — Durable Object with SQLite storage. Single `files` table stores paths, content (BLOB), metadata, and optional symlink targets. All methods are **synchronous** and exposed via RPC. This is where all filesystem logic lives.
|
|
10
|
+
- **`DurableFs`** (`src/durable-fs.ts`) — Thin adapter implementing `IFileSystem` from `just-bash`. Every method delegates to `FsObject` via a DO stub. Maintains a `Set<string>` cache for the synchronous `getAllPaths()` method. Use the static `DurableFs.create(namespace, name)` factory for construction — it handles stub creation and initial cache sync.
|
|
11
|
+
- **`src/errors.ts`** — `FsError` class and factory functions: `ENOENT`, `EEXIST`, `EISDIR`, `ENOTDIR`, `ENOTEMPTY`.
|
|
12
|
+
- **`src/types.ts`** — Wire-format types for RPC: `FsStatData`, `DirentData`.
|
|
13
|
+
- **`src/index.ts`** — Public API re-exports. Two entry points: `durable-bash` (adapter + errors + types) and `durable-bash/object` (DO class).
|
|
14
|
+
|
|
15
|
+
## Important Points
|
|
16
|
+
|
|
17
|
+
- **`IFileSystem` uses boolean properties**, not methods: `stat.isFile`, not `stat.isFile()`. `FsStat` and `DirentEntry` in `durable-fs.ts` define these shapes.
|
|
18
|
+
- **`getAllPaths()` is synchronous** but the DO is async. Solved with eager cache sync (handled by `DurableFs.create()`) + `Set` cache. `addToCache` walks ancestor dirs so auto-created parents are included.
|
|
19
|
+
- **Auto-parent creation**: `writeFile("/a/b/c.txt", ...)` auto-creates `/a` and `/a/b` via `ensureParentDirs` using `INSERT OR IGNORE`.
|
|
20
|
+
- **Symlinks**: stored as rows with `symlink_target` set. `resolveSymlinks()` follows chains up to 20 levels. `readFile`/`stat` follow symlinks; `lstat` does not.
|
|
21
|
+
- **`link()` copies content** at call time (not POSIX shared-inode semantics). This is by design per `IFileSystem` contract.
|
|
22
|
+
- **Content stored as BLOB** — `toBytes()` encodes strings to UTF-8 `Uint8Array`; binary is stored as-is.
|
|
23
|
+
- Mode constants: `DIR_MODE = 0o755`, `FILE_MODE = 0o644`, `SYMLINK_MODE = 0o777`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun test # all tests (121 tests across 4 files)
|
|
29
|
+
bun run test:unit # unit tests only (fs-object + durable-fs)
|
|
30
|
+
bun run test:integration # integration + smoke tests
|
|
31
|
+
bun run build # tsc → dist/
|
|
32
|
+
bun run check # biome lint
|
|
33
|
+
bun run format # biome format (tabs, write)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Testing
|
|
37
|
+
|
|
38
|
+
Tests use `bun:test` with `bun:sqlite` to simulate DO SQLite storage. The `cloudflare:workers` module is mocked via `tests/setup.ts` (preloaded in `bunfig.toml`).
|
|
39
|
+
|
|
40
|
+
- `tests/helpers.ts` — shared `createMockState()`, `createTestFsObject()`, `createDirectStub()` (Proxy wrapping sync methods as Promises for RPC simulation)
|
|
41
|
+
- `tests/fs-object.test.ts` — Unit tests for FsObject
|
|
42
|
+
- `tests/durable-fs.test.ts` — Unit tests for DurableFs with mock stub
|
|
43
|
+
- `tests/integration.test.ts` — DurableFs + FsObject wired together
|
|
44
|
+
- `tests/just-bash.test.ts` — Bash commands through DurableFs
|
|
45
|
+
|
|
46
|
+
## Extending the Package
|
|
47
|
+
|
|
48
|
+
**Adding a new filesystem operation:**
|
|
49
|
+
1. Add the synchronous method to `FsObject` in `src/fs-object.ts`
|
|
50
|
+
2. Add the async wrapper in `DurableFs` in `src/durable-fs.ts` — resolve paths with `this.resolve()`, call `this.stub.<method>()`, update cache with `addToCache`/`removeFromCache` if paths change
|
|
51
|
+
3. If new wire-format types are needed, add them to `src/types.ts`
|
|
52
|
+
4. Export any new public types from `src/index.ts`
|
|
53
|
+
5. Add tests to `tests/fs-object.test.ts` (unit) and `tests/integration.test.ts` (e2e)
|
|
54
|
+
|
|
55
|
+
**Adding a new error code:**
|
|
56
|
+
1. Add a factory function in `src/errors.ts` following the `ENOENT` pattern
|
|
57
|
+
|
|
58
|
+
**Key conventions:**
|
|
59
|
+
- Paths are always normalized (leading `/`, no trailing `/`, `.`/`..` resolved)
|
|
60
|
+
- `FsObject` methods are synchronous — no async/await inside the DO
|
|
61
|
+
- `DurableFs` cache must stay in sync: use `addToCache` for creates, `removeFromCache` for deletes, `sync()` for bulk operations like `cp`/`mv`
|
|
62
|
+
- Biome enforces tab indentation and recommended lint rules
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# durable-bash
|
|
2
|
+
|
|
3
|
+
Cloudflare Durable Object-backed [`IFileSystem`](https://www.npmjs.com/package/just-bash) adapter for [just-bash](https://www.npmjs.com/package/just-bash). Lets bash commands persist files in a Durable Object's SQLite storage.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @stablemodels/durable-bash
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `just-bash`, `@cloudflare/workers-types`
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Bash } from "just-bash";
|
|
17
|
+
import { DurableFs } from "@stablemodels/durable-bash";
|
|
18
|
+
|
|
19
|
+
// In a Cloudflare Worker:
|
|
20
|
+
const fs = await DurableFs.create(env.FS, "my-agent");
|
|
21
|
+
const bash = new Bash({ fs, cwd: "/home" });
|
|
22
|
+
const result = await bash.exec('echo "hello" > greeting.txt && cat greeting.txt');
|
|
23
|
+
console.log(result.stdout); // "hello\n"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Wrangler config
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[[durable_objects.bindings]]
|
|
30
|
+
name = "FS"
|
|
31
|
+
class_name = "FsObject"
|
|
32
|
+
|
|
33
|
+
[[migrations]]
|
|
34
|
+
tag = "v1"
|
|
35
|
+
new_sqlite_classes = ["FsObject"]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Re-export the DO class from your worker:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
export { FsObject } from "@stablemodels/durable-bash/object";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
`DurableFs` implements `IFileSystem` from `just-bash`:
|
|
47
|
+
|
|
48
|
+
- `readFile(path)` / `readFileBuffer(path)` — Read file contents
|
|
49
|
+
- `writeFile(path, content)` / `appendFile(path, content)` — Write/append
|
|
50
|
+
- `exists(path)` / `stat(path)` / `lstat(path)` — Check existence and metadata
|
|
51
|
+
- `mkdir(path, opts?)` / `readdir(path)` / `readdirWithFileTypes(path)` — Directories
|
|
52
|
+
- `rm(path, opts?)` / `cp(src, dest, opts?)` / `mv(src, dest)` — File operations
|
|
53
|
+
- `chmod(path, mode)` / `utimes(path, atime, mtime)` — Metadata changes
|
|
54
|
+
- `symlink(target, linkPath)` / `link(existing, new)` / `readlink(path)` / `realpath(path)` — Links
|
|
55
|
+
- `static create(namespace, name, cwd?)` — Factory: creates stub, syncs cache, returns ready instance
|
|
56
|
+
- `getAllPaths()` — Synchronous cached path list for glob matching
|
|
57
|
+
|
|
58
|
+
## Exports
|
|
59
|
+
|
|
60
|
+
| Export | Path | Description |
|
|
61
|
+
|--------|------|-------------|
|
|
62
|
+
| `DurableFs` | `@stablemodels/durable-bash` | `IFileSystem` adapter |
|
|
63
|
+
| `FsObject` | `@stablemodels/durable-bash/object` | Durable Object class — re-export in your worker |
|
|
64
|
+
| `FsError`, `ENOENT`, `EEXIST`, `EISDIR`, `ENOTDIR`, `ENOTEMPTY` | `@stablemodels/durable-bash` | POSIX-style errors |
|
|
65
|
+
| `FsStatData`, `DirentData` | `@stablemodels/durable-bash` | RPC wire-format types |
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bun install
|
|
71
|
+
bun test # all 121 tests
|
|
72
|
+
bun run test:unit # unit tests only
|
|
73
|
+
bun run test:integration # integration + smoke tests
|
|
74
|
+
bun run build # tsc → dist/
|
|
75
|
+
bun run check # biome lint
|
|
76
|
+
bun run format # biome format
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Test structure
|
|
80
|
+
|
|
81
|
+
| File | Scope |
|
|
82
|
+
|------|-------|
|
|
83
|
+
| `tests/fs-object.test.ts` | Unit tests for `FsObject` DO |
|
|
84
|
+
| `tests/durable-fs.test.ts` | Unit tests for `DurableFs` (mock stub) |
|
|
85
|
+
| `tests/integration.test.ts` | End-to-end: `DurableFs` + `FsObject` |
|
|
86
|
+
| `tests/just-bash.test.ts` | Bash commands through `DurableFs` |
|
|
87
|
+
|
|
88
|
+
Tests mock `cloudflare:workers` via a preload script (`tests/setup.ts`, configured in `bunfig.toml`) and use `bun:sqlite` to simulate DO SQLite storage.
|
|
89
|
+
|
|
90
|
+
## CI/CD
|
|
91
|
+
|
|
92
|
+
- **PR checks**: Lint, build, and tests run automatically on pull requests via GitHub Actions.
|
|
93
|
+
- **NPM publishing**: Triggered automatically on merge to `main`. Requires an `NPM_TOKEN` secret in the repository/org settings.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
package/biome.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"files": {
|
|
4
|
+
"ignore": ["dist/**"]
|
|
5
|
+
},
|
|
6
|
+
"organizeImports": {
|
|
7
|
+
"enabled": true
|
|
8
|
+
},
|
|
9
|
+
"linter": {
|
|
10
|
+
"enabled": true,
|
|
11
|
+
"rules": {
|
|
12
|
+
"recommended": true,
|
|
13
|
+
"suspicious": {
|
|
14
|
+
"noExplicitAny": "off"
|
|
15
|
+
},
|
|
16
|
+
"style": {
|
|
17
|
+
"noNonNullAssertion": "off"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"formatter": {
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"indentStyle": "tab"
|
|
24
|
+
}
|
|
25
|
+
}
|