typescript-virtual-container 1.5.4 → 1.5.6

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 (40) hide show
  1. package/README.md +107 -541
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/VirtualShell/shell.js +158 -11
  4. package/dist/commands/awk.d.ts +6 -11
  5. package/dist/commands/awk.js +462 -109
  6. package/dist/commands/bc.d.ts +2 -0
  7. package/dist/commands/bc.js +28 -0
  8. package/dist/commands/bzip2.d.ts +11 -0
  9. package/dist/commands/bzip2.js +91 -0
  10. package/dist/commands/find.d.ts +2 -2
  11. package/dist/commands/find.js +212 -37
  12. package/dist/commands/{ifconfig.d.ts → ip.d.ts} +1 -1
  13. package/dist/commands/{ifconfig.js → ip.js} +3 -3
  14. package/dist/commands/jobs.d.ts +4 -0
  15. package/dist/commands/jobs.js +27 -0
  16. package/dist/commands/lsof.d.ts +6 -0
  17. package/dist/commands/lsof.js +30 -0
  18. package/dist/commands/perl.d.ts +6 -0
  19. package/dist/commands/perl.js +76 -0
  20. package/dist/commands/registry.js +22 -3
  21. package/dist/commands/runtime.js +2 -1
  22. package/dist/commands/sed.d.ts +2 -2
  23. package/dist/commands/sed.js +216 -34
  24. package/dist/commands/set.js +20 -0
  25. package/dist/commands/sh.js +111 -1
  26. package/dist/commands/strace.d.ts +6 -0
  27. package/dist/commands/strace.js +26 -0
  28. package/dist/commands/tar.d.ts +2 -1
  29. package/dist/commands/tar.js +138 -52
  30. package/dist/commands/zip.d.ts +11 -0
  31. package/dist/commands/zip.js +232 -0
  32. package/dist/modules/linuxRootfs.js +1 -1
  33. package/dist/utils/expand.js +67 -1
  34. package/package.json +5 -4
  35. package/dist/self-standalone.d.ts +0 -1
  36. package/dist/self-standalone.js +0 -444
  37. package/dist/standalone-wo-sftp.d.ts +0 -1
  38. package/dist/standalone-wo-sftp.js +0 -30
  39. package/dist/standalone.d.ts +0 -1
  40. package/dist/standalone.js +0 -61
