npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codebolt/shell

v0.1.0

Published

Pure-JS virtual filesystem, full POSIX command set, and complete bash interpreter that runs in Cloudflare Workers, browsers, Node, Bun — anywhere V8 runs. No native bindings, no subprocess spawning, no WASM.

Readme

codeboltshell

A pure-JS virtual filesystem, POSIX command set, and full bash interpreter that runs in Cloudflare Workers, browsers, Node, Bun — anywhere V8 runs. No native bindings, no subprocess spawning, no WASM. Compose backends, search, edit, run git, run bash scripts, all behind one Vm object.

import { Vm } from "@codebolt/shell";

const vm = new Vm();

// Direct FileSystem
await vm.writeFile("/hello.txt", "world");

// POSIX commands (vendored from just-bash, no dep on it)
await vm.cmd.grep(["-rn", "TODO", "/src"]);
await vm.cmd.find(["/", "-name", "*.txt"]);

// Full bash interpreter — pipes, control flow, expansions, the lot
const r = await vm.bash.exec(`
  for f in /src/*.txt; do
    grep TODO "$f"
  done | wc -l
`);
console.log(r.stdout); // "3\n"

// Git — backed by isomorphic-git over the same FileSystem
await vm.git.init();
await vm.git.add({ filepath: "hello.txt" });
await vm.git.commit({
  message: "first",
  author: { name: "you", email: "[email protected]" }
});

Why it exists

Most "virtual filesystem" libraries fall into one of two camps:

  1. Userspace OS sandboxes (agent-os, secure-exec, WebContainers) — try to recreate POSIX so the agent can run real binaries. Heavy. Don't run on Workers.
  2. Edge-only typed APIs (@cloudflare/shell Workspace) — a curated state.* surface that runs on Workers but is tied to Cloudflare's bindings.

codeboltshell is the third option: a small, in-process library that exposes shell-shaped tools (fs, search, edit, git) over a virtual filesystem with pluggable backends, runs anywhere V8 runs, and has no external dependencies on a specific cloud.

The Vm is a thin facade — every method is implemented against a FileSystem interface that has multiple backends (in-memory, SQL, overlay, mountable, chunked) you can mix freely. The same agent code runs against an in-memory FS in tests, a Durable-Object-SQL FS in production, an overlay FS for per-agent isolation — without any code changes above the FS layer.

Install

npm install @codebolt/shell
import { Vm } from "@codebolt/shell";

Four runtime dependencies, all pure JS and Worker-safe:

  • diff — for vm.diff and the vendored vm.cmd.diff command
  • isomorphic-git — for vm.git
  • sprintf-js — for vm.cmd.printf (vendored from just-bash)
  • minimatch — for vm.cmd.ls's glob ignore patterns

No native bindings, no WASM.

The Vm

One object, every capability hangs off it:

import { Vm } from "@codebolt/shell";

const vm = new Vm({
  // Optional: provide your own FileSystem. Defaults to a fresh InMemoryFs.
  fs: someFileSystem,
  // Optional: seed the default InMemoryFs with files (only used when fs is omitted).
  files: { "/seed.txt": "hello" }
});

Construction is synchronous. Per-operation methods are async so the same code works with backends that have real I/O (D1, R2, S3) without a contract change. Spinning up a fresh Vm per request, per session, or per agent task is cheap — the constructor allocates a Map.

Filesystem

Reading

await vm.readFile(path);              // string | null
await vm.readFileBytes(path);         // Uint8Array | null
await vm.readFileStream(path);        // ReadableStream<Uint8Array> | null

await vm.exists(path);                // boolean
await vm.stat(path);                  // { type, size, mtime } | null
await vm.lstat(path);                 // same as stat unless backend has symlinks
await vm.readdir(dir);                // [{ name, type }, ...]

readFile returns null for missing files (not an exception). Reads that should fail loudly are explicit at the call site.

Writing

await vm.writeFile(path, "text");                            // string
await vm.writeFileBytes(path, new Uint8Array([0xff, 0xfe])); // raw bytes
await vm.writeFileStream(path, readableStream);              // streaming

await vm.mkdir(path, { recursive: true });
await vm.rm(path, { recursive: true, force: true });

All write operations auto-create parent directories. There's no "missing parent" failure mode.

Manipulating

await vm.cp(src, dest, { recursive: true });
await vm.mv(src, dest);
await vm.glob("/src/**/*.ts");        // string[]

