typescript-virtual-container 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
  3. package/.github/dependabot.yml +27 -0
  4. package/.github/pull_request_template.md +21 -0
  5. package/.github/workflows/create-pull-request.yml +83 -0
  6. package/.github/workflows/test-battery.yml +57 -0
  7. package/CHANGELOG.md +27 -0
  8. package/CODE_OF_CONDUCT.md +39 -0
  9. package/CONTRIBUTING.md +59 -0
  10. package/LICENSE +21 -0
  11. package/README.md +1283 -0
  12. package/SECURITY.md +33 -0
  13. package/biome.json +20 -0
  14. package/bun.lock +99 -0
  15. package/package.json +38 -0
  16. package/src/SSHMimic/client.ts +248 -0
  17. package/src/SSHMimic/commands/adduser.ts +22 -0
  18. package/src/SSHMimic/commands/cat.ts +16 -0
  19. package/src/SSHMimic/commands/cd.ts +20 -0
  20. package/src/SSHMimic/commands/clear.ts +7 -0
  21. package/src/SSHMimic/commands/curl.ts +27 -0
  22. package/src/SSHMimic/commands/deluser.ts +19 -0
  23. package/src/SSHMimic/commands/exit.ts +7 -0
  24. package/src/SSHMimic/commands/help.ts +9 -0
  25. package/src/SSHMimic/commands/helpers.ts +137 -0
  26. package/src/SSHMimic/commands/hostname.ts +7 -0
  27. package/src/SSHMimic/commands/htop.ts +13 -0
  28. package/src/SSHMimic/commands/index.ts +120 -0
  29. package/src/SSHMimic/commands/ls.ts +14 -0
  30. package/src/SSHMimic/commands/mkdir.ts +17 -0
  31. package/src/SSHMimic/commands/nano.ts +30 -0
  32. package/src/SSHMimic/commands/pwd.ts +7 -0
  33. package/src/SSHMimic/commands/rm.ts +26 -0
  34. package/src/SSHMimic/commands/su.ts +31 -0
  35. package/src/SSHMimic/commands/sudo.ts +90 -0
  36. package/src/SSHMimic/commands/touch.ts +20 -0
  37. package/src/SSHMimic/commands/tree.ts +11 -0
  38. package/src/SSHMimic/commands/wget.ts +33 -0
  39. package/src/SSHMimic/commands/who.ts +18 -0
  40. package/src/SSHMimic/commands/whoami.ts +7 -0
  41. package/src/SSHMimic/exec.ts +37 -0
  42. package/src/SSHMimic/hostKey.ts +21 -0
  43. package/src/SSHMimic/index.ts +203 -0
  44. package/src/SSHMimic/loginFormat.ts +10 -0
  45. package/src/SSHMimic/prompt.ts +14 -0
  46. package/src/SSHMimic/shell.ts +740 -0
  47. package/src/SSHMimic/users.ts +336 -0
  48. package/src/VirtualFileSystem.ts +420 -0
  49. package/src/index.ts +34 -0
  50. package/src/standalone.ts +14 -0
  51. package/src/types/commands.ts +98 -0
  52. package/src/types/streams.ts +32 -0
  53. package/src/types/tar-stream.d.ts +38 -0
  54. package/src/types/vfs.ts +81 -0
  55. package/src/vfs/archive.ts +74 -0
  56. package/src/vfs/internalTypes.ts +19 -0
  57. package/src/vfs/path.ts +74 -0
  58. package/src/vfs/snapshot.ts +84 -0
  59. package/src/vfs/tree.ts +34 -0
  60. package/tsconfig.json +31 -0
