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
@@ -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
 
@@ -82,37 +83,26 @@ function runHostWget(args: string[]): Promise<{
82
83
  export const wgetCommand: ShellModule = {
83
84
  name: "wget",
84
85
  params: ["[url]"],
85
- 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 = {
86
+ run: async ({ authUser, cwd, args, shell }) => {
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,9 +129,9 @@ 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
- vfs.writeFile(target, content);
134
+ shell.vfs.writeFile(target, content);
145
135
 
146
136
  return {
147
137
  stdout: `saved ${target}`,
@@ -4,8 +4,8 @@ import type { ShellModule } from "../../types/commands";
4
4
  export const whoCommand: ShellModule = {
5
5
  name: "who",
6
6
  params: [],
7
- run: ({ users }) => {
8
- const lines = users.listActiveSessions().map((session) => {
7
+ run: ({ shell }) => {
8
+ const lines = shell.users.listActiveSessions().map((session) => {
9
9
  const loginAt = new Date(session.startedAt);
10
10
  const displayDate = Number.isNaN(loginAt.getTime())
11
11
  ? session.startedAt
@@ -1,40 +1,96 @@
1
- import type { VirtualUserManager } from "../SSHMimic/users";
1
+ import { randomBytes } from "node:crypto";
2
2
  import type { CommandContext, CommandResult } from "../types/commands";
3
3
  import type { ShellStream } from "../types/streams";
4
- import type VirtualFileSystem from "../VirtualFileSystem";
4
+ import VirtualFileSystem from "../VirtualFileSystem";
5
+ import { VirtualUserManager } from "../VirtualUserManager";
5
6
  import { createCustomCommand, registerCommand, runCommand } from "./commands";
6
7
  import { startShell } from "./shell";
7
8
 
8
9
  export interface ShellProperties {
9
10
  kernel: string;
10
- os: "Fortune GNU/Linux x64";
11
- arch: "x86_64";
11
+ os: string;
12
+ arch: string;
12
13
  }
13
14
 
14
- export const defaultShellProperties: ShellProperties = {
15
+ const defaultShellProperties: ShellProperties = {
15
16
  kernel: "1.0.0+itsrealfortune+1-amd64",
16
17
  os: "Fortune GNU/Linux x64",
17
18
  arch: "x86_64",
18
19
  };
19
20
 
21
+ function resolveRootPassword(): string {
22
+ const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
23
+ if (configured && configured.trim().length > 0) {
24
+ return configured;
25
+ }
26
+
27
+ const generated = randomBytes(18).toString("base64url");
28
+ console.warn(
29
+ `[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`,
30
+ );
31
+ return generated;
32
+ }
33
+
34
+ function resolveAutoSudoForNewUsers(): boolean {
35
+ const configured = process.env.SSH_MIMIC_AUTO_SUDO_NEW_USERS;
36
+ if (!configured) {
37
+ return true;
38
+ }
39
+
40
+ return !["0", "false", "no", "off"].includes(configured.toLowerCase());
41
+ }
42
+
43
+ /**
44
+ * Coordinates the virtual filesystem, user manager, and command runtime.
45
+ *
46
+ * Instances are used both by the SSH server facade and by the programmatic
47
+ * client API.
48
+ */
20
49
  class VirtualShell {
21
- private vfs: VirtualFileSystem;
22
- private users: VirtualUserManager;
23
- private hostname: string;
24
- public properties: ShellProperties;
50
+ basePath: string = ".";
51
+ vfs: VirtualFileSystem;
52
+ users: VirtualUserManager;
53
+ hostname: string;
54
+ properties: ShellProperties;
25
55
 
56
+ /**
57
+ * Creates a new virtual shell instance.
58
+ *
59
+ * @param hostname Virtual hostname used for prompts and idents.
60
+ * @param properties Customizable properties shown in `uname -a` and similar commands.
61
+ * @param basePath Optional base path for the virtual filesystem (defaults to process.cwd()).
62
+ */
26
63
  constructor(
27
- vfs: VirtualFileSystem,
28
- users: VirtualUserManager,
29
64
  hostname: string,
30
65
  properties?: ShellProperties,
66
+ basePath?: string,
31
67
  ) {
32
- this.vfs = vfs;
33
- this.users = users;
34
68
  this.hostname = hostname;
35
69
  this.properties = properties || defaultShellProperties;
70
+ this.basePath = basePath || ".";
71
+ this.vfs = new VirtualFileSystem(this.basePath);
72
+ this.users = new VirtualUserManager(
73
+ this.vfs,
74
+ resolveRootPassword(),
75
+ resolveAutoSudoForNewUsers(),
76
+ );
77
+ this.vfs.restoreMirror().then(() => {
78
+ this.users = new VirtualUserManager(
79
+ this.vfs,
80
+ resolveRootPassword(),
81
+ resolveAutoSudoForNewUsers(),
82
+ );
83
+ this.users.initialize();
84
+ });
36
85
  }
37
86
 
87
+ /**
88
+ * Registers a new command in the shell runtime.
89
+ *
90
+ * @param name Case-insensitive command name (no spaces).
91
+ * @param params List of parameter names for help text (no validation).
92
+ * @param callback Function invoked with command context on execution.
93
+ */
38
94
  addCommand(
39
95
  name: string,
40
96
  params: string[],
@@ -48,19 +104,26 @@ class VirtualShell {
48
104
  registerCommand(createCustomCommand(normalized, params, callback));
49
105
  }
50
106
 
107
+ /**
108
+ * Executes a command line string in the context of this shell instance.
109
+ *
110
+ * @param rawInput
111
+ * @param authUser
112
+ * @param cwd
113
+ */
51
114
  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
- );
115
+ runCommand(rawInput, authUser, this.hostname, "shell", cwd, this);
62
116
  }
63
117
 
118
+ /**
119
+ * Starts an interactive session with the shell.
120
+ *
121
+ * @param stream The stream for the interactive session.
122
+ * @param authUser The authenticated user for the session.
123
+ * @param sessionId The ID of the session.
124
+ * @param remoteAddress The address of the remote client.
125
+ */
126
+
64
127
  startInteractiveSession(
65
128
  stream: ShellStream,
66
129
  authUser: string,
@@ -73,14 +136,40 @@ class VirtualShell {
73
136
  this.properties,
74
137
  stream,
75
138
  authUser,
76
- this.vfs!,
77
139
  this.hostname,
78
- this.users!,
79
140
  sessionId,
80
141
  remoteAddress,
81
142
  terminalSize,
143
+ this,
82
144
  );
83
145
  }
146
+
147
+ /**
148
+ * Returns virtual filesystem instance after server started.
149
+ *
150
+ * @returns VirtualFileSystem or null when not started.
151
+ */
152
+ public getVfs(): VirtualFileSystem | null {
153
+ return this?.vfs ?? null;
154
+ }
155
+
156
+ /**
157
+ * Returns user manager instance after server started.
158
+ *
159
+ * @returns VirtualUserManager or null when not started.
160
+ */
161
+ public getUsers(): VirtualUserManager | null {
162
+ return this?.users ?? null;
163
+ }
164
+
165
+ /**
166
+ * Returns hostname shown in prompts and idents.
167
+ *
168
+ * @returns Configured hostname label.
169
+ */
170
+ public getHostname(): string {
171
+ return this?.hostname;
172
+ }
84
173
  }
85
174
 
86
175
  export { VirtualShell };
@@ -1,10 +1,9 @@
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 ".";
4
+ import type { ShellProperties, VirtualShell } from ".";
5
5
  import { formatLoginDate } from "../SSHMimic/loginFormat";
6
6
  import { buildPrompt } from "../SSHMimic/prompt";
7
- import type { VirtualUserManager } from "../SSHMimic/users";
8
7
  import type { ShellStream } from "../types/streams";
9
8
  import type VirtualFileSystem from "../VirtualFileSystem";
10
9
  import { getCommandNames, runCommand } from "./commands";
@@ -45,16 +44,15 @@ export function startShell(
45
44
  properties: ShellProperties,
46
45
  stream: ShellStream,
47
46
  authUser: string,
48
- vfs: VirtualFileSystem,
49
47
  hostname: string,
50
- users: VirtualUserManager,
51
48
  sessionId: string | null,
52
49
  remoteAddress = "unknown",
53
50
  terminalSize: TerminalSize = { cols: 80, rows: 24 },
51
+ shell: VirtualShell,
54
52
  ): void {
55
53
  let lineBuffer = "";
56
54
  let cursorPos = 0;
57
- let history = loadHistory(vfs);
55
+ let history = loadHistory(shell.vfs);
58
56
  let historyIndex: number | null = null;
59
57
  let historyDraft = "";
60
58
  let cwd = `/home/${authUser}`;
@@ -66,6 +64,9 @@ export function startShell(
66
64
  return buildPrompt(authUser, hostname, cwdLabel);
67
65
  };
68
66
  const commandNames = Array.from(new Set(getCommandNames())).sort();
67
+ console.log(
68
+ `[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`,
69
+ );
69
70
 
70
71
  async function collectChildPids(parentPid: number): Promise<number[]> {
71
72
  try {
@@ -167,7 +168,7 @@ export function startShell(
167
168
  if (!challenge.commandLine) {
168
169
  authUser = challenge.targetUser;
169
170
  cwd = `/home/${authUser}`;
170
- users.updateSession(sessionId, authUser, remoteAddress);
171
+ shell.users.updateSession(sessionId, authUser, remoteAddress);
171
172
  stream.write("\r\n");
172
173
  renderLine();
173
174
  return;
@@ -179,11 +180,9 @@ export function startShell(
179
180
  challenge.commandLine,
180
181
  challenge.targetUser,
181
182
  hostname,
182
- users,
183
183
  "shell",
184
184
  runCwd,
185
- defaultShellProperties,
186
- vfs,
185
+ shell,
187
186
  ),
188
187
  );
189
188
 
@@ -218,12 +217,12 @@ export function startShell(
218
217
  if (result.switchUser) {
219
218
  authUser = result.switchUser;
220
219
  cwd = result.nextCwd ?? `/home/${authUser}`;
221
- users.updateSession(sessionId, authUser, remoteAddress);
220
+ shell.users.updateSession(sessionId, authUser, remoteAddress);
222
221
  } else if (result.nextCwd) {
223
222
  cwd = result.nextCwd;
224
223
  }
225
224
 
226
- await vfs.flushMirror();
225
+ await shell.vfs.flushMirror();
227
226
  renderLine();
228
227
  }
229
228
 
@@ -237,8 +236,8 @@ export function startShell(
237
236
  if (activeSession.kind === "nano") {
238
237
  try {
239
238
  const updatedContent = await readFile(activeSession.tempPath, "utf8");
240
- vfs.writeFile(activeSession.targetPath, updatedContent);
241
- await vfs.flushMirror();
239
+ shell.vfs.writeFile(activeSession.targetPath, updatedContent);
240
+ await shell.vfs.flushMirror();
242
241
  } catch {
243
242
  // If temp file does not exist, nano exited without writing.
244
243
  }
@@ -258,7 +257,7 @@ export function startShell(
258
257
  initialContent: string,
259
258
  tempPath: string,
260
259
  ): Promise<void> {
261
- if (vfs.exists(targetPath)) {
260
+ if (shell.vfs.exists(targetPath)) {
262
261
  await writeFile(tempPath, initialContent, "utf8");
263
262
  }
264
263
 
@@ -375,13 +374,13 @@ export function startShell(
375
374
  const basePath = resolvePath(cwd, dirPart || ".");
376
375
 
377
376
  try {
378
- return vfs
377
+ return shell.vfs
379
378
  .list(basePath)
380
379
  .filter((entry) => !entry.startsWith("."))
381
380
  .filter((entry) => entry.startsWith(namePart))
382
381
  .map((entry) => {
383
382
  const fullPath = path.posix.join(basePath, entry);
384
- const st = vfs.stat(fullPath);
383
+ const st = shell.vfs.stat(fullPath);
385
384
  const suffix = st.type === "directory" ? "/" : "";
386
385
  return `${dirPart}${entry}${suffix}`;
387
386
  })
@@ -437,17 +436,17 @@ export function startShell(
437
436
  }
438
437
 
439
438
  const data = history.length > 0 ? `${history.join("\n")}\n` : "";
440
- vfs.writeFile("/virtual-env-js/.bash_history", data);
439
+ shell.vfs.writeFile("/virtual-env-js/.bash_history", data);
441
440
  }
442
441
 
443
442
  function readLastLogin(): { at: string; from: string } | null {
444
443
  const lastlogPath = `/virtual-env-js/.lastlog/${authUser}.json`;
445
- if (!vfs.exists(lastlogPath)) {
444
+ if (!shell.vfs.exists(lastlogPath)) {
446
445
  return null;
447
446
  }
448
447
 
449
448
  try {
450
- return JSON.parse(vfs.readFile(lastlogPath)) as {
449
+ return JSON.parse(shell.vfs.readFile(lastlogPath)) as {
451
450
  at: string;
452
451
  from: string;
453
452
  };
@@ -458,12 +457,12 @@ export function startShell(
458
457
 
459
458
  function writeLastLogin(nowIso: string): void {
460
459
  const dir = "/virtual-env-js/.lastlog";
461
- if (!vfs.exists(dir)) {
462
- vfs.mkdir(dir, 0o700);
460
+ if (!shell.vfs.exists(dir)) {
461
+ shell.vfs.mkdir(dir, 0o700);
463
462
  }
464
463
 
465
464
  const lastlogPath = `${dir}/${authUser}.json`;
466
- vfs.writeFile(
465
+ shell.vfs.writeFile(
467
466
  lastlogPath,
468
467
  JSON.stringify({ at: nowIso, from: remoteAddress }),
469
468
  );
@@ -534,7 +533,10 @@ export function startShell(
534
533
  if (ch === "\r" || ch === "\n") {
535
534
  const password = pendingSudo.buffer;
536
535
  pendingSudo.buffer = "";
537
- const valid = users.verifyPassword(pendingSudo.username, password);
536
+ const valid = shell.users.verifyPassword(
537
+ pendingSudo.username,
538
+ password,
539
+ );
538
540
  await finishSudoPrompt(valid);
539
541
  return;
540
542
  }
@@ -647,16 +649,7 @@ export function startShell(
647
649
 
648
650
  if (line.length > 0) {
649
651
  const result = await Promise.resolve(
650
- runCommand(
651
- line,
652
- authUser,
653
- hostname,
654
- users,
655
- "shell",
656
- cwd,
657
- defaultShellProperties,
658
- vfs,
659
- ),
652
+ runCommand(line, authUser, hostname, "shell", cwd, shell),
660
653
  );
661
654
 
662
655
  pushHistory(line);
@@ -706,12 +699,12 @@ export function startShell(
706
699
  if (result.switchUser) {
707
700
  authUser = result.switchUser;
708
701
  cwd = result.nextCwd ?? `/home/${authUser}`;
709
- users.updateSession(sessionId, authUser, remoteAddress);
702
+ shell.users.updateSession(sessionId, authUser, remoteAddress);
710
703
  lineBuffer = "";
711
704
  cursorPos = 0;
712
705
  }
713
706
 
714
- await vfs.flushMirror();
707
+ await shell.vfs.flushMirror();
715
708
  }
716
709
 
717
710
  renderLine();
@@ -26,7 +26,9 @@ export interface VirtualActiveSession {
26
26
  }
27
27
 
28
28
  /**
29
- * User, sudoers, and active session manager for SSH mimic runtime.
29
+ * Persistent user, sudoers, and active-session manager for the shell runtime.
30
+ *
31
+ * Passwords are hashed with scrypt and stored in the backing virtual filesystem.
30
32
  */
31
33
  export class VirtualUserManager {
32
34
  private readonly usersPath = "/virtual-env-js/.auth/htpasswd";
@@ -38,10 +40,11 @@ export class VirtualUserManager {
38
40
  private nextTty = 0;
39
41
 
40
42
  /**
41
- * Creates user manager instance.
43
+ * Creates a user manager instance backed by a virtual filesystem.
42
44
  *
43
45
  * @param vfs Backing virtual filesystem used for persistence.
44
- * @param defaultRootPassword Initial root password used when root missing.
46
+ * @param defaultRootPassword Initial root password used when root is created.
47
+ * @param autoSudoForNewUsers Whether newly created users are added to sudoers.
45
48
  */
46
49
  constructor(
47
50
  private readonly vfs: VirtualFileSystem,
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SshClient } from "./SSHMimic/client";
2
2
  import { SshMimic } from "./SSHMimic/index";
3
- import { VirtualUserManager } from "./SSHMimic/users";
4
3
  import VirtualFileSystem from "./VirtualFileSystem";
5
- import type { VirtualShell } from "./VirtualShell";
4
+ import { VirtualShell } from "./VirtualShell";
5
+ import { VirtualUserManager } from "./VirtualUserManager";
6
6
 
7
7
  export type {
8
8
  CommandContext,
@@ -32,9 +32,9 @@ export type {
32
32
  export {
33
33
  SshClient,
34
34
  VirtualFileSystem,
35
- SshMimic as VirtualMachine,
35
+ VirtualShell,
36
+ SshMimic as VirtualSshServer,
36
37
  VirtualUserManager,
37
- type VirtualShell,
38
38
  };
39
39
 
40
40
  export {
package/src/standalone.ts CHANGED
@@ -1,21 +1,26 @@
1
- import { VirtualMachine } from ".";
1
+ import { VirtualShell, VirtualSshServer } from ".";
2
2
 
3
- const sshHostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
- const sshMimic = new VirtualMachine({ port: 2222, hostname: sshHostname });
3
+ const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
+ const virtualShell = new VirtualShell(hostname);
5
5
 
6
- sshMimic
6
+ virtualShell.addCommand("demo", [], () => {
7
+ return {
8
+ stdout: "This is a demo command. It does nothing useful.",
9
+ exitCode: 0,
10
+ };
11
+ });
12
+
13
+ new VirtualSshServer({
14
+ port: 2222,
15
+ hostname,
16
+ shell: virtualShell,
17
+ })
7
18
  .start()
8
19
  .then((port: number) => {
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
- }
20
+ // if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
21
+ // else {
22
+ console.log(`SSH Mimic initialized. Listening on port ${port}.`);
23
+ // }
19
24
  })
20
25
  .catch((error: unknown) => {
21
26
  console.error("Failed to start SSH Mimic:", error);
@@ -1,12 +1,8 @@
1
1
  /** Command invocation mode used by shell runtime. */
2
2
  export type CommandMode = "shell" | "exec";
3
3
 
4
- import type {
5
- VirtualActiveSession,
6
- VirtualUserManager,
7
- } from "../SSHMimic/users";
8
- import type VirtualFileSystem from "../VirtualFileSystem";
9
- import type { ShellProperties } from "../VirtualShell";
4
+ import type { VirtualShell } from "../VirtualShell";
5
+ import type { VirtualActiveSession } from "../VirtualUserManager";
10
6
 
11
7
  /**
12
8
  * Normalized command execution output.
@@ -67,8 +63,6 @@ export interface CommandContext {
67
63
  authUser: string;
68
64
  /** Virtual hostname shown in prompt and banners. */
69
65
  hostname: string;
70
- /** User and session manager instance. */
71
- users: VirtualUserManager;
72
66
  /** Snapshot of currently active user sessions. */
73
67
  activeSessions: VirtualActiveSession[];
74
68
  /** Original unparsed command line input. */
@@ -78,13 +72,11 @@ export interface CommandContext {
78
72
  /** Tokenized arguments excluding command name. */
79
73
  args: string[];
80
74
  /** Virtual shell instance. */
81
- shellProps: ShellProperties;
75
+ shell: VirtualShell;
82
76
  /** Optional stdin payload (used by pipes/redirections). */
83
77
  stdin?: string;
84
78
  /** Current working directory for command execution. */
85
79
  cwd: string;
86
- /** Virtual filesystem instance for IO operations. */
87
- vfs: VirtualFileSystem;
88
80
  }
89
81
 
90
82
  /** Contract implemented by each shell command module. */
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { VirtualShell } from "../src";
3
+ import { executePipeline } from "../src/SSHMimic/executor";
4
+ import { parseShellPipeline } from "../src/VirtualShell/shellParser";
5
+
6
+ describe("Pipeline parser and executor", () => {
7
+ test("parses simple pipeline", () => {
8
+ const pipeline = parseShellPipeline("echo hello | grep h");
9
+ expect(pipeline.isValid).toBe(true);
10
+ expect(pipeline.commands).toHaveLength(2);
11
+ expect(pipeline.commands[0]?.name).toBe("echo");
12
+ expect(pipeline.commands[1]?.name).toBe("grep");
13
+ });
14
+
15
+ test("handles invalid syntax", () => {
16
+ const pipeline = parseShellPipeline("echo hello |");
17
+ expect(pipeline.isValid).toBe(false);
18
+ expect(pipeline.error).toBeDefined();
19
+ });
20
+
21
+ test("executes simple pipeline", async () => {
22
+ const shell = new VirtualShell("localhost");
23
+ const pipeline = parseShellPipeline("echo hello | grep h");
24
+
25
+ const result = await executePipeline(
26
+ pipeline,
27
+ "root",
28
+ "localhost",
29
+ "shell",
30
+ "/",
31
+ shell
32
+ );
33
+
34
+ expect(result.exitCode).toBe(0);
35
+ expect(result.stdout).toContain("hello");
36
+ });
37
+ });
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
2
2
  import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { VirtualUserManager } from "../src/SSHMimic/users";
6
5
  import VirtualFileSystem from "../src/VirtualFileSystem";
6
+ import { VirtualUserManager } from "../src/VirtualUserManager";
7
7
 
8
8
  async function withTempVfs(
9
9
  run: (vfs: VirtualFileSystem) => Promise<void>,