typescript-virtual-container 1.4.1 → 1.4.3

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.
Files changed (67) hide show
  1. package/.vscode/settings.json +2 -0
  2. package/README.md +77 -36
  3. package/benchmark-virtualshell.ts +3 -11
  4. package/builds/self-standalone.js +224 -224
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +23 -23
  7. package/builds/standalone-wo-sftp.js.map +3 -3
  8. package/builds/standalone.js +23 -23
  9. package/builds/standalone.js.map +3 -3
  10. package/dist/VirtualFileSystem/index.d.ts +47 -0
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +159 -0
  13. package/dist/VirtualShell/index.d.ts +29 -0
  14. package/dist/VirtualShell/index.d.ts.map +1 -1
  15. package/dist/VirtualShell/index.js +29 -0
  16. package/dist/VirtualShell/shell.d.ts.map +1 -1
  17. package/dist/VirtualShell/shell.js +6 -10
  18. package/dist/VirtualShell/shellParser.js +28 -1
  19. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  20. package/dist/VirtualUserManager/index.js +4 -4
  21. package/dist/commands/export.d.ts.map +1 -1
  22. package/dist/commands/export.js +5 -3
  23. package/dist/commands/helpers.js +1 -1
  24. package/dist/commands/history.js +2 -2
  25. package/dist/commands/registry.d.ts.map +1 -1
  26. package/dist/commands/registry.js +2 -0
  27. package/dist/commands/runtime.d.ts.map +1 -1
  28. package/dist/commands/runtime.js +28 -3
  29. package/dist/commands/seq.d.ts +4 -0
  30. package/dist/commands/seq.d.ts.map +1 -0
  31. package/dist/commands/seq.js +50 -0
  32. package/dist/commands/sh.d.ts +0 -6
  33. package/dist/commands/sh.d.ts.map +1 -1
  34. package/dist/commands/sh.js +153 -10
  35. package/dist/modules/linuxRootfs.d.ts +1 -1
  36. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  37. package/dist/modules/linuxRootfs.js +5 -5
  38. package/dist/self-standalone.js +149 -102
  39. package/dist/types/pipeline.d.ts +6 -0
  40. package/dist/types/pipeline.d.ts.map +1 -1
  41. package/dist/types/vfs.d.ts +15 -0
  42. package/dist/types/vfs.d.ts.map +1 -1
  43. package/dist/utils/expand.d.ts +9 -0
  44. package/dist/utils/expand.d.ts.map +1 -1
  45. package/dist/utils/expand.js +84 -2
  46. package/dist/utils/tokenize.d.ts.map +1 -1
  47. package/dist/utils/tokenize.js +40 -0
  48. package/package.json +1 -1
  49. package/src/VirtualFileSystem/index.ts +164 -1
  50. package/src/VirtualShell/index.ts +36 -0
  51. package/src/VirtualShell/shell.ts +6 -11
  52. package/src/VirtualShell/shellParser.ts +26 -1
  53. package/src/VirtualUserManager/index.ts +4 -4
  54. package/src/commands/export.ts +5 -3
  55. package/src/commands/helpers.ts +1 -1
  56. package/src/commands/history.ts +2 -2
  57. package/src/commands/registry.ts +2 -0
  58. package/src/commands/runtime.ts +30 -3
  59. package/src/commands/seq.ts +43 -0
  60. package/src/commands/sh.ts +144 -19
  61. package/src/modules/linuxRootfs.ts +6 -6
  62. package/src/self-standalone.ts +190 -141
  63. package/src/types/pipeline.ts +6 -0
  64. package/src/types/vfs.ts +17 -0
  65. package/src/utils/expand.ts +75 -2
  66. package/src/utils/tokenize.ts +20 -0
  67. package/tests/helpers.test.ts +3 -3
@@ -9,6 +9,8 @@
9
9
 
10
10
  ".ssh-mimic": true,
11
11
  ".vfs": true,
12
+ "builds": true,
13
+ "examples": true,
12
14
 
13
15
  "LICENSE": true,
14
16
  "tsconfig.tsbuildinfo": true
