typescript-virtual-container 1.0.7 → 1.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +138 -87
  3. package/package.json +1 -1
  4. package/src/SSHMimic/client.ts +15 -18
  5. package/src/SSHMimic/exec.ts +5 -16
  6. package/src/SSHMimic/executor.ts +18 -29
  7. package/src/SSHMimic/index.ts +23 -85
  8. package/src/VirtualFileSystem/index.ts +3 -1
  9. package/src/VirtualShell/commands/adduser.ts +2 -2
  10. package/src/VirtualShell/commands/cat.ts +3 -3
  11. package/src/VirtualShell/commands/cd.ts +2 -2
  12. package/src/VirtualShell/commands/command-helpers.ts +64 -0
  13. package/src/VirtualShell/commands/curl.ts +14 -92
  14. package/src/VirtualShell/commands/deluser.ts +2 -2
  15. package/src/VirtualShell/commands/echo.ts +5 -12
  16. package/src/VirtualShell/commands/grep.ts +8 -16
  17. package/src/VirtualShell/commands/helpers.ts +74 -0
  18. package/src/VirtualShell/commands/index.ts +46 -112
  19. package/src/VirtualShell/commands/ls.ts +6 -4
  20. package/src/VirtualShell/commands/mkdir.ts +2 -2
  21. package/src/VirtualShell/commands/nano.ts +3 -3
  22. package/src/VirtualShell/commands/neofetch.ts +2 -2
  23. package/src/VirtualShell/commands/rm.ts +2 -2
  24. package/src/VirtualShell/commands/sh.ts +2 -13
  25. package/src/VirtualShell/commands/su.ts +2 -1
  26. package/src/VirtualShell/commands/sudo.ts +12 -25
  27. package/src/VirtualShell/commands/touch.ts +3 -3
  28. package/src/VirtualShell/commands/tree.ts +2 -2
  29. package/src/VirtualShell/commands/wget.ts +19 -29
  30. package/src/VirtualShell/commands/who.ts +2 -2
  31. package/src/VirtualShell/index.ts +114 -25
  32. package/src/VirtualShell/shell.ts +28 -35
  33. package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +6 -3
  34. package/src/index.ts +4 -4
  35. package/src/standalone.ts +19 -14
  36. package/src/types/commands.ts +3 -11
  37. package/tests/parser-executor.test.ts +37 -0
  38. package/tests/users.test.ts +1 -1
@@ -1,23 +1,16 @@
1
1
  import type { ShellModule } from "../../types/commands";
