typescript-virtual-container 1.0.8 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.vscode/settings.json +18 -0
  2. package/README.md +182 -91
  3. package/modules/shellInteractive.ts +45 -0
  4. package/modules/shellRuntime.ts +76 -0
  5. package/package.json +1 -1
  6. package/src/{SSHMimic/client.ts → SSHClient/index.ts} +17 -20
  7. package/src/SSHMimic/exec.ts +6 -17
  8. package/src/SSHMimic/executor.ts +20 -31
  9. package/src/SSHMimic/index.ts +23 -85
  10. package/src/VirtualFileSystem/index.ts +26 -1
  11. package/src/VirtualShell/index.ts +131 -26
  12. package/src/VirtualShell/shell.ts +43 -141
  13. package/src/VirtualShell/shellParser.ts +32 -7
  14. package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +155 -3
  15. package/src/{VirtualShell/commands → commands}/adduser.ts +3 -3
  16. package/src/{VirtualShell/commands → commands}/cat.ts +4 -4
  17. package/src/{VirtualShell/commands → commands}/cd.ts +3 -3
  18. package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
  19. package/src/{VirtualShell/commands → commands}/curl.ts +3 -3
  20. package/src/{VirtualShell/commands → commands}/deluser.ts +3 -3
  21. package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
  22. package/src/{VirtualShell/commands → commands}/env.ts +1 -1
  23. package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
  24. package/src/{VirtualShell/commands → commands}/export.ts +1 -1
  25. package/src/{VirtualShell/commands → commands}/grep.ts +3 -3
  26. package/src/{VirtualShell/commands → commands}/help.ts +1 -1
  27. package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
  28. package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
  29. package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
  30. package/src/{VirtualShell/commands → commands}/index.ts +19 -110
  31. package/src/{VirtualShell/commands → commands}/ls.ts +7 -5
  32. package/src/{VirtualShell/commands → commands}/mkdir.ts +3 -3
  33. package/src/{VirtualShell/commands → commands}/nano.ts +4 -4
  34. package/src/{VirtualShell/commands → commands}/neofetch.ts +4 -4
  35. package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
  36. package/src/{VirtualShell/commands → commands}/rm.ts +3 -3
  37. package/src/{VirtualShell/commands → commands}/set.ts +1 -1
  38. package/src/{VirtualShell/commands → commands}/sh.ts +3 -14
  39. package/src/{VirtualShell/commands → commands}/su.ts +3 -2
  40. package/src/{VirtualShell/commands → commands}/sudo.ts +4 -7
  41. package/src/{VirtualShell/commands → commands}/touch.ts +4 -4
  42. package/src/{VirtualShell/commands → commands}/tree.ts +3 -3
  43. package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
  44. package/src/{VirtualShell/commands → commands}/wget.ts +3 -3
  45. package/src/{VirtualShell/commands → commands}/who.ts +4 -4
  46. package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
  47. package/src/index.ts +6 -6
  48. package/src/standalone.ts +19 -14
  49. package/src/types/commands.ts +3 -11
  50. package/tests/command-helpers.test.ts +1 -1
  51. package/tests/helpers.test.ts +1 -1
  52. package/tests/parser-executor.test.ts +3 -6
  53. package/tests/users.test.ts +61 -1
  54. /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
@@ -1,4 +1,5 @@
1
1
  import { randomBytes, randomUUID, scryptSync } from "node:crypto";
2
+ import * as path from "node:path";
2
3
  import type VirtualFileSystem from "../VirtualFileSystem";
3
4
 
4
5
  /** Persisted virtual user credential record. */
@@ -26,22 +27,27 @@ export interface VirtualActiveSession {
26
27
  }
27
28
 
28
29
  /**
29
- * User, sudoers, and active session manager for SSH mimic runtime.
30
+ * Persistent user, sudoers, and active-session manager for the shell runtime.
31
+ *
32
+ * Passwords are hashed with scrypt and stored in the backing virtual filesystem.
30
33
  */
