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

typescript-virtual-container

v1.7.6

Published

Scalable Linux emulator with included SSH/SFTP server, virtual filesystem and typed programmatic API for testing, automation, and interactive shell scripting in TypeScript/JavaScript.

Readme

typescript-virtual-container

A complete virtual Linux environment in pure TypeScript — runs as an SSH/SFTP server, a browser-based web shell, or a standalone CLI. Ships with a realistic Linux rootfs, a virtual package manager, a full shell interpreter, and a typed programmatic API for testing, automation, honeypots, and embedded shell experiences.

npm version License: MIT TypeScript Runtime Live Demo


Table of Contents


Three ways to run

| Mode | Entry point | Use case | |------|-------------|----------| | SSH/SFTP server | VirtualSshServer / VirtualSftpServer | Honeypots, remote testing, training environments | | Web shell | builds/fortune-nyx-v1.7.6-web.min.js (ESM) | Embedded terminals, interactive tutorials, browser demos — run startxfce4 for a full XFCE desktop | | Standalone CLI | builds/fortune-nyx-v1.7.6-directbash-k6.1.0.mjs (single file) | Local shell, one-liner demos, no install required |

All three modes share the same core: a pure in-memory VFS, a real shell interpreter, a virtual package manager, and a typed programmatic API.


Get Started

Install

npm install typescript-virtual-container
# or: yarn add / bun add

Try instantly (zero install)

Interactive local shell — persists VFS in .vfs/ in the current directory

curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.7.6-directbash-k6.1.0.mjs -o fortune-nyx-v1.7.6-directbash-k6.1.0.mjs && node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs

SSH server with built-in SFTP subsystem (scp / sftp on port 2222)

curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.7.6-ssh.cjs -o fortune-nyx-v1.7.6-ssh.cjs && node fortune-nyx-v1.7.6-ssh.cjs

Custom SSH port

node fortune-nyx-v1.7.6-ssh.cjs --ssh-port=2022

SSH disabled (handler only, no server started)

node fortune-nyx-v1.7.6-ssh.cjs --no-ssh

[!NOTE] The standalone builds are intended for quick demos and testing. For production use, it's recommended to install the package and import the relevant classes directly in your codebase for better performance, stability, and security.

fortune-nyx-v1.7.6-directbash-k6.1.0.mjs options:

node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs                          # boot as root
node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs --user alice             # boot as alice (prompts for password if set)
node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs --user=alice             # same, inline form
node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs --hostname=my-box        # custom hostname
node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs --snapshot=/data/.vfs    # custom VFS snapshot path
node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs --help                   # show all options
node fortune-nyx-v1.7.6-directbash-k6.1.0.mjs --version                # print version

The VFS is persisted automatically to .vfs/vfs-snapshot.vfsb in the current directory — state survives between runs. Delete .vfs/ to start fresh.

The shell shows a login banner and Last login: timestamp on each start.

SSH server

import { VirtualSshServer } from "typescript-virtual-container";

const ssh = new VirtualSshServer({ port: 2222, hostname: "my-container" });
await ssh.start();
// ssh root@localhost -p 2222

Web shell (browser)

🖥 Try the live demo →

The library can run entirely in the browser. The VFS is persisted in IndexedDB — state survives page reloads.

| Bundle | Format | Entry point | Use case | |--------|--------|-------------|----------| | builds/fortune-nyx-v1.7.6-web.min.js | ESM | VirtualShell, VirtualFileSystem, SshClient, DesktopManager, … | Embedded terminals, browser demos |

Build the browser bundle:

bun run web-build       # → builds/fortune-nyx-v1.7.6-web.min.js
bun run build-all       # rebuild everything (library + standalone + demo + docs)

Use in a project:

<script type="module">
  import { VirtualShell, VirtualFileSystem, SshClient } from "./fortune-nyx-v1.7.6-web.min.js";

  // Wait for IndexedDB VFS to finish loading (required before any VFS ops)
  await globalThis.__fsReady__;

  const shell = new VirtualShell("web-vm");
  await shell.ensureInitialized();
  shell.setPassword("root", "root");

  shell.runCommand("echo Hello from the web shell!").then(result => {
    console.log(result.stdout); // "Hello from the web shell!\n"
  });
</script>

The bundle exports the full library API (VirtualShell, VirtualFileSystem, SshClient, DesktopManager, HoneyPot, Baie, …) — see the Programmatic API section for usage.

Run the demo locally:

bun run example-serve
# Open http://localhost:8787/index.html

The demo (demo/app.ts) adds a terminal renderer (WebTermRenderer) and a desktop environment (DesktopManager) on top of VirtualShell, bundled with esbuild and Node.js polyfills (node:fs, node:crypto, node:events, etc.).


import { VirtualShell, VirtualSshServer, SshClient } from "typescript-virtual-container";

const shell  = new VirtualShell("typescript-vm");
await shell.ensureInitialized();
shell.setPassword("root", "root");

const ssh = new VirtualSshServer({ port: 0, shell });
const port = await ssh.start();

