typescript-virtual-container 1.0.4 → 1.0.5

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 (53) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +50 -0
  3. package/package.json +1 -1
  4. package/src/SSHMimic/client.ts +1 -1
  5. package/src/SSHMimic/exec.ts +1 -1
  6. package/src/SSHMimic/executor.ts +2 -2
  7. package/src/SSHMimic/index.ts +14 -18
  8. package/src/{VirtualFileSystem.ts → VirtualFileSystem/index.ts} +6 -15
  9. package/src/{SSHMimic → VirtualShell}/commands/cat.ts +2 -1
  10. package/src/VirtualShell/commands/command-helpers.ts +135 -0
  11. package/src/{SSHMimic → VirtualShell}/commands/curl.ts +16 -37
  12. package/src/{SSHMimic → VirtualShell}/commands/echo.ts +10 -2
  13. package/src/{SSHMimic → VirtualShell}/commands/export.ts +7 -1
  14. package/src/{SSHMimic → VirtualShell}/commands/grep.ts +15 -8
  15. package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
  16. package/src/{SSHMimic → VirtualShell}/commands/index.ts +63 -8
  17. package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
  18. package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
  19. package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
  20. package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
  21. package/src/VirtualShell/commands/sh.ts +58 -0
  22. package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
  23. package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +16 -26
  24. package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
  25. package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
  26. package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
  27. package/src/VirtualShell/index.ts +69 -0
  28. package/src/{SSHMimic → VirtualShell}/shell.ts +3 -3
  29. package/src/index.ts +8 -0
  30. package/src/standalone.ts +10 -1
  31. package/tests/command-helpers.test.ts +40 -0
  32. package/tests/helpers.test.ts +1 -1
  33. package/src/SSHMimic/commands/sh.ts +0 -121
  34. /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
  35. /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
  36. /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
  37. /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
  38. /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
  39. /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
  40. /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
  41. /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
  42. /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
  43. /package/src/{SSHMimic → VirtualShell}/commands/env.ts +0 -0
  44. /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
  45. /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
  46. /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
  47. /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
  48. /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
  49. /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
  50. /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
  51. /package/src/{SSHMimic → VirtualShell}/commands/unset.ts +0 -0
  52. /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
  53. /package/src/{SSHMimic → VirtualShell}/shellParser.ts +0 -0
