typescript-virtual-container 1.2.5 → 1.2.7

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 (248) hide show
  1. package/README.md +387 -193
  2. package/benchmark-results.txt +21 -21
  3. package/biome.json +1 -1
  4. package/bun.lock +15 -41
  5. package/dist/SSHMimic/exec.js +2 -2
  6. package/dist/SSHMimic/executor.d.ts +6 -7
  7. package/dist/SSHMimic/executor.d.ts.map +1 -1
  8. package/dist/SSHMimic/executor.js +77 -60
  9. package/dist/SSHMimic/index.d.ts.map +1 -1
  10. package/dist/SSHMimic/index.js +6 -20
  11. package/dist/SSHMimic/sftp.d.ts.map +1 -1
  12. package/dist/SSHMimic/sftp.js +14 -0
  13. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  14. package/dist/VirtualFileSystem/index.js +13 -36
  15. package/dist/VirtualShell/shell.d.ts.map +1 -1
  16. package/dist/VirtualShell/shell.js +19 -2
  17. package/dist/VirtualShell/shellParser.d.ts +20 -2
  18. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  19. package/dist/VirtualShell/shellParser.js +229 -120
  20. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  21. package/dist/commands/adduser.d.ts.map +1 -1
  22. package/dist/commands/adduser.js +2 -0
  23. package/dist/commands/awk.d.ts +3 -0
  24. package/dist/commands/awk.d.ts.map +1 -0
  25. package/dist/commands/awk.js +29 -0
  26. package/dist/commands/base64.d.ts +3 -0
  27. package/dist/commands/base64.d.ts.map +1 -0
  28. package/dist/commands/base64.js +20 -0
  29. package/dist/commands/cat.d.ts.map +1 -1
  30. package/dist/commands/cat.js +2 -0
  31. package/dist/commands/cd.d.ts.map +1 -1
  32. package/dist/commands/cd.js +2 -0
  33. package/dist/commands/chmod.d.ts.map +1 -1
  34. package/dist/commands/chmod.js +2 -0
  35. package/dist/commands/clear.d.ts.map +1 -1
  36. package/dist/commands/clear.js +4 -1
  37. package/dist/commands/cp.d.ts.map +1 -1
  38. package/dist/commands/cp.js +2 -0
  39. package/dist/commands/curl.d.ts.map +1 -1
  40. package/dist/commands/curl.js +2 -0
  41. package/dist/commands/cut.d.ts +3 -0
  42. package/dist/commands/cut.d.ts.map +1 -0
  43. package/dist/commands/cut.js +27 -0
  44. package/dist/commands/date.d.ts +3 -0
  45. package/dist/commands/date.d.ts.map +1 -0
  46. package/dist/commands/date.js +22 -0
  47. package/dist/commands/deluser.d.ts.map +1 -1
  48. package/dist/commands/deluser.js +2 -0
  49. package/dist/commands/df.d.ts +3 -0
  50. package/dist/commands/df.d.ts.map +1 -0
  51. package/dist/commands/df.js +16 -0
  52. package/dist/commands/diff.d.ts +3 -0
  53. package/dist/commands/diff.d.ts.map +1 -0
  54. package/dist/commands/diff.js +40 -0
  55. package/dist/commands/du.d.ts +3 -0
  56. package/dist/commands/du.d.ts.map +1 -0
  57. package/dist/commands/du.js +39 -0
  58. package/dist/commands/echo.d.ts.map +1 -1
  59. package/dist/commands/echo.js +2 -0
  60. package/dist/commands/env.d.ts +1 -0
  61. package/dist/commands/env.d.ts.map +1 -1
  62. package/dist/commands/env.js +6 -14
  63. package/dist/commands/export.d.ts.map +1 -1
  64. package/dist/commands/export.js +11 -21
  65. package/dist/commands/find.d.ts.map +1 -1
  66. package/dist/commands/find.js +2 -0
  67. package/dist/commands/grep.d.ts.map +1 -1
  68. package/dist/commands/grep.js +4 -7
  69. package/dist/commands/groups.d.ts +3 -0
  70. package/dist/commands/groups.d.ts.map +1 -0
  71. package/dist/commands/groups.js +12 -0
  72. package/dist/commands/gzip.d.ts +4 -0
  73. package/dist/commands/gzip.d.ts.map +1 -0
  74. package/dist/commands/gzip.js +40 -0
  75. package/dist/commands/head.d.ts.map +1 -1
  76. package/dist/commands/head.js +2 -0
  77. package/dist/commands/help.d.ts +1 -1
  78. package/dist/commands/help.d.ts.map +1 -1
  79. package/dist/commands/help.js +66 -3
  80. package/dist/commands/hostname.d.ts.map +1 -1
  81. package/dist/commands/hostname.js +2 -0
  82. package/dist/commands/htop.d.ts.map +1 -1
  83. package/dist/commands/htop.js +2 -0
  84. package/dist/commands/id.d.ts +3 -0
  85. package/dist/commands/id.d.ts.map +1 -0
  86. package/dist/commands/id.js +14 -0
  87. package/dist/commands/index.d.ts +5 -2
  88. package/dist/commands/index.d.ts.map +1 -1
  89. package/dist/commands/index.js +89 -62
  90. package/dist/commands/kill.d.ts +3 -0
  91. package/dist/commands/kill.d.ts.map +1 -0
  92. package/dist/commands/kill.js +13 -0
  93. package/dist/commands/ln.d.ts.map +1 -1
  94. package/dist/commands/ln.js +2 -0
  95. package/dist/commands/ls.d.ts.map +1 -1
  96. package/dist/commands/ls.js +2 -0
  97. package/dist/commands/mkdir.d.ts.map +1 -1
  98. package/dist/commands/mkdir.js +2 -0
  99. package/dist/commands/mv.d.ts.map +1 -1
  100. package/dist/commands/mv.js +2 -0
  101. package/dist/commands/nano.d.ts.map +1 -1
  102. package/dist/commands/nano.js +2 -0
  103. package/dist/commands/neofetch.d.ts.map +1 -1
  104. package/dist/commands/neofetch.js +2 -0
  105. package/dist/commands/passwd.d.ts.map +1 -1
  106. package/dist/commands/passwd.js +2 -0
  107. package/dist/commands/ping.d.ts +3 -0
  108. package/dist/commands/ping.d.ts.map +1 -0
  109. package/dist/commands/ping.js +18 -0
  110. package/dist/commands/ps.d.ts +3 -0
  111. package/dist/commands/ps.d.ts.map +1 -0
  112. package/dist/commands/ps.js +17 -0
  113. package/dist/commands/pwd.d.ts.map +1 -1
  114. package/dist/commands/pwd.js +2 -0
  115. package/dist/commands/rm.d.ts.map +1 -1
  116. package/dist/commands/rm.js +2 -0
  117. package/dist/commands/sed.d.ts +3 -0
  118. package/dist/commands/sed.d.ts.map +1 -0
  119. package/dist/commands/sed.js +47 -0
  120. package/dist/commands/set.d.ts +3 -0
  121. package/dist/commands/set.d.ts.map +1 -1
  122. package/dist/commands/set.js +19 -46
  123. package/dist/commands/sh.d.ts +0 -1
  124. package/dist/commands/sh.d.ts.map +1 -1
  125. package/dist/commands/sh.js +229 -35
  126. package/dist/commands/sleep.d.ts +3 -0
  127. package/dist/commands/sleep.d.ts.map +1 -0
  128. package/dist/commands/sleep.js +13 -0
  129. package/dist/commands/sort.d.ts +3 -0
  130. package/dist/commands/sort.d.ts.map +1 -0
  131. package/dist/commands/sort.js +37 -0
  132. package/dist/commands/su.d.ts.map +1 -1
  133. package/dist/commands/su.js +2 -0
  134. package/dist/commands/sudo.d.ts.map +1 -1
  135. package/dist/commands/sudo.js +2 -0
  136. package/dist/commands/tail.d.ts.map +1 -1
  137. package/dist/commands/tail.js +2 -0
  138. package/dist/commands/tar.d.ts +3 -0
  139. package/dist/commands/tar.d.ts.map +1 -0
  140. package/dist/commands/tar.js +64 -0
  141. package/dist/commands/tee.d.ts +3 -0
  142. package/dist/commands/tee.d.ts.map +1 -0
  143. package/dist/commands/tee.js +29 -0
  144. package/dist/commands/touch.d.ts.map +1 -1
  145. package/dist/commands/touch.js +2 -0
  146. package/dist/commands/tr.d.ts +3 -0
  147. package/dist/commands/tr.d.ts.map +1 -0
  148. package/dist/commands/tr.js +24 -0
  149. package/dist/commands/tree.d.ts.map +1 -1
  150. package/dist/commands/tree.js +2 -0
  151. package/dist/commands/uname.d.ts +3 -0
  152. package/dist/commands/uname.d.ts.map +1 -0
  153. package/dist/commands/uname.js +21 -0
  154. package/dist/commands/uniq.d.ts +3 -0
  155. package/dist/commands/uniq.d.ts.map +1 -0
  156. package/dist/commands/uniq.js +33 -0
  157. package/dist/commands/unset.d.ts.map +1 -1
  158. package/dist/commands/unset.js +6 -10
  159. package/dist/commands/wc.d.ts.map +1 -1
  160. package/dist/commands/wc.js +2 -0
  161. package/dist/commands/wget.d.ts.map +1 -1
  162. package/dist/commands/wget.js +2 -0
  163. package/dist/commands/who.d.ts.map +1 -1
  164. package/dist/commands/who.js +2 -0
  165. package/dist/commands/whoami.d.ts.map +1 -1
  166. package/dist/commands/whoami.js +2 -0
  167. package/dist/commands/xargs.d.ts +3 -0
  168. package/dist/commands/xargs.d.ts.map +1 -0
  169. package/dist/commands/xargs.js +16 -0
  170. package/dist/types/commands.d.ts +13 -0
  171. package/dist/types/commands.d.ts.map +1 -1
  172. package/dist/types/pipeline.d.ts +20 -0
  173. package/dist/types/pipeline.d.ts.map +1 -1
  174. package/package.json +3 -3
  175. package/src/SSHMimic/exec.ts +2 -2
  176. package/src/SSHMimic/executor.ts +95 -98
  177. package/src/SSHMimic/index.ts +15 -49
  178. package/src/SSHMimic/sftp.ts +15 -0
  179. package/src/VirtualFileSystem/index.ts +27 -75
  180. package/src/VirtualShell/shell.ts +19 -2
  181. package/src/VirtualShell/shellParser.ts +202 -168
  182. package/src/VirtualUserManager/index.ts +2 -7
  183. package/src/commands/adduser.ts +2 -0
  184. package/src/commands/awk.ts +30 -0
  185. package/src/commands/base64.ts +18 -0
  186. package/src/commands/cat.ts +2 -0
  187. package/src/commands/cd.ts +2 -0
  188. package/src/commands/chmod.ts +2 -0
  189. package/src/commands/clear.ts +4 -1
  190. package/src/commands/cp.ts +2 -0
  191. package/src/commands/curl.ts +2 -0
  192. package/src/commands/cut.ts +29 -0
  193. package/src/commands/date.ts +24 -0
  194. package/src/commands/deluser.ts +2 -0
  195. package/src/commands/df.ts +18 -0
  196. package/src/commands/diff.ts +29 -0
  197. package/src/commands/du.ts +39 -0
  198. package/src/commands/echo.ts +2 -0
  199. package/src/commands/env.ts +7 -16
  200. package/src/commands/export.ts +11 -24
  201. package/src/commands/find.ts +2 -0
  202. package/src/commands/grep.ts +4 -7
  203. package/src/commands/groups.ts +14 -0
  204. package/src/commands/gzip.ts +31 -0
  205. package/src/commands/head.ts +2 -0
  206. package/src/commands/help.ts +72 -3
  207. package/src/commands/hostname.ts +2 -0
  208. package/src/commands/htop.ts +2 -0
  209. package/src/commands/id.ts +16 -0
  210. package/src/commands/index.ts +98 -99
  211. package/src/commands/kill.ts +14 -0
  212. package/src/commands/ln.ts +2 -0
  213. package/src/commands/ls.ts +2 -0
  214. package/src/commands/mkdir.ts +2 -0
  215. package/src/commands/mv.ts +2 -0
  216. package/src/commands/nano.ts +2 -0
  217. package/src/commands/neofetch.ts +2 -0
  218. package/src/commands/passwd.ts +2 -0
  219. package/src/commands/ping.ts +20 -0
  220. package/src/commands/ps.ts +19 -0
  221. package/src/commands/pwd.ts +2 -0
  222. package/src/commands/rm.ts +2 -0
  223. package/src/commands/sed.ts +45 -0
  224. package/src/commands/set.ts +19 -50
  225. package/src/commands/sh.ts +193 -43
  226. package/src/commands/sleep.ts +14 -0
  227. package/src/commands/sort.ts +37 -0
  228. package/src/commands/su.ts +2 -0
  229. package/src/commands/sudo.ts +2 -0
  230. package/src/commands/tail.ts +2 -0
  231. package/src/commands/tar.ts +58 -0
  232. package/src/commands/tee.ts +25 -0
  233. package/src/commands/touch.ts +2 -0
  234. package/src/commands/tr.ts +24 -0
  235. package/src/commands/tree.ts +2 -0
  236. package/src/commands/uname.ts +20 -0
  237. package/src/commands/uniq.ts +28 -0
  238. package/src/commands/unset.ts +5 -12
  239. package/src/commands/wc.ts +2 -0
  240. package/src/commands/wget.ts +2 -0
  241. package/src/commands/who.ts +2 -0
  242. package/src/commands/whoami.ts +2 -0
  243. package/src/commands/xargs.ts +17 -0
  244. package/src/types/commands.ts +14 -0
  245. package/src/types/pipeline.ts +23 -0
  246. package/standalone.js +92 -64
  247. package/standalone.js.map +4 -4
  248. package/tests/users.test.ts +5 -34