const client = new SshClient();
await client.connect({ host: "localhost", port, username: "root", password: "root" });

await client.mkdir("/app/config", true);
await client.writeFile("/app/config/settings.json", JSON.stringify({ env: "dev" }));

const r = await client.exec("ls /app && cat /app/config/settings.json");
console.log(r.stdout);

Multi-VM, proxy, VPN, and hosting examples

See guides/EXAMPLES.md for:

  • Baie — multi-VM network on a shared subnet
  • VirtualProxy — port forwarding and SOCKS5 proxy
  • Multi-tier lab — web/api/db with firewall isolation
  • Hosting platform — multi-tenant with SSH per tenant
  • VirtualVpn — encrypted tunnel between Baie instances
  • Traffic shaping — latency, jitter (Gaussian), packet loss, burst loss, reordering, duplication
  • Bandwidth enforcement — token bucket rate limiting per VM
  • MTU enforcement — packets exceeding interface MTU are dropped
  • Connection tracking — stateful conntrack with NEW/ESTABLISHED/TIME_WAIT states
  • Advanced routing — multiple routing tables, policy-based routing, route metrics
  • DNS, load balancer, partition

┌──────────────────────────────────────────────────────────────────────────┐
│  SshMimic (VirtualSshServer)             SftpMimic (VirtualSftpServer)  │
│  password auth · publickey auth          SFTP protocol handlers          │
│  per-IP rate limiting / lockout          home-dir confinement            │
└─────────────────────────┬────────────────────────────────────────────────┘
                          │
               ┌──────────▼──────────┐         ┌──────────────────────────┐
               │    VirtualShell     │────────▶ │    DesktopManager        │
               │  script parser      │          │  XFCE panel · windows    │
               │  command executor   │          │  Thunar · Mousepad       │
               │  .bashrc loader     │          │  Trash · terminal windows│
               │  session manager    │          │  (browser only)          │
               └──┬──────────────┬───┘          └──────────────────────────┘
                  │              │
     ┌────────────▼───┐    ┌─────▼───────────────┐
     │VirtualFileSystem│   │ VirtualUserManager   │
     │ in-memory tree  │   │ scrypt · sudoers     │
     │ gzip · symlinks │   │ publickey auth       │
     │ .vfsb binary    │   │ quotas · sessions    │
     │ mode:memory|fs  │   └─────────────────────-┘
     └─────────────────┘
                  │
     ┌────────────▼────────────┐
     │   VirtualNetworkManager │
     │ interfaces · routes     │
     │ firewall · ARP          │
     │ conntrack · policy rules│
     │ multi-table routing     │
     └─────────────────────────┘
                  │
     ┌────────────▼────────────┐
     │        HoneyPot         │
     │ audit log · stats       │
     │ anomaly detection       │
     └─────────────────────────┘

What it is: a shell emulator and virtual Linux environment for developer workflows.

What it is not: a fully isolated sandbox. The shell commands are contained — no host binary is ever spawned, execvp is never called, node/python3/npm are virtual stubs. Permission enforcement follows POSIX semantics (owner/group/other, sticky bit, setuid) for all file operations, and unknown users are auto-provisioned with non-root UIDs. But curl/wget use the real fetch() API (live network access), all instances share the same JS heap as the host application, and resource capping is enforced at the VFS/process level (not at the kernel level). Do not expose to untrusted input without additional infrastructure-level isolation.


Two execution modes for different isolation needs:

| | VirtualShell | SandboxedShell | |---|---|---| | Thread | Main thread | Worker thread (node:worker_threads) | | JS heap | Shared with host and other shells | Isolated (separate V8 isolate) | | API | Full shell — pipes, redirects, desktop, exec(), interactive sessions | Minimal — exec(cmd, user?, cwd?) only | | Overhead | ~0.1ms per command | IPC message passing + worker bootstrap | | Timeout | None | Configurable (default 30s) | | Lifespan | Long-lived, reusable | Single worker (create, exec, terminate) | | Use case | Test harnesses, embedded shells, automation | Multi-tenant, untrusted code, CI runners | | Browser | ✅ Yes (via polyfills) | ❌ No (requires worker_threads) |

import { VirtualShell, SandboxedShell } from "typescript-virtual-container";

// Shared heap — lightweight, full-featured
const shell = new VirtualShell("dev-vm");
await shell.ensureInitialized();
const r1 = await shell.exec("echo hello");
console.log(r1.stdout); // "hello\n"

// Isolated worker — separate heap, exec-only
const sandbox = new SandboxedShell({ execTimeoutMs: 10_000 });
const r2 = await sandbox.exec("echo hello", "root", "/root");
console.log(r2.stdout); // "hello\n"
sandbox.terminate();

Limit the RAM and CPU resources visible inside a virtual shell. Pass a VirtualShellResourceCaps object as the 4th constructor argument:

import { VirtualShell } from "typescript-virtual-container";

const shell = new VirtualShell("prod-vm", undefined, undefined, {
  ramCapBytes: 2 * 1024 * 1024 * 1024, // 2 GiB VFS storage limit
  cpuCapCores: 2,                        // 2 vCPUs
});