glob supports *, ?, ** (across segments), and the standard /**/ and trailing /** semantics from minimatch.

Streaming

readFileStream and writeFileStream use the Web Streams API. They're real streaming on ChunkedVFS (which backs SqlFs) — chunks are read from / written to the BlockStore one at a time, never holding the whole file in memory. On InMemoryFs they wrap the underlying buffer in a single-chunk stream (correct, but no memory savings since the data is already in RAM).

// Pipe a request body straight into the FS
await vm.writeFileStream("/uploads/file.bin", request.body);

// Stream a file straight back as the response
const stream = await vm.readFileStream("/big-export.csv");
return new Response(stream, { headers: { "content-type": "text/csv" } });

// Pipe through any TransformStream
const compressed = (await vm.readFileStream("/source.bin"))!
  .pipeThrough(new CompressionStream("gzip"));
await vm.writeFileStream("/source.bin.gz", compressed);

Backends

The Vm's fs field is a FileSystem. Five backends ship in the box; they all implement the same interface and compose freely.

InMemoryFs

The simplest. Map<path, Uint8Array> plus a Set<path> for directories. Cheap to construct, ephemeral, no I/O.

import { InMemoryFs } from "@codebolt/shell";

const fs = new InMemoryFs({
  "/seed.txt": "hello",      // optional seed files
  "/src/index.ts": "..."
});
const vm = new Vm({ fs });

Use for: tests, scratch sandboxes, the upper layer of an OverlayFs, the default in new Vm().

OverlayFs — copy-on-write

Wraps a lower (read-only) and an upper (writable) FileSystem. Reads check upper first, fall through to lower. Writes always go to upper. Deletes are tombstoned in an in-memory Set so the lower's copy doesn't reappear.

import { OverlayFs, InMemoryFs } from "@codebolt/shell";

// One persistent project, two agents working on it independently.
const project = persistentFs;        // shared, read-mostly

const vmA = new Vm({
  fs: new OverlayFs({ lower: project, upper: new InMemoryFs() })
});
const vmB = new Vm({
  fs: new OverlayFs({ lower: project, upper: new InMemoryFs() })
});

// vmA and vmB can both edit the same files; their writes never collide;
// the underlying `project` sees neither edit.

Use for: per-agent isolation over a shared base, "preview an edit" sandboxes, throw-away test runs against a real codebase.

MountableFs — multi-mount routing

Routes paths to multiple backends by longest-prefix match. Each backend sees its own world starting at /.

import { MountableFs } from "@codebolt/shell";

const vm = new Vm({
  fs: new MountableFs({
    "/project": persistentFs,         // D1 + R2
    "/tmp":     new InMemoryFs(),     // ephemeral
    "/refs":    referenceLibraryFs    // shared read-only
  })
});

await vm.writeFile("/tmp/scratch.txt", "...");
// → routes to the InMemoryFs as writeFile("/scratch.txt")

Mount points show up as synthetic directory entries in readdir("/"). Nested mounts are not supported (you can't mount /project/sub separately from /project); the one exception is /, which can be a catch-all base under which other mounts override.

ChunkedVFS — metadata + blocks split

Composes a MetadataStore (directory tree + inodes) with an optional BlockStore (blob bytes). Tiered storage: small files live inline in metadata; large files are split into chunkSize blocks in the BlockStore.

import {
  ChunkedVFS,
  InMemoryMetadataStore,
  InMemoryBlockStore
} from "@codebolt/shell";

const vm = new Vm({
  fs: new ChunkedVFS({
    metadata:        new InMemoryMetadataStore(),
    blocks:          new InMemoryBlockStore(),
    inlineThreshold: 1_500_000,    // ≤ this → inline (default 1.5 MB)
    chunkSize:       4 * 1024 * 1024 // > inlineThreshold → split into N × 4 MB blocks
  })
});

The two interfaces are independent. Swap metadata in (SQL → in-memory) without touching block storage; swap blocks in (R2 → S3 → in-memory) without touching metadata. This is the agent-os / JuiceFS pattern.

SqlFs — convenience wrapper

import { SqlFs } from "@codebolt/shell";

const vm = new Vm({
  fs: new SqlFs({
    backend: someSqlBackend,    // your D1 / DO SQL adapter
    blocks:  someBlockStore,    // optional
    inlineThreshold: 1_500_000,
    chunkSize: 4 * 1024 * 1024,
    tableName: "myproject_files" // optional namespace
  })
});

Equivalent to:

new ChunkedVFS({
  metadata: new SqlMetadataStore({ backend: someSqlBackend, tableName: "myproject_files" }),
  blocks:   someBlockStore
})

Persistent storage

codeboltshell ships no Cloudflare-specific code. Persistence plugs in via two adapter functions you write in your own consumer code.

SQL backend

The SqlBackend interface is three methods:

interface SqlBackend {
  exec(sql: string, params?: SqlParam[]): Promise<void>;
  get<T>(sql: string, params?: SqlParam[]): Promise<T | null>;
  all<T>(sql: string, params?: SqlParam[]): Promise<T[]>;
}

type SqlParam = string | number | null | Uint8Array;

A D1 adapter is ~15 lines:

import type { SqlBackend, SqlParam } from "@codebolt/shell";

export function d1ToSqlBackend(db: D1Database): SqlBackend {
  return {
    async exec(sql, params = []) { await db.prepare(sql).bind(...params).run(); },
    async get(sql, params = [])  { return db.prepare(sql).bind(...params).first() as never; },
    async all(sql, params = [])  {
      const r = await db.prepare(sql).bind(...params).all();
      return (r.results ?? []) as never;
    }
  };
}

A Durable Object SQL adapter is the same shape against ctx.storage.sql.exec(...). A better-sqlite3 adapter is the same shape against the prepared-statement API.

Block store

Three methods:

interface BlockStore {
  get(key: string): Promise<Uint8Array | null>;
  put(key: string, data: Uint8Array): Promise<void>;
  delete(key: string): Promise<void>;
}

An R2 adapter is ~15 lines:

import type { BlockStore } from "@codebolt/shell";

export function r2ToBlockStore(bucket: R2Bucket): BlockStore {
  return {
    async get(key) {
      const obj = await bucket.get(key);
      return obj ? new Uint8Array(await obj.arrayBuffer()) : null;
    },
    async put(key, data) { await bucket.put(key, data); },
    async delete(key)    { await bucket.delete(key); }
  };
}

S3 (via aws4fetch), Backblaze B2, MinIO — all the same shape.

Putting it together

import { Vm, SqlFs } from "@codebolt/shell";

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const vm = new Vm({
      fs: new SqlFs({
        backend: d1ToSqlBackend(env.DB),
        blocks:  r2ToBlockStore(env.BLOCKS)
      })
    });

    await vm.writeFile("/hello.txt", "persistent");
    return Response.json({ content: await vm.readFile("/hello.txt") });
  }
};

Search and edit

// Search one file
const matches = await vm.searchText("/src/index.ts", "TODO");
// → [{ line: 14, column: 6, text: "  // TODO: fix me", match: "TODO" }, ...]

// Search across many files
const results = await vm.searchFiles("/src/**/*.ts", "TODO");
// → [{ path, matches: [...] }, ...]

