typescript-virtual-container 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +50 -0
- package/modules/neofetch.ts +349 -0
- package/package.json +1 -1
- package/src/SSHMimic/client.ts +1 -1
- package/src/SSHMimic/exec.ts +3 -1
- package/src/SSHMimic/executor.ts +2 -2
- package/src/SSHMimic/index.ts +14 -18
- package/src/{VirtualFileSystem.ts → VirtualFileSystem/index.ts} +6 -15
- package/src/{SSHMimic → VirtualShell}/commands/cat.ts +2 -1
- package/src/VirtualShell/commands/command-helpers.ts +135 -0
- package/src/{SSHMimic → VirtualShell}/commands/curl.ts +16 -37
- package/src/{SSHMimic → VirtualShell}/commands/echo.ts +10 -2
- package/src/{SSHMimic → VirtualShell}/commands/export.ts +7 -1
- package/src/{SSHMimic → VirtualShell}/commands/grep.ts +15 -8
- package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
- package/src/{SSHMimic → VirtualShell}/commands/index.ts +71 -8
- package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
- package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
- package/src/VirtualShell/commands/neofetch.ts +37 -0
- package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
- package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
- package/src/VirtualShell/commands/sh.ts +68 -0
- package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
- package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +18 -26
- package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
- package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
- package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
- package/src/VirtualShell/index.ts +86 -0
- package/src/{SSHMimic → VirtualShell}/shell.ts +21 -14
- package/src/index.ts +8 -0
- package/src/standalone.ts +10 -1
- package/src/types/commands.ts +3 -0
- package/tests/command-helpers.test.ts +40 -0
- package/tests/helpers.test.ts +1 -1
- package/src/SSHMimic/commands/sh.ts +0 -121
- /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/env.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/unset.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/shellParser.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on Keep a Changelog.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.5] - 2026-04-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Refactored commands to use shared argument/flag parsing helpers.
|
|
14
|
+
- Improved maintainability and consistency of argument parsing across commands.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Verified all refactored commands pass existing test cases without regressions.
|
|
19
|
+
|
|
9
20
|
## [1.0.4] - 2026-04-15
|
|
10
21
|
|
|
11
22
|
### Added
|
package/README.md
CHANGED
|
@@ -417,6 +417,56 @@ console.log(client.getUsername()); // Username from constructor
|
|
|
417
417
|
|
|
418
418
|
---
|
|
419
419
|
|
|
420
|
+
### VirtualShell
|
|
421
|
+
|
|
422
|
+
Encapsulates shell execution primitives used by the SSH runtime for command dispatch and interactive sessions.
|
|
423
|
+
|
|
424
|
+
#### Constructor
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
new VirtualShell(
|
|
428
|
+
vfs: VirtualFileSystem,
|
|
429
|
+
users: VirtualUserManager,
|
|
430
|
+
hostname: string,
|
|
431
|
+
)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
- **vfs**: Virtual filesystem instance used by shell commands.
|
|
435
|
+
- **users**: User manager for authentication/session-aware command behavior.
|
|
436
|
+
- **hostname**: Hostname injected into command context and prompt behavior.
|
|
437
|
+
|
|
438
|
+
**Example:**
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
const shell = new VirtualShell(vfs, users, "typescript-vm");
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Methods
|
|
445
|
+
|
|
446
|
+
##### `executeCommand(rawInput: string, authUser: string, cwd: string): void`
|
|
447
|
+
|
|
448
|
+
Runs one command input in shell mode for a given user and working directory.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
shell.executeCommand("ls -la", "root", "/home/root");
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
##### `startInteractiveSession(stream: ShellStream, authUser: string, sessionId: string | null, remoteAddress: string, terminalSize: { cols: number; rows: number }): void`
|
|
455
|
+
|
|
456
|
+
Starts an interactive shell session over a shell stream.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
shell.startInteractiveSession(
|
|
460
|
+
stream,
|
|
461
|
+
"root",
|
|
462
|
+
sessionId,
|
|
463
|
+
"127.0.0.1",
|
|
464
|
+
{ cols: 120, rows: 30 },
|
|
465
|
+
);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
420
470
|
### VirtualFileSystem
|
|
421
471
|
|
|
422
472
|
In-memory filesystem with optional gzip compression and tar.gz persistence.
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ShellProperties } from "../src/VirtualShell";
|
|
5
|
+
|
|
6
|
+
function formatUptime(seconds: number): string {
|
|
7
|
+
const totalMinutes = Math.max(1, Math.floor(seconds / 60));
|
|
8
|
+
const days = Math.floor(totalMinutes / (24 * 60));
|
|
9
|
+
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
|
|
10
|
+
const minutes = totalMinutes % 60;
|
|
11
|
+
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
if (days > 0) {
|
|
14
|
+
parts.push(`${days} day${days > 1 ? "s" : ""}`);
|
|
15
|
+
}
|
|
16
|
+
if (hours > 0) {
|
|
17
|
+
parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
|
|
18
|
+
}
|
|
19
|
+
if (minutes > 0 || parts.length === 0) {
|
|
20
|
+
parts.push(`${minutes} min${minutes > 1 ? "s" : ""}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return parts.join(", ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function colorBlock(code: number): string {
|
|
27
|
+
return `\u001b[${code}m \u001b[0m`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildColorBars(): string[] {
|
|
31
|
+
const normal = [40, 41, 42, 43, 44, 45, 46, 47].map(colorBlock).join("");
|
|
32
|
+
const bright = [100, 101, 102, 103, 104, 105, 106, 107]
|
|
33
|
+
.map(colorBlock)
|
|
34
|
+
.join("");
|
|
35
|
+
return [normal, bright];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function colorizeLogoLine(line: string, index: number, total: number): string {
|
|
39
|
+
if (line.trim().length === 0) {
|
|
40
|
+
return line;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const start = { r: 255, g: 255, b: 255 };
|
|
44
|
+
const end = { r: 168, g: 85, b: 247 };
|
|
45
|
+
const ratio = total <= 1 ? 0 : index / (total - 1);
|
|
46
|
+
|
|
47
|
+
const r = Math.round(start.r + (end.r - start.r) * ratio);
|
|
48
|
+
const g = Math.round(start.g + (end.g - start.g) * ratio);
|
|
49
|
+
const b = Math.round(start.b + (end.b - start.b) * ratio);
|
|
50
|
+
|
|
51
|
+
return `\u001b[38;2;${r};${g};${b}m${line}\u001b[0m`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function colorizeDetailLine(line: string): string {
|
|
55
|
+
if (line.trim().length === 0) {
|
|
56
|
+
return line;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const colonIndex = line.indexOf(':');
|
|
60
|
+
|
|
61
|
+
if (colonIndex === -1) {
|
|
62
|
+
// Pas de ':', chercher '@' pour identifier user@host
|
|
63
|
+
if (line.includes('@')) {
|
|
64
|
+
// C'est user@host, appliquer dégradé horizontal
|
|
65
|
+
return applyHorizontalGradient(line);
|
|
66
|
+
}
|
|
67
|
+
// Sinon c'est un separator ou autre, laisser tel quel
|
|
68
|
+
return line;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Il y a un ':', c'est titre: valeur
|
|
72
|
+
const title = line.substring(0, colonIndex + 1);
|
|
73
|
+
const value = line.substring(colonIndex + 1);
|
|
74
|
+
|
|
75
|
+
// Appliquer le dégradé seulement au titre
|
|
76
|
+
const colorized = applyHorizontalGradient(title);
|
|
77
|
+
return colorized + value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyHorizontalGradient(text: string): string {
|
|
81
|
+
// Nettoyer les codes ANSI existants
|
|
82
|
+
const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*m`, 'g');
|
|
83
|
+
const cleaned = text.replace(ansiRegex, '');
|
|
84
|
+
|
|
85
|
+
if (cleaned.trim().length === 0) {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const start = { r: 255, g: 255, b: 255 };
|
|
90
|
+
const end = { r: 168, g: 85, b: 247 };
|
|
91
|
+
let result = '';
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < cleaned.length; i += 1) {
|
|
94
|
+
const ratio = cleaned.length <= 1 ? 0 : i / (cleaned.length - 1);
|
|
95
|
+
|
|
96
|
+
const r = Math.round(start.r + (end.r - start.r) * ratio);
|
|
97
|
+
const g = Math.round(start.g + (end.g - start.g) * ratio);
|
|
98
|
+
const b = Math.round(start.b + (end.b - start.b) * ratio);
|
|
99
|
+
|
|
100
|
+
result += `\u001b[38;2;${r};${g};${b}m${cleaned[i]}\u001b[0m`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface NeofetchInfo {
|
|
107
|
+
user: string;
|
|
108
|
+
host: string;
|
|
109
|
+
osName?: string;
|
|
110
|
+
kernel?: string;
|
|
111
|
+
uptimeSeconds?: number;
|
|
112
|
+
packages?: string;
|
|
113
|
+
shell?: string;
|
|
114
|
+
shellProps?: ShellProperties;
|
|
115
|
+
resolution?: string;
|
|
116
|
+
terminal?: string;
|
|
117
|
+
cpu?: string;
|
|
118
|
+
gpu?: string;
|
|
119
|
+
memoryUsedMiB?: number;
|
|
120
|
+
memoryTotalMiB?: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toMiB(bytes: number): number {
|
|
124
|
+
return Math.max(0, Math.round(bytes / (1024 * 1024)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readOsPrettyName(): string | undefined {
|
|
128
|
+
try {
|
|
129
|
+
const data = readFileSync("/etc/os-release", "utf8");
|
|
130
|
+
for (const line of data.split("\n")) {
|
|
131
|
+
if (!line.startsWith("PRETTY_NAME=")) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const value = line.slice("PRETTY_NAME=".length).trim();
|
|
136
|
+
return value.replace(/^"|"$/g, "");
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readFirstLine(filePath: string): string | undefined {
|
|
146
|
+
try {
|
|
147
|
+
const data = readFileSync(filePath, "utf8").split("\n")[0]?.trim();
|
|
148
|
+
if (!data || data.length === 0) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return data;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveHostLabel(fallback: string): string {
|
|
158
|
+
const vendor = readFirstLine("/sys/devices/virtual/dmi/id/sys_vendor");
|
|
159
|
+
const product = readFirstLine("/sys/devices/virtual/dmi/id/product_name");
|
|
160
|
+
|
|
161
|
+
if (vendor && product) {
|
|
162
|
+
return `${vendor} ${product}`;
|
|
163
|
+
}
|
|
164
|
+
if (product) {
|
|
165
|
+
return product;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return fallback;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function countDpkgPackages(): number | undefined {
|
|
172
|
+
const candidates = ["/var/lib/dpkg/status", "/usr/local/var/lib/dpkg/status"];
|
|
173
|
+
|
|
174
|
+
for (const filePath of candidates) {
|
|
175
|
+
if (!existsSync(filePath)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const data = readFileSync(filePath, "utf8");
|
|
181
|
+
const matches = data.match(/^Package:\s+/gm);
|
|
182
|
+
return matches?.length ?? 0;
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function countSnapPackages(): number | undefined {
|
|
191
|
+
const candidates = ["/snap", "/var/lib/snapd/snaps"];
|
|
192
|
+
|
|
193
|
+
for (const dirPath of candidates) {
|
|
194
|
+
if (!existsSync(dirPath)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
200
|
+
const count = entries.filter((entry) => entry.isDirectory()).length;
|
|
201
|
+
return count;
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolvePackagesLabel(): string {
|
|
210
|
+
const dpkgCount = countDpkgPackages();
|
|
211
|
+
const snapCount = countSnapPackages();
|
|
212
|
+
|
|
213
|
+
if (dpkgCount !== undefined && snapCount !== undefined) {
|
|
214
|
+
return `${dpkgCount} (dpkg), ${snapCount} (snap)`;
|
|
215
|
+
}
|
|
216
|
+
if (dpkgCount !== undefined) {
|
|
217
|
+
return `${dpkgCount} (dpkg)`;
|
|
218
|
+
}
|
|
219
|
+
if (snapCount !== undefined) {
|
|
220
|
+
return `${snapCount} (snap)`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return "n/a";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveCpuLabel(): string {
|
|
227
|
+
const cpus = os.cpus();
|
|
228
|
+
if (cpus.length === 0) {
|
|
229
|
+
return "unknown";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const first = cpus[0];
|
|
233
|
+
if (!first) {
|
|
234
|
+
return "unknown";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ghz = (first.speed / 1000).toFixed(2);
|
|
238
|
+
return `${first.model} (${cpus.length}) @ ${ghz}GHz`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveShellLabel(shell?: string): string {
|
|
242
|
+
if (!shell || shell.trim().length === 0) {
|
|
243
|
+
return "unknown";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return path.posix.basename(shell.trim());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function resolveDefaults(info: NeofetchInfo): Required<NeofetchInfo> {
|
|
250
|
+
const totalMem = os.totalmem();
|
|
251
|
+
const freeMem = os.freemem();
|
|
252
|
+
const usedMem = Math.max(0, totalMem - freeMem);
|
|
253
|
+
const shellProps = info.shellProps;
|
|
254
|
+
|
|
255
|
+
console.log("Resolving neofetch info with shellProps:", shellProps);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
user: info.user,
|
|
259
|
+
host: info.host,
|
|
260
|
+
osName: shellProps?.os ?? info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
|
|
261
|
+
kernel: shellProps?.kernel ?? info.kernel ?? os.release(),
|
|
262
|
+
uptimeSeconds: info.uptimeSeconds ?? os.uptime(),
|
|
263
|
+
packages: info.packages ?? resolvePackagesLabel(),
|
|
264
|
+
shell: resolveShellLabel(info.shell),
|
|
265
|
+
shellProps: info.shellProps as ShellProperties ?? {
|
|
266
|
+
kernel: info.kernel ?? os.release(),
|
|
267
|
+
os: info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
|
|
268
|
+
arch: os.arch(),
|
|
269
|
+
},
|
|
270
|
+
resolution: info.resolution ?? "n/a (ssh)",
|
|
271
|
+
terminal: info.terminal ?? "unknown",
|
|
272
|
+
cpu: info.cpu ?? resolveCpuLabel(),
|
|
273
|
+
gpu: info.gpu ?? "n/a",
|
|
274
|
+
memoryUsedMiB: info.memoryUsedMiB ?? toMiB(usedMem),
|
|
275
|
+
memoryTotalMiB: info.memoryTotalMiB ?? toMiB(totalMem),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildNeofetchOutput(info: NeofetchInfo): string {
|
|
280
|
+
const fields = resolveDefaults(info);
|
|
281
|
+
const uptime = formatUptime(fields.uptimeSeconds);
|
|
282
|
+
const colorBars = buildColorBars();
|
|
283
|
+
|
|
284
|
+
const distroLogo = [
|
|
285
|
+
" .. .:. ",
|
|
286
|
+
" .::.. .. .. ",
|
|
287
|
+
". .... ... .. ",
|
|
288
|
+
": .... .:. .. ",
|
|
289
|
+
": .:.:........:. .. ",
|
|
290
|
+
": .. ",
|
|
291
|
+
". : ",
|
|
292
|
+
". : ",
|
|
293
|
+
".. : ",
|
|
294
|
+
" :. .. ",
|
|
295
|
+
" .. .. ",
|
|
296
|
+
" :-. :: ",
|
|
297
|
+
" .:. :. ",
|
|
298
|
+
" ..: ... ",
|
|
299
|
+
" ..: :.. ",
|
|
300
|
+
" :... :....",
|
|
301
|
+
" .. ....",
|
|
302
|
+
" . .. ",
|
|
303
|
+
" .:. .: ",
|
|
304
|
+
" :. .. ",
|
|
305
|
+
" ::. .. ",
|
|
306
|
+
"..... ..:... ",
|
|
307
|
+
"...:. .. ",
|
|
308
|
+
".:...:. ::. .. ",
|
|
309
|
+
" ... ..:::::.. ..:::::::.. ",
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
const details = [
|
|
313
|
+
`${fields.user}@${fields.host}`,
|
|
314
|
+
"-------------------------",
|
|
315
|
+
`OS: ${fields.osName}`,
|
|
316
|
+
`Host: ${resolveHostLabel(fields.host)}`,
|
|
317
|
+
`Kernel: ${fields.kernel}`,
|
|
318
|
+
`Uptime: ${uptime}`,
|
|
319
|
+
`Packages: ${fields.packages}`,
|
|
320
|
+
`Shell: ${fields.shell}`,
|
|
321
|
+
// `Shell Props: ${fields.shellProps}`,
|
|
322
|
+
`Resolution: ${fields.resolution}`,
|
|
323
|
+
`Terminal: ${fields.terminal}`,
|
|
324
|
+
`CPU: ${fields.cpu}`,
|
|
325
|
+
`GPU: ${fields.gpu}`,
|
|
326
|
+
`Memory: ${fields.memoryUsedMiB}MiB / ${fields.memoryTotalMiB}MiB`,
|
|
327
|
+
"",
|
|
328
|
+
colorBars[0],
|
|
329
|
+
colorBars[1],
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const width = Math.max(distroLogo.length, details.length);
|
|
333
|
+
const lines: string[] = [];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < width; i += 1) {
|
|
336
|
+
const rawLeft = distroLogo[i] ?? "";
|
|
337
|
+
const right = details[i] ?? "";
|
|
338
|
+
if (right.length > 0) {
|
|
339
|
+
const left = colorizeLogoLine(rawLeft.padEnd(31, " "), i, distroLogo.length);
|
|
340
|
+
const coloredRight = colorizeDetailLine(right);
|
|
341
|
+
lines.push(`${left} ${coloredRight}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lines.push(colorizeLogoLine(rawLeft, i, distroLogo.length));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return lines.join("\n");
|
|
349
|
+
}
|
package/package.json
CHANGED
package/src/SSHMimic/client.ts
CHANGED
package/src/SSHMimic/exec.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExecStream } from "../types/streams";
|
|
2
2
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
3
|
-
import {
|
|
3
|
+
import { defaultShellProperties } from "../VirtualShell";
|
|
4
|
+
import { runCommand } from "../VirtualShell/commands";
|
|
4
5
|
import type { VirtualUserManager } from "./users";
|
|
5
6
|
|
|
6
7
|
function toTtyLines(text: string): string {
|
|
@@ -26,6 +27,7 @@ export function runExec(
|
|
|
26
27
|
users,
|
|
27
28
|
"exec",
|
|
28
29
|
`/home/${authUser}`,
|
|
30
|
+
defaultShellProperties,
|
|
29
31
|
vfs,
|
|
30
32
|
),
|
|
31
33
|
).then((result) => {
|
package/src/SSHMimic/executor.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CommandMode, CommandResult } from "../types/commands";
|
|
2
2
|
import type { Pipeline, PipelineCommand } from "../types/pipeline";
|
|
3
3
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
4
|
-
import { runCommand as runSingleCommand } from "
|
|
5
|
-
import { resolvePath } from "
|
|
4
|
+
import { runCommand as runSingleCommand } from "../VirtualShell/commands";
|
|
5
|
+
import { resolvePath } from "../VirtualShell/commands/helpers";
|
|
6
6
|
import type { VirtualUserManager } from "./users";
|
|
7
7
|
|
|
8
8
|
/**
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { Server as SshServer } from "ssh2";
|
|
3
3
|
import VirtualFileSystem from "../VirtualFileSystem";
|
|
4
|
-
import {
|
|
4
|
+
import { VirtualShell } from "../VirtualShell";
|
|
5
5
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
6
|
-
import { startShell } from "./shell";
|
|
7
6
|
import { VirtualUserManager } from "./users";
|
|
8
7
|
|
|
9
8
|
function resolveRootPassword(): string {
|
|
@@ -35,12 +34,13 @@ function resolveAutoSudoForNewUsers(): boolean {
|
|
|
35
34
|
* {@link SshMimic.stop} when your process exits.
|
|
36
35
|
*/
|
|
37
36
|
class SshMimic {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
port: number;
|
|
38
|
+
hostname: string;
|
|
39
|
+
server: SshServer | null;
|
|
40
|
+
vfs: VirtualFileSystem | null = null;
|
|
41
|
+
users: VirtualUserManager | null = null;
|
|
42
|
+
shell: VirtualShell | null = null;
|
|
43
|
+
basePath: string = ".";
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Creates a new SSH mimic server instance.
|
|
@@ -80,6 +80,8 @@ class SshMimic {
|
|
|
80
80
|
);
|
|
81
81
|
await this.users.initialize();
|
|
82
82
|
|
|
83
|
+
this.shell = new VirtualShell(this.vfs, this.users, this.hostname);
|
|
84
|
+
|
|
83
85
|
this.server = new SshServer(
|
|
84
86
|
{
|
|
85
87
|
hostKeys: [privateKey],
|
|
@@ -148,12 +150,9 @@ class SshMimic {
|
|
|
148
150
|
|
|
149
151
|
session.on("shell", (acceptShell) => {
|
|
150
152
|
const stream = acceptShell();
|
|
151
|
-
|
|
153
|
+
this.shell?.startInteractiveSession(
|
|
152
154
|
stream,
|
|
153
155
|
authUser,
|
|
154
|
-
this.vfs!,
|
|
155
|
-
this.hostname,
|
|
156
|
-
this.users!,
|
|
157
156
|
sessionId,
|
|
158
157
|
remoteAddress,
|
|
159
158
|
terminalSize,
|
|
@@ -161,14 +160,11 @@ class SshMimic {
|
|
|
161
160
|
});
|
|
162
161
|
|
|
163
162
|
session.on("exec", (acceptExec, _rejectExec, info) => {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
stream,
|
|
163
|
+
const _stream = acceptExec();
|
|
164
|
+
this.shell?.executeCommand(
|
|
167
165
|
info.command.trim(),
|
|
168
166
|
authUser,
|
|
169
|
-
|
|
170
|
-
this.users!,
|
|
171
|
-
this.vfs!,
|
|
167
|
+
`/home/${authUser}`,
|
|
172
168
|
);
|
|
173
169
|
});
|
|
174
170
|
});
|
|
@@ -5,21 +5,12 @@ import type {
|
|
|
5
5
|
RemoveOptions,
|
|
6
6
|
VfsNodeStats,
|
|
7
7
|
WriteFileOptions,
|
|
8
|
-
} from "
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./
|
|
14
|
-
import type { InternalDirectoryNode, InternalNode } from "./vfs/internalTypes";
|
|
15
|
-
import {
|
|
16
|
-
getNode,
|
|
17
|
-
getParentDirectory,
|
|
18
|
-
normalizePath,
|
|
19
|
-
splitPath,
|
|
20
|
-
} from "./vfs/path";
|
|
21
|
-
import { applySnapshot, createSnapshot } from "./vfs/snapshot";
|
|
22
|
-
import { renderTree } from "./vfs/tree";
|
|
8
|
+
} from "../types/vfs";
|
|
9
|
+
import { archiveExists, createTarBuffer, readSnapshotFromTar } from "./archive";
|
|
10
|
+
import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
|
|
11
|
+
import { getNode, getParentDirectory, normalizePath, splitPath } from "./path";
|
|
12
|
+
import { applySnapshot, createSnapshot } from "./snapshot";
|
|
13
|
+
import { renderTree } from "./tree";
|
|
23
14
|
|
|
24
15
|
/**
|
|
25
16
|
* In-memory virtual filesystem with tar.gz mirror persistence.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, resolveReadablePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
export const catCommand: ShellModule = {
|
|
5
6
|
name: "cat",
|
|
6
7
|
params: ["<file>"],
|
|
7
8
|
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
|
-
const fileArg = args
|
|
9
|
+
const fileArg = getArg(args, 0);
|
|
9
10
|
if (!fileArg) {
|
|
10
11
|
return { stderr: "cat: missing file operand", exitCode: 1 };
|
|
11
12
|
}
|