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
@@ -1,11 +1,11 @@
1
1
  import type { ShellModule } from "../../types/commands";
2
+ import { getArg } from "./command-helpers";
2
3
 
3
4
  export const suCommand: ShellModule = {
4
5
  name: "su",
5
6
  params: ["- <username>"],
6
7
  run: ({ authUser, users, args }) => {
7
- const filtered = args.filter((arg) => arg !== "-");
8
- const targetUser = filtered[0];
8
+ const targetUser = getArg(args, 0, { flags: ["-"] });
9
9
 
10
10
  if (!targetUser) {
11
11
  return { stderr: "su: missing username", exitCode: 1 };
@@ -16,7 +16,7 @@ export const suCommand: ShellModule = {
16
16
  }
17
17
 
18
18
  if (
19
- !users.verifyPassword(targetUser, filtered[1] ?? "") &&
19
+ !users.verifyPassword(targetUser, getArg(args, 1) ?? "") &&
20
20
  authUser !== "root"
21
21
  ) {
22
22
  return { stderr: "su: authentication failure", exitCode: 1 };
@@ -1,4 +1,6 @@
1
+ import { defaultShellProperties } from "..";
1
2
  import type { ShellModule } from "../../types/commands";
3
+ import { getArg, getFlag, ifFlag } from "./command-helpers";
2
4
  import { runCommand } from "./index";
3
5
 
4
6
  function parseSudoArgs(args: string[]): {
@@ -6,34 +8,23 @@ function parseSudoArgs(args: string[]): {
6
8
  loginShell: boolean;
7
9
  commandLine: string | null;
8
10
  } {
9
- let targetUser = "root";
10
- let loginShell = false;
11
- const commandParts: string[] = [];
12
-
13
- for (let index = 0; index < args.length; index += 1) {
14
- const arg = args[index]!;
15
-
16
- if (arg === "-i") {
17
- loginShell = true;
18
- continue;
19
- }
20
-
21
- if (arg === "-S") {
22
- continue;
23
- }
24
-
25
- if (arg === "-u") {
26
- targetUser = args[index + 1] ?? "root";
27
- index += 1;
28
- continue;
29
- }
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";
30
17
 
31
- if (arg.startsWith("-u=")) {
32
- targetUser = arg.slice(3) || "root";
33
- continue;
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;
34
26
  }
35
-
36
- commandParts.push(arg);
27
+ commandParts.push(part);
37
28
  }
38
29
 
39
30
  const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
@@ -72,6 +63,7 @@ export const sudoCommand: ShellModule = {
72
63
  users,
73
64
  mode,
74
65
  loginShell ? `/home/${effectiveUser}` : cwd,
66
+ defaultShellProperties,
75
67
  vfs,
76
68
  );
77
69
  }
@@ -1,11 +1,12 @@
1
1
  import type { ShellModule } from "../../types/commands";
2
+ import { getArg } from "./command-helpers";
2
3
  import { assertPathAccess, resolvePath } from "./helpers";
3
4
 
4
5
  export const treeCommand: ShellModule = {
5
6
  name: "tree",
6
7
  params: ["[path]"],
7
8
  run: ({ authUser, vfs, cwd, args }) => {
8
- const target = resolvePath(cwd, args[0] ?? cwd);
9
+ const target = resolvePath(cwd, getArg(args, 0) ?? cwd);
9
10
  assertPathAccess(authUser, target, "tree");
10
11
  return { stdout: vfs.tree(target), exitCode: 0 };
11
12
  },
@@ -3,10 +3,10 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { ShellModule } from "../../types/commands";
6
+ import { getArg, getFlag, ifFlag } from "./command-helpers";
6
7
  import {
7
8
  assertPathAccess,
8
9
  normalizeTerminalOutput,
9
- parseOutputPath,
10
10
  resolvePath,
11
11
  stripUrlFilename,
12
12
  } from "./helpers";
@@ -83,12 +83,29 @@ export const wgetCommand: ShellModule = {
83
83
  name: "wget",
84
84
  params: ["[url]"],
85
85
  run: async ({ authUser, vfs, cwd, args }) => {
86
- const { outputPath, inputArgs } = parseOutputPath(args);
86
+ const outputPathValue = getFlag(args, [
87
+ "-o",
88
+ "-O",
89
+ "--output",
90
+ "--output-document",
91
+ ]);
92
+ const outputPath =
93
+ typeof outputPathValue === "string" && outputPathValue.length > 0
94
+ ? outputPathValue
95
+ : null;
96
+ const parserOptions = {
97
+ flagsWithValue: ["-o", "-O", "--output", "--output-document"],
98
+ };
99
+ const inputArgs: string[] = [];
100
+ for (let index = 0; ; index += 1) {
101
+ const arg = getArg(args, index, parserOptions);
102
+ if (!arg) {
103
+ break;
104
+ }
105
+ inputArgs.push(arg);
106
+ }
87
107
  const url = inputArgs[0];
88
- const isHelpLike = inputArgs.some(
89
- (arg) =>
90
- arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version",
91
- );
108
+ const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
92
109
 
93
110
  if (!url) {
94
111
  return { stderr: "wget: missing URL", exitCode: 1 };
@@ -1,5 +1,5 @@
1
+ import { formatLoginDate } from "../../SSHMimic/loginFormat";
1
2
  import type { ShellModule } from "../../types/commands";
2
- import { formatLoginDate } from "../loginFormat";
3
3
 
4
4
  export const whoCommand: ShellModule = {
5
5
  name: "who",
@@ -0,0 +1,86 @@
1
+ import type { VirtualUserManager } from "../SSHMimic/users";
2
+ import type { CommandContext, CommandResult } from "../types/commands";
3
+ import type { ShellStream } from "../types/streams";
4
+ import type VirtualFileSystem from "../VirtualFileSystem";
5
+ import { createCustomCommand, registerCommand, runCommand } from "./commands";
6
+ import { startShell } from "./shell";
7
+
8
+ export interface ShellProperties {
9
+ kernel: string;
10
+ os: "Fortune GNU/Linux x64";
11
+ arch: "x86_64";
12
+ }
13
+
14
+ export const defaultShellProperties: ShellProperties = {
15
+ kernel: "1.0.0+itsrealfortune+1-amd64",
16
+ os: "Fortune GNU/Linux x64",
17
+ arch: "x86_64",
18
+ };
19
+
20
+ class VirtualShell {
21
+ private vfs: VirtualFileSystem;
22
+ private users: VirtualUserManager;
23
+ private hostname: string;
24
+ public properties: ShellProperties;
25
+
26
+ constructor(
27
+ vfs: VirtualFileSystem,
28
+ users: VirtualUserManager,
29
+ hostname: string,
30
+ properties?: ShellProperties,
31
+ ) {
32
+ this.vfs = vfs;
33
+ this.users = users;
34
+ this.hostname = hostname;
35
+ this.properties = properties || defaultShellProperties;
36
+ }
37
+
38
+ addCommand(
39
+ name: string,
40
+ params: string[],
41
+ callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
42
+ ): void {
43
+ const normalized = name.trim().toLowerCase();
44
+ if (normalized.length === 0 || /\s/.test(normalized)) {
45
+ throw new Error("Command name must be non-empty and contain no spaces");
46
+ }
47
+
48
+ registerCommand(createCustomCommand(normalized, params, callback));
49
+ }
50
+
51
+ executeCommand(rawInput: string, authUser: string, cwd: string): void {
52
+ runCommand(
53
+ rawInput,
54
+ authUser,
55
+ this.hostname,
56
+ this.users,
57
+ "shell",
58
+ cwd,
59
+ this.properties,
60
+ this.vfs,
61
+ );
62
+ }
63
+
64
+ startInteractiveSession(
65
+ stream: ShellStream,
66
+ authUser: string,
67
+ sessionId: string | null,
68
+ remoteAddress: string,
69
+ terminalSize: { cols: number; rows: number },
70
+ ): void {
71
+ // Interactive shell logic
72
+ startShell(
73
+ this.properties,
74
+ stream,
75
+ authUser,
76
+ this.vfs!,
77
+ this.hostname,
78
+ this.users!,
79
+ sessionId,
80
+ remoteAddress,
81
+ terminalSize,
82
+ );
83
+ }
84
+ }
85
+
86
+ export { VirtualShell };
@@ -1,12 +1,13 @@
1
1
  import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
2
  import { readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import { defaultShellProperties, type ShellProperties } from ".";
5
+ import { formatLoginDate } from "../SSHMimic/loginFormat";
6
+ import { buildPrompt } from "../SSHMimic/prompt";
7
+ import type { VirtualUserManager } from "../SSHMimic/users";
4
8
  import type { ShellStream } from "../types/streams";
5
9
  import type VirtualFileSystem from "../VirtualFileSystem";
6
10
  import { getCommandNames, runCommand } from "./commands";
7
- import { formatLoginDate } from "./loginFormat";
8
- import { buildPrompt } from "./prompt";
9
- import type { VirtualUserManager } from "./users";
10
11
 
11
12
  interface NanoSession {
12
13
  kind: "nano" | "htop";
@@ -41,6 +42,7 @@ function toTtyLines(text: string): string {
41
42
  }
42
43
 
43
44
  export function startShell(
45
+ properties: ShellProperties,
44
46
  stream: ShellStream,
45
47
  authUser: string,
46
48
  vfs: VirtualFileSystem,
@@ -180,6 +182,7 @@ export function startShell(
180
182
  users,
181
183
  "shell",
182
184
  runCwd,
185
+ defaultShellProperties,
183
186
  vfs,
184
187
  ),
185
188
  );
@@ -467,20 +470,15 @@ export function startShell(
467
470
  }
468
471
 
469
472
  function renderLoginBanner(): void {
470
- // const kernel = os.release();
471
- // const arch = os.arch();
472
-
473
- // Our own kernel and arch strings to avoid leaking host info and to provide a more "Linux-like" feel
474
- const kernel = "5.15.0-1051-azure";
475
- const arch = "x86_64";
476
-
477
473
  const last = readLastLogin();
478
474
  const nowIso = new Date().toISOString();
479
475
 
480
- stream.write(`Linux ${hostname} ${kernel} ${arch}\r\n`);
476
+ stream.write(
477
+ `Linux ${hostname} ${properties.kernel} ${properties.arch}\r\n`,
478
+ );
481
479
  stream.write("\r\n");
482
480
  stream.write(
483
- "The programs included with the Debian GNU/Linux system are free software;\r\n",
481
+ "The programs included with the Fortune GNU/Linux system are free software;\r\n",
484
482
  );
485
483
  stream.write(
486
484
  "the exact distribution terms for each program are described in the\r\n",
@@ -488,7 +486,7 @@ export function startShell(
488
486
  stream.write("individual files in /usr/share/doc/*/copyright.\r\n");
489
487
  stream.write("\r\n");
490
488
  stream.write(
491
- "Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent\r\n",
489
+ "Fortune GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent\r\n",
492
490
  );
493
491
  stream.write("permitted by applicable law.\r\n");
494
492
 
@@ -649,7 +647,16 @@ export function startShell(
649
647
 
650
648
  if (line.length > 0) {
651
649
  const result = await Promise.resolve(
652
- runCommand(line, authUser, hostname, users, "shell", cwd, vfs),
650
+ runCommand(
651
+ line,
652
+ authUser,
653
+ hostname,
654
+ users,
655
+ "shell",
656
+ cwd,
657
+ defaultShellProperties,
658
+ vfs,
659
+ ),
653
660
  );
654
661
 
655
662
  pushHistory(line);
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import { SshClient } from "./SSHMimic/client";
2
2
  import { SshMimic } from "./SSHMimic/index";
3
3
  import { VirtualUserManager } from "./SSHMimic/users";
4
4
  import VirtualFileSystem from "./VirtualFileSystem";
5
+ import type { VirtualShell } from "./VirtualShell";
5
6
 
6
7
  export type {
7
8
  CommandContext,
@@ -33,4 +34,11 @@ export {
33
34
  VirtualFileSystem,
34
35
  SshMimic as VirtualMachine,
35
36
  VirtualUserManager,
37
+ type VirtualShell,
36
38
  };
39
+
40
+ export {
41
+ getArg,
42
+ getFlag,
43
+ ifFlag,
44
+ } from "./VirtualShell/commands/command-helpers";
package/src/standalone.ts CHANGED
@@ -6,7 +6,16 @@ const sshMimic = new VirtualMachine({ port: 2222, hostname: sshHostname });
6
6
  sshMimic
7
7
  .start()
8
8
  .then((port: number) => {
9
- console.log(`SSH Mimic initialized. Listening on port ${port}.`);
9
+ if (!sshMimic.shell) console.error("Failed to initialize SSH Mimic shell.");
10
+ else {
11
+ console.log(`SSH Mimic initialized. Listening on port ${port}.`);
12
+ sshMimic.shell.addCommand("demo", [], () => {
13
+ return {
14
+ stdout: "This is a demo command. It does nothing useful.",
15
+ exitCode: 0,
16
+ };
17
+ });
18
+ }
10
19
  })
11
20
  .catch((error: unknown) => {
12
21
  console.error("Failed to start SSH Mimic:", error);
@@ -6,6 +6,7 @@ import type {
6
6
  VirtualUserManager,
7
7
  } from "../SSHMimic/users";
8
8
  import type VirtualFileSystem from "../VirtualFileSystem";
9
+ import type { ShellProperties } from "../VirtualShell";
9
10
 
10
11
  /**
11
12
  * Normalized command execution output.
@@ -76,6 +77,8 @@ export interface CommandContext {
76
77
  mode: CommandMode;
77
78
  /** Tokenized arguments excluding command name. */
78
79
  args: string[];
80
+ /** Virtual shell instance. */
81
+ shellProps: ShellProperties;
79
82
  /** Optional stdin payload (used by pipes/redirections). */
80
83
  stdin?: string;
81
84
  /** Current working directory for command execution. */
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getArg,
4
+ getFlag,
5
+ ifFlag,
6
+ } from "../src/VirtualShell/commands/command-helpers";
7
+
8
+ describe("command-helpers", () => {
9
+ test("ifFlag detects plain and inline flag forms", () => {
10
+ expect(ifFlag(["-l", "docs"], ["-l", "--long"])).toBe(true);
11
+ expect(ifFlag(["--user=root", "whoami"], "--user")).toBe(true);
12
+ expect(ifFlag(["docs"], ["-l", "--long"])).toBe(false);
13
+ });
14
+
15
+ test("getFlag returns value for adjacent and inline forms", () => {
16
+ expect(getFlag(["-u", "root", "id"], ["-u", "--user"])).toBe("root");
17
+ expect(getFlag(["--user=alice", "id"], ["-u", "--user"])).toBe("alice");
18
+ expect(getFlag(["-i", "whoami"], "-i")).toBe("whoami");
19
+ expect(getFlag(["-o"], "-o")).toBe(true);
20
+ expect(getFlag(["pwd"], ["-u", "--user"])).toBeUndefined();
21
+ });
22
+
23
+ test("getArg skips bool and value flags", () => {
24
+ const args = ["-i", "-u", "root", "sh", "-c", "whoami"];
25
+ const options = { flags: ["-i"], flagsWithValue: ["-u"] };
26
+
27
+ expect(getArg(args, 0, options)).toBe("sh");
28
+ expect(getArg(args, 1, options)).toBe("-c");
29
+ expect(getArg(args, 2, options)).toBe("whoami");
30
+ expect(getArg(args, 3, options)).toBeUndefined();
31
+ });
32
+
33
+ test("getArg keeps tokens after -- as positional", () => {
34
+ const args = ["-n", "--", "-n", "hello"];
35
+ const options = { flags: ["-n"] };
36
+
37
+ expect(getArg(args, 0, options)).toBe("-n");
38
+ expect(getArg(args, 1, options)).toBe("hello");
39
+ });
40
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { assertPathAccess } from "../src/SSHMimic/commands/helpers";
2
+ import { assertPathAccess } from "../src/VirtualShell/commands/helpers";
3
3
 
4
4
  describe("assertPathAccess", () => {
5
5
  test("blocks non-root access to auth store", () => {
@@ -1,121 +0,0 @@
1
- import type { CommandContext, ShellModule } from "../../types/commands";
2
- import { runCommand } from "./index";
3
-
4
- /** Simple shell script executor with basic variable support */
5
- export const shCommand: ShellModule = {
6
- name: "sh",
7
- params: ["-c <script>", "[<file>]"],
8
- aliases: ["bash"],
9
- run: async (ctx: CommandContext) => {
10
- const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
11
-
12
- // Handle -c option: sh -c "command"
13
- if (args[0] === "-c" && args.length >= 2) {
14
- const script = args[1] ?? "";
15
- if (!script) {
16
- return { stderr: "sh: -c requires a script", exitCode: 1 };
17
- }
18
- const scriptArgs = args.slice(2);
19
-
20
- // Split by semicolon and newline
21
- const lines = script
22
- .split(/[;\n]/)
23
- .map((line) => line.trim())
24
- .filter((line) => line && !line.startsWith("#"));
25
-
26
- let output = "";
27
- let exitCode = 0;
28
-
29
- for (const line of lines) {
30
- // Simple variable substitution
31
- let command = line;
32
- for (let i = 0; i < scriptArgs.length; i++) {
33
- const arg = scriptArgs[i] ?? "";
34
- command = command.replaceAll(`$${i}`, arg);
35
- }
36
- command = command.replaceAll("$@", scriptArgs.join(" "));
37
-
38
- // Execute the command
39
- const result = await Promise.resolve(
40
- runCommand(command, authUser, hostname, users, mode, cwd, vfs),
41
- );
42
-
43
- if (result.stdout) {
44
- output += `${result.stdout}\n`;
45
- }
46
-
47
- if (result.stderr) {
48
- return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
49
- }
50
-
51
- exitCode = result.exitCode ?? 0;
52
- if (exitCode !== 0) {
53
- break;
54
- }
55
- }
56
-
57
- return {
58
- stdout: output.trimEnd(),
59
- exitCode,
60
- };
61
- }
62
-
63
- // Handle script file execution: sh <file>
64
- if (args.length > 0 && args[0]) {
65
- try {
66
- const scriptFile = args[0];
67
- const content = vfs.readFile(scriptFile);
68
- const scriptArgs = args.slice(1);
69
-
70
- // Split by newline
71
- const lines = content
72
- .split("\n")
73
- .map((line) => line.trim())
74
- .filter((line) => line && !line.startsWith("#"));
75
-
76
- let output = "";
77
- let exitCode = 0;
78
-
79
- for (const line of lines) {
80
- // Simple variable substitution
81
- let command = line;
82
- for (let i = 0; i < scriptArgs.length; i++) {
83
- const arg = scriptArgs[i] ?? "";
84
- command = command.replaceAll(`$${i}`, arg);
85
- }
86
- command = command.replaceAll("$@", scriptArgs.join(" "));
87
-
88
- // Execute the command
89
- const result = await Promise.resolve(
90
- runCommand(command, authUser, hostname, users, mode, cwd, vfs),
91
- );
92
-
93
- if (result.stdout) {
94
- output += `${result.stdout}\n`;
95
- }
96
-
97
- if (result.stderr) {
98
- return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
99
- }
100
-
101
- exitCode = result.exitCode ?? 0;
102
- if (exitCode !== 0) {
103
- break;
104
- }
105
- }
106
-
107
- return {
108
- stdout: output.trimEnd(),
109
- exitCode,
110
- };
111
- } catch {
112
- return {
113
- stderr: `sh: ${args[0]}: No such file or directory`,
114
- exitCode: 1,
115
- };
116
- }
117
- }
118
-
119
- return { stderr: "sh: missing operand or script", exitCode: 1 };
120
- },
121
- };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes