typescript-virtual-container 1.0.7 → 1.0.8

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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@ The format is based on Keep a Changelog.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.6...1.0.8] - 2026-04-15
10
+
11
+ Too much refactor to list.
12
+
9
13
  ## [1.0.5] - 2026-04-15
10
14
 
11
15
  ### Changed
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "In-memory SSH server with virtual filesystem and typed programmatic API",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
- "version": "1.0.7",
6
+ "version": "1.0.8",
7
7
  "license": "MIT",
8
8
  "keywords": [
9
9
  "ssh",
@@ -58,6 +58,9 @@ class VirtualFileSystem {
58
58
  this.dirty = false;
59
59
  return;
60
60
  } catch {
61
+ console.warn(
62
+ `No valid mirror archive found at '${this.archivePath}'. Starting with empty filesystem.`,
63
+ );
61
64
  await this.flushMirror();
62
65
  }
63
66
  }
@@ -133,3 +133,67 @@ export function getArg(
133
133
  const positionals = collectPositionals(args, options);
134
134
  return positionals[index];
135
135
  }
136
+
137
+ /**
138
+ * Parse arguments into flags, flags with values, and positionals.
139
+ * @param args - Array of arguments to parse.
140
+ * @param options - Parsing options for flags and flags with values.
141
+ * @returns Parsed arguments as { flags, flagsWithValues, positionals }.
142
+ */
143
+ export function parseArgs(
144
+ args: string[],
145
+ options: { flags?: string[]; flagsWithValue?: string[] } = {},
146
+ ): {
147
+ flags: Set<string>;
148
+ flagsWithValues: Map<string, string>;
149
+ positionals: string[];
150
+ } {
151
+ const flags = new Set<string>();
152
+ const flagsWithValues = new Map<string, string>();
153
+ const positionals: string[] = [];
154
+ const boolFlags = new Set(options.flags ?? []);
155
+ const valueFlags = new Set(options.flagsWithValue ?? []);
156
+ let passthrough = false;
157
+
158
+ for (let index = 0; index < args.length; index += 1) {
159
+ const arg = args[index]!;
160
+
161
+ if (passthrough) {
162
+ positionals.push(arg);
163
+ continue;
164
+ }
165
+
166
+ if (arg === "--") {
167
+ passthrough = true;
168
+ continue;
169
+ }
170
+
171
+ if (boolFlags.has(arg)) {
172
+ flags.add(arg);
173
+ continue;
174
+ }
175
+
176
+ if (valueFlags.has(arg)) {
177
+ const next = args[index + 1];
178
+ if (next && !next.startsWith("-")) {
179
+ flagsWithValues.set(arg, next);
180
+ index += 1;
181
+ } else {
182
+ flagsWithValues.set(arg, "");
183
+ }
184
+ continue;
185
+ }
186
+
187
+ const inlineFlag = Array.from(valueFlags).find((flag) =>
188
+ arg.startsWith(`${flag}=`),
189
+ );
190
+ if (inlineFlag) {
191
+ flagsWithValues.set(inlineFlag, arg.slice(inlineFlag.length + 1));
192
+ continue;
193
+ }
194
+
195
+ positionals.push(arg);
196
+ }
197
+
198
+ return { flags, flagsWithValues, positionals };
199
+ }
@@ -1,107 +1,31 @@
1
- import { spawn } from "node:child_process";
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 {
5
4
  assertPathAccess,
6
5
  normalizeTerminalOutput,
7
6
  resolvePath,
7
+ runHostCommand,
8
8
  } from "./helpers";
9
9
 