package/README.md ADDED
@@ -0,0 +1,1283 @@
1
+ # `typescript-virtual-container`
2
+
3
+ > In-memory SSH server with a virtual filesystem and typed programmatic API for testing, automation, and interactive shell scripting in TypeScript/JavaScript.
4
+
5
+ [![npm version](https://badge.fury.io/js/typescript-virtual-container.svg)](https://www.npmjs.com/package/typescript-virtual-container)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
+ [![Runtime](https://img.shields.io/badge/runtime-Node.js%20%7C%20Bun-43853D.svg)](https://nodejs.org/)
9
+
10
+ ## Table of Contents
11
+
12
+ - [Overview](#overview)
13
+ - [Why This Package](#why-this-package)
14
+ - [Installation](#installation)
15
+ - [Compatibility](#compatibility)
16
+ - [Quick Start](#quick-start)
17
+ - [Architecture Overview](#architecture-overview)
18
+ - [API Reference](#api-reference)
19
+ - [Usage Examples](#usage-examples)
20
+ - [Built-in Commands](#built-in-commands)
21
+ - [Configuration](#configuration)
22
+ - [Performance & Scalability](#performance--scalability)
23
+ - [Types & TypeScript](#types--typescript)
24
+ - [FAQ](#faq)
25
+ - [Troubleshooting](#troubleshooting)
26
+ - [Migration Guide](#migration-guide)
27
+ - [Contributing](#contributing)
28
+ - [Security](#security)
29
+ - [Support](#support)
30
+ - [License](#license)
31
+ - [Roadmap](#roadmap)
32
+ - [Changelog](#changelog)
33
+
34
+ ## Overview
35
+
36
+ `typescript-virtual-container` is a lightweight, fully-typed SSH server written in TypeScript that provides:
37
+
38
+ - **SSH Protocol Support**: Serve SSH connections on any port with password authentication and support for both shell and exec modes.
39
+ - **Virtual Filesystem**: In-memory filesystem with optional compression, persistence to disk via tar.gz snapshots, and programmatic access.
40
+ - **User Management**: Create, authenticate, and manage virtual users with strict password hashing (scrypt) and sudo-like privilege elevation.
41
+ - **Programmatic Shell API**: Execute shell commands and query filesystem state directly from TypeScript without SSH overhead.
42
+ - **Built-in Commands**: `ls`, `cd`, `pwd`, `cat`, `mkdir`, `touch`, `rm`, `tree`, `whoami`, `hostname`, `who`, `sudo`, `su`, `adduser`, `deluser`, `nano` (text editor), `curl`, `wget`, and more.
43
+ - **Full TypeScript Support**: Complete JSDoc coverage, exported types, and first-class async/await for all operations.
44
+
45
+ ## Why This Package
46
+
47
+ This package is designed for teams that need a realistic SSH-like runtime without spinning up real containers or VMs.
48
+
49
+ - **Deterministic test environments**: Repeatable state for CI pipelines and integration tests.
50
+ - **Low operational overhead**: No Docker daemon, no kernel namespaces, no privileged setup.
51
+ - **Fast feedback loops**: Programmatic API for command execution and filesystem assertions.
52
+ - **Developer-friendly internals**: Typed APIs, clear boundaries, and composable building blocks.
53
+
54
+ ## Installation
55
+
56
+ ### From npm
57
+
58
+ ```bash
59
+ npm install typescript-virtual-container
60
+ # or
61
+ yarn add typescript-virtual-container
62
+ # or
63
+ bun add typescript-virtual-container
64
+ ```
65
+
66
+ ### From source (development)
67
+
68
+ ```bash
69
+ git clone <repo-url>
70
+ cd virtual-env-js
71
+ bun install
72
+ bun format # Format code per Biome
73
+ bun check # Lint and typecheck
74
+ ```
75
+
76
+ ## Compatibility
77
+
78
+ - **Node.js**: Recommended `>=18`
79
+ - **Bun**: Supported for development and runtime
80
+ - **TypeScript**: Recommended `>=5.0`
81
+ - **OS**: Linux, macOS, and Windows (via Node/Bun runtime)
82
+
83
+ The virtual filesystem and shell behavior are intentionally portable and do not depend on host-specific POSIX syscalls.
84
+
85
+ ## Quick Start
86
+
87
+ ### Running an SSH Server
88
+
89
+ ```typescript
90
+ import { VirtualMachine } from "typescript-virtual-container";
91
+
92
+ // Create server on port 2222
93
+ const ssh = new VirtualMachine({
94
+ port: 2222,
95
+ hostname: "my-container"
96
+ });
97
+
98
+ // Start server
99
+ await ssh.start();
100
+ console.log("SSH server listening on :2222");
101
+
102
+ // Connect externally via SSH
103
+ // ssh root@localhost -p 2222 (password: "root")
104
+
105
+ // Graceful shutdown
106
+ process.on("SIGTERM", () => {
107
+ ssh.stop();
108
+ process.exit(0);
109
+ });
110
+ ```
111
+
112
+ ### Using the Programmatic Client API
113
+
114
+ ```typescript
115
+ import { VirtualMachine, SshClient } from "typescript-virtual-container";
116
+
117
+ const ssh = new VirtualMachine({ port: 2222 });
118
+ await ssh.start();
119
+
120
+ // Create authenticated client for specific user
121
+ const client = new SshClient(ssh, "root");
122
+
123
+ // Execute commands programmatically
124
+ const list = await client.ls("/home");
125
+ console.log("stdout:", list.stdout); // Directory listing
126
+
127
+ const result = await client.pwd();
128
+ console.log("Current dir:", result.stdout);
129
+
130
+ await client.mkdir("/tmp/work", true);
131
+ await client.cd("/tmp/work");
132
+
133
+ const content = await client.readFile("/etc/hostname");
134
+ console.log("Hostname file:", content.stdout);
135
+
136
+ await client.writeFile("output.txt", "Hello, World!");
137
+
138
+ ssh.stop();
139
+ ```
140
+
141
+ ## Architecture Overview
142
+
143
+ ### Core Components
144
+
145
+ ```
146
+ ┌─────────────────────────────────────────────┐
147
+ │ SSH Server (SshMimic) │
148
+ │ Listens on :port, handles auth & sessions │
149
+ └──────────────┬──────────────────────────────┘
150
+
151
+ ┌───────┴────────┬──────────────┐
152
+ │ │ │
153
+ ┌──────┴──────┐ ┌─────┴─────┐ ┌────┴─────┐
154
+ │ VirtualFileSystem │ VirtualUserManager │ Command Runtime
155
+ │ In-mem FS w/ persist │ Auth & Sudoers │ Shell/Exec Mode
156
+ └──────────────┘ └──────────┘ └──────────┘
157
+
158
+ │ Backed by disk
159
+ │ .vfs/mirror.tar.gz
160
+ └──────────────────────────────────┘
161
+ ```
162
+
163
+ ### Execution Modes
164
+
165
+ 1. **SSH Shell Mode**: Interactive terminal session over SSH with readline, prompt, TTY resizing.
166
+ 2. **SSH Exec Mode**: Non-interactive command execution (e.g., `ssh user@host "ls -la"`).
167
+ 3. **Programmatic Mode** (new): Direct TypeScript API via `SshClient`, no SSH protocol overhead.
168
+
169
+ ### Persistence
170
+
171
+ - Filesystem state saved as gzip-compressed tar archive at `.vfs/mirror.tar.gz`
172
+ - Users/passwords stored in virtual paths `/virtual-env-js/.auth/htpasswd` and `/virtual-env-js/.auth/sudoers`
173
+ - Manual flush via `VirtualFileSystem.flushMirror()` or automatic on command completion
174
+
175
+ ---
176
+
177
+ ## API Reference
178
+
179
+ ### SshMimic (SSH Server)
180
+
181
+ Main SSH server class. Manages virtual filesystem, user authentication, and session handlers.
182
+
183
+ #### Constructor
184
+
185
+ ```typescript
186
+ new SshMimic(options: {
187
+ port: number; // TCP port to bind on localhost
188
+ hostname?: string; // Virtual hostname (default: "typescript-vm")
189
+ basePath?: string; // Base directory for VFS snapshot storage (default: ".")
190
+ })
191
+ ```
192
+
193
+ **Example:**
194
+
195
+ ```typescript
196
+ const ssh = new SshMimic({
197
+ port: 2222,
198
+ hostname: "my-lab",
199
+ basePath: "./data" // Snapshots stored in ./data/.vfs/mirror.tar.gz
200
+ });
201
+ ```
202
+
203
+ #### Methods
204
+
205
+ ##### `async start(): Promise<number>`
206
+
207
+ Initializes virtual filesystem, user manager, and starts listening for SSH connections.
208
+
209
+ - **Returns**: Bound port number
210
+ - **Throws**: Error if port not available or initialization fails
211
+
212
+ ```typescript
213
+ const port = await ssh.start();
214
+ console.log(`Listening on ${port}`);
215
+ ```
216
+
217
+ ##### `stop(): void`
218
+
219
+ Cleanly closes server and all active connections.
220
+
221
+ ```typescript
222
+ ssh.stop();
223
+ ```
224
+
225
+ ##### `getVfs(): VirtualFileSystem | null`
226
+
227
+ Returns the virtual filesystem instance. Null if server not started.
228
+
229
+ ```typescript
230
+ const vfs = ssh.getVfs();
231
+ if (vfs) {
232
+ const content = vfs.readFile("/etc/hosts");
233
+ }
234
+ ```
235
+
236
+ ##### `getUsers(): VirtualUserManager | null`
237
+
238
+ Returns the user manager instance. Null if server not started.
239
+
240
+ ```typescript
241
+ const users = ssh.getUsers();
242
+ const sessions = users.listActiveSessions();
243
+ ```
244
+
245
+ ##### `getHostname(): string`
246
+
247
+ Returns configured server hostname.
248
+
249
+ ```typescript
250
+ console.log(`Server name: ${ssh.getHostname()}`);
251
+ ```
252
+
253
+ ---
254
+
255
+ ### SshClient (Programmatic Shell API)
256
+
257
+ Execute shell commands as a specific user without SSH overhead. Maintains connection state (current working directory) across calls.
258
+
259
+ #### Constructor
260
+
261
+ ```typescript
262
+ new SshClient(ssh: SshMimic, username: string)
263
+ ```
264
+
265
+ - **ssh**: Parent SSH server instance (must be started)
266
+ - **username**: User to authenticate as (no password required)
267
+
268
+ **Example:**
269
+
270
+ ```typescript
271
+ const client = new SshClient(ssh, "alice");
272
+ ```
273
+
274
+ #### Methods
275
+
276
+ ##### `async exec(command: string): Promise<CommandResult>`
277
+
278
+ Raw command execution. Returns structured output.
279
+
280
+ ```typescript
281
+ const result = await client.exec("echo hello && exit 42");
282
+ console.log(result.stdout); // "hello"
283
+ console.log(result.exitCode); // 42
284
+ ```
285
+
286
+ ##### `async ls(path?: string): Promise<CommandResult>`
287
+
288
+ Lists directory contents. Defaults to current directory.
289
+
290
+ ```typescript
291
+ const result = await client.ls("/tmp");
292
+ // result.stdout contains formatted listing
293
+ ```
294
+
295
+ ##### `async pwd(): Promise<CommandResult>`
296
+
297
+ Prints current working directory.
298
+
299
+ ```typescript
300
+ const result = await client.pwd();
301
+ console.log("cwd:", result.stdout); // "/home/alice"
302
+ ```
303
+
304
+ ##### `async cd(path: string): Promise<CommandResult>`
305
+
306
+ Changes working directory. Updates internal state on success.
307
+
308
+ ```typescript
309
+ const result = await client.cd("/var/log");
310
+ // Internal cwd now "/var/log"
311
+
312
+ const result2 = await client.ls(); // Listed from /var/log
313
+ ```
314
+
315
+ ##### `async cat(path: string): Promise<CommandResult>`
316
+
317
+ Reads file content via command.
318
+
319
+ ```typescript
320
+ const result = await client.cat("/etc/hostname");
321
+ console.log(result.stdout);
322
+ ```
323
+
324
+ ##### `async mkdir(path: string, recursive?: boolean): Promise<CommandResult>`
325
+
326
+ Creates directory. Set `recursive=true` for `-p` flag.
327
+
328
+ ```typescript
329
+ await client.mkdir("/tmp/nested/dirs", true);
330
+ ```
331
+
332
+ ##### `async touch(path: string): Promise<CommandResult>`
333
+
334
+ Creates empty file.
335
+
336
+ ```typescript
337
+ await client.touch("/tmp/marker.txt");
338
+ ```
339
+
340
+ ##### `async rm(path: string, recursive?: boolean): Promise<CommandResult>`
341
+
342
+ Removes file or directory. Set `recursive=true` for `-r` flag.
343
+
344
+ ```typescript
345
+ await client.rm("/tmp/old", true); // rm -r /tmp/old
346
+ ```
347
+
348
+ ##### `async readFile(path: string): Promise<CommandResult>`
349
+
350
+ Reads file content directly from VFS (programmatic, no shell).
351
+
352
+ ```typescript
353
+ const result = await client.readFile("/etc/hostname");
354
+ console.log(result.stdout); // File content
355
+ if (result.exitCode !== 0) console.error(result.stderr);
356
+ ```
357
+
358
+ ##### `async writeFile(path: string, content: string): Promise<CommandResult>`
359
+
360
+ Writes file content directly to VFS (programmatic, no shell).
361
+
362
+ ```typescript
363
+ await client.writeFile("/tmp/config.txt", "port=8080\nhost=localhost");
364
+ ```
365
+
366
+ ##### `async tree(path?: string): Promise<CommandResult>`
367
+
368
+ Renders ASCII directory tree.
369
+
370
+ ```typescript
371
+ const result = await client.tree("/home");
372
+ console.log(result.stdout);
373
+ ```
374
+
375
+ ##### `async whoami(): Promise<CommandResult>`
376
+
377
+ Shows authenticated user.
378
+
379
+ ```typescript
380
+ const result = await client.whoami();
381
+ console.log(result.stdout); // "alice" (or user passed to constructor)
382
+ ```
383
+
384
+ ##### `async hostname(): Promise<CommandResult>`
385
+
386
+ Shows server hostname.
387
+
388
+ ```typescript
389
+ const result = await client.hostname();
390
+ ```
391
+
392
+ ##### `async who(): Promise<CommandResult>`
393
+
394
+ Lists active user sessions.
395
+
396
+ ```typescript
397
+ const result = await client.who();
398
+ console.log(result.stdout); // Active sessions
399
+ ```
400
+
401
+ ##### `getCwd(): string`
402
+
403
+ Returns current working directory (local state, no I/O).
404
+
405
+ ```typescript
406
+ await client.cd("/tmp");
407
+ console.log(client.getCwd()); // "/tmp"
408
+ ```
409
+
410
+ ##### `getUsername(): string`
411
+
412
+ Returns authenticated username (local state, no I/O).
413
+
414
+ ```typescript
415
+ console.log(client.getUsername()); // Username from constructor
416
+ ```
417
+
418
+ ---
419
+
420
+ ### VirtualFileSystem
421
+
422
+ In-memory filesystem with optional gzip compression and tar.gz persistence.
423
+
424
+ #### Constructor
425
+
426
+ ```typescript
427
+ new VirtualFileSystem(baseDir?: string)
428
+ ```
429
+
430
+ - **baseDir**: Directory to store `.vfs/mirror.tar.gz` snapshot (default: current working directory)
431
+
432
+ ```typescript
433
+ const vfs = new VirtualFileSystem("./container-data");
434
+ // Snapshot at ./container-data/.vfs/mirror.tar.gz
435
+ ```
436
+
437
+ #### Methods
438
+
439
+ ##### `async restoreMirror(): Promise<void>`
440
+
441
+ Loads filesystem state from disk snapshot. If missing, creates fresh filesystem.
442
+
443
+ ```typescript
444
+ await vfs.restoreMirror();
445
+ ```
446
+
447
+ ##### `async flushMirror(): Promise<void>`
448
+
449
+ Persists current filesystem state to disk. No-op if nothing changed.
450
+
451
+ ```typescript
452
+ // After file modifications...
453
+ await vfs.flushMirror();
454
+ ```
455
+
456
+ ##### `mkdir(path: string, mode?: number): void`
457
+
458
+ Creates directory and any missing parents. Throws if parent is a file.
459
+
460
+ ```typescript
461
+ vfs.mkdir("/home/user/.ssh", 0o700);
462
+ ```
463
+
464
+ ##### `writeFile(path: string, content: string | Buffer, options?: WriteFileOptions): void`
465
+
466
+ Writes file content. Creates parent directories if missing.
467
+
468
+ - **options.mode**: POSIX file mode (default: 0o644)
469
+ - **options.compress**: Store as gzip (default: false)
470
+
471
+ ```typescript
472
+ vfs.writeFile("/etc/app.conf", "debug=true\n", { compress: true });
473
+ ```
474
+
475
+ ##### `readFile(path: string): string`
476
+
477
+ Reads file as UTF-8 string. Transparently decompresses if needed.
478
+
479
+ ```typescript
480
+ const content = vfs.readFile("/etc/app.conf");
481
+ ```
482
+
483
+ ##### `exists(path: string): boolean`
484
+
485
+ Checks node existence (file or directory).
486
+
487
+ ```typescript
488
+ if (!vfs.exists("/var/log")) {
489
+ vfs.mkdir("/var/log");
490
+ }
491
+ ```
492
+
493
+ ##### `stat(path: string): VfsNodeStats`
494
+
495
+ Returns metadata (type, size, dates, mode, etc.).
496
+
497
+ ```typescript
498
+ const stats = vfs.stat("/etc/hostname");
499
+ if (stats.type === "file") {
500
+ console.log(`File size: ${stats.size} bytes`);
501
+ }
502
+ ```
503
+
504
+ ##### `list(dirPath?: string): string[]`
505
+
506
+ Lists child names in directory (sorted). Throws if path not a directory.
507
+
508
+ ```typescript
509
+ const files = vfs.list("/home");
510
+ // ["alice", "bob", "root"]
511
+ ```
512
+
513
+ ##### `tree(dirPath?: string): string`
514
+
515
+ Renders ASCII tree view of directory hierarchy.
516
+
517
+ ```typescript
518
+ console.log(vfs.tree("/home"));
519
+ ```
520
+
521
+ ##### `chmod(path: string, mode: number): void`
522
+
523
+ Updates file/dir permissions.
524
+
525
+ ```typescript
526
+ vfs.chmod("/tmp/script.sh", 0o755);
527
+ ```
528
+
529
+ ##### `remove(path: string, options?: RemoveOptions): void`
530
+
531
+ Removes file or directory. Throws if directory not empty unless `recursive: true`.
532
+
533
+ ```typescript
534
+ vfs.remove("/tmp/old", { recursive: true });
535
+ ```
536
+
537
+ ##### `move(fromPath: string, toPath: string): void`
538
+
539
+ Renames or moves node. Throws if destination exists.
540
+
541
+ ```typescript
542
+ vfs.move("/var/tmp", "/var/backup");
543
+ ```
544
+
545
+ ##### `compressFile(path: string): void`
546
+
547
+ Gzip-compresses file content and marks as compressed.
548
+
549
+ ```typescript
550
+ vfs.compressFile("/var/log/app.log");
551
+ ```
552
+
553
+ ##### `decompressFile(path: string): void`
554
+
555
+ Decompresses file content (inverse of `compressFile`).
556
+
557
+ ```typescript
558
+ vfs.decompressFile("/var/log/app.log");
559
+ ```
560
+
561
+ ---
562
+
563
+ ### VirtualUserManager
564
+
565
+ User authentication, password hashing (scrypt), sudo privilege management, and session tracking.
566
+
567
+ #### Constructor
568
+
569
+ ```typescript
570
+ new VirtualUserManager(vfs: VirtualFileSystem, defaultRootPassword?: string)
571
+ ```
572
+
573
+ - **vfs**: Virtual filesystem (for auth data persistence)
574
+ - **defaultRootPassword**: Root password if creating new user (default: "root")
575
+
576
+ ```typescript
577
+ const users = new VirtualUserManager(vfs, "SecureRootPass123");
578
+ ```
579
+
580
+ #### Methods
581
+
582
+ ##### `async initialize(): Promise<void>`
583
+
584
+ Loads users/sudoers from disk, ensures root exists, and initializes sessions.
585
+
586
+ ```typescript
587
+ await users.initialize();
588
+ ```
589
+
590
+ ##### `verifyPassword(username: string, password: string): boolean`
591
+
592
+ Checks plaintext password against hashed record.
593
+
594
+ ```typescript
595
+ if (users.verifyPassword("alice", "password123")) {
596
+ console.log("Auth OK");
597
+ }
598
+ ```
599
+
600
+ ##### `async addUser(username: string, password: string): Promise<void>`
601
+
602
+ Creates new user with home directory.
603
+
604
+ ```typescript
605
+ await users.addUser("bob", "bob_password");
606
+ // ~/bob created, added to sudoers
607
+ ```
608
+
609
+ ##### `async deleteUser(username: string): Promise<void>`
610
+
611
+ Removes user. Cannot delete root.
612
+
613
+ ```typescript
614
+ await users.deleteUser("bob");
615
+ ```
616
+
617
+ ##### `isSudoer(username: string): boolean`
618
+
619
+ Checks sudo access.
620
+
621
+ ```typescript
622
+ if (users.isSudoer("alice")) {
623
+ console.log("alice can use sudo");
624
+ }
625
+ ```
626
+
627
+ ##### `async addSudoer(username: string): Promise<void>`
628
+
629
+ Grants sudo privileges to user.
630
+
631
+ ```typescript
632
+ await users.addSudoer("charlie");
633
+ ```
634
+
635
+ ##### `async removeSudoer(username: string): Promise<void>`
636
+
637
+ Revokes sudo privileges. Cannot remove root.
638
+
639
+ ```typescript
640
+ await users.removeSudoer("charlie");
641
+ ```
642
+
643
+ ##### `registerSession(username: string, remoteAddress: string): VirtualActiveSession`
644
+
645
+ Creates active session (called on SSH auth). Returns session descriptor with UUID, tty, start time.
646
+
647
+ ```typescript
648
+ const session = users.registerSession("alice", "192.168.1.100");
649
+ console.log(session.id); // UUID
650
+ ```
651
+
652
+ ##### `unregisterSession(sessionId: string | null): void`
653
+
654
+ Closes session. Safe to call with null.
655
+
656
+ ```typescript
657
+ users.unregisterSession(sessionId);
658
+ ```
659
+
660
+ ##### `updateSession(sessionId: string | null, username: string, remoteAddress: string): void`
661
+
662
+ Updates session metadata (used for su/sudo).
663
+
664
+ ```typescript
665
+ users.updateSession(sessionId, "root", "192.168.1.100");
666
+ ```
667
+
668
+ ##### `listActiveSessions(): VirtualActiveSession[]`
669
+
670
+ Returns snapshot of active sessions (sorted by start time).
671
+
672
+ ```typescript
673
+ const sessions = users.listActiveSessions();
674
+ sessions.forEach(s => {
675
+ console.log(`${s.username}@${s.remoteAddress} on ${s.tty}`);
676
+ });
677
+ ```
678
+
679
+ ---
680
+
681
+ ### Key Types
682
+
683
+ #### CommandResult
684
+
685
+ Response from command execution (shell or programmatic).
686
+
687
+ ```typescript
688
+ interface CommandResult {
689
+ stdout?: string; // Standard output
690
+ stderr?: string; // Standard error
691
+ exitCode?: number; // Exit code (default: 0)
692
+ nextCwd?: string; // Updated cwd (used by cd command)
693
+ clearScreen?: boolean; // Request terminal clear
694
+ closeSession?: boolean; // Request session close
695
+ switchUser?: string; // User change request (su/sudo)
696
+ openEditor?: NanoEditorSession; // Text editor launch
697
+ openHtop?: boolean; // System monitor launch
698
+ sudoChallenge?: SudoChallenge; // Sudo password challenge
699
+ }
700
+ ```
701
+
702
+ #### VfsNodeStats
703
+
704
+ File/directory metadata.
705
+
706
+ ```typescript
707
+ type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
708
+
709
+ interface VfsFileNode {
710
+ type: "file";
711
+ name: string;
712
+ path: string;
713
+ mode: number; // POSIX mode bits
714
+ size: number; // Byte length
715
+ compressed: boolean; // Is gzip compressed?
716
+ createdAt: Date;
717
+ updatedAt: Date;
718
+ }
719
+
720
+ interface VfsDirectoryNode {
721
+ type: "directory";
722
+ name: string;
723
+ path: string;
724
+ mode: number;
725
+ childrenCount: number;
726
+ createdAt: Date;
727
+ updatedAt: Date;
728
+ }
729
+ ```
730
+
731
+ #### VirtualActiveSession
732
+
733
+ Active SSH/programmatic session descriptor.
734
+
735
+ ```typescript
736
+ interface VirtualActiveSession {
737
+ id: string; // UUID
738
+ username: string;
739
+ tty: string; // e.g., "pts/0"
740
+ remoteAddress: string; // Client IP or label
741
+ startedAt: string; // ISO-8601 timestamp
742
+ }
743
+ ```
744
+
745
+ ---
746
+
747
+ ## Usage Examples
748
+
749
+ ### Example 1: Basic SSH Server
750
+
751
+ Minimal server startup that accepts SSH connections:
752
+
753
+ ```typescript
754
+ import { VirtualMachine } from "typescript-virtual-container";
755
+
756
+ const ssh = new VirtualMachine({
757
+ port: 2222,
758
+ hostname: "lab-environment"
759
+ });
760
+
761
+ await ssh.start();
762
+ console.log("SSH server ready. Connect via: ssh root@localhost -p 2222");
763
+
764
+ // Keep running (e.g., in cloud deployment)
765
+ process.on("SIGINT", () => {
766
+ ssh.stop();
767
+ process.exit(0);
768
+ });
769
+ ```
770
+
771
+ **External SSH connection:**
772
+
773
+ ```bash
774
+ ssh root@localhost -p 2222
775
+ # Password: root
776
+ # $ whoami
777
+ # root
778
+ # $
779
+ ```
780
+
781
+ ---
782
+
783
+ ### Example 2: Programmatic File Operations
784
+
785
+ Create, read, modify files without SSH:
786
+
787
+ ```typescript
788
+ import { VirtualMachine, SshClient } from "typescript-virtual-container";
789
+
790
+ const ssh = new VirtualMachine({ port: 2222 });
791
+ await ssh.start();
792
+
793
+ const client = new SshClient(ssh, "root");
794
+
795
+ // Create structure
796
+ await client.mkdir("/app/config", true);
797
+ await client.mkdir("/app/logs", true);
798
+
799
+ // Write config
800
+ await client.writeFile("/app/config/settings.json", JSON.stringify({
801
+ environment: "dev",
802
+ port: 8080,
803
+ debug: true
804
+ }, null, 2));
805
+
806
+ // Read it back
807
+ const result = await client.readFile("/app/config/settings.json");
808
+ console.log("Config:", result.stdout);
809
+
810
+ // List directory
811
+ const list = await client.ls("/app");
812
+ console.log(list.stdout);
813
+
814
+ // Verify tree
815
+ console.log(await client.tree("/app"));
816
+
817
+ ssh.stop();
818
+ ```
819
+
820
+ ---
821
+
822
+ ### Example 3: Multi-User Environment
823
+
824
+ Create users, manage permissions, session tracking:
825
+
826
+ ```typescript
827
+ import { VirtualMachine, SshClient } from "typescript-virtual-container";
828
+
829
+ const ssh = new VirtualMachine({ port: 2222 });
830
+ await ssh.start();
831
+
832
+ const users = ssh.getUsers()!;
833
+
834
+ // Create users
835
+ await users.addUser("alice", "alice123");
836
+ await users.addUser("bob", "bob456");
837
+ console.log("Created users: alice, bob");
838
+
839
+ // Grant sudo to alice only
840
+ await users.removeSudoer("bob");
841
+ await users.addSudoer("alice");
842
+
843
+ // Alice: High privilege
844
+ const alice = new SshClient(ssh, "alice");
845
+ await alice.writeFile("/etc/important.conf", "secret=yes");
846
+
847
+ // Bob: Regular user
848
+ const bob = new SshClient(ssh, "bob");
849
+ const result = await bob.cat("/etc/important.conf");
850
+ console.log("Bob read file:", result.stderr);
851
+
852
+ ssh.stop();
853
+ ```
854
+
855
+ ---
856
+
857
+ ### Example 4: Persistent State
858
+
859
+ Save filesystem state between runs:
860
+
861
+ ```typescript
862
+ import { VirtualMachine } from "typescript-virtual-container";
863
+
864
+ // First run: Initialize
865
+ const ssh1 = new VirtualMachine({
866
+ port: 2222,
867
+ basePath: "./container"
868
+ });
869
+ await ssh1.start();
870
+ const vfs1 = ssh1.getVfs()!;
871
+
872
+ vfs1.mkdir("/data", 0o777);
873
+ vfs1.writeFile("/data/report.txt", "Baseline data");
874
+ await vfs1.flushMirror();
875
+ ssh1.stop();
876
+
877
+ console.log("State saved to ./container/.vfs/mirror.tar.gz");
878
+
879
+ // Later: Reload and continue
880
+ const ssh2 = new VirtualMachine({
881
+ port: 2223,
882
+ basePath: "./container"
883
+ });
884
+ await ssh2.start();
885
+ const vfs2 = ssh2.getVfs()!;
886
+ await vfs2.restoreMirror();
887
+
888
+ const content = vfs2.readFile("/data/report.txt");
889
+ console.log("Restored:", content);
890
+
891
+ ssh2.stop();
892
+ ```
893
+
894
+ ---
895
+
896
+ ### Example 5: CI/CD Automation
897
+
898
+ Simulate filesystem changes and verify outcomes:
899
+
900
+ ```typescript
901
+ import { VirtualMachine, SshClient } from "typescript-virtual-container";
902
+
903
+ async function testDeployment() {
904
+ const ssh = new VirtualMachine({ port: 2222 });
905
+ await ssh.start();
906
+
907
+ const client = new SshClient(ssh, "root");
908
+
909
+ // Pre-deployment: Set up base structure
910
+ await client.mkdir("/srv/app", true);
911
+ await client.writeFile("/srv/app/package.json", '{"name":"myapp"}');
912
+
913
+ // Simulate deployment: Write new version
914
+ await client.writeFile("/srv/app/app.js", 'console.log("v2.0");');
915
+
916
+ // Verify deployment: Read and validate
917
+ const appContent = await client.readFile("/srv/app/app.js");
918
+ if (appContent.stdout.includes("v2.0")) {
919
+ console.log("✓ Deployment verified");
920
+ } else {
921
+ console.error("✗ Deployment failed");
922
+ }
923
+
924
+ ssh.stop();
925
+ }
926
+
927
+ testDeployment().catch(console.error);
928
+ ```
929
+
930
+ ---
931
+
932
+ ### Example 6: Complex Navigation
933
+
934
+ Simulate shell workflows:
935
+
936
+ ```typescript
937
+ import { VirtualMachine, SshClient } from "typescript-virtual-container";
938
+
939
+ const ssh = new VirtualMachine({ port: 2222 });
940
+ await ssh.start();
941
+
942
+ const client = new SshClient(ssh, "root");
943
+
944
+ // Create nested structure
945
+ await client.mkdir("/home/user/projects/myapp/src", true);
946
+ await client.cd("/home/user/projects");
947
+
948
+ console.log(client.getCwd()); // "/home/user/projects"
949
+
950
+ // Navigate deeper
951
+ await client.cd("myapp/src");
952
+ console.log(client.getCwd()); // "/home/user/projects/myapp/src"
953
+
954
+ // Create files in new location
955
+ await client.writeFile("main.ts", "export function main() {}");
956
+ await client.writeFile("utils.ts", "export function util() {}");
957
+
958
+ // List current
959
+ const srcFiles = await client.ls();
960
+ console.log(srcFiles.stdout); // main.ts, utils.ts
961
+
962
+ // Navigate up (relative paths)
963
+ await client.cd("..");
964
+ console.log(client.getCwd()); // "/home/user/projects/myapp"
965
+
966
+ const appTree = await client.tree();
967
+ console.log(appTree.stdout);
968
+
969
+ ssh.stop();
970
+ ```
971
+
972
+ ---
973
+
974
+ ### Example 7: Error Handling
975
+
976
+ Graceful error handling in programmatic workflows:
977
+
978
+ ```typescript
979
+ import { VirtualMachine, SshClient } from "typescript-virtual-container";
980
+
981
+ const ssh = new VirtualMachine({ port: 2222 });
982
+ await ssh.start();
983
+
984
+ const client = new SshClient(ssh, "root");
985
+
986
+ // Try read non-existent file
987
+ const result = await client.readFile("/etc/nonexistent.conf");
988
+ if (result.exitCode !== 0) {
989
+ console.error("Read error:", result.stderr);
990
+ }
991
+
992
+ // Try change to non-existent directory
993
+ const cdResult = await client.cd("/invalid/path");
994
+ if (cdResult.exitCode !== 0) {
995
+ console.error("Invalid path");
996
+ }
997
+
998
+ // Try remove root
999
+ const rmResult = await client.rm("/", true);
1000
+ console.log("Remove root:", rmResult.stderr); // Error
1001
+
1002
+ ssh.stop();
1003
+ ```
1004
+
1005
+ ---
1006
+
1007
+ ## Built-in Commands
1008
+
1009
+ The following commands are available in both SSH shell mode and via `SshClient.exec()`:
1010
+
1011
+ | Command | Purpose | Notes |
1012
+ |---------|---------|-------|
1013
+ | `pwd` | Print working directory | No args |
1014
+ | `cd <path>` | Change directory | Updates client cwd |
1015
+ | `ls [path]` | List directory | Defaults to `.` |
1016
+ | `mkdir [-p] <path>` | Create directory | `-p` for parents |
1017
+ | `touch <path>` | Create empty file | Updates timestamps |
1018
+ | `cat <path>` | Read file | Displays content |
1019
+ | `rm [-r] <path>` | Remove file/dir | `-r` for recursive |
1020
+ | `tree [path]` | ASCII tree view | Defaults to `.` |
1021
+ | `whoami` | Current user | No args |
1022
+ | `hostname` | Server hostname | No args |
1023
+ | `who` | Active sessions | No args |
1024
+ | `sudo [-i] <cmd>` | Elevation | Requires sudoer status |
1025
+ | `su <user>` | Switch user | Requires password/sudo |
1026
+ | `adduser <name> <pass>` | Create user | Root only |
1027
+ | `deluser <name>` | Delete user | Root only, not root |
1028
+ | `curl <url>` | Fetch URL | Mock implementation |
1029
+ | `wget <url>` | Download | Mock implementation |
1030
+ | `nano <path>` | Text editor | Interactive mode |
1031
+ | `htop` | System monitor | Mock display |
1032
+ | `clear` | Clear screen | No args |
1033
+ | `exit [code]` | Close session | Optional exit code |
1034
+ | `help` | List commands | No args |
1035
+
1036
+ ---
1037
+
1038
+ ## Configuration
1039
+
1040
+ ### Environment Variables
1041
+
1042
+ - **`SSH_MIMIC_HOSTNAME`**: Override server hostname at startup (default: "typescript-vm")
1043
+ - **`SSH_MIMIC_ROOT_PASSWORD`**: Set root password (default: "root")
1044
+
1045
+ **Example:**
1046
+
1047
+ ```bash
1048
+ export SSH_MIMIC_HOSTNAME=production-lab
1049
+ export SSH_MIMIC_ROOT_PASSWORD=SecurePass123
1050
+ npm run start
1051
+ ```
1052
+
1053
+ ### Runtime Options
1054
+
1055
+ ```typescript
1056
+ const ssh = new VirtualMachine({
1057
+ port: 2222, // Required
1058
+ hostname: "my-container", // Optional
1059
+ basePath: "./data" // Optional, default: "."
1060
+ });
1061
+ ```
1062
+
1063
+ ---
1064
+
1065
+ ## Performance & Scalability
1066
+
1067
+ ### Memory Model
1068
+
1069
+ - **In-Memory FS**: Full filesystem tree kept in RAM (no lazy loading)
1070
+ - **Typical footprint**: ~1-10 MB for 1000 files, increases with file content
1071
+ - **Compression**: Use `compressFile()` or `compress: true` in `writeFile()` to reduce RAM usage
1072
+
1073
+ ### Concurrency
1074
+
1075
+ - SSH server handles multiple concurrent connections (event-driven)
1076
+ - Programmatic `SshClient` is synchronous (executes sequentially per instance)
1077
+ - Create multiple client instances for parallel operations
1078
+
1079
+ **Example:**
1080
+
1081
+ ```typescript
1082
+ const client1 = new SshClient(ssh, "alice");
1083
+ const client2 = new SshClient(ssh, "bob");
1084
+
1085
+ const [result1, result2] = await Promise.all([
1086
+ client1.writeFile("/tmp/alice.txt", "..."),
1087
+ client2.writeFile("/tmp/bob.txt", "...")
1088
+ ]);
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ## Types & TypeScript
1094
+
1095
+ Full TypeScript support with exported types:
1096
+
1097
+ ```typescript
1098
+ import type {
1099
+ CommandResult,
1100
+ VirtualActiveSession,
1101
+ VfsNodeStats,
1102
+ VfsFileNode,
1103
+ VfsDirectoryNode,
1104
+ SudoChallenge
1105
+ } from "typescript-virtual-container";
1106
+
1107
+ async function processResult(r: CommandResult) {
1108
+ if (r.exitCode === 0 && r.stdout) {
1109
+ console.log("Success:", r.stdout);
1110
+ } else if (r.stderr) {
1111
+ console.error("Error:", r.stderr);
1112
+ }
1113
+ }
1114
+ ```
1115
+
1116
+ ---
1117
+
1118
+ ## FAQ
1119
+
1120
+ ### Is this a real container runtime?
1121
+
1122
+ No. It emulates SSH sessions, users, and filesystem behavior in memory. It is ideal for testing, simulations, and automation workflows where full OS isolation is not required.
1123
+
1124
+ ### Can I use this in production?
1125
+
1126
+ You can use it in production-like automation contexts (sandboxed command runners, test harnesses, training environments), but it is not a security boundary like a real container/VM.
1127
+
1128
+ ### Does data persist between restarts?
1129
+
1130
+ Yes, if you call `flushMirror()` and use a stable `basePath`. State is restored from `.vfs/mirror.tar.gz`.
1131
+
1132
+ ### Is networking fully implemented for curl/wget?
1133
+
1134
+ `curl` and `wget` are command-layer implementations intended for realistic workflows, not full parity with GNU tooling.
1135
+
1136
+ ### Can I create custom commands?
1137
+
1138
+ Yes. Commands are modular and can be extended in the command runtime layer to fit project-specific use cases.
1139
+
1140
+ ---
1141
+
1142
+ ## Troubleshooting
1143
+
1144
+ ### Port Already in Use
1145
+
1146
+ ```
1147
+ Error: listen EADDRINUSE :::2222
1148
+ ```
1149
+
1150
+ **Solution**: Use a different port
1151
+
1152
+ ```typescript
1153
+ const ssh = new VirtualMachine({ port: 3333 });
1154
+ ```
1155
+
1156
+ ### SSH Authentication Failed
1157
+
1158
+ **Causes**: Server not started, wrong password, SSH client not found
1159
+
1160
+ **Solution**:
1161
+
1162
+ ```typescript
1163
+ process.env.SSH_MIMIC_ROOT_PASSWORD = "your-password";
1164
+ await ssh.start();
1165
+ ```
1166
+
1167
+ ### File Not Found Errors
1168
+
1169
+ **Cause**: Directory doesn't exist
1170
+
1171
+ **Solution**: Create directories first
1172
+
1173
+ ```typescript
1174
+ const vfs = ssh.getVfs();
1175
+ vfs.mkdir("/home/alice", 0o755);
1176
+ ```
1177
+
1178
+ ### Filesystem State Not Persisted
1179
+
1180
+ **Cause**: `flushMirror()` not called
1181
+
1182
+ **Solution**:
1183
+
1184
+ ```typescript
1185
+ await ssh.getVfs().flushMirror();
1186
+ ```
1187
+
1188
+ ---
1189
+
1190
+ ## Migration Guide
1191
+
1192
+ ### From v0.0.x to v1.x
1193
+
1194
+ Old API:
1195
+
1196
+ ```typescript
1197
+ const ssh = new SshMimic(2222, "hostname");
1198
+ ```
1199
+
1200
+ New API:
1201
+
1202
+ ```typescript
1203
+ const ssh = new VirtualMachine({ port: 2222, hostname: "hostname" });
1204
+ ```
1205
+
1206
+ **Changes**:
1207
+ - Object-based constructor
1208
+ - Optional `basePath` parameter
1209
+ - Exports renamed: `SshMimic` → `VirtualMachine` from main entry
1210
+
1211
+ ---
1212
+
1213
+ ## Contributing
1214
+
1215
+ 1. Fork repository
1216
+ 2. Create feature branch: `git checkout -b feat/my-feature`
1217
+ 3. Make changes and add tests
1218
+ 4. Format & lint: `bun format && bun check`
1219
+ 5. Push and open PR
1220
+
1221
+ **Code Quality**:
1222
+ - Biome formatting (opinionated)
1223
+ - Full TypeScript (no `any`)
1224
+ - JSDoc comments on public API
1225
+ - Async/await (no callbacks)
1226
+
1227
+ ---
1228
+
1229
+ ## Security
1230
+
1231
+ - Passwords are hashed with `scrypt` in the virtual auth store.
1232
+ - Root account is always protected and cannot be deleted.
1233
+ - Sudo privileges are explicit and persisted in sudoers data.
1234
+ - This project is not intended to provide kernel-level or process-level isolation.
1235
+
1236
+ If you discover a vulnerability, avoid public disclosure in issues and contact maintainers privately first.
1237
+
1238
+ ---
1239
+
1240
+ ## Support
1241
+
1242
+ - Open an issue for bugs, regressions, or feature requests.
1243
+ - Include Node/Bun version, package version, and a minimal reproduction.
1244
+ - For API questions, include the exact command sequence and expected vs actual result.
1245
+
1246
+ ---
1247
+
1248
+ ## License
1249
+
1250
+ MIT License. See LICENSE file for details.
1251
+
1252
+ ---
1253
+
1254
+ ## Roadmap
1255
+
1256
+ - [ ] Custom command plugin API
1257
+ - [ ] Optional per-user quotas for virtual filesystem usage
1258
+ - [ ] Improved shell compatibility for complex piping and redirection
1259
+ - [ ] Snapshot diff tooling for test assertions
1260
+ - [ ] Structured event hooks (session open/close, file write, sudo challenge)
1261
+
1262
+ ---
1263
+
1264
+ ## Changelog
1265
+
1266
+ ### v1.0.0 (2026-04-14)
1267
+
1268
+ **Initial Release**
1269
+
1270
+ - ✨ SSH server with password auth
1271
+ - ✨ Virtual filesystem with persistence
1272
+ - ✨ User management & sudoers
1273
+ - ✨ Programmatic `SshClient` API
1274
+ - ✨ 20+ built-in shell commands
1275
+ - ✨ Full TypeScript support & JSDoc coverage
1276
+ - ✨ tar.gz snapshot persistence
1277
+ - ✨ Session & TTY management
1278
+ - ✨ Interactive `nano` editor
1279
+ - ✨ Sudo/su privilege escalation
1280
+
1281
+ ---
1282
+
1283
+ **Made with ❤️ for testing, automation, and interactive TypeScript development.**