// Regex
const usages = await vm.searchFiles("/src/**/*.ts", "useState\\((.*)\\)", {
  regex: true,
  caseInsensitive: true,
  maxMatchesPerFile: 100
});

// Replace in one file (dry-run preview)
const preview = await vm.replaceInFile("/api/client.ts", "fetch(", "fetchWithRetry(", {
  dryRun: true
});
console.log(`Would replace ${preview.replaced} occurrences`);
console.log(preview.newContent);

// Replace across many files
const result = await vm.replaceInFiles("/src/**/*.ts", "old_url", "new_url");
// → { totalReplaced: 7, perFile: [{ path, replaced }, ...] }

Diff

// Diff two files in the FS (unified diff string)
const patch = await vm.diff("/a.txt", "/b.txt");

// Diff a file's current content against a candidate edit
const preview = await vm.diffContent("/src/index.ts", "export const v = 'NEW';\n");

Both return standard diff -u formatted strings via the diff package.

Git

vm.git is a lazy Git instance backed by isomorphic-git over the same FileSystem.

const vm = new Vm();
const dir = "/repo";

await vm.git.init({ dir });
await vm.writeFile("/repo/README.md", "# hello");
await vm.git.add({ dir, filepath: "README.md" });
const oid = await vm.git.commit({
  dir,
  message: "first",
  author: { name: "you", email: "[email protected]" }
});

const log = await vm.git.log({ dir });
const status = await vm.git.statusMatrix({ dir });
const branches = await vm.git.listBranches({ dir });

