typescript-virtual-container 1.3.4 → 1.4.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 (368) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +674 -1504
  3. package/benchmark-results.txt +21 -21
  4. package/builds/self-standalone.js +282 -332
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +218 -282
  7. package/builds/standalone-wo-sftp.js.map +4 -4
  8. package/builds/standalone.js +271 -335
  9. package/builds/standalone.js.map +4 -4
  10. package/builds/web-full-api.min.js +3 -3
  11. package/builds/web-full-api.min.js.map +4 -4
  12. package/builds/web.min.js +2 -2
  13. package/builds/web.min.js.map +4 -4
  14. package/bun.lock +14 -12
  15. package/dist/SSHClient/index.d.ts.map +1 -1
  16. package/dist/SSHClient/index.js +5 -3
  17. package/dist/SSHMimic/executor.d.ts +1 -3
  18. package/dist/SSHMimic/executor.d.ts.map +1 -1
  19. package/dist/SSHMimic/executor.js +20 -22
  20. package/dist/SSHMimic/index.d.ts.map +1 -1
  21. package/dist/SSHMimic/index.js +5 -3
  22. package/dist/SSHMimic/sftp.d.ts.map +1 -1
  23. package/dist/SSHMimic/sftp.js +26 -21
  24. package/dist/VirtualPackageManager/index.d.ts.map +1 -1
  25. package/dist/VirtualPackageManager/index.js +29 -1
  26. package/dist/VirtualShell/shell.d.ts.map +1 -1
  27. package/dist/VirtualShell/shell.js +25 -3
  28. package/dist/VirtualShell/shellParser.d.ts +1 -8
  29. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  30. package/dist/VirtualShell/shellParser.js +2 -81
  31. package/dist/VirtualUserManager/index.d.ts +7 -1
  32. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  33. package/dist/VirtualUserManager/index.js +47 -16
  34. package/dist/commands/adduser.d.ts +10 -4
  35. package/dist/commands/adduser.d.ts.map +1 -1
  36. package/dist/commands/adduser.js +75 -12
  37. package/dist/commands/alias.d.ts +5 -0
  38. package/dist/commands/alias.d.ts.map +1 -1
  39. package/dist/commands/alias.js +5 -0
  40. package/dist/commands/apt.d.ts +5 -0
  41. package/dist/commands/apt.d.ts.map +1 -1
  42. package/dist/commands/apt.js +5 -0
  43. package/dist/commands/awk.d.ts +10 -8
  44. package/dist/commands/awk.d.ts.map +1 -1
  45. package/dist/commands/awk.js +156 -28
  46. package/dist/commands/cd.d.ts.map +1 -1
  47. package/dist/commands/cd.js +0 -3
  48. package/dist/commands/clear.d.ts +5 -0
  49. package/dist/commands/clear.d.ts.map +1 -1
  50. package/dist/commands/clear.js +5 -0
  51. package/dist/commands/command-helpers.d.ts.map +1 -1
  52. package/dist/commands/command-helpers.js +8 -0
  53. package/dist/commands/curl.d.ts.map +1 -1
  54. package/dist/commands/curl.js +2 -1
  55. package/dist/commands/declare.d.ts +5 -0
  56. package/dist/commands/declare.d.ts.map +1 -1
  57. package/dist/commands/declare.js +5 -0
  58. package/dist/commands/deluser.d.ts +12 -0
  59. package/dist/commands/deluser.d.ts.map +1 -1
  60. package/dist/commands/deluser.js +72 -6
  61. package/dist/commands/df.d.ts +5 -0
  62. package/dist/commands/df.d.ts.map +1 -1
  63. package/dist/commands/df.js +5 -0
  64. package/dist/commands/du.d.ts +5 -0
  65. package/dist/commands/du.d.ts.map +1 -1
  66. package/dist/commands/du.js +5 -0
  67. package/dist/commands/export.d.ts +5 -0
  68. package/dist/commands/export.d.ts.map +1 -1
  69. package/dist/commands/export.js +5 -0
  70. package/dist/commands/grep.d.ts.map +1 -1
  71. package/dist/commands/grep.js +22 -4
  72. package/dist/commands/groups.d.ts +5 -0
  73. package/dist/commands/groups.d.ts.map +1 -1
  74. package/dist/commands/groups.js +5 -0
  75. package/dist/commands/gzip.d.ts +5 -2
  76. package/dist/commands/gzip.d.ts.map +1 -1
  77. package/dist/commands/gzip.js +54 -28
  78. package/dist/commands/head.d.ts.map +1 -1
  79. package/dist/commands/head.js +12 -3
  80. package/dist/commands/htop.d.ts +5 -0
  81. package/dist/commands/htop.d.ts.map +1 -1
  82. package/dist/commands/htop.js +5 -0
  83. package/dist/commands/kill.d.ts +5 -0
  84. package/dist/commands/kill.d.ts.map +1 -1
  85. package/dist/commands/kill.js +5 -0
  86. package/dist/commands/ln.d.ts +2 -0
  87. package/dist/commands/ln.d.ts.map +1 -1
  88. package/dist/commands/ln.js +22 -0
  89. package/dist/commands/ls.d.ts.map +1 -1
  90. package/dist/commands/ls.js +15 -0
  91. package/dist/commands/lsb-release.d.ts +5 -0
  92. package/dist/commands/lsb-release.d.ts.map +1 -1
  93. package/dist/commands/lsb-release.js +5 -0
  94. package/dist/commands/man.d.ts.map +1 -1
  95. package/dist/commands/man.js +30 -136
  96. package/dist/commands/mkdir.d.ts +5 -0
  97. package/dist/commands/mkdir.d.ts.map +1 -1
  98. package/dist/commands/mkdir.js +5 -0
  99. package/dist/commands/mv.d.ts +5 -0
  100. package/dist/commands/mv.d.ts.map +1 -1
  101. package/dist/commands/mv.js +5 -0
  102. package/dist/commands/nano.d.ts +5 -0
  103. package/dist/commands/nano.d.ts.map +1 -1
  104. package/dist/commands/nano.js +5 -0
  105. package/dist/commands/neofetch.d.ts +5 -0
  106. package/dist/commands/neofetch.d.ts.map +1 -1
  107. package/dist/commands/neofetch.js +14 -5
  108. package/dist/commands/passwd.d.ts +8 -0
  109. package/dist/commands/passwd.d.ts.map +1 -1
  110. package/dist/commands/passwd.js +32 -11
  111. package/dist/commands/ping.d.ts +5 -0
  112. package/dist/commands/ping.d.ts.map +1 -1
  113. package/dist/commands/ping.js +5 -0
  114. package/dist/commands/printf.d.ts +5 -0
  115. package/dist/commands/printf.d.ts.map +1 -1
  116. package/dist/commands/printf.js +43 -12
  117. package/dist/commands/ps.d.ts +5 -0
  118. package/dist/commands/ps.d.ts.map +1 -1
  119. package/dist/commands/ps.js +5 -0
  120. package/dist/commands/read.d.ts +5 -0
  121. package/dist/commands/read.d.ts.map +1 -1
  122. package/dist/commands/read.js +5 -0
  123. package/dist/commands/registry.d.ts.map +1 -1
  124. package/dist/commands/registry.js +4 -1
  125. package/dist/commands/rm.d.ts +5 -0
  126. package/dist/commands/rm.d.ts.map +1 -1
  127. package/dist/commands/rm.js +5 -0
  128. package/dist/commands/runtime.d.ts.map +1 -1
  129. package/dist/commands/runtime.js +1 -57
  130. package/dist/commands/sed.d.ts +5 -0
  131. package/dist/commands/sed.d.ts.map +1 -1
  132. package/dist/commands/sed.js +5 -0
  133. package/dist/commands/set.d.ts +5 -6
  134. package/dist/commands/set.d.ts.map +1 -1
  135. package/dist/commands/set.js +5 -22
  136. package/dist/commands/sh.d.ts +6 -0
  137. package/dist/commands/sh.d.ts.map +1 -1
  138. package/dist/commands/sh.js +6 -0
  139. package/dist/commands/shift.d.ts +10 -0
  140. package/dist/commands/shift.d.ts.map +1 -1
  141. package/dist/commands/shift.js +10 -0
  142. package/dist/commands/sleep.d.ts +5 -0
  143. package/dist/commands/sleep.d.ts.map +1 -1
  144. package/dist/commands/sleep.js +5 -0
  145. package/dist/commands/sort.d.ts +5 -0
  146. package/dist/commands/sort.d.ts.map +1 -1
  147. package/dist/commands/sort.js +5 -0
  148. package/dist/commands/source.d.ts +5 -0
  149. package/dist/commands/source.d.ts.map +1 -1
  150. package/dist/commands/source.js +5 -0
  151. package/dist/commands/stat.d.ts +7 -0
  152. package/dist/commands/stat.d.ts.map +1 -0
  153. package/dist/commands/stat.js +56 -0
  154. package/dist/commands/su.d.ts +13 -0
  155. package/dist/commands/su.d.ts.map +1 -1
  156. package/dist/commands/su.js +45 -14
  157. package/dist/commands/sudo.d.ts.map +1 -1
  158. package/dist/commands/sudo.js +5 -0
  159. package/dist/commands/tail.d.ts +5 -0
  160. package/dist/commands/tail.d.ts.map +1 -1
  161. package/dist/commands/tail.js +15 -3
  162. package/dist/commands/tar.d.ts +5 -0
  163. package/dist/commands/tar.d.ts.map +1 -1
  164. package/dist/commands/tar.js +40 -10
  165. package/dist/commands/tee.d.ts +5 -0
  166. package/dist/commands/tee.d.ts.map +1 -1
  167. package/dist/commands/tee.js +5 -0
  168. package/dist/commands/touch.d.ts +5 -0
  169. package/dist/commands/touch.d.ts.map +1 -1
  170. package/dist/commands/touch.js +5 -0
  171. package/dist/commands/tr.d.ts.map +1 -1
  172. package/dist/commands/tr.js +45 -10
  173. package/dist/commands/tree.d.ts +5 -0
  174. package/dist/commands/tree.d.ts.map +1 -1
  175. package/dist/commands/tree.js +5 -0
  176. package/dist/commands/true.d.ts +10 -0
  177. package/dist/commands/true.d.ts.map +1 -1
  178. package/dist/commands/true.js +10 -0
  179. package/dist/commands/type.d.ts +5 -0
  180. package/dist/commands/type.d.ts.map +1 -1
  181. package/dist/commands/type.js +5 -0
  182. package/dist/commands/uname.d.ts +5 -0
  183. package/dist/commands/uname.d.ts.map +1 -1
  184. package/dist/commands/uname.js +5 -0
  185. package/dist/commands/uniq.d.ts +5 -0
  186. package/dist/commands/uniq.d.ts.map +1 -1
  187. package/dist/commands/uniq.js +5 -0
  188. package/dist/commands/unset.d.ts +5 -0
  189. package/dist/commands/unset.d.ts.map +1 -1
  190. package/dist/commands/unset.js +5 -0
  191. package/dist/commands/uptime.d.ts +5 -0
  192. package/dist/commands/uptime.d.ts.map +1 -1
  193. package/dist/commands/uptime.js +5 -0
  194. package/dist/commands/wc.d.ts +5 -0
  195. package/dist/commands/wc.d.ts.map +1 -1
  196. package/dist/commands/wc.js +5 -0
  197. package/dist/commands/wget.d.ts +5 -0
  198. package/dist/commands/wget.d.ts.map +1 -1
  199. package/dist/commands/wget.js +16 -1
  200. package/dist/commands/who.d.ts +5 -0
  201. package/dist/commands/who.d.ts.map +1 -1
  202. package/dist/commands/who.js +5 -0
  203. package/dist/commands/whoami.d.ts +5 -0
  204. package/dist/commands/whoami.d.ts.map +1 -1
  205. package/dist/commands/whoami.js +5 -0
  206. package/dist/commands/xargs.d.ts +5 -0
  207. package/dist/commands/xargs.d.ts.map +1 -1
  208. package/dist/commands/xargs.js +5 -0
  209. package/dist/self-standalone.js +254 -30
  210. package/dist/types/commands.d.ts +36 -0
  211. package/dist/types/commands.d.ts.map +1 -1
  212. package/dist/utils/tokenize.d.ts +20 -0
  213. package/dist/utils/tokenize.d.ts.map +1 -0
  214. package/dist/utils/tokenize.js +74 -0
  215. package/examples/web.min.js +2 -2
  216. package/package.json +2 -2
  217. package/src/SSHClient/index.ts +6 -3
  218. package/src/SSHMimic/executor.ts +21 -44
  219. package/src/SSHMimic/index.ts +7 -5
  220. package/src/SSHMimic/sftp.ts +28 -21
  221. package/src/VirtualPackageManager/index.ts +29 -1
  222. package/src/VirtualShell/shell.ts +34 -4
  223. package/src/VirtualShell/shellParser.ts +2 -103
  224. package/src/VirtualUserManager/index.ts +43 -19
  225. package/src/commands/adduser.ts +86 -13
  226. package/src/commands/alias.ts +5 -0
  227. package/src/commands/apt.ts +5 -0
  228. package/src/commands/awk.ts +154 -29
  229. package/src/commands/cd.ts +0 -4
  230. package/src/commands/clear.ts +5 -0
  231. package/src/commands/command-helpers.ts +9 -0
  232. package/src/commands/curl.ts +2 -1
  233. package/src/commands/declare.ts +5 -0
  234. package/src/commands/deluser.ts +84 -7
  235. package/src/commands/df.ts +5 -0
  236. package/src/commands/du.ts +5 -0
  237. package/src/commands/export.ts +5 -0
  238. package/src/commands/grep.ts +21 -8
  239. package/src/commands/groups.ts +5 -0
  240. package/src/commands/gzip.ts +61 -28
  241. package/src/commands/head.ts +14 -4
  242. package/src/commands/htop.ts +5 -0
  243. package/src/commands/kill.ts +5 -0
  244. package/src/commands/ln.ts +22 -0
  245. package/src/commands/ls.ts +17 -0
  246. package/src/commands/lsb-release.ts +5 -0
  247. package/src/commands/man.ts +38 -143
  248. package/src/commands/manuals/adduser.txt +11 -0
  249. package/src/commands/manuals/apt-cache.txt +12 -0
  250. package/src/commands/manuals/apt.txt +20 -0
  251. package/src/commands/manuals/awk.txt +13 -0
  252. package/src/commands/manuals/cat.txt +14 -0
  253. package/src/commands/manuals/cd.txt +16 -0
  254. package/src/commands/manuals/chmod.txt +16 -0
  255. package/src/commands/manuals/clear.txt +10 -0
  256. package/src/commands/manuals/cp.txt +10 -0
  257. package/src/commands/manuals/curl.txt +20 -0
  258. package/src/commands/manuals/date.txt +14 -0
  259. package/src/commands/manuals/declare.txt +12 -0
  260. package/src/commands/manuals/deluser.txt +10 -0
  261. package/src/commands/manuals/df.txt +10 -0
  262. package/src/commands/manuals/dpkg-query.txt +11 -0
  263. package/src/commands/manuals/dpkg.txt +14 -0
  264. package/src/commands/manuals/du.txt +11 -0
  265. package/src/commands/manuals/echo.txt +11 -0
  266. package/src/commands/manuals/false.txt +10 -0
  267. package/src/commands/manuals/find.txt +11 -0
  268. package/src/commands/manuals/free.txt +12 -0
  269. package/src/commands/manuals/grep.txt +13 -0
  270. package/src/commands/manuals/groups.txt +10 -0
  271. package/src/commands/manuals/gzip.txt +11 -0
  272. package/src/commands/manuals/head.txt +10 -0
  273. package/src/commands/manuals/help.txt +11 -0
  274. package/src/commands/manuals/history.txt +11 -0
  275. package/src/commands/manuals/hostname.txt +10 -0
  276. package/src/commands/manuals/id.txt +10 -0
  277. package/src/commands/manuals/kill.txt +13 -0
  278. package/src/commands/manuals/ls.txt +20 -0
  279. package/src/commands/manuals/lsb_release.txt +14 -0
  280. package/src/commands/manuals/mkdir.txt +10 -0
  281. package/src/commands/manuals/mv.txt +10 -0
  282. package/src/commands/manuals/nano.txt +11 -0
  283. package/src/commands/manuals/neofetch.txt +10 -0
  284. package/src/commands/manuals/node.txt +13 -0
  285. package/src/commands/manuals/npm.txt +13 -0
  286. package/src/commands/manuals/npx.txt +13 -0
  287. package/src/commands/manuals/passwd.txt +11 -0
  288. package/src/commands/manuals/ping.txt +10 -0
  289. package/src/commands/manuals/printf.txt +11 -0
  290. package/src/commands/manuals/ps.txt +10 -0
  291. package/src/commands/manuals/pwd.txt +10 -0
  292. package/src/commands/manuals/python3.txt +13 -0
  293. package/src/commands/manuals/readlink.txt +10 -0
  294. package/src/commands/manuals/return.txt +10 -0
  295. package/src/commands/manuals/rm.txt +10 -0
  296. package/src/commands/manuals/sed.txt +11 -0
  297. package/src/commands/manuals/set.txt +11 -0
  298. package/src/commands/manuals/shift.txt +10 -0
  299. package/src/commands/manuals/sleep.txt +10 -0
  300. package/src/commands/manuals/sort.txt +12 -0
  301. package/src/commands/manuals/source.txt +11 -0
  302. package/src/commands/manuals/ssh.txt +11 -0
  303. package/src/commands/manuals/stat.txt +10 -0
  304. package/src/commands/manuals/su.txt +13 -0
  305. package/src/commands/manuals/sudo.txt +11 -0
  306. package/src/commands/manuals/tail.txt +10 -0
  307. package/src/commands/manuals/tar.txt +19 -0
  308. package/src/commands/manuals/tee.txt +10 -0
  309. package/src/commands/manuals/test.txt +11 -0
  310. package/src/commands/manuals/touch.txt +11 -0
  311. package/src/commands/manuals/tr.txt +10 -0
  312. package/src/commands/manuals/trap.txt +10 -0
  313. package/src/commands/manuals/true.txt +10 -0
  314. package/src/commands/manuals/type.txt +10 -0
  315. package/src/commands/manuals/uname.txt +12 -0
  316. package/src/commands/manuals/uniq.txt +12 -0
  317. package/src/commands/manuals/unset.txt +10 -0
  318. package/src/commands/manuals/uptime.txt +11 -0
  319. package/src/commands/manuals/wc.txt +12 -0
  320. package/src/commands/manuals/wget.txt +12 -0
  321. package/src/commands/manuals/which.txt +10 -0
  322. package/src/commands/manuals/whoami.txt +10 -0
  323. package/src/commands/manuals/xargs.txt +10 -0
  324. package/src/commands/mkdir.ts +5 -0
  325. package/src/commands/mv.ts +5 -0
  326. package/src/commands/nano.ts +5 -0
  327. package/src/commands/neofetch.ts +15 -6
  328. package/src/commands/passwd.ts +35 -12
  329. package/src/commands/ping.ts +5 -0
  330. package/src/commands/printf.ts +30 -13
  331. package/src/commands/ps.ts +5 -0
  332. package/src/commands/read.ts +5 -0
  333. package/src/commands/registry.ts +4 -1
  334. package/src/commands/rm.ts +5 -0
  335. package/src/commands/runtime.ts +1 -61
  336. package/src/commands/sed.ts +5 -0
  337. package/src/commands/set.ts +5 -24
  338. package/src/commands/sh.ts +9 -3
  339. package/src/commands/shift.ts +10 -0
  340. package/src/commands/sleep.ts +5 -0
  341. package/src/commands/sort.ts +5 -0
  342. package/src/commands/source.ts +5 -0
  343. package/src/commands/stat.ts +61 -0
  344. package/src/commands/su.ts +54 -16
  345. package/src/commands/sudo.ts +5 -0
  346. package/src/commands/tail.ts +17 -3
  347. package/src/commands/tar.ts +38 -15
  348. package/src/commands/tee.ts +5 -0
  349. package/src/commands/touch.ts +5 -0
  350. package/src/commands/tr.ts +54 -10
  351. package/src/commands/tree.ts +5 -0
  352. package/src/commands/true.ts +10 -0
  353. package/src/commands/type.ts +5 -0
  354. package/src/commands/uname.ts +5 -0
  355. package/src/commands/uniq.ts +5 -0
  356. package/src/commands/unset.ts +5 -0
  357. package/src/commands/uptime.ts +5 -0
  358. package/src/commands/wc.ts +5 -0
  359. package/src/commands/wget.ts +17 -1
  360. package/src/commands/who.ts +5 -0
  361. package/src/commands/whoami.ts +5 -0
  362. package/src/commands/xargs.ts +5 -0
  363. package/src/self-standalone.ts +316 -33
  364. package/src/types/commands.ts +37 -0
  365. package/src/utils/tokenize.ts +78 -0
  366. package/tests/new-features.test.ts +2 -2
  367. package/builds/web-iife.min.js +0 -13
  368. package/builds/web-iife.min.js.map +0 -7
package/README.md CHANGED
@@ -1,289 +1,198 @@
1
1
  # `typescript-virtual-container`
2
2
 
