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
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Remove shell variable from the environment.
5
+ * @category shell
6
+ * @params ["<VAR>"]
7
+ */
3
8
  export const unsetCommand: ShellModule = {
4
9
  name: "unset",
5
10
  description: "Remove shell variable",
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
 
4
+ /**
5
+ * Tell how long the system has been running.
6
+ * @category system
7
+ * @params ["[-p] [-s]"]
8
+ */
4
9
  export const uptimeCommand: ShellModule = {
5
10
  name: "uptime",
6
11
  description: "Tell how long the system has been running",
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
+ /**
6
+ * Count words, lines, and/or bytes in files or stdin.
7
+ * @category text
8
+ * @params ["[-l] [-w] [-c] [file...]"]
9
+ */
5
10
  export const wcCommand: ShellModule = {
6
11
  name: "wc",
7
12
  description: "Count words/lines/bytes",
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag, parseArgs } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath, stripUrlFilename } from "./helpers";
4
4
 
5
+ /**
6
+ * Download files from the web (fetch-based implementation).
7
+ * @category network
8
+ * @params ["[options] <url>"]
9
+ */
5
10
  export const wgetCommand: ShellModule = {
6
11
  name: "wget",
7
12
  description: "File downloader (pure fetch)",
@@ -44,7 +49,18 @@ export const wgetCommand: ShellModule = {
44
49
  };
45
50
  }
46
51
 
47
- const url = positionals[0];
52
+ const urlWithoutProtocol = positionals[0];
53
+ if (!urlWithoutProtocol) {
54
+ return {
55
+ stderr: "wget: missing URL\nUsage: wget [OPTION]... [URL]...",
56
+ exitCode: 1,
57
+ };
58
+ }
59
+
60
+ const url = urlWithoutProtocol.startsWith("http://") ||
61
+ urlWithoutProtocol.startsWith("https://")
62
+ ? urlWithoutProtocol
63
+ : `http://${urlWithoutProtocol}`;
48
64
  if (!url)
49
65
  return {
50
66
  stderr: "wget: missing URL\nUsage: wget [OPTION]... [URL]...",
@@ -1,6 +1,11 @@
1
1
  import { formatLoginDate } from "../SSHMimic/loginFormat";
2
2
  import type { ShellModule } from "../types/commands";
3
3
 
4
+ /**
5
+ * Show active user sessions.
6
+ * @category system
7
+ * @params []
8
+ */
4
9
  export const whoCommand: ShellModule = {
5
10
  name: "who",
6
11
  description: "Show active sessions",
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Print the current user name.
5
+ * @category system
6
+ * @params []
7
+ */
3
8
  export const whoamiCommand: ShellModule = {
4
9
  name: "whoami",
5
10
  description: "Print current user",
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { runCommand } from "./runtime";
3
3
 
4
+ /**
5
+ * Build and execute commands from stdin arguments.
6
+ * @category text
7
+ * @params ["[command] [args...]"]
8
+ */
4
9
  export const xargsCommand: ShellModule = {
5
10
  name: "xargs",
6
11
  description: "Build and execute command lines from stdin",
@@ -1,10 +1,13 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
1
2
  import { basename } from "node:path";
2
3
  import { stdin, stdout } from "node:process";
3
4
  import { createInterface, type Interface } from "node:readline";
4
5
 
5
6
  import { makeDefaultEnv, runCommand } from "./commands/runtime";
7
+ import { spawnNanoEditorProcess } from "./modules/shellInteractive";
6
8
  import { buildLoginBanner, type LoginBannerState } from "./SSHMimic/loginBanner";
7
9
  import { buildPrompt } from "./SSHMimic/prompt";
10
+ import type { CommandResult, PasswordChallenge, SudoChallenge } from "./types/commands";
8
11
  import { VirtualShell } from "./VirtualShell";
9
12
 
10
13
  const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
@@ -47,9 +50,55 @@ function readLastLogin(username: string): LoginBannerState | null {
47
50
  }
48
51
  }
49
52
 
50
- function askQuestion(rl: Interface, promptText: string): Promise<string> {
53
+ function askHiddenQuestion(rl: Interface, promptText: string): Promise<string> {
51
54
  return new Promise((resolve) => {
52
- rl.question(promptText, resolve);
55
+ if (!stdin.isTTY || !stdout.isTTY) {
56
+ rl.question(promptText, resolve);
57
+ return;
58
+ }
59
+
60
+ const wasRawMode = Boolean(stdin.isRaw);
61
+ let buffer = "";
62
+
63
+ const cleanup = (): void => {
64
+ stdin.off("data", onData);
65
+ if (!wasRawMode) {
66
+ stdin.setRawMode(false);
67
+ }
68
+ rl.resume();
69
+ };
70
+
71
+ const finish = (value: string): void => {
72
+ cleanup();
73
+ stdout.write("\n");
74
+ resolve(value);
75
+ };
76
+
77
+ const onData = (chunk: Buffer): void => {
78
+ const input = chunk.toString("utf8");
79
+ for (let index = 0; index < input.length; index += 1) {
80
+ const ch = input[index]!;
81
+ if (ch === "\r" || ch === "\n") {
82
+ finish(buffer);
83
+ return;
84
+ }
85
+ if (ch === "\u007f" || ch === "\b") {
86
+ buffer = buffer.slice(0, -1);
87
+ continue;
88
+ }
89
+ if (ch >= " ") {
90
+ buffer += ch;
91
+ }
92
+ }
93
+ };
94
+
95
+ rl.pause();
96
+ stdout.write(promptText);
97
+ if (!wasRawMode) {
98
+ stdin.setRawMode(true);
99
+ }
100
+ stdin.resume();
101
+ stdin.on("data", onData);
53
102
  });
54
103
  }
55
104
 
@@ -65,6 +114,53 @@ function writeLastLogin(username: string, from: string): void {
65
114
  );
66
115
  }
67
116
 
117
+ async function flushVfs(): Promise<void> {
118
+ await virtualShell.vfs.flushMirror();
119
+ }
120
+
121
+ function loadHistory(): string[] {
122
+ const historyPath = "/virtual-env-js/.bash_history";
123
+ if (!virtualShell.vfs.exists(historyPath)) {
124
+ virtualShell.vfs.writeFile(historyPath, "");
125
+ return [];
126
+ }
127
+
128
+ return virtualShell.vfs
129
+ .readFile(historyPath)
130
+ .split("\n")
131
+ .map((line) => line.trim())
132
+ .filter((line) => line.length > 0);
133
+ }
134
+
135
+ function saveHistory(history: string[]): void {
136
+ const data = history.length > 0 ? `${history.join("\n")}\n` : "";
137
+ virtualShell.vfs.writeFile("/virtual-env-js/.bash_history", data);
138
+ }
139
+
140
+ function applySessionState(
141
+ authUserState: string,
142
+ cwdState: string,
143
+ result: CommandResult,
144
+ shellEnvState: ReturnType<typeof makeDefaultEnv>,
145
+ ): { authUser: string; cwd: string } {
146
+ let authUser = authUserState;
147
+ let cwd = cwdState;
148
+
149
+ if (result.switchUser) {
150
+ authUser = result.switchUser;
151
+ cwd = result.nextCwd ?? `/home/${authUser}`;
152
+ shellEnvState.vars.USER = authUser;
153
+ shellEnvState.vars.LOGNAME = authUser;
154
+ shellEnvState.vars.HOME = `/home/${authUser}`;
155
+ shellEnvState.vars.PWD = cwd;
156
+ } else if (result.nextCwd) {
157
+ cwd = result.nextCwd;
158
+ shellEnvState.vars.PWD = cwd;
159
+ }
160
+
161
+ return { authUser, cwd };
162
+ }
163
+
68
164
  virtualShell.addCommand("demo", [], () => {
69
165
  return {
70
166
  stdout: "This is a demo command. It does nothing useful.",
@@ -75,6 +171,9 @@ virtualShell.addCommand("demo", [], () => {
75
171
  async function runReadlineShell() {
76
172
  const rl = createInterface({ input: stdin, output: stdout, terminal: true });
77
173
  await virtualShell.ensureInitialized();
174
+ let history = loadHistory();
175
+ const rlWithHistory = rl as Interface & { history: string[] };
176
+ rlWithHistory.history = [...history].reverse();
78
177
 
79
178
  const selectedUser = initialUser.trim() || "root";
80
179
  const userExists = virtualShell.users.getPasswordHash(selectedUser) !== null;
@@ -88,9 +187,207 @@ async function runReadlineShell() {
88
187
  let cwd = `/home/${authUser}`;
89
188
  shellEnv.vars.PWD = cwd;
90
189
  const remoteAddress = "localhost";
190
+ const terminalSize = {
191
+ cols: stdout.columns ?? 80,
192
+ rows: stdout.rows ?? 24,
193
+ };
194
+
195
+ async function startNanoEditor(
196
+ targetPath: string,
197
+ initialContent: string,
198
+ tempPath: string,
199
+ ): Promise<void> {
200
+ if (virtualShell.vfs.exists(targetPath)) {
201
+ await writeFile(tempPath, initialContent, "utf8");
202
+ }
203
+
204
+ rl.pause();
205
+ const editor = spawnNanoEditorProcess(
206
+ tempPath,
207
+ terminalSize,
208
+ {
209
+ write: stdout.write.bind(stdout),
210
+ exit: () => undefined,
211
+ end: () => undefined,
212
+ } as unknown as Parameters<typeof spawnNanoEditorProcess>[2],
213
+ );
214
+
215
+ const wasRawMode = Boolean(stdin.isRaw);
216
+ const forwardInput = (chunk: Buffer): void => {
217
+ editor.stdin.write(chunk);
218
+ };
219
+
220
+ stdin.resume();
221
+ if (!wasRawMode) {
222
+ stdin.setRawMode(true);
223
+ }
224
+ stdin.on("data", forwardInput);
225
+
226
+ await new Promise<void>((resolve) => {
227
+ const cleanup = (): void => {
228
+ stdin.off("data", forwardInput);
229
+ if (!wasRawMode) {
230
+ stdin.setRawMode(false);
231
+ }
232
+ rl.resume();
233
+ };
234
+
235
+ editor.on("error", (error: Error) => {
236
+ cleanup();
237
+ stdout.write(`nano: ${error.message}\r\n`);
238
+ resolve();
239
+ });
240
+
241
+ editor.on("close", async () => {
242
+ cleanup();
243
+ rl.write("", { ctrl: true, name: "u" });
244
+ try {
245
+ const updatedContent = await readFile(tempPath, "utf8");
246
+ virtualShell.writeFileAsUser(authUser, targetPath, updatedContent);
247
+ await flushVfs();
248
+ } catch {
249
+ // Save skipped or temp file missing.
250
+ }
251
+
252
+ await unlink(tempPath).catch(() => undefined);
253
+ stdout.write("\r\n");
254
+ resolve();
255
+ });
256
+ });
257
+ }
258
+
259
+ async function handleSudoChallenge(challenge: SudoChallenge): Promise<void> {
260
+ if (challenge.onPassword) {
261
+ let promptText = challenge.prompt;
262
+ while (true) {
263
+ const typed = await askHiddenQuestion(rl, promptText);
264
+ const step = await challenge.onPassword(typed, virtualShell);
265
+ if (step.result === null) {
266
+ promptText = step.nextPrompt ?? promptText;
267
+ continue;
268
+ }
269
+
270
+ await handleCommandResult(step.result);
271
+ return;
272
+ }
273
+ }
274
+
275
+ const password = await askHiddenQuestion(rl, challenge.prompt);
276
+ if (!virtualShell.users.verifyPassword(challenge.username, password)) {
277
+ process.stderr.write("Sorry, try again.\n");
278
+ return;
279
+ }
280
+
281
+ if (!challenge.commandLine) {
282
+ authUser = challenge.targetUser;
283
+ cwd = `/home/${authUser}`;
284
+ shellEnv.vars.USER = authUser;
285
+ shellEnv.vars.LOGNAME = authUser;
286
+ shellEnv.vars.HOME = `/home/${authUser}`;
287
+ shellEnv.vars.PWD = cwd;
288
+ return;
289
+ }
290
+
291
+ const runCwd = challenge.loginShell ? `/home/${challenge.targetUser}` : cwd;
292
+ const nestedResult = await runCommand(
293
+ challenge.commandLine,
294
+ challenge.targetUser,
295
+ hostname,
296
+ "shell",
297
+ runCwd,
298
+ virtualShell,
299
+ undefined,
300
+ shellEnv,
301
+ );
302
+ await handleCommandResult(nestedResult);
303
+ }
304
+
305
+ async function handlePasswordChallenge(
306
+ challenge: PasswordChallenge,
307
+ ): Promise<void> {
308
+ const first = await askHiddenQuestion(rl, challenge.prompt);
309
+ if (challenge.confirmPrompt) {
310
+ const second = await askHiddenQuestion(rl, challenge.confirmPrompt);
311
+ if (second !== first) {
312
+ process.stderr.write("passwords do not match\n");
313
+ return;
314
+ }
315
+ }
316
+
317
+ switch (challenge.action) {
318
+ case "passwd":
319
+ await virtualShell.users.setPassword(challenge.targetUsername, first);
320
+ stdout.write("passwd: password updated successfully\n");
321
+ break;
322
+ case "adduser":
323
+ if (!challenge.newUsername) {
324
+ process.stderr.write("adduser: missing username\n");
325
+ return;
326
+ }
327
+ await virtualShell.users.addUser(challenge.newUsername, first);
328
+ stdout.write(`adduser: user '${challenge.newUsername}' created\n`);
329
+ break;
330
+ case "deluser":
331
+ await virtualShell.users.deleteUser(challenge.targetUsername);
332
+ stdout.write(`Removing user '${challenge.targetUsername}' ...\ndeluser: done.\n`);
333
+ break;
334
+ case "su":
335
+ authUser = challenge.targetUsername;
336
+ cwd = `/home/${authUser}`;
337
+ shellEnv.vars.USER = authUser;
338
+ shellEnv.vars.LOGNAME = authUser;
339
+ shellEnv.vars.HOME = `/home/${authUser}`;
340
+ shellEnv.vars.PWD = cwd;
341
+ break;
342
+ }
343
+ }
344
+
345
+ async function handleCommandResult(result: CommandResult): Promise<void> {
346
+ if (result.openEditor) {
347
+ await startNanoEditor(
348
+ result.openEditor.targetPath,
349
+ result.openEditor.initialContent,
350
+ result.openEditor.tempPath,
351
+ );
352
+ return;
353
+ }
354
+
355
+ if (result.sudoChallenge) {
356
+ await handleSudoChallenge(result.sudoChallenge);
357
+ return;
358
+ }
359
+
360
+ if (result.passwordChallenge) {
361
+ await handlePasswordChallenge(result.passwordChallenge);
362
+ return;
363
+ }
364
+
365
+ if (result.stdout) {
366
+ stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
367
+ }
368
+
369
+ if (result.stderr) {
370
+ process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
371
+ }
372
+
373
+ if (result.clearScreen) {
374
+ stdout.write("\u001b[2J\u001b[H");
375
+ console.clear();
376
+ }
377
+
378
+ const updatedState = applySessionState(authUser, cwd, result, shellEnv);
379
+ authUser = updatedState.authUser;
380
+ cwd = updatedState.cwd;
381
+
382
+ if (result.closeSession) {
383
+ await flushVfs();
384
+ rl.close();
385
+ process.exit(result.exitCode ?? 0);
386
+ }
387
+ }
91
388
 
92
389
  if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
93
- const password = await askQuestion(rl, `Password for ${authUser}: `);
390
+ const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
94
391
  if (!virtualShell.users.verifyPassword(authUser, password)) {
95
392
  process.stderr.write("self-standalone: authentication failed\n");
96
393
  process.exit(1);
@@ -114,12 +411,16 @@ async function runReadlineShell() {
114
411
  });
115
412
 
116
413
  rl.on("close", () => {
117
- console.log("")
118
- process.exit(0);
414
+ void (async () => {
415
+ await flushVfs();
416
+ console.log("");
417
+ process.exit(0);
418
+ })();
119
419
  });
120
420
 
121
421
  stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
122
422
  writeLastLogin(authUser, remoteAddress);
423
+ await flushVfs();
123
424
  prompt();
124
425
 
125
426
  while (true) {
@@ -128,37 +429,19 @@ async function runReadlineShell() {
128
429
  });
129
430
 
130
431
  rl.pause();
131
-
132
- const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
133
-
134
- if (result.stdout) {
135
- stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
136
- }
137
-
138
- if (result.stderr) {
139
- process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
140
- }
141
-
142
- if (result.clearScreen) {
143
- stdout.write("\u001b[2J\u001b[H");
432
+ if (inputLine.trim().length > 0) {
433
+ history.push(inputLine);
434
+ if (history.length > 500) {
435
+ history = history.slice(history.length - 500);
436
+ }
437
+ saveHistory(history);
438
+ rlWithHistory.history = [...history].reverse();
144
439
  }
145
440
 
146
- if (result.switchUser) {
147
- authUser = result.switchUser;
148
- cwd = result.nextCwd ?? `/home/${authUser}`;
149
- shellEnv.vars.USER = authUser;
150
- shellEnv.vars.LOGNAME = authUser;
151
- shellEnv.vars.HOME = `/home/${authUser}`;
152
- shellEnv.vars.PWD = cwd;
153
- } else if (result.nextCwd) {
154
- cwd = result.nextCwd;
155
- shellEnv.vars.PWD = cwd;
156
- }
441
+ const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
442
+ await handleCommandResult(result);
157
443
 
158
- if (result.closeSession) {
159
- rl.close();
160
- process.exit(result.exitCode ?? 0);
161
- }
444
+ await flushVfs();
162
445
 
163
446
  prompt();
164
447
  rl.resume();
@@ -31,6 +31,8 @@ export interface CommandResult {
31
31
  openHtop?: boolean;
32
32
  /** Request sudo password challenge flow. */
33
33
  sudoChallenge?: SudoChallenge;
34
+ /** Request a generic password challenge (adduser, passwd). */
35
+ passwordChallenge?: PasswordChallenge;
34
36
  }
35
37
 
36
38
  /** Deferred sudo challenge metadata returned by sudo command. */
@@ -45,6 +47,41 @@ export interface SudoChallenge {
45
47
  loginShell: boolean;
46
48
  /** Prompt text shown before password input. */
47
49
  prompt: string;
50
+ /**
51
+ * Challenge mode.
52
+ * - `"sudo"` (default): verify `username`'s password, then run `commandLine`.
53
+ * - `"passwd"`: multi-step new-password flow; `onPassword` handles each step.
54
+ * - `"confirm"`: text confirmation flow (e.g. deluser); `onPassword` receives typed text.
55
+ */
56
+ mode?: "sudo" | "passwd" | "confirm";
57
+ /**
58
+ * Optional async handler called when the user submits input.
59
+ * Receives the typed text and the shell instance.
60
+ * Returns a `CommandResult` written to the terminal, or `null` to show
61
+ * another prompt (pass `nextPrompt` to change the prompt text).
62
+ */
63
+ onPassword?: (input: string, shell: import("../VirtualShell").VirtualShell) => Promise<{
64
+ result: CommandResult | null;
65
+ nextPrompt?: string;
66
+ }>;
67
+ }
68
+
69
+ /** Generic password challenge — used by adduser, passwd, deluser. */
70
+ export interface PasswordChallenge {
71
+ /** Lines to print before the first prompt. */
72
+ preamble?: string;
73
+ /** Primary prompt text (e.g. "New password: "). */
74
+ prompt: string;
75
+ /** If set, a second prompt is shown for confirmation. */
76
+ confirmPrompt?: string;
77
+ /** Prompt shown for a destructive confirmation (y/N). */
78
+ confirmText?: string;
79
+ /** Tag identifying what to do with the entered value. */
80
+ action: "adduser" | "passwd" | "deluser" | "su";
81
+ /** Username targeted by the action. */
82
+ targetUsername: string;
83
+ /** For adduser: the new user's username (already validated). */
84
+ newUsername?: string;
48
85
  }
49
86
 
50
87
  /** State payload used by nano command interactive editor flow. */
@@ -0,0 +1,78 @@
1
+ /**
2
+ * tokenize.ts
3
+ *
4
+ * Shared shell tokenizer used by `shellParser.ts` and `runtime.ts`.
5
+ * Splits a shell input string into tokens respecting single and double
6
+ * quotes, and separates `>`, `>>`, `<` as standalone redirect tokens.
7
+ */
8
+
9
+ /**
10
+ * Tokenize a shell command line respecting quoted strings and redirect
11
+ * operators.
12
+ *
13
+ * - Single-quoted content is preserved verbatim.
14
+ * - Double-quoted content is preserved (expansion happens later).
15
+ * - `>`, `>>`, and `<` are emitted as standalone tokens.
16
+ *
17
+ * @param input Raw shell command string.
18
+ * @returns Array of string tokens.
19
+ */
20
+ export function tokenizeCommand(input: string): string[] {
21
+ const tokens: string[] = [];
22
+ let current = "";
23
+ let inQ = false;
24
+ let qChar = "";
25
+ let i = 0;
26
+
27
+ while (i < input.length) {
28
+ const ch = input[i]!;
29
+ const next = input[i + 1];
30
+
31
+ if ((ch === '"' || ch === "'") && !inQ) {
32
+ inQ = true;
33
+ qChar = ch;
34
+ i++;
35
+ continue;
36
+ }
37
+ if (inQ && ch === qChar) {
38
+ inQ = false;
39
+ qChar = "";
40
+ i++;
41
+ continue;
42
+ }
43
+ if (inQ) {
44
+ current += ch;
45
+ i++;
46
+ continue;
47
+ }
48
+
49
+ if (ch === " ") {
50
+ if (current) {
51
+ tokens.push(current);
52
+ current = "";
53
+ }
54
+ i++;
55
+ continue;
56
+ }
57
+
58
+ if ((ch === ">" || ch === "<") && !inQ) {
59
+ if (current) {
60
+ tokens.push(current);
61
+ current = "";
62
+ }
63
+ if (ch === ">" && next === ">") {
64
+ tokens.push(">>");
65
+ i += 2;
66
+ } else {
67
+ tokens.push(ch);
68
+ i++;
69
+ }
70
+ continue;
71
+ }
72
+
73
+ current += ch;
74
+ i++;
75
+ }
76
+ if (current) tokens.push(current);
77
+ return tokens;
78
+ }
@@ -223,7 +223,7 @@ describe("Package manager (apt/dpkg)", () => {
223
223
  });
224
224
 
225
225
  test("neofetch shows package count after installs", async () => {
226
- await client.exec("apt install curl wget htop");
226
+ await client.exec("apt install curl wget htop neofetch");
227
227
  const r = await client.exec("neofetch");
228
228
  expect(r.exitCode).toBe(0);
229
229
  expect(r.stdout).toContain("(dpkg)");
@@ -675,7 +675,7 @@ describe("/proc/self and /proc/<pid>", () => {
675
675
  });
676
676
  });
677
677
 
678
- import { diffSnapshots, formatDiff, assertDiff } from "../src";
678
+ import { assertDiff, diffSnapshots, formatDiff } from "../src";
679
679
 
680
680
  describe("VFS snapshot diff tooling", () => {
681
681
  let shell5: VirtualShell;