Reporting (always active)

When a cap is set, every /proc file, sysctl entry, cgroup file, and system-monitor command reports the capped value instead of the real host total:

| Command | CPU cap | RAM cap | |---|---|---| | free | — | ✅ | | top | — | ✅ | | htop | ✅ | ✅ | | nproc | ✅ | — | | lscpu | ✅ | — | | neofetch | ✅ | ✅ | | /proc/cpuinfo | ✅ | — | | /proc/meminfo | — | ✅ | | /proc/stat | ✅ | — | | cgroup files | ✅ | ✅ |

RAM enforcement

When ramCapBytes is set, the VFS tracks total bytes stored in its in-memory tree. Any write that would cause the total to exceed the cap is rejected with an ENOMEM error:

$ dd if=/dev/zero of=/tmp/big bs=1M count=3000
ENOMEM: Cannot allocate memory: write to '/tmp/big' would exceed RAM cap

This covers all VFS writes: echo > file, dd, cp, tee, SFTP uploads, etc. Compressed files count their compressed size.

Note: this caps VFS storage, not Node.js heap usage. A process that allocates large JS objects without writing to the VFS will not be blocked.

CPU enforcement

When cpuCapCores is set, a background watcher tracks wall-clock time per process. If a process exceeds the per-window budget (cpuCapCores × 1000 ms per second), it is killed with SIGKILL (exit code 137):

$ while true; do :; done
Killed  # exit code 137

Short commands (ls, echo, cat) are unaffected. The process:killed:cpu event is emitted with { pid, command, cpuTime }.

Process scheduler

The VFS includes a priority-based process scheduler inspired by Linux CFS. When enabled, processes receive CPU time proportional to their nice value (-20 = highest priority, 19 = lowest):

shell.users.enableScheduler({
  baseTimesliceMs: 100,
  maxTimesliceMs: 500,
  enforceFairShare: true,
});

shell.users.setProcessNice(1234, -10); // boost priority
shell.users.getSchedulerStats();       // { scheduleCount, throttleCount, ... }

In the shell:

nice -n -10 my-command    # run with high priority
nice -n 15 -p 1234        # lower priority of running process
ps aux                    # shows NI (nice) and PRI columns

The scheduler tracks per-process CPU time in accounting windows and throttles processes that consume more than 2× their fair share, preventing any single process from monopolizing the CPU.

Runtime changes

Both caps can be changed at runtime via sysctl:

sysctl vm.ram_cap_bytes=1073741824   # 1 GiB
sysctl kernel.cpu_cap_cores=2         # 2 vCPUs
sysctl vm.ram_cap_bytes=0             # remove RAM cap

When running in "fs" persistence mode with swapEnabled: true, the VFS can offload evicted file contents to individual swap files on disk. This provides two benefits:

  1. O(1) reload — reading an evicted file loads from its swap file directly, instead of parsing the entire snapshot.
  2. RAM overflow handling — when a write would exceed ramCapBytes, the VFS swaps out the least recently used (LRU) files to make room instead of immediately rejecting with ENOMEM.
import { VirtualFileSystem } from "typescript-virtual-container";

const vfs = new VirtualFileSystem({
  mode: "fs",
  snapshotPath: "./data",
  swapEnabled: true,         // enable swap store
  swapDir: "./data/swap",    // optional: custom swap directory (defaults to <snapshotPath>/swap)
});

How it works

  • When a file is evicted (by evictLargeFiles(), evictUnusedLargeFiles(), or RAM cap pressure), its content is written to <swapDir>/<hash>.swap with a small binary header.
  • The in-memory node keeps evicted: true and content = Buffer.alloc(0).
  • On the next readFile(), the content is loaded from the swap file and the swap entry is deleted.
  • Swap is disabled by default — it must be explicitly enabled via VfsOptions.

Swap command

$ swap -s              # show swap statistics
Swap usage:
  Files swapped out : 12
  Swap disk usage   : 4.2 MB
  Original size     : 3.8 MB
  Swap-in ops       : 45
  Swap-out ops      : 57

$ swap -c              # clear all swap files
swap: swap files cleared

The apt/dpkg/pacman commands are fully virtual — they do not install real system packages. Instead, they manage a curated database of ~25 virtual packages that write files into the VFS and register new shell commands.

What apt install actually does

  1. Looks up the package in the internal database (VirtualPackageManager)
  2. Writes the package's files into the VFS (e.g. /usr/bin/python3.12, /usr/lib/...)
  3. Registers any bundled commands in the shell's command registry
  4. Records the installation in /var/lib/dpkg/status

Available packages