3
- > Pure in-memory SSH/SFTP server with a realistic Linux rootfs, a virtual package manager, a real shell interpreter, and a typed programmatic API for testing, automation, honeypots, and interactive shell scripting in TypeScript/JavaScript.
3
+ > A complete virtual Linux environment in pure TypeScript — runs as an SSH/SFTP server, a browser-based web shell, or a standalone CLI. Ships with a realistic Linux rootfs, a virtual package manager, a full shell interpreter, and a typed programmatic API for testing, automation, honeypots, and embedded shell experiences.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/typescript-virtual-container.svg)](https://www.npmjs.com/package/typescript-virtual-container)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
8
  [![Runtime](https://img.shields.io/badge/runtime-Node.js%20%7C%20Bun-43853D.svg)](https://nodejs.org/)
9
9
 
10
+ ---
11
+
10
12
  ## Table of Contents
11
13
 
12
- - [Overview](#overview)
13
- - [What This Is / What This Is Not](#what-this-is--what-this-is-not)
14
- - [Why This Package](#why-this-package)
15
- - [Installation](#installation)
16
- - [Browser Build (web.min.js)](#browser-build-webminjs)
17
- - [Compatibility](#compatibility)
18
- - [Quick Start](#quick-start)
19
- - [Architecture Overview](#architecture-overview)
20
- - [API Reference](#api-reference)
21
- - [VirtualSshServer](#virtualsshserver)
22
- - [VirtualSftpServer](#virtualsftpserver)
23
- - [VirtualShell](#virtualshell)
24
- - [VirtualFileSystem](#virtualfilesystem)
25
- - [VFSB Binary Format](#vfsb-binary-format)
26
- - [VirtualUserManager](#virtualusermanager)
27
- - [VirtualPackageManager](#virtualpackagemanager)
28
- - [HoneyPot](#honeypot)
29
- - [SshClient](#sshclient-programmatic-api)
14
+ - [Three ways to run](#three-ways-to-run)
15
+ - [Get Started](#get-started)
16
+ - [Install](#install)
17
+ - [Try instantly (zero install)](#try-instantly-zero-install)
18
+ - [SSH server](#ssh-server)
19
+ - [Web shell (browser)](#web-shell-browser)
20
+ - [Programmatic API](#programmatic-api)
21
+ - [How It Works](#how-it-works)
22
+ - [API Reference](#api-reference) *(open by default)*
23
+ - [`VirtualSshServer`](#virtualsshserver)
24
+ - [`VirtualSftpServer`](#virtualsftpserver)
25
+ - [`VirtualShell`](#virtualshell)
26
+ - [`VirtualFileSystem`](#virtualfilesystem)
27
+ - [`VirtualUserManager`](#virtualusermanager)
28
+ - [`VirtualPackageManager`](#virtualpackagemanager)
29
+ - [Snapshot Diff Tooling](#snapshot-diff-tooling)
30
+ - [`HoneyPot`](#honeypot)
31
+ - [`SshClient`](#sshclient-programmatic-api)
30
32
  - [Key Types](#key-types)
31
- - [Usage Examples](#usage-examples)
32
- - [Linux Rootfs](#linux-rootfs)
33
- - [Package Manager (apt/dpkg)](#package-manager-aptdpkg)
34
- - [Built-in Commands](#built-in-commands)
33
+ - [Command Helpers](#command-helpers)
34
+ - [Examples](#examples)
35
+ - [Built-in Commands (91)](#built-in-commands-91)
35
36
  - [Shell Scripting](#shell-scripting)
37
+ - [Linux Rootfs & VFS PATH Resolution](#linux-rootfs--vfs-path-resolution)
36
38
  - [Configuration](#configuration)
37
39
  - [Performance & Scalability](#performance--scalability)
38
40
  - [Types & TypeScript](#types--typescript)
39
- - [FAQ](#faq)
40
41
  - [Troubleshooting](#troubleshooting)
42
+ - [FAQ](#faq)
41
43
  - [Contributing](#contributing)
42
44
  - [Security](#security)
43
- - [Support](#support)
45
+ - [Compatibility](#compatibility)
44
46
  - [License](#license)
45
47
  - [Roadmap](#roadmap)
46
- - [Changelog](#changelog)
47
-
48
- ---
49
-
50
- ## Overview
51
-
52
- `typescript-virtual-container` is a lightweight, fully-typed SSH/SFTP runtime written in TypeScript that provides:
53
-
54
- - **Pure in-memory filesystem**: No disk I/O at runtime. All state lives in a fast recursive in-memory tree. Persist via a compact binary snapshot format (`.vfsb`) or JSON for interoperability.
55
- - **SSH + SFTP Protocol Support**: Serve SSH shell/exec sessions and SFTP file operations on configurable ports.
56
- - **Password & public-key authentication**: Register SSH public keys per user alongside (or instead of) password auth.
57
- - **Rate limiting / brute-force protection**: Configurable per-IP lockout after N failed auth attempts.
58
- - **User Management**: Create, authenticate, and manage virtual users with scrypt password hashing, sudo-like privilege elevation, and optional per-user disk quotas.
59
- - **Programmatic Shell API**: Execute shell commands and query filesystem state directly from TypeScript without SSH overhead.
60
- - **Real shell interpreter**: `&&` / `||` / `;` operators, `if`/`elif`/`else`/`fi`, `for`/`do`/`done`, `while`/`do`/`done`, variable expansion (`$VAR`, `${VAR:-default}`), `$?`, per-session environment.
61
- - **`.bashrc` support**: Loaded automatically at interactive session start from `/home/<user>/.bashrc`.
62
- - **Event-Driven Architecture**: All core classes extend `EventEmitter` for lifecycle and operation tracking.
63
- - **Security Auditing**: Built-in `HoneyPot` utility for comprehensive activity logging, event tracking, statistics collection, and anomaly detection across all components.
64
- - **Linux rootfs on boot**: Realistic `/etc`, `/proc`, `/sys`, `/dev`, `/usr`, `/var` hierarchy populated at startup — `os-release`, `passwd`, `hosts`, `resolv.conf`, `/proc/meminfo`, `/proc/cpuinfo`, and more.
65
- - **Virtual package manager**: `apt install`, `apt remove`, `apt search`, `dpkg -l`, `dpkg -s` — 25 packages in the built-in registry (vim, git, nodejs, python3, curl, openssh, gcc…). Writes files into VFS, tracks state in `/var/lib/dpkg/status`.
66
- - **87 Built-in Commands**: Full navigation, text processing, archiving, system info, package management, and user management commands — grouped and documented in the interactive `help` system.
67
- - **`$(cmd)` command substitution**: Nested command execution in any argument position.
68
- - **Alias support**: `alias`, `unalias` — persisted in session environment.
69
- - **Full TypeScript Support**: Complete JSDoc coverage, exported types, and first-class async/await for all operations.
70
-
71
- ---
72
-
73
- ## What This Is / What This Is Not
74
-
75
- ### What This Is
76
-
77
- - A virtual shell runtime written in TypeScript with a **pure in-memory filesystem**.
78
- - A virtual environment with its own filesystem, user management, and a real shell interpreter.
79
- - A practical tool for deterministic testing, automation pipelines, and SSH-like workflows without running real containers.
80
- - A honeypot framework for capturing and auditing attacker behavior.
81
-
82
- TL;DR: this is a shell emulator and virtual environment for developer workflows, not a security sandbox or container runtime.
83
-
84
- ### What This Is Not
85
-
86
- - Not a fully isolated container runtime.
87
- - Not a kernel-level isolation boundary (unlike Docker/VM-based isolation).
88
- - Package stubs (e.g. `node`, `python3`) write files into the VFS and are visible to `which`/`dpkg -L`, but do not execute real binaries — the shell is pure TypeScript with no `execvp`.
89
-
90
- This project emulates shell behavior for developer workflows. `curl` and `wget` use the native `fetch()` API (no host binary). All other network and execution primitives are simulated. It is designed for realism and deployability, not kernel-level security isolation.
91
-
92
- TL;DR: this is not a secure sandbox for running untrusted code. Do not expose it to untrusted users or the public internet without additional isolation layers.
93
48
 
94
49
  ---
95
50
 
96
- ## Why This Package
51
+ ## Three ways to run
97
52
 
98
- This package is designed for teams that need a realistic SSH-like runtime without spinning up real containers or VMs.
53
+ | Mode | Entry point | Use case |
54
+ |------|-------------|----------|
55
+ | **SSH/SFTP server** | `VirtualSshServer` / `VirtualSftpServer` | Honeypots, remote testing, training environments |
56
+ | **Web shell** | `builds/web.min.js` (browser bundle) | Embedded terminals, interactive tutorials, browser demos |
57
+ | **Standalone CLI** | `builds/self-standalone.js` (single file) | Local shell, one-liner demos, no install required |
99
58
 
100
- - **Zero disk footprint by default**: The VFS operates entirely in memory. Opt into binary snapshot persistence (`.vfsb`) when you need durability — ~27% smaller and significantly faster than JSON+base64.
101
- - **Deterministic test environments**: Repeatable state for CI pipelines and integration tests. Build a fixture snapshot once, hydrate for each test.
102
- - **Low operational overhead**: No Docker daemon, no kernel namespaces, no privileged setup.
103
- - **Fast feedback loops**: Programmatic API for command execution and filesystem assertions.
104
- - **Real shell scripting**: `&&`/`||`/`;`, `if`/`for`/`while`, variable expansion — not just command dispatch.
105
- - **Developer-friendly internals**: Typed APIs, clear boundaries, composable building blocks, and full JSDoc.
59
+ All three modes share the same core: a pure in-memory VFS, a real shell interpreter, a virtual package manager, and a typed programmatic API.
106
60
 
107
61
  ---
108
62
 
109
- ## Installation
63
+ ## Get Started
110
64
 
111
- ### From npm
65
+ ### Install
112
66
 
113
67
  ```bash
114
68
  npm install typescript-virtual-container
115
- # or
116
- yarn add typescript-virtual-container
117
- # or
118
- bun add typescript-virtual-container
69
+ # or: yarn add / bun add
119
70
  ```
120
71
 
121
- ### From source (development)
72
+ ### Try instantly (zero install)
122
73
 
123
74
  ```bash
124
- git clone https://github.com/itsrealfortune/typescript-virtual-container/
125
- cd typescript-virtual-container
126
- bun install
127
- bun format # Format code per Biome
128
- bun check # Lint and typecheck
129
- bun run build
130
- ```
75
+ # Interactive local shell — persists VFS in .vfs/ in the current directory
76
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/self-standalone.js -o self-standalone.js && node self-standalone.js && rm -f self-standalone.js
77
+
78
+ # SSH server (connect with any SSH client on port 2222)
79
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/standalone.js -o standalone.js && node standalone.js && rm -f standalone.js
131
80
 
132
- ### Standalone (zero install)
81
+ # SSH server without SFTP (lighter build)
82
+ curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/standalone-wo-sftp.js -o standalone.js && node standalone.js && rm -f standalone.js
83
+ ```
133
84
 
134
- To quickly try a standalone demo:
85
+ **`self-standalone.js` options:**
135
86
 
136
87
  ```bash
137
- curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/standalone.js -o standalone.js && node standalone.js && rm -f standalone.js
88
+ node self-standalone.js # boot as root
89
+ node self-standalone.js --user alice # boot as alice (prompts for password if set)
90
+ node self-standalone.js --user=alice # same, inline form
91
+ SSH_MIMIC_HOSTNAME=my-box node self-standalone.js # custom hostname
138
92
  ```
139
93
 
140
- ### Interactive Readline Demo
94
+ The VFS is persisted automatically to `.vfs/vfs-snapshot.vfsb` in the current directory — state survives between runs. Delete `.vfs/` to start fresh.
141
95
 
142
- Build the local readline-based SSH mimic and connect as a specific user:
96
+ The shell shows a login banner and `Last login:` timestamp on each start.
143
97
 
144
- ```bash
145
- bun run self-standalone-build
146
- node builds/self-standalone.js --user root
147
- ```
98
+ ### SSH server
148
99
 
149
- If the selected user exists and has a password, the demo prompts for it. Root skips the password prompt.
100
+ ```typescript
101
+ import { VirtualSshServer } from "typescript-virtual-container";
150
102
 
151
- Standalone JS build is executable from:
152
- ```bash
153
- curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/builds/self-standalone.js -o self-standalone.js && node self-standalone.js && rm -f self-standalone.js
103
+ const ssh = new VirtualSshServer({ port: 2222, hostname: "my-container" });
104
+ await ssh.start();
105
+ // ssh root@localhost -p 2222
154
106
  ```
155
- ---
156
107
 
157
- ## Browser Build (web.min.js)
108
+ ### Web shell (browser)
158
109
 
159
- The package includes a browser-only runtime entrypoint in `src/web.ts`.
160
- It runs the shell fully in-browser and persists the virtual filesystem in IndexedDB.
110
+ Three browser bundles are available pick the one that fits your deployment:
161
111
 
162
- Build the minified browser bundle:
112
+ | Bundle | Format | Entry | Use case |
113
+ |--------|--------|-------|----------|
114
+ | `builds/web.min.js` | ESM | `createWebShell()` | Local dev, modern bundlers |
115
+ | `builds/web-iife.min.js` | IIFE (`WebShellLib`) | `WebShellLib.createWebShell()` | Cloudflare, CDN, reverse proxies |
116
+ | `builds/web-full-api.min.js` | ESM | `createVirtualShellShim()` | API surface close to `VirtualShell` |
117
+
118
+ All bundles persist the VFS in **IndexedDB** — state survives page reloads.
163
119
 
164
120
  ```bash
165
- bun run web-build
121
+ bun run web-build # → builds/web.min.js + examples/web.min.js
122
+ bun run web-build-iife # → builds/web-iife.min.js
123
+ bun run web-full-build # → builds/web-full-api.min.js
124
+ bun run build-all # rebuild everything
166
125
  ```
167
126
 
168
- This generates:
169
-
170
- - `builds/web.min.js`
171
- - `builds/web.min.js.map`
172
-
173
- Use it from a browser module script:
127
+ **ESM (`web.min.js`)**
174
128
 
175
129
  ```html
176
130
  <script type="module">
177
131
  import { createWebShell } from "./builds/web.min.js";
178
132
 
179
133
  const shell = createWebShell("web-vm", {
180
- indexedDbName: "virtual-env-js",
181
- storeName: "vfs",
134
+ vfs: { databaseName: "virtual-env-js", storeName: "vfs" },
182
135
  });
136
+ await shell.ensureInitialized();
183
137
 
184
- const out = await shell.executeCommandLine("pwd && mkdir -p /tmp/demo && cd /tmp/demo && echo hello > file.txt && ls -la");
138
+ const out = await shell.executeCommandLine("ls /etc && echo hello");
185
139
  console.log(out.stdout);
186
140
  </script>
187
141
  ```
188
142
 
189
- Notes:
190
-
191
- - This build is browser-targeted and does not include SSH/SFTP networking servers.
192
- - State is mirrored to IndexedDB via the VFS mirror implementation.
193
- - If you need SSH/SFTP, use the Node standalone builds instead (`standalone.js` or `standalone-wo-sftp.js`). For a local SSH-like terminal, use `builds/self-standalone.js`.
194
-
195
- ---
196
-
197
- ## Compatibility
198
-
199
- - **Node.js**: Recommended `>=18`
200
- - **Bun**: Supported for development and runtime
201
- - **TypeScript**: Recommended `>=5.0`
202
- - **OS**: Linux, macOS, and Windows (via Node/Bun runtime)
203
-
204
- The virtual filesystem and shell behavior are intentionally portable and do not depend on host-specific POSIX syscalls.
205
-
206
- ---
207
-
208
- ## Quick Start
209
-
210
- ### Running an SSH Server
143
+ **IIFE (`web-iife.min.js`)** — no `type="module"` needed, works through Cloudflare:
211
144
 
212
- ```typescript
213
- import { VirtualSshServer } from "typescript-virtual-container";
214
-
215
- const ssh = new VirtualSshServer({
216
- port: 2222,
217
- hostname: "my-container",
218
- });
219
-
220
- await ssh.start();
221
- console.log("SSH server listening on :2222");
222
-
223
- // Connect externally:
224
- // ssh root@localhost -p 2222
225
- // root has no password by default — login is allowed without verification.
226
-
227
- process.on("SIGTERM", () => {
228
- ssh.stop();
229
- process.exit(0);
230
- });
145
+ ```html
146
+ <script src="./builds/web-iife.min.js"></script>
147
+ <script>
148
+ const shell = WebShellLib.createWebShell("web-vm");
149
+ shell.ensureInitialized().then(() =>
150
+ shell.executeCommandLine("whoami").then(r => console.log(r.stdout))
151
+ );
152
+ </script>
231
153
  ```
232
154
 
233
- ### Running SSH + SFTP with Shared State
234
-
235
- ```typescript
236
- import { VirtualSftpServer, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
155
+ **Full API shim (`web-full-api.min.js`)** mirrors the `VirtualShell` programmatic API:
237
156
 
238
- const shell = new VirtualShell("my-container");
157
+ ```html
158
+ <script type="module">
159
+ import { createVirtualShellShim } from "./builds/web-full-api.min.js";
239
160
 
240
- const ssh = new VirtualSshServer({ port: 2222, hostname: "my-container", shell });
241
- const sftp = new VirtualSftpServer({ port: 2223, hostname: "my-container", shell });
161
+ const shell = createVirtualShellShim("web-vm");
162
+ await shell.ensureInitialized();
163
+ await shell.executeCommandLine("mkdir -p /app && echo hello > /app/file.txt");
164
+ const vfs = shell.getVfs();
165
+ console.log(vfs.readFile("/app/file.txt")); // hello
166
+ </script>
167
+ ```
242
168
 
243
- await ssh.start();
244
- await sftp.start();
169
+ **Run the demo locally:**
245
170
 
246
- console.log("SSH on :2222, SFTP on :2223");
171
+ ```bash
172
+ bun run example-serve
173
+ # Open http://localhost:8787/index.html (ESM)
174
+ # or http://localhost:8787/index-cf.html (IIFE, Cloudflare-compatible)
247
175
  ```
248
176
 
249
- ### Using the Programmatic Client API
177
+ ### Programmatic API
250
178
 
251
179
  ```typescript
252
- import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
253
-
254
- const shell = new VirtualShell("typescript-vm");
255
- const ssh = new VirtualSshServer({ port: 2222, shell });
256
- await ssh.start();
180
+ import { SshClient, VirtualShell } from "typescript-virtual-container";
257
181
 
182
+ const shell = new VirtualShell("typescript-vm");
183
+ await shell.ensureInitialized();
258
184
  const client = new SshClient(shell, "root");
259
185
 
260
- const list = await client.ls("/home");
261
- console.log("stdout:", list.stdout);
262
-
263
- const result = await client.pwd();
264
- console.log("Current dir:", result.stdout);
265
-
266
- await client.mkdir("/tmp/work", true);
267
- await client.cd("/tmp/work");
268
-
269
- const content = await client.readFile("/etc/hostname");
270
- console.log("Hostname file:", content.stdout);
271
-
272
- await client.writeFile("output.txt", "Hello, World!");
186
+ await client.mkdir("/app/config", true);
187
+ await client.writeFile("/app/config/settings.json", JSON.stringify({ env: "dev" }));
273
188
 
274
- ssh.stop();
189
+ const r = await client.exec("ls /app && cat /app/config/settings.json");
190
+ console.log(r.stdout);
275
191
  ```
276
192
 
277
193
  ---
278
194
 
279
- ## Architecture Overview
280
-
281
- ### Execution Modes
282
-
283
- 1. **SSH Shell Mode**: Interactive terminal session over SSH with readline, history, `.bashrc` loading, TTY resizing, `Ctrl+W` word delete, `Ctrl+U` line clear.
284
- 2. **SSH Exec Mode**: Non-interactive command execution (e.g. `ssh user@host "ls -la"`).
285
- 3. **SFTP Mode**: Remote file operations (`readdir`, `stat`, `readFile`, `writeFile`, `mkdir`, `rename`, etc.) with home-directory confinement.
286
- 4. **Programmatic Mode**: Direct TypeScript API via `SshClient` — no SSH protocol overhead.
195
+ ## How It Works
287
196
 
288
197
  ```
289
198
  ┌──────────────────────────────────────────────────────────────────────────┐
@@ -294,7 +203,7 @@ ssh.stop();
294
203
 
295
204
  ┌──────────▼──────────┐
296
205
  │ VirtualShell │
297
- │ script parser │ ← &&/||/; · if/for/while
206
+ │ script parser │ ← &&/||/; · if/for/while/case
298
207
  │ command executor │ ← per-session ShellEnv
299
208
  │ .bashrc loader │ ← /home/<user>/.bashrc
300
209
  │ session manager │
@@ -315,682 +224,345 @@ ssh.stop();
315
224
  └─────────────────────────┘
316
225
  ```
317
226
 
318
- ---
227
+ **What it is:** a shell emulator and virtual Linux environment for developer workflows.
319
228
 
320
- ## API Reference
229
+ **What it is not:** a kernel-level security boundary. `curl`/`wget` use the native `fetch()` API — no host binary spawned. `node`/`python3`/`npm` are virtual REPL stubs, not real runtimes. `execvp` is never called. Do not expose to the public internet without additional isolation.
321
230
 
322
- ### `VirtualSshServer`
231
+ ---
323
232
 
324
- Main SSH server class. Wires the virtual shell runtime into `ssh2` sessions and manages authentication and session handlers.
233
+ <details open>
234
+ <summary><strong>API Reference</strong></summary>
325
235
 
326
- #### Constructor
236
+ ### `VirtualSshServer`
327
237
 
328
238
  ```typescript
329
239
  new VirtualSshServer({
330
- port: number; // TCP port to bind
331
- hostname?: string; // Virtual hostname (default: "typescript-vm")
332
- shell?: VirtualShell; // Optional shared shell instance (share state with SFTP)
333
- maxAuthAttempts?: number; // Max failed auth per IP before lockout (default: 5)
334
- lockoutDurationMs?: number; // Lockout duration in ms (default: 60_000)
240
+ port: number;
241
+ hostname?: string; // default: "typescript-vm"
242
+ shell?: VirtualShell; // share state with SFTP server
243
+ maxAuthAttempts?: number; // default: 5
244
+ lockoutDurationMs?: number; // default: 60_000
335
245
  })
336
246
  ```
337
247
 
338
248
  If `shell` is omitted, the server creates `new VirtualShell(hostname)` internally.
339
249
 
340
- **Example:**
341
-
342
- ```typescript
343
- const shell = new VirtualShell("my-lab", {
344
- kernel: "1.0.0+itsrealfortune+1-amd64",
345
- os: "Fortune GNU/Linux x64",
346
- arch: "x86_64",
347
- });
348
-
349
- const ssh = new VirtualSshServer({ port: 2222, hostname: "my-lab", shell });
350
- ```
351
-
352
- #### Methods
250
+ **Methods**
353
251
 
354
252
  | Method | Description |
355
253
  |--------|-------------|
356
254
  | `start(): Promise<number>` | Initialize VFS, users, start listening. Returns bound port. |
357
255
  | `stop(): void` | Gracefully close server and all active connections. |
358
- | `clearLockout(ip: string): void` | Manually lift a rate-limit lockout for an IP. |
256
+ | `clearLockout(ip): void` | Manually lift a rate-limit lockout for an IP. |
359
257
  | `getVfs(): VirtualFileSystem \| null` | Access VFS instance (null before start). |
360
258
  | `getUsers(): VirtualUserManager \| null` | Access user manager (null before start). |
361
259
  | `getHostname(): string` | Returns configured hostname. |
362
260
 
363
- #### Events
261
+ **Events**
364
262
 
365
263
  | Event | Data | Description |
366
264
  |-------|------|-------------|
367
- | `start` | `{ port: number }` | Server started and listening |
265
+ | `start` | `{ port }` | Server started |
368
266
  | `stop` | — | Server stopped |
369
- | `auth:success` | `{ username, remoteAddress, method? }` | User authenticated |
267
+ | `auth:success` | `{ username, remoteAddress, method? }` | Authenticated |
370
268
  | `auth:failure` | `{ username, remoteAddress, reason?, method? }` | Auth failed |
371
- | `auth:lockout` | `{ ip, until: Date }` | IP locked out after too many failures |
269
+ | `auth:lockout` | `{ ip, until: Date }` | IP locked out |
372
270
  | `client:connect` | — | New SSH client connected |
373
- | `client:disconnect` | `{ user: string }` | SSH client disconnected |
374
-
375
- **Example:**
376
-
377
- ```typescript
378
- ssh.on("auth:success", ({ username, remoteAddress }) => {
379
- console.log(`[SSH] ${username} authenticated from ${remoteAddress}`);
380
- });
381
-
382
- ssh.on("auth:lockout", ({ ip, until }) => {
383
- console.warn(`[SSH] ${ip} locked until ${until}`);
384
- });
385
- ```
271
+ | `client:disconnect` | `{ user }` | SSH client disconnected |
386
272
 
387
273
  ---
388
274
 
389
275
  ### `VirtualSftpServer`
390
276
 
391
- SFTP server class. Can share a `VirtualShell` with `VirtualSshServer` (recommended) or accept explicit `vfs` + `users` dependencies.
392
-
393
- #### Constructor
394
-
395
277
  ```typescript
396
278
  new VirtualSftpServer({
397
279
  port: number;
398
280
  hostname?: string;
399
- shell?: VirtualShell; // share state with SSH server
400
- vfs?: VirtualFileSystem; // explicit if no shell
401
- users?: VirtualUserManager; // explicit if no shell
281
+ shell?: VirtualShell; // share state with SSH server (recommended)
282
+ vfs?: VirtualFileSystem; // explicit if no shell
283
+ users?: VirtualUserManager; // explicit if no shell
402
284
  })
403
285
  ```
404
286
 
405
- #### Methods
406
-
407
- | Method | Description |
408
- |--------|-------------|
409
- | `start(): Promise<number>` | Start SFTP server, returns bound port. |
410
- | `stop(): void` | Stop SFTP server. |
411
-
412
- #### Behavior Notes
413
-
414
- - Supports `password` and `keyboard-interactive` authentication. Users without a password set are accepted on any attempt.
415
- - Resolves relative SFTP paths from `/home/<user>`.
416
- - Confines all SFTP operations to `/home/<user>` — blocks traversal attempts outside home.
417
- - Unsupported operations (`READLINK`, `SYMLINK`) return `OP_UNSUPPORTED`.
287
+ Supports `password` and `keyboard-interactive` auth. Confines all operations to `/home/<user>`. Unsupported operations return `OP_UNSUPPORTED`.
418
288
 
419
- #### Events
289
+ **Methods:** `start(): Promise<number>`, `stop(): void`
420
290
 
421
- | Event | Data | Description |
422
- |-------|------|-------------|
423
- | `start` | `{ port: number }` | SFTP server started |
424
- | `stop` | — | SFTP server stopped |
425
- | `auth:success` | `{ username, remoteAddress }` | User authenticated |
426
- | `auth:failure` | `{ username, remoteAddress }` | Auth failed |
427
- | `client:connect` | — | New SFTP client connected |
428
- | `client:disconnect` | `{ user: string }` | SFTP client disconnected |
291
+ **Events:** `start`, `stop`, `auth:success { username, remoteAddress }`, `auth:failure { username, remoteAddress }`, `client:connect`, `client:disconnect { user }`
429
292
 
430
293
  ---
431
294
 
432
295
  ### `VirtualShell`
433
296
 
434
- Coordinates the virtual filesystem, user manager, and command runtime. Used by both SSH servers and the programmatic `SshClient`.
435
-
436
- #### Constructor
297
+ Coordinates the VFS, user manager, package manager, and command runtime.
437
298
 
438
299
  ```typescript
439
300
  new VirtualShell(
440
301
  hostname: string,
441
- properties?: ShellProperties,
442
- vfsOptions?: VfsOptions,
302
+ properties?: ShellProperties, // kernel, os, arch — surfaced by uname/neofetch
303
+ vfsOptions?: VfsOptions, // { mode: "memory"|"fs", snapshotPath?: string }
443
304
  )
444
305
  ```
445
306
 
446
- - **hostname**: Injected into command context and prompt.
447
- - **properties**: Optional shell metadata shown in `uname`-like output. Defaults to `defaultShellProperties`.
448
- - **vfsOptions**: Optional VFS persistence options — see [VirtualFileSystem](#virtualfilesystem).
449
-
450
307
  ```typescript
451
308
  interface ShellProperties {
452
- kernel: string; // Kernel version string — shown by uname, neofetch, /proc/version
453
- os: string; // Full OS description — shown by neofetch, /etc/os-release, lsb_release
454
- arch: string; // CPU architecture label — shown by uname -m, neofetch
309
+ kernel: string; // e.g. "1.0.0+itsrealfortune+1-amd64"
310
+ os: string; // e.g. "Fortune GNU/Linux x64"
311
+ arch: string; // e.g. "x86_64"
455
312
  }
456
313
  ```
457
314
 
458
- Fields map directly to the values reported by `uname -a`, `neofetch`, `lsb_release`, `/etc/os-release`, and `/proc/version` inside the shell. Changing them after construction has no effect — pass them to the constructor.
459
-
460
- **Example:**
461
-
462
- ```typescript
463
- const shell = new VirtualShell("typescript-vm", {
464
- kernel: "1.0.0+itsrealfortune+1-amd64",
465
- os: "Fortune GNU/Linux x64",
466
- arch: "x86_64",
467
- }, {
468
- mode: "fs",
469
- snapshotPath: "./data",
470
- });
471
- ```
472
-
473
- #### Methods
315
+ **Methods**
474
316
 
475
317
  | Method | Description |
476
318
  |--------|-------------|
477
- | `ensureInitialized(): Promise<void>` | Await this before using the shell programmatically. |
319
+ | `ensureInitialized(): Promise<void>` | Await before using programmatically. |
478
320
  | `addCommand(name, params, callback)` | Register a custom shell command. |
479
- | `executeCommand(rawInput, authUser, cwd)` | Run a raw command string (supports `&&`, `\|`, `$(cmd)`, aliases). |
480
- | `startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize)` | Attach an interactive PTY session to this shell. |
481
- | `writeFileAsUser(authUser, path, content)` | Write a file on behalf of a user with quota enforcement. |
482
- | `refreshProcFs(): void` | Refresh `/proc/uptime`, `/proc/meminfo`, `/proc/cpuinfo`, etc. from live host data. |
483
- | `syncPasswd(): void` | Sync `/etc/passwd`, `/etc/group`, `/etc/shadow` from `VirtualUserManager` state. |
484
- | `getVfs(): VirtualFileSystem \| null` | Access the VFS instance. |
485
- | `getUsers(): VirtualUserManager \| null` | Access the user manager. |
486
- | `getHostname(): string` | Returns the configured hostname. |
321
+ | `executeCommand(rawInput, authUser, cwd)` | Run a raw command string. |
322
+ | `startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize)` | Attach a PTY session. |
323
+ | `writeFileAsUser(authUser, path, content)` | Write with quota enforcement. |
324
+ | `refreshProcFs(): void` | Refresh all `/proc/*` files (uptime, meminfo, cpuinfo, per-pid). |
325
+ | `refreshProcSessions(): void` | Lightweight refresh of `/proc/<pid>` and `/proc/self` only. |
326
+ | `syncPasswd(): void` | Sync `/etc/passwd`, `/etc/group`, `/etc/shadow` from user manager. |
327
+ | `getVfs(): VirtualFileSystem \| null` | Access VFS instance. |
328
+ | `getUsers(): VirtualUserManager \| null` | Access user manager. |
329
+ | `getHostname(): string` | Returns configured hostname. |
487
330
 
488
- #### Public fields
331
+ **Public fields**
489
332
 
490
333
  | Field | Type | Description |
491
334
  |-------|------|-------------|
492
- | `vfs` | `VirtualFileSystem` | Backing virtual filesystem — use for direct path operations. |
493
- | `users` | `VirtualUserManager` | Virtual user database — auth, quotas, and session tracking. |
494
- | `packageManager` | `VirtualPackageManager` | APT/dpkg package manager backed by the built-in registry. |
495
- | `hostname` | `string` | Hostname shown in the shell prompt and SSH ident string. |
496
- | `properties` | `ShellProperties` | Distro identity strings surfaced by `uname`, `neofetch`, etc. |
497
- | `startTime` | `number` | Unix ms timestamp of shell creation — used by `uptime` and `/proc/uptime`. |
335
+ | `vfs` | `VirtualFileSystem` | Backing virtual filesystem. |
336
+ | `users` | `VirtualUserManager` | Virtual user database. |
337
+ | `packageManager` | `VirtualPackageManager` | APT/dpkg package manager. |
338
+ | `hostname` | `string` | Hostname shown in prompt and SSH ident. |
339
+ | `properties` | `ShellProperties` | Distro identity strings. |
340
+ | `startTime` | `number` | Unix ms timestamp of shell creation. |
341
+
342
+ **Events:** `initialized`, `command { command, user, cwd }`, `session:start { user, sessionId, remoteAddress }`
498
343
 
499
344
  **Custom command example:**
500
345
 
501
346
  ```typescript
502
347
  shell.addCommand("greet", ["[name]"], ({ args, authUser }) => {
503
348
  const name = args[0] ?? authUser;
504
- return { stdout: `Hello, ${name}!`, exitCode: 0 };
349
+ return { stdout: `Hello, ${name}!\n`, exitCode: 0 };
505
350
  });
506
- // Inside the shell: greet world → Hello, world!
507
351
  ```
508
352
 
509
- #### Events
510
-
511
- | Event | Data | Description |
512
- |-------|------|-------------|
513
- | `initialized` | — | Shell initialization complete |
514
- | `command` | `{ command, user, cwd }` | A command was executed |
515
- | `session:start` | `{ user, sessionId, remoteAddress }` | Interactive session started |
516
-
517
353
  ---
518
354
 
519
355
  ### `VirtualFileSystem`
520
356
 
521
- Pure in-memory virtual filesystem. All state lives in a recursive `Map`-based tree — no host filesystem access at runtime.
522
-
523
- Two persistence modes are available via the `VfsOptions` constructor argument:
524
-
525
- ```typescript
526
- // Default — pure in-memory, zero disk I/O
527
- const vfs = new VirtualFileSystem();
528
- const vfs = new VirtualFileSystem({ mode: "memory" });
529
-
530
- // FS mode — binary snapshot (.vfsb) auto-saved to disk on flushMirror()
531
- const vfs = new VirtualFileSystem({
532
- mode: "fs",
533
- snapshotPath: "./data", // writes ./data/vfs-snapshot.vfsb
534
- });
535
- await vfs.restoreMirror(); // load from disk (silent no-op if no file yet)
536
- // ... use vfs ...
537
- await vfs.flushMirror(); // persist to disk
538
- ```
539
-
540
- Both modes expose exactly the same API. The tree always lives in memory; `"fs"` mode adds a binary round-trip on `restoreMirror` / `flushMirror`. See [VFSB Binary Format](#vfsb-binary-format) for details.
541
-
542
- #### Constructor
357
+ Pure in-memory virtual filesystem. No host filesystem access at runtime.
543
358
 
544
359
  ```typescript
545
- interface VfsOptions {
546
- mode?: "memory" | "fs"; // default: "memory"
547
- snapshotPath?: string; // required when mode is "fs"
548
- }
360
+ // Memory mode (default) — ephemeral
361
+ new VirtualFileSystem()
549
362
 
550
- new VirtualFileSystem(options?: VfsOptions)
363
+ // FS mode — persists to binary .vfsb file
364
+ new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" })
365
+ await vfs.restoreMirror(); // load from disk (no-op if no file yet)
366
+ await vfs.flushMirror(); // save to disk
551
367
  ```
552
368
 
553
- #### Methods
369
+ **Methods**
554
370
 
555
371
  | Method | Description |
556
372
  |--------|-------------|
557
373
  | `mkdir(path, mode?)` | Create directory and any missing parents. |
558
- | `writeFile(path, content, options?)` | Write file (creates parent dirs). `options.compress` stores as gzip; `options.mode` sets POSIX mode bits. |
559
- | `readFile(path): string` | Read file as UTF-8. Transparently decompresses gzip files. |
560
- | `readFileRaw(path): Buffer` | Read file as Buffer (decompresses if needed). |
561
- | `exists(path): boolean` | Test whether a file or directory exists. |
562
- | `stat(path): VfsNodeStats` | Returns file/directory metadata. |
563
- | `list(path?): string[]` | List direct children of a directory (sorted). |
374
+ | `writeFile(path, content, options?)` | Write file. `options.compress` gzips; `options.mode` sets POSIX mode bits. |
375
+ | `readFile(path): string` | Read as UTF-8. Transparently decompresses gzip. |
376
+ | `readFileRaw(path): Buffer` | Read as Buffer. |
377
+ | `exists(path): boolean` | Test existence. |
378
+ | `stat(path): VfsNodeStats` | Returns metadata. |
379
+ | `list(path?): string[]` | List direct children (sorted). |
564
380
  | `tree(path?): string` | Render ASCII directory tree. |
565
- | `move(from, to)` | Move or rename a node. Throws if destination exists. |
566
- | `remove(path, options?)` | Delete file or directory. `options.recursive` required for non-empty dirs. |
381
+ | `move(from, to)` | Move or rename. |
382
+ | `remove(path, options?)` | Delete. `options.recursive` required for non-empty dirs. |
567
383
  | `chmod(path, mode)` | Update POSIX mode bits. |
568
- | `compressFile(path)` | Gzip-compress file content in place. |
569
- | `decompressFile(path)` | Gunzip file content in place. |
570
- | `symlink(target, linkPath)` | Create a symbolic link (mode `0o120777`). |
571
- | `isSymlink(path): boolean` | Returns true if the path is a symlink node. |
572
- | `resolveSymlink(path, maxDepth?): string` | Resolve symlink chain to real path (default max 8 hops). |
384
+ | `compressFile(path)` / `decompressFile(path)` | Gzip-compress / gunzip in place. |
385
+ | `symlink(target, linkPath)` | Create symbolic link. |
386
+ | `isSymlink(path): boolean` | Test if path is a symlink. |
387
+ | `resolveSymlink(path, maxDepth?): string` | Resolve symlink chain (default max 8 hops). |
573
388
  | `getUsageBytes(path?): number` | Total stored bytes under a path. |
574
- | `getMode(): VfsPersistenceMode` | Returns `"memory"` or `"fs"`. |
575
- | `getSnapshotPath(): string \| null` | Snapshot file path in `"fs"` mode, or null. |
576
- | `toSnapshot(): VfsSnapshot` | Export the whole tree as a JSON-serialisable snapshot. |
577
- | `importSnapshot(snapshot)` | Replace current state from a snapshot (preserves mode). |
578
- | `restoreMirror(): Promise<void>` | Load from disk (`"fs"` mode) / no-op (`"memory"` mode). |
579
- | `flushMirror(): Promise<void>` | Save to disk (`"fs"` mode) / emit `mirror:flush` (`"memory"` mode). |
580
- | `VirtualFileSystem.fromSnapshot(snapshot)` | **Static.** Create a new memory-mode instance from a snapshot. |
581
-
582
- #### Events
583
-
584
- | Event | Data | Description |
585
- |-------|------|-------------|
586
- | `file:write` | `{ path, size }` | File written |
587
- | `file:read` | `{ path, size }` | File read |
588
- | `dir:create` | `{ path, mode }` | Directory created |
589
- | `node:remove` | `{ path }` | File or directory deleted |
590
- | `symlink:create` | `{ link, target }` | Symlink created |
591
- | `snapshot:import` | — | `importSnapshot()` called |
592
- | `snapshot:restore` | `{ path }` | Restored from disk (fs mode) |
593
- | `mirror:flush` | `{ path? }` | Flushed (path present in fs mode) |
594
-
595
- **Example:**
596
-
597
- ```typescript
598
- vfs.on("file:write", ({ path, size }) => {
599
- console.log(`[VFS] Written: ${path} (${size} bytes)`);
600
- });
601
-
602
- vfs.on("dir:create", ({ path, mode }) => {
603
- console.log(`[VFS] Dir created: ${path} (mode: ${mode.toString(8)})`);
604
- });
605
- ```
606
-
607
- #### Memory mode — manual snapshot persistence
608
-
609
- ```typescript
610
- import { VirtualFileSystem } from "typescript-virtual-container";
611
- import { writeFileSync, readFileSync } from "node:fs";
612
-
613
- const vfs = new VirtualFileSystem(); // mode: "memory"
614
- vfs.writeFile("/etc/config.json", JSON.stringify({ debug: true }));
615
-
616
- // Export to disk manually as JSON (portable, human-readable)
617
- // For binary format, use "fs" mode instead — see VFSB Binary Format.
618
- writeFileSync("vfs-snapshot.json", JSON.stringify(vfs.toSnapshot()));
619
-
620
- // Restore into a new instance
621
- const snapshot = JSON.parse(readFileSync("vfs-snapshot.json", "utf8"));
622
- const restored = VirtualFileSystem.fromSnapshot(snapshot);
623
- console.log(restored.readFile("/etc/config.json")); // {"debug":true}
624
- ```
625
-
626
- #### FS mode — automatic persistence across restarts
627
-
628
- ```typescript
629
- import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
630
-
631
- const shell = new VirtualShell("my-vm", undefined, {
632
- mode: "fs",
633
- snapshotPath: "./vfs-data",
634
- });
635
-
636
- const ssh = new VirtualSshServer({ port: 2222, shell });
637
- await ssh.start();
638
- // VFS is restored from ./vfs-data/vfs-snapshot.vfsb on start (if it exists).
639
- // flushMirror() is called after each write, persisting state to disk automatically.
640
- ```
641
-
642
- ---
643
-
644
- ## VFSB Binary Format
645
-
646
- When `mode: "fs"` is configured, the VFS persists its state to disk as a compact binary file (`vfs-snapshot.vfsb`) rather than JSON. This is the default and only on-disk format for `"fs"` mode.
647
-
648
- ### Why not JSON?
649
-
650
- The JSON+base64 approach has two compounding costs: file content is base64-encoded (33% size bloat), and the entire tree must be stringified and parsed on every save/load. For a 10 MB VFS, that means writing ~13.3 MB of base64 data wrapped in JSON — and parsing all of it as a string on restart.
651
-
652
- The VFSB format eliminates both costs.
389
+ | `toSnapshot(): VfsSnapshot` | Export tree as JSON-serialisable snapshot. |
390
+ | `importSnapshot(snapshot)` | Replace current state from a snapshot. |
391
+ | `restoreMirror(): Promise<void>` | Load from disk (`"fs"` mode) / no-op otherwise. |
392
+ | `flushMirror(): Promise<void>` | Save to disk (`"fs"` mode) / emit event otherwise. |
393
+ | `VirtualFileSystem.fromSnapshot(snapshot)` | **Static.** Create memory-mode instance from snapshot. |
653
394
 
654
- ### Wire format
395
+ **Events:** `file:write { path, size }`, `file:read { path, size }`, `dir:create { path, mode }`, `node:remove { path }`, `symlink:create { link, target }`, `snapshot:import`, `snapshot:restore { path }`, `mirror:flush { path? }`
655
396
 
656
- All multi-byte integers are little-endian. The file starts with a 5-byte header, followed by a single recursive node tree.
397
+ #### VFSB Binary Format
657
398
 
658
- ```
659
- File header
660
- [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
661
- [1] version = 0x01
662
-
663
- Node (recursive)
664
- [1] type = 0x01 (file) | 0x02 (directory)
665
- [2] name length (uint16)
666
- [N] name bytes (UTF-8)
667
- [4] mode (uint32)
668
- [8] createdAt ms (float64 big-endian)
669
- [8] updatedAt ms (float64 big-endian)
670
-
671
- File node extra
672
- [1] compressed flag (0x00 | 0x01)
673
- [4] content length (uint32)
674
- [N] content bytes (raw — no base64)
675
-
676
- Directory node extra
677
- [4] children count (uint32)
678
- [N] children nodes (recursive)
679
- ```
680
-
681
- ### Performance
682
-
683
- Measured on a VFS tree with ~50 nodes and mixed file content:
399
+ In `"fs"` mode, state is persisted as a compact binary file (`vfs-snapshot.vfsb`).
684
400
 
685
401
  | Metric | JSON+base64 | VFSB binary |
686
402
  |--------|-------------|-------------|
687
- | File size (10 MB of content) | ~13.7 MB | ~10.0 MB |
403
+ | File size (10 MB content) | ~13.7 MB | ~10.0 MB |
688
404
  | Encode time | ~12 ms | ~0.04 ms |
689
405
  | Decode time | ~18 ms | ~0.07 ms |
690
- | External dependencies | none | none |
691
-
692
- Size reduction comes from eliminating base64 encoding (33% overhead on raw bytes) and JSON string wrapping. Speed improvement comes from sequential buffer reads/writes instead of string parsing.
693
-
694
- ### Backward compatibility
695
-
696
- If a legacy JSON snapshot file is found at the configured `snapshotPath`, it is automatically detected by the absence of the `VFS!` magic bytes and parsed as JSON. A migration notice is logged to `console.info`. On the next `flushMirror()` call, the file is rewritten in VFSB format — no manual migration step needed.
697
-
698
- ```typescript
699
- // This just works — auto-migrates any existing JSON snapshot on first flush
700
- const vfs = new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" });
701
- await vfs.restoreMirror(); // reads JSON if .vfsb contains JSON, binary otherwise
702
- await vfs.flushMirror(); // always writes VFSB binary
703
- ```
704
406
 
705
- ### Using the binary API directly
706
-
707
- The encoder and decoder are exported from the VFS module internals for advanced use cases (e.g. replication, custom storage backends):
407
+ Wire format: 5-byte header (`VFS!` magic + version), followed by a recursive node tree with type, name, mode, timestamps, and raw content bytes (no base64). Legacy JSON snapshots are auto-detected and migrated on first `flushMirror()`.
708
408
 
409
+ Low-level API:
709
410
  ```typescript
710
411
  import { encodeVfs, decodeVfs, isBinarySnapshot } from "typescript-virtual-container/src/VirtualFileSystem/binaryPack";
711
-
712
- // Encode the current tree to a Buffer
713
- const buf = encodeVfs(vfs.root);
714
-
715
- // Detect format
716
- isBinarySnapshot(buf); // true — starts with "VFS!" magic
717
- isBinarySnapshot(jsonBuf); // false — JSON or other format
718
-
719
- // Restore from a Buffer
412
+ const buf = encodeVfs(vfs.root);
720
413
  const root = decodeVfs(buf);
414
+ isBinarySnapshot(buf); // true — starts with "VFS!" magic
721
415
  ```
722
416
 
723
- These are low-level APIs. For normal usage, `flushMirror()` and `restoreMirror()` are all you need.
724
-
725
-
726
417
  ---
727
418
 
728
419
  ### `VirtualUserManager`
729
420
 
730
- Manages virtual users, password hashing (scrypt), sudo privileges, per-user storage quotas, SSH public keys, and active session tracking.
731
-
732
- #### Constructor
421
+ Manages users, password hashing (scrypt), sudo privileges, storage quotas, SSH public keys, and session tracking.
733
422
 
734
423
  ```typescript
735
424
  new VirtualUserManager(
736
425
  vfs: VirtualFileSystem,
737
- autoSudoForNewUsers?: boolean, // default: true
426
+ autoSudoForNewUsers?: boolean, // default: true
738
427
  )
739
428
  ```
740
429
 
741
- - Auth data is stored inside the VFS at protected paths under `/virtual-env-js/.auth/`.
742
- - `autoSudoForNewUsers`: when true, newly created users are automatically added to sudoers.
430
+ Auth data is stored at `/virtual-env-js/.auth/` inside the VFS.
743
431
 
744
- #### Methods
432
+ **Methods**
745
433
 
746
434
  | Method | Description |
747
435
  |--------|-------------|
748
- | `initialize(): Promise<void>` | Load users/sudoers from VFS, ensure root exists. Call once on startup. |
749
- | `verifyPassword(username, password): boolean` | Check plaintext password against stored hash. |
750
- | `hasPassword(username): boolean` | Returns `true` if a non-empty password is set for the user. |
751
- | `hashPassword(password): string` | Hash a password using scrypt (or SHA-256 with `SSH_MIMIC_FAST_PASSWORD_HASH=1`). |
752
- | `getPasswordHash(username): string \| null` | Returns the raw stored hash for a user, or `null` if not found. |
436
+ | `initialize(): Promise<void>` | Load users/sudoers, ensure root exists. |
437
+ | `verifyPassword(username, password): boolean` | Check plaintext password. |
438
+ | `hasPassword(username): boolean` | Returns `true` if a password is set. |
439
+ | `hashPassword(password): string` | Hash with scrypt (or SHA-256 with `SSH_MIMIC_FAST_PASSWORD_HASH=1`). |
440
+ | `getPasswordHash(username): string \| null` | Raw stored hash. |
753
441
  | `addUser(username, password): Promise<void>` | Create user with home directory. |
754
- | `deleteUser(username): Promise<void>` | Delete user. Throws when user is `root` or does not exist. |
755
- | `setPassword(username, password): Promise<void>` | Update password for an existing user. Throws when user does not exist. |
756
- | `isSudoer(username): boolean` | Returns `true` when the user has sudo privileges. |
757
- | `addSudoer(username): Promise<void>` | Grant sudo privileges. Throws when user does not exist. |
758
- | `removeSudoer(username): Promise<void>` | Revoke sudo privileges. Throws when username is `root`. |
759
- | `setQuotaBytes(username, maxBytes): Promise<void>` | Set per-user write quota (bytes under `/home/<user>`). |
760
- | `clearQuota(username): Promise<void>` | Remove quota limit for a user. |
761
- | `getQuotaBytes(username): number \| null` | Returns quota in bytes, or `null` if unlimited. |
762
- | `getUsageBytes(username): number` | Returns current usage in bytes under `/home/<user>`. |
763
- | `assertWriteWithinQuota(username, path, content)` | Throws if the write would exceed the user's quota. |
764
- | `listUsers(): string[]` | Returns a sorted list of all registered usernames. |
765
- | `addAuthorizedKey(username, algo, data)` | Register an SSH public key for the user. |
766
- | `getAuthorizedKeys(username)` | Returns the list of authorized keys for a user. |
767
- | `removeAuthorizedKeys(username)` | Revoke all authorized keys for a user. |
768
- | `registerSession(username, remoteAddress): VirtualActiveSession` | Register an active session and allocate a virtual TTY. |
769
- | `unregisterSession(sessionId): void` | Remove a session record on disconnect. Safe to call with `null`. |
770
- | `updateSession(sessionId, username, remoteAddress): void` | Update session metadata after `su`/`sudo` identity change. |
771
- | `listActiveSessions(): VirtualActiveSession[]` | Returns all active sessions sorted by start time. |
772
-
773
- #### Events
774
-
775
- | Event | Data | Description |
776
- |-------|------|-------------|
777
- | `initialized` | — | User manager ready, root account ensured |
778
- | `user:add` | `{ username }` | New user created |
779
- | `user:delete` | `{ username }` | User deleted |
780
- | `key:add` | `{ username, algo }` | Public key added |
781
- | `key:remove` | `{ username }` | Public keys removed |
782
- | `session:register` | `{ sessionId, username, remoteAddress }` | Session started |
783
- | `session:unregister` | `{ sessionId, username }` | Session ended |
784
-
785
- **Example:**
786
-
787
- ```typescript
788
- users.on("user:add", ({ username }) => {
789
- console.log(`[USERS] Created: ${username}`);
790
- });
791
-
792
- users.on("session:register", ({ sessionId, username, remoteAddress }) => {
793
- console.log(`[USERS] Session ${sessionId}: ${username} from ${remoteAddress}`);
794
- });
795
- ```
442
+ | `deleteUser(username): Promise<void>` | Delete user. Throws on `root` or missing user. |
443
+ | `setPassword(username, password): Promise<void>` | Update password. |
444
+ | `isSudoer(username): boolean` | Returns `true` if user has sudo. |
445
+ | `addSudoer(username): Promise<void>` | Grant sudo. |
446
+ | `removeSudoer(username): Promise<void>` | Revoke sudo. Throws on `root`. |
447
+ | `setQuotaBytes(username, maxBytes): Promise<void>` | Set per-user write quota. |
448
+ | `clearQuota(username): Promise<void>` | Remove quota limit. |
449
+ | `getQuotaBytes(username): number \| null` | Quota in bytes, or `null` if unlimited. |
450
+ | `getUsageBytes(username): number` | Current usage under `/home/<user>`. |
451
+ | `assertWriteWithinQuota(username, path, content)` | Throws if write would exceed quota. |
452
+ | `listUsers(): string[]` | Sorted list of all usernames. |
453
+ | `addAuthorizedKey(username, algo, data)` | Register SSH public key. |
454
+ | `getAuthorizedKeys(username)` | List authorized keys. |
455
+ | `removeAuthorizedKeys(username)` | Revoke all authorized keys. |
456
+ | `registerSession(username, remoteAddress): VirtualActiveSession` | Allocate virtual TTY and register session. |
457
+ | `unregisterSession(sessionId): void` | Remove session on disconnect. Safe with `null`. |
458
+ | `updateSession(sessionId, username, remoteAddress): void` | Update after `su`/`sudo` identity change. |
459
+ | `listActiveSessions(): VirtualActiveSession[]` | All active sessions sorted by start time. |
460
+
461
+ **Events:** `initialized`, `user:add { username }`, `user:delete { username }`, `key:add { username, algo }`, `key:remove { username }`, `session:register { sessionId, username, remoteAddress }`, `session:unregister { sessionId, username }`
796
462
 
797
463
  ---
798
464
 
799
-
800
465
  ### `VirtualPackageManager`
801
466
 
802
- Simulates APT/dpkg package management backed by a built-in registry. Accessed via `shell.packageManager`.
467
+ Simulates APT/dpkg backed by a 25-package registry. Accessed via `shell.packageManager`.
803
468
 
804
- #### Constructor
805
-
806
- Instantiated automatically by `VirtualShell`. Not constructed directly.
807
-
808
- #### Methods
469
+ **Methods**
809
470
 
810
471
  | Method | Description |
811
472
  |--------|-------------|
812
- | `load()` | Load installed packages from `/var/lib/dpkg/status` (called on shell init) |
813
473
  | `install(names, opts?)` | Install packages (resolves deps, writes files to VFS). Returns `{ output, exitCode }` |
814
- | `remove(names, opts?)` | Remove packages. `opts.purge` also removes config files |
815
- | `search(term)` | Search available packages by name or description |
816
- | `show(name)` | Show dpkg-style metadata block for a package |
817
- | `listInstalled()` | List all installed packages as `InstalledPackage[]` |
818
- | `listAvailable()` | List all packages in the registry |
819
- | `isInstalled(name)` | Returns `true` if package is installed |
820
- | `installedCount()` | Count of installed packages (used by `neofetch`) |
821
- | `findInRegistry(name)` | Look up a `PackageDefinition` by name |
822
-
823
- #### Types
824
-
825
- ```ts
826
- interface PackageDefinition {
827
- name: string;
828
- version: string;
829
- description: string;
830
- files?: PackageFile[]; // written to VFS on install
831
- depends?: string[]; // resolved recursively
832
- onInstall?: (vfs, users) => void;
833
- onRemove?: (vfs) => void;
834
- }
474
+ | `remove(names, opts?)` | Remove. `opts.purge` also removes config files. |
475
+ | `search(term)` | Search by name or description. |
476
+ | `show(name)` | dpkg-style metadata block. |
477
+ | `listInstalled()` | All installed packages as `InstalledPackage[]`. |
478
+ | `listAvailable()` | All registry packages. |
479
+ | `isInstalled(name)` | Returns `true` if installed. |
480
+ | `installedCount()` | Count of installed packages. |
481
+ | `findInRegistry(name)` | Look up `PackageDefinition` by name. |
835
482
 
836
- interface InstalledPackage {
837
- name: string;
838
- version: string;
839
- architecture: string;
840
- installedAt: string; // ISO-8601
841
- files: string[]; // paths written to VFS
842
- }
483
+ **Custom packages:**
484
+
485
+ ```typescript
486
+ const customPkg = {
487
+ name: "myapp",
488
+ version: "1.0.0",
489
+ description: "My application",
490
+ files: [
491
+ { path: "/usr/bin/myapp", content: "#!/bin/sh\necho myapp v1.0.0\n", mode: 0o755 },
492
+ { path: "/etc/myapp/config.json", content: JSON.stringify({ port: 3000 }) },
493
+ ],
494
+ onInstall: (vfs) => {
495
+ vfs.mkdir("/var/lib/myapp", 0o755);
496
+ vfs.mkdir("/var/log/myapp", 0o755);
497
+ },
498
+ };
843
499
  ```
844
500
 
845
501
  ---
846
502
 
847
503
  ### Snapshot Diff Tooling
848
504
 
849
- Three utility functions for comparing VFS snapshots in tests and deployment verification.
850
-
851
505
  ```typescript
852
- import {
853
- diffSnapshots,
854
- formatDiff,
855
- assertDiff,
856
- } from "typescript-virtual-container";
506
+ import { diffSnapshots, formatDiff, assertDiff } from "typescript-virtual-container";
857
507
  ```
858
508
 
859
- #### `diffSnapshots(before, after, options?): VfsDiff`
860
-
861
- Compares two snapshots and returns structured diff results.
509
+ **`diffSnapshots(before, after, options?): VfsDiff`**
862
510
 
863
- ```ts
511
+ ```typescript
864
512
  const before = shell.vfs.toSnapshot();
865
513
  await client.exec("apt install vim && mkdir -p /app");
866
514
  const after = shell.vfs.toSnapshot();
867
515
 
868
- const diff = diffSnapshots(before, after, {
869
- ignore: ["/proc", "/var/log"], // skip volatile paths
870
- });
871
-
872
- diff.clean; // false
873
- diff.added; // [{ path: "/usr/bin/vim", type: "file" }, ...]
874
- diff.removed; // []
875
- diff.modified; // [{ path: "/var/lib/dpkg/status", before: "...", after: "..." }]
876
- ```
877
-
878
- #### `formatDiff(diff, options?): string`
879
-
880
- Human-readable output similar to `git diff --stat`.
881
-
882
- ```ts
883
- console.log(formatDiff(diff));
884
- // + /app [directory]
885
- // + /usr/bin/vim [file]
886
- // ~ /var/lib/dpkg/status [modified]
887
- //
888
- // 2 added, 1 modified
889
-
890
- console.log(formatDiff(diff, { showContent: true, maxContentChars: 80 }));
516
+ const diff = diffSnapshots(before, after, { ignore: ["/proc", "/var/log"] });
517
+ diff.clean; // false
518
+ diff.added; // [{ path: "/usr/bin/vim", type: "file" }, ...]
519
+ diff.modified; // [{ path: "/var/lib/dpkg/status", before: "...", after: "..." }]
891
520
  ```
892
521
 
893
- #### `assertDiff(diff, expected): void`
894
-
895
- Throws on mismatch — designed for test suites.
896
-
897
- ```ts
898
- assertDiff(diff, {
899
- added: ["/app", "/usr/bin/vim"],
900
- modified: ["/var/lib/dpkg/status"],
901
- });
902
- // throws if any expected path is not in the diff
903
- ```
522
+ **`formatDiff(diff, options?): string`** — human-readable output like `git diff --stat`. Options: `showContent: boolean`, `maxContentChars: number`.
904
523
 
905
- #### Types
524
+ **`assertDiff(diff, expected): void`** — throws on mismatch, designed for test suites:
906
525
 
907
- ```ts
908
- interface VfsDiff {
909
- added: VfsDiffEntry[]; // { path, type }
910
- removed: VfsDiffEntry[]; // { path, type }
911
- modified: VfsDiffModified[]; // { path, type, before, after }
912
- clean: boolean;
913
- }
526
+ ```typescript
527
+ assertDiff(diff, { added: ["/app", "/usr/bin/vim"], modified: ["/var/lib/dpkg/status"] });
914
528
  ```
915
529
 
916
530
  ---
917
531
 
918
532
  ### `HoneyPot`
919
533
 
920
- Comprehensive security auditing and event tracking utility. Attaches listeners to all core components to log activity, track statistics, and detect anomalies.
921
-
922
- #### Constructor
534
+ Comprehensive security auditing. Attaches to all core components to log activity and detect anomalies.
923
535
 
924
536
  ```typescript
925
- new HoneyPot(maxLogSize?: number) // default: 10000
537
+ new HoneyPot(maxLogSize?: number) // default: 10000
926
538
  ```
927
539
 
928
- #### Methods
540
+ **Methods**
929
541
 
930
542
  | Method | Description |
931
543
  |--------|-------------|
932
544
  | `attach(shell, vfs, users, ssh?, sftp?)` | Subscribe to all event sources. |
933
- | `getAuditLog(type?, source?): AuditLogEntry[]` | Full log, optionally filtered by event type and/or source component. |
545
+ | `getAuditLog(type?, source?): AuditLogEntry[]` | Full log, optionally filtered. |
934
546
  | `getStats(): Readonly<HoneyPotStats>` | Aggregated activity counters. |
935
- | `getRecent(limit?): AuditLogEntry[]` | Most recent entries in reverse chronological order. |
936
- | `detectAnomalies()` | Analyze patterns — returns `{ type, severity, message }[]`. |
937
- | `reset()` | Clear audit log and reset all stat counters. |
938
- | `exportJson(): string` | Serialise full log + stats to a JSON string. |
547
+ | `getRecent(limit?): AuditLogEntry[]` | Most recent entries, reverse-chronological. |
548
+ | `detectAnomalies()` | Returns `{ type, severity, message }[]`. |
549
+ | `reset()` | Clear log and reset counters. |
550
+ | `exportJson(): string` | Serialise full log + stats to JSON. |
939
551
 
940
- #### HoneyPotStats fields
552
+ `detectAnomalies` detects: high auth failure rates, excessive failures, unusual command volume, unusual file write volume.
941
553
 
942
554
  ```typescript
943
- interface HoneyPotStats {
944
- authAttempts: number;
945
- authSuccesses: number;
946
- authFailures: number;
947
- commands: number;
948
- fileWrites: number;
949
- fileReads: number;
950
- sessionStarts: number;
951
- sessionEnds: number;
952
- userCreated: number;
953
- userDeleted: number;
954
- clientConnects: number;
955
- clientDisconnects: number;
956
- }
957
- ```
555
+ const hp = new HoneyPot(50_000);
556
+ hp.attach(shell, shell.vfs, shell.users, ssh);
958
557
 
959
- #### Audit Log Entry
960
-
961
- ```typescript
962
- interface AuditLogEntry {
963
- timestamp: string; // ISO-8601
964
- type: string; // e.g. "auth:failure", "file:write"
965
- source: string; // e.g. "SshMimic", "VirtualFileSystem"
966
- details: Record<string, unknown>; // event-specific payload
967
- }
968
- ```
969
-
970
- `detectAnomalies` detects: high authentication failure rates, excessive auth failures, unusual command volume, unusual file write volume.
971
-
972
- #### Example
973
-
974
- ```typescript
975
- import { HoneyPot, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
976
-
977
- const shell = new VirtualShell("honeypot");
978
- const ssh = new VirtualSshServer({ port: 2222, shell });
979
- const hp = new HoneyPot(50_000);
980
-
981
- await ssh.start();
982
- hp.attach(shell, shell.vfs, shell.users, ssh);
983
-
984
- // Filter audit log
985
- const failures = hp.getAuditLog("auth:failure");
986
- failures.forEach(e => console.log(e.details.username, e.details.remoteAddress));
558
+ hp.getAuditLog("auth:failure").forEach(e =>
559
+ console.log(e.details.username, e.details.remoteAddress)
560
+ );
987
561
 
988
- // Detect anomalies
989
562
  hp.detectAnomalies().forEach(a =>
990
563
  console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
991
564
  );
992
565
 
993
- // Export on shutdown
994
566
  process.on("SIGINT", () => {
995
567
  require("fs").writeFileSync("audit.json", hp.exportJson());
996
568
  process.exit(0);
@@ -1001,68 +573,35 @@ process.on("SIGINT", () => {
1001
573
 
1002
574
  ### `SshClient` (Programmatic API)
1003
575
 
1004
- Execute shell commands against a `VirtualShell` without SSH protocol overhead. Maintains working-directory state across calls.
1005
-
1006
- #### Constructor
576
+ Execute shell commands against a `VirtualShell` without SSH overhead. Maintains working-directory state.
1007
577
 
1008
578
  ```typescript
1009
579
  new SshClient(shell: VirtualShell, username: string)
1010
580
  ```
1011
581
 
1012
- No password required — the client authenticates by username only.
1013
-
1014
- #### Methods
582
+ **Methods**
1015
583
 
1016
584
  | Method | Description |
1017
585
  |--------|-------------|
1018
- | `exec(command): Promise<CommandResult>` | Run arbitrary raw command string (supports `&&`, `\|`, etc.). |
1019
- | `ls(path?)` | List directory (default: cwd). |
586
+ | `exec(command): Promise<CommandResult>` | Run raw command string (supports `&&`, `\|`, etc.). |
587
+ | `ls(path?)` | List directory. |
1020
588
  | `pwd()` | Print current working directory. |
1021
- | `cd(path)` | Change directory. Updates internal cwd state on success. |
1022
- | `cat(path)` | Read file content via `cat` command. |
1023
- | `readFile(path)` | Read file directly from VFS (programmatic, no shell parse). |
1024
- | `writeFile(path, content)` | Write file directly to VFS (programmatic). |
1025
- | `mkdir(path, recursive?)` | Create directory. `recursive=true` adds `-p`. |
589
+ | `cd(path)` | Change directory. Updates internal cwd on success. |
590
+ | `cat(path)` | Read file via `cat` command. |
591
+ | `readFile(path)` | Read file directly from VFS. |
592
+ | `writeFile(path, content)` | Write file directly to VFS. |
593
+ | `mkdir(path, recursive?)` | Create directory. |
1026
594
  | `touch(path)` | Create empty file. |
1027
- | `rm(path, recursive?)` | Remove file or directory. `recursive=true` adds `-r`. |
595
+ | `rm(path, recursive?)` | Remove file or directory. |
1028
596
  | `tree(path?)` | Render ASCII directory tree. |
1029
- | `whoami()` | Print current user. |
1030
- | `hostname()` | Print server hostname. |
1031
- | `who()` | List active sessions. |
1032
- | `getCwd(): string` | Returns current working directory (local, no I/O). |
597
+ | `whoami()` / `hostname()` / `who()` | System info commands. |
598
+ | `getCwd(): string` | Returns current cwd (no I/O). |
1033
599
  | `getUsername(): string` | Returns authenticated username. |
1034
600
 
1035
- **Example:**
1036
-
1037
- ```typescript
1038
- const shell = new VirtualShell("typescript-vm");
1039
- const client = new SshClient(shell, "alice");
1040
-
1041
- await client.mkdir("/home/alice/projects", true);
1042
- await client.cd("/home/alice/projects");
1043
-
1044
- console.log(client.getCwd()); // /home/alice/projects
1045
-
1046
- await client.writeFile("notes.txt", "Work in progress");
1047
- const list = await client.ls();
1048
- console.log(list.stdout); // notes.txt
1049
-
1050
- const read = await client.readFile("notes.txt");
1051
- console.log(read.stdout); // Work in progress
1052
-
1053
- // Shell operators work in exec()
1054
- const r = await client.exec("echo hello && echo world");
1055
- console.log(r.stdout); // hello\nworld
1056
- ```
1057
-
1058
601
  ---
1059
602
 
1060
603
  ### Key Types
1061
604
 
1062
- #### `CommandResult`
1063
-
1064
- Returned by all command executions (shell or programmatic).
1065
-
1066
605
  ```typescript
1067
606
  interface CommandResult {
1068
607
  stdout?: string;
@@ -1076,32 +615,18 @@ interface CommandResult {
1076
615
  openHtop?: boolean;
1077
616
  sudoChallenge?: SudoChallenge;
1078
617
  }
1079
- ```
1080
-
1081
- #### `ShellEnv`
1082
-
1083
- Per-session shell environment. Passed as `env` in `CommandContext`.
1084
618
 
1085
- ```typescript
1086
619
  interface ShellEnv {
1087
- vars: Record<string, string>; // $VAR accessible in expansions
1088
- lastExitCode: number; // $? value
620
+ vars: Record<string, string>; // $VAR accessible in expansions
621
+ lastExitCode: number; // $?
1089
622
  }
1090
- ```
1091
-
1092
- Default variables initialized per session: `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM`, `HOSTNAME`, `PS1`.
1093
623
 
1094
- #### `ShellModule`
1095
-
1096
- Contract for custom command plugins:
1097
-
1098
- ```typescript
1099
624
  interface ShellModule {
1100
625
  name: string;
1101
626
  params: string[];
1102
627
  aliases?: string[];
1103
- description?: string; // shown in grouped help
1104
- category?: string; // navigation|files|text|archive|system|network|shell|users|misc
628
+ description?: string;
629
+ category?: string; // navigation|files|text|archive|system|package|network|shell|users|misc
1105
630
  run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
1106
631
  }
1107
632
 
@@ -1115,157 +640,86 @@ interface CommandContext {
1115
640
  stdin?: string;
1116
641
  cwd: string;
1117
642
  shell: VirtualShell;
1118
- env: ShellEnv; // per-session environment (read/write)
1119
- }
1120
- ```
1121
-
1122
- #### `VfsNodeStats`
1123
-
1124
- ```typescript
1125
- type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
1126
-
1127
- interface VfsFileNode {
1128
- type: "file";
1129
- name: string;
1130
- path: string;
1131
- mode: number;
1132
- size: number;
1133
- compressed: boolean;
1134
- createdAt: Date;
1135
- updatedAt: Date;
643
+ env: ShellEnv;
1136
644
  }
1137
645
 
1138
- interface VfsDirectoryNode {
1139
- type: "directory";
1140
- name: string;
1141
- path: string;
1142
- mode: number;
1143
- childrenCount: number;
1144
- createdAt: Date;
1145
- updatedAt: Date;
1146
- }
1147
- ```
1148
-
1149
- #### `VirtualActiveSession`
1150
-
1151
- ```typescript
1152
646
  interface VirtualActiveSession {
1153
647
  id: string;
1154
648
  username: string;
1155
649
  tty: string;
1156
650
  remoteAddress: string;
1157
- startedAt: string; // ISO-8601
1158
- }
1159
- ```
1160
-
1161
- #### `VfsSnapshot`
1162
-
1163
- ```typescript
1164
- interface VfsSnapshot {
1165
- root: VfsSnapshotDirectoryNode;
651
+ startedAt: string; // ISO-8601
1166
652
  }
1167
- // File nodes store content as base64 in contentBase64.
1168
653
  ```
1169
654
 
1170
655
  ---
1171
656
 
1172
657
  ### Command Helpers
1173
658
 
1174
- Three utility functions are exported from `typescript-virtual-container` to assist with argument parsing inside custom command handlers.
1175
-
1176
- #### `ifFlag(args, flags): boolean`
1177
-
1178
- Returns `true` when any of the given flags appear in the argument array. Matches both standalone tokens (`-s`, `--silent`) and inline forms (`--output=file`).
1179
-
1180
659
  ```typescript
1181
- import { ifFlag } from "typescript-virtual-container";
660
+ import { ifFlag, getFlag, getArg, parseArgs } from "typescript-virtual-container";
1182
661
 
1183
- const recursive = ifFlag(args, ["-r", "--recursive"]);
1184
- const silent = ifFlag(args, "-s");
1185
- ```
662
+ // ifFlag true if any given flag appears in args
663
+ ifFlag(args, ["-r", "--recursive"]) // boolean
1186
664
 
1187
- #### `getFlag(args, flags): string | true | undefined`
665
+ // getFlag — value, true if valueless, undefined if absent
666
+ getFlag(args, ["-o", "--output"])
667
+ // ["--output", "file.txt"] → "file.txt"
668
+ // ["--output=file.txt"] → "file.txt"
669
+ // ["--verbose"] → true
670
+ // [] → undefined
1188
671
 
1189
- Returns the value of a flag, `true` if the flag has no value, or `undefined` if absent.
1190
-
1191
- ```typescript
1192
- import { getFlag } from "typescript-virtual-container";
672
+ // getArg positional at index N, skipping known flags
673
+ // args = ["-r", "src", "dest"]
674
+ getArg(args, 0, { flags: ["-r"] }) // "src"
675
+ getArg(args, 1, { flags: ["-r"] }) // "dest"
1193
676
 
1194
- const output = getFlag(args, ["-o", "--output"]);
1195
- // args = ["--output", "file.txt"] "file.txt"
1196
- // args = ["--output=file.txt"] → "file.txt"
1197
- // args = ["--verbose"] → true (when "verbose" is in flags list)
1198
- // args = [] → undefined
677
+ // parseArgs structured parse
678
+ const { flags, flagsWithValues, positionals } = parseArgs(args, {
679
+ flags: ["-r", "--recursive"],
680
+ flagsWithValue: ["-o", "--output"],
681
+ });
1199
682
  ```
1200
683
 
1201
- #### `getArg(args, index, options?): string | undefined`
1202
-
1203
- Returns the positional argument at a given zero-based index, skipping known flags and their values.
1204
-
1205
- ```typescript
1206
- import { getArg } from "typescript-virtual-container";
1207
-
1208
- // args = ["-r", "src", "dest"]
1209
- const src = getArg(args, 0, { flags: ["-r"] }); // "src"
1210
- const dest = getArg(args, 1, { flags: ["-r"] }); // "dest"
1211
- ```
684
+ </details>
1212
685
 
1213
686
  ---
1214
687
 
1215
- ## Usage Examples
688
+ <details>
689
+ <summary><strong>Examples</strong></summary>
1216
690
 
1217
- ### Example 1: Basic SSH Server
691
+ ### SSH Server with Events
1218
692
 
1219
693
  ```typescript
1220
694
  import { VirtualSshServer } from "typescript-virtual-container";
1221
695
 
1222
696
  const ssh = new VirtualSshServer({ port: 2222, hostname: "lab-environment" });
1223
- await ssh.start();
1224
- console.log("SSH server ready. Connect: ssh root@localhost -p 2222");
1225
697
 
1226
- process.on("SIGINT", () => { ssh.stop(); process.exit(0); });
1227
- ```
698
+ ssh.on("auth:success", ({ username, remoteAddress }) => {
699
+ console.log(`[SSH] ${username} from ${remoteAddress}`);
700
+ });
701
+ ssh.on("auth:lockout", ({ ip, until }) => {
702
+ console.warn(`[SSH] ${ip} locked until ${until}`);
703
+ });
1228
704
 
1229
- ```bash
1230
- ssh root@localhost -p 2222
1231
- # $ whoami
1232
- # root
705
+ await ssh.start();
706
+ process.on("SIGINT", () => { ssh.stop(); process.exit(0); });
1233
707
  ```
1234
708
 
1235
- ---
1236
-
1237
- ### Example 2: Programmatic File Operations
709
+ ### SSH + SFTP with Shared State
1238
710
 
1239
711
  ```typescript
1240
- import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1241
-
1242
- const shell = new VirtualShell("typescript-vm");
1243
- const ssh = new VirtualSshServer({ port: 2222, shell });
1244
- await ssh.start();
1245
-
1246
- const client = new SshClient(shell, "root");
1247
-
1248
- await client.mkdir("/app/config", true);
1249
- await client.mkdir("/app/logs", true);
1250
-
1251
- await client.writeFile("/app/config/settings.json", JSON.stringify({
1252
- environment: "dev",
1253
- port: 8080,
1254
- debug: true,
1255
- }, null, 2));
1256
-
1257
- const result = await client.readFile("/app/config/settings.json");
1258
- console.log("Config:", result.stdout);
712
+ import { VirtualSftpServer, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1259
713
 
1260
- const tree = await client.tree("/app");
1261
- console.log(tree.stdout);
714
+ const shell = new VirtualShell("my-container");
715
+ const ssh = new VirtualSshServer({ port: 2222, shell });
716
+ const sftp = new VirtualSftpServer({ port: 2223, shell });
1262
717
 
1263
- ssh.stop();
718
+ await ssh.start();
719
+ await sftp.start();
1264
720
  ```
1265
721
 
1266
- ---
1267
-
1268
- ### Example 3: Multi-User Environment with Quotas
722
+ ### Multi-User Environment with Quotas
1269
723
 
1270
724
  ```typescript
1271
725
  import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
@@ -1275,10 +729,8 @@ const ssh = new VirtualSshServer({ port: 2222, shell });
1275
729
  await ssh.start();
1276
730
 
1277
731
  const users = ssh.getUsers()!;
1278
-
1279
732
  await users.addUser("alice", "alice123");
1280
- await users.addUser("bob", "bob456");
1281
-
733
+ await users.addUser("bob", "bob456");
1282
734
  await users.removeSudoer("bob");
1283
735
  await users.setQuotaBytes("bob", 5 * 1024 * 1024); // 5 MB
1284
736
 
@@ -1286,59 +738,43 @@ const alice = new SshClient(shell, "alice");
1286
738
  await alice.writeFile("/etc/important.conf", "secret=yes");
1287
739
 
1288
740
  const bob = new SshClient(shell, "bob");
1289
- const result = await bob.cat("/etc/important.conf");
1290
- console.log("Bob read file:", result.stderr); // permission denied
1291
-
1292
- ssh.stop();
741
+ const r = await bob.cat("/etc/important.conf");
742
+ console.log(r.stderr); // permission denied
1293
743
  ```
1294
744
 
1295
- ---
1296
-
1297
- ### Example 4: Persistent State across Restarts
1298
-
1299
- #### Memory mode (manual)
745
+ ### Persistent State
1300
746
 
1301
747
  ```typescript
748
+ // FS mode — automatic .vfsb persistence
749
+ const shell = new VirtualShell("my-vm", undefined, {
750
+ mode: "fs",
751
+ snapshotPath: "./container-data",
752
+ });
753
+ const ssh = new VirtualSshServer({ port: 2222, shell });
754
+ await ssh.start();
755
+ // Restored from ./container-data/vfs-snapshot.vfsb on start, saved on every write.
756
+
757
+ // Memory mode — manual JSON snapshot
1302
758
  import { VirtualFileSystem } from "typescript-virtual-container";
1303
759
  import { writeFileSync, readFileSync } from "node:fs";
1304
760
 
1305
761
  const vfs = new VirtualFileSystem();
1306
762
  vfs.writeFile("/data/report.txt", "Baseline data");
1307
-
1308
- // JSON — portable/human-readable. For binary persistence use mode: "fs".
1309
763
  writeFileSync("snapshot.json", JSON.stringify(vfs.toSnapshot()));
1310
764
 
1311
- const snapshot = JSON.parse(readFileSync("snapshot.json", "utf8"));
1312
- const restored = VirtualFileSystem.fromSnapshot(snapshot);
765
+ const restored = VirtualFileSystem.fromSnapshot(
766
+ JSON.parse(readFileSync("snapshot.json", "utf8"))
767
+ );
1313
768
  console.log(restored.readFile("/data/report.txt")); // Baseline data
1314
769
  ```
1315
770
 
1316
- #### FS mode (automatic)
1317
-
1318
- ```typescript
1319
- import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1320
-
1321
- const shell = new VirtualShell("my-vm", undefined, {
1322
- mode: "fs",
1323
- snapshotPath: "./container-data",
1324
- });
1325
-
1326
- const ssh = new VirtualSshServer({ port: 2222, shell });
1327
- await ssh.start();
1328
- process.on("SIGTERM", () => { ssh.stop(); process.exit(0); });
1329
- ```
1330
-
1331
- ---
1332
-
1333
- ### Example 5: Public-Key Authentication
771
+ ### Public-Key Authentication
1334
772
 
1335
773
  ```typescript
1336
- import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1337
774
  import { readFileSync } from "node:fs";
1338
775
 
1339
776
  const shell = new VirtualShell("secure-vm");
1340
777
  await shell.ensureInitialized();
1341
-
1342
778
  await shell.users.addUser("alice", "fallback-password");
1343
779
 
1344
780
  const pubLine = readFileSync(`${process.env.HOME}/.ssh/id_ed25519.pub`, "utf8").trim();
@@ -1350,9 +786,7 @@ await ssh.start();
1350
786
  // ssh -i ~/.ssh/id_ed25519 alice@localhost -p 2222
1351
787
  ```
1352
788
 
1353
- ---
1354
-
1355
- ### Example 6: Rate Limiting
789
+ ### Rate Limiting
1356
790
 
1357
791
  ```typescript
1358
792
  const ssh = new VirtualSshServer({
@@ -1360,81 +794,26 @@ const ssh = new VirtualSshServer({
1360
794
  maxAuthAttempts: 3,
1361
795
  lockoutDurationMs: 300_000,
1362
796
  });
1363
-
1364
- ssh.on("auth:lockout", ({ ip, until }) => {
1365
- console.warn(`[SSH] ${ip} locked until ${until}`);
1366
- });
1367
-
797
+ ssh.on("auth:lockout", ({ ip, until }) => console.warn(`${ip} locked until ${until}`));
1368
798
  ssh.clearLockout("192.168.1.100"); // manual override
1369
799
  ```
1370
800
 
1371
- ---
1372
-
1373
- ### Example 7: Shell Operators and Variables
801
+ ### Shell Operators and Variables
1374
802
 
1375
803
  ```typescript
1376
- import { SshClient, VirtualShell } from "typescript-virtual-container";
1377
-
1378
- const shell = new VirtualShell("typescript-vm");
1379
- await shell.ensureInitialized();
1380
804
  const client = new SshClient(shell, "root");
1381
805
 
1382
- // && and || operators
1383
806
  await client.exec("mkdir /tmp/test && echo created || echo failed");
807
+ await client.exec("export GREETING=hello && echo $GREETING world");
808
+ await client.exec("false; echo exit=$?"); // exit=1
1384
809
 
1385
- // Chaining with ;
1386
- await client.exec("echo a; echo b; echo c");
1387
-
1388
- // Variable expansion via export then use
1389
- await client.exec("export GREETING=hello");
1390
- await client.exec("echo $GREETING world"); // hello world
1391
-
1392
- // $? last exit code
1393
- await client.exec("false; echo exit=$?"); // exit=1
1394
-
1395
- // Piping
1396
810
  const r = await client.exec("echo -e 'banana\\napple\\ncherry' | sort");
1397
811
  console.log(r.stdout); // apple\nbanana\ncherry
1398
812
  ```
1399
813
 
1400
- ---
1401
-
1402
- ### Example 8: .bashrc
1403
-
1404
- ```typescript
1405
- import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1406
-
1407
- const shell = new VirtualShell("typescript-vm");
1408
- await shell.ensureInitialized();
1409
-
1410
- // Write a .bashrc for the root user
1411
- shell.vfs.mkdir("/home/root", 0o755);
1412
- shell.vfs.writeFile("/home/root/.bashrc", `
1413
- export PS1="\\u@\\h:\\w\\$ "
1414
- export EDITOR=nano
1415
- export PATH="/usr/local/bin:/usr/bin:/bin"
1416
- alias ll="ls -l"
1417
- echo "Welcome back, root!"
1418
- `.trim());
1419
-
1420
- const ssh = new VirtualSshServer({ port: 2222, shell });
1421
- await ssh.start();
1422
- // On interactive login, .bashrc is sourced automatically.
1423
- // "Welcome back, root!" is printed, and $EDITOR is set in the session.
1424
- ```
1425
-
1426
- ---
1427
-
1428
- ### Example 9: Shell Scripting
814
+ ### Shell Scripting
1429
815
 
1430
816
  ```typescript
1431
- import { SshClient, VirtualShell } from "typescript-virtual-container";
1432
-
1433
- const shell = new VirtualShell("typescript-vm");
1434
- await shell.ensureInitialized();
1435
- const client = new SshClient(shell, "root");
1436
-
1437
- // Write a script to VFS
1438
817
  shell.vfs.writeFile("/usr/local/bin/setup.sh", `
1439
818
  #!/bin/sh
1440
819
  for dir in config logs tmp; do
@@ -1443,23 +822,13 @@ for dir in config logs tmp; do
1443
822
  done
1444
823
  if [ -d /app/config ]; then
1445
824
  echo "Setup complete"
1446
- else
1447
- echo "Setup failed"
1448
825
  fi
1449
826
  `);
1450
-
1451
- // Execute it
1452
827
  const r = await client.exec("sh /usr/local/bin/setup.sh");
1453
828
  console.log(r.stdout);
1454
- // Created /app/config
1455
- // Created /app/logs
1456
- // Created /app/tmp
1457
- // Setup complete
1458
829
  ```
1459
830
 
1460
- ---
1461
-
1462
- ### Example 10: Snapshot-Based Test Fixtures
831
+ ### Snapshot-Based Test Fixtures
1463
832
 
1464
833
  ```typescript
1465
834
  import { VirtualFileSystem } from "typescript-virtual-container";
@@ -1469,26 +838,37 @@ function buildFixture(): VfsSnapshot {
1469
838
  const vfs = new VirtualFileSystem();
1470
839
  vfs.mkdir("/app/config");
1471
840
  vfs.writeFile("/app/config/settings.json", JSON.stringify({ env: "test" }));
1472
- vfs.writeFile("/app/README.md", "# My App");
1473
841
  return vfs.toSnapshot();
1474
842
  }
1475
843
 
1476
844
  const FIXTURE = buildFixture();
1477
845
 
1478
846
  test("reads config file", () => {
1479
- const vfs = VirtualFileSystem.fromSnapshot(FIXTURE);
847
+ const vfs = VirtualFileSystem.fromSnapshot(FIXTURE);
1480
848
  const content = JSON.parse(vfs.readFile("/app/config/settings.json"));
1481
849
  expect(content.env).toBe("test");
1482
850
  });
1483
851
  ```
1484
852
 
1485
- ---
853
+ ### Snapshot Diff in Tests
854
+
855
+ ```typescript
856
+ import { diffSnapshots, assertDiff } from "typescript-virtual-container";
857
+
858
+ const before = shell.vfs.toSnapshot();
859
+ await client.exec("apt install vim && mkdir -p /app");
860
+ const after = shell.vfs.toSnapshot();
861
+
862
+ assertDiff(diffSnapshots(before, after, { ignore: ["/proc"] }), {
863
+ added: ["/app", "/usr/bin/vim"],
864
+ modified: ["/var/lib/dpkg/status"],
865
+ });
866
+ ```
1486
867
 
1487
- ### Example 11: Symlinks
868
+ ### Symlinks
1488
869
 
1489
870
  ```typescript
1490
871
  const vfs = new VirtualFileSystem();
1491
- vfs.mkdir("/usr/local/bin");
1492
872
  vfs.writeFile("/opt/myapp/bin/app", "#!/bin/sh\necho hello");
1493
873
  vfs.symlink("/opt/myapp/bin/app", "/usr/local/bin/app");
1494
874
 
@@ -1496,12 +876,10 @@ console.log(vfs.isSymlink("/usr/local/bin/app")); // true
1496
876
  console.log(vfs.resolveSymlink("/usr/local/bin/app")); // /opt/myapp/bin/app
1497
877
  ```
1498
878
 
1499
- ---
1500
-
1501
- ### Example 12: Security Auditing with HoneyPot
879
+ ### Security Auditing with HoneyPot
1502
880
 
1503
881
  ```typescript
1504
- import { HoneyPot, SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
882
+ import { HoneyPot, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
1505
883
 
1506
884
  const shell = new VirtualShell("typescript-vm");
1507
885
  const ssh = new VirtualSshServer({ port: 2222, shell });
@@ -1510,46 +888,15 @@ await ssh.start();
1510
888
  const hp = new HoneyPot(5000);
1511
889
  hp.attach(shell, shell.vfs, shell.users, ssh);
1512
890
 
1513
- const alice = new SshClient(shell, "alice");
1514
- await alice.mkdir("/home/alice/projects", true);
1515
- await alice.writeFile("/home/alice/projects/app.txt", "My application");
1516
-
1517
891
  const stats = hp.getStats();
1518
- console.log(`Commands run: ${stats.commands}`);
1519
- console.log(`File writes: ${stats.fileWrites}`);
892
+ console.log(`Commands: ${stats.commands}, File writes: ${stats.fileWrites}`);
1520
893
 
1521
894
  hp.detectAnomalies().forEach(a =>
1522
895
  console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
1523
896
  );
1524
-
1525
- const authFailures = hp.getAuditLog("auth:failure");
1526
- const sshEvents = hp.getAuditLog(undefined, "SshMimic");
1527
- console.log(`Auth failures: ${authFailures.length}`);
1528
- console.log(`SSH events: ${sshEvents.length}`);
1529
-
1530
- ssh.stop();
1531
897
  ```
1532
898
 
1533
- ---
1534
-
1535
- ### Example 13: Error Handling
1536
-
1537
- ```typescript
1538
- const client = new SshClient(shell, "root");
1539
-
1540
- const r1 = await client.readFile("/etc/nonexistent.conf");
1541
- if (r1.exitCode !== 0) console.error("Read error:", r1.stderr);
1542
-
1543
- const r2 = await client.cd("/invalid/path");
1544
- if (r2.exitCode !== 0) console.error("cd failed");
1545
-
1546
- const r3 = await client.rm("/", true);
1547
- console.log("Remove root:", r3.stderr); // Cannot remove root directory.
1548
- ```
1549
-
1550
- ---
1551
-
1552
- ### Example 14: Concurrent Clients
899
+ ### Concurrent Clients
1553
900
 
1554
901
  ```typescript
1555
902
  const shell = new VirtualShell("typescript-vm");
@@ -1562,160 +909,33 @@ const [r1, r2] = await Promise.all([
1562
909
  ]);
1563
910
  ```
1564
911
 
1565
- ---
1566
-
1567
- ## Linux Rootfs
1568
-
1569
- On every `VirtualShell` init, a realistic Linux directory hierarchy is bootstrapped into the VFS. All paths are created idempotently — FS-mode snapshots survive restarts without duplication.
1570
-
1571
- ### Directory layout
1572
-
1573
- ```
1574
- /
1575
- ├── bin -> /usr/bin (symlink, Debian-style)
1576
- ├── dev/ null, zero, random, urandom, pts/, shm/
1577
- ├── etc/
1578
- │ ├── apt/sources.list Fortune package sources
1579
- │ ├── debian_version
1580
- │ ├── group synced from VirtualUserManager
1581
- │ ├── hostname
1582
- │ ├── hosts 127.0.0.1 localhost + VM hostname
1583
- │ ├── motd
1584
- │ ├── os-release NAME="Fortune GNU/Linux" + ShellProperties
1585
- │ ├── passwd synced from VirtualUserManager
1586
- │ ├── resolv.conf 1.1.1.1 + 8.8.8.8
1587
- │ └── shadow (mode 0o640)
1588
- ├── proc/
1589
- │ ├── 1/ init process (cmdline, status, comm, environ, fd/)
1590
- │ ├── <pid>/ one entry per active session (based on pts/* TTY)
1591
- │ ├── self/ mirrors the most recent session's /proc/<pid>/
1592
- │ ├── cpuinfo real host CPU info
1593
- │ ├── loadavg
1594
- │ ├── meminfo real host memory
1595
- │ ├── net/dev eth0 + lo
1596
- │ ├── uptime shell uptime in seconds
1597
- │ └── version kernel from ShellProperties
1598
- ├── root/ root home + .bashrc
1599
- ├── sys/devices/virtual/dmi/id/
1600
- │ ├── sys_vendor "Fortune Systems"
1601
- │ └── product_name "VirtualContainer v1"
1602
- ├── tmp/ (mode 0o1777 sticky)
1603
- ├── usr/bin/ stubs for all built-in commands
1604
- ├── var/
1605
- │ ├── lib/dpkg/status managed by VirtualPackageManager
1606
- │ └── log/ syslog, auth.log, dpkg.log, apt/
1607
- └── ...
1608
- ```
1609
-
1610
- ### API
1611
-
1612
- ```ts
1613
- shell.refreshProcFs(); // refresh /proc/* with current system state
1614
- shell.syncPasswd(); // sync /etc/passwd|group|shadow from VirtualUserManager
1615
- ```
1616
-
1617
- `syncPasswd()` is called automatically on `bootstrapLinuxRootfs`. Call it again after `users.addUser()` / `users.deleteUser()` to keep `/etc/passwd` consistent.
1618
-
1619
- ---
1620
-
1621
- ## Package Manager (apt/dpkg)
1622
-
1623
- A pure-TypeScript APT/dpkg simulation backed by a built-in package registry. No external process is spawned.
1624
-
1625
- ### Workflow
1626
-
1627
- ```
1628
- apt install <pkg> → resolves deps → writes files to VFS → updates /var/lib/dpkg/status
1629
- apt remove <pkg> → removes VFS files → updates status
1630
- dpkg -l → reads installed packages from VirtualPackageManager
1631
- ```
1632
-
1633
- ### Built-in package registry (25 packages)
1634
-
1635
- | Package | Version | Section |
1636
- |---------|---------|---------|
1637
- | `vim` | 2:9.0.1378-2 | editors |
1638
- | `git` | 1:2.39.2-1 | vcs |
1639
- | `python3` | 3.11.2-1 | python |
1640
- | `nodejs` | 18.19.0 | javascript |
1641
- | `npm` | 9.2.0 | javascript |
1642
- | `curl` | 7.88.1-10 | web |
1643
- | `wget` | 1.21.3-1 | web |
1644
- | `htop` | 3.2.2-2 | utils |
1645
- | `openssh-client` | 1:9.2p1-2 | net |
1646
- | `openssh-server` | 1:9.2p1-2 | net |
1647
- | `net-tools` | 2.10-0.1 | net |
1648
- | `iputils-ping` | 3:20221126-1 | net |
1649
- | `jq` | 1.6-2.1 | utils |
1650
- | `build-essential` | 12.9 | devel |
1651
- | `gcc` | 4:12.2.0-3 | devel |
1652
- | `g++` | 4:12.2.0-3 | devel |
1653
- | `make` | 4.3-4.1 | devel |
1654
- | `less` | 590-2 | text |
1655
- | `unzip` | 6.0-28 | utils |
1656
- | `rsync` | 3.2.7-1 | net |
1657
- | `tmux` | 3.3a-3 | utils |
1658
- | `tree` | 2.1.0-1 | utils |
1659
- | `ca-certificates` | 20230311 | misc |
1660
- | `sudo` | 1.9.13p3-1 | admin |
1661
- | `systemd` | 252.22-1 | admin |
1662
-
1663
- > **Note on package stubs**: installing `nodejs` or `python3` writes stubs into `/usr/bin/` and makes them discoverable via `which` and `dpkg -L`. The stubs print a version line but do not execute real binaries — the shell runtime is pure TypeScript with no `execvp`. This makes them useful for testing install workflows and `which`/`dpkg -L` queries, not for running actual scripts.
1664
-
1665
- ### `VirtualPackageManager` API
1666
-
1667
- ```ts
1668
- // Access via shell instance
1669
- const pm = shell.packageManager;
1670
-
1671
- pm.install(["vim", "git"], { quiet: false }) // → { output, exitCode }
1672
- pm.remove(["vim"], { purge: false }) // → { output, exitCode }
1673
- pm.search("editor") // → PackageDefinition[]
1674
- pm.show("vim") // → string (dpkg-style block)
1675
- pm.listInstalled() // → InstalledPackage[]
1676
- pm.listAvailable() // → PackageDefinition[]
1677
- pm.isInstalled("vim") // → boolean
1678
- pm.installedCount() // → number
1679
- pm.findInRegistry("nodejs") // → PackageDefinition | undefined
1680
- ```
1681
-
1682
- ### Registering custom packages
1683
-
1684
- ```ts
1685
- import { VirtualPackageManager } from "typescript-virtual-container";
1686
-
1687
- // Custom packages can be added to the registry before shell init
1688
- // by extending PACKAGE_REGISTRY or by calling install() with custom defs.
912
+ ### .bashrc
1689
913
 
1690
- // Or: register a post-install hook via onInstall
1691
- const customPkg = {
1692
- name: "myapp",
1693
- version: "1.0.0",
1694
- description: "My application",
1695
- files: [
1696
- { path: "/usr/bin/myapp", content: "#!/bin/sh\necho myapp v1.0.0\n", mode: 0o755 },
1697
- { path: "/etc/myapp/config.json", content: JSON.stringify({ port: 3000 }) },
1698
- ],
1699
- onInstall: (vfs) => {
1700
- vfs.mkdir("/var/lib/myapp", 0o755);
1701
- vfs.mkdir("/var/log/myapp", 0o755);
1702
- },
1703
- };
914
+ ```typescript
915
+ shell.vfs.writeFile("/home/root/.bashrc", `
916
+ export EDITOR=nano
917
+ export PATH="/usr/local/bin:/usr/bin:/bin"
918
+ alias ll="ls -l"
919
+ echo "Welcome back, root!"
920
+ `.trim());
921
+ // On interactive SSH login, .bashrc is sourced automatically.
1704
922
  ```
1705
923
 
924
+ </details>
1706
925
 
1707
926
  ---
1708
927
 
1709
- ## Built-in Commands
928
+ <details>
929
+ <summary><strong>Built-in Commands (91)</strong></summary>
1710
930
 
1711
- All commands are available in SSH shell mode and via `SshClient.exec()`. Type `help` in the shell for a grouped, colorized listing. Type `help <command>` for detailed usage.
931
+ Type `help` in the shell for a grouped, colorized listing. Type `help <command>` for detailed usage.
1712
932
 
1713
933
  ### Navigation
1714
934
 
1715
935
  | Command | Flags | Description |
1716
936
  |---------|-------|-------------|
1717
937
  | `cd <path>` | | Change directory |
1718
- | `ls [path]` | `-l` `-a` | List directory contents (`-a` shows dotfiles) |
938
+ | `ls [path]` | `-l` `-a` | List directory (`-a` shows dotfiles) |
1719
939
  | `pwd` | | Print working directory |
1720
940
  | `tree [path]` | | ASCII directory tree |
1721
941
 
@@ -1723,29 +943,32 @@ All commands are available in SSH shell mode and via `SshClient.exec()`. Type `h
1723
943
 
1724
944
  | Command | Flags | Description |
1725
945
  |---------|-------|-------------|
1726
- | `cat <path...>` | `-n` `-b` | Concatenate and print files; `-n` numbers all lines, `-b` numbers non-blank lines |
1727
- | `chmod <mode> <file>` | | Change file permissions — octal (`755`) or symbolic (`+x`, `u+x`, `go-w`, `a=rx`) |
946
+ | `cat <path...>` | `-n` `-b` | Concatenate and print; `-n` numbers lines, `-b` numbers non-blank |
947
+ | `chmod <mode> <file>` | | Octal (`755`) or symbolic (`+x`, `u+x`, `go-w`, `a=rx`) |
1728
948
  | `cp <src> <dest>` | `-r` | Copy file or directory |
1729
- | `find [path]` | `-name <pat>` `-type f\|d` | Search for files |
1730
- | `ln <target> <link>` | `-s` | Create hard or symbolic link |
949
+ | `find [path]` | `-name` `-type` | Search for files |
950
+ | `ln <target> <link>` | `-s` | Hard or symbolic link |
951
+ | `readlink <path>` | `-f` | Print resolved path of symbolic link |
1731
952
  | `mkdir <path>` | `-p` | Create directory |
1732
953
  | `mv <src> <dest>` | | Move or rename |
1733
- | `rm <path>` | `-r` | Remove file or directory |
1734
954
  | `nano <path>` | | Interactive text editor |
955
+ | `rm <path>` | `-r` | Remove file or directory |
956
+ | `stat <path>` | `-c <format>` | Display file status (inode, size, mode, timestamps) |
1735
957
  | `touch <path>` | | Create or update file |
1736
958
 
1737
959
  ### Text Processing
1738
960
 
1739
961
  | Command | Flags | Description |
1740
962
  |---------|-------|-------------|
1741
- | `awk [-F <sep>] '<prog>'` | | Pattern scanning (print $N) |
1742
- | `cut` | `-d <sep>` `-f <cols>` | Remove sections from lines |
963
+ | `awk [-F <sep>] '<prog>'` | | Pattern scanning |
964
+ | `base64` | `-d` | Encode/decode base64 |
965
+ | `cut` | `-d` `-f` | Remove sections from lines |
1743
966
  | `diff <f1> <f2>` | | Compare files line by line |
1744
967
  | `grep <pattern> [files]` | `-i` `-v` `-n` `-r` | Search file content |
1745
- | `head [files]` | `-n <N>` | First N lines (default 10) |
968
+ | `head [files]` | `-n <N>` | First N lines |
1746
969
  | `sed -e 's/pat/rep/[g]'` | `-i` | Stream editor |
1747
970
  | `sort [files]` | `-r` `-n` `-u` | Sort lines |
1748
- | `tail [files]` | `-n <N>` | Last N lines (default 10) |
971
+ | `tail [files]` | `-n <N>` | Last N lines |
1749
972
  | `tee [files]` | `-a` | Read stdin, write to stdout and files |
1750
973
  | `tr <set1> [set2]` | `-d` | Translate or delete characters |
1751
974
  | `uniq` | `-c` `-d` `-u` | Filter repeated lines |
@@ -1756,85 +979,82 @@ All commands are available in SSH shell mode and via `SshClient.exec()`. Type `h
1756
979
 
1757
980
  | Command | Flags | Description |
1758
981
  |---------|-------|-------------|
1759
- | `base64` | `-d` | Encode/decode base64 |
1760
- | `gzip <file>` | | Compress file |
1761
- | `gunzip <file>` | | Decompress file |
982
+ | `gzip <file>` / `gunzip <file>` | | Compress / decompress |
1762
983
  | `tar <archive> [files]` | `-czf` `-xzf` `-tf` | Archive utility |
1763
984
 
1764
985
  ### System
1765
986
 
1766
987
  | Command | Flags | Description |
1767
988
  |---------|-------|-------------|
1768
- | `date` | `+format` | Print current date and time |
1769
- | `df` | `-h` | Filesystem disk space usage |
1770
- | `du [path]` | `-h` `-s` | Estimate file space usage |
1771
- | `groups [user]` | | Print group memberships |
989
+ | `date` | `+format` | Current date and time |
990
+ | `df` | `-h` | Filesystem disk space |
991
+ | `du [path]` | `-h` `-s` | Estimate file space |
992
+ | `free` | `-h` `-m` `-g` | Memory usage (real host data) |
993
+ | `groups [user]` | | Group memberships |
1772
994
  | `hostname` | | Print hostname |
1773
995
  | `htop` | | System monitor (mock) |
1774
- | `id [user]` | | Print user identity (uid/gid/groups) |
1775
- | `kill [-9] <pid>` | | Send signal to process (mock) |
1776
- | `free` | `-h` `-m` `-g` | Display free and used memory |
1777
- | `lsb_release` | `-a` `-i` `-d` `-r` `-c` | Print distribution information |
1778
- | `neofetch` | | System info display (shows real packages/uptime) |
1779
- | `node` | `--version` `-e` `-p` | JavaScript runtime (virtual REPL evaluates expressions and VFS `.js` files) |
1780
- | `python3` | `--version` `-c` `-V` | Python 3 interpreter (virtual REPL — evaluates expressions and VFS `.py` files); alias `python` |
1781
- | `uptime` | `-p` `-s` | Tell how long the system has been running |
1782
- | `ping [-c <n>] <host>` | | Send ICMP ECHO_REQUEST (mock) |
1783
- | `ps` | `-a` `-u` `-x` `aux` | Report process status; `-u` / `aux` shows USER/PID/%CPU/%MEM columns |
996
+ | `id [user]` | | User identity (uid/gid/groups) |
997
+ | `kill [-9] <pid>` | | Send signal (mock) |
998
+ | `lsb_release` | `-a` `-i` `-d` `-r` `-c` | Distribution info |
999
+ | `neofetch` | | System info (real package count and uptime) |
1000
+ | `node` | `--version` `-e` `-p` | Virtual JS runtime; **requires `apt install nodejs`** |
1001
+ | `npm` | `--version` `list` `version` | Node.js package manager (install/run stubbed); **requires `apt install npm`** |
1002
+ | `npx` | `--version` | Node.js package runner (stubbed); **requires `apt install npm`** |
1003
+ | `ping [-c <n>] <host>` | | ICMP ECHO_REQUEST (mock) |
1004
+ | `ps` | `-a` `-u` `-x` `aux` | Process status |
1005
+ | `python3` | `--version` `-c` `-V` | Virtual Python 3 interpreter; alias `python`; **requires `apt install python3`** |
1784
1006
  | `sleep <seconds>` | | Delay execution |
1785
- | `uname` | `-a` `-r` `-m` | Print system information |
1786
- | `who` | | List active sessions |
1787
- | `whoami` | | Print current user |
1007
+ | `uname` | `-a` `-r` `-m` | System information |
1008
+ | `uptime` | `-p` `-s` | Running time |
1009
+ | `who` | | Active sessions |
1010
+ | `whoami` | | Current user |
1788
1011
 
1789
1012
  ### Network
1790
1013
 
1791
1014
  | Command | Flags | Description |
1792
1015
  |---------|-------|-------------|
1793
- | `curl <url>` | `-o` `-X` `-d` `-H` `-s` `-I` `-L` `-v` | HTTP client (pure `fetch()`, no host binary) |
1794
- | `wget <url>` | `-O` `-P` `-q` | File downloader (pure `fetch()`, no host binary) |
1016
+ | `curl <url>` | `-o` `-X` `-d` `-H` `-s` `-I` `-L` `-v` | HTTP client (pure `fetch()`) |
1017
+ | `wget <url>` | `-O` `-P` `-q` | File downloader (pure `fetch()`) |
1795
1018
 
1796
- ### Shell
1019
+ ### Shell & Scripting
1797
1020
 
1798
1021
  | Command | Flags | Description |
1799
1022
  |---------|-------|-------------|
1800
1023
  | `alias [name=value]` | | Define or display aliases |
1801
- | `clear` | | Clear terminal screen (full ANSI reset) |
1802
- | `declare [name=value]` | `-i` `-r` `-x` | Declare variables with attributes; aliases `local`, `typeset` |
1803
- | `echo <text>` | `-n` `-e` | Display text; `-n` suppresses newline, `-e` interprets `\n` `\t` `\r` `\\` |
1804
- | `env` | | Print session environment variables |
1805
- | `exit [code]` | | Exit session with optional exit code |
1806
- | `export NAME=VALUE` | | Set shell variable in current session |
1807
- | `help [command]` | | List commands grouped by category, or show usage for a specific command |
1808
- | `history [n]` | | Display command history (last N entries) |
1809
- | `man <command>` | | Display command reference manual |
1810
- | `printf <fmt> [args...]` | | Format and print data (`%s` `%d` `%f` `%x` `\n` `\t`) |
1811
- | `read [-r] <var...>` | `-r` `-p` | Read a line from stdin into variable(s) |
1812
- | `return [n]` | | Return from a shell function with optional exit code |
1024
+ | `clear` | | Clear terminal screen |
1025
+ | `declare [name=value]` | `-i` `-r` `-x` | Declare variables; aliases `local`, `typeset` |
1026
+ | `echo <text>` | `-n` `-e` | Display text; `-e` interprets `\n` `\t` `\r` `\\` |
1027
+ | `env` | | Print session environment |
1028
+ | `exit [code]` | | Exit session |
1029
+ | `export NAME=VALUE` | | Set shell variable |
1030
+ | `false` | | Return exit code 1 |
1031
+ | `help [command]` | | List commands or show command usage |
1032
+ | `history [n]` | | Command history |
1033
+ | `man <command>` | | Command reference manual |
1034
+ | `printf <fmt> [args...]` | | Format and print (`%s` `%d` `%f` `%x` `\n` `\t`) |
1035
+ | `read [-r] <var...>` | `-r` `-p` | Read stdin into variable(s) |
1036
+ | `return [n]` | | Return from shell function |
1813
1037
  | `set [VAR=val]` | | Display or set shell variables |
1814
- | `sh` | `-c <script>` `[file]` | Execute shell script — supports `if`/`for`/`while`/`case`/functions, bare `VAR=val` assignments, `$((expr))` arithmetic; single-quoted args are never expanded |
1815
- | `shift [n]` | | Shift positional parameters left by n (default 1) |
1816
- | `source <file>` | | Execute file in current shell environment; aliases and exports persist |
1817
- | `. <file>` | | Alias for `source` |
1818
- | `test <expr>` | | Evaluate POSIX conditional expression |
1819
- | `[ <expr> ]` | | Alias for `test`; supports `-f` `-d` `-e` `-z` `-n` `-x` `-s` `=` `!=` `-eq` `-lt` `-gt` `-le` `-ge` `!` `-a` `-o` |
1820
- | `trap [action] [signal]` | | Register handler for shell signals; supports `EXIT` |
1821
- | `true` | | Return success exit code (0) |
1822
- | `false` | | Return failure exit code (1) |
1823
- | `type <command>` | | Describe how a command would be interpreted (builtin vs PATH) |
1824
- | `unalias <name>` | `-a` | Remove alias definitions |
1038
+ | `sh` | `-c <script>` `[file]` | Execute shell script — `if`/`for`/`while`/`case`/functions, `$((expr))`, single-quote-safe |
1039
+ | `shift [n]` | | Shift positional parameters |
1040
+ | `source <file>` | | Execute file in current env; alias `.` |
1041
+ | `test <expr>` / `[ <expr> ]` | | POSIX conditional: `-f` `-d` `-e` `-z` `-n` `-x` `-s` `=` `!=` `-eq` `-lt` `-gt` `-le` `-ge` `!` `-a` `-o` |
1042
+ | `trap [action] [signal]` | | Signal handlers; supports `EXIT` |
1043
+ | `true` | | Return exit code 0 |
1044
+ | `type <command>` | | Describe command interpretation |
1045
+ | `unalias <name>` | `-a` | Remove aliases |
1825
1046
  | `unset <VAR>` | | Remove shell variable |
1826
- | `which <command>` | | Locate a command in the session PATH |
1047
+ | `which <command>` | | Locate command in `$PATH` |
1827
1048
 
1828
1049
  ### Package Management
1829
1050
 
1830
1051
  | Command | Flags | Description |
1831
1052
  |---------|-------|-------------|
1832
- | `apt <cmd> [pkg...]` | | Package manager (`install`, `remove`, `purge`, `update`, `upgrade`, `search`, `show`, `list`) |
1833
- | `apt-get <cmd>` | | Alias for `apt` |
1834
- | `apt-cache <cmd>` | | Query package cache (`search`, `show`, `policy`) |
1835
- | `dpkg` | `-l` `-s` `-L` `-r` `-P` | Debian package manager low-level tool |
1836
- | `dpkg-query` | `-W` `-l` | Show information about installed packages |
1837
-
1053
+ | `apt <cmd> [pkg...]` | | `install`, `remove`, `purge`, `update`, `upgrade`, `search`, `show`, `list` |
1054
+ | `apt-get` | | Alias for `apt` |
1055
+ | `apt-cache <cmd>` | | `search`, `show`, `policy` |
1056
+ | `dpkg` | `-l` `-s` `-L` `-r` `-P` | Low-level package tool |
1057
+ | `dpkg-query` | `-W` `-l` | Show installed package info |
1838
1058
 
1839
1059
  ### Users & Permissions
1840
1060
 
@@ -1846,29 +1066,29 @@ All commands are available in SSH shell mode and via `SshClient.exec()`. Type `h
1846
1066
  | `su [user]` | | Switch user |
1847
1067
  | `sudo <cmd>` | `-i` | Run as root |
1848
1068
 
1849
- Custom commands can be added via `shell.addCommand()`.
1069
+ **ℹ️ All 91 built-in commands include complete JSDoc documentation** with `@category` and `@params` tags. See [src/commands/](src/commands/) for source code and inline documentation.
1850
1070
 
1851
- ---
1071
+ Custom commands: `shell.addCommand(name, params, callback)`.
1852
1072
 
1853
- ## Shell Scripting
1073
+ </details>
1854
1074
 
1855
- The shell interpreter supports a subset of POSIX sh syntax, usable both interactively and via `sh -c '...'` or `sh <file>`.
1075
+ ---
1856
1076
 
1857
- ### Logical Operators
1077
+ <details>
1078
+ <summary><strong>Shell Scripting</strong></summary>
1858
1079
 
1859
- ```bash
1860
- mkdir /app && echo "created" # run second only if first succeeds
1861
- rm /missing || echo "not found" # run second only if first fails
1862
- echo a; echo b; echo c # always run all three
1863
- ```
1080
+ The interpreter supports a POSIX sh subset via `sh -c '...'`, `sh <file>`, or interactive session.
1864
1081
 
1865
- ### Pipes and Redirections
1082
+ ### Operators and Redirections
1866
1083
 
1867
1084
  ```bash
1085
+ mkdir /app && echo "created" # && — only if previous succeeds
1086
+ rm /missing || echo "not found" # || — only if previous fails
1087
+ echo a; echo b; echo c # ; — always all three
1088
+
1868
1089
  cat /etc/hosts | grep local
1869
- ls /home | sort | head -5
1870
- echo "hello world" > /tmp/out.txt
1871
- cat /tmp/out.txt >> /tmp/log.txt
1090
+ echo "hello" > /tmp/out.txt # overwrite
1091
+ cat /tmp/out.txt >> /tmp/log.txt # append
1872
1092
  ```
1873
1093
 
1874
1094
  ### Variable Expansion
@@ -1876,34 +1096,25 @@ cat /tmp/out.txt >> /tmp/log.txt
1876
1096
  ```bash
1877
1097
  export NAME=world
1878
1098
  echo "Hello $NAME" # Hello world
1879
- echo "${NAME}" # world (braced form)
1880
1099
  echo "${NAME:-fallback}" # world (or fallback if unset)
1881
- echo "${UNSET:-default}" # default (assign-on-read: use ${UNSET:=default})
1100
+ echo "${UNSET:-default}" # default
1882
1101
  echo "${NAME:+alternate}" # alternate (only if NAME is set)
1102
+ echo "${UNSET:=assigned}" # assigns and returns "assigned"
1883
1103
  echo "${#NAME}" # 5 (string length)
1884
1104
  echo "$?" # last exit code
1885
- echo "~" # /home/<user> (tilde expansion)
1105
+ echo ~ # /home/<user> (tilde expansion)
1886
1106
  ```
1887
1107
 
1888
- ### Arithmetic Expansion
1108
+ > **Single-quote isolation** — `$VAR` and `$((...))` are never expanded inside `'...'`.
1109
+
1110
+ ### Arithmetic
1889
1111
 
1890
1112
  ```bash
1891
- echo $((2 + 3)) # 5
1892
- echo $((10 % 3)) # 1
1893
- X=4; echo $((X * 2)) # 8
1894
- i=0; i=$((i + 1)); echo $i # 1
1895
-
1896
- # In loops (via sh):
1897
- sh -c 'i=0
1898
- while [ $i -lt 5 ]; do
1899
- echo $i
1900
- i=$((i + 1))
1901
- done'
1113
+ echo $((2 + 3)) # 5
1114
+ X=4; echo $((X * 2)) # 8
1115
+ i=0; i=$((i + 1)) # increment
1902
1116
  ```
1903
1117
 
1904
- > **Single-quote isolation** — variable and arithmetic expansion never occurs inside single quotes.
1905
- > `echo '$NAME'` always outputs the literal string `$NAME`.
1906
-
1907
1118
  ### Conditionals
1908
1119
 
1909
1120
  ```bash
@@ -1915,41 +1126,44 @@ else
1915
1126
  echo "nothing found"
1916
1127
  fi
1917
1128
 
1918
- # String comparison
1919
- if [ "$USER" = "root" ]; then echo "root"; fi
1920
-
1921
- # Numeric comparison
1922
- if [ $COUNT -gt 10 ]; then echo "large"; fi
1129
+ [ "$USER" = "root" ] && echo "root"
1130
+ [ $COUNT -gt 10 ] && echo "large"
1923
1131
  ```
1924
1132
 
1925
1133
  ### Loops
1926
1134
 
1927
1135
  ```bash
1928
- # for loop
1929
1136
  for name in alice bob charlie; do
1930
1137
  echo "Hello $name"
1931
1138
  done
1932
1139
 
1933
- # while loop
1934
1140
  COUNT=0
1935
1141
  while [ $COUNT -lt 3 ]; do
1936
1142
  echo "Count: $COUNT"
1937
- export COUNT=$((COUNT + 1))
1143
+ COUNT=$((COUNT + 1))
1938
1144
  done
1939
1145
  ```
1940
1146
 
1941
- ### Script Files
1942
-
1943
- Write a script to the VFS and execute it:
1147
+ ### Functions and case
1944
1148
 
1945
1149
  ```bash
1946
- # Via SSH shell:
1947
- nano /usr/local/bin/deploy.sh
1948
- # ... write the script ...
1949
- sh /usr/local/bin/deploy.sh
1150
+ greet() {
1151
+ echo "Hello, $1!"
1152
+ }
1153
+ greet world
1154
+
1155
+ case "$1" in
1156
+ start) echo "Starting..." ;;
1157
+ stop) echo "Stopping..." ;;
1158
+ *) echo "Usage: $0 {start|stop}" ;;
1159
+ esac
1160
+ ```
1950
1161
 
1951
- # Via programmatic client:
1952
- await client.writeFile("/usr/local/bin/setup.sh", `
1162
+ ### Script Files
1163
+
1164
+ ```typescript
1165
+ shell.vfs.writeFile("/usr/local/bin/setup.sh", `
1166
+ #!/bin/sh
1953
1167
  for dir in config logs tmp; do
1954
1168
  mkdir /app/$dir
1955
1169
  done
@@ -1959,252 +1173,247 @@ await client.exec("sh /usr/local/bin/setup.sh");
1959
1173
 
1960
1174
  ### .bashrc
1961
1175
 
1962
- On every interactive SSH login, `/home/<user>/.bashrc` is sourced automatically. Use it to set environment variables, aliases (via `sh -c`), or print a welcome message.
1176
+ Sourced automatically on every interactive session from `/home/<user>/.bashrc`:
1963
1177
 
1964
1178
  ```bash
1965
- # /home/alice/.bashrc
1966
1179
  export EDITOR=nano
1967
- export PATH="/usr/local/bin:/usr/bin:/bin"
1968
- echo "Welcome, Alice!"
1180
+ alias ll="ls -l"
1181
+ echo "Welcome, $USER!"
1182
+ ```
1183
+
1184
+ </details>
1185
+
1186
+ ---
1187
+
1188
+ <details>
1189
+ <summary><strong>Linux Rootfs &amp; VFS PATH Resolution</strong></summary>
1190
+
1191
+ ### Directory Layout
1192
+
1193
+ On every `VirtualShell` init, a realistic Linux hierarchy is bootstrapped idempotently:
1194
+
1195
+ ```
1196
+ /
1197
+ ├── bin -> /usr/bin (symlink, Debian-style)
1198
+ ├── dev/ null, zero, random, urandom, pts/, shm/
1199
+ ├── etc/
1200
+ │ ├── group synced from VirtualUserManager
1201
+ │ ├── hostname
1202
+ │ ├── hosts 127.0.0.1 localhost + VM hostname
1203
+ │ ├── os-release NAME="Fortune GNU/Linux" + ShellProperties
1204
+ │ ├── passwd synced from VirtualUserManager
1205
+ │ ├── resolv.conf 1.1.1.1 + 8.8.8.8
1206
+ │ └── shadow (mode 0o640)
1207
+ ├── proc/
1208
+ │ ├── 1/ init process (cmdline, status, comm, environ, fd/)
1209
+ │ ├── <pid>/ one entry per active session (pts/* TTY)
1210
+ │ ├── self/ mirrors most recent session's /proc/<pid>/
1211
+ │ ├── cpuinfo real host CPU info
1212
+ │ ├── meminfo real host memory
1213
+ │ └── ...
1214
+ ├── sys/devices/virtual/dmi/id/
1215
+ ├── tmp/ (mode 0o1777 sticky)
1216
+ ├── usr/bin/ stubs for all built-in commands
1217
+ └── var/lib/dpkg/status managed by VirtualPackageManager
1218
+ ```
1219
+
1220
+ ```typescript
1221
+ shell.refreshProcFs(); // refresh /proc/* with current system state
1222
+ shell.syncPasswd(); // sync /etc/passwd|group|shadow from user manager
1969
1223
  ```
1970
1224
 
1225
+ ### VFS PATH Resolution
1226
+
1227
+ When a command is not a built-in, the shell searches `$PATH` in the VFS — binaries installed by `apt` become immediately accessible.
1228
+
1229
+ - `/sbin` and `/usr/sbin` are excluded from `$PATH` for non-root users.
1230
+ - Stubs containing `exec builtin <name>` delegate to the named built-in TS command.
1231
+
1232
+ **Package-gated commands** (require `apt install`):
1233
+
1234
+ | Command | Package |
1235
+ |---------|---------|
1236
+ | `node` | `nodejs` |
1237
+ | `python3` / `python` | `python3` |
1238
+ | `npm` / `npx` | `npm` |
1239
+
1240
+ ### Built-in Package Registry (25 packages)
1241
+
1242
+ | Package | Version | Section |
1243
+ |---------|---------|---------|
1244
+ | `vim` | 2:9.0.1378-2 | editors |
1245
+ | `git` | 1:2.39.2-1 | vcs |
1246
+ | `python3` | 3.11.2-1 | python |
1247
+ | `nodejs` | 18.19.0 | javascript |
1248
+ | `npm` | 9.2.0 | javascript |
1249
+ | `curl` | 7.88.1-10 | web |
1250
+ | `wget` | 1.21.3-1 | web |
1251
+ | `htop` | 3.2.2-2 | utils |
1252
+ | `openssh-client` | 1:9.2p1-2 | net |
1253
+ | `openssh-server` | 1:9.2p1-2 | net |
1254
+ | `net-tools` | 2.10-0.1 | net |
1255
+ | `iputils-ping` | 3:20221126-1 | net |
1256
+ | `jq` | 1.6-2.1 | utils |
1257
+ | `build-essential` | 12.9 | devel |
1258
+ | `gcc` | 4:12.2.0-3 | devel |
1259
+ | `g++` | 4:12.2.0-3 | devel |
1260
+ | `make` | 4.3-4.1 | devel |
1261
+ | `less` | 590-2 | text |
1262
+ | `unzip` | 6.0-28 | utils |
1263
+ | `rsync` | 3.2.7-1 | net |
1264
+ | `tmux` | 3.3a-3 | utils |
1265
+ | `tree` | 2.1.0-1 | utils |
1266
+ | `ca-certificates` | 20230311 | misc |
1267
+ | `sudo` | 1.9.13p3-1 | admin |
1268
+ | `systemd` | 252.22-1 | admin |
1269
+
1270
+ </details>
1271
+
1971
1272
  ---
1972
1273
 
1973
- ## Configuration
1274
+ <details>
1275
+ <summary><strong>Configuration</strong></summary>
1974
1276
 
1975
1277
  ### Environment Variables
1976
1278
 
1977
1279
  | Variable | Default | Description |
1978
1280
  |----------|---------|-------------|
1979
- | `SSH_MIMIC_FAST_PASSWORD_HASH` | `""` | Use SHA-256 instead of scrypt (faster, less secure — dev only). Set to `1` or `true`. |
1281
+ | `SSH_MIMIC_FAST_PASSWORD_HASH` | `""` | Use SHA-256 instead of scrypt (dev only). Set to `1` or `true`. |
1980
1282
  | `SSH_MIMIC_AUTO_SUDO_NEW_USERS` | `"true"` | Auto-grant sudo to new users. Set to `0`, `false`, `no`, or `off` to disable. |
1981
1283
  | `DEV_MODE` | `""` | Enable performance logging. |
1982
1284
  | `RENDER_PERF` | `""` | Enable render performance logging. |
1983
1285
 
1984
- **Example:**
1985
-
1986
- ```bash
1987
- export SSH_MIMIC_FAST_PASSWORD_HASH=1
1988
- export SSH_MIMIC_AUTO_SUDO_NEW_USERS=false
1989
- node server.js
1990
- ```
1991
-
1992
- ### Runtime Options Summary
1286
+ ### Constructor Options
1993
1287
 
1994
1288
  ```typescript
1995
- // VirtualShell
1996
- new VirtualShell(
1997
- hostname,
1998
- properties?, // kernel, os, arch strings
1999
- vfsOptions?, // { mode: "memory"|"fs", snapshotPath?: string }
2000
- )
1289
+ new VirtualShell(hostname, properties?, vfsOptions?)
1290
+ // vfsOptions: { mode: "memory"|"fs", snapshotPath?: string }
2001
1291
 
2002
- // VirtualSshServer
2003
1292
  new VirtualSshServer({
2004
- port,
2005
- hostname?,
1293
+ port, hostname?,
2006
1294
  shell?,
2007
- maxAuthAttempts?, // default: 5
2008
- lockoutDurationMs?, // default: 60_000
1295
+ maxAuthAttempts?, // default: 5
1296
+ lockoutDurationMs?, // default: 60_000
2009
1297
  })
2010
1298
 
2011
- // VirtualSftpServer
2012
- new VirtualSftpServer({
2013
- port,
2014
- hostname?,
2015
- shell?, // or: vfs + users separately
2016
- })
1299
+ new VirtualSftpServer({ port, hostname?, shell? })
1300
+ // or: { port, vfs, users } without a shell instance
2017
1301
  ```
2018
1302
 
2019
- ---
2020
-
2021
- ## Performance & Scalability
1303
+ </details>
2022
1304
 
2023
- ### Benchmarking
1305
+ ---
2024
1306
 
2025
- Use the built-in benchmark script:
1307
+ <details>
1308
+ <summary><strong>Performance &amp; Scalability</strong></summary>
2026
1309
 
2027
1310
  ```bash
2028
1311
  bun ./benchmark-virtualshell.ts
2029
1312
  ```
2030
1313
 
2031
- The benchmark reports:
2032
- - Shell initialization time by concurrency level
2033
- - Command execution time across active shells
2034
- - RSS memory growth during the run
1314
+ Reports shell initialization time, command execution time, and RSS memory by concurrency. Designed to scale to **1000+ parallel environments**.
2035
1315
 
2036
- Recent baselines show strong startup behavior up to 100 concurrent shells. The runtime is designed to scale easily to **1000+ parallel environments** for testing and automation workloads.
1316
+ - SSH server is event-driven handles many concurrent connections.
1317
+ - `SshClient` is sequential per instance — create multiple for parallel operations.
1318
+ - Each `VirtualShell` is fully independent.
2037
1319
 
2038
- ### Concurrency
2039
-
2040
- - SSH server is event-driven and handles multiple concurrent connections.
2041
- - `SshClient` is sequential per instance — create multiple instances for parallel operations.
2042
- - Each `VirtualShell` instance is fully independent (separate VFS, users, env state).
2043
-
2044
- ### Performance Tips
2045
-
2046
- - Use `SSH_MIMIC_FAST_PASSWORD_HASH=1` in test environments to skip scrypt overhead.
1320
+ **Tips:**
1321
+ - Use `SSH_MIMIC_FAST_PASSWORD_HASH=1` in tests to skip scrypt overhead.
2047
1322
  - Reuse long-lived shell instances for low-latency command bursts.
2048
- - Keep `DEV_MODE=1` enabled only during development (adds logging overhead).
2049
-
2050
- **Parallel clients example:**
2051
-
2052
- ```typescript
2053
- const shell = new VirtualShell("typescript-vm");
2054
- const client1 = new SshClient(shell, "alice");
2055
- const client2 = new SshClient(shell, "bob");
2056
1323
 
2057
- const [r1, r2] = await Promise.all([
2058
- client1.writeFile("/tmp/alice.txt", "..."),
2059
- client2.writeFile("/tmp/bob.txt", "..."),
2060
- ]);
2061
- ```
1324
+ </details>
2062
1325
 
2063
1326
  ---
2064
1327
 
2065
- ## Types & TypeScript
2066
-
2067
- Full TypeScript support with exported types:
1328
+ <details>
1329
+ <summary><strong>Types &amp; TypeScript</strong></summary>
2068
1330
 
2069
1331
  ```typescript
2070
1332
  import type {
2071
- // Persistence
2072
- VfsOptions,
2073
- VfsPersistenceMode,
2074
- // Filesystem
2075
- VfsSnapshot,
2076
- VfsNodeStats,
2077
- VfsFileNode,
2078
- VfsDirectoryNode,
2079
- WriteFileOptions,
2080
- RemoveOptions,
2081
- // Commands
2082
- CommandContext,
2083
- CommandResult,
2084
- CommandMode,
2085
- CommandOutcome,
2086
- ShellModule,
2087
- ShellEnv,
2088
- SudoChallenge,
2089
- NanoEditorSession,
2090
- // Audit
2091
- AuditLogEntry,
2092
- HoneyPotStats,
2093
- // Streams
2094
- ShellStream,
2095
- ExecStream,
1333
+ VfsOptions, VfsPersistenceMode,
1334
+ VfsSnapshot, VfsNodeStats, VfsFileNode, VfsDirectoryNode,
1335
+ WriteFileOptions, RemoveOptions,
1336
+ CommandContext, CommandResult, CommandMode, CommandOutcome,
1337
+ ShellModule, ShellEnv, SudoChallenge, NanoEditorSession,
1338
+ AuditLogEntry, HoneyPotStats,
1339
+ ShellStream, ExecStream,
2096
1340
  } from "typescript-virtual-container";
2097
1341
  ```
2098
1342
 
2099
- ---
2100
-
2101
- ## FAQ
2102
-
2103
- **Is this a real container runtime?**
2104
- No. It emulates SSH sessions, users, and filesystem behavior in a virtual runtime. Ideal for testing, simulations, and automation where full OS isolation is not required.
2105
-
2106
- **Can I use this in production?**
2107
- You can use it in production-like automation contexts (sandboxed command runners, test harnesses, training environments, honeypots). It is not a security boundary like a real container/VM.
2108
-
2109
- **Does the VFS touch the host filesystem?**
2110
- In the default `"memory"` mode: no, all data lives in memory. In `"fs"` mode, it reads/writes a single binary file (`vfs-snapshot.vfsb`) inside the configured `snapshotPath` directory. No other host paths are accessed. See [VFSB Binary Format](#vfsb-binary-format).
1343
+ </details>
2111
1344
 
2112
- **Does data persist between restarts?**
2113
- Only if you explicitly use `"fs"` mode or call `toSnapshot()` / `fromSnapshot()` manually. Memory mode is ephemeral. In `"fs"` mode, the snapshot is stored as a binary `.vfsb` file — see [VFSB Binary Format](#vfsb-binary-format).
1345
+ ---
2114
1346
 
2115
- **Can I run multiple isolated shells?**
2116
- Yes. Each `new VirtualShell(...)` creates a completely independent VFS, user manager, and shell environment.
1347
+ ## Troubleshooting
2117
1348
 
2118
- **Are custom commands shared between shell instances?**
2119
- No. Custom commands registered with `shell.addCommand()` are instance-local.
1349
+ **`Error: listen EADDRINUSE :::2222`** port already in use. Change port or stop existing process.
2120
1350
 
2121
- **Does the shell support `&&`, `||`, and `;`?**
2122
- Yes. The shell parser handles logical operators, pipes, and redirections. `if`/`elif`/`else`/`fi`, `for`/`do`/`done`, and `while`/`do`/`done` are supported in scripts.
1351
+ **Auth always fails** root has no password by default. Verify with `users.verifyPassword(username, password)`. Check lockout: `ssh.clearLockout(ip)`.
2123
1352
 
2124
- **Does `.bashrc` work?**
2125
- Yes. When an interactive SSH session starts, `/home/<user>/.bashrc` is loaded automatically and each non-comment line is executed in the session environment.
1353
+ **`Command 'xyz' not found` (exit 127)** — not a registered built-in and not found in VFS `$PATH`. Register with `shell.addCommand()` or `apt install` the package.
2126
1354
 
2127
- **Is networking fully implemented for curl/wget?**
2128
- `curl` and `wget` delegate to the host binaries. They are intended for realistic workflows, not full GNU tooling parity.
1355
+ **Shell scripting `if` block not working** — ensure each keyword is on its own line or separated by `;`.
2129
1356
 
2130
- **Can I create custom commands?**
2131
- Yes — use `shell.addCommand()` or implement the `ShellModule` interface directly. Set `description` and `category` to appear in the grouped `help` output.
1357
+ **`snapshotPath` required error** set `mode: "fs"` without `snapshotPath`: `new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" })`.
2132
1358
 
2133
- **Is SFTP fully supported?**
2134
- Core SFTP operations (open, read, write, stat, mkdir, remove, rename) are implemented. Some optional operations (extended attributes, symlinks) return `OP_UNSUPPORTED`.
1359
+ **Variables not persisting between `exec()` calls** — `SshClient` shares one `ShellEnv` per shell instance. `export` in one call is visible in the next. For isolation, create a new `SshClient`.
2135
1360
 
2136
- **Can I use this for honeypot deployments?**
2137
- Yes — that is one of its primary use-cases. Use `HoneyPot.attach()` to capture all activity, configure `maxAuthAttempts` to throttle scanners, and export audit logs on shutdown.
1361
+ **`Too many levels of symbolic links`** — symlink chain exceeds 8 hops. Check for circular links or increase `maxDepth` in `resolveSymlink()`.
2138
1362
 
2139
1363
  ---
2140
1364
 
2141
- ## Troubleshooting
2142
-
2143
- **`Error: listen EADDRINUSE :::2222`**
2144
- The port is already in use. Use a different port or stop the existing process.
2145
-
2146
- **SSH authentication always fails**
2147
- Check the password (root has no password by default). If you set a password, verify it with `users.verifyPassword(username, password)`. Check if the IP is rate-limited: call `ssh.clearLockout(ip)`.
1365
+ ## FAQ
2148
1366
 
2149
- **Auth always fails with "lockout"**
2150
- Call `ssh.clearLockout(ip)` or increase `maxAuthAttempts`. In tests, use `maxAuthAttempts: Infinity`.
1367
+ **Is this a real container runtime?**
1368
+ No it emulates SSH sessions, users, and filesystem behavior in a virtual runtime. Ideal for testing, simulations, and automation where full OS isolation is not required.
2151
1369
 
2152
- **`Error: Too many levels of symbolic links`**
2153
- A symlink chain exceeds 8 hops. Check for circular links or pass a larger `maxDepth` to `resolveSymlink()`.
1370
+ **Can I use this in production?**
1371
+ Yes for automation contexts (sandboxed command runners, test harnesses, training environments, honeypots). It is not a security boundary like a real container/VM.
2154
1372
 
2155
- **`Command 'xyz' not found` (exit code 127)**
2156
- The command is not registered. Register it with `shell.addCommand()`.
1373
+ **Does the VFS touch the host filesystem?**
1374
+ In `"memory"` mode: no. In `"fs"` mode: one binary file (`vfs-snapshot.vfsb`) inside `snapshotPath` only.
2157
1375
 
2158
- **Shell scripting `if` block not working**
2159
- Ensure each keyword is on its own line or separated by `;`. The interpreter does not support complex one-liners like `if condition; then cmd; fi` as a single string passed to `sh -c` — split by line or use semicolons.
1376
+ **Can I run multiple isolated shells?**
1377
+ Yes. Each `new VirtualShell(...)` is completely independent (separate VFS, users, env state).
2160
1378
 
2161
- **File not found errors**
2162
- Create the parent directory first with `vfs.mkdir(path, 0o755)`.
1379
+ **Does the shell support `&&`, `||`, and `;`?**
1380
+ Yes plus pipes, redirections, `if`/`for`/`while`/`case`, and function definitions.
2163
1381
 
2164
- **`snapshotPath` is required error**
2165
- You set `mode: "fs"` without providing `snapshotPath`: `new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" })`.
1382
+ **Can I use this for honeypot deployments?**
1383
+ Yes use `HoneyPot.attach()` to capture all activity, configure `maxAuthAttempts` to throttle scanners, export on shutdown.
2166
1384
 
2167
- **Variables not persisting between `exec()` calls**
2168
- Each `SshClient.exec()` call shares the same `ShellEnv` object per shell instance. Variables set via `export` in one exec call are visible in the next. If you need full isolation, create a new `SshClient` instance.
1385
+ **Is SFTP fully supported?**
1386
+ Core operations are implemented. Extended attributes and symlinks return `OP_UNSUPPORTED`.
2169
1387
 
2170
1388
  ---
2171
1389
 
2172
1390
  ## Contributing
2173
1391
 
2174
- 1. Fork the repository.
2175
- 2. Create a feature branch: `git checkout -b feat/my-feature`
2176
- 3. Make changes and add tests.
2177
- 4. Format and lint: `bun format && bun check`
2178
- 5. Push and open a PR.
1392
+ 1. Fork and create a feature branch: `git checkout -b feat/my-feature`
1393
+ 2. Add changes and tests.
1394
+ 3. Format and lint: `bun format && bun check`
1395
+ 4. Push and open a PR.
2179
1396
 
2180
- **Code quality standards:**
2181
- - Biome formatting (opinionated, enforced by CI)
2182
- - Full TypeScript — no `any`
2183
- - JSDoc comments on all public API surface
2184
- - Async/await throughout — no callbacks
2185
- - Tests for new commands and VFS behavior
2186
- - New commands must include `description` and `category` fields for `help`
1397
+ **Standards:** Biome formatting · full TypeScript (no `any`) · ✅ **JSDoc on all built-in commands** · async/await · `description` and `category` fields on new `ShellModule` · unit tests (prioritized for core features; advanced tests planned).
2187
1398
 
2188
1399
  ---
2189
1400
 
2190
1401
  ## Security
2191
1402
 
2192
- - Passwords are hashed with `scrypt` by default (N=32768, r=8, p=1), with a random per-user salt.
1403
+ - Passwords hashed with scrypt (N=32768, r=8, p=1) with random per-user salt.
2193
1404
  - Root account always exists and cannot be deleted.
2194
- - Sudo privileges are explicit and stored in the VFS under `/virtual-env-js/.auth/sudoers`.
2195
- - Per-IP rate limiting prevents automated brute-force attacks on the SSH server.
1405
+ - Per-IP rate limiting prevents automated brute-force on the SSH server.
2196
1406
  - This project does **not** provide kernel-level or process-level isolation.
2197
- - Do **not** expose a running instance to the public internet without understanding the risks.
1407
+ - Do **not** expose to the public internet without understanding the risks.
2198
1408
 
2199
- If you discover a vulnerability, avoid public disclosure in GitHub Issues. Contact maintainers privately first — see `SECURITY.md`.
1409
+ Vulnerability reports: contact maintainers privately before public disclosure — see `SECURITY.md`.
2200
1410
 
2201
1411
  ---
2202
1412
 
2203
- ## Support
1413
+ ## Compatibility
2204
1414
 
2205
- - Open an issue for bugs, regressions, or feature requests.
2206
- - Include your Node.js/Bun version, package version, and a minimal reproduction.
2207
- - For API questions, include the exact call sequence plus expected vs. actual behavior.
1415
+ - **Node.js**: `>=18` · **Bun**: Supported · **TypeScript**: `>=5.0`
1416
+ - **OS**: Linux, macOS, Windows (via Node/Bun)
2208
1417
 
2209
1418
  ---
2210
1419
 
@@ -2216,63 +1425,24 @@ MIT — see [LICENSE](./LICENSE).
2216
1425
 
2217
1426
  ## Roadmap
2218
1427
 
2219
- - [x] Custom command plugin API
2220
- - [x] Optional per-user storage quotas
2221
- - [x] Improved shell compatibility (pipelines, redirections)
2222
- - [x] Pure in-memory VFS with snapshot import/export
2223
- - [x] Symlinks (`ln -s`, `isSymlink`, `resolveSymlink`)
2224
- - [x] SSH public-key authentication
2225
- - [x] Per-IP rate limiting and lockout
2226
- - [x] Shell operators: `&&` / `||` / `;`
2227
- - [x] Shell scripting: `if`/`elif`/`else`/`fi`, `for`/`do`/`done`, `while`/`do`/`done`
2228
- - [x] Variable expansion: `$VAR`, `${VAR:-default}`, `$?`
2229
- - [x] Per-session `ShellEnv` (no more global variable store)
2230
- - [x] `.bashrc` auto-sourced on interactive login
2231
- - [x] `clear` with full ANSI screen reset (`\x1b[2J\x1b[H\x1b[3J`)
2232
- - [x] `Ctrl+W` delete word in interactive shell
2233
- - [x] Grouped, colorized `help` with per-command detail
2234
- - [x] New commands: `sort`, `uniq`, `tee`, `cut`, `tr`, `xargs`, `diff`, `sed`, `awk`, `tar`, `gzip`, `gunzip`, `base64`, `date`, `sleep`, `id`, `groups`, `uname`, `ps`, `kill`, `df`, `du`, `ping`
2235
- - [x] Structured event hooks (session open/close, file write, sudo challenge)
2236
- - [x] Binary snapshot format (VFSB) — replaces JSON+base64, ~27% smaller, no string parsing overhead, backward-compatible JSON migration
2237
- - [x] Linux rootfs on boot — `/etc`, `/proc`, `/sys`, `/dev`, `/usr`, `/var` populated at init; `os-release`, `passwd`, `hosts`, `/proc/meminfo`, `/proc/cpuinfo`, `/proc/version`, `/proc/uptime`, `/sys/devices/virtual/dmi/`, symlinks `/bin`→`/usr/bin`
2238
- - [x] Virtual package manager — `apt install/remove/purge/update/upgrade/search/show/list`, `apt-get`, `apt-cache`, `dpkg`, `dpkg-query`; 25 packages in registry; writes files to VFS; `/var/lib/dpkg/status` persistence
2239
- - [x] `curl` / `wget` reimplemented as pure `fetch()` — no host binary spawned, full isolation
2240
- - [x] New commands: `which`, `type`, `man`, `uptime`, `free`, `lsb_release`, `alias`, `unalias`
2241
- - [x] `$(cmd)` command substitution in variable expansion
2242
- - [x] Alias expansion in command dispatch
2243
- - [x] `neofetch` shows real package count and shell uptime
2244
- - [x] `syncPasswd()` / `refreshProcFs()` public API on `VirtualShell`
2245
- - [x] `test` / `[` — full POSIX conditional expressions (`-f`, `-d`, `-e`, `-z`, `-n`, `-x`, `-s`, `-L`, `=`, `!=`, `-eq`, `-lt`, `-gt`, `-le`, `-ge`, `!`, `-a`, `-o`)
2246
- - [x] `source` / `.` — execute file in current shell env (aliases and exports persist across commands)
2247
- - [x] `history [n]` — display command history from VFS `.bash_history`
2248
- - [x] `echo -e` / `echo -n` — escape sequence interpretation and newline suppression
2249
- - [x] `ls -a` — show dotfiles
2250
- - [x] `chmod` symbolic modes — `+x`, `u+x`, `go-w`, `a=rx`, comma-separated
2251
- - [x] `cat -n` / `-b` — line numbering, multi-file concatenation, stdin support
2252
- - [x] `ping -c N` — respects packet count flag
2253
- - [x] `ps -u` / `ps aux` — extended format with USER/PID/%CPU/%MEM/VSZ/RSS columns
2254
- - [x] `$(cmd)` in single-quoted args preserved — `sh -c 'echo $(whoami)'` now works correctly
2255
- - [x] Pipeline executor: `runCommandDirect` — args with `;`, `|`, `>` no longer re-parsed
2256
- - [x] `>>` append redirect fixed — was broken because `echo` lacked terminal newline
2257
- - [x] `export A=1 && echo $A` — env vars visible to subsequent commands in same pipeline
2258
- - [x] `echo` uses session `env.vars` (not stale global store)
2259
- - [x] `printf` — format string with `%s` `%d` `%f` `%x` `\n` `\t`
2260
- - [x] `read` — read stdin into variables (supports multiple vars, splits on whitespace)
2261
- - [x] `declare` / `local` / `typeset` — variable declaration with `-i` integer, `-r` readonly, `-x` export
2262
- - [x] `shift [n]` — shift positional parameters
2263
- - [x] `trap [action] [signal]` — signal handlers with `EXIT` support
2264
- - [x] `return [n]` — return from shell functions
2265
- - [x] `exit [code]` — optional exit code
2266
- - [x] `help` rewrite — Package Management category, aliases shown inline, `help <cmd>` shows category
2267
- - [x] Category corrections — `neofetch`→system, `nano`→files, `apt`/`dpkg`→package
2268
- - [x] `src/utils/expand.ts` — centralised expansion module used by `runCommand`, `echo`, and `sh.ts`
2269
- - [x] `${#VAR}` string length, `$((expr))` arithmetic, `~` tilde expansion
2270
- - [x] `${VAR:=assign}` assign-on-read, `${VAR:+alternate}` alternate-if-set
2271
- - [x] Single-quote isolation in expansion — `$VAR` never expanded inside `'...'`
2272
- - [x] `true` / `false` builtins
2273
- - [x] Bare `VAR=val` assignments in `sh` scripts (`i=0`, `i=$((i+1))`)
2274
- - [x] `$?` reflects last command exit code correctly across `&&` / `;` chains
2275
- - [x] `node` / `python3` virtual REPL stubs — `node -e`, `node <file>`, `python3 -c`, `python3 <file>`, `--version` flags
2276
- - [x] `/proc/self` and `/proc/<pid>` per-session process entries — `comm`, `status`, `cmdline`, `environ`, `cwd`, `exe`, `fd/`
2277
- - [x] Snapshot diff tooling — `diffSnapshots()`, `formatDiff()`, `assertDiff()` exported from `typescript-virtual-container`
1428
+ Open:
1429
+
2278
1430
  - [ ] WebSocket-based remote shell client (experimental)
1431
+
1432
+ <details>
1433
+ <summary>Completed</summary>
1434
+
1435
+ - [x] Custom command plugin API · per-user quotas · SSH public-key auth · per-IP rate limiting
1436
+ - [x] Pure in-memory VFS · symlinks · binary snapshot format (VFSB, ~27% smaller than JSON+base64)
1437
+ - [x] Linux rootfs on boot — `/etc`, `/proc`, `/sys`, `/dev`, `/usr`, `/var`
1438
+ - [x] Virtual package manager — `apt`/`dpkg`, 25 packages, VFS file writes
1439
+ - [x] 91 built-in commands across 9 categories
1440
+ - [x] Real shell interpreter — `if`/`for`/`while`/`case`/functions, `$(cmd)`, `$((expr))`, `${#VAR}`, single-quote isolation
1441
+ - [x] `curl`/`wget` as pure `fetch()` · VFS PATH resolution · `/sbin` root-only
1442
+ - [x] `/proc/self` and `/proc/<pid>` per-session entries
1443
+ - [x] Snapshot diff tooling — `diffSnapshots`, `formatDiff`, `assertDiff`
1444
+ - [x] `node`/`python3`/`npm`/`npx` — package-gated virtual REPL stubs
1445
+ - [x] Web shell bundle (`web.min.js`) — fully browser-native with IndexedDB VFS
1446
+ - [x] Self-standalone CLI (`self-standalone.js`) — single-file interactive shell, no install
1447
+
1448
+ </details>