package/README.md CHANGED
@@ -20,21 +20,9 @@
20
20
  - [Web shell (browser)](#web-shell-browser)
21
21
  - [Programmatic API](#programmatic-api)
22
22
  - [How It Works](#how-it-works)
23
- - [API Reference](#api-reference) *(open by default)*
24
- - [`VirtualSshServer`](#virtualsshserver)
25
- - [`VirtualSftpServer`](#virtualsftpserver)
26
- - [`VirtualShell`](#virtualshell)
27
- - [`VirtualFileSystem`](#virtualfilesystem)
28
- - [Mount API](#mount-api)
29
- - [`VirtualUserManager`](#virtualusermanager)
30
- - [`VirtualPackageManager`](#virtualpackagemanager)
31
- - [Snapshot Diff Tooling](#snapshot-diff-tooling)
32
- - [`HoneyPot`](#honeypot)
33
- - [`SshClient`](#sshclient-programmatic-api)
34
- - [Key Types](#key-types)
35
- - [Command Helpers](#command-helpers)
23
+ - [API Reference](#api-reference)
36
24
  - [Examples](#examples)
37
- - [Built-in Commands (106)](#built-in-commands-106)
25
+ - [Built-in Commands (118)](#built-in-commands-118)
38
26
  - [Shell Scripting](#shell-scripting)
39
27
  - [Linux Rootfs & VFS PATH Resolution](#linux-rootfs--vfs-path-resolution)
40
28
  - [Configuration](#configuration)
@@ -56,8 +44,8 @@
56
44
  | Mode | Entry point | Use case |
57
45
  |------|-------------|----------|
58
46
  | **SSH/SFTP server** | `VirtualSshServer` / `VirtualSftpServer` | Honeypots, remote testing, training environments |
59
- | **Web shell** | `builds/fortune-nyx-v1.5.4-web.min.js` (ESM) | Embedded terminals, interactive tutorials, browser demos |
60
- | **Standalone CLI** | `builds/fortune-nyx-v1.5.4-directbash-k6.1.0.mjs` (single file) | Local shell, one-liner demos, no install required |
47
+ | **Web shell** | `builds/fortune-nyx-v1.5.6-web.min.js` (ESM) | Embedded terminals, interactive tutorials, browser demos |
48
+ | **Standalone CLI** | `builds/fortune-nyx-v1.5.6-directbash-k6.1.0.mjs` (single file) | Local shell, one-liner demos, no install required |
61
49
  <!-- /BUILD:mode-table -->
62
50
 
63
51
  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.
@@ -78,17 +66,17 @@ npm install typescript-virtual-container
78
66
  <!-- BUILD:curl-start -->
79
67
  #### Interactivea local shell — persists VFS in .vfs/ in the current directory
80
68
  ```bash
81
- curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.5.4-directbash-k6.1.0.mjs -o fortune-nyx-v1.5.4-directbash-k6.1.0.mjs && node fortune-nyx-v1.5.4-directbash-k6.1.0.mjs
69
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.5.6-directbash-k6.1.0.mjs -o fortune-nyx-v1.5.6-directbash-k6.1.0.mjs && node fortune-nyx-v1.5.6-directbash-k6.1.0.mjs
82
70
  ```
83
71
 
84
72
  #### SSH server (connect with any SSH client on port 2222)
85
73
  ```bash
86
- curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.5.4-ssh.cjs -o fortune-nyx-v1.5.4-ssh.cjs && node fortune-nyx-v1.5.4-ssh.cjs
74
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.5.6-ssh.cjs -o fortune-nyx-v1.5.6-ssh.cjs && node fortune-nyx-v1.5.6-ssh.cjs
87
75
  ```
88
76
 
89
77
  #### SSH server without SFTP (lighter build)
90
78
  ```bash
91
- curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.5.4-ssh-nosftp.js -o fortune-nyx-v1.5.4-ssh-nosftp.js && node fortune-nyx-v1.5.4-ssh-nosftp.js
79
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/fortune-nyx-v1.5.6-ssh-nosftp.js -o fortune-nyx-v1.5.6-ssh-nosftp.js && node fortune-nyx-v1.5.6-ssh-nosftp.js
92
80
  ```
93
81
  <!-- /BUILD:curl-start -->
94
82
 
@@ -96,13 +84,13 @@ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-cont
96
84
  > 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.
97
85
 
98
86
  <!-- BUILD:selfStandalone-options -->
99
- **`fortune-nyx-v1.5.4-directbash-k6.1.0.mjs` options:**
87
+ **`fortune-nyx-v1.5.6-directbash-k6.1.0.mjs` options:**
100
88
 
101
89
  ```bash
102
- node fortune-nyx-v1.5.4-directbash-k6.1.0.mjs # boot as root
103
- node fortune-nyx-v1.5.4-directbash-k6.1.0.mjs --user alice # boot as alice (prompts for password if set)
104
- node fortune-nyx-v1.5.4-directbash-k6.1.0.mjs --user=alice # same, inline form
105
- SSH_MIMIC_HOSTNAME=my-box node fortune-nyx-v1.5.4-directbash-k6.1.0.mjs # custom hostname
90
+ node fortune-nyx-v1.5.6-directbash-k6.1.0.mjs # boot as root
91
+ node fortune-nyx-v1.5.6-directbash-k6.1.0.mjs --user alice # boot as alice (prompts for password if set)
92
+ node fortune-nyx-v1.5.6-directbash-k6.1.0.mjs --user=alice # same, inline form
93
+ SSH_MIMIC_HOSTNAME=my-box node fortune-nyx-v1.5.6-directbash-k6.1.0.mjs # custom hostname
106
94
  ```
107
95
  <!-- /BUILD:selfStandalone-options -->
108
96
 
@@ -129,7 +117,7 @@ Two browser bundles are available:
129
117
  <!-- BUILD:web-table -->
130
118
  | Bundle | Format | Entry point | Use case |
131
119
  |--------|--------|-------------|----------|
132
- | `builds/fortune-nyx-v1.5.4-web.min.js` | ESM | `createWebShell()` | Embedded terminals, modern bundlers |
120
+ | `builds/fortune-nyx-v1.5.6-web.min.js` | ESM | `createWebShell()` | Embedded terminals, modern bundlers |
133
121
  <!-- /BUILD:web-table -->
134
122
 
135
123
  Both bundles persist the VFS in **IndexedDB** — state survives page reloads.
@@ -141,11 +129,11 @@ bun run build-all # rebuild everything
141
129
  ```
142
130
 
143
131
  <!-- BUILD:web-options -->
144
- **`fortune-nyx-v1.5.4-web.min.js`** — lightweight shell with IndexedDB VFS:
132
+ **`fortune-nyx-v1.5.6-web.min.js`** — lightweight shell with IndexedDB VFS:
145
133
 
146
134
  ```html
147
135
  <script type="module">
148
- import { createWebShell } from "./builds/fortune-nyx-v1.5.4-web.min.js";
136
+ import { createWebShell } from "./builds/fortune-nyx-v1.5.6-web.min.js";
149
137
 
150
138
  const shell = createWebShell("web-vm", {
151
139
  vfs: { databaseName: "virtual-env-js", storeName: "vfs" },
@@ -157,11 +145,11 @@ bun run build-all # rebuild everything
157
145
  </script>
158
146
  ```
159
147
 
160
- **`fortune-nyx-v1.5.4-web.min.js`** — mirrors the `VirtualShell` programmatic API:
148
+ **`fortune-nyx-v1.5.6-web.min.js`** — mirrors the `VirtualShell` programmatic API:
161
149
 
162
150
  ```html
163
151
  <script type="module">
164
- import { createVirtualShellShim } from "./builds/fortune-nyx-v1.5.4-web.min.js";
152
+ import { createVirtualShellShim } from "./builds/fortune-nyx-v1.5.6-web.min.js";
165
153
 
166
154
  const shell = createVirtualShellShim("web-vm");
167
155
  await shell.ensureInitialized();
@@ -242,502 +230,6 @@ console.log(r.stdout);
242
230
  <!-- https://itsrealfortune.fr/typescript-virtual-container/ -->
243
231
  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 <a href="https://itsrealfortune.fr/typescript-virtual-container/">the documentation</a>.
244
232
 
245
- ### `VirtualSshServer`
246
-
247
- ```typescript
248
- new VirtualSshServer({
249
- port: number;
250
- hostname?: string; // default: "typescript-vm"
251
- shell?: VirtualShell; // share state with SFTP server
252
- maxAuthAttempts?: number; // default: 5
253
- lockoutDurationMs?: number; // default: 60_000
254
- })
255
- ```
256
-
257
- If `shell` is omitted, the server creates `new VirtualShell(hostname)` internally.
258
-
259
- **Methods**
260
-
261
- | Method | Description |
262
- |--------|-------------|
263
- | `start(): Promise<number>` | Initialize VFS, users, start listening. Returns bound port. |
264
- | `stop(): void` | Gracefully close server and all active connections. |
265
- | `clearLockout(ip): void` | Manually lift a rate-limit lockout for an IP. |
266
- | `getVfs(): VirtualFileSystem \| null` | Access VFS instance (null before start). |
267
- | `getUsers(): VirtualUserManager \| null` | Access user manager (null before start). |
268
- | `getHostname(): string` | Returns configured hostname. |
269
-
270
- **Events**
271
-
272
- | Event | Data | Description |
273
- |-------|------|-------------|
274
- | `start` | `{ port }` | Server started |
275
- | `stop` | — | Server stopped |
276
- | `auth:success` | `{ username, remoteAddress, method? }` | Authenticated |
277
- | `auth:failure` | `{ username, remoteAddress, reason?, method? }` | Auth failed |
278
- | `auth:lockout` | `{ ip, until: Date }` | IP locked out |
279
- | `client:connect` | — | New SSH client connected |
280
- | `client:disconnect` | `{ user }` | SSH client disconnected |
281
-
282
- ---
283
-
284
- ### `VirtualSftpServer`
285
-
286
- ```typescript
287
- new VirtualSftpServer({
288
- port: number;
289
- hostname?: string;
290
- shell?: VirtualShell; // share state with SSH server (recommended)
291
- vfs?: VirtualFileSystem; // explicit if no shell
292
- users?: VirtualUserManager; // explicit if no shell
293
- })
294
- ```
295
-
296
- Supports `password` and `keyboard-interactive` auth. Confines all operations to `/home/<user>`. Unsupported operations return `OP_UNSUPPORTED`.
297
-
298
- **Methods:** `start(): Promise<number>`, `stop(): void`
299
-
300
- **Events:** `start`, `stop`, `auth:success { username, remoteAddress }`, `auth:failure { username, remoteAddress }`, `client:connect`, `client:disconnect { user }`
301
-
302
- ---
303
-
304
- ### `VirtualShell`
305
-
306
- Coordinates the VFS, user manager, package manager, and command runtime.
307
-
308
- ```typescript
309
- new VirtualShell(
310
- hostname: string,
311
- properties?: ShellProperties, // kernel, os, arch — surfaced by uname/neofetch
312
- vfsOptions?: VfsOptions, // { mode: "memory"|"fs", snapshotPath?: string }
313
- )
314
- ```
315
-
316
- ```typescript
317
- interface ShellProperties {
318
- kernel: string; // e.g. "1.0.0+itsrealfortune+1-amd64"
319
- os: string; // e.g. "Fortune GNU/Linux x64"
320
- arch: string; // e.g. "x86_64"
321
- }
322
- ```
323
-
324
- **Methods**
325
-
326
- | Method | Description |
327
- |--------|-------------|
328
- | `ensureInitialized(): Promise<void>` | Await before using programmatically. |
329
- | `addCommand(name, params, callback)` | Register a custom shell command. |
330
- | `executeCommand(rawInput, authUser, cwd)` | Run a raw command string. |
331
- | `startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize)` | Attach a PTY session. |
332
- | `writeFileAsUser(authUser, path, content)` | Write with quota enforcement. |
333
- | `refreshProcFs(): void` | Refresh all `/proc/*` files (uptime, meminfo, cpuinfo, per-pid). |
334
- | `refreshProcSessions(): void` | Lightweight refresh of `/proc/<pid>` and `/proc/self` only. |
335
- | `mount(vPath, hostPath, options?): void` | Mount a host directory into the VFS. See [Mount API](#mount-api). |
336
- | `unmount(vPath): void` | Remove a host directory mount. |
337
- | `getMounts()` | List all active mounts as `{ vPath, hostPath, readOnly }[]`. |
338
- | `syncPasswd(): void` | Sync `/etc/passwd`, `/etc/group`, `/etc/shadow` from user manager. |
339
- | `getVfs(): VirtualFileSystem \| null` | Access VFS instance. |
340
- | `getUsers(): VirtualUserManager \| null` | Access user manager. |
341
- | `getHostname(): string` | Returns configured hostname. |
342
-
343
- **Public fields**
344
-
345
- | Field | Type | Description |
346
- |-------|------|-------------|
347
- | `vfs` | `VirtualFileSystem` | Backing virtual filesystem. |
348
- | `users` | `VirtualUserManager` | Virtual user database. |
349
- | `packageManager` | `VirtualPackageManager` | APT/dpkg package manager. |
350
- | `hostname` | `string` | Hostname shown in prompt and SSH ident. |
351
- | `properties` | `ShellProperties` | Distro identity strings. |
352
- | `startTime` | `number` | Unix ms timestamp of shell creation. |
353
-
354
- **Events:** `initialized`, `command { command, user, cwd }`, `session:start { user, sessionId, remoteAddress }`
355
-
356
- **Custom command example:**
357
-
358
- ```typescript
359
- shell.addCommand("greet", ["[name]"], ({ args, authUser }) => {
360
- const name = args[0] ?? authUser;
361
- return { stdout: `Hello, ${name}!\n`, exitCode: 0 };
362
- });
363
- ```
364
-
365
- ---
366
-
367
- ### `VirtualFileSystem`
368
-
369
- Pure in-memory virtual filesystem. No host filesystem access at runtime.
370
-
371
- ```typescript
372
- // Memory mode (default) — ephemeral
373
- new VirtualFileSystem()
374
-
375
- // FS mode — persists to binary .vfsb file
376
- new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" })
377
- await vfs.restoreMirror(); // load from disk (no-op if no file yet)
378
- await vfs.flushMirror(); // save to disk
379
- ```
380
-
381
- **Methods**
382
-
383
- | Method | Description |
384
- |--------|-------------|
385
- | `mkdir(path, mode?)` | Create directory and any missing parents. |
386
- | `writeFile(path, content, options?)` | Write file. `options.compress` gzips; `options.mode` sets POSIX mode bits. |
387
- | `readFile(path): string` | Read as UTF-8. Transparently decompresses gzip. |
388
- | `readFileRaw(path): Buffer` | Read as Buffer. |
389
- | `exists(path): boolean` | Test existence. |
390
- | `stat(path): VfsNodeStats` | Returns metadata. |
391
- | `list(path?): string[]` | List direct children (sorted). |
392
- | `tree(path?): string` | Render ASCII directory tree. |
393
- | `move(from, to)` | Move or rename. |
394
- | `remove(path, options?)` | Delete. `options.recursive` required for non-empty dirs. |
395
- | `chmod(path, mode)` | Update POSIX mode bits. |
396
- | `compressFile(path)` / `decompressFile(path)` | Gzip-compress / gunzip in place. |
397
- | `symlink(target, linkPath)` | Create symbolic link. |
398
- | `isSymlink(path): boolean` | Test if path is a symlink. |
399
- | `resolveSymlink(path, maxDepth?): string` | Resolve symlink chain (default max 8 hops). |
400
- | `getUsageBytes(path?): number` | Total stored bytes under a path. |
401
- | `toSnapshot(): VfsSnapshot` | Export tree as JSON-serialisable snapshot. |
402
- | `importSnapshot(snapshot)` | Replace current state from a snapshot. |
403
- | `restoreMirror(): Promise<void>` | Load from disk (`"fs"` mode) / no-op otherwise. |
404
- | `flushMirror(): Promise<void>` | Save to disk (`"fs"` mode) / emit event otherwise. |
405
- | `VirtualFileSystem.fromSnapshot(snapshot)` | **Static.** Create memory-mode instance from snapshot. |
406
-
407
- **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? }`
408
-
409
-
410
- #### Mount API
411
-
412
- Mount a host directory inside the VM — all standard VFS operations (`readFile`, `writeFile`, `exists`, `stat`, `list`) are transparently delegated to the host filesystem.
413
-
414
- > **Node.js only.** In browser environments `mount()` is a silent no-op — the `vPath` remains an empty in-memory directory.
415
-
416
- ```typescript
417
- // Read-only mount (default) — shell commands can read host files
418
- shell.mount("/workspace", "./my-project");
419
-
420
- // Read-write mount — shell commands can also write back to the host
421
- shell.mount("/data", "./data", { readOnly: false });
422
-
423
- // Unmount — delegation removed, vPath stays as an empty VFS directory
424
- shell.unmount("/workspace");
425
-
426
- // Introspect
427
- shell.getMounts();
428
- // → [{ vPath: "/workspace", hostPath: "/abs/path/my-project", readOnly: true }]
429
- ```
430
-
431
- Direct VFS usage:
432
-
433
- ```typescript
434
- shell.vfs.mount("/workspace", "./my-project");
435
- shell.vfs.unmount("/workspace");
436
- shell.vfs.getMounts();
437
- ```
438
-
439
- **Events:** `mount { vPath, hostPath, readOnly }`, `unmount { vPath }`
440
-
441
- **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()`.
442
-
443
- #### VFSB Binary Format
444
-
445
- In `"fs"` mode, state is persisted as a compact binary file (`vfs-snapshot.vfsb`).
446
-
447
- | Metric | JSON+base64 | VFSB binary |
448
- |--------|-------------|-------------|
449
- | File size (10 MB content) | ~13.7 MB | ~10.0 MB |
450
- | Encode time | ~12 ms | ~0.04 ms |
451
- | Decode time | ~18 ms | ~0.07 ms |
452
-
453
- Wire format: 5-byte header (`VFS!` magic + version), followed by a recursive node tree with type, name, mode, timestamps, and raw content bytes (no base64). Legacy JSON snapshots are auto-detected and migrated on first `flushMirror()`.
454
-
455
- Low-level API:
456
- ```typescript
457
- import { encodeVfs, decodeVfs, isBinarySnapshot } from "typescript-virtual-container/src/VirtualFileSystem/binaryPack";
458
- const buf = encodeVfs(vfs.root);
459
- const root = decodeVfs(buf);
460
- isBinarySnapshot(buf); // true — starts with "VFS!" magic
461
- ```
462
-
463
- ---
464
-
465
- ### `VirtualUserManager`
466
-
467
- Manages users, password hashing (scrypt), sudo privileges, storage quotas, SSH public keys, and session tracking.
468
-
469
- ```typescript
470
- new VirtualUserManager(
471
- vfs: VirtualFileSystem,
472
- autoSudoForNewUsers?: boolean, // default: true
473
- )
474
- ```
475
-
476
- Auth data is stored at `/etc/` inside the VFS.
477
-
478
- **Methods**
479
-
480
- | Method | Description |
481
- |--------|-------------|
482
- | `initialize(): Promise<void>` | Load users/sudoers, ensure root exists. |
483
- | `verifyPassword(username, password): boolean` | Check plaintext password. |
484
- | `hasPassword(username): boolean` | Returns `true` if a password is set. |
485
- | `hashPassword(password): string` | Hash with scrypt (or SHA-256 with `SSH_MIMIC_FAST_PASSWORD_HASH=1`). |
486
- | `getPasswordHash(username): string \| null` | Raw stored hash. |
487
- | `addUser(username, password): Promise<void>` | Create user with home directory. |
488
- | `deleteUser(username): Promise<void>` | Delete user. Throws on `root` or missing user. |
489
- | `setPassword(username, password): Promise<void>` | Update password. |
490
- | `isSudoer(username): boolean` | Returns `true` if user has sudo. |
491
- | `addSudoer(username): Promise<void>` | Grant sudo. |
492
- | `removeSudoer(username): Promise<void>` | Revoke sudo. Throws on `root`. |
493
- | `setQuotaBytes(username, maxBytes): Promise<void>` | Set per-user write quota. |
494
- | `clearQuota(username): Promise<void>` | Remove quota limit. |
495
- | `getQuotaBytes(username): number \| null` | Quota in bytes, or `null` if unlimited. |
496
- | `getUsageBytes(username): number` | Current usage under `/home/<user>`. |
497
- | `assertWriteWithinQuota(username, path, content)` | Throws if write would exceed quota. |
498
- | `listUsers(): string[]` | Sorted list of all usernames. |
499
- | `addAuthorizedKey(username, algo, data)` | Register SSH public key. |
500
- | `getAuthorizedKeys(username)` | List authorized keys. |
501
- | `removeAuthorizedKeys(username)` | Revoke all authorized keys. |
502
- | `registerSession(username, remoteAddress): VirtualActiveSession` | Allocate virtual TTY and register session. |
503
- | `unregisterSession(sessionId): void` | Remove session on disconnect. Safe with `null`. |
504
- | `updateSession(sessionId, username, remoteAddress): void` | Update after `su`/`sudo` identity change. |
505
- | `listActiveSessions(): VirtualActiveSession[]` | All active sessions sorted by start time. |
506
-
507
- **Events:** `initialized`, `user:add { username }`, `user:delete { username }`, `key:add { username, algo }`, `key:remove { username }`, `session:register { sessionId, username, remoteAddress }`, `session:unregister { sessionId, username }`
508
-
509
- ---
510
-
511
- ### `VirtualPackageManager`
512
-
513
- Simulates APT/dpkg backed by a 25-package registry. Accessed via `shell.packageManager`.
514
-
515
- **Methods**
516
-
517
- | Method | Description |
518
- |--------|-------------|
519
- | `install(names, opts?)` | Install packages (resolves deps, writes files to VFS). Returns `{ output, exitCode }` |
520
- | `remove(names, opts?)` | Remove. `opts.purge` also removes config files. |
521
- | `search(term)` | Search by name or description. |
522
- | `show(name)` | dpkg-style metadata block. |
523
- | `listInstalled()` | All installed packages as `InstalledPackage[]`. |
524
- | `listAvailable()` | All registry packages. |
525
- | `isInstalled(name)` | Returns `true` if installed. |
526
- | `installedCount()` | Count of installed packages. |
527
- | `findInRegistry(name)` | Look up `PackageDefinition` by name. |
528
-
529
- **Custom packages:**
530
-
531
- ```typescript
532
- const customPkg = {
533
- name: "myapp",
534
- version: "1.0.0",
535
- description: "My application",
536
- files: [
537
- { path: "/usr/bin/myapp", content: "#!/bin/sh\necho myapp v1.0.0\n", mode: 0o755 },
538
- { path: "/etc/myapp/config.json", content: JSON.stringify({ port: 3000 }) },
539
- ],
540
- onInstall: (vfs) => {
541
- vfs.mkdir("/var/lib/myapp", 0o755);
542
- vfs.mkdir("/var/log/myapp", 0o755);
543
- },
544
- };
545
- ```
546
-
547
- ---
548
-
549
- ### Snapshot Diff Tooling
550
-
551
- ```typescript
552
- import { diffSnapshots, formatDiff, assertDiff } from "typescript-virtual-container";
553
- ```
554
-
555
- **`diffSnapshots(before, after, options?): VfsDiff`**
556
-
557
- ```typescript
558
- const before = shell.vfs.toSnapshot();
559
- await client.exec("apt install vim && mkdir -p /app");
560
- const after = shell.vfs.toSnapshot();
561
-
562
- const diff = diffSnapshots(before, after, { ignore: ["/proc", "/var/log"] });
563
- diff.clean; // false
564
- diff.added; // [{ path: "/usr/bin/vim", type: "file" }, ...]
565
- diff.modified; // [{ path: "/var/lib/dpkg/status", before: "...", after: "..." }]
566
- ```
567
-
568
- **`formatDiff(diff, options?): string`** — human-readable output like `git diff --stat`. Options: `showContent: boolean`, `maxContentChars: number`.
569
-
570
- **`assertDiff(diff, expected): void`** — throws on mismatch, designed for test suites:
571
-
572
- ```typescript
573
- assertDiff(diff, { added: ["/app", "/usr/bin/vim"], modified: ["/var/lib/dpkg/status"] });
574
- ```
575
-
576
- ---
577
-
578
- ### `HoneyPot`
579
-
580
- Comprehensive security auditing. Attaches to all core components to log activity and detect anomalies.
581
-
582
- ```typescript
583
- new HoneyPot(maxLogSize?: number) // default: 10000
584
- ```
585
-
586
- **Methods**
587
-
588
- | Method | Description |
589
- |--------|-------------|
590
- | `attach(shell, vfs, users, ssh?, sftp?)` | Subscribe to all event sources. |
591
- | `getAuditLog(type?, source?): AuditLogEntry[]` | Full log, optionally filtered. |
592
- | `getStats(): Readonly<HoneyPotStats>` | Aggregated activity counters. |
593
- | `getRecent(limit?): AuditLogEntry[]` | Most recent entries, reverse-chronological. |
594
- | `detectAnomalies()` | Returns `{ type, severity, message }[]`. |
595
- | `reset()` | Clear log and reset counters. |
596
- | `exportJson(): string` | Serialise full log + stats to JSON. |
597
-
598
- `detectAnomalies` detects: high auth failure rates, excessive failures, unusual command volume, unusual file write volume.
599
-
600
- ```typescript
601
- const hp = new HoneyPot(50_000);
602
- hp.attach(shell, shell.vfs, shell.users, ssh);
603
-
604
- hp.getAuditLog("auth:failure").forEach(e =>
605
- console.log(e.details.username, e.details.remoteAddress)
606
- );
607
-
608
- hp.detectAnomalies().forEach(a =>
609
- console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
610
- );
611
-
612
- process.on("SIGINT", () => {
613
- require("fs").writeFileSync("audit.json", hp.exportJson());
614
- process.exit(0);
615
- });
616
- ```
617
-
618
- ---
619
-
620
- ### `SshClient` (Programmatic API)
621
-
622
- Execute shell commands against a `VirtualShell` without SSH overhead. Maintains working-directory state.
623
-
624
- ```typescript
625
- new SshClient(shell: VirtualShell, username: string)
626
- ```
627
-
628
- **Methods**
629
-
630
- | Method | Description |
631
- |--------|-------------|
632
- | `exec(command): Promise<CommandResult>` | Run raw command string (supports `&&`, `\|`, etc.). |
633
- | `ls(path?)` | List directory. |
634
- | `pwd()` | Print current working directory. |
635
- | `cd(path)` | Change directory. Updates internal cwd on success. |
636
- | `cat(path)` | Read file via `cat` command. |
637
- | `readFile(path)` | Read file directly from VFS. |
638
- | `writeFile(path, content)` | Write file directly to VFS. |
639
- | `mkdir(path, recursive?)` | Create directory. |
640
- | `touch(path)` | Create empty file. |
641
- | `rm(path, recursive?)` | Remove file or directory. |
642
- | `tree(path?)` | Render ASCII directory tree. |
643
- | `whoami()` / `hostname()` / `who()` | System info commands. |
644
- | `getCwd(): string` | Returns current cwd (no I/O). |
645
- | `getUsername(): string` | Returns authenticated username. |
646
-
647
- ---
648
-
649
- ### Key Types
650
-
651
- ```typescript
652
- interface CommandResult {
653
- stdout?: string;
654
- stderr?: string;
655
- exitCode?: number;
656
- nextCwd?: string;
657
- clearScreen?: boolean;
658
- closeSession?: boolean;
659
- switchUser?: string;
660
- openEditor?: NanoEditorSession;
661
- openHtop?: boolean;
662
- sudoChallenge?: SudoChallenge;
663
- }
664
-
665
- interface ShellEnv {
666
- vars: Record<string, string>; // $VAR accessible in expansions
667
- lastExitCode: number; // $?
668
- }
669
-
670
- interface ShellModule {
671
- name: string;
672
- params: string[];
673
- aliases?: string[];
674
- description?: string;
675
- category?: string; // navigation|files|text|archive|system|package|network|shell|users|misc
676
- run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
677
- }
678
-
679
- interface CommandContext {
680
- authUser: string;
681
- hostname: string;
682
- activeSessions: VirtualActiveSession[];
683
- rawInput: string;
684
- mode: "shell" | "exec";
685
- args: string[];
686
- stdin?: string;
687
- cwd: string;
688
- shell: VirtualShell;
689
- env: ShellEnv;
690
- }
691
-
692
- interface VirtualActiveSession {
693
- id: string;
694
- username: string;
695
- tty: string;
696
- remoteAddress: string;
697
- startedAt: string; // ISO-8601
698
- }
699
-
700
- /** Returned by adduser, passwd, deluser — triggers interactive password prompt in the terminal. */
701
- interface PasswordChallenge {
702
- preamble?: string; // Lines printed before the first prompt
703
- prompt: string; // e.g. "New password: "
704
- confirmPrompt?: string; // Second prompt for confirmation
705
- confirmText?: string; // Destructive confirmation prompt (y/N)
706
- action: "adduser" | "passwd" | "deluser" | "su";
707
- targetUsername: string;
708
- newUsername?: string; // adduser only
709
- }
710
- ```
711
-
712
- ---
713
-
714
- ### Command Helpers
715
-
716
- ```typescript
717
- import { ifFlag, getFlag, getArg, parseArgs } from "typescript-virtual-container";
718
-
719
- // ifFlag — true if any given flag appears in args
720
- ifFlag(args, ["-r", "--recursive"]) // boolean
721
-
722
- // getFlag — value, true if valueless, undefined if absent
723
- getFlag(args, ["-o", "--output"])
724
- // ["--output", "file.txt"] → "file.txt"
725
- // ["--output=file.txt"] → "file.txt"
726
- // ["--verbose"] → true
727
- // [] → undefined
728
-
729
- // getArg — positional at index N, skipping known flags
730
- // args = ["-r", "src", "dest"]
731
- getArg(args, 0, { flags: ["-r"] }) // "src"
732
- getArg(args, 1, { flags: ["-r"] }) // "dest"
733
-
734
- // parseArgs — structured parse
735
- const { flags, flagsWithValues, positionals } = parseArgs(args, {
736
- flags: ["-r", "--recursive"],
737
- flagsWithValue: ["-o", "--output"],
738
- });
739
- ```
740
-
741
233
  </details>
742
234
 
743
235
  ---
@@ -983,9 +475,9 @@ echo "Welcome back, root!"
983
475
  ---
984
476
 
985
477
  <details>
986
- <summary><strong>Built-in Commands (106)</strong></summary>
478
+ <summary><strong>Built-in Commands (118)</strong></summary>
987
479
 
988
- Type `help` in the shell for a grouped, colorized listing. Type `help <command>` for detailed usage. Type `man <command>` for full manual pages — all 106 commands are documented.
480
+ Type `help` in the shell for a grouped, colorized listing. Type `help <command>` for detailed usage. Type `man <command>` for full manual pages — all 118 commands are documented.
989
481
 
990
482
  ### Navigation
991
483
 
@@ -1003,7 +495,7 @@ Type `help` in the shell for a grouped, colorized listing. Type `help <command>`
1003
495
  | `cat <path...>` | `-n` `-b` | Concatenate and print; `-n` numbers lines, `-b` numbers non-blank |
1004
496
  | `chmod <mode> <file>` | | Octal (`755`) or symbolic (`+x`, `u+x`, `go-w`, `a=rx`) |
1005
497
  | `cp <src> <dest>` | `-r` | Copy file or directory |
1006
- | `find [path]` | `-name` `-type` | Search for files |
498
+ | `find [path]` | `-name` `-iname` `-type` `-maxdepth` `-mindepth` `-exec` `-not` `-o` `-a` `-empty` `-size` | Search for files |
1007
499
  | `ln <target> <link>` | `-s` | Hard or symbolic link |
1008
500
  | `readlink <path>` | `-f` | Print resolved path of symbolic link |
1009
501
  | `mkdir <path>` | `-p` | Create directory |
@@ -1017,13 +509,13 @@ Type `help` in the shell for a grouped, colorized listing. Type `help <command>`
1017
509
 
1018
510
  | Command | Flags | Description |
1019
511
  |---------|-------|-------------|
1020
- | `awk [-F <sep>] '<prog>'` | | Pattern scanning |
512
+ | `awk [-F <sep>] '<prog>'` | `-v var=val` | Pattern scanning — NR/NF, `BEGIN`/`END`, field assign, `gsub`/`sub`/`substr`/`split`/`length`, `printf` |
1021
513
  | `base64` | `-d` | Encode/decode base64 |
1022
514
  | `cut` | `-d` `-f` | Remove sections from lines |
1023
515
  | `diff <f1> <f2>` | | Compare files line by line |
1024
516
  | `grep <pattern> [files]` | `-i` `-v` `-n` `-r` | Search file content |
1025
517
  | `head [files]` | `-n <N>` | First N lines |
1026
- | `sed -e 's/pat/rep/[g]'` | `-i` | Stream editor |
518
+ | `sed -e 's/pat/rep/[g]'` | `-n` `-i` `-e` | Stream editor — `s///[gI]`, `d`, `p`, `=`, `q`, line/regex/range addresses |
1027
519
  | `sort [files]` | `-r` `-n` `-u` | Sort lines |
1028
520
  | `tail [files]` | `-n <N>` | Last N lines |
1029
521
  | `tee [files]` | `-a` | Read stdin, write to stdout and files |
@@ -1039,8 +531,10 @@ Type `help` in the shell for a grouped, colorized listing. Type `help <command>`
1039
531
 
1040
532
  | Command | Flags | Description |
1041
533
  |---------|-------|-------------|
1042
- | `gzip <file>` / `gunzip <file>` | | Compress / decompress |
1043
- | `tar <archive> [files]` | `-czf` `-xzf` `-tf` | Archive utility |
534
+ | `gzip <file>` / `gunzip <file>` | `-k` `-d` | Compress / decompress (real gzip, browser-native) |
535
+ | `bzip2 <file>` / `bunzip2 <file>` | `-k` `-d` | Compress / decompress bzip2 (VFS round-trip) |
536
+ | `zip [-r] <archive> <files>` / `unzip <archive>` | `-l` `-d <dir>` | Real PKZIP + DEFLATE (interoperable) |
537
+ | `tar <archive> [files]` | `-czf` `-xzf` `-tf` `-v` | Archive utility — real POSIX ustar binary format (interoperable) |
1044
538
 
1045
539
  ### System
1046
540
 
@@ -1068,6 +562,9 @@ Type `help` in the shell for a grouped, colorized listing. Type `help <command>`
1068
562
  | `python3` | `--version` `-c` `-V` | Virtual Python 3 interpreter; alias `python`; **requires `apt install python3`** |
1069
563
  | `sleep <seconds>` | | Delay execution |
1070
564
  | `uname` | `-a` `-r` `-m` | System information |
565
+ | `bc` | | Arithmetic calculator (integer; `+` `-` `*` `/` `%` `**` `()`) |
566
+ | `lsof` | `-i` | List open files (simulated) |
567
+ | `strace <cmd>` | `-e` `-o` | Trace system calls (stub with realistic output) |
1071
568
  | `uptime` | `-p` `-s` | Running time |
1072
569
  | `w` | | Who is logged on and what they are doing |
1073
570
  | `who` | | Active sessions |
@@ -1094,12 +591,16 @@ Type `help` in the shell for a grouped, colorized listing. Type `help <command>`
1094
591
  | `false` | | Return exit code 1 |
1095
592
  | `help [command]` | | List commands or show command usage |
1096
593
  | `history [n]` | | Command history |
594
+ | `jobs` | | List active jobs |
595
+ | `bg [%n]` | | Resume job in background |
596
+ | `fg [%n]` | | Resume job in foreground |
1097
597
  | `man <command>` | | Command reference manual |
1098
598
  | `printf <fmt> [args...]` | | Format and print (`%s` `%d` `%f` `%x` `\n` `\t`) |
1099
599
  | `read [-r] <var...>` | `-r` `-p` | Read stdin into variable(s) |
1100
600
  | `return [n]` | | Return from shell function |
1101
- | `set [VAR=val]` | | Display or set shell variables |
1102
- | `sh` | `-c <script>` `[file]` | Execute shell script — `if`/`for`/`while`/`case`/functions, `$((expr))`, single-quote-safe |
601
+ | `set [VAR=val]` | `-e` `-x` `+e` `+x` | Display or set shell variables; `-e` exit on error, `-x` trace execution |
602
+ | `sh` | `-c <script>` `[file]` | Execute shell script — `if`/`for`/`while`/`until`/`case`/functions, arrays `arr=(a b c)`, `$((expr))`, single-quote-safe |
603
+ | `perl` | `-e` `-p` `-n` | One-liner interpreter (`print`/`say`, `s///`, `-p`/`-n` loop) |
1103
604
  | `shift [n]` | | Shift positional parameters |
1104
605
  | `source <file>` | | Execute file in current env; alias `.` |
1105
606
  | `test <expr>` / `[ <expr> ]` | | POSIX conditional: `-f` `-d` `-e` `-z` `-n` `-x` `-s` `=` `!=` `-eq` `-lt` `-gt` `-le` `-ge` `!` `-a` `-o` |
@@ -1143,7 +644,7 @@ Type `help` in the shell for a grouped, colorized listing. Type `help <command>`
1143
644
  | `su [user]` | | Switch user |
1144
645
  | `sudo <cmd>` | `-i` | Run as root |
1145
646
 
1146
- **ℹ️ All 106 built-in commands include complete JSDoc documentation** with `@category` and `@params` tags. See [src/commands/](https://github.com/itsrealfortune/typescript-virtual-container/tree/main/src/commands) for source code and inline documentation.
647
+ **ℹ️ All 118 built-in commands include complete JSDoc documentation** with `@category` and `@params` tags. See [src/commands/](https://github.com/itsrealfortune/typescript-virtual-container/tree/main/src/commands) for source code and inline documentation.
1147
648
 
1148
649
  Custom commands: `shell.addCommand(name, params, callback)`.
1149
650
 
@@ -1178,8 +679,22 @@ echo "${UNSET:-default}" # default
1178
679
  echo "${NAME:+alternate}" # alternate (only if NAME is set)
1179
680
  echo "${UNSET:=assigned}" # assigns and returns "assigned"
1180
681
  echo "${#NAME}" # 5 (string length)
682
+ echo "${NAME:2}" # rld (substring from offset 2)
683
+ echo "${NAME:1:3}" # orl (substring offset 1, length 3)
684
+ echo "${NAME/o/0}" # w0rld (replace first)
685
+ echo "${NAME//l/L}" # worLd (replace all)
686
+ echo "${PATH##*/}" # strip longest prefix match
687
+ echo "${FILE%.txt}" # strip shortest suffix match
1181
688
  echo "$?" # last exit code
689
+ echo "$RANDOM" # random integer 0–32767
690
+ echo "$LINENO" # current line number
1182
691
  echo ~ # /home/<user> (tilde expansion)
692
+
693
+ # Arrays
694
+ arr=(alpha beta gamma)
695
+ echo "${arr[0]}" # alpha
696
+ echo "${arr[@]}" # alpha beta gamma
697
+ echo "${#arr[@]}" # 3
1183
698
  ```
1184
699
 
1185
700
  > **Single-quote isolation** — `$VAR` and `$((...))` are never expanded inside `'...'`.
@@ -1219,6 +734,12 @@ while [ $COUNT -lt 3 ]; do
1219
734
  echo "Count: $COUNT"
1220
735
  COUNT=$((COUNT + 1))
1221
736
  done
737
+
738
+ COUNT=5
739
+ until [ $COUNT -eq 0 ]; do
740
+ echo "Countdown: $COUNT"
741
+ COUNT=$((COUNT - 1))
742
+ done
1222
743
  ```
1223
744
 
1224
745
  ### Functions and case
@@ -1236,6 +757,30 @@ case "$1" in
1236
757
  esac
1237
758
  ```
1238
759
 
760
+ ### Heredoc
761
+
762
+ ```bash
763
+ cat << EOF
764
+ line one
765
+ line two
766
+ EOF
767
+
768
+ # write to file
769
+ cat > /tmp/config << EOF
770
+ HOST=localhost
771
+ PORT=3000
772
+ EOF
773
+ ```
774
+
775
+ ### set -e / set -x
776
+
777
+ ```bash
778
+ set -e # exit immediately on any non-zero exit code
779
+ set -x # print each command before executing (trace)
780
+ set +e # disable errexit
781
+ set +x # disable xtrace
782
+ ```
783
+
1239
784
  ### Script Files
1240
785
 
1241
786
  ```typescript
@@ -1248,9 +793,13 @@ done
1248
793
  await client.exec("sh /usr/local/bin/setup.sh");
1249
794
  ```
1250
795
 
1251
- ### .bashrc
796
+ ### Login Files
797
+
798
+ Sourced automatically at session start, in order:
1252
799
 
1253
- Sourced automatically on every interactive session from `/home/<user>/.bashrc`:
800
+ 1. `/etc/environment` `KEY=VALUE` pairs only, no shell syntax
801
+ 2. `~/.profile` — user login script
802
+ 3. `~/.bashrc` — interactive shell config
1254
803
 
1255
804
  ```bash
1256
805
  export EDITOR=nano
@@ -1258,6 +807,20 @@ alias ll="ls -l"
1258
807
  echo "Welcome, $USER!"
1259
808
  ```
1260
809
 
810
+ ### Line Editing
811
+
812
+ Interactive shell supports full readline-style key bindings:
813
+
814
+ | Key | Action |
815
+ |-----|--------|
816
+ | `←` / `→` | Move cursor left / right |
817
+ | `Home` / `Ctrl+A` | Jump to start of line |
818
+ | `End` / `Ctrl+E` | Jump to end of line |
819
+ | `Ctrl+K` | Kill to end of line |
820
+ | `Ctrl+U` | Kill to start of line |
821
+ | `Ctrl+W` | Kill word backward |
822
+ | `!!` | Expand to last command |
823
+
1261
824
  </details>
1262
825
 
1263
826
  ---
@@ -1666,18 +1229,21 @@ Open:
1666
1229
  - [x] Pure in-memory VFS · symlinks · binary snapshot format (VFSB, ~27% smaller than JSON+base64)
1667
1230
  - [x] Linux rootfs on boot — `/etc`, `/proc`, `/sys`, `/dev`, `/usr`, `/var`
1668
1231
  - [x] Virtual package manager — `apt`/`dpkg`, 25 packages, VFS file writes
1669
- - [x] 106 built-in commands across 10 categories (added `w`, `ip`, `dmesg`, `last`, `basename`, `dirname`, `file`, `tput`, `stty`, `yes`, `fortune`, `cowsay`, `cowthink`, `cmatrix`, `sl`)
1670
- - [x] Real shell interpreter — `if`/`for`/`while`/`case`/functions, `$(cmd)`, `$((expr))`, `${#VAR}`, `{a,b,c}` brace expansion, `{1..N}` ranges, `*.glob` expansion, `!!`/`!n` history expansion, `\` line continuation, `2>/dev/null` stderr redirect, `2>&1`, `(( x++ ))`
1232
+ - [x] 118 built-in commands across 11 categories (added `zip`, `unzip`, `bzip2`, `bunzip2`, `lsof`, `strace`, `perl`, `w`, `ip`, `dmesg`, `last`, `basename`, `dirname`, `file`, `tput`, `stty`, `yes`, `fortune`, `cowsay`, `cowthink`, `cmatrix`, `sl`, `bc`, `jobs`, `bg`, `fg`)
1233
+ - [x] Real shell interpreter — `if`/`for`/`while`/`until`/`case`/functions, arrays `arr=(...)`, `$(cmd)`, `$((expr))`, `${#VAR}`, `${var#pfx}` `${var##pfx}` `${var%sfx}` `${var%%sfx}` `${var/p/r}` `${var//p/r}` `${var:off:len}` `${arr[@]}`, `{a,b,c}` brace expansion, `{1..N}` ranges, `*.glob` expansion, `!!` history expansion, `\` line continuation, `2>/dev/null` stderr redirect, `2>&1`, `(( x++ ))`, heredoc `<< EOF`, `set -e`/`set -x`, `$RANDOM`/`$LINENO`
1671
1234
  - [x] `curl`/`wget` as pure `fetch()` · VFS PATH resolution · `/sbin` root-only
1672
1235
  - [x] `/proc/self` and `/proc/<pid>` per-session entries
1673
1236
  - [x] Snapshot diff tooling — `diffSnapshots`, `formatDiff`, `assertDiff`
1674
1237
  - [x] `node`/`python3`/`npm`/`npx` — package-gated virtual REPL stubs
1675
1238
  <!-- BUILD:changelog -->
1676
- - [x] Web shell bundles (`fortune-nyx-v1.5.4-web.min.js`) — fully browser-native with IndexedDB VFS
1677
- - [x] Self-standalone CLI (`fortune-nyx-v1.5.4-directbash-k6.1.0.mjs`) — single-file interactive shell, per-user history, tab completion
1239
+ - [x] Web shell bundles (`fortune-nyx-v1.5.6-web.min.js`) — fully browser-native with IndexedDB VFS
1240
+ - [x] Self-standalone CLI (`fortune-nyx-v1.5.6-directbash-k6.1.0.mjs`) — single-file interactive shell, per-user history, tab completion
1678
1241
  <!-- /BUILD:changelog -->
1679
1242
  - [x] 120+ `man` pages — all built-in commands documented via `man <cmd>`
1680
1243
  - [x] Shared `tokenize.ts` — unified tokenizer for shell parser and runtime (eliminates duplication)
1244
+ - [x] Full readline line editing — `Ctrl+A/E/K/U/W`, `Home`/`End`, `!!` history expansion, `/etc/environment` + `~/.profile` login sourcing
1245
+ - [x] Interoperable archive formats — `tar` writes real POSIX ustar binary; `zip`/`unzip` use PKZIP+DEFLATE (fflate); files extracted by real system tools via SFTP
1246
+ - [x] Overhauled `sed` — `d`/`p`/`=`/`q`, `-n` suppress, line/regex/range/`$` addresses; overhauled `awk` — `-v`, field assignment, `gsub`/`sub`/`substr`/`split`/`length`/`printf`/`next`; overhauled `find` — `-exec`, `-maxdepth`, `-iname`, `-not`/`!`, `-o`/`-a`, `-empty`, `-size`
1681
1247
  - [x] `PasswordChallenge` type — generic interactive password flow for `adduser`, `passwd`, `deluser`
1682
1248
  - [x] `VirtualFileSystem.mount(vPath, hostPath, { readOnly })` — bind-mount host directories into the VM; read-only by default; browser-safe (silent no-op)
1683
1249