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
@@ -5,6 +5,7 @@ import type {
5
5
  Statement,
6
6
  LogicalOp,
7
7
  } from "../types/pipeline";
8
+ import { tokenizeCommand } from "../utils/tokenize";
8
9
 
9
10
  // ── Public API ───────────────────────────────────────────────────────────────
10
11
 
@@ -25,7 +26,7 @@ export function parseScript(rawInput: string): Script {
25
26
  }
26
27
  }
27
28
 
28
- /** Legacy compat: parse a single pipeline (no &&/||/;) */
29
+ /** Parse a single pipeline string (no &&/||/;) into a `Pipeline` object. */
29
30
  export function parseShellPipeline(rawInput: string): Pipeline {
30
31
  const trimmed = rawInput.trim();
31
32
  if (!trimmed) return { commands: [], isValid: true };
@@ -39,49 +40,6 @@ export function parseShellPipeline(rawInput: string): Pipeline {
39
40
 
40
41
  // ── Variable & tilde expansion ────────────────────────────────────────────────
41
42
 
42
- /**
43
- * Expand ~ and $VAR / ${VAR} / ${VAR:-default} / $(cmd placeholder) in a
44
- * token, given the current env vars and home path.
45
- * Command substitution $(…) is NOT executed here — it's left as a marker so
46
- * the executor can handle it.
47
- */
48
- export function expandToken(
49
- token: string,
50
- env: Record<string, string>,
51
- authUser: string,
52
- lastExitCode = 0,
53
- ): string {
54
- // tilde expansion
55
- token = token.replace(/^~(\/|$)/, `/home/${authUser}$1`);
56
-
57
- // $? special var
58
- token = token.replace(/\$\?/g, String(lastExitCode));
59
- // $$ PID (mock)
60
- token = token.replace(/\$\$/g, "1");
61
- // $# argc (0 for interactive)
62
- token = token.replace(/\$#/g, "0");
63
-
64
- // ${VAR:-default} and ${VAR:+value}
65
- token = token.replace(
66
- /\$\{([^}:]+):-([^}]*)\}/g,
67
- (_, name, def) => env[name] ?? def,
68
- );
69
- token = token.replace(/\$\{([^}:]+):\+([^}]*)\}/g, (_, name, val) =>
70
- env[name] ? val : "",
71
- );
72
-
73
- // ${VAR}
74
- token = token.replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? "");
75
-
76
- // $VAR (greedy: match longest valid identifier)
77
- token = token.replace(
78
- /\$([A-Za-z_][A-Za-z0-9_]*)/g,
79
- (_, name) => env[name] ?? "",
80
- );
81
-
82
- return token;
83
- }
84
-
85
43
  /**
86
44
  * Expand glob patterns (*, ?, [abc]) against a list of entries.
87
45
  * Returns the original pattern if no match.
@@ -299,62 +257,3 @@ function parseCommandWithRedirections(token: string): PipelineCommand {
299
257
  return { name, args: cmdParts.slice(1), inputFile, outputFile, appendOutput };
300
258
  }
301
259
 
302
- function tokenizeCommand(input: string): string[] {
303
- const tokens: string[] = [];
304
- let current = "";
305
- let inQ = false;
306
- let qChar = "";
307
- let i = 0;
308
-
309
- while (i < input.length) {
310
- const ch = input[i]!;
311
- const next = input[i + 1];
312
-
313
- if ((ch === '"' || ch === "'") && !inQ) {
314
- inQ = true;
315
- qChar = ch;
316
- i++;
317
- continue;
318
- }
319
- if (inQ && ch === qChar) {
320
- inQ = false;
321
- qChar = "";
322
- i++;
323
- continue;
324
- }
325
- if (inQ) {
326
- current += ch;
327
- i++;
328
- continue;
329
- }
330
-
331
- if (ch === " ") {
332
- if (current) {
333
- tokens.push(current);
334
- current = "";
335
- }
336
- i++;
337
- continue;
338
- }
339
-
340
- if ((ch === ">" || ch === "<") && !inQ) {
341
- if (current) {
342
- tokens.push(current);
343
- current = "";
344
- }
345
- if (ch === ">" && next === ">") {
346
- tokens.push(">>");
347
- i += 2;
348
- } else {
349
- tokens.push(ch);
350
- i++;
351
- }
352
- continue;
353
- }
354
-
355
- current += ch;
356
- i++;
357
- }
358
- if (current) tokens.push(current);
359
- return tokens;
360
- }
@@ -1,4 +1,4 @@
1
- import { createHash, randomBytes, randomUUID, scryptSync } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
3
  import * as path from "node:path";
4
4
  import type { PerfLogger } from "../utils/perfLogger";
@@ -101,14 +101,14 @@ export class VirtualUserManager extends EventEmitter {
101
101
  // changed = true;
102
102
  // }
103
103
 
104
- const homePath = `/home/root`;
105
- if (!this.vfs.exists(homePath)) {
106
- this.vfs.mkdir(homePath, 0o755);
107
- this.vfs.writeFile(
108
- `${homePath}/README.txt`,
109
- `Welcome to the virtual environment, root`,
110
- );
111
- }
104
+ // const homePath = `/home/root`;
105
+ // if (!this.vfs.exists(homePath)) {
106
+ // this.vfs.mkdir(homePath, 0o755);
107
+ // this.vfs.writeFile(
108
+ // `${homePath}/README.txt`,
109
+ // `Welcome to the virtual environment, root`,
110
+ // );
111
+ // }
112
112
 
113
113
  if (changed) {
114
114
  await this.persist();
@@ -239,10 +239,22 @@ export class VirtualUserManager extends EventEmitter {
239
239
  perf.mark("verifyPassword");
240
240
  const record = this.users.get(username);
241
241
  if (!record) {
242
+ // Perform a dummy hash to avoid timing leakage on unknown usernames
243
+ this.hashPassword(password, "");
242
244
  return false;
243
245
  }
244
246
 
245
- return this.hashPassword(password) === record.passwordHash;
247
+ const computed = this.hashPassword(password, record.salt);
248
+ const expected = record.passwordHash;
249
+ // timingSafeEqual prevents timing-based password oracle attacks
250
+ try {
251
+ const a = Buffer.from(computed, "hex");
252
+ const b = Buffer.from(expected, "hex");
253
+ if (a.length !== b.length) return false;
254
+ return timingSafeEqual(a, b);
255
+ } catch {
256
+ return computed === expected;
257
+ }
246
258
  }
247
259
 
248
260
  /**
@@ -617,7 +629,8 @@ export class VirtualUserManager extends EventEmitter {
617
629
  }
618
630
 
619
631
  private createRecord(username: string, password: string): VirtualUserRecord {
620
- const cacheKey = `${username}:${password}`;
632
+ // Cache key is a hash of the inputs — never store plaintext password in memory
633
+ const cacheKey = createHash("sha256").update(username).update(":").update(password).digest("hex");
621
634
  const cached = VirtualUserManager.recordCache.get(cacheKey);
622
635
  if (cached) {
623
636
  return cached;
@@ -627,7 +640,8 @@ export class VirtualUserManager extends EventEmitter {
627
640
  const record = {
628
641
  username,
629
642
  salt,
630
- passwordHash: this.hashPassword(password),
643
+ // Hash uses the generated salt — verifyPassword must use record.salt
644
+ passwordHash: this.hashPassword(password, salt),
631
645
  };
632
646
 
633
647
  VirtualUserManager.recordCache.set(cacheKey, record);
@@ -644,11 +658,12 @@ export class VirtualUserManager extends EventEmitter {
644
658
  */
