typescript-virtual-container 1.0.3 → 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 (56) hide show
  1. package/.github/workflows/test-battery.yml +22 -0
  2. package/CHANGELOG.md +42 -0
  3. package/README.md +56 -28
  4. package/package.json +1 -1
  5. package/src/SSHMimic/client.ts +1 -1
  6. package/src/SSHMimic/exec.ts +1 -1
  7. package/src/SSHMimic/executor.ts +201 -0
  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/VirtualShell/commands/echo.ts +34 -0
  14. package/src/VirtualShell/commands/env.ts +22 -0
  15. package/src/VirtualShell/commands/export.ts +38 -0
  16. package/src/VirtualShell/commands/grep.ts +88 -0
  17. package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
  18. package/src/VirtualShell/commands/index.ts +327 -0
  19. package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
  20. package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
  21. package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
  22. package/src/VirtualShell/commands/set.ts +73 -0
  23. package/src/VirtualShell/commands/sh.ts +58 -0
  24. package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
  25. package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +16 -26
  26. package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
  27. package/src/VirtualShell/commands/unset.ts +19 -0
  28. package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
  29. package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
  30. package/src/VirtualShell/index.ts +69 -0
  31. package/src/{SSHMimic → VirtualShell}/shell.ts +3 -3
  32. package/src/VirtualShell/shellParser.ts +203 -0
  33. package/src/index.ts +8 -0
  34. package/src/standalone.ts +10 -1
  35. package/src/types/commands.ts +2 -0
  36. package/src/types/pipeline.ts +23 -0
  37. package/tests/command-helpers.test.ts +40 -0
  38. package/tests/helpers.test.ts +1 -1
  39. package/src/SSHMimic/commands/index.ts +0 -120
  40. /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
  41. /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
  42. /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
  43. /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
  44. /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
  45. /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
  46. /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
  47. /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
  48. /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
  49. /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
  50. /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
  51. /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
  52. /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
  53. /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
  54. /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
  55. /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
  56. /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
@@ -0,0 +1,73 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: env variables */
2
+ import type { ShellModule } from "../../types/commands";
3
+ import { getArg } from "./command-helpers";
4
+
5
+ // Simple in-memory environment variables store
6
+ // In a real implementation, this would be per-session/per-user
7
+ const envVars: Record<string, string> = {
8
+ PATH: "/usr/local/bin:/usr/bin:/bin",
9
+ HOME: "/home/user",
10
+ SHELL: "/bin/sh",
11
+ TERM: "xterm-256color",
12
+ USER: "user",
13
+ };
14
+
15
+ export function getEnvVar(name: string): string | undefined {
16
+ return envVars[name];
17
+ }
18
+
19
+ export function setEnvVar(name: string, value: string): void {
20
+ envVars[name] = value;
21
+ }
22
+
23
+ export function getAllEnvVars(authUser: string): Record<string, string> {
24
+ envVars.USER = authUser;
25
+ envVars.HOME = `/home/${authUser}`;
26
+ return { ...envVars };
27
+ }
28
+
29
+ export const setCommand: ShellModule = {
30
+ name: "set",
31
+ params: ["[VAR=value]"],
32
+ run: ({ args }) => {
33
+ // No arguments: display all environment variables
34
+ if (args.length === 0) {
35
+ const output = Object.entries(envVars)
36
+ .map(([key, value]) => `${key}=${value}`)
37
+ .sort()
38
+ .join("\n");
39
+
40
+ return { stdout: output, exitCode: 0 };
41
+ }
42
+
43
+ // Parse VAR=value format
44
+ const assignments: string[] = [];
45
+ for (let index = 0; ; index += 1) {
46
+ const arg = getArg(args, index);
47
+ if (!arg) {
48
+ break;
49
+ }
50
+
51
+ if (arg.includes("=")) {
52
+ const [varName, varValue] = arg.split("=", 2);
53
+ if (varName && varValue !== undefined) {
54
+ setEnvVar(varName, varValue);
55
+ assignments.push(arg);
56
+ }
57
+ } else {
58
+ // If no '=' present, display that specific variable
59
+ const value = getEnvVar(arg);
60
+ if (value !== undefined) {
61
+ assignments.push(`${arg}=${value}`);
62
+ } else {
63
+ assignments.push(`${arg}: not set`);
64
+ }
65
+ }
66
+ }
67
+
68
+ return {
69
+ stdout: assignments.length > 0 ? assignments.join("\n") : "",
70
+ exitCode: 0,
71
+ };
72
+ },
73
+ };
@@ -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
  },