@@ -1,13 +1,79 @@
1
1
  import { runCommand as runSingleCommand } from "../commands";
2
2
  import { resolvePath } from "../commands/helpers";
3
- import type { CommandMode, CommandResult } from "../types/commands";
4
- import type { Pipeline, PipelineCommand } from "../types/pipeline";
3
+ import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
4
+ import type { Pipeline, PipelineCommand, Script, Statement } from "../types/pipeline";
5
5
  import type { VirtualShell } from "../VirtualShell";
6
6
 
7
- /**
8
- * Execute a parsed pipeline, chaining commands and handling redirections.
9
- * Manages stdout/stderr flow between commands and file I/O.
10
- */
7
+ // ── Script executor (handles &&/||/;) ────────────────────────────────────────
8
+
9
+ export async function executeScript(
10
+ script: Script,
11
+ authUser: string,
12
+ hostname: string,
13
+ mode: CommandMode,
14
+ cwd: string,
15
+ shell: VirtualShell,
16
+ env: ShellEnv,
17
+ ): Promise<CommandResult> {
18
+ if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
19
+
20
+ let lastResult: CommandResult = { exitCode: 0 };
21
+
22
+ for (const stmt of script.statements) {
23
+ // Decide whether to run this statement based on previous op
24
+ lastResult = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
25
+ env.lastExitCode = lastResult.exitCode ?? 0;
26
+
27
+ // Propagate session-control signals
28
+ if (lastResult.closeSession || lastResult.switchUser || lastResult.nextCwd) {
29
+ break;
30
+ }
31
+ }
32
+
33
+ return lastResult;
34
+ }
35
+
36
+ /** Execute statements connected by &&/||/; */
37
+ export async function executeStatements(
38
+ statements: Statement[],
39
+ authUser: string,
40
+ hostname: string,
41
+ mode: CommandMode,
42
+ cwd: string,
43
+ shell: VirtualShell,
44
+ env: ShellEnv,
45
+ ): Promise<CommandResult> {
46
+ let last: CommandResult = { exitCode: 0 };
47
+ let i = 0;
48
+
49
+ while (i < statements.length) {
50
+ const stmt = statements[i]!;
51
+ last = await executePipeline(stmt.pipeline, authUser, hostname, mode, cwd, shell, env);
52
+ env.lastExitCode = last.exitCode ?? 0;
53
+
54
+ if (last.closeSession || last.switchUser) return last;
55
+
56
+ const op = stmt.op;
57
+ if (!op || op === ";") {
58
+ // always run next
59
+ } else if (op === "&&") {
60
+ if ((last.exitCode ?? 0) !== 0) {
61
+ // skip until next ; or end
62
+ while (i < statements.length && statements[i]?.op === "&&") i++;
63
+ }
64
+ } else if (op === "||") {
65
+ if ((last.exitCode ?? 0) === 0) {
66
+ // skip until next ; or end
67
+ while (i < statements.length && statements[i]?.op === "||") i++;
68
+ }
69
+ }
70
+ i++;
71
+ }
72
+ return last;
73
+ }
74
+
75
+ // ── Pipeline executor ─────────────────────────────────────────────────────────
76
+
11
77
  export async function executePipeline(
12
78
  pipeline: Pipeline,
13
79
  authUser: string,
@@ -15,37 +81,26 @@ export async function executePipeline(
15
81
  mode: CommandMode,
16
82
  cwd: string,
17
83
  shell: VirtualShell,
84
+ env?: ShellEnv,
18
85
  ): Promise<CommandResult> {
19
- if (pipeline.commands.length === 0) {
20
- return { exitCode: 0 };
21
- }
86
+ if (!pipeline.isValid) return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
87
+ if (pipeline.commands.length === 0) return { exitCode: 0 };
88
+
89
+ const shellEnv: ShellEnv = env ?? { vars: {}, lastExitCode: 0 };
22
90
 
23
91
  if (pipeline.commands.length === 1) {
24
- // Single command with possible redirections
25
92
  return executeSingleCommandWithRedirections(
26
93
  pipeline.commands[0] as PipelineCommand,
27
- authUser,
28
- hostname,
29
- mode,
30
- cwd,
31
- shell,
94
+ authUser, hostname, mode, cwd, shell, shellEnv,
32
95
  );
33
96
  }
34
97
 
35
- // Multiple commands in a pipeline
36
98
  return executePipelineChain(
37
99
  pipeline.commands as PipelineCommand[],
38
- authUser,
39
- hostname,
40
- mode,
41
- cwd,
42
- shell,
100
+ authUser, hostname, mode, cwd, shell, shellEnv,
43
101
  );
44
102
  }
45
103
 
46
- /**
47
- * Execute a single command with input/output redirections
48
- */
49
104
  async function executeSingleCommandWithRedirections(
50
105
  cmd: PipelineCommand,
51
106
  authUser: string,
@@ -53,66 +108,37 @@ async function executeSingleCommandWithRedirections(
53
108
  mode: CommandMode,
54
109
  cwd: string,
55
110
  shell: VirtualShell,
111
+ env: ShellEnv,
56
112
  ): Promise<CommandResult> {
57
- // Prepare input if input file specified
58
113
  let stdin: string | undefined;
59
114
  if (cmd.inputFile) {
60
115
  const inputPath = resolvePath(cwd, cmd.inputFile);
61
- try {
62
- stdin = shell.vfs.readFile(inputPath);
63
- } catch {
64
- return {
65
- stderr: `cat: ${cmd.inputFile}: No such file or directory`,
66
- exitCode: 1,
67
- };
68
- }
116
+ try { stdin = shell.vfs.readFile(inputPath); }
117
+ catch { return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 }; }
69
118
  }
70
119
 
71
- // Build raw input for the command
72
120
  const rawInput = [cmd.name, ...cmd.args].join(" ");
121
+ const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, stdin, env);
73
122
 
74
- // Run the command with potential input
75
- const result = await runSingleCommand(
76
- rawInput,
77
- authUser,
78
- hostname,
79
- mode,
80
- cwd,
81
- shell,
82
- stdin,
83
- );
84
-
85
- // Handle output redirection
86
123
  if (cmd.outputFile) {
87
124
  const outputPath = resolvePath(cwd, cmd.outputFile);
88
125
  const output = result.stdout || "";
89
126
  try {
90
127
  if (cmd.appendOutput) {
91
- try {
92
- const existing = shell.vfs.readFile(outputPath);
93
- shell.writeFileAsUser(authUser, outputPath, existing + output);
94
- } catch {
95
- shell.writeFileAsUser(authUser, outputPath, output);
96
- }
128
+ const existing = (() => { try { return shell.vfs.readFile(outputPath); } catch { return ""; } })();
129
+ shell.writeFileAsUser(authUser, outputPath, existing + output);
97
130
  } else {
98
131
  shell.writeFileAsUser(authUser, outputPath, output);
99
132
  }
100
133
  return { ...result, stdout: "" };
101
134
  } catch {
102
- return {
103
- ...result,
104
- stderr: `Failed to write to ${cmd.outputFile}`,
105
- exitCode: 1,
106
- };
135
+ return { ...result, stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
107
136
  }
108
137
  }
109
138
 
110
139
  return result;
111
140
  }
112
141
 
113
- /**
114
- * Execute a chain of commands connected by pipes
115
- */
116
142
  async function executePipelineChain(
117
143
  commands: PipelineCommand[],
118
144
  authUser: string,
@@ -120,6 +146,7 @@ async function executePipelineChain(
120
146
  mode: CommandMode,
121
147
  cwd: string,
122
148
  shell: VirtualShell,
149
+ env: ShellEnv,
123
150
  ): Promise<CommandResult> {
124
151
  let currentOutput = "";
125
152
  let exitCode = 0;
@@ -127,66 +154,36 @@ async function executePipelineChain(
127
154
  for (let i = 0; i < commands.length; i++) {
128
155
  const cmd = commands[i] as PipelineCommand;
129
156
 
130
- // Handle input file for first command
131
157
  if (i === 0 && cmd.inputFile) {
132
158
  const inputPath = resolvePath(cwd, cmd.inputFile);
133
- try {
134
- currentOutput = shell.vfs.readFile(inputPath);
135
- } catch {
136
- return {
137
- stderr: `cat: ${cmd.inputFile}: No such file or directory`,
138
- exitCode: 1,
139
- };
140
- }
159
+ try { currentOutput = shell.vfs.readFile(inputPath); }
160
+ catch { return { stderr: `${cmd.inputFile}: No such file or directory`, exitCode: 1 }; }
141
161
  }
142
162
 
143
- // Build raw input
144
163
  const rawInput = [cmd.name, ...cmd.args].join(" ");
145
-
146
- // Create a modified context that might accept stdin
147
- // For now, we'll append input as an additional arg for commands that support it
148
- const result = await runSingleCommand(
149
- rawInput,
150
- authUser,
151
- hostname,
152
- mode,
153
- cwd,
154
- shell,
155
- currentOutput,
156
- );
157
-
164
+ const result = await runSingleCommand(rawInput, authUser, hostname, mode, cwd, shell, currentOutput, env);
158
165
  exitCode = result.exitCode ?? 0;
159
166
 
160
- // Handle output redirection (only for last command)
161
167
  if (i === commands.length - 1 && cmd.outputFile) {
162
168
  const outputPath = resolvePath(cwd, cmd.outputFile);
163
169
  const output = result.stdout || "";
164
170
  try {
165
171
  if (cmd.appendOutput) {
166
- try {
167
- const existing = shell.vfs.readFile(outputPath);
168
- shell.writeFileAsUser(authUser, outputPath, existing + output);
169
- } catch {
170
- shell.writeFileAsUser(authUser, outputPath, output);
171
- }
172
+ const existing = (() => { try { return shell.vfs.readFile(outputPath); } catch { return ""; } })();
173
+ shell.writeFileAsUser(authUser, outputPath, existing + output);
172
174
  } else {
173
175
  shell.writeFileAsUser(authUser, outputPath, output);
174
176
  }
175
177
  currentOutput = "";
176
178
  } catch {
177
- return {
178
- stderr: `Failed to write to ${cmd.outputFile}`,
179
- exitCode: 1,
180
- };
179
+ return { stderr: `Failed to write to ${cmd.outputFile}`, exitCode: 1 };
181
180
  }
182
181
  } else {
183
- // Pass output to next command
184
182
  currentOutput = result.stdout || "";
185
183
  }
186
184
 
187
- if (result.stderr && exitCode !== 0) {
188
- return { stderr: result.stderr, exitCode };
189
- }
185
+ if (result.stderr && exitCode !== 0) return { stderr: result.stderr, exitCode };
186
+ if (result.closeSession || result.switchUser) return result;
190
187
  }
191
188
 
192
189
  return { stdout: currentOutput, exitCode };
@@ -140,11 +140,7 @@ class SshMimic extends EventEmitter {
140
140
 
141
141
  // Rate-limit check
142
142
  if (this.isLockedOut(remoteAddress)) {
143
- this.emit("auth:failure", {
144
- username: candidateUser,
145
- remoteAddress,
146
- reason: "lockout",
147
- });
143
+ this.emit("auth:failure", { username: candidateUser, remoteAddress, reason: "lockout" });
148
144
  ctx.reject();
149
145
  return;
150
146
  }
@@ -156,10 +152,7 @@ class SshMimic extends EventEmitter {
156
152
  `User ${candidateUser} has no password set, allowing login without verification`,
157
153
  );
158
154
  authUser = candidateUser;
159
- sessionId = shell.users.registerSession(
160
- authUser,
161
- remoteAddress,
162
- ).id;
155
+ sessionId = shell.users.registerSession(authUser, remoteAddress).id;
163
156
  this.recordSuccess(remoteAddress);
164
157
  this.emit("auth:success", { username: authUser, remoteAddress });
165
158
  this.ensureHomeDir(authUser);
@@ -173,10 +166,7 @@ class SshMimic extends EventEmitter {
173
166
  !shell.users.verifyPassword(candidateUser, ctx.password)
174
167
  ) {
175
168
  this.recordFailure(remoteAddress);
176
- this.emit("auth:failure", {
177
- username: candidateUser,
178
- remoteAddress,
179
- });
169
+ this.emit("auth:failure", { username: candidateUser, remoteAddress });
180
170
  ctx.reject();
181
171
  return;
182
172
  }
@@ -202,16 +192,13 @@ class SshMimic extends EventEmitter {
202
192
  const incomingKey = ctx.key;
203
193
  const keyMatches = authorizedKeys.some(
204
194
  (k) =>
205
- k.algo === incomingKey.algo && k.data.equals(incomingKey.data),
195
+ k.algo === incomingKey.algo &&
196
+ k.data.equals(incomingKey.data),
206
197
  );
207
198
 
208
199
  if (!keyMatches) {
209
200
  this.recordFailure(remoteAddress);
210
- this.emit("auth:failure", {
211
- username: candidateUser,
212
- remoteAddress,
213
- method: "publickey",
214
- });
201
+ this.emit("auth:failure", { username: candidateUser, remoteAddress, method: "publickey" });
215
202
  ctx.reject();
216
203
  return;
217
204
  }
@@ -219,16 +206,9 @@ class SshMimic extends EventEmitter {
219
206
  // Key matched — if this is a signature check step, accept
220
207
  if (ctx.signature) {
221
208
  authUser = candidateUser;
222
- sessionId = shell.users.registerSession(
223
- authUser,
224
- remoteAddress,
225
- ).id;
209
+ sessionId = shell.users.registerSession(authUser, remoteAddress).id;
226
210
  this.recordSuccess(remoteAddress);
227
- this.emit("auth:success", {
228
- username: authUser,
229
- remoteAddress,
230
- method: "publickey",
231
- });
211
+ this.emit("auth:success", { username: authUser, remoteAddress, method: "publickey" });
232
212
  this.ensureHomeDir(authUser);
233
213
  ctx.accept();
234
214
  } else {
@@ -258,35 +238,20 @@ class SshMimic extends EventEmitter {
258
238
  acceptPty();
259
239
  });
260
240
 
261
- session.on(
262
- "window-change",
263
- (_acceptChange, _rejectChange, info) => {
264
- terminalSize.cols = info?.cols ?? terminalSize.cols;
265
- terminalSize.rows = info?.rows ?? terminalSize.rows;
266
- },
267
- );
241
+ session.on("window-change", (_acceptChange, _rejectChange, info) => {
242
+ terminalSize.cols = info?.cols ?? terminalSize.cols;
243
+ terminalSize.rows = info?.rows ?? terminalSize.rows;
244
+ });
268
245
 
269
246
  session.on("shell", (acceptShell) => {
270
247
  const stream = acceptShell();
271
- shell?.startInteractiveSession(
272
- stream,
273
- authUser,
274
- sessionId,
275
- remoteAddress,
276
- terminalSize,
277
- );
248
+ shell?.startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize);
278
249
  });
279
250
 
280
251
  session.on("exec", (acceptExec, _rejectExec, info) => {
281
252
  const stream = acceptExec();
282
253
  if (stream) {
283
- runExec(
284
- stream,
285
- info.command.trim(),
286
- authUser,
287
- shell.hostname,
288
- shell,
289
- );
254
+ runExec(stream, info.command.trim(), authUser, shell.hostname, shell);
290
255
  }
291
256
  });
292
257
  });
@@ -328,3 +293,4 @@ class SshMimic extends EventEmitter {
328
293
 
329
294
  export { SftpMimic } from "./sftp";
330
295
  export { SshMimic };
296
+
@@ -253,6 +253,14 @@ export class SftpMimic extends EventEmitter {
253
253
  );
254
254
 
255
255
  if (ctx.method === "password") {
256
+ // If no password is set for the user, allow login without verification
257
+ if (!this.getUsers().hasPassword(candidateUser)) {
258
+ acceptSession(candidateUser);
259
+ this.emit("auth:success", { username: authUser, remoteAddress });
260
+ ctx.accept();
261
+ return;
262
+ }
263
+
256
264
  if (
257
265
  !this.getUsers().verifyPassword(candidateUser, ctx.password ?? "")
258
266
  ) {
@@ -272,6 +280,13 @@ export class SftpMimic extends EventEmitter {
272
280
 
273
281
  if (ctx.method === "keyboard-interactive") {
274
282
  const keyboardCtx = ctx as KeyboardAuthContext;
283
+ // If no password is set, accept immediately
284
+ if (!this.getUsers().hasPassword(candidateUser)) {
285
+ acceptSession(candidateUser);
286
+ this.emit("auth:success", { username: authUser, remoteAddress });
287
+ keyboardCtx.accept();
288
+ return;
289
+ }
275
290
  keyboardCtx.prompt(
276
291
  [{ prompt: "Password: ", echo: false }],
277
292
  (answers) => {
@@ -210,11 +210,7 @@ class VirtualFileSystem extends EventEmitter {
210
210
  public mkdir(targetPath: string, mode: number = 0o755): void {
211
211
  const normalized = normalizePath(targetPath);
212
212
  const existing = (() => {
213
- try {
214
- return getNode(this.root, normalized);
215
- } catch {
216
- return null;
217
- }
213
+ try { return getNode(this.root, normalized); } catch { return null; }
218
214
  })();
219
215
  if (existing && existing.type !== "directory") {
220
216
  throw new Error(
@@ -305,9 +301,7 @@ class VirtualFileSystem extends EventEmitter {
305
301
  try {
306
302
  getNode(this.root, normalizePath(targetPath));
307
303
  return true;
308
- } catch {
309
- return false;
310
- }
304
+ } catch { return false; }
311
305
  }
312
306
 
313
307
  /** Updates mode bits on a node. */
@@ -323,24 +317,15 @@ class VirtualFileSystem extends EventEmitter {
323
317
  if (node.type === "file") {
324
318
  const f = node as InternalFileNode;
325
319
  return {
326
- type: "file",
327
- name,
328
- path: normalized,
329
- mode: f.mode,
330
- createdAt: f.createdAt,
331
- updatedAt: f.updatedAt,
332
- compressed: f.compressed,
333
- size: f.content.length,
320
+ type: "file", name, path: normalized, mode: f.mode,
321
+ createdAt: f.createdAt, updatedAt: f.updatedAt,
322
+ compressed: f.compressed, size: f.content.length,
334
323
  };
335
324
  }
336
325
  const d = node as InternalDirectoryNode;
337
326
  return {
338
- type: "directory",
339
- name,
340
- path: normalized,
341
- mode: d.mode,
342
- createdAt: d.createdAt,
343
- updatedAt: d.updatedAt,
327
+ type: "directory", name, path: normalized, mode: d.mode,
328
+ createdAt: d.createdAt, updatedAt: d.updatedAt,
344
329
  childrenCount: d.children.size,
345
330
  };
346
331
  }
@@ -360,7 +345,9 @@ class VirtualFileSystem extends EventEmitter {
360
345
  const normalized = normalizePath(dirPath);
361
346
  const node = getNode(this.root, normalized);
362
347
  if (node.type !== "directory") {
363
- throw new Error(`Cannot render tree for '${dirPath}': not a directory.`);
348
+ throw new Error(
349
+ `Cannot render tree for '${dirPath}': not a directory.`,
350
+ );
364
351
  }
365
352
  const label = dirPath === "/" ? "/" : path.posix.basename(normalized);
366
353
  return this.renderTreeLines(node as InternalDirectoryNode, label);
@@ -378,8 +365,7 @@ class VirtualFileSystem extends EventEmitter {
378
365
  lines.push(`${connector}${name}`);
379
366
  if (child.type === "directory") {
380
367
  const sub = this.renderTreeLines(child as InternalDirectoryNode, "")
381
- .split("\n")
382
- .slice(1)
368
+ .split("\n").slice(1)
383
369
  .map((l) => `${nextPrefix}${l}`);
384
370
  lines.push(...sub);
385
371
  }
@@ -404,8 +390,7 @@ class VirtualFileSystem extends EventEmitter {
404
390
  /** Compresses a file's content with gzip in place. */
405
391
  public compressFile(targetPath: string): void {
406
392
  const node = getNode(this.root, normalizePath(targetPath));
407
- if (node.type !== "file")
408
- throw new Error(`Cannot compress '${targetPath}': not a file.`);
393
+ if (node.type !== "file") throw new Error(`Cannot compress '${targetPath}': not a file.`);
409
394
  const f = node as InternalFileNode;
410
395
  if (!f.compressed) {
411
396
  f.content = gzipSync(f.content);
@@ -417,8 +402,7 @@ class VirtualFileSystem extends EventEmitter {
417
402
  /** Decompresses a gzip-compressed file in place. */
418
403
  public decompressFile(targetPath: string): void {
419
404
  const node = getNode(this.root, normalizePath(targetPath));
420
- if (node.type !== "file")
421
- throw new Error(`Cannot decompress '${targetPath}': not a file.`);
405
+ if (node.type !== "file") throw new Error(`Cannot decompress '${targetPath}': not a file.`);
422
406
  const f = node as InternalFileNode;
423
407
  if (f.compressed) {
424
408
  f.content = gunzipSync(f.content);
@@ -437,25 +421,18 @@ class VirtualFileSystem extends EventEmitter {
437
421
  ? normalizePath(targetPath)
438
422
  : targetPath;
439
423
  const { parent, name } = getParentDirectory(
440
- this.root,
441
- normalizedLink,
442
- true,
424
+ this.root, normalizedLink, true,
443
425
  (p) => this.mkdirRecursive(p, 0o755),
444
426
  );
445
427
  const symNode: InternalFileNode = {
446
- type: "file",
447
- name,
428
+ type: "file", name,
448
429
  content: Buffer.from(normalizedTarget, "utf8"),
449
430
  mode: 0o120777,
450
431
  compressed: false,
451
- createdAt: new Date(),
452
- updatedAt: new Date(),
432
+ createdAt: new Date(), updatedAt: new Date(),
453
433
  };
454
434
  parent.children.set(name, symNode);
455
- this.emit("symlink:create", {
456
- link: normalizedLink,
457
- target: normalizedTarget,
458
- });
435
+ this.emit("symlink:create", { link: normalizedLink, target: normalizedTarget });
459
436
  }
460
437
 
461
438
  /** Returns true when the path is a symbolic link node. */
@@ -463,9 +440,7 @@ class VirtualFileSystem extends EventEmitter {
463
440
  try {
464
441
  const node = getNode(this.root, normalizePath(targetPath));
465
442
  return node.type === "file" && node.mode === 0o120777;
466
- } catch {
467
- return false;
468
- }
443
+ } catch { return false; }
469
444
  }
470
445
 
471
446
  /**
@@ -481,14 +456,10 @@ class VirtualFileSystem extends EventEmitter {
481
456
  const target = (node as InternalFileNode).content.toString("utf8");
482
457
  current = target.startsWith("/")
483
458
  ? target
484
- : normalizePath(
485
- path.posix.join(path.posix.dirname(current), target),
486
- );
459
+ : normalizePath(path.posix.join(path.posix.dirname(current), target));
487
460
  continue;
488
461
  }
489
- } catch {
490
- break;
491
- }
462
+ } catch { break; }
492
463
  return current;
493
464
  }
494
465
  throw new Error(`Too many levels of symbolic links: ${linkPath}`);
@@ -507,12 +478,7 @@ class VirtualFileSystem extends EventEmitter {
507
478
  );
508
479
  }
509
480
  }
510
- const { parent, name } = getParentDirectory(
511
- this.root,
512
- normalized,
513
- false,
514
- () => {},
515
- );
481
+ const { parent, name } = getParentDirectory(this.root, normalized, false, () => {});
516
482
  parent.children.delete(name);
517
483
  this.emit("node:remove", { path: normalized });
518
484
  }
@@ -530,16 +496,10 @@ class VirtualFileSystem extends EventEmitter {
530
496
  }
531
497
  this.mkdirRecursive(path.posix.dirname(toNormalized), 0o755);
532
498
  const { parent: destParent, name: destName } = getParentDirectory(
533
- this.root,
534
- toNormalized,
535
- false,
536
- () => {},
499
+ this.root, toNormalized, false, () => {},
537
500
  );
538
501
  const { parent: srcParent, name: srcName } = getParentDirectory(
539
- this.root,
540
- fromNormalized,
541
- false,
542
- () => {},
502
+ this.root, fromNormalized, false, () => {},
543
503
  );
544
504
  srcParent.children.delete(srcName);
545
505
  node.name = destName;
@@ -568,9 +528,7 @@ class VirtualFileSystem extends EventEmitter {
568
528
  );
569
529
  }
570
530
  return {
571
- type: "directory",
572
- name: dir.name,
573
- mode: dir.mode,
531
+ type: "directory", name: dir.name, mode: dir.mode,
574
532
  createdAt: dir.createdAt.toISOString(),
575
533
  updatedAt: dir.updatedAt.toISOString(),
576
534
  children,
@@ -579,9 +537,7 @@ class VirtualFileSystem extends EventEmitter {
579
537
 
580
538
  private serializeFile(file: InternalFileNode): VfsSnapshotFileNode {
581
539
  return {
582
- type: "file",
583
- name: file.name,
584
- mode: file.mode,
540
+ type: "file", name: file.name, mode: file.mode,
585
541
  createdAt: file.createdAt.toISOString(),
586
542
  updatedAt: file.updatedAt.toISOString(),
587
543
  compressed: file.compressed,
@@ -622,9 +578,7 @@ class VirtualFileSystem extends EventEmitter {
622
578
  name: string,
623
579
  ): InternalDirectoryNode {
624
580
  const dir: InternalDirectoryNode = {
625
- type: "directory",
626
- name,
627
- mode: snap.mode,
581
+ type: "directory", name, mode: snap.mode,
628
582
  createdAt: new Date(snap.createdAt),
629
583
  updatedAt: new Date(snap.updatedAt),
630
584
  children: new Map(),
@@ -633,9 +587,7 @@ class VirtualFileSystem extends EventEmitter {
633
587
  if (child.type === "file") {
634
588
  const f = child as VfsSnapshotFileNode;
635
589
  dir.children.set(f.name, {
636
- type: "file",
637
- name: f.name,
638
- mode: f.mode,
590
+ type: "file", name: f.name, mode: f.mode,
639
591
  createdAt: new Date(f.createdAt),
640
592
  updatedAt: new Date(f.updatedAt),
641
593
  compressed: f.compressed,