645
659
  public hasPassword(username: string): boolean {
646
660
  perf.mark("hasPassword");
647
- if (this.getPasswordHash(username) === this.hashPassword("")) {
648
- return false;
649
- }
650
661
  const record = this.users.get(username);
651
- return !!record && !!record.passwordHash;
662
+ if (!record) return false;
663
+ // Empty password hash computed with the record's own salt
664
+ const emptyHash = this.hashPassword("", record.salt);
665
+ if (record.passwordHash === emptyHash) return false;
666
+ return !!record.passwordHash;
652
667
  }
653
668
 
654
669
  /**
@@ -660,12 +675,21 @@ export class VirtualUserManager extends EventEmitter {
660
675
  * @param password Plaintext password string.
661
676
  * @returns Hex-encoded hash string.
662
677
  */
663
- public hashPassword(password: string): string {
678
+ /**
679
+ * Hash a password with an optional salt.
680
+ * When salt is provided (verify path), the same salt is used for a
681
+ * deterministic hash. When omitted (create path), an empty salt is used
682
+ * for backward compat — callers should pass the stored salt on verify.
683
+ */
684
+ public hashPassword(password: string, salt = ""): string {
664
685
  if (VirtualUserManager.fastPasswordHash) {
665
- return createHash("sha256").update(`${password}`).digest("hex");
686
+ return createHash("sha256")
687
+ .update(salt)
688
+ .update(password)
689
+ .digest("hex");
666
690
  }
667
691
 
668
- return scryptSync(password, "", 32).toString("hex");
692
+ return scryptSync(password, salt || "", 32).toString("hex");
669
693
  }
670
694
 
671
695
  private validateUsername(username: string): void {
@@ -1,30 +1,103 @@
1
- import type { ShellModule } from "../types/commands";
1
+ import type { CommandResult, ShellModule } from "../types/commands";
2
+ import type { VirtualShell } from "../VirtualShell";
2
3
 
3
4
  /**
4
- * Add a new user to the virtual user database.
5
- * @category users
6
- * @params ["<username> <password>"]
7
- * @returns ShellModule
5
+ * Add a new user interactively.
6
+ *
7
+ * Usage: `adduser <username>`
8
+ *
9
+ * Prompts for:
10
+ * New password: ****
11
+ * Retype new password: ****
12
+ *
13
+ * Mirrors the real `adduser` behaviour — password is never passed on the
14
+ * command line. Root-only.
8
15
  */
9
16
  export const adduserCommand: ShellModule = {
10
17
  name: "adduser",
11
18
  description: "Add a new user",
12
19
  category: "users",
13
- params: ["<username> <password>"],
14
- run: async ({ authUser, shell, args }) => {
20
+ params: ["<username>"],
21
+ run: ({ authUser, shell, args }) => {
15
22
  if (authUser !== "root") {
16
- return { stderr: "adduser: permission denied", exitCode: 1 };
23
+ return { stderr: "adduser: permission denied\n", exitCode: 1 };
17
24
  }
18
25
 
19
- const [username, password] = args;
20
- if (!username || !password) {
26
+ const username = args[0];
27
+ if (!username) {
21
28
  return {
22
- stderr: "adduser: usage: adduser <username> <password>",
29
+ stderr: "Usage: adduser <username>\n",
23
30
  exitCode: 1,
24
31
  };
25
32
  }
26
33
 
27
- await shell.users.addUser(username, password);
28
- return { stdout: `adduser: user '${username}' created`, exitCode: 0 };
34
+ // Reject if user already exists
35
+ if (shell.users.listUsers().includes(username)) {
36
+ return {
37
+ stderr: `adduser: user '${username}' already exists\n`,
38
+ exitCode: 1,
39
+ };
40
+ }
41
+
42
+ let newPassword = "";
43
+ type Step = "new" | "retype";
44
+ let step: Step = "new";
45
+
46
+ const onPassword = async (
47
+ input: string,
48
+ sh: VirtualShell,
49
+ ): Promise<{ result: CommandResult | null; nextPrompt?: string }> => {
50
+ if (step === "new") {
51
+ if (input.length < 1) {
52
+ return {
53
+ result: {
54
+ stderr: "adduser: password cannot be empty\n",
55
+ exitCode: 1,
56
+ },
57
+ };
58
+ }
59
+ newPassword = input;
60
+ step = "retype";
61
+ return { result: null, nextPrompt: "Retype new password: " };
62
+ }
63
+
64
+ // step === "retype"
65
+ if (input !== newPassword) {
66
+ return {
67
+ result: {
68
+ stderr: "adduser: passwords do not match — user not created\n",
69
+ exitCode: 1,
70
+ },
71
+ };
72
+ }
73
+
74
+ await sh.users.addUser(username, newPassword);
75
+ return {
76
+ result: {
77
+ stdout: `${[
78
+ `Adding user '${username}' ...`,
79
+ `Adding new group '${username}' (1001) ...`,
80
+ `Adding new user '${username}' (1001) with group '${username}' ...`,
81
+ `Creating home directory '/home/${username}' ...`,
82
+ `passwd: password set for '${username}'`,
83
+ `adduser: done.`,
84
+ ].join("\n")}\n`,
85
+ exitCode: 0,
86
+ },
87
+ };
88
+ };
89
+
90
+ return {
91
+ sudoChallenge: {
92
+ username,
93
+ targetUser: username,
94
+ commandLine: null,
95
+ loginShell: false,
96
+ prompt: "New password: ",
97
+ mode: "passwd",
98
+ onPassword,
99
+ },
100
+ exitCode: 0,
101
+ };
29
102
  },
30
103
  };
@@ -41,6 +41,11 @@ export const aliasCommand: ShellModule = {
41
41
  },
42
42
  };
43
43
 
44
+ /**
45
+ * Remove shell aliases.
46
+ * @category shell
47
+ * @params ["<name...>"]
48
+ */
44
49
  export const unaliasCommand: ShellModule = {
45
50
  name: "unalias",
46
51
  description: "Remove alias definitions",
@@ -162,6 +162,11 @@ export const aptCommand: ShellModule = {
162
162
  },
163
163
  };
164
164
 
165
+ /**
166
+ * Query the package cache and retrieve package information.
167
+ * @category package
168
+ * @params ["<search|show|policy> [pkg]"]
169
+ */
165
170
  export const aptCacheCommand: ShellModule = {
166
171
  name: "apt-cache",
167
172
  description: "Query the package cache",
@@ -1,44 +1,169 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { getFlag } from "./command-helpers";
3
+ import { resolvePath, assertPathAccess } from "./helpers";
3
4
 
4
5
  /**
5
- * Minimal `awk`-like pattern scanner (supports simple print patterns).
6
- * @category text
7
- * @params ["[-F <sep>] '<program>' [file]"]
6
+ * Minimal awk-like pattern scanner.
8
7
  *
9
- * Supported program patterns:
10
- * - `print $N` (e.g. `print $1`, `print $2, $3`, `print $0`)
11
- * - `{print $N}` (e.g. `{print $1}`, `{print $2, $3}`, `{print $0}`)
12
- *
13
- * The field separator can be set with `-F` (default is space, which splits on any whitespace).
8
+ * Supported:
9
+ * - `NR==N` pattern (line number condition)
10
+ * - `NF` (number of fields)
11
+ * - `/regex/` pattern
12
+ * - `{ print $N, $M, ... }` action
13
+ * - `{ print }` / `{ print $0 }`
14
+ * - `BEGIN { ... }` and `END { ... }` blocks (no side effects)
15
+ * - `$NF` (last field)
16
+ * - `-F sep` field separator
14
17
  */
15
18
  export const awkCommand: ShellModule = {
16
19
  name: "awk",
17
- description: "Pattern scanning and processing language (minimal)",
20
+ description: "Pattern scanning and processing language",
18
21
  category: "text",
19
22
  params: ["[-F <sep>] '<program>' [file]"],
20
- run: ({ args, stdin }) => {
23
+ run: ({ authUser, args, stdin, cwd, shell }) => {
21
24
  const sep = (getFlag(args, ["-F"]) as string | undefined) ?? " ";
22
- const prog = args.find((a) => !a.startsWith("-") && a !== sep);
25
+ const nonFlagArgs = args.filter((a) => !a.startsWith("-") && a !== sep);
26
+ const prog = nonFlagArgs[0];
27
+ const fileArg = nonFlagArgs[1];
28
+
23
29
  if (!prog) return { stderr: "awk: no program", exitCode: 1 };
24
30
 
25
- // Only support print $N and {print $N} patterns
26
- const printMatch = prog.match(/^\{?\s*print\s+([^}]+)\s*\}?$/);
27
- if (!printMatch)
28
- return { stderr: `awk: unsupported program: ${prog}`, exitCode: 1 };
29
-
30
- const fields = printMatch[1]!.split(/\s*,\s*/).map((f) => f.trim());
31
- const lines = (stdin ?? "").split("\n").filter(Boolean);
32
- const out = lines.map((line) => {
33
- const parts = line.split(sep === " " ? /\s+/ : sep);
34
- return fields
35
- .map((f) => {
36
- if (f === "$0") return line;
37
- const n = parseInt(f.replace("$", ""), 10);
38
- return Number.isNaN(n) ? f.replace(/"/g, "") : (parts[n - 1] ?? "");
39
- })
40
- .join(sep === " " ? "\t" : sep);
41
- });
42
- return { stdout: out.join("\n"), exitCode: 0 };
31
+ let input = stdin ?? "";
32
+ if (fileArg) {
33
+ const filePath = resolvePath(cwd, fileArg);
34
+ try {
35
+ assertPathAccess(authUser, filePath, "awk");
36
+ input = shell.vfs.readFile(filePath);
37
+ } catch {
38
+ return { stderr: `awk: ${fileArg}: No such file or directory`, exitCode: 1 };
39
+ }
40
+ }
41
+
42
+ const lines = input.split("\n");
43
+ // Remove empty last element if input ends with \n
44
+ if (lines[lines.length - 1] === "") lines.pop();
45
+
46
+ // Parse program into clauses: [pattern, action]
47
+ type Clause = { pattern: string; action: string };
48
+ const clauses: Clause[] = [];
49
+
50
+ const progTrim = prog.trim();
51
+
52
+ // Handle single unbraced pattern (NR==2, /regex/)
53
+ if (!progTrim.startsWith("{") && !progTrim.includes("{")) {
54
+ clauses.push({ pattern: progTrim, action: "print $0" });
55
+ } else {
56
+ // Parse "pattern { action } pattern2 { action2 }"
57
+ const clauseRe = /([^{]*)\{([^}]*)\}/g;
58
+ let m2 = clauseRe.exec(progTrim);
59
+ while (m2 !== null) {
60
+ clauses.push({ pattern: m2[1]!.trim(), action: m2[2]!.trim() });
61
+ m2 = clauseRe.exec(progTrim);
62
+ }
63
+ if (clauses.length === 0) {
64
+ clauses.push({ pattern: "", action: progTrim.replace(/[{}]/g, "").trim() });
65
+ }
66
+ }
67
+
68
+ const out: string[] = [];
69
+
70
+ // BEGIN / END
71
+ const beginClause = clauses.find((c) => c.pattern === "BEGIN");
72
+ const endClause = clauses.find((c) => c.pattern === "END");
73
+ const mainClauses = clauses.filter((c) => c.pattern !== "BEGIN" && c.pattern !== "END");
74
+
75
+ function splitFields(line: string): string[] {
76
+ if (sep === " ") return line.trim().split(/\s+/).filter(Boolean);
77
+ return line.split(sep);
78
+ }
79
+
80
+ function evalAction(action: string, line: string, nr: number): void {
81
+ const parts = splitFields(line);
82
+ const nf = parts.length;
83
+
84
+ // Expand variables
85
+ const resolve = (expr: string): string => {
86
+ expr = expr.trim();
87
+ if (expr === "NR") return String(nr);
88
+ if (expr === "NF") return String(nf);
89
+ if (expr === "$0") return line;
90
+ if (expr === "$NF") return parts[nf - 1] ?? "";
91
+ if (/^\$\d+$/.test(expr)) return parts[parseInt(expr.slice(1), 10) - 1] ?? "";
92
+ // Arithmetic NR+1, NF-1
93
+ const arith = expr.replace(/\bNR\b/g, String(nr)).replace(/\bNF\b/g, String(nf));
94
+ if (/^[\d\s+\-*/()]+$/.test(arith)) {
95
+ // biome-ignore lint/security/noGlobalEval: safe arithmetic — input contains only digits and operators after variable substitution
96
+ try { return String(Function(`"use strict"; return (${arith});`)()); } catch {} }
97
+ return expr.replace(/"/g, "");
98
+ };
99
+
100
+ const stmts = action.split(";").map((s) => s.trim()).filter(Boolean);
101
+ for (const stmt of stmts) {
102
+ if (stmt === "print" || stmt === "print $0") {
103
+ out.push(line);
104
+ } else if (stmt.startsWith("print ")) {
105
+ const args2 = stmt.slice(6).split(/\s*,\s*/);
106
+ out.push(args2.map(resolve).join("\t"));
107
+ }
108
+ }
109
+ }
110
+
111
+ function matchPattern(pattern: string, line: string, nr: number): boolean {
112
+ if (!pattern) return true;
113
+ if (pattern === "1") return true;
114
+
115
+ // NR==N or NR>N etc.
116
+ const nrCond = pattern.match(/^NR\s*([=!<>]=?|==)\s*(\d+)$/);
117
+ if (nrCond) {
118
+ const op = nrCond[1]!;
119
+ const val = parseInt(nrCond[2]!, 10);
120
+ switch (op) {
121
+ case "==": return nr === val;
122
+ case "!=": return nr !== val;
123
+ case ">": return nr > val;
124
+ case ">=": return nr >= val;
125
+ case "<": return nr < val;
126
+ case "<=": return nr <= val;
127
+ }
128
+ }
129
+
130
+ // NR%N==M
131
+ const nrMod = pattern.match(/^NR%(\d+)==(\d+)$/);
132
+ if (nrMod) {
133
+ return nr % parseInt(nrMod[1]!, 10) === parseInt(nrMod[2]!, 10);
134
+ }
135
+
136
+ // /regex/ pattern
137
+ if (pattern.startsWith("/") && pattern.endsWith("/")) {
138
+ try {
139
+ return new RegExp(pattern.slice(1, -1)).test(line);
140
+ } catch { return false; }
141
+ }
142
+
143
+ // $N~/regex/
144
+ const fieldMatch = pattern.match(/^\$(\d+)~\/(.*)\/$/);
145
+ if (fieldMatch) {
146
+ const parts = splitFields(line);
147
+ const field = parts[parseInt(fieldMatch[1]!, 10) - 1] ?? "";
148
+ try { return new RegExp(fieldMatch[2]!).test(field); } catch { return false; }
149
+ }
150
+
151
+ return false;
152
+ }
153
+
154
+ if (beginClause) evalAction(beginClause.action, "", 0);
155
+
156
+ for (let nr = 1; nr <= lines.length; nr++) {
157
+ const line = lines[nr - 1]!;
158
+ for (const clause of mainClauses) {
159
+ if (matchPattern(clause.pattern, line, nr)) {
160
+ evalAction(clause.action, line, nr);
161
+ }
162
+ }
163
+ }
164
+
165
+ if (endClause) evalAction(endClause.action, "", lines.length + 1);
166
+
167
+ return { stdout: out.join("\n") + (out.length > 0 ? "\n" : ""), exitCode: 0 };
43
168
  },
44
169
  };
@@ -19,10 +19,6 @@ export const cdCommand: ShellModule = {
19
19
  return { stderr: `cd: not a directory: ${target}`, exitCode: 1 };
20
20
  }
21
21
 
22
- if (mode === "exec") {
23
- return { exitCode: 0 };
24
- }
25
-
26
22
  return { nextCwd: target, exitCode: 0 };
27
23
  },
28
24
  };
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Clear the terminal screen.
5
+ * @category shell
6
+ * @params []
7
+ */
3
8
  export const clearCommand: ShellModule = {
4
9
  name: "clear",
5
10
  description: "Clear the terminal screen",
@@ -15,11 +15,20 @@ function matchFlagToken(
15
15
  return { matched: true, inlineValue: null };
16
16
  }
17
17
 
18
+ // --flag=value style
18
19
  const prefix = `${flag}=`;
19
20
  if (token.startsWith(prefix)) {
20
21
  return { matched: true, inlineValue: token.slice(prefix.length) };
21
22
  }
22
23
 
24
+ // Short flag inline value: -f2, -d: (single char flag like -f, -d, -n)
25
+ // Only applies to single-char flags (-X), not long flags (--flag)
26
+ if (flag.length === 2 && flag.startsWith("-") && !flag.startsWith("--")) {
27
+ if (token.startsWith(flag) && token.length > flag.length) {
28
+ return { matched: true, inlineValue: token.slice(flag.length) };
29
+ }
30
+ }
31
+
23
32
  return { matched: false, inlineValue: null };
24
33
  }
25
34
 
@@ -97,7 +97,8 @@ export const curlCommand: ShellModule = {
97
97
 
98
98
  let response: Response;
99
99
  try {
100
- response = await fetch(url, fetchOpts);
100
+ const urlWithHttp = url.startsWith("http://") || url.startsWith("https://") ? url : `http://${url}`;
101
+ response = await fetch(urlWithHttp, fetchOpts);
101
102
  } catch (err) {
102
103
  const msg = err instanceof Error ? err.message : String(err);
103
104
  return {
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
 
4
+ /**
5
+ * Declare variables and give them attributes (integer, readonly, export, array).
6
+ * @category shell
7
+ * @params ["[-i] [-r] [-x] [-a] [name[=value]...]"]
8
+ */
4
9
  export const declareCommand: ShellModule = {
5
10
  name: "declare",
6
11
  aliases: ["local", "typeset"],