| Package | Version | Category | What it provides | |---|---|---|---| | python3 | 3.12.3 | interpreter | Virtual Python REPL, /usr/bin/python3.12 | | node | 22.x | interpreter | Virtual Node.js REPL | | npm | 10.x | package manager | Virtual npm stub | | gcc | 13.3.0 | compiler | Compiler stub + headers | | g++ | 13.3.0 | compiler | C++ compiler stub | | openjdk-21 | 21.0.1 | runtime | Java runtime stub | | curl | 8.5.0 | network | Already built-in, package is metadata only | | git | 2.43.0 | vcs | Git stub with basic commands | | vim | 9.0 | editor | Already built-in (nano), package is metadata only | | htop | 3.3.0 | system | Already built-in, package is metadata only | | tmux | 3.4 | terminal | Tmux stub | | wget | 1.21 | network | Already built-in, package is metadata only | | zip | 3.0 | archive | Already built-in, package is metadata only | | unzip | 6.0 | archive | Already built-in, package is metadata only | | rsync | 3.2.7 | network | Rsync stub | | strace | 6.6 | debug | Already built-in, package is metadata only | | lsof | 4.95 | system | Already built-in, package is metadata only | | iperf3 | 3.16 | network | Iperf stub | | jq | 1.7 | text | jq stub | | tree | 2.1.1 | files | Already built-in, package is metadata only | | neofetch | 7.1.0 | system | Already built-in, package is metadata only | | cmatrix | 2.0 | fun | Already built-in, package is metadata only | | sl | 5.02 | fun | Already built-in, package is metadata only | | fortune | 1.99 | fun | Already built-in, package is metadata only | | cowsay | 3.03 | fun | Already built-in, package is metadata only |

Important: These are not real packages. Installing python3 does not give you a full Python interpreter — it provides a virtual REPL stub that simulates basic Python behavior. The same applies to all other packages. This is intentional: the project is designed to be a self-contained virtual environment, not a real package manager.

Package-gated commands

Some commands require apt install before they become available:

$ gcc --version
Command 'gcc' not found

$ apt install gcc
$ gcc --version
gcc (Fortune 13.3.0-nyx1) 13.3.0

API reference for all core classes and utilities. Designed for quick lookup while developing with the library. More extensive documentation, examples, and guides are available in the documentation.


All examples are available as runnable TypeScript files in the examples/ directory:

| # | File | Description | |---|------|-------------| | 01 | 01-ssh-server-events.ts | SSH server with auth and lockout event handlers | | 02 | 02-ssh-sftp-shared-state.ts | SSH + SFTP servers sharing the same VirtualShell | | 03 | 03-multi-user-quotas.ts | Multi-user setup with sudo management and disk quotas | | 04 | 04-persistent-state.ts | FS mode auto-persistence and manual JSON snapshots | | 05 | 05-public-key-auth.ts | Public-key SSH authentication setup | | 06 | 06-rate-limiting.ts | Auth rate limiting and manual lockout override | | 07 | 07-snapshot-test-fixtures.ts | Snapshot-based test fixtures for isolated tests | | 08 | 08-snapshot-diff.ts | VFS snapshot diffing to verify expected changes | | 09 | 09-symlinks.ts | Creating and resolving symbolic links | | 10 | 10-honeypot-auditing.ts | HoneyPot event logging, stats, and anomaly detection | | 11 | 11-concurrent-clients.ts | Parallel operations from multiple SshClient instances |


Type help in the shell for a grouped, colorized listing. Type help <command> for detailed usage. Type man <command> for full manual pages — all commands are documented.

Navigation

| Command | Description | |---------|-------------| | pwd | Print working directory | | cd <path> | Change directory | | ls [path] | List directory | | tree [path] | ASCII directory tree | | help | Full list: type help in the shell |

Files

| Command | Description | |---------|-------------| | cat <path...> | Concatenate and print | | touch <path> | Create or update file | | rm <path> | Remove file or directory | | mkdir <path> | Create directory | | cp <src> <dest> | Copy file or directory | | mv <src> <dest> | Move or rename | | chmod <mode> <file> | Change file mode | | ln <target> <link> | Hard or symbolic link | | find [path] | Search for files | | stat <path> | Display file status | | dd | Convert and copy a file | | realpath <path> | Print resolved absolute path | | help | Full list: type help in the shell |

Text

| Command | Description | |---------|-------------| | grep <pattern> [files] | Search file content | | sed -e 's/pat/rep/' | Stream editor | | awk '<prog>' | Pattern scanning and processing | | sort [files] | Sort lines | | uniq | Filter repeated lines | | wc [files] | Word/line/byte count | | head [files] | First N lines | | tail [files] | Last N lines | | diff <f1> <f2> | Compare files | | tee [files] | Read stdin, write to stdout and files | | md5sum <file> | Compute MD5 checksum | | sha256sum <file> | Compute SHA-256 checksum | | fold <file> | Wrap lines to specified width | | expand <file> | Convert tabs to spaces | | fmt <file> | Simple text formatter | | help | Full list: type help in the shell |

Archives

| Command | Description | |---------|-------------| | tar <archive> [files] | Archive utility (POSIX ustar) | | gzip <file> / gunzip <file> | Compress / decompress | | zip [-r] <archive> <files> / unzip <archive> | PKZIP + DEFLATE archive | | bzip2 <file> / bunzip2 <file> | Bzip2 compress / decompress | | base64 | Encode/decode base64 | | help | Full list: type help in the shell |

