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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +50 -0
  3. package/modules/neofetch.ts +349 -0
  4. package/package.json +1 -1
  5. package/src/SSHMimic/client.ts +1 -1
  6. package/src/SSHMimic/exec.ts +3 -1
  7. package/src/SSHMimic/executor.ts +2 -2
  8. package/src/SSHMimic/index.ts +14 -18
  9. package/src/{VirtualFileSystem.ts → VirtualFileSystem/index.ts} +6 -15
  10. package/src/{SSHMimic → VirtualShell}/commands/cat.ts +2 -1
  11. package/src/VirtualShell/commands/command-helpers.ts +135 -0
  12. package/src/{SSHMimic → VirtualShell}/commands/curl.ts +16 -37
  13. package/src/{SSHMimic → VirtualShell}/commands/echo.ts +10 -2
  14. package/src/{SSHMimic → VirtualShell}/commands/export.ts +7 -1
  15. package/src/{SSHMimic → VirtualShell}/commands/grep.ts +15 -8
  16. package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
  17. package/src/{SSHMimic → VirtualShell}/commands/index.ts +71 -8
  18. package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
  19. package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
  20. package/src/VirtualShell/commands/neofetch.ts +37 -0
  21. package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
  22. package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
  23. package/src/VirtualShell/commands/sh.ts +68 -0
  24. package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
  25. package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +18 -26
  26. package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
  27. package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
  28. package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
  29. package/src/VirtualShell/index.ts +86 -0
  30. package/src/{SSHMimic → VirtualShell}/shell.ts +21 -14
  31. package/src/index.ts +8 -0
  32. package/src/standalone.ts +10 -1
  33. package/src/types/commands.ts +3 -0
  34. package/tests/command-helpers.test.ts +40 -0
  35. package/tests/helpers.test.ts +1 -1
  36. package/src/SSHMimic/commands/sh.ts +0 -121
  37. /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
  38. /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
  39. /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
  40. /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
  41. /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
  42. /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
  43. /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
  44. /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
  45. /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
  46. /package/src/{SSHMimic → VirtualShell}/commands/env.ts +0 -0
  47. /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
  48. /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
  49. /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
  50. /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
  51. /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
  52. /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
  53. /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
  54. /package/src/{SSHMimic → VirtualShell}/commands/unset.ts +0 -0
  55. /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
  56. /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
@@ -3,7 +3,7 @@
3
3
  "description": "In-memory SSH server with virtual filesystem and typed programmatic API",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
- "version": "1.0.4",
6
+ "version": "1.0.6",
7
7
  "license": "MIT",
8
8
  "keywords": [
9
9
  "ssh",
@@ -1,5 +1,5 @@
1
1
  import type { CommandResult } from "../types/commands";
2
- import { runCommand } from "./commands";
2
+ import { runCommand } from "../VirtualShell/commands";
3
3
  import type { SshMimic } from "./index";
4
4
 
5
5
  /**
@@ -1,6 +1,7 @@
1
1
  import type { ExecStream } from "../types/streams";
2
2
  import type VirtualFileSystem from "../VirtualFileSystem";
3
- import { runCommand } from "./commands";
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) => {
@@ -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 "./commands";
5
- import { resolvePath } from "./commands/helpers";
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
  /**
@@ -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 { runExec } from "./exec";
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
- private port: number;
39
- private hostname: string;
40
- private server: SshServer | null;
41
- private vfs: VirtualFileSystem | null = null;
42
- private users: VirtualUserManager | null = null;
43
- private basePath: string = ".";
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
- startShell(
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 stream = acceptExec();
165
- runExec(
166
- stream,
163
+ const _stream = acceptExec();
164
+ this.shell?.executeCommand(
167
165
  info.command.trim(),
168
166
  authUser,
169
- this.hostname,
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 "./types/vfs";
9
- import {
10
- archiveExists,
11
- createTarBuffer,
12
- readSnapshotFromTar,
13
- } from "./vfs/archive";
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[0];
9
+ const fileArg = getArg(args, 0);
9
10
  if (!fileArg) {
10
11
  return { stderr: "cat: missing file operand", exitCode: 1 };
11
12
  }