10
- function runHostCurl(args: string[]): Promise<{
11
- stdout: string;
12
- stderr: string;
13
- exitCode: number;
14
- }> {
15
- return new Promise((resolve) => {
16
- let childProcess: ReturnType<typeof spawn>;
17
-
18
- try {
19
- childProcess = spawn("curl", args, {
20
- stdio: ["ignore", "pipe", "pipe"],
21
- });
22
- } catch (error) {
23
- resolve({
24
- stdout: "",
25
- stderr: `curl: ${error instanceof Error ? error.message : String(error)}`,
26
- exitCode: 1,
27
- });
28
- return;
29
- }
30
-
31
- let stdout = "";
32
- let stderr = "";
33
- const stdoutStream = childProcess.stdout;
34
- const stderrStream = childProcess.stderr;
35
-
36
- if (!stdoutStream || !stderrStream) {
37
- resolve({
38
- stdout: "",
39
- stderr: "curl: failed to capture process output",
40
- exitCode: 1,
41
- });
42
- return;
43
- }
44
-
45
- stdoutStream.setEncoding("utf8");
46
- stderrStream.setEncoding("utf8");
47
-
48
- stdoutStream.on("data", (chunk: string) => {
49
- stdout += chunk;
50
- });
51
-
52
- stderrStream.on("data", (chunk: string) => {
53
- stderr += chunk;
54
- });
55
-
56
- childProcess.on("error", (error) => {
57
- const errorCode =
58
- error instanceof Error && "code" in error
59
- ? String((error as NodeJS.ErrnoException).code ?? "")
60
- : "";
61
- resolve({
62
- stdout: "",
63
- stderr: `curl: ${error.message}`,
64
- exitCode: errorCode === "ENOENT" ? 127 : 1,
65
- });
66
- });
67
-
68
- childProcess.on("close", (code) => {
69
- resolve({
70
- stdout,
71
- stderr,
72
- exitCode: code ?? 1,
73
- });
74
- });
75
- });
76
- }
77
-
78
10
  export const curlCommand: ShellModule = {
79
11
  name: "curl",
80
12
  params: ["[-o file] <url>"],
81
13
  run: async ({ authUser, vfs, cwd, args }) => {
82
- const outputPathValue = getFlag(args, ["-o", "--output"]);
14
+ const { flagsWithValues, positionals } = parseArgs(args, {
15
+ flagsWithValue: ["-o", "--output"],
16
+ });
83
17
  const outputPath =
84
- typeof outputPathValue === "string" && outputPathValue.length > 0
85
- ? outputPathValue
86
- : null;
87
- const parserOptions = { flagsWithValue: ["-o", "--output"] };
88
- const inputArgs: string[] = [];
89
- for (let index = 0; ; index += 1) {
90
- const arg = getArg(args, index, parserOptions);
91
- if (!arg) {
92
- break;
93
- }
94
- inputArgs.push(arg);
95
- }
96
- const url = inputArgs[0];
97
- const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
18
+ flagsWithValues.get("-o") || flagsWithValues.get("--output") || null;
19
+ const url = positionals[0];
98
20
 
99
21
  if (!url) {
100
22
  return { stderr: "curl: missing URL", exitCode: 1 };
101
23
  }
102
24
 
103
- const passthroughArgs = outputPath ? [...inputArgs, "-o", "-"] : inputArgs;
104
- const result = await runHostCurl(passthroughArgs);
25
+ const passthroughArgs = outputPath
26
+ ? [...positionals, "-o", "-"]
27
+ : positionals;
28
+ const result = await runHostCommand("curl", passthroughArgs);
105
29
 
106
30
  if (result.exitCode !== 0) {
107
31
  return {
@@ -125,9 +49,7 @@ export const curlCommand: ShellModule = {
125
49
  }
126
50
 
127
51
  return {
128
- stdout: isHelpLike
129
- ? normalizeTerminalOutput(result.stdout)
130
- : result.stdout,
52
+ stdout: result.stdout,
131
53
  stderr: result.stderr
132
54
  ? normalizeTerminalOutput(result.stderr)
133
55
  : undefined,
@@ -1,5 +1,5 @@
1
1
  import type { ShellModule } from "../../types/commands";
2
- import { getArg, ifFlag } from "./command-helpers";
2
+ import { parseArgs } from "./command-helpers";
3
3
  import { getAllEnvVars } from "./set";
4
4
 
5
5
  function expandEnvVars(input: string, env: Record<string, string>): string {
@@ -12,18 +12,11 @@ export const echoCommand: ShellModule = {
12
12
  name: "echo",
13
13
  params: ["[options] [text...]"],
14
14
  run: ({ args, authUser, stdin }) => {
15
- const newline = !ifFlag(args, "-n");
16
- const filteredArgs: string[] = [];
17
- for (let index = 0; ; index += 1) {
18
- const value = getArg(args, index, { flags: ["-n"] });
19
- if (value === undefined) {
20
- break;
21
- }
22
- filteredArgs.push(value);
23
- }
24
- const env = getAllEnvVars(authUser);
15
+ const { flags, positionals } = parseArgs(args, { flags: ["-n"] });
16
+ const newline = !flags.has("-n");
25
17
  const rawText =
26
- filteredArgs.length > 0 ? filteredArgs.join(" ") : (stdin ?? "");
18
+ positionals.length > 0 ? positionals.join(" ") : (stdin ?? "");
19
+ const env = getAllEnvVars(authUser);
27
20
  const text = expandEnvVars(rawText, env);
28
21
 
29
22
  return {
@@ -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
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
- }
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 };
@@ -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),
@@ -3,7 +3,6 @@ import type { VirtualUserManager } from "../../SSHMimic/users";
3
3
  import type {
4
4
  CommandContext,
5
5
  CommandMode,
6
- CommandOutcome,
7
6
  CommandResult,
8
7
  ShellModule,
9
8
  } from "../../types/commands";
@@ -77,11 +76,29 @@ const helpCommand = createHelpCommand(() =>
77
76
  getCommandModules().map((cmd) => cmd.name),
78
77
  );
79
78
 
79
+ const commandRegistry = new Map<string, ShellModule>();
80
+ let cachedCommandNames: string[] | null = null;
81
+
82
+ function invalidateCache(): void {
83
+ cachedCommandNames = null;
84
+ }
85
+
86
+ function buildCache(): void {
87
+ commandRegistry.clear();
88
+ for (const mod of getCommandModules()) {
89
+ commandRegistry.set(mod.name, mod);
90
+ for (const alias of mod.aliases ?? []) {
91
+ commandRegistry.set(alias, mod);
92
+ }
93
+ }
94
+ cachedCommandNames = Array.from(commandRegistry.keys()).sort();
95
+ }
96
+
80
97
  function getCommandModules(): ShellModule[] {
81
98
  return [...BASE_COMMANDS, ...customCommands, helpCommand];
82
99
  }
83
100
 
84
- function getTakenCommandNames(modules: ShellModule[]): Set<string> {
101
+ function _getTakenCommandNames(modules: ShellModule[]): Set<string> {
85
102
  const taken = new Set<string>();
86
103
  for (const mod of modules) {
87
104
  taken.add(mod.name);
@@ -106,13 +123,14 @@ export function registerCommand(module: ShellModule): void {
106
123
  );
107
124
  }
108
125
 
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`);
126
+ for (const name of names) {
127
+ if (commandRegistry.has(name)) {
128
+ throw new Error(`Command '${name}' already exists`);
129
+ }
130
+ commandRegistry.set(name, normalized);
113
131
  }
114
132
 
115
- customCommands.push(normalized);
133
+ invalidateCache();
116
134
  }
117
135
 
118
136
  export function createCustomCommand(
@@ -128,17 +146,14 @@ export function createCustomCommand(
128
146
  }
129
147
 
130
148
  export function getCommandNames(): string[] {
131
- return getCommandModules().flatMap((cmd) => [
132
- cmd.name,
133
- ...(cmd.aliases ?? []),
134
- ]);
149
+ if (!cachedCommandNames) {
150
+ buildCache();
151
+ }
152
+ return cachedCommandNames!;
135
153
  }
136
154
 
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
- );
155
+ export function resolveModule(name: string): ShellModule | undefined {
156
+ return commandRegistry.get(name.toLowerCase());
142
157
  }
143
158
 
144
159
  function splitArgsRespectingQuotes(input: string): string[] {
@@ -192,7 +207,7 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
192
207
  }
193
208
 
194
209
  // Internal async function for pipeline execution
195
- async function runCommandInternal(
210
+ async function _runCommandInternal(
196
211
  rawInput: string,
197
212
  authUser: string,
198
213
  hostname: string,
@@ -271,7 +286,7 @@ async function runCommandInternal(
271
286
  }
272
287
  }
273
288
 
274
- export function runCommand(
289
+ export async function runCommand(
275
290
  rawInput: string,
276
291
  authUser: string,
277
292
  hostname: string,
@@ -281,29 +296,42 @@ export function runCommand(
281
296
  shellProps: ShellProperties,
282
297
  vfs: VirtualFileSystem,
283
298
  stdin?: string,
284
- ): CommandOutcome {
299
+ ): Promise<CommandResult> {
285
300
  const trimmed = rawInput.trim();
286
301
 
287
302
  if (trimmed.length === 0) {
288
303
  return { exitCode: 0 };
289
304
  }
290
305
 
291
- // Check if input contains pipes or redirections - use async version
292
306
  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
- );
307
+ const { parseShellPipeline } = await import("../shellParser");
308
+ const { executePipeline } = await import("../../SSHMimic/executor");
309
+
310
+ const pipeline = parseShellPipeline(trimmed);
311
+ if (!pipeline.isValid) {
312
+ return {
313
+ stderr: pipeline.error || "Syntax error",
314
+ exitCode: 1,
315
+ };
316
+ }
317
+
318
+ try {
319
+ return await executePipeline(
320
+ pipeline,
321
+ authUser,
322
+ hostname,
323
+ users,
324
+ mode,
325
+ cwd,
326
+ vfs,
327
+ );
328
+ } catch (error: unknown) {
329
+ const message =
330
+ error instanceof Error ? error.message : "Pipeline execution failed";
331
+ return { stderr: message, exitCode: 1 };
332
+ }
304
333
  }
305
334
 
306
- // Regular synchronous command execution
307
335
  const { commandName, args } = parseInput(trimmed);
308
336
  const mod = resolveModule(commandName);
309
337
 
@@ -315,7 +343,7 @@ export function runCommand(
315
343
  }
316
344
 
317
345
  try {
318
- return mod.run({
346
+ return await mod.run({
319
347
  authUser,
320
348
  hostname,
321
349
  users,
@@ -1,6 +1,6 @@
1
1
  import { defaultShellProperties } from "..";
2
2
  import type { ShellModule } from "../../types/commands";
3
- import { getArg, getFlag, ifFlag } from "./command-helpers";
3
+ import { parseArgs } from "./command-helpers";
4
4
  import { runCommand } from "./index";
5
5
 
6
6
  function parseSudoArgs(args: string[]): {
@@ -8,26 +8,16 @@ function parseSudoArgs(args: string[]): {
8
8
  loginShell: boolean;
9
9
  commandLine: string | null;
10
10
  } {
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";
11
+ const { flags, flagsWithValues, positionals } = parseArgs(args, {
12
+ flags: ["-i", "-S"],
13
+ flagsWithValue: ["-u", "--user"],
14
+ });
17
15
 
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
- }
16
+ const loginShell = flags.has("-i");
17
+ const targetUser =
18
+ flagsWithValues.get("-u") || flagsWithValues.get("--user") || "root";
19
+ const commandLine = positionals.length > 0 ? positionals.join(" ") : null;
29
20
 
30
- const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
31
21
  return { targetUser, loginShell, commandLine };
32
22
  }
33
23
  export const sudoCommand: ShellModule = {
@@ -3,11 +3,12 @@ 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
+ import { ifFlag, parseArgs } from "./command-helpers";
7
7
  import {
8
8
  assertPathAccess,
9
9
  normalizeTerminalOutput,
10
10
  resolvePath,
11
+ runHostCommand,
11
12
  stripUrlFilename,
12
13
  } from "./helpers";
13
14
 
@@ -83,36 +84,25 @@ export const wgetCommand: ShellModule = {
83
84
  name: "wget",
84
85
  params: ["[url]"],
85
86
  run: async ({ authUser, vfs, cwd, 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 = {
87
+ const { flagsWithValues, positionals } = parseArgs(args, {
97
88
  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
- }
107
- const url = inputArgs[0];
108
- const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
89
+ });
90
+ const outputPath =
91
+ flagsWithValues.get("-o") ||
92
+ flagsWithValues.get("-O") ||
93
+ flagsWithValues.get("--output") ||
94
+ flagsWithValues.get("--output-document") ||
95
+ null;
96
+ const url = positionals[0];
109
97
 
110
98
  if (!url) {
111
99
  return { stderr: "wget: missing URL", exitCode: 1 };
112
100
  }
113
101
 
102
+ const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
103
+
114
104
  if (isHelpLike) {
115
- const result = await runHostWget(inputArgs);
105
+ const result = await runHostWget(args);
116
106
  return {
117
107
  stdout: normalizeTerminalOutput(result.stdout),
118
108
  stderr: result.stderr
@@ -126,8 +116,8 @@ export const wgetCommand: ShellModule = {
126
116
  const tempFile = join(tempDir, "download");
127
117
 
128
118
  try {
129
- const hostArgs = [...inputArgs, "-O", tempFile];
130
- const result = await runHostWget(hostArgs);
119
+ const hostArgs = [...positionals, "-O", tempFile];
120
+ const result = await runHostCommand("wget", hostArgs);
131
121
 
132
122
  if (result.exitCode !== 0) {
133
123
  return {
@@ -139,7 +129,7 @@ export const wgetCommand: ShellModule = {
139
129
  }
140
130
 
141
131
  const content = await readFile(tempFile, "utf8");
142
- const target = resolvePath(cwd, outputPath ?? stripUrlFilename(url));
132
+ const target = resolvePath(cwd, outputPath || stripUrlFilename(url));
143
133
  assertPathAccess(authUser, target, "wget");
144
134
  vfs.writeFile(target, content);
145
135
 
@@ -66,6 +66,9 @@ export function startShell(
66
66
  return buildPrompt(authUser, hostname, cwdLabel);
67
67
  };
68
68
  const commandNames = Array.from(new Set(getCommandNames())).sort();
69
+ console.log(
70
+ `[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`,
71
+ );
69
72
 
70
73
  async function collectChildPids(parentPid: number): Promise<number[]> {
71
74
  try {
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { executePipeline } from "../src/SSHMimic/executor";
3
+ import { VirtualUserManager } from "../src/SSHMimic/users";
4
+ import VirtualFileSystem from "../src/VirtualFileSystem";
5
+ import { parseShellPipeline } from "../src/VirtualShell/shellParser";
6
+
7
+ describe("Pipeline parser and executor", () => {
8
+ test("parses simple pipeline", () => {
9
+ const pipeline = parseShellPipeline("echo hello | grep h");
10
+ expect(pipeline.isValid).toBe(true);
11
+ expect(pipeline.commands).toHaveLength(2);
12
+ expect(pipeline.commands[0]?.name).toBe("echo");
13
+ expect(pipeline.commands[1]?.name).toBe("grep");
14
+ });
15
+
16
+ test("handles invalid syntax", () => {
17
+ const pipeline = parseShellPipeline("echo hello |");
18
+ expect(pipeline.isValid).toBe(false);
19
+ expect(pipeline.error).toBeDefined();
20
+ });
21
+
22
+ test("executes simple pipeline", async () => {
23
+ const vfs = new VirtualFileSystem("/tmp");
24
+ const users = new VirtualUserManager(vfs, "root-pass");
25
+ const pipeline = parseShellPipeline("echo hello | grep h");
26
+
27
+ const result = await executePipeline(
28
+ pipeline,
29
+ "root",
30
+ "localhost",
31
+ users,
32
+ "shell",
33
+ "/",
34
+ vfs,
35
+ );
36
+
37
+ expect(result.exitCode).toBe(0);
38
+ expect(result.stdout).toContain("hello");
39
+ });
40
+ });