@@ -0,0 +1,19 @@
1
+ import type { ShellModule } from "../../types/commands";
2
+ import { setEnvVar } from "./set";
3
+
4
+ export const unsetCommand: ShellModule = {
5
+ name: "unset",
6
+ params: ["<VAR...>"],
7
+ run: ({ args }) => {
8
+ if (args.length === 0) {
9
+ return { stderr: "unset: missing variable name", exitCode: 1 };
10
+ }
11
+
12
+ // Unset (remove) all specified variables
13
+ for (const varName of args) {
14
+ setEnvVar(varName, "");
15
+ }
16
+
17
+ return { exitCode: 0 };
18
+ },
19
+ };
@@ -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";
@@ -0,0 +1,203 @@
1
+ import type { Pipeline, PipelineCommand } from "../types/pipeline";
2
+
3
+ /** Parse a shell command line into a structured pipeline */
4
+ export function parseShellPipeline(rawInput: string): Pipeline {
5
+ const trimmed = rawInput.trim();
6
+
7
+ if (!trimmed) {
8
+ return { commands: [], isValid: true };
9
+ }
10
+
11
+ const commands: PipelineCommand[] = [];
12
+ const pipeTokens = tokenizePipeline(trimmed);
13
+
14
+ for (const token of pipeTokens) {
15
+ const cmd = parseCommandWithRedirections(token);
16
+ if (!cmd.isValid) {
17
+ return {
18
+ commands: [],
19
+ isValid: false,
20
+ error: cmd.error,
21
+ };
22
+ }
23
+ if (cmd.command) {
24
+ commands.push(cmd.command);
25
+ }
26
+ }
27
+
28
+ return { commands, isValid: true };
29
+ }
30
+
31
+ /** Tokenize input by pipes, respecting quoted strings */
32
+ function tokenizePipeline(input: string): string[] {
33
+ const tokens: string[] = [];
34
+ let current = "";
35
+ let inQuotes = false;
36
+ let quoteChar = "";
37
+ let i = 0;
38
+
39
+ while (i < input.length) {
40
+ const ch = input[i];
41
+
42
+ if ((ch === '"' || ch === "'") && (i === 0 || input[i - 1] !== "\\")) {
43
+ if (!inQuotes) {
44
+ inQuotes = true;
45
+ quoteChar = ch;
46
+ } else if (ch === quoteChar) {
47
+ inQuotes = false;
48
+ }
49
+ current += ch;
50
+ i++;
51
+ } else if (ch === "|" && !inQuotes) {
52
+ if (current.trim()) {
53
+ tokens.push(current.trim());
54
+ }
55
+ current = "";
56
+ i++;
57
+ } else {
58
+ current += ch;
59
+ i++;
60
+ }
61
+ }
62
+
63
+ if (current.trim()) {
64
+ tokens.push(current.trim());
65
+ }
66
+
67
+ return tokens;
68
+ }
69
+
70
+ interface ParseResult {
71
+ command?: PipelineCommand;
72
+ isValid: boolean;
73
+ error?: string;
74
+ }
75
+
76
+ /** Parse a single command with its redirections (>, >>, <) */
77
+ function parseCommandWithRedirections(token: string): ParseResult {
78
+ const parts = tokenizeCommand(token);
79
+
80
+ if (parts.length === 0) {
81
+ return { isValid: true };
82
+ }
83
+
84
+ const cmdParts: string[] = [];
85
+ let inputFile: string | undefined;
86
+ let outputFile: string | undefined;
87
+ let appendOutput = false;
88
+
89
+ let i = 0;
90
+ while (i < parts.length) {
91
+ const part = parts[i] as string;
92
+
93
+ if (part === "<") {
94
+ i++;
95
+ if (i >= parts.length) {
96
+ return {
97
+ isValid: false,
98
+ error: "Syntax error: expected filename after <",
99
+ };
100
+ }
101
+ inputFile = parts[i];
102
+ i++;
103
+ } else if (part === ">>") {
104
+ i++;
105
+ if (i >= parts.length) {
106
+ return {
107
+ isValid: false,
108
+ error: "Syntax error: expected filename after >>",
109
+ };
110
+ }
111
+ outputFile = parts[i];
112
+ appendOutput = true;
113
+ i++;
114
+ } else if (part === ">") {
115
+ i++;
116
+ if (i >= parts.length) {
117
+ return {
118
+ isValid: false,
119
+ error: "Syntax error: expected filename after >",
120
+ };
121
+ }
122
+ outputFile = parts[i];
123
+ appendOutput = false;
124
+ i++;
125
+ } else {
126
+ cmdParts.push(part);
127
+ i++;
128
+ }
129
+ }
130
+
131
+ if (cmdParts.length === 0) {
132
+ return { isValid: true };
133
+ }
134
+
135
+ const name = (cmdParts[0] as string).toLowerCase();
136
+ const args = cmdParts.slice(1);
137
+
138
+ return {
139
+ command: {
140
+ name,
141
+ args,
142
+ inputFile,
143
+ outputFile,
144
+ appendOutput,
145
+ },
146
+ isValid: true,
147
+ };
148
+ }
149
+
150
+ /** Tokenize a command, respecting quotes and handling >> vs > */
151
+ function tokenizeCommand(input: string): string[] {
152
+ const tokens: string[] = [];
153
+ let current = "";
154
+ let inQuotes = false;
155
+ let quoteChar = "";
156
+ let i = 0;
157
+
158
+ while (i < input.length) {
159
+ const ch = input[i];
160
+ const next = input[i + 1];
161
+
162
+ // Handle quotes
163
+ if ((ch === '"' || ch === "'") && (i === 0 || input[i - 1] !== "\\")) {
164
+ if (!inQuotes) {
165
+ inQuotes = true;
166
+ quoteChar = ch;
167
+ } else if (ch === quoteChar) {
168
+ inQuotes = false;
169
+ quoteChar = "";
170
+ } else {
171
+ current += ch;
172
+ }
173
+ i++;
174
+ } else if (ch === " " && !inQuotes) {
175
+ if (current) {
176
+ tokens.push(current);
177
+ current = "";
178
+ }
179
+ i++;
180
+ } else if ((ch === ">" || ch === "<") && !inQuotes) {
181
+ if (current) {
182
+ tokens.push(current);
183
+ current = "";
184
+ }
185
+ if (ch === ">" && next === ">") {
186
+ tokens.push(">>");
187
+ i += 2;
188
+ } else {
189
+ tokens.push(ch);
190
+ i++;
191
+ }
192
+ } else {
193
+ current += ch;
194
+ i++;
195
+ }
196
+ }
197
+
198
+ if (current) {
199
+ tokens.push(current);
200
+ }
201
+
202
+ return tokens;
203
+ }
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);
@@ -76,6 +76,8 @@ export interface CommandContext {
76
76
  mode: CommandMode;
77
77
  /** Tokenized arguments excluding command name. */
78
78
  args: string[];
79
+ /** Optional stdin payload (used by pipes/redirections). */
80
+ stdin?: string;
79
81
  /** Current working directory for command execution. */
80
82
  cwd: string;
81
83
  /** Virtual filesystem instance for IO operations. */
@@ -0,0 +1,23 @@
1
+ /** Represents a single command in a pipeline. */
2
+ export interface PipelineCommand {
3
+ /** Command name */
4
+ name: string;
5
+ /** Command arguments */
6
+ args: string[];
7
+ /** Input redirection file path (< file) */
8
+ inputFile?: string;
9
+ /** Output redirection file path (> file) */
10
+ outputFile?: string;
11
+ /** Append to output file (>> file) */
12
+ appendOutput?: boolean;
13
+ }
14
+
15
+ /** Represents a parsed shell pipeline */
16
+ export interface Pipeline {
17
+ /** List of commands in the pipeline */
18
+ commands: PipelineCommand[];
19
+ /** Whether this is a valid pipeline */
20
+ isValid: boolean;
21
+ /** Error message if parsing failed */
22
+ error?: string;
23
+ }