typescript-virtual-container 1.2.9 → 1.3.1

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 (281) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +141 -50
  3. package/biome.json +7 -0
  4. package/dist/SSHMimic/exec.d.ts.map +1 -1
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +32 -16
  7. package/dist/SSHMimic/index.d.ts.map +1 -1
  8. package/dist/SSHMimic/index.js +20 -6
  9. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/binaryPack.js +29 -6
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +36 -13
  13. package/dist/VirtualPackageManager/index.d.ts.map +1 -1
  14. package/dist/VirtualPackageManager/index.js +192 -43
  15. package/dist/VirtualShell/index.d.ts +10 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +18 -7
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +3 -1
  20. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  21. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  22. package/dist/commands/adduser.d.ts +6 -0
  23. package/dist/commands/adduser.d.ts.map +1 -1
  24. package/dist/commands/adduser.js +6 -0
  25. package/dist/commands/alias.d.ts +5 -0
  26. package/dist/commands/alias.d.ts.map +1 -1
  27. package/dist/commands/alias.js +5 -0
  28. package/dist/commands/apt.d.ts +5 -0
  29. package/dist/commands/apt.d.ts.map +1 -1
  30. package/dist/commands/apt.js +32 -9
  31. package/dist/commands/awk.d.ts +11 -0
  32. package/dist/commands/awk.d.ts.map +1 -1
  33. package/dist/commands/awk.js +15 -2
  34. package/dist/commands/base64.d.ts +5 -0
  35. package/dist/commands/base64.d.ts.map +1 -1
  36. package/dist/commands/base64.js +9 -1
  37. package/dist/commands/cat.d.ts +5 -0
  38. package/dist/commands/cat.d.ts.map +1 -1
  39. package/dist/commands/cat.js +10 -2
  40. package/dist/commands/cd.d.ts +5 -0
  41. package/dist/commands/cd.d.ts.map +1 -1
  42. package/dist/commands/cd.js +5 -0
  43. package/dist/commands/chmod.d.ts +5 -0
  44. package/dist/commands/chmod.d.ts.map +1 -1
  45. package/dist/commands/chmod.js +5 -0
  46. package/dist/commands/cp.d.ts +5 -0
  47. package/dist/commands/cp.d.ts.map +1 -1
  48. package/dist/commands/cp.js +5 -0
  49. package/dist/commands/curl.d.ts +5 -0
  50. package/dist/commands/curl.d.ts.map +1 -1
  51. package/dist/commands/curl.js +34 -6
  52. package/dist/commands/cut.d.ts +5 -0
  53. package/dist/commands/cut.d.ts.map +1 -1
  54. package/dist/commands/cut.js +8 -1
  55. package/dist/commands/date.d.ts +5 -0
  56. package/dist/commands/date.d.ts.map +1 -1
  57. package/dist/commands/date.js +7 -1
  58. package/dist/commands/declare.d.ts +3 -0
  59. package/dist/commands/declare.d.ts.map +1 -0
  60. package/dist/commands/declare.js +39 -0
  61. package/dist/commands/diff.d.ts +5 -0
  62. package/dist/commands/diff.d.ts.map +1 -1
  63. package/dist/commands/diff.js +5 -0
  64. package/dist/commands/dpkg.d.ts +5 -0
  65. package/dist/commands/dpkg.d.ts.map +1 -1
  66. package/dist/commands/dpkg.js +24 -7
  67. package/dist/commands/du.d.ts.map +1 -1
  68. package/dist/commands/du.js +8 -2
  69. package/dist/commands/echo.d.ts +5 -0
  70. package/dist/commands/echo.d.ts.map +1 -1
  71. package/dist/commands/echo.js +13 -4
  72. package/dist/commands/env.d.ts +5 -0
  73. package/dist/commands/env.d.ts.map +1 -1
  74. package/dist/commands/env.js +11 -1
  75. package/dist/commands/exit.d.ts +5 -0
  76. package/dist/commands/exit.d.ts.map +1 -1
  77. package/dist/commands/exit.js +12 -2
  78. package/dist/commands/export.d.ts.map +1 -1
  79. package/dist/commands/export.js +3 -1
  80. package/dist/commands/find.d.ts +5 -0
  81. package/dist/commands/find.d.ts.map +1 -1
  82. package/dist/commands/find.js +5 -0
  83. package/dist/commands/free.d.ts +5 -0
  84. package/dist/commands/free.d.ts.map +1 -1
  85. package/dist/commands/free.js +5 -0
  86. package/dist/commands/grep.d.ts +5 -0
  87. package/dist/commands/grep.d.ts.map +1 -1
  88. package/dist/commands/grep.js +12 -2
  89. package/dist/commands/gzip.d.ts +5 -0
  90. package/dist/commands/gzip.d.ts.map +1 -1
  91. package/dist/commands/gzip.js +18 -2
  92. package/dist/commands/head.d.ts +5 -0
  93. package/dist/commands/head.d.ts.map +1 -1
  94. package/dist/commands/head.js +5 -0
  95. package/dist/commands/help.d.ts.map +1 -1
  96. package/dist/commands/help.js +98 -45
  97. package/dist/commands/history.d.ts +5 -0
  98. package/dist/commands/history.d.ts.map +1 -1
  99. package/dist/commands/history.js +5 -0
  100. package/dist/commands/hostname.d.ts +5 -0
  101. package/dist/commands/hostname.d.ts.map +1 -1
  102. package/dist/commands/hostname.js +5 -0
  103. package/dist/commands/id.d.ts.map +1 -1
  104. package/dist/commands/id.js +4 -1
  105. package/dist/commands/index.d.ts +2 -17
  106. package/dist/commands/index.d.ts.map +1 -1
  107. package/dist/commands/index.js +2 -340
  108. package/dist/commands/ls.d.ts.map +1 -1
  109. package/dist/commands/ls.js +3 -1
  110. package/dist/commands/lsb-release.d.ts.map +1 -1
  111. package/dist/commands/lsb-release.js +8 -2
  112. package/dist/commands/nano.js +1 -1
  113. package/dist/commands/neofetch.js +1 -1
  114. package/dist/commands/node.d.ts +9 -0
  115. package/dist/commands/node.d.ts.map +1 -0
  116. package/dist/commands/node.js +316 -0
  117. package/dist/commands/npm.d.ts +19 -0
  118. package/dist/commands/npm.d.ts.map +1 -0
  119. package/dist/commands/npm.js +109 -0
  120. package/dist/commands/ping.d.ts.map +1 -1
  121. package/dist/commands/ping.js +3 -1
  122. package/dist/commands/printf.d.ts +3 -0
  123. package/dist/commands/printf.d.ts.map +1 -0
  124. package/dist/commands/printf.js +113 -0
  125. package/dist/commands/ps.d.ts.map +1 -1
  126. package/dist/commands/ps.js +4 -1
  127. package/dist/commands/python.d.ts +30 -0
  128. package/dist/commands/python.d.ts.map +1 -0
  129. package/dist/commands/python.js +2058 -0
  130. package/dist/commands/read.d.ts +3 -0
  131. package/dist/commands/read.d.ts.map +1 -0
  132. package/dist/commands/read.js +34 -0
  133. package/dist/commands/registry.d.ts +8 -0
  134. package/dist/commands/registry.d.ts.map +1 -0
  135. package/dist/commands/registry.js +229 -0
  136. package/dist/commands/runtime.d.ts +6 -0
  137. package/dist/commands/runtime.d.ts.map +1 -0
  138. package/dist/commands/runtime.js +280 -0
  139. package/dist/commands/sed.d.ts.map +1 -1
  140. package/dist/commands/sed.js +11 -3
  141. package/dist/commands/set.d.ts.map +1 -1
  142. package/dist/commands/set.js +9 -3
  143. package/dist/commands/sh.d.ts.map +1 -1
  144. package/dist/commands/sh.js +57 -36
  145. package/dist/commands/shift.d.ts +5 -0
  146. package/dist/commands/shift.d.ts.map +1 -0
  147. package/dist/commands/shift.js +52 -0
  148. package/dist/commands/sleep.d.ts.map +1 -1
  149. package/dist/commands/sort.d.ts.map +1 -1
  150. package/dist/commands/sort.js +4 -2
  151. package/dist/commands/source.d.ts.map +1 -1
  152. package/dist/commands/source.js +5 -2
  153. package/dist/commands/sudo.js +1 -1
  154. package/dist/commands/tar.d.ts.map +1 -1
  155. package/dist/commands/tar.js +11 -3
  156. package/dist/commands/tee.d.ts.map +1 -1
  157. package/dist/commands/tee.js +8 -6
  158. package/dist/commands/test.d.ts.map +1 -1
  159. package/dist/commands/test.js +46 -24
  160. package/dist/commands/tr.d.ts.map +1 -1
  161. package/dist/commands/tr.js +3 -1
  162. package/dist/commands/true.d.ts +4 -0
  163. package/dist/commands/true.d.ts.map +1 -0
  164. package/dist/commands/true.js +14 -0
  165. package/dist/commands/type.d.ts.map +1 -1
  166. package/dist/commands/type.js +1 -1
  167. package/dist/commands/uname.d.ts.map +1 -1
  168. package/dist/commands/uname.js +4 -1
  169. package/dist/commands/uniq.d.ts.map +1 -1
  170. package/dist/commands/uptime.d.ts.map +1 -1
  171. package/dist/commands/uptime.js +4 -1
  172. package/dist/commands/wget.d.ts.map +1 -1
  173. package/dist/commands/wget.js +32 -7
  174. package/dist/commands/which.d.ts.map +1 -1
  175. package/dist/commands/xargs.d.ts.map +1 -1
  176. package/dist/commands/xargs.js +1 -1
  177. package/dist/index.d.ts +15 -14
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +9 -9
  180. package/dist/modules/linuxRootfs.d.ts +18 -1
  181. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  182. package/dist/modules/linuxRootfs.js +160 -17
  183. package/dist/standalone-wo-sftp.d.ts +2 -0
  184. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  185. package/dist/standalone-wo-sftp.js +30 -0
  186. package/dist/utils/expand.d.ts +50 -0
  187. package/dist/utils/expand.d.ts.map +1 -0
  188. package/dist/utils/expand.js +183 -0
  189. package/dist/utils/vfsDiff.d.ts +90 -0
  190. package/dist/utils/vfsDiff.d.ts.map +1 -0
  191. package/dist/utils/vfsDiff.js +177 -0
  192. package/package.json +2 -1
  193. package/src/SSHMimic/exec.ts +10 -1
  194. package/src/SSHMimic/executor.ts +104 -18
  195. package/src/SSHMimic/index.ts +49 -15
  196. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  197. package/src/VirtualFileSystem/index.ts +78 -28
  198. package/src/VirtualPackageManager/index.ts +208 -49
  199. package/src/VirtualShell/index.ts +35 -7
  200. package/src/VirtualShell/shell.ts +23 -3
  201. package/src/VirtualShell/shellParser.ts +134 -36
  202. package/src/VirtualUserManager/index.ts +7 -2
  203. package/src/commands/adduser.ts +6 -0
  204. package/src/commands/alias.ts +5 -1
  205. package/src/commands/apt.ts +47 -17
  206. package/src/commands/awk.ts +20 -6
  207. package/src/commands/base64.ts +13 -2
  208. package/src/commands/cat.ts +13 -5
  209. package/src/commands/cd.ts +5 -0
  210. package/src/commands/chmod.ts +5 -0
  211. package/src/commands/cp.ts +5 -0
  212. package/src/commands/curl.ts +56 -12
  213. package/src/commands/cut.ts +8 -1
  214. package/src/commands/date.ts +7 -1
  215. package/src/commands/declare.ts +44 -0
  216. package/src/commands/diff.ts +17 -3
  217. package/src/commands/dpkg.ts +33 -11
  218. package/src/commands/du.ts +17 -5
  219. package/src/commands/echo.ts +22 -9
  220. package/src/commands/env.ts +11 -1
  221. package/src/commands/exit.ts +12 -2
  222. package/src/commands/export.ts +3 -1
  223. package/src/commands/find.ts +5 -0
  224. package/src/commands/free.ts +9 -2
  225. package/src/commands/grep.ts +12 -2
  226. package/src/commands/gzip.ts +28 -4
  227. package/src/commands/head.ts +5 -0
  228. package/src/commands/help.ts +121 -47
  229. package/src/commands/history.ts +7 -2
  230. package/src/commands/hostname.ts +5 -0
  231. package/src/commands/id.ts +4 -1
  232. package/src/commands/index.ts +9 -360
  233. package/src/commands/ls.ts +5 -3
  234. package/src/commands/lsb-release.ts +8 -2
  235. package/src/commands/nano.ts +1 -1
  236. package/src/commands/neofetch.ts +1 -1
  237. package/src/commands/node.ts +341 -0
  238. package/src/commands/npm.ts +132 -0
  239. package/src/commands/ping.ts +6 -2
  240. package/src/commands/printf.ts +112 -0
  241. package/src/commands/ps.ts +21 -9
  242. package/src/commands/python.ts +2229 -0
  243. package/src/commands/read.ts +41 -0
  244. package/src/commands/registry.ts +244 -0
  245. package/src/commands/runtime.ts +353 -0
  246. package/src/commands/sed.ts +27 -9
  247. package/src/commands/set.ts +9 -3
  248. package/src/commands/sh.ts +159 -55
  249. package/src/commands/shift.ts +53 -0
  250. package/src/commands/sleep.ts +2 -1
  251. package/src/commands/sort.ts +10 -6
  252. package/src/commands/source.ts +15 -3
  253. package/src/commands/sudo.ts +1 -1
  254. package/src/commands/tar.ts +28 -7
  255. package/src/commands/tee.ts +7 -1
  256. package/src/commands/test.ts +61 -26
  257. package/src/commands/tr.ts +3 -1
  258. package/src/commands/true.ts +17 -0
  259. package/src/commands/type.ts +6 -3
  260. package/src/commands/uname.ts +5 -1
  261. package/src/commands/uniq.ts +8 -2
  262. package/src/commands/uptime.ts +4 -1
  263. package/src/commands/wget.ts +51 -12
  264. package/src/commands/which.ts +5 -2
  265. package/src/commands/xargs.ts +11 -2
  266. package/src/index.ts +23 -24
  267. package/src/modules/linuxRootfs.ts +233 -30
  268. package/src/standalone-wo-sftp.ts +38 -0
  269. package/src/utils/expand.ts +238 -0
  270. package/src/utils/vfsDiff.ts +275 -0
  271. package/standalone-wo-sftp.js +507 -0
  272. package/standalone-wo-sftp.js.map +7 -0
  273. package/standalone.js +253 -191
  274. package/standalone.js.map +4 -4
  275. package/tests/bun-test-shim.ts +9 -1
  276. package/tests/command-helpers.test.ts +1 -5
  277. package/tests/new-features.test.ts +415 -5
  278. package/tests/parser-executor.test.ts +27 -27
  279. package/tests/sftp.test.ts +122 -42
  280. package/tests/users.test.ts +23 -5
  281. package/CHANGELOG.md +0 -150
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: ENV VAR KEYS */
1
2
  /**
2
3
  * linuxRootfs.ts
3
4
  *
@@ -62,21 +63,12 @@ function bootstrapEtc(
62
63
  ].join("\n")}\n`,
63
64
  );
64
65
 
65
- ensureFile(
66
- vfs,
67
- "/etc/issue",
68
- `Fortune GNU/Linux 1.0 \\n \\l\n`,
69
- );
66
+ ensureFile(vfs, "/etc/issue", `Fortune GNU/Linux 1.0 \\n \\l\n`);
70
67
 
71
68
  ensureFile(
72
69
  vfs,
73
70
  "/etc/motd",
74
- [
75
- "",
76
- `Welcome to ${props.os}`,
77
- `Kernel: ${props.kernel}`,
78
- "",
79
- ].join("\n"),
71
+ ["", `Welcome to ${props.os}`, `Kernel: ${props.kernel}`, ""].join("\n"),
80
72
  );
81
73
 
82
74
  // APT sources
@@ -130,6 +122,12 @@ function bootstrapEtc(
130
122
 
131
123
  // ─── /etc/passwd + /etc/group + /etc/shadow ─────────────────────────────────
132
124
 
125
+ /**
126
+ * Sync `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the
127
+ * VirtualUserManager's current user list into the VFS.
128
+ * @param vfs VirtualFileSystem instance to write files into
129
+ * @param users VirtualUserManager to source users from
130
+ */
133
131
  export function syncEtcPasswd(
134
132
  vfs: VirtualFileSystem,
135
133
  users: VirtualUserManager,
@@ -175,16 +173,90 @@ export function syncEtcPasswd(
175
173
 
176
174
  // ─── /proc ───────────────────────────────────────────────────────────────────
177
175
 
176
+ /** Derive a stable virtual PID from a tty string like "pts/0" → 1000, "pts/1" → 1001 */
177
+ function ttyToPid(tty: string): number {
178
+ const match = tty.match(/(\d+)$/);
179
+ return 1000 + (match?.[1] ? parseInt(match[1], 10) : 0);
180
+ }
181
+
182
+ /** Write /proc/<pid>/ subtree for a single virtual process */
183
+ function writeProcPid(
184
+ vfs: VirtualFileSystem,
185
+ pid: number,
186
+ username: string,
187
+ _tty: string,
188
+ cmdline: string,
189
+ startedAt: string,
190
+ env: Record<string, string>,
191
+ ): void {
192
+ const dir = `/proc/${pid}`;
193
+ ensureDir(vfs, dir);
194
+ ensureDir(vfs, `${dir}/fd`);
195
+ ensureDir(vfs, `${dir}/fdinfo`);
196
+
197
+ const uptimeSec = Math.floor(
198
+ (Date.now() - new Date(startedAt).getTime()) / 1000,
199
+ );
200
+
201
+ vfs.writeFile(`${dir}/cmdline`, `${cmdline.replace(/\s+/g, "\0")}\0`);
202
+ vfs.writeFile(`${dir}/comm`, cmdline.split(/\s+/)[0] ?? "bash");
203
+ vfs.writeFile(
204
+ `${dir}/status`,
205
+ `${[
206
+ `Name: ${cmdline.split(/\s+/)[0] ?? "bash"}`,
207
+ `State: S (sleeping)`,
208
+ `Pid: ${pid}`,
209
+ `PPid: 1`,
210
+ `Uid: 0\t0\t0\t0`,
211
+ `Gid: 0\t0\t0\t0`,
212
+ `VmRSS: 4096 kB`,
213
+ `VmSize: 16384 kB`,
214
+ `Threads: 1`,
215
+ ].join("\n")}\n`,
216
+ );
217
+ vfs.writeFile(
218
+ `${dir}/stat`,
219
+ `${pid} (${cmdline.split(/\s+/)[0] ?? "bash"}) S 1 ${pid} ${pid} 0 -1 4194304 0 0 0 0 ${uptimeSec} 0 0 0 20 0 1 0 0 16384 4096 0\n`,
220
+ );
221
+ vfs.writeFile(
222
+ `${dir}/environ`,
223
+ `${Object.entries(env)
224
+ .map(([k, v]) => `${k}=${v}`)
225
+ .join("\0")}\0`,
226
+ );
227
+ vfs.writeFile(`${dir}/cwd`, `/home/${username}\0`);
228
+ vfs.writeFile(`${dir}/exe`, "/bin/bash\0");
229
+
230
+ // Standard fd entries
231
+ vfs.writeFile(`${dir}/fd/0`, "");
232
+ vfs.writeFile(`${dir}/fd/1`, "");
233
+ vfs.writeFile(`${dir}/fd/2`, "");
234
+ }
235
+
236
+ /**
237
+ * Populate and refresh `/proc` virtual entries based on host stats and
238
+ * provided active sessions. Rewrites `/proc/uptime`, `/proc/meminfo`,
239
+ * `/proc/cpuinfo`, `/proc/<pid>` entries and `/proc/self` content.
240
+ * @param vfs VirtualFileSystem instance
241
+ * @param props ShellProperties used for version strings
242
+ * @param hostname Hostname to write into /proc/hostname
243
+ * @param shellStartTime Start time used to compute uptime
244
+ * @param sessions Optional active sessions list to populate per-pid entries
245
+ */
178
246
  export function refreshProc(
179
247
  vfs: VirtualFileSystem,
180
248
  props: ShellProperties,
181
249
  hostname: string,
182
250
  shellStartTime: number,
251
+ sessions?: import("../VirtualUserManager").VirtualActiveSession[],
183
252
  ): void {
184
253
  ensureDir(vfs, "/proc");
185
254
 
186
255
  const uptimeSec = Math.floor((Date.now() - shellStartTime) / 1000);
187
- vfs.writeFile("/proc/uptime", `${uptimeSec}.00 ${Math.floor(uptimeSec * 0.9)}.00\n`);
256
+ vfs.writeFile(
257
+ "/proc/uptime",
258
+ `${uptimeSec}.00 ${Math.floor(uptimeSec * 0.9)}.00\n`,
259
+ );
188
260
 
189
261
  const totalMemKb = Math.floor(os.totalmem() / 1024);
190
262
  const freeMemKb = Math.floor(os.freemem() / 1024);
@@ -207,7 +279,7 @@ export function refreshProc(
207
279
  for (let i = 0; i < cpus.length; i++) {
208
280
  const c = cpus[i];
209
281
  if (!c) continue;
210
- const mhz = (c.speed).toFixed(3);
282
+ const mhz = c.speed.toFixed(3);
211
283
  cpuLines.push(
212
284
  `processor\t: ${i}`,
213
285
  `model name\t: ${c.model}`,
@@ -227,7 +299,11 @@ export function refreshProc(
227
299
 
228
300
  // /proc/loadavg
229
301
  const load = (Math.random() * 0.5).toFixed(2);
230
- vfs.writeFile("/proc/loadavg", `${load} ${load} ${load} 1/1 1\n`);
302
+ const numProcs = 1 + (sessions?.length ?? 0);
303
+ vfs.writeFile(
304
+ "/proc/loadavg",
305
+ `${load} ${load} ${load} ${numProcs}/${numProcs} 1\n`,
306
+ );
231
307
 
232
308
  // /proc/net stubs
233
309
  ensureDir(vfs, "/proc/net");
@@ -241,6 +317,85 @@ export function refreshProc(
241
317
  " eth0: 131072 1024 0 0 0 0 0 0 65536 512 0 0 0 0 0 0",
242
318
  ].join("\n")}\n`,
243
319
  );
320
+
321
+ // ── /proc/1 — init process ────────────────────────────────────────────────
322
+ writeProcPid(
323
+ vfs,
324
+ 1,
325
+ "root",
326
+ "pts/0",
327
+ "/sbin/init",
328
+ new Date(shellStartTime).toISOString(),
329
+ {},
330
+ );
331
+
332
+ // ── /proc/<pid> per session ───────────────────────────────────────────────
333
+ const activeSessions = sessions ?? [];
334
+ for (const session of activeSessions) {
335
+ const pid = ttyToPid(session.tty);
336
+ writeProcPid(
337
+ vfs,
338
+ pid,
339
+ session.username,
340
+ session.tty,
341
+ "bash",
342
+ session.startedAt,
343
+ {
344
+ USER: session.username,
345
+ HOME: `/home/${session.username}`,
346
+ TERM: "xterm-256color",
347
+ SHELL: "/bin/bash",
348
+ },
349
+ );
350
+ }
351
+
352
+ // ── /proc/self — symlink to current session PID or 1 ────────────────────
353
+ // We can't know which session is "current" at populate time,
354
+ // so /proc/self is a directory that mirrors the most recent session,
355
+ // or init if no sessions. Commands that read /proc/self get consistent data.
356
+ const selfPid =
357
+ activeSessions.length > 0
358
+ ? ttyToPid(activeSessions[activeSessions.length - 1]!.tty)
359
+ : 1;
360
+
361
+ // Remove existing /proc/self and recreate as content copy
362
+ if (vfs.exists("/proc/self")) {
363
+ try {
364
+ vfs.remove("/proc/self");
365
+ } catch {}
366
+ }
367
+ // /proc/self is a real directory (not a symlink, which VFS may not support for dirs)
368
+ const selfSrc = `/proc/${selfPid}`;
369
+ if (vfs.exists(selfSrc)) {
370
+ ensureDir(vfs, "/proc/self");
371
+ ensureDir(vfs, "/proc/self/fd");
372
+ for (const entry of vfs.list(selfSrc)) {
373
+ const srcPath = `${selfSrc}/${entry}`;
374
+ const dstPath = `/proc/self/${entry}`;
375
+ try {
376
+ const st = vfs.stat(srcPath);
377
+ if (st.type === "file") {
378
+ vfs.writeFile(dstPath, vfs.readFile(srcPath));
379
+ }
380
+ } catch {}
381
+ }
382
+ vfs.writeFile(
383
+ "/proc/self/status",
384
+ vfs.exists(`${selfSrc}/status`) ? vfs.readFile(`${selfSrc}/status`) : "",
385
+ );
386
+ } else {
387
+ // Fallback minimal /proc/self
388
+ ensureDir(vfs, "/proc/self");
389
+ vfs.writeFile("/proc/self/cmdline", "bash\0");
390
+ vfs.writeFile("/proc/self/comm", "bash");
391
+ vfs.writeFile(
392
+ "/proc/self/status",
393
+ "Name:\tbash\nState:\tS (sleeping)\nPid:\t1\nPPid:\t0\n",
394
+ );
395
+ vfs.writeFile("/proc/self/environ", "");
396
+ vfs.writeFile("/proc/self/cwd", "/root\0");
397
+ vfs.writeFile("/proc/self/exe", "/bin/bash\0");
398
+ }
244
399
  }
245
400
 
246
401
  // ─── /sys ─────────────────────────────────────────────────────────────────────
@@ -252,8 +407,16 @@ function bootstrapSys(vfs: VirtualFileSystem, props: ShellProperties): void {
252
407
  ensureDir(vfs, "/sys/devices/virtual/dmi");
253
408
  ensureDir(vfs, "/sys/devices/virtual/dmi/id");
254
409
 
255
- ensureFile(vfs, "/sys/devices/virtual/dmi/id/sys_vendor", "Fortune Systems\n");
256
- ensureFile(vfs, "/sys/devices/virtual/dmi/id/product_name", "VirtualContainer v1\n");
410
+ ensureFile(
411
+ vfs,
412
+ "/sys/devices/virtual/dmi/id/sys_vendor",
413
+ "Fortune Systems\n",
414
+ );
415
+ ensureFile(
416
+ vfs,
417
+ "/sys/devices/virtual/dmi/id/product_name",
418
+ "VirtualContainer v1\n",
419
+ );
257
420
  ensureFile(vfs, "/sys/devices/virtual/dmi/id/board_name", "fortune-board\n");
258
421
 
259
422
  ensureDir(vfs, "/sys/class");
@@ -261,11 +424,7 @@ function bootstrapSys(vfs: VirtualFileSystem, props: ShellProperties): void {
261
424
 
262
425
  ensureDir(vfs, "/sys/kernel");
263
426
  ensureFile(vfs, "/sys/kernel/hostname", "fortune-vm\n");
264
- ensureFile(
265
- vfs,
266
- "/sys/kernel/osrelease",
267
- `${props.kernel}\n`,
268
- );
427
+ ensureFile(vfs, "/sys/kernel/osrelease", `${props.kernel}\n`);
269
428
  ensureFile(vfs, "/sys/kernel/ostype", "Linux\n");
270
429
  }
271
430
 
@@ -299,22 +458,66 @@ function bootstrapUsr(vfs: VirtualFileSystem): void {
299
458
 
300
459
  // Stub binaries so `which` can find built-in commands
301
460
  const builtins = [
302
- "sh", "bash", "ls", "cat", "echo", "grep", "find", "sort",
303
- "head", "tail", "cut", "tr", "sed", "awk", "wc", "tee",
304
- "tar", "gzip", "gunzip", "touch", "mkdir", "rm", "mv", "cp",
305
- "chmod", "ln", "pwd", "env", "date", "sleep", "id", "whoami",
306
- "hostname", "uname", "ps", "kill", "df", "du", "curl", "wget",
307
- "nano", "diff", "uniq", "xargs", "base64",
461
+ "sh",
462
+ "bash",
463
+ "ls",
464
+ "cat",
465
+ "echo",
466
+ "grep",
467
+ "find",
468
+ "sort",
469
+ "head",
470
+ "tail",
471
+ "cut",
472
+ "tr",
473
+ "sed",
474
+ "awk",
475
+ "wc",
476
+ "tee",
477
+ "tar",
478
+ "gzip",
479
+ "gunzip",
480
+ "touch",
481
+ "mkdir",
482
+ "rm",
483
+ "mv",
484
+ "cp",
485
+ "chmod",
486
+ "ln",
487
+ "pwd",
488
+ "env",
489
+ "date",
490
+ "sleep",
491
+ "id",
492
+ "whoami",
493
+ "hostname",
494
+ "uname",
495
+ "ps",
496
+ "kill",
497
+ "df",
498
+ "du",
499
+ "curl",
500
+ "wget",
501
+ "nano",
502
+ "diff",
503
+ "uniq",
504
+ "xargs",
505
+ "base64",
308
506
  ];
309
507
  for (const bin of builtins) {
310
- ensureFile(vfs, `/usr/bin/${bin}`, `#!/bin/sh\nexec builtin ${bin} "$@"\n`, 0o755);
508
+ ensureFile(
509
+ vfs,
510
+ `/usr/bin/${bin}`,
511
+ `#!/bin/sh\nexec builtin ${bin} "$@"\n`,
512
+ 0o755,
513
+ );
311
514
  }
312
515
 
313
516
  // lsb_release script
314
517
  ensureFile(
315
518
  vfs,
316
519
  "/usr/bin/lsb_release",
317
- "#!/bin/sh\nexec lsb_release \"$@\"\n",
520
+ '#!/bin/sh\nexec lsb_release "$@"\n',
318
521
  0o755,
319
522
  );
320
523
  }
@@ -434,6 +637,6 @@ export function bootstrapLinuxRootfs(
434
637
  bootstrapTmp(vfs);
435
638
  bootstrapRoot(vfs);
436
639
  bootstrapMisc(vfs);
437
- refreshProc(vfs, props, hostname, shellStartTime);
640
+ refreshProc(vfs, props, hostname, shellStartTime, []);
438
641
  syncEtcPasswd(vfs, users);
439
642
  }
@@ -0,0 +1,38 @@
1
+ import { SshMimic } from "./SSHMimic/index";
2
+ import { VirtualShell } from "./VirtualShell";
3
+
4
+ const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
5
+ const virtualShell = new VirtualShell(hostname, undefined, {
6
+ mode: "fs",
7
+ snapshotPath: ".vfs",
8
+ });
9
+
10
+ virtualShell.addCommand("demo", [], () => {
11
+ return {
12
+ stdout: "This is a demo command. It does nothing useful.",
13
+ exitCode: 0,
14
+ };
15
+ });
16
+
17
+ new SshMimic({
18
+ port: 2222,
19
+ hostname,
20
+ shell: virtualShell,
21
+ })
22
+ .start()
23
+ .catch((error: unknown) => {
24
+ console.error("Failed to start SSH Mimic:", error);
25
+ process.exit(1);
26
+ });
27
+
28
+ process.on("uncaughtException", (error) => {
29
+ console.log("Oh my god, something terrible happened: ", error);
30
+ });
31
+
32
+ process.on("unhandledRejection", (error, promise) => {
33
+ console.log(
34
+ " Oh Lord! We forgot to handle a promise rejection here: ",
35
+ promise,
36
+ );
37
+ console.log(" The error was: ", error);
38
+ });
@@ -0,0 +1,238 @@
1
+ /**
2
+ * expand.ts
3
+ *
4
+ * Centralised shell variable and expression expansion.
5
+ * Used by `runCommand` (index.ts), `echo`, and `sh.ts`.
6
+ *
7
+ * Handles (in order):
8
+ * ~ tilde to $HOME
9
+ * $? last exit code
10
+ * $$ mock PID
11
+ * $# argument count (0 outside scripts)
12
+ * ${#VAR} string length
13
+ * ${VAR:-def} default if unset/empty
14
+ * ${VAR:=def} assign default if unset/empty
15
+ * ${VAR:+val} alternate value if set
16
+ * ${VAR} simple braced reference
17
+ * $VAR simple reference
18
+ * $((expr)) arithmetic (integer)
19
+ */
20
+
21
+ // ─── arithmetic evaluator ────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Evaluate a simple integer arithmetic expression.
25
+ * Supports: + - * / % ** unary- ( )
26
+ * Variables are resolved from `env` before evaluation.
27
+ * Returns NaN on syntax error.
28
+ */
29
+ export function evalArith(expr: string, env: Record<string, string>): number {
30
+ // Substitute variable names before evaluating
31
+ const substituted = expr.replace(
32
+ /\b([A-Za-z_][A-Za-z0-9_]*)\b/g,
33
+ (_, name) => {
34
+ const val = env[name];
35
+ return val !== undefined && val !== "" ? val : "0";
36
+ },
37
+ );
38
+
39
+ // Whitelist: only digits, operators, spaces, parens
40
+ if (!/^[\d\s+\-*/%()^!&|<>=,. ]+$/.test(substituted)) return NaN;
41
+
42
+ try {
43
+ // Use Function constructor for safe subset (no identifiers remain)
44
+ // eslint-disable-next-line no-new-func
45
+ const result = Function(
46
+ `"use strict"; return (${substituted.replace(/\*\*/g, "**")});`,
47
+ )();
48
+ return typeof result === "number" ? Math.trunc(result) : NaN;
49
+ } catch {
50
+ return NaN;
51
+ }
52
+ }
53
+
54
+ // ─── synchronous expansion ───────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Apply a replacer only to portions of `input` that are NOT inside single quotes.
58
+ * Single-quoted content is passed through verbatim (POSIX sh behaviour).
59
+ */
60
+ function outsideSingleQuotes(
61
+ input: string,
62
+ replacer: (chunk: string) => string,
63
+ ): string {
64
+ const parts: string[] = [];
65
+ let i = 0;
66
+ while (i < input.length) {
67
+ const sqIdx = input.indexOf("'", i);
68
+ if (sqIdx === -1) {
69
+ // No more single quotes — expand the rest
70
+ parts.push(replacer(input.slice(i)));
71
+ break;
72
+ }
73
+ // Expand the part before the single quote
74
+ parts.push(replacer(input.slice(i, sqIdx)));
75
+ // Find closing single quote — everything inside is literal
76
+ const closeIdx = input.indexOf("'", sqIdx + 1);
77
+ if (closeIdx === -1) {
78
+ // Unclosed quote — treat rest as literal
79
+ parts.push(input.slice(sqIdx));
80
+ break;
81
+ }
82
+ parts.push(input.slice(sqIdx, closeIdx + 1)); // include quotes
83
+ i = closeIdx + 1;
84
+ }
85
+ return parts.join("");
86
+ }
87
+
88
+ /**
89
+ * Expand all shell variable and expression forms synchronously.
90
+ * Does NOT handle `$(cmd)` — that requires async; see `expandAsync`.
91
+ * Content inside single quotes is left verbatim per POSIX sh rules.
92
+ *
93
+ * @param input Raw string possibly containing `$VAR`, `${...}`, `$((...))`.
94
+ * @param env Current session env vars.
95
+ * @param lastExit Last command exit code (for `$?`).
96
+ * @param home Home directory path (for `~`).
97
+ */
98
+ export function expandSync(
99
+ input: string,
100
+ env: Record<string, string>,
101
+ lastExit = 0,
102
+ home?: string,
103
+ ): string {
104
+ const homePath = home ?? env.HOME ?? "/home/user";
105
+
106
+ return outsideSingleQuotes(input, (chunk) => {
107
+ let s = chunk;
108
+
109
+ // Tilde expansion — only at start of token or after `:` or whitespace
110
+ s = s.replace(
111
+ /(^|[\s:])~(\/|$)/g,
112
+ (_, pre, post) => `${pre}${homePath}${post}`,
113
+ );
114
+
115
+ // $? $$ $#
116
+ s = s.replace(/\$\?/g, String(lastExit));
117
+ s = s.replace(/\$\$/g, "1");
118
+ s = s.replace(/\$#/g, "0");
119
+
120
+ // $(( arithmetic )) — must come before ${ and $VAR to avoid conflicts
121
+ s = s.replace(/\$\(\(([^)]+(?:\([^)]*\)[^)]*)*)\)\)/g, (_, expr) => {
122
+ const result = evalArith(expr, env);
123
+ return Number.isNaN(result) ? "0" : String(result);
124
+ });
125
+
126
+ // ${#VAR} — string length
127
+ s = s.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) =>
128
+ String((env[name] ?? "").length),
129
+ );
130
+
131
+ // ${VAR:-default}
132
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):-([^}]*)\}/g, (_, name, def) =>
133
+ env[name] !== undefined && env[name] !== "" ? (env[name] as string) : def,
134
+ );
135
+
136
+ // ${VAR:=default} — also assigns to env
137
+ s = s.replace(
138
+ /\$\{([A-Za-z_][A-Za-z0-9_]*):=([^}]*)\}/g,
139
+ (_, name, def) => {
140
+ if (env[name] === undefined || env[name] === "") env[name] = def;
141
+ return env[name] as string;
142
+ },
143
+ );
144
+
145
+ // ${VAR:+alternate}
146
+ s = s.replace(
147
+ /\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g,
148
+ (_, name, alt) =>
149
+ env[name] !== undefined && env[name] !== "" ? alt : "",
150
+ );
151
+
152
+ // ${VAR}
153
+ s = s.replace(
154
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
155
+ (_, name) => env[name] ?? "",
156
+ );
157
+
158
+ // $VAR
159
+ s = s.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => env[name] ?? "");
160
+
161
+ return s;
162
+ });
163
+ }
164
+
165
+ // ─── async expansion (includes $(cmd)) ──────────────────────────────────────
166
+
167
+ /**
168
+ * Expand all shell forms including `$(cmd)` command substitution.
169
+ *
170
+ * Processes `$(...)` blocks depth-first, respecting single-quote boundaries.
171
+ * Then delegates to `expandSync` for the remaining forms.
172
+ *
173
+ * @param input Raw string.
174
+ * @param env Current session env vars.
175
+ * @param lastExit Last exit code.
176
+ * @param runCmd Async callback to execute a command and return its stdout.
177
+ */
178
+ export async function expandAsync(
179
+ input: string,
180
+ env: Record<string, string>,
181
+ lastExit: number,
182
+ runCmd: (cmd: string) => Promise<string>,
183
+ ): Promise<string> {
184
+ // $(cmd) substitution — skip content inside single quotes
185
+ if (input.includes("$(")) {
186
+ let result = "";
187
+ let inSingle = false;
188
+ let i = 0;
189
+
190
+ while (i < input.length) {
191
+ const ch = input[i]!;
192
+
193
+ if (ch === "'" && !inSingle) {
194
+ inSingle = true;
195
+ result += ch;
196
+ i++;
197
+ continue;
198
+ }
199
+ if (ch === "'" && inSingle) {
200
+ inSingle = false;
201
+ result += ch;
202
+ i++;
203
+ continue;
204
+ }
205
+
206
+ if (!inSingle && ch === "$" && input[i + 1] === "(") {
207
+ // $((expr)) arithmetic — NOT a $(cmd) substitution, skip it
208
+ if (input[i + 2] === "(") {
209
+ result += ch;
210
+ i++;
211
+ continue;
212
+ }
213
+ // Find matching ) with depth tracking
214
+ let depth = 0;
215
+ let j = i + 1;
216
+ while (j < input.length) {
217
+ if (input[j] === "(") depth++;
218
+ else if (input[j] === ")") {
219
+ depth--;
220
+ if (depth === 0) break;
221
+ }
222
+ j++;
223
+ }
224
+ const sub = input.slice(i + 2, j).trim();
225
+ const out = (await runCmd(sub)).replace(/\n$/, "");
226
+ result += out;
227
+ i = j + 1;
228
+ continue;
229
+ }
230
+
231
+ result += ch;
232
+ i++;
233
+ }
234
+ input = result;
235
+ }
236
+
237
+ return expandSync(input, env, lastExit);
238
+ }