2
- import { getArg, ifFlag } from "./command-helpers";
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 }) => {
9
- const caseInsensitive = ifFlag(args, "-i");
10
- const invertMatch = ifFlag(args, "-v");
11
- const parserOptions = { flags: ["-i", "-v"] };
12
- const pattern = getArg(args, 0, parserOptions);
13
- const files: string[] = [];
14
- for (let index = 1; ; index += 1) {
15
- const file = getArg(args, index, parserOptions);
16
- if (!file) {
17
- break;
18
- }
19
- files.push(file);
20
- }
8
+ run: ({ authUser, shell, cwd, args, stdin }) => {
9
+ const { flags, positionals } = parseArgs(args, { flags: ["-i", "-v"] });
10
+ const caseInsensitive = flags.has("-i");
11
+ const invertMatch = flags.has("-v");
12
+ const pattern = positionals[0];
13
+ const files = positionals.slice(1);
21
14
 
22
15
  if (!pattern) {
23
16
  return { stderr: "grep: no pattern specified", exitCode: 1 };
@@ -33,7 +26,6 @@ export const grepCommand: ShellModule = {
33
26
 
34
27
  const results: string[] = [];
35
28
 
36
- // If no files specified, read from stdin (pipe/input redirection).
37
29
  if (files.length === 0) {
38
30
  if (!stdin) {
39
31
  return { stdout: "", exitCode: 1 };
@@ -60,7 +52,7 @@ export const grepCommand: ShellModule = {
60
52
  const target = resolvePath(cwd, file);
61
53
  try {
62
54
  assertPathAccess(authUser, target, "grep");
63
- const content = vfs.readFile(target);
55
+ const content = shell.vfs.readFile(target);
64
56
  const lines = content.split("\n");
65
57
 
66
58
  for (const line of lines) {
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import * as path from "node:path";
2
3
  import type VirtualFileSystem from "../../VirtualFileSystem";
3
4
 
@@ -76,6 +77,79 @@ export async function fetchResource(
76
77
  };
77
78
  }
78
79
 
80
+ /**
81
+ * Run a host command like curl or wget and capture its output.
82
+ * @param binary - The binary to execute (e.g., "curl", "wget").
83
+ * @param args - Arguments to pass to the binary.
84
+ * @returns Promise resolving with stdout, stderr, and exit code.
85
+ */
86
+ export function runHostCommand(
87
+ binary: string,
88
+ args: string[],
89
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
90
+ return new Promise((resolve) => {
91
+ let childProcess: ReturnType<typeof spawn>;
92
+
93
+ try {
94
+ childProcess = spawn(binary, args, {
95
+ stdio: ["ignore", "pipe", "pipe"],
96
+ });
97
+ } catch (error) {
98
+ resolve({
99
+ stdout: "",
100
+ stderr: `${binary}: ${error instanceof Error ? error.message : String(error)}`,
101
+ exitCode: 1,
102
+ });
103
+ return;
104
+ }
105
+
106
+ let stdout = "";
107
+ let stderr = "";
108
+ const stdoutStream = childProcess.stdout;
109
+ const stderrStream = childProcess.stderr;
110
+
111
+ if (!stdoutStream || !stderrStream) {
112
+ resolve({
113
+ stdout: "",
114
+ stderr: `${binary}: failed to capture process output`,
115
+ exitCode: 1,
116
+ });
117
+ return;
118
+ }
119
+
120
+ stdoutStream.setEncoding("utf8");
121
+ stderrStream.setEncoding("utf8");
122
+
123
+ stdoutStream.on("data", (chunk: string) => {
124
+ stdout += chunk;
125
+ });
126
+
127
+ stderrStream.on("data", (chunk: string) => {
128
+ stderr += chunk;
129
+ });
130
+
131
+ childProcess.on("error", (error) => {
132
+ const errorCode =
133
+ error instanceof Error && "code" in error
134
+ ? String((error as NodeJS.ErrnoException).code ?? "")
135
+ : "";
136
+ resolve({
137
+ stdout: "",
138
+ stderr: `${binary}: ${error.message}`,
139
+ exitCode: errorCode === "ENOENT" ? 127 : 1,
140
+ });
141
+ });
142
+
143
+ childProcess.on("close", (code) => {
144
+ resolve({
145
+ stdout,
146
+ stderr,
147
+ exitCode: code ?? 1,
148
+ });
149
+ });
150
+ });
151
+ }
152
+
79
153
  function levenshtein(a: string, b: string): number {
80
154
  const dp: number[][] = Array.from({ length: a.length + 1 }, () =>
81
155
  Array<number>(b.length + 1).fill(0),
@@ -1,13 +1,10 @@
1
- import type { ShellProperties } from "..";
2
- import type { VirtualUserManager } from "../../SSHMimic/users";
1
+ import type { VirtualShell } from "..";
3
2
  import type {
4
3
  CommandContext,
5
4
  CommandMode,
6
- CommandOutcome,
7
5
  CommandResult,
8
6
  ShellModule,
9
7
  } from "../../types/commands";
10
- import type VirtualFileSystem from "../../VirtualFileSystem";
11
8
  import { adduserCommand } from "./adduser";
12
9
  import { catCommand } from "./cat";
13
10
  import { cdCommand } from "./cd";
@@ -77,19 +74,28 @@ const helpCommand = createHelpCommand(() =>
77
74
  getCommandModules().map((cmd) => cmd.name),
78
75
  );
79
76
 
80
- function getCommandModules(): ShellModule[] {
81
- return [...BASE_COMMANDS, ...customCommands, helpCommand];
82
- }
77
+ const commandRegistry = new Map<string, ShellModule>();
78
+ let cachedCommandNames: string[] | null = null;
83
79
 
84
- function getTakenCommandNames(modules: ShellModule[]): Set<string> {
85
- const taken = new Set<string>();
86
- for (const mod of modules) {
87
- taken.add(mod.name);
80
+ function buildCache(): void {
81
+ for (const mod of getCommandModules()) {
82
+ commandRegistry.set(mod.name, mod);
88
83
  for (const alias of mod.aliases ?? []) {
89
- taken.add(alias);
84
+ commandRegistry.set(alias, mod);
90
85
  }
91
86
  }
92
- return taken;
87
+ cachedCommandNames = Array.from(commandRegistry.keys()).sort();
88
+ }
89
+
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
+ return [...BASE_COMMANDS, ...customCommands, helpCommand];
93
99
  }
94
100
 
95
101
  export function registerCommand(module: ShellModule): void {
@@ -106,13 +112,14 @@ export function registerCommand(module: ShellModule): void {
106
112
  );
107
113
  }
108
114
 
109
- const takenNames = getTakenCommandNames(getCommandModules());
110
- const conflict = names.find((name) => takenNames.has(name));
111
- if (conflict) {
112
- throw new Error(`Command '${conflict}' already exists`);
115
+ for (const name of names) {
116
+ if (commandRegistry.has(name)) {
117
+ throw new Error(`Command '${name}' already exists`);
118
+ }
119
+ commandRegistry.set(name, normalized);
113
120
  }
114
121
 
115
- customCommands.push(normalized);
122
+ buildCache();
116
123
  }
117
124
 
118
125
  export function createCustomCommand(
@@ -128,17 +135,14 @@ export function createCustomCommand(
128
135
  }
129
136
 
130
137
  export function getCommandNames(): string[] {
131
- return getCommandModules().flatMap((cmd) => [
132
- cmd.name,
133
- ...(cmd.aliases ?? []),
134
- ]);
138
+ if (!cachedCommandNames) {
139
+ buildCache();
140
+ }
141
+ return cachedCommandNames!;
135
142
  }
136
143
 
137
- function resolveModule(name: string): ShellModule | undefined {
138
- const lowered = name.toLowerCase();
139
- return getCommandModules().find(
140
- (cmd) => cmd.name === lowered || cmd.aliases?.includes(lowered),
141
- );
144
+ export function resolveModule(name: string): ShellModule | undefined {
145
+ return commandRegistry.get(name.toLowerCase());
142
146
  }
143
147
 
144
148
  function splitArgsRespectingQuotes(input: string): string[] {
@@ -192,28 +196,27 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
192
196
  }
193
197
 
194
198
  // Internal async function for pipeline execution
195
- async function runCommandInternal(
199
+
200
+ export async function runCommand(
196
201
  rawInput: string,
197
202
  authUser: string,
198
203
  hostname: string,
199
- users: VirtualUserManager,
200
204
  mode: CommandMode,
201
205
  cwd: string,
202
- shellProps: ShellProperties,
203
- vfs: VirtualFileSystem,
206
+ shell: VirtualShell,
204
207
  stdin?: string,
205
208
  ): Promise<CommandResult> {
206
- // Check if input contains pipes or redirections
207
- if (
208
- rawInput.includes("|") ||
209
- rawInput.includes(">") ||
210
- rawInput.includes("<")
211
- ) {
212
- // Use pipeline executor
209
+ const trimmed = rawInput.trim();
210
+
211
+ if (trimmed.length === 0) {
212
+ return { exitCode: 0 };
213
+ }
214
+
215
+ if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
213
216
  const { parseShellPipeline } = await import("../shellParser");
214
217
  const { executePipeline } = await import("../../SSHMimic/executor");
215
218
 
216
- const pipeline = parseShellPipeline(rawInput);
219
+ const pipeline = parseShellPipeline(trimmed);
217
220
  if (!pipeline.isValid) {
218
221
  return {
219
222
  stderr: pipeline.error || "Syntax error",
@@ -226,10 +229,9 @@ async function runCommandInternal(
226
229
  pipeline,
227
230
  authUser,
228
231
  hostname,
229
- users,
230
232
  mode,
231
233
  cwd,
232
- vfs,
234
+ shell,
233
235
  );
234
236
  } catch (error: unknown) {
235
237
  const message =
@@ -238,72 +240,6 @@ async function runCommandInternal(
238
240
  }
239
241
  }
240
242
 
241
- // Regular command execution
242
- const { commandName, args } = parseInput(rawInput);
243
- const mod = resolveModule(commandName);
244
-
245
- if (!mod) {
246
- return {
247
- stderr: `Command '${rawInput}' not found`,
248
- exitCode: 127,
249
- };
250
- }
251
-
252
- try {
253
- const result = mod.run({
254
- authUser,
255
- hostname,
256
- users,
257
- activeSessions: users.listActiveSessions(),
258
- rawInput,
259
- mode,
260
- args,
261
- shellProps,
262
- stdin,
263
- cwd,
264
- vfs,
265
- });
266
-
267
- return await Promise.resolve(result);
268
- } catch (error: unknown) {
269
- const message = error instanceof Error ? error.message : "Command failed";
270
- return { stderr: message, exitCode: 1 };
271
- }
272
- }
273
-
274
- export function runCommand(
275
- rawInput: string,
276
- authUser: string,
277
- hostname: string,
278
- users: VirtualUserManager,
279
- mode: CommandMode,
280
- cwd: string,
281
- shellProps: ShellProperties,
282
- vfs: VirtualFileSystem,
283
- stdin?: string,
284
- ): CommandOutcome {
285
- const trimmed = rawInput.trim();
286
-
287
- if (trimmed.length === 0) {
288
- return { exitCode: 0 };
289
- }
290
-
291
- // Check if input contains pipes or redirections - use async version
292
- if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
293
- return runCommandInternal(
294
- trimmed,
295
- authUser,
296
- hostname,
297
- users,
298
- mode,
299
- cwd,
300
- shellProps,
301
- vfs,
302
- stdin,
303
- );
304
- }
305
-
306
- // Regular synchronous command execution
307
243
  const { commandName, args } = parseInput(trimmed);
308
244
  const mod = resolveModule(commandName);
309
245
 
@@ -315,18 +251,16 @@ export function runCommand(
315
251
  }
316
252
 
317
253
  try {
318
- return mod.run({
254
+ return await mod.run({
319
255
  authUser,
320
256
  hostname,
321
- users,
322
- activeSessions: users.listActiveSessions(),
257
+ activeSessions: shell.users.listActiveSessions(),
323
258
  rawInput: trimmed,
324
259
  mode,
325
260
  args,
326
261
  stdin,
327
262
  cwd,
328
- vfs,
329
- shellProps,
263
+ shell,
330
264
  });
331
265
  } catch (error: unknown) {
332
266
  const message = error instanceof Error ? error.message : "Command failed";
@@ -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
  };
@@ -5,7 +5,7 @@ import { assertPathAccess, resolvePath } from "./helpers";
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
  },
@@ -5,7 +5,7 @@ import { assertPathAccess, resolvePath } from "./helpers";
5
5
  export const nanoCommand: ShellModule = {
6
6
  name: "nano",
7
7
  params: ["<file>"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  const fileArg = args[0];
10
10
  if (!fileArg) {
11
11
  return { stderr: "nano: missing file operand", exitCode: 1 };
@@ -13,8 +13,8 @@ export const nanoCommand: ShellModule = {
13
13
 
14
14
  const targetPath = resolvePath(cwd, fileArg);
15
15
  assertPathAccess(authUser, targetPath, "nano");
16
- const initialContent = vfs.exists(targetPath)
17
- ? vfs.readFile(targetPath)
16
+ const initialContent = shell.vfs.exists(targetPath)
17
+ ? shell.vfs.readFile(targetPath)
18
18
  : "";
19
19
  const safeName = path.posix.basename(targetPath) || "buffer";
20
20
  const tempPath = `/tmp/sshmimic-nano-${Date.now()}-${safeName}.tmp`;
@@ -6,7 +6,7 @@ import { getAllEnvVars } from "./set";
6
6
  export const neofetchCommand: ShellModule = {
7
7
  name: "neofetch",
8
8
  params: ["[--off]"],
9
- run: ({ args, authUser, hostname, shellProps }) => {
9
+ run: ({ args, authUser, hostname, shell }) => {
10
10
  const env = getAllEnvVars(authUser);
11
11
 
12
12
  if (ifFlag(args, "--help")) {
@@ -28,7 +28,7 @@ export const neofetchCommand: ShellModule = {
28
28
  user: authUser,
29
29
  host: hostname,
30
30
  shell: env.SHELL,
31
- shellProps: shellProps,
31
+ shellProps: shell.properties,
32
32
  terminal: env.TERM,
33
33
  }),
34
34
  exitCode: 0,
@@ -5,7 +5,7 @@ import { assertPathAccess, resolvePath } from "./helpers";
5
5
  export const rmCommand: ShellModule = {
6
6
  name: "rm",
7
7
  params: ["[-r|-rf] <path>"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  if (args.length === 0) {
10
10
  return { stderr: "rm: missing operand", exitCode: 1 };
11
11
  }
@@ -27,7 +27,7 @@ export const rmCommand: ShellModule = {
27
27
  for (const target of targets) {
28
28
  const resolvedTarget = resolvePath(cwd, target);
29
29
  assertPathAccess(authUser, resolvedTarget, "rm");
30
- vfs.remove(resolvedTarget, { recursive });
30
+ shell.vfs.remove(resolvedTarget, { recursive });
31
31
  }
32
32
 
33
33
  return { exitCode: 0 };
@@ -1,4 +1,3 @@
1
- import { defaultShellProperties } from "..";
2
1
  import type { CommandContext, ShellModule } from "../../types/commands";
3
2
  import { getArg, getFlag } from "./command-helpers";
4
3
  import { runCommand } from "./index";
@@ -9,8 +8,7 @@ export const shCommand: ShellModule = {
9
8
  params: ["-c <script>", "[<file>]"],
10
9
  aliases: ["bash"],
11
10
  run: async (ctx: CommandContext) => {
12
- const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
13
-
11
+ const { args, authUser, hostname, mode, cwd } = ctx;
14
12
  // Handle -c option: sh -c "command"
15
13
  if (getFlag(args, "-c") && args.length >= 2) {
16
14
  const script = getArg(args, 1) ?? "";
@@ -39,16 +37,7 @@ export const shCommand: ShellModule = {
39
37
 
40
38
  // Execute the command
41
39
  const result = await Promise.resolve(
42
- runCommand(
43
- command,
44
- authUser,
45
- hostname,
46
- users,
47
- mode,
48
- cwd,
49
- defaultShellProperties,
50
- vfs,
51
- ),
40
+ runCommand(command, authUser, hostname, mode, cwd, ctx.shell),
52
41
  );
53
42
 
54
43
  if (result.stdout) {
@@ -4,7 +4,8 @@ import { getArg } from "./command-helpers";
4
4
  export const suCommand: ShellModule = {
5
5
  name: "su",
6
6
  params: ["- <username>"],
7
- run: ({ authUser, users, args }) => {
7
+ run: ({ authUser, shell, args }) => {
8
+ const users = shell.users!;
8
9
  const targetUser = getArg(args, 0, { flags: ["-"] });
9
10
 
10
11
  if (!targetUser) {
@@ -1,6 +1,5 @@
1
- import { defaultShellProperties } from "..";
2
1
  import type { ShellModule } from "../../types/commands";
3
- import { getArg, getFlag, ifFlag } from "./command-helpers";
2
+ import { parseArgs } from "./command-helpers";
4
3
  import { runCommand } from "./index";
5
4
 
6
5
  function parseSudoArgs(args: string[]): {
@@ -8,35 +7,25 @@ function parseSudoArgs(args: string[]): {
8
7
  loginShell: boolean;
9
8
  commandLine: string | null;
10
9
  } {
11
- const loginShell = ifFlag(args, "-i");
12
- const targetUserValue = getFlag(args, ["-u", "--user"]);
13
- const targetUser =
14
- typeof targetUserValue === "string" && targetUserValue.length > 0
15
- ? targetUserValue
16
- : "root";
10
+ const { flags, flagsWithValues, positionals } = parseArgs(args, {
11
+ flags: ["-i", "-S"],
12
+ flagsWithValue: ["-u", "--user"],
13
+ });
17
14
 
18
- const commandParts: string[] = [];
19
- for (let index = 0; ; index += 1) {
20
- const part = getArg(args, index, {
21
- flags: ["-i", "-S"],
22
- flagsWithValue: ["-u", "--user"],
23
- });
24
- if (!part) {
25
- break;
26
- }
27
- commandParts.push(part);
28
- }
15
+ const loginShell = flags.has("-i");
16
+ const targetUser =
17
+ flagsWithValues.get("-u") || flagsWithValues.get("--user") || "root";
18
+ const commandLine = positionals.length > 0 ? positionals.join(" ") : null;
29
19
 
30
- const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
31
20
  return { targetUser, loginShell, commandLine };
32
21
  }
33
22
  export const sudoCommand: ShellModule = {
34
23
  name: "sudo",
35
24
  params: ["<command...>"],
36
- run: async ({ authUser, hostname, users, mode, cwd, vfs, args }) => {
25
+ run: async ({ authUser, hostname, mode, cwd, shell, args }) => {
37
26
  const { targetUser, loginShell, commandLine } = parseSudoArgs(args);
38
27
 
39
- if (authUser !== "root" && !users.isSudoer(authUser)) {
28
+ if (authUser !== "root" && !shell.users.isSudoer(authUser)) {
40
29
  return { stderr: "sudo: permission denied", exitCode: 1 };
41
30
  }
42
31
 
@@ -60,11 +49,9 @@ export const sudoCommand: ShellModule = {
60
49
  commandLine,
61
50
  effectiveUser,
62
51
  hostname,
63
- users,
64
52
  mode,
65
53
  loginShell ? `/home/${effectiveUser}` : cwd,
66
- defaultShellProperties,
67
- vfs,
54
+ shell,
68
55
  );
69
56
  }
70
57
 
@@ -4,7 +4,7 @@ import { assertPathAccess, resolvePath } from "./helpers";
4
4
  export const touchCommand: ShellModule = {
5
5
  name: "touch",
6
6
  params: ["<file>"],
7
- run: ({ authUser, vfs, cwd, args }) => {
7
+ run: ({ authUser, shell, cwd, args }) => {
8
8
  if (args.length === 0) {
9
9
  return { stderr: "touch: missing file operand", exitCode: 1 };
10
10
  }
@@ -12,8 +12,8 @@ export const touchCommand: ShellModule = {
12
12
  for (const file of args) {
13
13
  const target = resolvePath(cwd, file);
14
14
  assertPathAccess(authUser, target, "touch");
15
- if (!vfs.exists(target)) {
16
- vfs.writeFile(target, "");
15
+ if (!shell.vfs.exists(target)) {
16
+ shell.vfs.writeFile(target, "");
17
17
  }
18
18
  }
19
19
  return { exitCode: 0 };
@@ -5,9 +5,9 @@ import { assertPathAccess, resolvePath } from "./helpers";
5
5
  export const treeCommand: ShellModule = {
6
6
  name: "tree",
7
7
  params: ["[path]"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  const target = resolvePath(cwd, getArg(args, 0) ?? cwd);
10
10
  assertPathAccess(authUser, target, "tree");
11
- return { stdout: vfs.tree(target), exitCode: 0 };
11
+ return { stdout: shell.vfs.tree(target), exitCode: 0 };
12
12
  },
13
13
  };