System

| Command | Description | |---------|-------------| | whoami | Current user | | id [user] | User identity (uid/gid/groups) | | uname | System information | | hostname | Print hostname | | ps | Process status | | kill [-9] <pid> | Send signal (mock) | | df | Filesystem disk space | | du [path] | Estimate file space | | free | Memory usage (real host data) | | uptime | Running time | | lsof | List open files (simulated) | | strace <cmd> | Trace system calls (stub) | | lscpu | CPU architecture info | | lspci | PCI device listing | | nice <cmd> | Run with adjusted priority | | nohup <cmd> | Run immune to hangups | | pgrep <pattern> | Search processes by name | | pkill <pattern> | Kill processes by name | | top | Real-time process monitor (mock) | | help | Full list: type help in the shell |

Network

| Command | Description | |---------|-------------| | curl <url> | HTTP client (pure fetch()) | | wget <url> | File downloader (pure fetch()) | | ip <object> | Network interface/routing tool (addr, route, link, neigh, rule) | | ping [-c <n>] <host> | ICMP ECHO_REQUEST (mock) | | nc <host> <port> | TCP netcat (mock) | | tc qdisc | Traffic control — netem, tbf, htb qdiscs | | ss / netstat | Socket statistics and connection tracking | | traceroute <host> | Route tracing with simulated hop paths | | conntrack | Connection tracking management | | ifconfig | Legacy interface configuration | | iptables | Firewall rule management | | help | Full list: type help in the shell |

Session

| Command | Description | |---------|-------------| | ssh <user>@<host> | SSH client (mock) | | sftp <user>@<host> | SFTP client (mock) | | nano <path> | Interactive text editor | | su [user] | Switch user | | sudo <cmd> | Run as root | | passwd [user] | Change password | | adduser <name> | Create user interactively (root only) | | deluser <name> | Delete user (root only) | | last [user] | Show login history | | w | Who is logged on and what they are doing | | who | Active sessions | | help | Full list: type help in the shell |

User & Group Management

| Command | Description | |---------|-------------| | id [-u|-g|-G|-n] [user] | User identity — real uid/gid/supplementary groups | | groups [user] | All group memberships | | getent passwd\|group [key] | Query user/group database | | groupadd [-g GID] <group> | Create a new group | | groupdel <group> | Delete a group | | gpasswd -a\|-d -G group user | Add/remove users from groups | | usermod [-g\|-G\|-aG\|-L\|-U] <user> | Modify user account | | newgrp [group] | Switch primary group for session | | chage [-m\|-M\|-W\|-I\|-E\|-l] <user> | Password aging & account expiry | | help | Full list: type help in the shell |

Package

| Command | Description | |---------|-------------| | apt <cmd> [pkg...] | Package manager | | dpkg | Low-level package tool | | npm | Node.js package manager | | npx | Node.js package runner | | node | Virtual JS runtime | | perl | One-liner interpreter | | python3 | Virtual Python 3 interpreter | | help | Full list: type help in the shell |

Shell

| Command | Description | |---------|-------------| | sh | Execute shell script | | echo <text> | Display text | | printf <fmt> [args...] | Format and print | | read [-d] [-n] [-t] [-a] <var...> | Read stdin into variable(s) | | source <file> | Execute file in current env | | export [-n] [-f] NAME=VALUE | Set shell variable or function | | readonly [-p] <name...> | Mark variables as readonly | | set [-o] [+o] [--] [VAR=val] | Display or set shell options/variables | | unset [-f] [-v] <VAR> | Remove variable or function | | alias [name=value] | Define or display aliases | | type [-a] [-t] [-p] <command> | Describe command interpretation | | command [-v] [-V] [-p] <cmd> | Run or identify command (skip functions) | | builtin <cmd> [args...] | Run a shell builtin (skip functions/aliases) | | test <expr> / [ <expr> ] | POSIX conditional | | [[ <expr> ]] | Extended conditional (bash) | | expr <expr> | Evaluate expression | | declare [name=value] | Declare variables | | hash [-r] <name> | PATH cache utilities | | shopt [-s] [-u] <opt> | Shell option management (bash) | | caller [n] | Print stack trace | | exit [code] | Exit session | | shift [n] | Shift positional parameters | | return [n] | Return from shell function | | trap [-p] [action] [signal] | Signal handlers | | true / false | Return exit code 0 / 1 | | sleep <seconds> | Delay execution | | timeout <n> <cmd> | Run command with time limit | | wait [-n] [job...] | Wait for background jobs | | history [n] | Command history | | help | Full list: type help in the shell |

Desktop (browser only)

| Command | Description | |---------|-------------| | startxfce4 | Launch the XFCE desktop — blocks the shell until logout | | thunar [path] | Open the file manager window | | mousepad [file] | Open the text editor (gedit and xed are aliases) |

Desktop commands are no-ops outside the browser environment.

Fun