The full surface: init, clone, status, statusMatrix, add, remove, commit, log, listBranches, branch, checkout, fetch, pull, push, listRemotes, addRemote, resolveRef, readCommit. Each method takes an optional dir (defaults to the Git instance's defaultDir, which is /).

Auth

// GitHub PAT
await vm.git.clone({
  url: "https://github.com/org/repo.git",
  dir: "/repo",
  auth: { token: "ghp_..." }
});

// Username + password
await vm.git.push({
  dir: "/repo",
  auth: { username: "you", password: "..." }
});

Cloning over HTTPS

vm.git.clone works against any CORS-enabled git endpoint. GitHub itself isn't CORS-enabled — you need a CORS proxy like cors.isomorphic-git.org or your own. Same constraint as isomorphic-git itself.

Shell commands and bash

codeboltshell ships a complete POSIX shell layer vendored from just-bash 2.14.0 (Apache-2.0). No runtime dependency on just-bash itself — the source lives at src/bash/, attribution at src/bash/NOTICE.md. We vendored to keep full control: every command runs over our FileSystem, and consumers can patch any vendored file in place without negotiating with an upstream.

Three command surfaces, ordered roughly by power and weight:

vm.exec — lightweight zero-dep runner

Tiny single-line command runner with a quote-aware tokenizer. No pipes, no expansions, no control flow. Use it when you have a single line to run and don't need the full bash machinery.

await vm.exec.run("mkdir -p /a/b");
await vm.exec.run("cd /a/b");
const r = await vm.exec.run("pwd");          // r.stdout === "/a/b\n"

Builtins: echo, pwd, cd, ls, cat, mkdir, rm, cp, mv, touch, grep, help. Register your own with vm.exec.define(defineCommand("name", async (args, ctx) => ...)).

vm.cmd.<name>(args) — vendored POSIX commands

~57 just-bash commands, each callable as a typed method. Returns a real {stdout, stderr, exitCode}. Argv is passed exactly as you'd write it on a real shell command line.

await vm.cmd.grep(["-rn", "TODO", "/src"]);
await vm.cmd.find(["/src", "-name", "*.ts"]);
await vm.cmd.sed(["s/foo/bar/g", "/file.txt"]);
await vm.cmd.awk(["-F,", "$3==\"eng\"{print $1}", "/data.csv"]);
await vm.cmd.wc(["-l", "/file.txt"]);
await vm.cmd.tr(["a-z", "A-Z"], { stdin: "hello\n" });
await vm.cmd.sha256sum([], { stdin: "hello" });

Full list (categorised):

  • Search / text: grep, sed, awk, cat, head, tail, wc, sort, uniq, cut, paste, comm, join, nl, tac, rev, tr, expand, unexpand, fold, od, strings, split, diff, column
  • Filesystem: ls, cp, mv, rm, mkdir, rmdir, touch, ln, stat, basename, dirname, readlink, find, du, chmod, tree
  • Misc: echo, printf, pwd, env, printenv, date, seq, tee, expr, true, false, sleep, which, whoami, clear, base64, md5sum, sha1sum, sha256sum, xargs, time, timeout

xargs, timeout, and find -exec work because the vm.cmd constructor wires a sub-execution callback into every command's context — they dispatch back into the same registry.

Skipped: tar / gzip (need Node child_process / zlib), file (needs file-type), python3 / js-exec / sqlite3 (WASM bundles).

vm.bash.exec(script) — full bash interpreter

The big one. Parser + interpreter + expansion + redirections + control flow + functions + arrays + [[ ]] + here-docs + set -e + the whole bash surface, all vendored, all running over vm.fs.

await vm.bash.exec("echo hello world");
await vm.bash.exec('FOO=hi; echo "${FOO:-default}"');
await vm.bash.exec("cat /work/data.txt | grep TODO | wc -l");
await vm.bash.exec("for f in /work/*.txt; do grep TODO $f; done | wc -l");
await vm.bash.exec(`
  if [ -f /work/data.txt ]; then
    echo found
  else
    echo missing
  fi
`);
await vm.bash.exec("greet() { echo hello $1; }; greet world");

vm.bash holds mutable shell state across calls — env vars, exported set, functions, last exit code, cwd. Successive calls behave like one shell session:

await vm.bash.exec("export FOO=persistent; cd /work");
await vm.bash.exec("echo $FOO");      // → "persistent\n"
await vm.bash.exec("pwd");            // → "/work\n"

Every command in vm.cmd is automatically available inside scripts. The interpreter looks them up via the same registry.

Transform plugins

Hand vm.bash.exec an AST transform plugin (or array, or pipeline) to inspect or rewrite the script between parse and execute. Plugin metadata comes back on result.transformMetadata.

import {
  BashTransformPipeline,
  CommandCollectorPlugin,
  TeePlugin
} from "@codebolt/shell";

// Single plugin
const collector = new CommandCollectorPlugin();
const r = await vm.bash.exec("cat data.txt | grep foo", { transform: collector });
console.log(r.transformMetadata); // { commands: ["cat", "grep"] }

// Pipeline form (chains plugins, merges metadata)
const pipeline = new BashTransformPipeline()
  .use(new TeePlugin({ outputDir: "/tmp/tee", targetCommandPattern: /grep/ }))
  .use(new CommandCollectorPlugin());

const r2 = await vm.bash.exec(script, { transform: pipeline });
// r2.transformMetadata: { teeFiles: [...], commands: [...] }

Bring your own plugin by implementing TransformPlugin:

import type { TransformPlugin, ScriptNode } from "@codebolt/shell";

const myPlugin: TransformPlugin<{ stmtCount: number }> = {
  name: "my-plugin",
  transform({ ast }) {
    return {
      ast,                               // unchanged or rewritten
      metadata: { stmtCount: ast.statements.length }
    };
  }
};

Standalone parse + serialize

The parser and serializer are exported on their own — no Vm required — for tools that just want to manipulate bash scripts as data:

import { parse, serialize } from "@codebolt/shell";

const ast = parse("for i in 1 2 3; do echo $i; done");
const back = serialize(ast);

Round-trip is "lossy but semantically equivalent" today. Known quirks: [ -f x ] round-trips with an escaped closing bracket, and ${a}_${b} loses the braces around ${a}. Both are upstream just-bash issues we inherited; tracked for a future pass.

API surface

Everything exported from the package:

// Core
export { Vm, type VmConfig };

// FileSystem implementations
export { InMemoryFs };
export { OverlayFs };
export { MountableFs };
export { ChunkedVFS, type ChunkedVFSConfig };
export { SqlFs, type SqlFsConfig };

// MetadataStore (directory tree side of ChunkedVFS)
export { InMemoryMetadataStore };
export { SqlMetadataStore, type SqlMetadataStoreConfig };
export type { Inode, MetadataStore };

// BlockStore (blob side of ChunkedVFS)
export { InMemoryBlockStore };
export type { BlockStore };

// SQL backend interface (adapters in consumer code)
export type { SqlBackend, SqlParam, SqlRow };

// FileSystem interface
export type { DirEntry, FileStat, FileSystem };
export { FsError };

// Tools
export { searchText, searchFiles };
export { replaceInFile, replaceInFiles };
export { diff, diffContent };
export type {
  SearchOptions, SearchMatch, FileSearchResult,
  ReplaceOptions, ReplaceResult, MultiFileReplaceResult
};

// Git
export { Git, type GitAuth, type GitAuthor };
export { createGitFs };

// Lightweight exec runner
export { Exec, defineCommand, type ExecOptions, type RunOptions };
export { tokenize };
export type { Command, CommandContext, ExecResult };

// Vendored bash layer (just-bash 2.14.0, Apache-2.0; src/bash/NOTICE.md)
export { Bash, type BashOptions, type BashExecOptions };
export { Cmd };
export { parse, serialize };
export { BashTransformPipeline };
export { CommandCollectorPlugin, type CommandCollectorMetadata };
export { TeePlugin, type TeePluginOptions, type TeePluginMetadata, type TeeFileInfo };
// Plus the full set of bash AST node types (ScriptNode, StatementNode,
// PipelineNode, SimpleCommandNode, IfNode, ForNode, ...)

Architecture

┌─ Vm ─────────────────────────────────────────────────────────┐
│                                                               │
│  vm.fs ── one FileSystem ── any backend                      │
│                                                               │
│  ┌─ FileSystem (interface) ──────────────────────────────┐  │
│  │  read(File|FileBytes|FileStream) / write(...)         │  │
│  │  exists / stat / lstat / readdir / glob               │  │
│  │  mkdir / rm / cp / mv / symlink / readlink            │  │
│  └────────────────────────────────────────────────────────┘  │
│       ▲          ▲           ▲           ▲           ▲       │
│       │          │           │           │           │       │
│  InMemoryFs  OverlayFs  MountableFs  ChunkedVFS    SqlFs     │
│  (Map)       (lower+    (longest-    (metadata    (=Chunked  │
│              upper+     prefix       + blocks)     VFS over  │
│              tombstone) routing)                   SqlMeta)  │
│                                          ▲                    │
│                            ┌─────────────┴─────────────┐     │
│                            │                            │     │
│                       MetadataStore             BlockStore    │
│                       (directory tree,         (blob bytes)   │
│                        inodes, chunk maps)                    │
│                            ▲                            ▲     │
│                            │                            │     │
│              ┌─────────────┴─────┐         ┌────────────┴┐    │
│              InMemoryMetadataStore         InMemoryBlockStore │
│              SqlMetadataStore              R2BlockStore       │
│                                            (your adapter)    │
│                                                               │
│  vm.git   ── Git ── createGitFs(this.fs) ── isomorphic-git   │
│  vm.searchText / replaceInFile / diff ── helpers over fs     │
│                                                               │
│  vm.exec  ── Exec ── tokenize + builtins ── small + zero-dep │
│                                                               │
│  vm.cmd   ── Cmd ── ~57 vendored just-bash POSIX commands    │
│             grep / sed / awk / find / ls / cat / cp / mv / ... │
│                       │                                       │
│                       └─ adaptFs(this.fs) ─ IFileSystem shim │
│                                                               │
│  vm.bash  ── Bash ── parser + interpreter (vendored)         │
│             pipes, redir, expansion, control flow, fns       │
│             • shares vm.cmd's registry as the command lookup │
│             • optional transform pipeline (TeePlugin,        │
│               CommandCollectorPlugin, custom plugins)         │
└───────────────────────────────────────────────────────────────┘

The shape that matters: one FileSystem interface, multiple implementations, all composable, all running over the same Vm. The two-axis split (metadata + blocks) inside ChunkedVFS lets the persistent backend mix and match storage independently — D1 metadata with R2 blocks, in-memory metadata with R2 blocks for tests, etc.

Status and limits

codeboltshell is v0.0.x — the API is stable enough to build against but not 1.0. Deliberate gaps:

  • No symlinks on most backends. InMemoryFs, ChunkedVFS, and SqlFs throw FsError("ENOTSUP") on symlink/readlink. The interface is there for backends that want them.
  • No transactions across SQL + blocks. A streaming write that fails mid-flight can leave orphan blocks in the BlockStore. A GC pass against bucket.list() − SELECT block_keys FROM ... is the fix; not yet shipped.
  • InMemoryFs is not chunked. It stores Map<path, Uint8Array> and its streams are single-chunk. Chunking only kicks in when you pair ChunkedVFS with a real BlockStore.
  • vm.git.clone over HTTPS needs a CORS proxy for any git server that isn't CORS-enabled (i.e., GitHub). Same limit as upstream isomorphic-git.
  • isomorphic-git is the heaviest dependency (~250 KiB raw, less after tree-shaking). If bundle size becomes a problem, the git surface is a candidate to split into a sibling package.
  • No real path normalization. .., . mid-path, and // aren't collapsed by the FileSystem layer (only by the git fs adapter, where it's needed). If you pass weird paths, you get weird results.
  • writeFileStream is consumer-driven. It takes a ReadableStream and reads from it. A truly progressive vm.openWriteStream(path) → WritableStream API is future work.
  • Vendored bash layer is ~58K lines under src/bash/. It tree-shakes per-file in modern bundlers (esbuild, wrangler) — consumers who only use vm.fs and vm.git won't pay for the bash code.
  • Bash serializer round-trip is lossy in two known cases (escaped ] in [ ... ] tests; dropped braces in ${a}_${b}). Inherited from upstream just-bash; tracked.
  • Skipped vendored commands: tar, gzip (Node-only deps), file (file-type dep), python3 / js-exec / sqlite3 (WASM bundles).

License

@codebolt/shell is licensed under the Apache License 2.0. See LICENSE for the full text and NOTICE.md at the package root for the third-party attribution summary.

The vendored bash layer under src/bash/ is derived from just-bash 2.14.0, copyright Vercel-Labs and contributors, also under Apache-2.0. The upstream attribution is preserved at src/bash/NOTICE.md and the license text at src/bash/LICENSE.apache-2.0.