package/README.md CHANGED
@@ -24,6 +24,7 @@
24
24
  - [`VirtualSftpServer`](#virtualsftpserver)
25
25
  - [`VirtualShell`](#virtualshell)
26
26
  - [`VirtualFileSystem`](#virtualfilesystem)
27
+ - [Mount API](#mount-api)
27
28
  - [`VirtualUserManager`](#virtualusermanager)
28
29
  - [`VirtualPackageManager`](#virtualpackagemanager)
29
30
  - [Snapshot Diff Tooling](#snapshot-diff-tooling)
@@ -53,7 +54,7 @@
53
54
  | Mode | Entry point | Use case |
54
55
  |------|-------------|----------|
55
56
  | **SSH/SFTP server** | `VirtualSshServer` / `VirtualSftpServer` | Honeypots, remote testing, training environments |
56
- | **Web shell** | `builds/web.min.js` (browser bundle) | Embedded terminals, interactive tutorials, browser demos |
57
+ | **Web shell** | `builds/web.min.js` / `builds/web-full-api.min.js` (ESM) | Embedded terminals, interactive tutorials, browser demos |
57
58
  | **Standalone CLI** | `builds/self-standalone.js` (single file) | Local shell, one-liner demos, no install required |
58
59
 
59
60
  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.
@@ -82,6 +83,10 @@ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-cont
82
83
  curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/standalone-wo-sftp.js -o standalone.js && node standalone.js && rm -f standalone.js
83
84
  ```
84
85
 
86
+ > [!NOTE]
87
+ > 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.
88
+ > It implies command `man` is not available in standalone mode because manuals are not included in the minimal build, but you can always refer to the [API Reference](#api-reference) for detailed documentation.
89
+
85
90
  **`self-standalone.js` options:**
86
91
 
87
92
  ```bash
@@ -107,24 +112,22 @@ await ssh.start();
107
112
 
108
113
  ### Web shell (browser)
109
114
 
110
- Three browser bundles are available — pick the one that fits your deployment:
115
+ Two browser bundles are available:
111
116
 
112
- | Bundle | Format | Entry | Use case |
113
- |--------|--------|-------|----------|
114
- | `builds/web.min.js` | ESM | `createWebShell()` | Local dev, modern bundlers |
115
- | `builds/web-iife.min.js` | IIFE (`WebShellLib`) | `WebShellLib.createWebShell()` | Cloudflare, CDN, reverse proxies |
116
- | `builds/web-full-api.min.js` | ESM | `createVirtualShellShim()` | API surface close to `VirtualShell` |
117
+ | Bundle | Format | Entry point | Use case |
118
+ |--------|--------|-------------|----------|
119
+ | `builds/web.min.js` | ESM | `createWebShell()` | Embedded terminals, modern bundlers |
120
+ | `builds/web-full-api.min.js` | ESM | `createVirtualShellShim()` | Full `VirtualShell`-like API in the browser |
117
121
 
118
- All bundles persist the VFS in **IndexedDB** — state survives page reloads.
122
+ Both bundles persist the VFS in **IndexedDB** — state survives page reloads.
119
123
 
120
124
  ```bash
121
- bun run web-build # → builds/web.min.js + examples/web.min.js
122
- bun run web-build-iife # → builds/web-iife.min.js
123
- bun run web-full-build # builds/web-full-api.min.js
124
- bun run build-all # rebuild everything
125
+ bun run web-build # → builds/web.min.js + examples/web.min.js
126
+ bun run web-full-build # → builds/web-full-api.min.js
127
+ bun run build-all # rebuild everything
125
128
  ```
126
129
 
127
- **ESM (`web.min.js`)**
130
+ **`web.min.js`** — lightweight shell with IndexedDB VFS:
128
131
 
129
132
  ```html
130
133
  <script type="module">
@@ -140,19 +143,7 @@ bun run build-all # rebuild everything
140
143
  </script>
141
144
  ```
142
145
 
143
- **IIFE (`web-iife.min.js`)**no `type="module"` needed, works through Cloudflare:
144
-
145
- ```html
146
- <script src="./builds/web-iife.min.js"></script>
147
- <script>
148
- const shell = WebShellLib.createWebShell("web-vm");
149
- shell.ensureInitialized().then(() =>
150
- shell.executeCommandLine("whoami").then(r => console.log(r.stdout))
151
- );
152
- </script>
153
- ```
154
-
155
- **Full API shim (`web-full-api.min.js`)** — mirrors the `VirtualShell` programmatic API:
146
+ **`web-full-api.min.js`**mirrors the `VirtualShell` programmatic API:
156
147
 
157
148
  ```html
158
149
  <script type="module">
@@ -161,8 +152,7 @@ bun run build-all # rebuild everything
161
152
  const shell = createVirtualShellShim("web-vm");
162
153
  await shell.ensureInitialized();
163
154
  await shell.executeCommandLine("mkdir -p /app && echo hello > /app/file.txt");
164
- const vfs = shell.getVfs();
165
- console.log(vfs.readFile("/app/file.txt")); // hello
155
+ console.log(shell.getVfs().readFile("/app/file.txt")); // hello
166
156
  </script>
167
157
  ```
168
158
 
@@ -170,8 +160,7 @@ bun run build-all # rebuild everything
170
160
 
171
161
  ```bash
172
162
  bun run example-serve
173
- # Open http://localhost:8787/index.html (ESM)
174
- # or http://localhost:8787/index-cf.html (IIFE, Cloudflare-compatible)
163
+ # Open http://localhost:8787/index.html
175
164
  ```
176
165
 
177
166
  ### Programmatic API
@@ -323,6 +312,9 @@ interface ShellProperties {
323
312
  | `writeFileAsUser(authUser, path, content)` | Write with quota enforcement. |
324
313
  | `refreshProcFs(): void` | Refresh all `/proc/*` files (uptime, meminfo, cpuinfo, per-pid). |
325
314
  | `refreshProcSessions(): void` | Lightweight refresh of `/proc/<pid>` and `/proc/self` only. |
315
+ | `mount(vPath, hostPath, options?): void` | Mount a host directory into the VFS. See [Mount API](#mount-api). |
316
+ | `unmount(vPath): void` | Remove a host directory mount. |
317
+ | `getMounts()` | List all active mounts as `{ vPath, hostPath, readOnly }[]`. |
326
318
  | `syncPasswd(): void` | Sync `/etc/passwd`, `/etc/group`, `/etc/shadow` from user manager. |
327
319
  | `getVfs(): VirtualFileSystem \| null` | Access VFS instance. |
328
320
  | `getUsers(): VirtualUserManager \| null` | Access user manager. |
@@ -394,6 +386,40 @@ await vfs.flushMirror(); // save to disk
394
386
 
395
387
  **Events:** `file:write { path, size }`, `file:read { path, size }`, `dir:create { path, mode }`, `node:remove { path }`, `symlink:create { link, target }`, `snapshot:import`, `snapshot:restore { path }`, `mirror:flush { path? }`
396
388
 
389
+
390
+ #### Mount API
391
+
392
+ Mount a host directory inside the VM — all standard VFS operations (`readFile`, `writeFile`, `exists`, `stat`, `list`) are transparently delegated to the host filesystem.
393
+
394
+ > **Node.js only.** In browser environments `mount()` is a silent no-op — the `vPath` remains an empty in-memory directory.
395
+
396
+ ```typescript
397
+ // Read-only mount (default) — shell commands can read host files
398
+ shell.mount("/workspace", "./my-project");
399
+
400
+ // Read-write mount — shell commands can also write back to the host
401
+ shell.mount("/data", "./data", { readOnly: false });
402
+
403
+ // Unmount — delegation removed, vPath stays as an empty VFS directory
404
+ shell.unmount("/workspace");
405
+
406
+ // Introspect
407
+ shell.getMounts();
408
+ // → [{ vPath: "/workspace", hostPath: "/abs/path/my-project", readOnly: true }]
409
+ ```
410
+
411
+ Direct VFS usage:
412
+
413
+ ```typescript
414
+ shell.vfs.mount("/workspace", "./my-project");
415
+ shell.vfs.unmount("/workspace");
416
+ shell.vfs.getMounts();
417
+ ```
418
+
419
+ **Events:** `mount { vPath, hostPath, readOnly }`, `unmount { vPath }`
420
+
421
+ **Snapshot behaviour:** mounted files are **not** included in `toSnapshot()` — only the in-memory VFS tree is serialised. The mount configuration itself is also not persisted; restore it after each `fromSnapshot()` or `restoreMirror()`.
422
+
397
423
  #### VFSB Binary Format
398
424
 
399
425
  In `"fs"` mode, state is persisted as a compact binary file (`vfs-snapshot.vfsb`).
@@ -427,7 +453,7 @@ new VirtualUserManager(
427
453
  )
428
454
  ```
429
455
 
430
- Auth data is stored at `/virtual-env-js/.auth/` inside the VFS.
456
+ Auth data is stored at `/etc/` inside the VFS.
431
457
 
432
458
  **Methods**
433
459
 
@@ -650,6 +676,17 @@ interface VirtualActiveSession {
650
676
  remoteAddress: string;
651
677
  startedAt: string; // ISO-8601
652
678
  }
679
+
680
+ /** Returned by adduser, passwd, deluser — triggers interactive password prompt in the terminal. */
681
+ interface PasswordChallenge {
682
+ preamble?: string; // Lines printed before the first prompt
683
+ prompt: string; // e.g. "New password: "
684
+ confirmPrompt?: string; // Second prompt for confirmation
685
+ confirmText?: string; // Destructive confirmation prompt (y/N)
686
+ action: "adduser" | "passwd" | "deluser" | "su";
687
+ targetUsername: string;
688
+ newUsername?: string; // adduser only
689
+ }
653
690
  ```
654
691
 
655
692
  ---
@@ -928,7 +965,7 @@ echo "Welcome back, root!"
928
965
  <details>
929
966
  <summary><strong>Built-in Commands (91)</strong></summary>
930
967
 
931
- Type `help` in the shell for a grouped, colorized listing. Type `help <command>` for detailed usage.
968
+ Type `help` in the shell for a grouped, colorized listing. Type `help <command>` for detailed usage. Type `man <command>` for full manual pages — all 91 commands are documented.
932
969
 
933
970
  ### Navigation
934
971
 
@@ -1436,13 +1473,17 @@ Open:
1436
1473
  - [x] Pure in-memory VFS · symlinks · binary snapshot format (VFSB, ~27% smaller than JSON+base64)
1437
1474
  - [x] Linux rootfs on boot — `/etc`, `/proc`, `/sys`, `/dev`, `/usr`, `/var`
1438
1475
  - [x] Virtual package manager — `apt`/`dpkg`, 25 packages, VFS file writes
1439
- - [x] 91 built-in commands across 9 categories
1440
- - [x] Real shell interpreter — `if`/`for`/`while`/`case`/functions, `$(cmd)`, `$((expr))`, `${#VAR}`, single-quote isolation
1476
+ - [x] 92 built-in commands across 9 categories (`seq` added)
1477
+ - [x] Real shell interpreter — `if`/`for`/`while`/`case`/functions, `$(cmd)`, `$((expr))`, `${#VAR}`, `{a,b,c}` brace expansion, `{1..N}` ranges, `2>/dev/null` stderr redirect, `2>&1`, `(( x++ ))`
1441
1478
  - [x] `curl`/`wget` as pure `fetch()` · VFS PATH resolution · `/sbin` root-only
1442
1479
  - [x] `/proc/self` and `/proc/<pid>` per-session entries
1443
1480
  - [x] Snapshot diff tooling — `diffSnapshots`, `formatDiff`, `assertDiff`
1444
1481
  - [x] `node`/`python3`/`npm`/`npx` — package-gated virtual REPL stubs
1445
- - [x] Web shell bundle (`web.min.js`) — fully browser-native with IndexedDB VFS
1446
- - [x] Self-standalone CLI (`self-standalone.js`) — single-file interactive shell, no install
1482
+ - [x] Web shell bundles (`web.min.js`, `web-full-api.min.js`) — fully browser-native with IndexedDB VFS
1483
+ - [x] Self-standalone CLI (`self-standalone.js`) — single-file interactive shell, per-user history, tab completion
1484
+ - [x] 120+ `man` pages — all built-in commands documented via `man <cmd>`
1485
+ - [x] Shared `tokenize.ts` — unified tokenizer for shell parser and runtime (eliminates duplication)
1486
+ - [x] `PasswordChallenge` type — generic interactive password flow for `adduser`, `passwd`, `deluser`
1487
+ - [x] `VirtualFileSystem.mount(vPath, hostPath, { readOnly })` — bind-mount host directories into the VM; read-only by default; browser-safe (silent no-op)
1447
1488
 
1448
1489
  </details>
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env bun
2
-
3
- import { mkdirSync, rmSync } from "node:fs";
4
- import { join } from "node:path";
5
2
  import { VirtualShell } from "./src/index.ts";
6
3
 
7
4
  const counts = [1, 2, 5, 10, 20, 50, 100];
8
- const rootBenchmarkPath = join(process.cwd(), ".benchmark-shells");
9
5
 
10
6
  function bytesToMb(bytes: number): string {
11
7
  return `${Math.round(bytes / 1024 / 1024)} MB`;
12
8
  }
13
9
 
14
10
  async function createShell(baseName: string, index: number): Promise<VirtualShell> {
15
- const basePath = join(rootBenchmarkPath, `${baseName}-${index}`);
16
- mkdirSync(basePath, { recursive: true });
17
- const shell = new VirtualShell(`${baseName}-${index}`, undefined, basePath);
11
+ const shell = new VirtualShell(`${baseName}-${index}`, undefined, {
12
+ mode: "memory",
13
+ });
18
14
  await shell.ensureInitialized();
19
15
  return shell;
20
16
  }
@@ -59,8 +55,6 @@ async function runSingleBenchmark(count: number) {
59
55
  }
60
56
 
61
57
  async function main() {
62
- rmSync(rootBenchmarkPath, { recursive: true, force: true });
63
- mkdirSync(rootBenchmarkPath, { recursive: true });
64
58
 
65
59
  console.log("Benchmarking VirtualShell concurrency:\n");
66
60
  const results = [];
@@ -86,8 +80,6 @@ async function main() {
86
80
  `${row.count}\t${row.initMs}\t${row.commandMs}\t${bytesToMb(row.initRss)}\t${bytesToMb(row.finalRss)}\t${bytesToMb(row.deltaRss)}`,
87
81
  );
88
82
  }
89
-
90
- console.log(`\nBenchmark data stored under ${rootBenchmarkPath}`);
91
83
  }
92
84
 
93
85
  main().catch((error) => {