typescript-virtual-container 1.0.8 → 1.1.1

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 (54) hide show
  1. package/.vscode/settings.json +18 -0
  2. package/README.md +182 -91
  3. package/modules/shellInteractive.ts +45 -0
  4. package/modules/shellRuntime.ts +76 -0
  5. package/package.json +1 -1
  6. package/src/{SSHMimic/client.ts → SSHClient/index.ts} +17 -20
  7. package/src/SSHMimic/exec.ts +6 -17
  8. package/src/SSHMimic/executor.ts +20 -31
  9. package/src/SSHMimic/index.ts +23 -85
  10. package/src/VirtualFileSystem/index.ts +26 -1
  11. package/src/VirtualShell/index.ts +131 -26
  12. package/src/VirtualShell/shell.ts +43 -141
  13. package/src/VirtualShell/shellParser.ts +32 -7
  14. package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +155 -3
  15. package/src/{VirtualShell/commands → commands}/adduser.ts +3 -3
  16. package/src/{VirtualShell/commands → commands}/cat.ts +4 -4
  17. package/src/{VirtualShell/commands → commands}/cd.ts +3 -3
  18. package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
  19. package/src/{VirtualShell/commands → commands}/curl.ts +3 -3
  20. package/src/{VirtualShell/commands → commands}/deluser.ts +3 -3
  21. package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
  22. package/src/{VirtualShell/commands → commands}/env.ts +1 -1
  23. package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
  24. package/src/{VirtualShell/commands → commands}/export.ts +1 -1
  25. package/src/{VirtualShell/commands → commands}/grep.ts +3 -3
  26. package/src/{VirtualShell/commands → commands}/help.ts +1 -1
  27. package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
  28. package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
  29. package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
  30. package/src/{VirtualShell/commands → commands}/index.ts +19 -110
  31. package/src/{VirtualShell/commands → commands}/ls.ts +7 -5
  32. package/src/{VirtualShell/commands → commands}/mkdir.ts +3 -3
  33. package/src/{VirtualShell/commands → commands}/nano.ts +4 -4
  34. package/src/{VirtualShell/commands → commands}/neofetch.ts +4 -4
  35. package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
  36. package/src/{VirtualShell/commands → commands}/rm.ts +3 -3
  37. package/src/{VirtualShell/commands → commands}/set.ts +1 -1
  38. package/src/{VirtualShell/commands → commands}/sh.ts +3 -14
  39. package/src/{VirtualShell/commands → commands}/su.ts +3 -2
  40. package/src/{VirtualShell/commands → commands}/sudo.ts +4 -7
  41. package/src/{VirtualShell/commands → commands}/touch.ts +4 -4
  42. package/src/{VirtualShell/commands → commands}/tree.ts +3 -3
  43. package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
  44. package/src/{VirtualShell/commands → commands}/wget.ts +3 -3
  45. package/src/{VirtualShell/commands → commands}/who.ts +4 -4
  46. package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
  47. package/src/index.ts +6 -6
  48. package/src/standalone.ts +19 -14
  49. package/src/types/commands.ts +3 -11
  50. package/tests/command-helpers.test.ts +1 -1
  51. package/tests/helpers.test.ts +1 -1
  52. package/tests/parser-executor.test.ts +3 -6
  53. package/tests/users.test.ts +61 -1
  54. /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "files.exclude": {
3
+ "**/.git": true,
4
+ "**/.hg": true,
5
+ "**/.svn": true,
6
+ "**/.DS_Store": true,
7
+ "**/Thumbs.db": true,
8
+ "**/node_modules": true,
9
+
10
+ ".ssh-mimic": true,
11
+ ".vfs": true,
12
+
13
+ "LICENSE": true,
14
+ "*.md": true,
15
+ "biome.json": true,
16
+ "tsconfig.tsbuildinfo": true
17
+ }
18
+ }
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  ## Table of Contents
11
11
 
