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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/create-pull-request.yml +83 -0
- package/.github/workflows/test-battery.yml +57 -0
- package/CHANGELOG.md +27 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +1283 -0
- package/SECURITY.md +33 -0
- package/biome.json +20 -0
- package/bun.lock +99 -0
- package/package.json +38 -0
- package/src/SSHMimic/client.ts +248 -0
- package/src/SSHMimic/commands/adduser.ts +22 -0
- package/src/SSHMimic/commands/cat.ts +16 -0
- package/src/SSHMimic/commands/cd.ts +20 -0
- package/src/SSHMimic/commands/clear.ts +7 -0
- package/src/SSHMimic/commands/curl.ts +27 -0
- package/src/SSHMimic/commands/deluser.ts +19 -0
- package/src/SSHMimic/commands/exit.ts +7 -0
- package/src/SSHMimic/commands/help.ts +9 -0
- package/src/SSHMimic/commands/helpers.ts +137 -0
- package/src/SSHMimic/commands/hostname.ts +7 -0
- package/src/SSHMimic/commands/htop.ts +13 -0
- package/src/SSHMimic/commands/index.ts +120 -0
- package/src/SSHMimic/commands/ls.ts +14 -0
- package/src/SSHMimic/commands/mkdir.ts +17 -0
- package/src/SSHMimic/commands/nano.ts +30 -0
- package/src/SSHMimic/commands/pwd.ts +7 -0
- package/src/SSHMimic/commands/rm.ts +26 -0
- package/src/SSHMimic/commands/su.ts +31 -0
- package/src/SSHMimic/commands/sudo.ts +90 -0
- package/src/SSHMimic/commands/touch.ts +20 -0
- package/src/SSHMimic/commands/tree.ts +11 -0
- package/src/SSHMimic/commands/wget.ts +33 -0
- package/src/SSHMimic/commands/who.ts +18 -0
- package/src/SSHMimic/commands/whoami.ts +7 -0
- package/src/SSHMimic/exec.ts +37 -0
- package/src/SSHMimic/hostKey.ts +21 -0
- package/src/SSHMimic/index.ts +203 -0
- package/src/SSHMimic/loginFormat.ts +10 -0
- package/src/SSHMimic/prompt.ts +14 -0
- package/src/SSHMimic/shell.ts +740 -0
- package/src/SSHMimic/users.ts +336 -0
- package/src/VirtualFileSystem.ts +420 -0
- package/src/index.ts +34 -0
- package/src/standalone.ts +14 -0
- package/src/types/commands.ts +98 -0
- package/src/types/streams.ts +32 -0
- package/src/types/tar-stream.d.ts +38 -0
- package/src/types/vfs.ts +81 -0
- package/src/vfs/archive.ts +74 -0
- package/src/vfs/internalTypes.ts +19 -0
- package/src/vfs/path.ts +74 -0
- package/src/vfs/snapshot.ts +84 -0
- package/src/vfs/tree.ts +34 -0
- 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
|
+
[](https://www.npmjs.com/package/typescript-virtual-container)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](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.**
|