@@ -1,4 +1,5 @@
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 mkdirCommand: ShellModule = {
@@ -9,7 +10,11 @@ export const mkdirCommand: ShellModule = {
9
10
  return { stderr: "mkdir: missing operand", exitCode: 1 };
10
11
  }
11
12
 
12
- for (const dir of args) {
13
+ for (let index = 0; index < args.length; index++) {
14
+ const dir = getArg(args, index);
15
+ if (!dir) {
16
+ return { stderr: "mkdir: missing operand", exitCode: 1 };
17
+ }
13
18
  const target = resolvePath(cwd, dir);
14
19
  assertPathAccess(authUser, target, "mkdir");
15
20
  vfs.mkdir(target);
@@ -1,4 +1,5 @@
1
1
  import type { ShellModule } from "../../types/commands";
2
+ import { getArg, ifFlag } from "./command-helpers";
2
3
  import { assertPathAccess, resolvePath } from "./helpers";
3
4
 
4
5
  export const rmCommand: ShellModule = {
@@ -9,9 +10,15 @@ export const rmCommand: ShellModule = {
9
10
  return { stderr: "rm: missing operand", exitCode: 1 };
10
11
  }
11
12
 
12
- const recursive =
13
- args.includes("-r") || args.includes("-rf") || args.includes("-fr");
14
- const targets = args.filter((arg) => !arg.startsWith("-"));
13
+ const recursive = ifFlag(args, ["-r", "-rf", "-fr"]);
14
+ const targets: string[] = [];
15
+ for (let index = 0; ; index += 1) {
16
+ const target = getArg(args, index, { flags: ["-r", "-rf", "-fr"] });
17
+ if (!target) {
18
+ break;
19
+ }
20
+ targets.push(target);
21
+ }
15
22
 
16
23
  if (targets.length === 0) {
17
24
  return { stderr: "rm: missing operand", exitCode: 1 };
@@ -1,5 +1,6 @@
1
1
  /** biome-ignore-all lint/style/useNamingConvention: env variables */
2
2
  import type { ShellModule } from "../../types/commands";
3
+ import { getArg } from "./command-helpers";
3
4
 
4
5
  // Simple in-memory environment variables store
5
6
  // In a real implementation, this would be per-session/per-user
@@ -41,7 +42,12 @@ export const setCommand: ShellModule = {
41
42
 
42
43
  // Parse VAR=value format
43
44
  const assignments: string[] = [];
44
- for (const arg of args) {
45
+ for (let index = 0; ; index += 1) {
46
+ const arg = getArg(args, index);
47
+ if (!arg) {
48
+ break;
49
+ }
50
+
45
51
  if (arg.includes("=")) {
46
52
  const [varName, varValue] = arg.split("=", 2);
47
53
  if (varName && varValue !== undefined) {
@@ -0,0 +1,58 @@
1
+ import type { CommandContext, ShellModule } from "../../types/commands";
2
+ import { getArg, getFlag } from "./command-helpers";
3
+ import { runCommand } from "./index";
4
+
5
+ /** Simple shell script executor with basic variable support */
6
+ export const shCommand: ShellModule = {
7
+ name: "sh",
8
+ params: ["-c <script>", "[<file>]"],
9
+ aliases: ["bash"],
10
+ run: async (ctx: CommandContext) => {
11
+ const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
12
+
13
+ // Handle -c option: sh -c "command"
14
+ if (getFlag(args, "-c") && args.length >= 2) {
15
+ const script = getArg(args, 1) ?? "";
16
+ if (!script) {
17
+ return { stderr: "sh: -c requires a script", exitCode: 1 };
18
+ }
19
+ const scriptArgs = args.slice(2);
20
+
21
+ // Split by semicolon and newline
22
+ const lines = script
23
+ .split(/[;\n]/)
24
+ .map((line) => line.trim())
25
+ .filter((line) => line && !line.startsWith("#"));
26
+
27
+ let output = "";
28
+ const exitCode = 0;
29
+
30
+ for (const line of lines) {
31
+ // Simple variable substitution
32
+ let command = line;
33
+ for (let i = 0; i < scriptArgs.length; i++) {
34
+ const arg = scriptArgs[i] ?? "";
35
+ command = command.replaceAll(`$${i}`, arg);
36
+ }
37
+ command = command.replaceAll("$@", scriptArgs.join(" "));
38
+
39
+ // Execute the command
40
+ const result = await Promise.resolve(
41
+ runCommand(command, authUser, hostname, users, mode, cwd, vfs),
42
+ );
43
+
44
+ if (result.stdout) {
45
+ output += `${result.stdout}\n`;
46
+ }
47
+
48
+ if (result.stderr) {
49
+ return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
50
+ }
51
+ }
52
+
53
+ return { stdout: output.trim(), exitCode };
54
+ }
55
+
56
+ return { stderr: "sh: invalid usage", exitCode: 1 };
57
+ },
58
+ };
@@ -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,5 @@
1
1
  import type { ShellModule } from "../../types/commands";
2
+ import { getArg, getFlag, ifFlag } from "./command-helpers";
2
3
  import { runCommand } from "./index";
3
4
 
4
5
  function parseSudoArgs(args: string[]): {
@@ -6,34 +7,23 @@ function parseSudoArgs(args: string[]): {
6
7
  loginShell: boolean;
7
8
  commandLine: string | null;
8
9
  } {
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
- }
10
+ const loginShell = ifFlag(args, "-i");
11
+ const targetUserValue = getFlag(args, ["-u", "--user"]);
12
+ const targetUser =
13
+ typeof targetUserValue === "string" && targetUserValue.length > 0
14
+ ? targetUserValue
15
+ : "root";
30
16
 
31
- if (arg.startsWith("-u=")) {
32
- targetUser = arg.slice(3) || "root";
33
- continue;
17
+ const commandParts: string[] = [];
18
+ for (let index = 0; ; index += 1) {
19
+ const part = getArg(args, index, {
20
+ flags: ["-i", "-S"],
21
+ flagsWithValue: ["-u", "--user"],
22
+ });
23
+ if (!part) {
24
+ break;
34
25
  }
35
-
36
- commandParts.push(arg);
26
+ commandParts.push(part);
37
27
  }
38
28
 
39
29
  const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
@@ -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,69 @@
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
+ class VirtualShell {
9
+ private vfs: VirtualFileSystem;
10
+ private users: VirtualUserManager;
11
+ private hostname: string;
12
+
13
+ constructor(
14
+ vfs: VirtualFileSystem,
15
+ users: VirtualUserManager,
16
+ hostname: string,
17
+ ) {
18
+ this.vfs = vfs;
19
+ this.users = users;
20
+ this.hostname = hostname;
21
+ }
22
+
23
+ addCommand(
24
+ name: string,
25
+ params: string[],
26
+ callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
27
+ ): void {
28
+ const normalized = name.trim().toLowerCase();
29
+ if (normalized.length === 0 || /\s/.test(normalized)) {
30
+ throw new Error("Command name must be non-empty and contain no spaces");
31
+ }
32
+
33
+ registerCommand(createCustomCommand(normalized, params, callback));
34
+ }
35
+
36
+ executeCommand(rawInput: string, authUser: string, cwd: string): void {
37
+ runCommand(
38
+ rawInput,
39
+ authUser,
40
+ this.hostname,
41
+ this.users,
42
+ "shell",
43
+ cwd,
44
+ this.vfs,
45
+ );
46
+ }
47
+
48
+ startInteractiveSession(
49
+ stream: ShellStream,
50
+ authUser: string,
51
+ sessionId: string | null,
52
+ remoteAddress: string,
53
+ terminalSize: { cols: number; rows: number },
54
+ ): void {
55
+ // Interactive shell logic
56
+ startShell(
57
+ stream,
58
+ authUser,
59
+ this.vfs!,
60
+ this.hostname,
61
+ this.users!,
62
+ sessionId,
63
+ remoteAddress,
64
+ terminalSize,
65
+ );
66
+ }
67
+ }
68
+
69
+ export { VirtualShell };
@@ -1,12 +1,12 @@
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 { formatLoginDate } from "../SSHMimic/loginFormat";
5
+ import { buildPrompt } from "../SSHMimic/prompt";
6
+ import type { VirtualUserManager } from "../SSHMimic/users";
4
7
  import type { ShellStream } from "../types/streams";
5
8
  import type VirtualFileSystem from "../VirtualFileSystem";
6
9
  import { getCommandNames, runCommand } from "./commands";
7
- import { formatLoginDate } from "./loginFormat";
8
- import { buildPrompt } from "./prompt";
9
- import type { VirtualUserManager } from "./users";
10
10
 
11
11
  interface NanoSession {
12
12
  kind: "nano" | "htop";
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);
@@ -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