12
12
  - [Overview](#overview)
13
+ - [What This Is / What This Is Not](#what-this-is--what-this-is-not)
13
14
  - [Why This Package](#why-this-package)
14
15
  - [Installation](#installation)
15
16
  - [Compatibility](#compatibility)
@@ -42,6 +43,22 @@
42
43
  - **Built-in Commands**: `ls`, `cd`, `pwd`, `cat`, `mkdir`, `touch`, `rm`, `tree`, `whoami`, `hostname`, `who`, `sudo`, `su`, `adduser`, `deluser`, `nano` (text editor), `curl`, `wget`, and a growing set of additional commands. Not everything is implemented yet, and shell compatibility is still being expanded.
43
44
  - **Full TypeScript Support**: Complete JSDoc coverage, exported types, and first-class async/await for all operations.
44
45
 
46
+ ## What This Is / What This Is Not
47
+
48
+ ### What This Is
49
+
50
+ - A virtual shell runtime written in TypeScript.
51
+ - An in-memory environment with its own virtual filesystem, user management, and command runtime.
52
+ - A practical tool for deterministic testing, automation pipelines, and SSH-like workflows without running real containers.
53
+
54
+ ### What This Is Not
55
+
56
+ - Not a fully isolated container runtime.
57
+ - Not a security sandbox.
58
+ - Not a kernel-level isolation boundary (unlike Docker/VM-based isolation).
59
+
60
+ This project emulates shell behavior for developer workflows. It is designed for realism and productivity, not hard security isolation.
61
+
45
62
  ## Why This Package
46
63
 
47
64
  This package is designed for teams that need a realistic SSH-like runtime without spinning up real containers or VMs.
@@ -87,10 +104,10 @@ The virtual filesystem and shell behavior are intentionally portable and do not
87
104
  ### Running an SSH Server
88
105
 
89
106
  ```typescript
90
- import { VirtualMachine } from "typescript-virtual-container";
107
+ import { VirtualSshServer } from "typescript-virtual-container";
91
108
 
92
109
  // Create server on port 2222
93
- const ssh = new VirtualMachine({
110
+ const ssh = new VirtualSshServer({
94
111
  port: 2222,
95
112
  hostname: "my-container"
96
113
  });
@@ -112,13 +129,14 @@ process.on("SIGTERM", () => {
112
129
  ### Using the Programmatic Client API
113
130
 
114
131
  ```typescript
115
- import { VirtualMachine, SshClient } from "typescript-virtual-container";
132
+ import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
116
133
 
117
- const ssh = new VirtualMachine({ port: 2222 });
134
+ const shell = new VirtualShell("typescript-vm");
135
+ const ssh = new VirtualSshServer({ port: 2222, shell });
118
136
  await ssh.start();
119
137
 
120
138
  // Create authenticated client for specific user
121
- const client = new SshClient(ssh, "root");
139
+ const client = new SshClient(shell, "root");
122
140
 
123
141
  // Execute commands programmatically
124
142
  const list = await client.ls("/home");
@@ -178,7 +196,7 @@ ssh.stop();
178
196
 
179
197
  ### SshMimic (SSH Server)
180
198
 
181
- Main SSH server class. Manages virtual filesystem, user authentication, and session handlers.
199
+ Main SSH server class, exported as `VirtualSshServer` in the package entrypoint. It wires the virtual shell runtime into ssh2 sessions and manages authentication/session handlers.
182
200
 
183
201
  #### Constructor
184
202
 
@@ -186,17 +204,25 @@ Main SSH server class. Manages virtual filesystem, user authentication, and sess
186
204
  new SshMimic(options: {
187
205
  port: number; // TCP port to bind on localhost
188
206
  hostname?: string; // Virtual hostname (default: "typescript-vm")
189
- basePath?: string; // Base directory for VFS snapshot storage (default: ".")
207
+ shell?: VirtualShell; // Optional preconfigured shell instance
190
208
  })
191
209
  ```
192
210
 
211
+ - `hostname` controls the SSH ident label and the default hostname used by a generated shell.
212
+ - If `shell` is omitted, the server creates `new VirtualShell(hostname)` for you.
213
+
193
214
  **Example:**
194
215
 
195
216
  ```typescript
217
+ const virtualShell = new VirtualShell("my-lab", {
218
+ kernel: "1.0.0+itsrealfortune+1-amd64",
219
+ os: "Fortune GNU/Linux x64",
220
+ arch: "x86_64",
221
+ }, "./data");
196
222
  const ssh = new SshMimic({
197
223
  port: 2222,
198
224
  hostname: "my-lab",
199
- basePath: "./data" // Snapshots stored in ./data/.vfs/mirror.tar.gz
225
+ shell: virtualShell
200
226
  });
201
227
  ```
202
228
 
@@ -254,21 +280,22 @@ console.log(`Server name: ${ssh.getHostname()}`);
254
280
 
255
281
  ### SshClient (Programmatic Shell API)
256
282
 
257
- Execute shell commands as a specific user without SSH overhead. Maintains connection state (current working directory) across calls.
283
+ Execute shell commands against a `VirtualShell` instance without SSH overhead. Maintains connection state (current working directory) across calls.
258
284
 
259
285
  #### Constructor
260
286
 
261
287
  ```typescript
262
- new SshClient(ssh: SshMimic, username: string)
288
+ new SshClient(shell: VirtualShell, username: string)
263
289
  ```
264
290
 
265
- - **ssh**: Parent SSH server instance (must be started)
291
+ - **shell**: Parent virtual shell instance
266
292
  - **username**: User to authenticate as (no password required)
267
293
 
268
294
  **Example:**
269
295
 
270
296
  ```typescript
271
- const client = new SshClient(ssh, "alice");
297
+ const shell = new VirtualShell("typescript-vm");
298
+ const client = new SshClient(shell, "alice");
272
299
  ```
273
300
 
274
301
  #### Methods
@@ -419,30 +446,57 @@ console.log(client.getUsername()); // Username from constructor
419
446
 
420
447
  ### VirtualShell
421
448
 
422
- Encapsulates shell execution primitives used by the SSH runtime for command dispatch and interactive sessions.
449
+ Encapsulates shell execution primitives used by the SSH runtime for command dispatch, interactive sessions, and the programmatic client.
450
+
451
+ #### ShellProperties
452
+
453
+ ```typescript
454
+ interface ShellProperties {
455
+ kernel: string;
456
+ os: "Fortune GNU/Linux x64";
457
+ arch: "x86_64";
458
+ }
459
+
460
+ const defaultShellProperties: ShellProperties;
461
+ ```
462
+
463
+ - `kernel` is displayed in shell/system information output.
464
+ - `os` and `arch` are fixed labels used by the shell runtime.
423
465
 
424
466
  #### Constructor
425
467
 
426
468
  ```typescript
427
469
  new VirtualShell(
428
- vfs: VirtualFileSystem,
429
- users: VirtualUserManager,
430
470
  hostname: string,
471
+ properties?: ShellProperties,
472
+ basePath?: string,
431
473
  )
432
474
  ```
433
475
 
434
- - **vfs**: Virtual filesystem instance used by shell commands.
435
- - **users**: User manager for authentication/session-aware command behavior.
436
476
  - **hostname**: Hostname injected into command context and prompt behavior.
477
+ - **properties**: Optional shell metadata. Defaults to `defaultShellProperties`.
478
+ - **basePath**: Optional directory used to resolve `.vfs/mirror.tar.gz` (defaults to `.`).
437
479
 
438
480
  **Example:**
439
481
 
440
482
  ```typescript
441
- const shell = new VirtualShell(vfs, users, "typescript-vm");
483
+ const shell = new VirtualShell("typescript-vm", {
484
+ kernel: "1.0.0+itsrealfortune+1-amd64",
485
+ os: "Fortune GNU/Linux x64",
486
+ arch: "x86_64",
487
+ }, "./data");
442
488
  ```
443
489
 
444
490
  #### Methods
445
491
 
492
+ ##### `addCommand(name: string, params: string[], callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>): void`
493
+
494
+ Registers a custom command at runtime.
495
+
496
+ ```typescript
497
+ shell.addCommand("hello", [], () => ({ stdout: "hello", exitCode: 0 }));
498
+ ```
499
+
446
500
  ##### `executeCommand(rawInput: string, authUser: string, cwd: string): void`
447
501
 
448
502
  Runs one command input in shell mode for a given user and working directory.
@@ -617,11 +671,12 @@ User authentication, password hashing (scrypt), sudo privilege management, and s
617
671
  #### Constructor
618
672
 
619
673
  ```typescript
620
- new VirtualUserManager(vfs: VirtualFileSystem, defaultRootPassword?: string)
674
+ new VirtualUserManager(vfs: VirtualFileSystem, defaultRootPassword?: string, autoSudoForNewUsers?: boolean)
621
675
  ```
622
676
 
623
677
  - **vfs**: Virtual filesystem (for auth data persistence)
624
- - **defaultRootPassword**: Root password if creating new user (default: "root")
678
+ - **defaultRootPassword**: Root password used when root is created (default: "root")
679
+ - **autoSudoForNewUsers**: When true, new users are added to sudoers automatically (default: `true` unless `SSH_MIMIC_AUTO_SUDO_NEW_USERS` disables it)
625
680
 
626
681
  ```typescript
627
682
  const users = new VirtualUserManager(vfs, "SecureRootPass123");
@@ -690,6 +745,46 @@ Revokes sudo privileges. Cannot remove root.
690
745
  await users.removeSudoer("charlie");
691
746
  ```
692
747
 
748
+ ##### `async setQuotaBytes(username: string, maxBytes: number): Promise<void>`
749
+
750
+ Sets an optional per-user quota (bytes) for writes under `/home/<username>`.
751
+
752
+ ```typescript
753
+ await users.setQuotaBytes("alice", 5 * 1024 * 1024); // 5 MB
754
+ ```
755
+
756
+ ##### `async clearQuota(username: string): Promise<void>`
757
+
758
+ Removes quota limit for a user.
759
+
760
+ ```typescript
761
+ await users.clearQuota("alice");
762
+ ```
763
+
764
+ ##### `getQuotaBytes(username: string): number | null`
765
+
766
+ Returns configured quota in bytes, or `null` if unlimited.
767
+
768
+ ```typescript
769
+ console.log(users.getQuotaBytes("alice"));
770
+ ```
771
+
772
+ ##### `getUsageBytes(username: string): number`
773
+
774
+ Returns current stored usage in bytes under `/home/<username>`.
775
+
776
+ ```typescript
777
+ console.log(users.getUsageBytes("alice"));
778
+ ```
779
+
780
+ ##### `assertWriteWithinQuota(username: string, targetPath: string, nextContent: string | Buffer): void`
781
+
782
+ Validates a write operation against quota rules; throws when projected usage exceeds quota.
783
+
784
+ ```typescript
785
+ users.assertWriteWithinQuota("alice", "/home/alice/data.txt", "payload");
786
+ ```
787
+
693
788
  ##### `registerSession(username: string, remoteAddress: string): VirtualActiveSession`
694
789
 
695
790
  Creates active session (called on SSH auth). Returns session descriptor with UUID, tty, start time.
@@ -801,9 +896,9 @@ interface VirtualActiveSession {
801
896
  Minimal server startup that accepts SSH connections:
802
897
 
803
898
  ```typescript
804
- import { VirtualMachine } from "typescript-virtual-container";
899
+ import { VirtualSshServer } from "typescript-virtual-container";
805
900
 
806
- const ssh = new VirtualMachine({
901
+ const ssh = new VirtualSshServer({
807
902
  port: 2222,
808
903
  hostname: "lab-environment"
809
904
  });
@@ -835,12 +930,13 @@ ssh root@localhost -p 2222
835
930
  Create, read, modify files without SSH:
836
931
 
837
932
  ```typescript
838
- import { VirtualMachine, SshClient } from "typescript-virtual-container";
933
+ import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
839
934
 
840
- const ssh = new VirtualMachine({ port: 2222 });
935
+ const shell = new VirtualShell("typescript-vm");
936
+ const ssh = new VirtualSshServer({ port: 2222, shell });
841
937
  await ssh.start();
842
938
 
843
- const client = new SshClient(ssh, "root");
939
+ const client = new SshClient(shell, "root");
844
940
 
845
941
  // Create structure
846
942
  await client.mkdir("/app/config", true);
@@ -874,9 +970,10 @@ ssh.stop();
874
970
  Create users, manage permissions, session tracking:
875
971
 
876
972
  ```typescript
877
- import { VirtualMachine, SshClient } from "typescript-virtual-container";
973
+ import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
878
974
 
879
- const ssh = new VirtualMachine({ port: 2222 });
975
+ const shell = new VirtualShell("typescript-vm");
976
+ const ssh = new VirtualSshServer({ port: 2222, shell });
880
977
  await ssh.start();
881
978
 
882
979
  const users = ssh.getUsers()!;
@@ -891,11 +988,11 @@ await users.removeSudoer("bob");
891
988
  await users.addSudoer("alice");
892
989
 
893
990
  // Alice: High privilege
894
- const alice = new SshClient(ssh, "alice");
991
+ const alice = new SshClient(shell, "alice");
895
992
  await alice.writeFile("/etc/important.conf", "secret=yes");
896
993
 
897
994
  // Bob: Regular user
898
- const bob = new SshClient(ssh, "bob");
995
+ const bob = new SshClient(shell, "bob");
899
996
  const result = await bob.cat("/etc/important.conf");
900
997
  console.log("Bob read file:", result.stderr);
901
998
 
@@ -909,12 +1006,13 @@ ssh.stop();
909
1006
  Save filesystem state between runs:
910
1007
 
911
1008
  ```typescript
912
- import { VirtualMachine } from "typescript-virtual-container";
1009
+ import { VirtualSshServer, VirtualShell } from "typescript-virtual-container";
913
1010
 
914
1011
  // First run: Initialize
915
- const ssh1 = new VirtualMachine({
1012
+ const shell1 = new VirtualShell("typescript-vm", undefined, "./container");
1013
+ const ssh1 = new VirtualSshServer({
916
1014
  port: 2222,
917
- basePath: "./container"
1015
+ shell: shell1
918
1016
  });
919
1017
  await ssh1.start();
920
1018
  const vfs1 = ssh1.getVfs()!;
@@ -927,9 +1025,10 @@ ssh1.stop();
927
1025
  console.log("State saved to ./container/.vfs/mirror.tar.gz");
928
1026
 
929
1027
  // Later: Reload and continue
930
- const ssh2 = new VirtualMachine({
1028
+ const shell2 = new VirtualShell("typescript-vm", undefined, "./container");
1029
+ const ssh2 = new VirtualSshServer({
931
1030
  port: 2223,
932
- basePath: "./container"
1031
+ shell: shell2
933
1032
  });
934
1033
  await ssh2.start();
935
1034
  const vfs2 = ssh2.getVfs()!;
@@ -948,13 +1047,14 @@ ssh2.stop();
948
1047
  Simulate filesystem changes and verify outcomes:
949
1048
 
950
1049
  ```typescript
951
- import { VirtualMachine, SshClient } from "typescript-virtual-container";
1050
+ import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
952
1051
 
953
1052
  async function testDeployment() {
954
- const ssh = new VirtualMachine({ port: 2222 });
1053
+ const shell = new VirtualShell("typescript-vm");
1054
+ const ssh = new VirtualSshServer({ port: 2222, shell });
955
1055
  await ssh.start();
956
1056
 
957
- const client = new SshClient(ssh, "root");
1057
+ const client = new SshClient(shell, "root");
958
1058
 
959
1059
  // Pre-deployment: Set up base structure
960
1060
  await client.mkdir("/srv/app", true);
@@ -984,12 +1084,13 @@ testDeployment().catch(console.error);
984
1084
  Simulate shell workflows:
985
1085
 
986
1086
  ```typescript
987
- import { VirtualMachine, SshClient } from "typescript-virtual-container";
1087
+ import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
988
1088
 
989
- const ssh = new VirtualMachine({ port: 2222 });
1089
+ const shell = new VirtualShell("typescript-vm");
1090
+ const ssh = new VirtualSshServer({ port: 2222, shell });
990
1091
  await ssh.start();
991
1092
 
992
- const client = new SshClient(ssh, "root");
1093
+ const client = new SshClient(shell, "root");
993
1094
 
994
1095
  // Create nested structure
995
1096
  await client.mkdir("/home/user/projects/myapp/src", true);
@@ -1026,12 +1127,13 @@ ssh.stop();
1026
1127
  Graceful error handling in programmatic workflows:
1027
1128
 
1028
1129
  ```typescript
1029
- import { VirtualMachine, SshClient } from "typescript-virtual-container";
1130
+ import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
1030
1131
 
1031
- const ssh = new VirtualMachine({ port: 2222 });
1132
+ const shell = new VirtualShell("typescript-vm");
1133
+ const ssh = new VirtualSshServer({ port: 2222, shell });
1032
1134
  await ssh.start();
1033
1135
 
1034
- const client = new SshClient(ssh, "root");
1136
+ const client = new SshClient(shell, "root");
1035
1137
 
1036
1138
  // Try read non-existent file
1037
1139
  const result = await client.readFile("/etc/nonexistent.conf");
@@ -1056,32 +1158,42 @@ ssh.stop();
1056
1158
 
1057
1159
  ## Built-in Commands
1058
1160
 
1059
- The following commands are available in both SSH shell mode and via `SshClient.exec()`. This list is intentionally incomplete: some commands, flags, and edge cases are still missing or only partially compatible with real shells, and that will continue to be worked on.
1161
+ The following commands are currently registered and available in both SSH shell mode and via `SshClient.exec()`. Some flags and edge-case behavior are still being expanded for shell compatibility.
1060
1162
 
1061
1163
  | Command | Purpose | Notes |
1062
1164
  |---------|---------|-------|
1063
- | `pwd` | Print working directory | No args |
1165
+ | `adduser <name> <pass>` | Create user | Root only |
1166
+ | `cat <path>` | Read file | Displays content |
1064
1167
  | `cd <path>` | Change directory | Updates client cwd |
1168
+ | `clear` | Clear screen | No args |
1169
+ | `curl <url>` | Fetch URL | Mock implementation |
1170
+ | `deluser <name>` | Delete user | Root only, not root |
1171
+ | `echo <text...>` | Print text | Supports shell-like argument output |
1172
+ | `env` | List environment variables | Shell environment view |
1173
+ | `exit [code]` | Close session | Optional exit code |
1174
+ | `export NAME=VALUE` | Set/export environment variable | Persists in shell env |
1175
+ | `grep <pattern> [path]` | Search for text | Simplified grep behavior |
1176
+ | `help` | List commands | No args |
1177
+ | `hostname` | Server hostname | No args |
1178
+ | `htop` | System monitor | Mock display |
1179
+ | `pwd` | Print working directory | No args |
1065
1180
  | `ls [path]` | List directory | Defaults to `.` |
1066
1181
  | `mkdir [-p] <path>` | Create directory | `-p` for parents |
1182
+ | `nano <path>` | Text editor | Interactive mode |
1183
+ | `neofetch` | Show system summary | Mock display |
1067
1184
  | `touch <path>` | Create empty file | Updates timestamps |
1068
- | `cat <path>` | Read file | Displays content |
1069
1185
  | `rm [-r] <path>` | Remove file/dir | `-r` for recursive |
1070
- | `tree [path]` | ASCII tree view | Defaults to `.` |
1071
- | `whoami` | Current user | No args |
1072
- | `hostname` | Server hostname | No args |
1073
- | `who` | Active sessions | No args |
1074
- | `sudo [-i] <cmd>` | Elevation | Requires sudoer status |
1186
+ | `set` | Show shell options/variables | Simplified behavior |
1187
+ | `sh <script>` | Run shell script | Simplified execution model |
1075
1188
  | `su <user>` | Switch user | Requires password/sudo |
1076
- | `adduser <name> <pass>` | Create user | Root only |
1077
- | `deluser <name>` | Delete user | Root only, not root |
1078
- | `curl <url>` | Fetch URL | Mock implementation |
1189
+ | `sudo [-i] <cmd>` | Elevation | Requires sudoer status |
1190
+ | `tree [path]` | ASCII tree view | Defaults to `.` |
1191
+ | `unset <name>` | Remove environment variable | Shell environment update |
1079
1192
  | `wget <url>` | Download | Mock implementation |
1080
- | `nano <path>` | Text editor | Interactive mode |
1081
- | `htop` | System monitor | Mock display |
1082
- | `clear` | Clear screen | No args |
1083
- | `exit [code]` | Close session | Optional exit code |
1084
- | `help` | List commands | No args |
1193
+ | `who` | Active sessions | No args |
1194
+ | `whoami` | Current user | No args |
1195
+
1196
+ Commands can be added via the VirtualShell addCommand() method for custom behavior.
1085
1197
 
1086
1198
  ---
1087
1199
 
@@ -1105,10 +1217,11 @@ npm run start
1105
1217
  ### Runtime Options
1106
1218
 
1107
1219
  ```typescript
1108
- const ssh = new VirtualMachine({
1220
+ const shell = new VirtualShell("my-container", undefined, "./data");
1221
+ const ssh = new VirtualSshServer({
1109
1222
  port: 2222, // Required
1110
1223
  hostname: "my-container", // Optional
1111
- basePath: "./data" // Optional, default: "."
1224
+ shell // Optional, prebuilt shell instance
1112
1225
  });
1113
1226
  ```
1114
1227
 
@@ -1131,8 +1244,9 @@ const ssh = new VirtualMachine({
1131
1244
  **Example:**
1132
1245
 
1133
1246
  ```typescript
1134
- const client1 = new SshClient(ssh, "alice");
1135
- const client2 = new SshClient(ssh, "bob");
1247
+ const shell = new VirtualShell("typescript-vm");
1248
+ const client1 = new SshClient(shell, "alice");
1249
+ const client2 = new SshClient(shell, "bob");
1136
1250
 
1137
1251
  const [result1, result2] = await Promise.all([
1138
1252
  client1.writeFile("/tmp/alice.txt", "..."),
@@ -1202,7 +1316,7 @@ Error: listen EADDRINUSE :::2222
1202
1316
  **Solution**: Use a different port
1203
1317
 
1204
1318
  ```typescript
1205
- const ssh = new VirtualMachine({ port: 3333 });
1319
+ const ssh = new VirtualSshServer({ port: 3333 });
1206
1320
  ```
1207
1321
 
1208
1322
  ### SSH Authentication Failed
@@ -1239,29 +1353,6 @@ await ssh.getVfs().flushMirror();
1239
1353
 
1240
1354
  ---
1241
1355
 
1242
- ## Migration Guide
1243
-
1244
- ### From v0.0.x to v1.x
1245
-
1246
- Old API:
1247
-
1248
- ```typescript
1249
- const ssh = new SshMimic(2222, "hostname");
1250
- ```
1251
-
1252
- New API:
1253
-
1254
- ```typescript
1255
- const ssh = new VirtualMachine({ port: 2222, hostname: "hostname" });
1256
- ```
1257
-
1258
- **Changes**:
1259
- - Object-based constructor
1260
- - Optional `basePath` parameter
1261
- - Exports renamed: `SshMimic` → `VirtualMachine` from main entry
1262
-
1263
- ---
1264
-
1265
1356
  ## Contributing
1266
1357
 
1267
1358
  1. Fork repository
@@ -1307,9 +1398,9 @@ MIT License. See LICENSE file for details.
1307
1398
 
1308
1399
  ## Roadmap
1309
1400
 
1310
- - [ ] Custom command plugin API
1311
- - [ ] Optional per-user quotas for virtual filesystem usage
1312
- - [ ] Improved shell compatibility for complex piping and redirection
1401
+ - [x] Custom command plugin API
1402
+ - [x] Optional per-user quotas for virtual filesystem usage
1403
+ - [x] Improved shell compatibility for complex piping and redirection
1313
1404
  - [ ] Snapshot diff tooling for test assertions
1314
- - [ ] Structured event hooks (session open/close, file write, sudo challenge)
1405
+ - [x] Structured event hooks (session open/close, file write, sudo challenge)
1315
1406
  - [ ] WebSocket-based remote shell client (experimental)
@@ -0,0 +1,45 @@
1
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
+ import type { ShellStream } from "../src/types/streams";
3
+ import { shellQuote, type TerminalSize, withTerminalSize } from "./shellRuntime";
4
+
5
+ function spawnScriptProcess(
6
+ command: string,
7
+ terminalSize: TerminalSize,
8
+ stream: ShellStream,
9
+ ): ChildProcessWithoutNullStreams {
10
+ const formatted = withTerminalSize(command, terminalSize);
11
+ const proc = spawn("script", ["-qfec", formatted, "/dev/null"], {
12
+ stdio: ["pipe", "pipe", "pipe"],
13
+ env: {
14
+ ...process.env,
15
+ // biome-ignore lint/style/useNamingConvention: env variable should be uppercase
16
+ TERM: process.env.TERM ?? "xterm-256color",
17
+ },
18
+ });
19
+
20
+ proc.stdout.on("data", (data: Buffer) => {
21
+ stream.write(data.toString("utf8"));
22
+ });
23
+
24
+ proc.stderr.on("data", (data: Buffer) => {
25
+ stream.write(data.toString("utf8"));
26
+ });
27
+
28
+ return proc;
29
+ }
30
+
31
+ export function spawnNanoEditorProcess(
32
+ tempPath: string,
33
+ terminalSize: TerminalSize,
34
+ stream: ShellStream,
35
+ ): ChildProcessWithoutNullStreams {
36
+ return spawnScriptProcess(`nano -- ${shellQuote(tempPath)}`, terminalSize, stream);
37
+ }
38
+
39
+ export function spawnHtopProcess(
40
+ pidList: string,
41
+ terminalSize: TerminalSize,
42
+ stream: ShellStream,
43
+ ): ChildProcessWithoutNullStreams {
44
+ return spawnScriptProcess(`htop -p ${shellQuote(pidList)}`, terminalSize, stream);
45
+ }
@@ -0,0 +1,76 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ export interface TerminalSize {
5
+ cols: number;
6
+ rows: number;
7
+ }
8
+
9
+ export function shellQuote(value: string): string {
10
+ return `'${value.replace(/'/g, `'\\''`)}'`;
11
+ }
12
+
13
+ export function toTtyLines(text: string): string {
14
+ return text
15
+ .replace(/\r\n/g, "\n")
16
+ .replace(/\r/g, "\n")
17
+ .replace(/\n/g, "\r\n");
18
+ }
19
+
20
+ export function withTerminalSize(
21
+ command: string,
22
+ terminalSize: TerminalSize,
23
+ ): string {
24
+ const cols =
25
+ Number.isFinite(terminalSize.cols) && terminalSize.cols > 0
26
+ ? Math.floor(terminalSize.cols)
27
+ : 80;
28
+ const rows =
29
+ Number.isFinite(terminalSize.rows) && terminalSize.rows > 0
30
+ ? Math.floor(terminalSize.rows)
31
+ : 24;
32
+ return `stty cols ${cols} rows ${rows} 2>/dev/null; ${command}`;
33
+ }
34
+
35
+ export function resolvePath(base: string, inputPath: string): string {
36
+ if (!inputPath || inputPath.trim() === "" || inputPath === ".") {
37
+ return base;
38
+ }
39
+ return inputPath.startsWith("/")
40
+ ? path.posix.normalize(inputPath)
41
+ : path.posix.normalize(path.posix.join(base, inputPath));
42
+ }
43
+
44
+ export async function collectChildPids(parentPid: number): Promise<number[]> {
45
+ try {
46
+ const childrenRaw = await readFile(
47
+ `/proc/${parentPid}/task/${parentPid}/children`,
48
+ "utf8",
49
+ );
50
+ const directChildren = childrenRaw
51
+ .trim()
52
+ .split(/\s+/)
53
+ .filter(Boolean)
54
+ .map((value) => Number.parseInt(value, 10))
55
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
56
+
57
+ const nested = await Promise.all(
58
+ directChildren.map((pid) => collectChildPids(pid)),
59
+ );
60
+ return [...directChildren, ...nested.flat()];
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ export async function getVisibleHtopPidList(
67
+ rootPid = process.pid,
68
+ ): Promise<string | null> {
69
+ const descendants = await collectChildPids(rootPid);
70
+ const unique = Array.from(new Set(descendants)).sort((a, b) => a - b);
71
+ if (unique.length === 0) {
72
+ return null;
73
+ }
74
+
75
+ return unique.join(",");
76
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "In-memory SSH server with virtual filesystem and typed programmatic API",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
- "version": "1.0.8",
6
+ "version": "1.1.1",
7
7
  "license": "MIT",
8
8
  "keywords": [
9
9
  "ssh",