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.
- package/.vscode/settings.json +18 -0
- package/README.md +182 -91
- package/modules/shellInteractive.ts +45 -0
- package/modules/shellRuntime.ts +76 -0
- package/package.json +1 -1
- package/src/{SSHMimic/client.ts → SSHClient/index.ts} +17 -20
- package/src/SSHMimic/exec.ts +6 -17
- package/src/SSHMimic/executor.ts +20 -31
- package/src/SSHMimic/index.ts +23 -85
- package/src/VirtualFileSystem/index.ts +26 -1
- package/src/VirtualShell/index.ts +131 -26
- package/src/VirtualShell/shell.ts +43 -141
- package/src/VirtualShell/shellParser.ts +32 -7
- package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +155 -3
- package/src/{VirtualShell/commands → commands}/adduser.ts +3 -3
- package/src/{VirtualShell/commands → commands}/cat.ts +4 -4
- package/src/{VirtualShell/commands → commands}/cd.ts +3 -3
- package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
- package/src/{VirtualShell/commands → commands}/curl.ts +3 -3
- package/src/{VirtualShell/commands → commands}/deluser.ts +3 -3
- package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
- package/src/{VirtualShell/commands → commands}/env.ts +1 -1
- package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
- package/src/{VirtualShell/commands → commands}/export.ts +1 -1
- package/src/{VirtualShell/commands → commands}/grep.ts +3 -3
- package/src/{VirtualShell/commands → commands}/help.ts +1 -1
- package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
- package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
- package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
- package/src/{VirtualShell/commands → commands}/index.ts +19 -110
- package/src/{VirtualShell/commands → commands}/ls.ts +7 -5
- package/src/{VirtualShell/commands → commands}/mkdir.ts +3 -3
- package/src/{VirtualShell/commands → commands}/nano.ts +4 -4
- package/src/{VirtualShell/commands → commands}/neofetch.ts +4 -4
- package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
- package/src/{VirtualShell/commands → commands}/rm.ts +3 -3
- package/src/{VirtualShell/commands → commands}/set.ts +1 -1
- package/src/{VirtualShell/commands → commands}/sh.ts +3 -14
- package/src/{VirtualShell/commands → commands}/su.ts +3 -2
- package/src/{VirtualShell/commands → commands}/sudo.ts +4 -7
- package/src/{VirtualShell/commands → commands}/touch.ts +4 -4
- package/src/{VirtualShell/commands → commands}/tree.ts +3 -3
- package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
- package/src/{VirtualShell/commands → commands}/wget.ts +3 -3
- package/src/{VirtualShell/commands → commands}/who.ts +4 -4
- package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
- package/src/index.ts +6 -6
- package/src/standalone.ts +19 -14
- package/src/types/commands.ts +3 -11
- package/tests/command-helpers.test.ts +1 -1
- package/tests/helpers.test.ts +1 -1
- package/tests/parser-executor.test.ts +3 -6
- package/tests/users.test.ts +61 -1
- /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 {
|
|
107
|
+
import { VirtualSshServer } from "typescript-virtual-container";
|
|
91
108
|
|
|
92
109
|
// Create server on port 2222
|
|
93
|
-
const ssh = new
|
|
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 {
|
|
132
|
+
import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
|
|
116
133
|
|
|
117
|
-
const
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
288
|
+
new SshClient(shell: VirtualShell, username: string)
|
|
263
289
|
```
|
|
264
290
|
|
|
265
|
-
- **
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 {
|
|
899
|
+
import { VirtualSshServer } from "typescript-virtual-container";
|
|
805
900
|
|
|
806
|
-
const ssh = new
|
|
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 {
|
|
933
|
+
import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
|
|
839
934
|
|
|
840
|
-
const
|
|
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(
|
|
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 {
|
|
973
|
+
import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
|
|
878
974
|
|
|
879
|
-
const
|
|
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(
|
|
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(
|
|
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 {
|
|
1009
|
+
import { VirtualSshServer, VirtualShell } from "typescript-virtual-container";
|
|
913
1010
|
|
|
914
1011
|
// First run: Initialize
|
|
915
|
-
const
|
|
1012
|
+
const shell1 = new VirtualShell("typescript-vm", undefined, "./container");
|
|
1013
|
+
const ssh1 = new VirtualSshServer({
|
|
916
1014
|
port: 2222,
|
|
917
|
-
|
|
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
|
|
1028
|
+
const shell2 = new VirtualShell("typescript-vm", undefined, "./container");
|
|
1029
|
+
const ssh2 = new VirtualSshServer({
|
|
931
1030
|
port: 2223,
|
|
932
|
-
|
|
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 {
|
|
1050
|
+
import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
|
|
952
1051
|
|
|
953
1052
|
async function testDeployment() {
|
|
954
|
-
const
|
|
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(
|
|
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 {
|
|
1087
|
+
import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
|
|
988
1088
|
|
|
989
|
-
const
|
|
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(
|
|
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 {
|
|
1130
|
+
import { VirtualSshServer, SshClient, VirtualShell } from "typescript-virtual-container";
|
|
1030
1131
|
|
|
1031
|
-
const
|
|
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(
|
|
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()`.
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
1071
|
-
| `
|
|
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
|
-
| `
|
|
1077
|
-
| `
|
|
1078
|
-
| `
|
|
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
|
-
| `
|
|
1081
|
-
| `
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1135
|
-
const
|
|
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
|
|
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
|
-
- [
|
|
1311
|
-
- [
|
|
1312
|
-
- [
|
|
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
|
-
- [
|
|
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
|
+
}
|