31
34
  export class VirtualUserManager {
32
35
  private readonly usersPath = "/virtual-env-js/.auth/htpasswd";
33
36
  private readonly sudoersPath = "/virtual-env-js/.auth/sudoers";
37
+ private readonly quotasPath = "/virtual-env-js/.auth/quotas";
34
38
  private readonly authDirPath = "/virtual-env-js/.auth";
35
39
  private readonly users = new Map<string, VirtualUserRecord>();
36
40
  private readonly sudoers = new Set<string>();
41
+ private readonly quotas = new Map<string, number>();
37
42
  private readonly activeSessions = new Map<string, VirtualActiveSession>();
38
43
  private nextTty = 0;
39
44
 
40
45
  /**
41
- * Creates user manager instance.
46
+ * Creates a user manager instance backed by a virtual filesystem.
42
47
  *
43
48
  * @param vfs Backing virtual filesystem used for persistence.
44
- * @param defaultRootPassword Initial root password used when root missing.
49
+ * @param defaultRootPassword Initial root password used when root is created.
50
+ * @param autoSudoForNewUsers Whether newly created users are added to sudoers.
45
51
  */
46
52
  constructor(
47
53
  private readonly vfs: VirtualFileSystem,
@@ -55,6 +61,7 @@ export class VirtualUserManager {
55
61
  public async initialize(): Promise<void> {
56
62
  this.loadFromVfs();
57
63
  this.loadSudoersFromVfs();
64
+ this.loadQuotasFromVfs();
58
65
 
59
66
  this.users.set("root", this.createRecord("root", this.defaultRootPassword));
60
67
 
@@ -63,6 +70,113 @@ export class VirtualUserManager {
63
70
  await this.persist();
64
71
  }
65
72
 
73
+ /**
74
+ * Sets max allowed bytes under /home/<username>.
75
+ *
76
+ * @param username Target username.
77
+ * @param maxBytes Quota ceiling in bytes.
78
+ */
79
+ public async setQuotaBytes(
80
+ username: string,
81
+ maxBytes: number,
82
+ ): Promise<void> {
83
+ this.validateUsername(username);
84
+ if (!this.users.has(username)) {
85
+ throw new Error(`quota: user '${username}' does not exist`);
86
+ }
87
+
88
+ if (!Number.isFinite(maxBytes) || maxBytes < 0) {
89
+ throw new Error("quota: maxBytes must be a non-negative number");
90
+ }
91
+
92
+ this.quotas.set(username, Math.floor(maxBytes));
93
+ await this.persist();
94
+ }
95
+
96
+ /**
97
+ * Removes quota for a user.
98
+ *
99
+ * @param username Target username.
100
+ */
101
+ public async clearQuota(username: string): Promise<void> {
102
+ this.validateUsername(username);
103
+ this.quotas.delete(username);
104
+ await this.persist();
105
+ }
106
+
107
+ /**
108
+ * Gets configured quota in bytes for a user.
109
+ *
110
+ * @param username Target username.
111
+ * @returns Quota in bytes, or null when unlimited.
112
+ */
113
+ public getQuotaBytes(username: string): number | null {
114
+ return this.quotas.get(username) ?? null;
115
+ }
116
+
117
+ /**
118
+ * Computes current usage under /home/<username>.
119
+ *
120
+ * @param username Target username.
121
+ * @returns Current usage in bytes.
122
+ */
123
+ public getUsageBytes(username: string): number {
124
+ const homePath = `/home/${username}`;
125
+ if (!this.vfs.exists(homePath)) {
126
+ return 0;
127
+ }
128
+
129
+ return this.vfs.getUsageBytes(homePath);
130
+ }
131
+
132
+ /**
133
+ * Validates that writing file content would not exceed user quota.
134
+ *
135
+ * Quotas are enforced only for writes inside /home/<username>.
136
+ *
137
+ * @param username Authenticated user.
138
+ * @param targetPath Target file path.
139
+ * @param nextContent New file content.
140
+ */
141
+ public assertWriteWithinQuota(
142
+ username: string,
143
+ targetPath: string,
144
+ nextContent: string | Buffer,
145
+ ): void {
146
+ const quota = this.quotas.get(username);
147
+ if (quota === undefined) {
148
+ return;
149
+ }
150
+
151
+ const normalizedPath = normalizeVfsPath(targetPath);
152
+ const homePath = normalizeVfsPath(`/home/${username}`);
153
+ const inUserHome =
154
+ normalizedPath === homePath || normalizedPath.startsWith(`${homePath}/`);
155
+ if (!inUserHome) {
156
+ return;
157
+ }
158
+
159
+ const currentUsage = this.getUsageBytes(username);
160
+ let existingSize = 0;
161
+ if (this.vfs.exists(normalizedPath)) {
162
+ const existing = this.vfs.stat(normalizedPath);
163
+ if (existing.type === "file") {
164
+ existingSize = existing.size;
165
+ }
166
+ }
167
+
168
+ const incomingSize = Buffer.isBuffer(nextContent)
169
+ ? nextContent.length
170
+ : Buffer.byteLength(nextContent, "utf8");
171
+ const projectedUsage = currentUsage - existingSize + incomingSize;
172
+
173
+ if (projectedUsage > quota) {
174
+ throw new Error(
175
+ `quota exceeded for '${username}': ${projectedUsage}/${quota} bytes`,
176
+ );
177
+ }
178
+ }
179
+
66
180
  /**
67
181
  * Verifies plaintext password against stored record.
68
182
  *
@@ -288,6 +402,30 @@ export class VirtualUserManager {
288
402
  }
289
403
  }
290
404
 
405
+ private loadQuotasFromVfs(): void {
406
+ this.quotas.clear();
407
+
408
+ if (!this.vfs.exists(this.quotasPath)) {
409
+ return;
410
+ }
411
+
412
+ const raw = this.vfs.readFile(this.quotasPath);
413
+ for (const line of raw.split("\n")) {
414
+ const trimmed = line.trim();
415
+ if (trimmed.length === 0) {
416
+ continue;
417
+ }
418
+
419
+ const [username, value] = trimmed.split(":");
420
+ const bytes = Number.parseInt(value ?? "", 10);
421
+ if (!username || !Number.isFinite(bytes) || bytes < 0) {
422
+ continue;
423
+ }
424
+
425
+ this.quotas.set(username, bytes);
426
+ }
427
+ }
428
+
291
429
  private async persist(): Promise<void> {
292
430
  if (!this.vfs.exists(this.authDirPath)) {
293
431
  this.vfs.mkdir(this.authDirPath, 0o700);
@@ -311,6 +449,15 @@ export class VirtualUserManager {
311
449
  sudoersContent.length > 0 ? `${sudoersContent}\n` : "",
312
450
  { mode: 0o600 },
313
451
  );
452
+ const quotasContent = Array.from(this.quotas.entries())
453
+ .sort(([left], [right]) => left.localeCompare(right))
454
+ .map(([username, maxBytes]) => `${username}:${maxBytes}`)
455
+ .join("\n");
456
+ this.vfs.writeFile(
457
+ this.quotasPath,
458
+ quotasContent.length > 0 ? `${quotasContent}\n` : "",
459
+ { mode: 0o600 },
460
+ );
314
461
  await this.vfs.flushMirror();
315
462
  }
316
463
 
@@ -343,3 +490,8 @@ export class VirtualUserManager {
343
490
  }
344
491
  }
345
492
  }
493
+
494
+ function normalizeVfsPath(targetPath: string): string {
495
+ const normalized = path.posix.normalize(targetPath);
496
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
497
+ }
@@ -1,9 +1,9 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const adduserCommand: ShellModule = {
4
4
  name: "adduser",
5
5
  params: ["<username> <password>"],
6
- run: async ({ authUser, users, args }) => {
6
+ run: async ({ authUser, shell, args }) => {
7
7
  if (authUser !== "root") {
8
8
  return { stderr: "adduser: permission denied", exitCode: 1 };
9
9
  }
@@ -16,7 +16,7 @@ export const adduserCommand: ShellModule = {
16
16
  };
17
17
  }
18
18
 
19
- await users.addUser(username, password);
19
+ await shell.users.addUser(username, password);
20
20
  return { stdout: `adduser: user '${username}' created`, exitCode: 0 };
21
21
  },
22
22
  };
@@ -1,18 +1,18 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg } from "./command-helpers";
3
3
  import { assertPathAccess, resolveReadablePath } from "./helpers";
4
4
 
5
5
  export const catCommand: ShellModule = {
6
6
  name: "cat",
7
7
  params: ["<file>"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  const fileArg = getArg(args, 0);
10
10
  if (!fileArg) {
11
11
  return { stderr: "cat: missing file operand", exitCode: 1 };
12
12
  }
13
13
 
14
- const target = resolveReadablePath(vfs, cwd, fileArg);
14
+ const target = resolveReadablePath(shell.vfs, cwd, fileArg);
15
15
  assertPathAccess(authUser, target, "cat");
16
- return { stdout: vfs.readFile(target), exitCode: 0 };
16
+ return { stdout: shell.vfs.readFile(target), exitCode: 0 };
17
17
  },
18
18
  };
@@ -1,13 +1,13 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { assertPathAccess, resolvePath } from "./helpers";
3
3
 
4
4
  export const cdCommand: ShellModule = {
5
5
  name: "cd",
6
6
  params: ["[path]"],
7
- run: ({ authUser, vfs, cwd, args, mode }) => {
7
+ run: ({ authUser, shell, cwd, args, mode }) => {
8
8
  const target = resolvePath(cwd, args[0] ?? "/virtual-env-js");
9
9
  assertPathAccess(authUser, target, "cd");
10
- const stats = vfs.stat(target);
10
+ const stats = shell.vfs.stat(target);
11
11
  if (stats.type !== "directory") {
12
12
  return { stderr: `cd: not a directory: ${target}`, exitCode: 1 };
13
13
  }
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const clearCommand: ShellModule = {
4
4
  name: "clear",
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { parseArgs } from "./command-helpers";
3
3
  import {
4
4
  assertPathAccess,
@@ -10,7 +10,7 @@ import {
10
10
  export const curlCommand: ShellModule = {
11
11
  name: "curl",
12
12
  params: ["[-o file] <url>"],
13
- run: async ({ authUser, vfs, cwd, args }) => {
13
+ run: async ({ authUser, cwd, args, shell }) => {
14
14
  const { flagsWithValues, positionals } = parseArgs(args, {
15
15
  flagsWithValue: ["-o", "--output"],
16
16
  });
@@ -39,7 +39,7 @@ export const curlCommand: ShellModule = {
39
39
  if (outputPath) {
40
40
  const target = resolvePath(cwd, outputPath);
41
41
  assertPathAccess(authUser, target, "curl");
42
- vfs.writeFile(target, result.stdout);
42
+ shell.writeFileAsUser(authUser, target, result.stdout);
43
43
  return {
44
44
  stderr: result.stderr
45
45
  ? normalizeTerminalOutput(result.stderr)
@@ -1,9 +1,9 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const deluserCommand: ShellModule = {
4
4
  name: "deluser",
5
5
  params: ["<username>"],
6
- run: async ({ authUser, users, args }) => {
6
+ run: async ({ authUser, args, shell }) => {
7
7
  if (authUser !== "root") {
8
8
  return { stderr: "deluser: permission denied", exitCode: 1 };
9
9
  }
@@ -13,7 +13,7 @@ export const deluserCommand: ShellModule = {
13
13
  return { stderr: "deluser: usage: deluser <username>", exitCode: 1 };
14
14
  }
15
15
 
16
- await users.deleteUser(username);
16
+ await shell.users.deleteUser(username);
17
17
  return { stdout: `deluser: user '${username}' deleted`, exitCode: 0 };
18
18
  },
19
19
  };
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { parseArgs } from "./command-helpers";
3
3
  import { getAllEnvVars } from "./set";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getAllEnvVars } from "./set";
3
3
 
4
4
  export const envCommand: ShellModule = {
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const exitCommand: ShellModule = {
4
4
  name: "exit",
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg } from "./command-helpers";
3
3
  import { getEnvVar, setEnvVar } from "./set";
4
4
 
@@ -1,11 +1,11 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { parseArgs } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const grepCommand: ShellModule = {
6
6
  name: "grep",
7
7
  params: ["[-i] [-v] <pattern> [file...]"],
8
- run: ({ authUser, vfs, cwd, args, stdin }) => {
8
+ run: ({ authUser, shell, cwd, args, stdin }) => {
9
9
  const { flags, positionals } = parseArgs(args, { flags: ["-i", "-v"] });
10
10
  const caseInsensitive = flags.has("-i");
11
11
  const invertMatch = flags.has("-v");
@@ -52,7 +52,7 @@ export const grepCommand: ShellModule = {
52
52
  const target = resolvePath(cwd, file);
53
53
  try {
54
54
  assertPathAccess(authUser, target, "grep");
55
- const content = vfs.readFile(target);
55
+ const content = shell.vfs.readFile(target);
56
56
  const lines = content.split("\n");
57
57
 
58
58
  for (const line of lines) {
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export function createHelpCommand(getNames: () => string[]): ShellModule {
4
4
  return {
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import * as path from "node:path";
3
- import type VirtualFileSystem from "../../VirtualFileSystem";
3
+ import type VirtualFileSystem from "../VirtualFileSystem";
4
4
 
5
5
  const PROTECTED_PREFIXES = ["/virtual-env-js/.auth"] as const;
6
6
 
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const hostnameCommand: ShellModule = {
4
4
  name: "hostname",
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const htopCommand: ShellModule = {
4
4
  name: "htop",
@@ -1,12 +1,10 @@
1
- import type { ShellProperties } from "..";
2
- import type { VirtualUserManager } from "../../SSHMimic/users";
1
+ import type { VirtualShell } from "../VirtualShell";
3
2
  import type {
4
3
  CommandContext,
5
4
  CommandMode,
6
5
  CommandResult,
7
6
  ShellModule,
8
- } from "../../types/commands";
9
- import type VirtualFileSystem from "../../VirtualFileSystem";
7
+ } from "../types/commands";
10
8
  import { adduserCommand } from "./adduser";
11
9
  import { catCommand } from "./cat";
12
10
  import { cdCommand } from "./cd";
@@ -79,12 +77,7 @@ const helpCommand = createHelpCommand(() =>
79
77
  const commandRegistry = new Map<string, ShellModule>();
80
78
  let cachedCommandNames: string[] | null = null;
81
79
 
82
- function invalidateCache(): void {
83
- cachedCommandNames = null;
84
- }
85
-
86
80
  function buildCache(): void {
87
- commandRegistry.clear();
88
81
  for (const mod of getCommandModules()) {
89
82
  commandRegistry.set(mod.name, mod);
90
83
  for (const alias of mod.aliases ?? []) {
@@ -95,20 +88,16 @@ function buildCache(): void {
95
88
  }
96
89
 
97
90
  function getCommandModules(): ShellModule[] {
91
+ // console.log("Loading command modules...");
92
+ // console.log(
93
+ // `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
94
+ // );
95
+ // console.log(
96
+ // `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
97
+ // );
98
98
  return [...BASE_COMMANDS, ...customCommands, helpCommand];
99
99
  }
100
100
 
101
- function _getTakenCommandNames(modules: ShellModule[]): Set<string> {
102
- const taken = new Set<string>();
103
- for (const mod of modules) {
104
- taken.add(mod.name);
105
- for (const alias of mod.aliases ?? []) {
106
- taken.add(alias);
107
- }
108
- }
109
- return taken;
110
- }
111
-
112
101
  export function registerCommand(module: ShellModule): void {
113
102
  const normalized: ShellModule = {
114
103
  ...module,
@@ -130,7 +119,7 @@ export function registerCommand(module: ShellModule): void {
130
119
  commandRegistry.set(name, normalized);
131
120
  }
132
121
 
133
- invalidateCache();
122
+ buildCache();
134
123
  }
135
124
 
136
125
  export function createCustomCommand(
@@ -153,6 +142,9 @@ export function getCommandNames(): string[] {
153
142
  }
154
143
 
155
144
  export function resolveModule(name: string): ShellModule | undefined {
145
+ if (!cachedCommandNames) {
146
+ buildCache();
147
+ }
156
148
  return commandRegistry.get(name.toLowerCase());
157
149
  }
158
150
 
@@ -207,94 +199,14 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
207
199
  }
208
200
 
209
201
  // Internal async function for pipeline execution
210
- async function _runCommandInternal(
211
- rawInput: string,
212
- authUser: string,
213
- hostname: string,
214
- users: VirtualUserManager,
215
- mode: CommandMode,
216
- cwd: string,
217
- shellProps: ShellProperties,
218
- vfs: VirtualFileSystem,
219
- stdin?: string,
220
- ): Promise<CommandResult> {
221
- // Check if input contains pipes or redirections
222
- if (
223
- rawInput.includes("|") ||
224
- rawInput.includes(">") ||
225
- rawInput.includes("<")
226
- ) {
227
- // Use pipeline executor
228
- const { parseShellPipeline } = await import("../shellParser");
229
- const { executePipeline } = await import("../../SSHMimic/executor");
230
-
231
- const pipeline = parseShellPipeline(rawInput);
232
- if (!pipeline.isValid) {
233
- return {
234
- stderr: pipeline.error || "Syntax error",
235
- exitCode: 1,
236
- };
237
- }
238
-
239
- try {
240
- return await executePipeline(
241
- pipeline,
242
- authUser,
243
- hostname,
244
- users,
245
- mode,
246
- cwd,
247
- vfs,
248
- );
249
- } catch (error: unknown) {
250
- const message =
251
- error instanceof Error ? error.message : "Pipeline execution failed";
252
- return { stderr: message, exitCode: 1 };
253
- }
254
- }
255
-
256
- // Regular command execution
257
- const { commandName, args } = parseInput(rawInput);
258
- const mod = resolveModule(commandName);
259
-
260
- if (!mod) {
261
- return {
262
- stderr: `Command '${rawInput}' not found`,
263
- exitCode: 127,
264
- };
265
- }
266
-
267
- try {
268
- const result = mod.run({
269
- authUser,
270
- hostname,
271
- users,
272
- activeSessions: users.listActiveSessions(),
273
- rawInput,
274
- mode,
275
- args,
276
- shellProps,
277
- stdin,
278
- cwd,
279
- vfs,
280
- });
281
-
282
- return await Promise.resolve(result);
283
- } catch (error: unknown) {
284
- const message = error instanceof Error ? error.message : "Command failed";
285
- return { stderr: message, exitCode: 1 };
286
- }
287
- }
288
202
 
289
203
  export async function runCommand(
290
204
  rawInput: string,
291
205
  authUser: string,
292
206
  hostname: string,
293
- users: VirtualUserManager,
294
207
  mode: CommandMode,
295
208
  cwd: string,
296
- shellProps: ShellProperties,
297
- vfs: VirtualFileSystem,
209
+ shell: VirtualShell,
298
210
  stdin?: string,
299
211
  ): Promise<CommandResult> {
300
212
  const trimmed = rawInput.trim();
@@ -304,8 +216,8 @@ export async function runCommand(
304
216
  }
305
217
 
306
218
  if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
307
- const { parseShellPipeline } = await import("../shellParser");
308
- const { executePipeline } = await import("../../SSHMimic/executor");
219
+ const { parseShellPipeline } = await import("../VirtualShell/shellParser");
220
+ const { executePipeline } = await import("../SSHMimic/executor");
309
221
 
310
222
  const pipeline = parseShellPipeline(trimmed);
311
223
  if (!pipeline.isValid) {
@@ -320,10 +232,9 @@ export async function runCommand(
320
232
  pipeline,
321
233
  authUser,
322
234
  hostname,
323
- users,
324
235
  mode,
325
236
  cwd,
326
- vfs,
237
+ shell,
327
238
  );
328
239
  } catch (error: unknown) {
329
240
  const message =
@@ -346,15 +257,13 @@ export async function runCommand(
346
257
  return await mod.run({
347
258
  authUser,
348
259
  hostname,
349
- users,
350
- activeSessions: users.listActiveSessions(),
260
+ activeSessions: shell.users.listActiveSessions(),
351
261
  rawInput: trimmed,
352
262
  mode,
353
263
  args,
354
264
  stdin,
355
265
  cwd,
356
- vfs,
357
- shellProps,
266
+ shell,
358
267
  });
359
268
  } catch (error: unknown) {
360
269
  const message = error instanceof Error ? error.message : "Command failed";
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg, ifFlag } from "./command-helpers";
3
3
  import { assertPathAccess, joinListWithType, resolvePath } from "./helpers";
4
4
 
@@ -29,22 +29,24 @@ function formatDate(date: Date): string {
29
29
  export const lsCommand: ShellModule = {
30
30
  name: "ls",
31
31
  params: ["[path]"],
32
- run: ({ authUser, vfs, cwd, args }) => {
32
+ run: ({ authUser, shell, cwd, args }) => {
33
33
  const longFormat = ifFlag(args, ["-l", "--long"]);
34
34
  const targetArg = getArg(args, 0, { flags: ["-l", "--long"] });
35
35
  const target = resolvePath(cwd, targetArg ?? cwd);
36
36
  assertPathAccess(authUser, target, "ls");
37
- const items = vfs.list(target).filter((name) => !name.startsWith("."));
37
+ const items = shell.vfs
38
+ .list(target)
39
+ .filter((name) => !name.startsWith("."));
38
40
  const rendered = longFormat
39
41
  ? items
40
42
  .map((name) => {
41
43
  const childPath = resolvePath(target, name);
42
- const stat = vfs.stat(childPath);
44
+ const stat = shell.vfs.stat(childPath);
43
45
  const size = stat.type === "file" ? stat.size : stat.childrenCount;
44
46
  return `${formatPermissions(stat.mode, stat.type === "directory")} 1 ${size} ${formatDate(stat.updatedAt)} ${name}${stat.type === "directory" ? "/" : ""}`;
45
47
  })
46
48
  .join("\n")
47
- : joinListWithType(target, items, (p) => vfs.stat(p));
49
+ : joinListWithType(target, items, (p) => shell.vfs.stat(p));
48
50
  return { stdout: rendered, exitCode: 0 };
49
51
  },
50
52
  };
@@ -1,11 +1,11 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const mkdirCommand: ShellModule = {
6
6
  name: "mkdir",
7
7
  params: ["<dir>"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  if (args.length === 0) {
10
10
  return { stderr: "mkdir: missing operand", exitCode: 1 };
11
11
  }
@@ -17,7 +17,7 @@ export const mkdirCommand: ShellModule = {
17
17
  }
18
18
  const target = resolvePath(cwd, dir);
19
19
  assertPathAccess(authUser, target, "mkdir");
20
- vfs.mkdir(target);
20
+ shell.vfs.mkdir(target);
21
21
  }
22
22
  return { exitCode: 0 };
23
23
  },