| Command | Description | |---------|-------------| | neofetch | System info display | | cowsay [msg] | ASCII cow saying something | | fortune | Print a random adage | | cmatrix | Matrix-style falling characters | | sl | Steam locomotive | | yes [string] | Repeatedly output string | | bc | Arithmetic calculator | | seq | Print sequence of numbers | | pacman | Pacman game (mock) | | help | Full list: type help in the shell |

ℹ️ All 223 built-in commands include complete JSDoc documentation with @category and @params tags. See src/commands/ for source code and inline documentation.

Custom commands: shell.addCommand(name, params, callback).


The interpreter supports a POSIX sh subset via sh -c '...', sh <file>, or interactive session.

Operators and Redirections

mkdir /app && echo "created"     # && — only if previous succeeds
rm /missing || echo "not found"  # || — only if previous fails
echo a; echo b; echo c           # ; — always all three

cat /etc/hosts | grep local
echo "hello" > /tmp/out.txt      # overwrite
cat /tmp/out.txt >> /tmp/log.txt # append

Variable Expansion

export NAME=world
echo "Hello $NAME"            # basic variable
echo "${NAME:-fallback}"      # default if unset
echo "$(whoami)"              # command substitution
echo $((2 + 3))               # arithmetic expansion
echo "${HOME}"                # curly brace delimited

Arithmetic

echo $((2 + 3))        # 5
X=4; echo $((X * 2))   # 8
i=0; i=$((i + 1))      # increment

Conditionals

if [ -f /etc/config ]; then
  echo "config exists"
elif [ -d /etc ]; then
  echo "etc is a directory"
else
  echo "nothing found"
fi

[ "$USER" = "root" ] && echo "root"
[ $COUNT -gt 10 ] && echo "large"

Loops

for name in alice bob charlie; do
  echo "Hello $name"
done

COUNT=0
while [ $COUNT -lt 3 ]; do
  echo "Count: $COUNT"
  COUNT=$((COUNT + 1))
done

until [ $COUNT -eq 0 ]; do
  echo "Countdown: $COUNT"
  COUNT=$((COUNT - 1))
done

Functions and case

greet() {
  echo "Hello, $1!"
}
greet world

case "$1" in
  start) echo "Starting..." ;;
  stop)  echo "Stopping..." ;;
  *)     echo "Usage: $0 {start|stop}" ;;
esac

Heredoc

cat << EOF
line one
line two
EOF

# write to file
cat > /tmp/config << EOF
HOST=localhost
PORT=3000
EOF

set -e / set -x

set -e          # exit immediately on any non-zero exit code
set -x          # print each command before executing (trace)
set +e          # disable errexit
set +x          # disable xtrace

Script Files

shell.vfs.writeFile("/usr/local/bin/setup.sh", `
#!/bin/sh
for dir in config logs tmp; do
  mkdir /app/$dir
done
`);
await client.exec("sh /usr/local/bin/setup.sh");

Login Files

Sourced automatically at session start, in order:

  1. /etc/environmentKEY=VALUE pairs only, no shell syntax
  2. ~/.profile — user login script
  3. ~/.bashrc — interactive shell config
export EDITOR=nano
alias ll="ls -l"
echo "Welcome, $USER!"

Set .bashrc programmatically:

shell.vfs.writeFile("/home/root/.bashrc", `
export EDITOR=nano
alias ll="ls -l"
echo "Welcome back, root!"
`.trim());

Directory Layout

On every VirtualShell init, a realistic Linux hierarchy is bootstrapped idempotently:

/
├── bin/
│   ├── [47 stubs: sh, bash, python3, ...]
├── boot/
├── dev/
│   ├── null, zero, random, urandom, tty, ...
├── etc/
│   ├── passwd, shadow, group, sudoers, hostname, ...
├── home/
│   └── root/
├── opt/
├── proc/
│   ├── (dynamic — uptime, meminfo, cpuinfo, stat, ...)
├── root/
├── run/
├── sbin/
│   └── init
├── sys/
├── tmp/
├── usr/
│   ├── bin/
│   ├── lib/python3.12/
│   ├── share/
│   └── src/
└── var/
    ├── log/
    └── lib/apt/
shell.refreshProcFs();   // refresh /proc/* with current system state
shell.syncPasswd();      // sync /etc/passwd|group|shadow from user manager

VFS PATH Resolution

When a command is not a built-in, the shell searches $PATH in the VFS — binaries installed by apt become immediately accessible.

  • /sbin and /usr/sbin are excluded from $PATH for non-root users.
  • Stubs containing exec builtin <name> delegate to the named built-in TS command.

Package-gated commands (require apt install):

| Command | Package | |---------|---------| | node | nodejs | | python3 / python | python3 | | npm / npx | npm |

Built-in Package Registry (25 packages)

| Package | Version | Section | |---------|---------|---------| | vim | 2:9.0.1378-2 | editors | | git | 1:2.39.2-1 | vcs | | python3 | 3.11.2-1 | python | | nodejs | 18.19.0 | javascript | | npm | 9.2.0 | javascript | | curl | 7.88.1-10 | web | | wget | 1.21.3-1 | web | | htop | 3.2.2-2 | utils | | openssh-client | 1:9.2p1-2 | net | | openssh-server | 1:9.2p1-2 | net | | net-tools | 2.10-0.1 | net | | iputils-ping | 3:20221126-1 | net | | jq | 1.6-2.1 | utils | | build-essential | 12.9 | devel | | gcc | 4:12.2.0-3 | devel | | g++ | 4:12.2.0-3 | devel | | make | 4.3-4.1 | devel | | less | 590-2 | text | | unzip | 6.0-28 | utils | | rsync | 3.2.7-1 | net | | tmux | 3.3a-3 | utils | | tree | 2.1.0-1 | utils | | ca-certificates | 20230311 | misc | | sudo | 1.9.13p3-1 | admin | | systemd | 252.22-1 | admin |


Environment Variables

| Variable | Default | Description | |----------|---------|-------------| | SSH_MIMIC_FAST_PASSWORD_HASH | "" | Use SHA-256 instead of scrypt (dev only). Set to 1 or true. | | SSH_MIMIC_AUTO_SUDO_NEW_USERS | "true" | Auto-grant sudo to new users. Set to 0, false, no, or off to disable. | | DEV_MODE | "" | Enable performance logging. | | RENDER_PERF | "" | Enable render performance logging. |

Constructor Options

new VirtualShell(hostname, properties?, vfsOptions?)
// vfsOptions: { mode: "memory"|"fs", snapshotPath?: string }

new VirtualSshServer({
  port, hostname?,
  shell?,
  maxAuthAttempts?,   // default: 5
  lockoutDurationMs?, // default: 60_000
})

new VirtualSftpServer({ port, hostname?, shell? })
// or: { port, vfs, users } without a shell instance

bun ./benchmark-virtualshell.ts

Reports shell initialization time, command execution time, and RSS memory by concurrency. Designed to scale to 1000+ parallel environments.

  • SSH server is event-driven — handles many concurrent connections.
  • SshClient is sequential per instance — create multiple for parallel operations.
  • Each VirtualShell is fully independent.

Tips:

  • Use SSH_MIMIC_FAST_PASSWORD_HASH=1 in tests to skip scrypt overhead.
  • Reuse long-lived shell instances for low-latency command bursts.

Each VirtualShell holds its VFS tree, process table, sessions, and file descriptors entirely in the JS heap. For long-running servers or multi-shell deployments, two mechanisms keep memory in check:

Idle freeze/thaw

After a configurable period of inactivity, the VFS tree is serialised to a compact binary buffer and the live object graph is released. On the next command the tree is reconstructed in ~0.1 ms.

await shell.ensureInitialized();
shell.enableIdleManagement({
  idleThresholdMs: 60_000,   // freeze after 60s of inactivity
  checkIntervalMs: 15_000,   // check every 15s
});
// Events: shell.on("shell:freeze", ...) / shell.on("shell:thaw", ...)

Garbage collector

The GC is built into IdleManager and runs automatically when idle management is enabled. Each cycle (default every 30s) performs four steps in order:

  1. Scan process table — find all processes with status === "done" and remove them.
  2. Clean stale CPU entries — remove CPU time tracking for PIDs that no longer exist.
  3. Evict closed large files — walk the VFS tree and zero out file contents that exceed the eviction threshold and have no open file descriptors. Evicted files are reloaded on demand from the snapshot.
  4. Trigger Node.js GC — if the runtime was started with --expose-gc, call globalThis.gc() to force V8 to reclaim memory immediately.
shell.enableIdleManagement({
  idleThresholdMs: 60_000,
  gcIntervalMs: 30_000,  // GC runs every 30s (0 = disabled)
});

The GC timer is unref()'d so it never blocks process.exit().

Manual trigger and stats:

const idle = new IdleManager(shell);
const stats = idle.runGc();
// stats: {
//   terminatedProcesses: 3,   // process records removed this cycle
//   staleCpuEntries: 1,       // orphaned CPU time entries cleared
//   evictedFiles: 2,          // large files zeroed out (no open FDs)
//   forcedGc: false,          // true only when --expose-gc is passed to Node/Bun
// }

idle.on("gc:run", (stats) => console.log(stats));

Note: In "fs" mode the existing evictionThresholdBytes option (default 64 KB) already evicts large files after each flushMirror(). The GC extends this by also checking open file descriptor state — a large file that is currently open by a running command will NOT be evicted, even if it exceeds the threshold.


import type {
  VfsOptions, VfsPersistenceMode,
  VfsSnapshot, VfsNodeStats, VfsFileNode, VfsDirectoryNode,
  WriteFileOptions, RemoveOptions,
  CommandContext, CommandResult, CommandMode, CommandOutcome,
  ShellModule, ShellEnv, SudoChallenge, NanoEditorSession,
  AuditLogEntry, HoneyPotStats,
  ShellStream, ExecStream,
} from "typescript-virtual-container";

Error: listen EADDRINUSE :::2222 — port already in use. Change port or stop existing process.

Auth always fails — root has no password by default. Verify with users.verifyPassword(username, password). Check lockout: ssh.clearLockout(ip).

Command 'xyz' not found (exit 127) — not a registered built-in and not found in VFS $PATH. Register with shell.addCommand() or apt install the package.

Shell scripting — if block not working — ensure each keyword is on its own line or separated by ;.

snapshotPath required error — set mode: "fs" without snapshotPath: new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" }).

Variables not persisting between exec() callsSshClient shares one ShellEnv per shell instance. export in one call is visible in the next. For isolation, create a new SshClient.

Too many levels of symbolic links — symlink chain exceeds 8 hops. Check for circular links or increase maxDepth in resolveSymlink().


What exactly is this project? A self-contained Linux environment implemented entirely in TypeScript. It is not a mock, a stub, or a thin SSH wrapper — it is a full virtual container with its own filesystem (VirtualFileSystem), user and permission system (VirtualUserManager), shell interpreter, package manager, network layer, process table, and desktop environment. Each VirtualShell instance is an independent container that runs anywhere TypeScript runs: Node.js, Bun, or the browser.

How is this different from a Docker container or a real VM? Docker and VMs enforce kernel-level isolation and run real binaries. This project runs entirely in the JavaScript runtime — no subprocess is ever spawned, no host binary is called. The trade-off is intentional: you get zero-dependency portability (including the browser), a fully inspectable and programmable environment, and no host OS requirements. It is not a security boundary.

Is the environment fully isolated? No — and this is important to understand before exposing it to untrusted input:

  • Network is real. curl and wget use the host's fetch() — requests reach the actual internet.
  • The JS heap is shared. All VirtualShell instances run in the same JavaScript runtime as your application. There is no memory boundary between them or between the shell and the host.
  • No resource enforcement. CPU usage and memory consumption are not capped by the container itself — a runaway loop consumes real host resources.
  • Filesystem isolation holds in "memory" mode (the VFS never touches the host FS) and is limited to one .vfsb file in "fs" mode.

In short: the shell commands are sandboxed, the runtime is not.

Can I use this in production? Yes, for the right use cases: automated test harnesses, interactive tutorials, training environments, honeypots, and browser-based shell experiences. Do not expose it to arbitrary untrusted input without additional isolation at the infrastructure level — it is not a security boundary.

Does the VFS touch the host filesystem? In "memory" mode: never. In "fs" mode: exactly one binary file (vfs-snapshot.vfsb) inside snapshotPath. In the browser: IndexedDB only.

Can I run multiple isolated environments in parallel? Yes. Each new VirtualShell(...) is fully independent — separate VFS, user database, environment state, and session manager. They share no global state.

Does the shell support &&, ||, ;, and &? Yes — plus pipes, redirections, if/for/while/until/case, function definitions, arrays, brace expansion, glob expansion, heredocs, set -e/set -x, and background jobs (cmd &).

Can I use this for honeypot deployments? Yes — HoneyPot.attach() captures every command, file write, auth attempt, and session event with timestamps. Configure maxAuthAttempts to throttle scanners. Export the audit log on shutdown.

Is SFTP fully supported? Core operations are implemented. Extended attributes and symlinks return OP_UNSUPPORTED.

Does the desktop work outside the browser? No — startxfce4, thunar, and mousepad require a DOM. In Node.js/Bun they return an error immediately. The DesktopManager is only instantiated in the web shell example (demo/app.ts).

What does the desktop simulate? A single-user XFCE session: a panel with Applications menu and clock, draggable windows, a file manager (Thunar) with navigation, right-click context menu, rename, and trash, a text editor (Mousepad) with Ctrl+S save, and terminal windows each backed by a real interactive shell session against the same VFS.


  1. Fork and create a feature branch: git checkout -b feat/my-feature
  2. Add changes and tests.
  3. Format and lint: bun format && bun check
  4. Push and open a PR.

Standards: Biome formatting · full TypeScript (no any) · ✅ JSDoc on all built-in commands · async/await · description and category fields on new ShellModule · unit tests (prioritized for core features; advanced tests planned).


  • Passwords hashed with scrypt (N=32768, r=8, p=1) with random per-user salt.
  • Root account always exists and cannot be deleted.
  • Per-IP rate limiting prevents automated brute-force on the SSH server.
  • Account lockout — accounts are locked after 5 consecutive failed login attempts.
  • Password aging — configurable min/max age, warning period, and account expiry via chage.
  • Account lock/unlock — password hash prefixed with ! to disable authentication.
  • Sudo timestamp caching — 5-minute credential cache matching real sudo behavior.
  • Group-based permissions — supplementary groups tracked in /etc/group with full membership management.
  • This project does not provide kernel-level or process-level isolation. All VirtualShell instances share the same JS heap as the host application.
  • curl and wget issue real network requests via fetch() — they are not sandboxed.
  • Do not expose to untrusted input without additional infrastructure-level isolation.

Vulnerability reports: contact maintainers privately before public disclosure — see SECURITY.md.


  • Node.js: >=18 · Bun: Supported · TypeScript: >=5.0
  • OS: Linux, macOS, Windows (via Node/Bun)

License

MIT — see LICENSE.