typescript-virtual-container 1.3.4 → 1.4.0

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 (282) 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 +274 -208
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +201 -149
  7. package/builds/standalone-wo-sftp.js.map +4 -4
  8. package/builds/standalone.js +263 -211
  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/VirtualShell/shell.d.ts.map +1 -1
  25. package/dist/VirtualShell/shell.js +25 -3
  26. package/dist/VirtualShell/shellParser.d.ts +1 -8
  27. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  28. package/dist/VirtualShell/shellParser.js +2 -81
  29. package/dist/VirtualUserManager/index.d.ts +7 -1
  30. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  31. package/dist/VirtualUserManager/index.js +47 -16
  32. package/dist/commands/adduser.d.ts +10 -4
  33. package/dist/commands/adduser.d.ts.map +1 -1
  34. package/dist/commands/adduser.js +75 -12
  35. package/dist/commands/alias.d.ts +5 -0
  36. package/dist/commands/alias.d.ts.map +1 -1
  37. package/dist/commands/alias.js +5 -0
  38. package/dist/commands/apt.d.ts +5 -0
  39. package/dist/commands/apt.d.ts.map +1 -1
  40. package/dist/commands/apt.js +5 -0
  41. package/dist/commands/awk.d.ts +10 -8
  42. package/dist/commands/awk.d.ts.map +1 -1
  43. package/dist/commands/awk.js +156 -28
  44. package/dist/commands/cd.d.ts.map +1 -1
  45. package/dist/commands/cd.js +0 -3
  46. package/dist/commands/clear.d.ts +5 -0
  47. package/dist/commands/clear.d.ts.map +1 -1
  48. package/dist/commands/clear.js +5 -0
  49. package/dist/commands/command-helpers.d.ts.map +1 -1
  50. package/dist/commands/command-helpers.js +8 -0
  51. package/dist/commands/declare.d.ts +5 -0
  52. package/dist/commands/declare.d.ts.map +1 -1
  53. package/dist/commands/declare.js +5 -0
  54. package/dist/commands/deluser.d.ts +12 -0
  55. package/dist/commands/deluser.d.ts.map +1 -1
  56. package/dist/commands/deluser.js +72 -6
  57. package/dist/commands/df.d.ts +5 -0
  58. package/dist/commands/df.d.ts.map +1 -1
  59. package/dist/commands/df.js +5 -0
  60. package/dist/commands/du.d.ts +5 -0
  61. package/dist/commands/du.d.ts.map +1 -1
  62. package/dist/commands/du.js +5 -0
  63. package/dist/commands/export.d.ts +5 -0
  64. package/dist/commands/export.d.ts.map +1 -1
  65. package/dist/commands/export.js +5 -0
  66. package/dist/commands/grep.d.ts.map +1 -1
  67. package/dist/commands/grep.js +22 -4
  68. package/dist/commands/groups.d.ts +5 -0
  69. package/dist/commands/groups.d.ts.map +1 -1
  70. package/dist/commands/groups.js +5 -0
  71. package/dist/commands/gzip.d.ts +5 -2
  72. package/dist/commands/gzip.d.ts.map +1 -1
  73. package/dist/commands/gzip.js +48 -28
  74. package/dist/commands/head.d.ts.map +1 -1
  75. package/dist/commands/head.js +12 -3
  76. package/dist/commands/htop.d.ts +5 -0
  77. package/dist/commands/htop.d.ts.map +1 -1
  78. package/dist/commands/htop.js +5 -0
  79. package/dist/commands/kill.d.ts +5 -0
  80. package/dist/commands/kill.d.ts.map +1 -1
  81. package/dist/commands/kill.js +5 -0
  82. package/dist/commands/ln.d.ts +2 -0
  83. package/dist/commands/ln.d.ts.map +1 -1
  84. package/dist/commands/ln.js +22 -0
  85. package/dist/commands/ls.d.ts.map +1 -1
  86. package/dist/commands/ls.js +15 -0
  87. package/dist/commands/lsb-release.d.ts +5 -0
  88. package/dist/commands/lsb-release.d.ts.map +1 -1
  89. package/dist/commands/lsb-release.js +5 -0
  90. package/dist/commands/mkdir.d.ts +5 -0
  91. package/dist/commands/mkdir.d.ts.map +1 -1
  92. package/dist/commands/mkdir.js +5 -0
  93. package/dist/commands/mv.d.ts +5 -0
  94. package/dist/commands/mv.d.ts.map +1 -1
  95. package/dist/commands/mv.js +5 -0
  96. package/dist/commands/nano.d.ts +5 -0
  97. package/dist/commands/nano.d.ts.map +1 -1
  98. package/dist/commands/nano.js +5 -0
  99. package/dist/commands/neofetch.d.ts +5 -0
  100. package/dist/commands/neofetch.d.ts.map +1 -1
  101. package/dist/commands/neofetch.js +8 -5
  102. package/dist/commands/passwd.d.ts +8 -0
  103. package/dist/commands/passwd.d.ts.map +1 -1
  104. package/dist/commands/passwd.js +32 -11
  105. package/dist/commands/ping.d.ts +5 -0
  106. package/dist/commands/ping.d.ts.map +1 -1
  107. package/dist/commands/ping.js +5 -0
  108. package/dist/commands/printf.d.ts +5 -0
  109. package/dist/commands/printf.d.ts.map +1 -1
  110. package/dist/commands/printf.js +43 -12
  111. package/dist/commands/ps.d.ts +5 -0
  112. package/dist/commands/ps.d.ts.map +1 -1
  113. package/dist/commands/ps.js +5 -0
  114. package/dist/commands/read.d.ts +5 -0
  115. package/dist/commands/read.d.ts.map +1 -1
  116. package/dist/commands/read.js +5 -0
  117. package/dist/commands/registry.d.ts.map +1 -1
  118. package/dist/commands/registry.js +4 -1
  119. package/dist/commands/rm.d.ts +5 -0
  120. package/dist/commands/rm.d.ts.map +1 -1
  121. package/dist/commands/rm.js +5 -0
  122. package/dist/commands/runtime.d.ts.map +1 -1
  123. package/dist/commands/runtime.js +1 -57
  124. package/dist/commands/sed.d.ts +5 -0
  125. package/dist/commands/sed.d.ts.map +1 -1
  126. package/dist/commands/sed.js +5 -0
  127. package/dist/commands/set.d.ts +5 -6
  128. package/dist/commands/set.d.ts.map +1 -1
  129. package/dist/commands/set.js +5 -22
  130. package/dist/commands/sh.d.ts +6 -0
  131. package/dist/commands/sh.d.ts.map +1 -1
  132. package/dist/commands/sh.js +6 -0
  133. package/dist/commands/shift.d.ts +10 -0
  134. package/dist/commands/shift.d.ts.map +1 -1
  135. package/dist/commands/shift.js +10 -0
  136. package/dist/commands/sleep.d.ts +5 -0
  137. package/dist/commands/sleep.d.ts.map +1 -1
  138. package/dist/commands/sleep.js +5 -0
  139. package/dist/commands/sort.d.ts +5 -0
  140. package/dist/commands/sort.d.ts.map +1 -1
  141. package/dist/commands/sort.js +5 -0
  142. package/dist/commands/source.d.ts +5 -0
  143. package/dist/commands/source.d.ts.map +1 -1
  144. package/dist/commands/source.js +5 -0
  145. package/dist/commands/stat.d.ts +7 -0
  146. package/dist/commands/stat.d.ts.map +1 -0
  147. package/dist/commands/stat.js +56 -0
  148. package/dist/commands/su.d.ts +13 -0
  149. package/dist/commands/su.d.ts.map +1 -1
  150. package/dist/commands/su.js +45 -14
  151. package/dist/commands/sudo.d.ts.map +1 -1
  152. package/dist/commands/sudo.js +5 -0
  153. package/dist/commands/tail.d.ts +5 -0
  154. package/dist/commands/tail.d.ts.map +1 -1
  155. package/dist/commands/tail.js +15 -3
  156. package/dist/commands/tar.d.ts +5 -0
  157. package/dist/commands/tar.d.ts.map +1 -1
  158. package/dist/commands/tar.js +40 -10
  159. package/dist/commands/tee.d.ts +5 -0
  160. package/dist/commands/tee.d.ts.map +1 -1
  161. package/dist/commands/tee.js +5 -0
  162. package/dist/commands/touch.d.ts +5 -0
  163. package/dist/commands/touch.d.ts.map +1 -1
  164. package/dist/commands/touch.js +5 -0
  165. package/dist/commands/tr.d.ts.map +1 -1
  166. package/dist/commands/tr.js +45 -10
  167. package/dist/commands/tree.d.ts +5 -0
  168. package/dist/commands/tree.d.ts.map +1 -1
  169. package/dist/commands/tree.js +5 -0
  170. package/dist/commands/true.d.ts +10 -0
  171. package/dist/commands/true.d.ts.map +1 -1
  172. package/dist/commands/true.js +10 -0
  173. package/dist/commands/type.d.ts +5 -0
  174. package/dist/commands/type.d.ts.map +1 -1
  175. package/dist/commands/type.js +5 -0
  176. package/dist/commands/uname.d.ts +5 -0
  177. package/dist/commands/uname.d.ts.map +1 -1
  178. package/dist/commands/uname.js +5 -0
  179. package/dist/commands/uniq.d.ts +5 -0
  180. package/dist/commands/uniq.d.ts.map +1 -1
  181. package/dist/commands/uniq.js +5 -0
  182. package/dist/commands/unset.d.ts +5 -0
  183. package/dist/commands/unset.d.ts.map +1 -1
  184. package/dist/commands/unset.js +5 -0
  185. package/dist/commands/uptime.d.ts +5 -0
  186. package/dist/commands/uptime.d.ts.map +1 -1
  187. package/dist/commands/uptime.js +5 -0
  188. package/dist/commands/wc.d.ts +5 -0
  189. package/dist/commands/wc.d.ts.map +1 -1
  190. package/dist/commands/wc.js +5 -0
  191. package/dist/commands/wget.d.ts +5 -0
  192. package/dist/commands/wget.d.ts.map +1 -1
  193. package/dist/commands/wget.js +5 -0
  194. package/dist/commands/who.d.ts +5 -0
  195. package/dist/commands/who.d.ts.map +1 -1
  196. package/dist/commands/who.js +5 -0
  197. package/dist/commands/whoami.d.ts +5 -0
  198. package/dist/commands/whoami.d.ts.map +1 -1
  199. package/dist/commands/whoami.js +5 -0
  200. package/dist/commands/xargs.d.ts +5 -0
  201. package/dist/commands/xargs.d.ts.map +1 -1
  202. package/dist/commands/xargs.js +5 -0
  203. package/dist/self-standalone.js +254 -30
  204. package/dist/types/commands.d.ts +36 -0
  205. package/dist/types/commands.d.ts.map +1 -1
  206. package/dist/utils/tokenize.d.ts +20 -0
  207. package/dist/utils/tokenize.d.ts.map +1 -0
  208. package/dist/utils/tokenize.js +74 -0
  209. package/examples/web.min.js +2 -2
  210. package/package.json +1 -1
  211. package/src/SSHClient/index.ts +6 -3
  212. package/src/SSHMimic/executor.ts +21 -44
  213. package/src/SSHMimic/index.ts +7 -5
  214. package/src/SSHMimic/sftp.ts +28 -21
  215. package/src/VirtualShell/shell.ts +34 -4
  216. package/src/VirtualShell/shellParser.ts +2 -103
  217. package/src/VirtualUserManager/index.ts +43 -19
  218. package/src/commands/adduser.ts +86 -13
  219. package/src/commands/alias.ts +5 -0
  220. package/src/commands/apt.ts +5 -0
  221. package/src/commands/awk.ts +154 -29
  222. package/src/commands/cd.ts +0 -4
  223. package/src/commands/clear.ts +5 -0
  224. package/src/commands/command-helpers.ts +9 -0
  225. package/src/commands/declare.ts +5 -0
  226. package/src/commands/deluser.ts +84 -7
  227. package/src/commands/df.ts +5 -0
  228. package/src/commands/du.ts +5 -0
  229. package/src/commands/export.ts +5 -0
  230. package/src/commands/grep.ts +21 -8
  231. package/src/commands/groups.ts +5 -0
  232. package/src/commands/gzip.ts +54 -28
  233. package/src/commands/head.ts +14 -4
  234. package/src/commands/htop.ts +5 -0
  235. package/src/commands/kill.ts +5 -0
  236. package/src/commands/ln.ts +22 -0
  237. package/src/commands/ls.ts +17 -0
  238. package/src/commands/lsb-release.ts +5 -0
  239. package/src/commands/mkdir.ts +5 -0
  240. package/src/commands/mv.ts +5 -0
  241. package/src/commands/nano.ts +5 -0
  242. package/src/commands/neofetch.ts +8 -6
  243. package/src/commands/passwd.ts +35 -12
  244. package/src/commands/ping.ts +5 -0
  245. package/src/commands/printf.ts +30 -13
  246. package/src/commands/ps.ts +5 -0
  247. package/src/commands/read.ts +5 -0
  248. package/src/commands/registry.ts +4 -1
  249. package/src/commands/rm.ts +5 -0
  250. package/src/commands/runtime.ts +1 -61
  251. package/src/commands/sed.ts +5 -0
  252. package/src/commands/set.ts +5 -24
  253. package/src/commands/sh.ts +9 -3
  254. package/src/commands/shift.ts +10 -0
  255. package/src/commands/sleep.ts +5 -0
  256. package/src/commands/sort.ts +5 -0
  257. package/src/commands/source.ts +5 -0
  258. package/src/commands/stat.ts +61 -0
  259. package/src/commands/su.ts +54 -16
  260. package/src/commands/sudo.ts +5 -0
  261. package/src/commands/tail.ts +17 -3
  262. package/src/commands/tar.ts +38 -15
  263. package/src/commands/tee.ts +5 -0
  264. package/src/commands/touch.ts +5 -0
  265. package/src/commands/tr.ts +54 -10
  266. package/src/commands/tree.ts +5 -0
  267. package/src/commands/true.ts +10 -0
  268. package/src/commands/type.ts +5 -0
  269. package/src/commands/uname.ts +5 -0
  270. package/src/commands/uniq.ts +5 -0
  271. package/src/commands/unset.ts +5 -0
  272. package/src/commands/uptime.ts +5 -0
  273. package/src/commands/wc.ts +5 -0
  274. package/src/commands/wget.ts +5 -0
  275. package/src/commands/who.ts +5 -0
  276. package/src/commands/whoami.ts +5 -0
  277. package/src/commands/xargs.ts +5 -0
  278. package/src/self-standalone.ts +316 -33
  279. package/src/types/commands.ts +37 -0
  280. package/src/utils/tokenize.ts +78 -0
  281. package/builds/web-iife.min.js +0 -13
  282. package/builds/web-iife.min.js.map +0 -7
@@ -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
 
@@ -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"],
@@ -1,21 +1,98 @@
1
- import type { ShellModule } from "../types/commands";
1
+ import type { CommandResult, ShellModule } from "../types/commands";
2
+ import type { VirtualShell } from "../VirtualShell";
2
3
 
4
+ /**
5
+ * Delete a user. Root-only.
6
+ *
7
+ * Without `-f`: prompts for confirmation — the user must type the exact
8
+ * username to proceed.
9
+ *
10
+ * With `-f` / `--force`: deletes without confirmation.
11
+ *
12
+ * Usage:
13
+ * deluser <username>
14
+ * deluser -f <username>
15
+ */
3
16
  export const deluserCommand: ShellModule = {
4
17
  name: "deluser",
5
18
  description: "Delete a user",
6
19
  category: "users",
7
- params: ["<username>"],
20
+ params: ["[-f] <username>"],
8
21
  run: async ({ authUser, args, shell }) => {
9
22
  if (authUser !== "root") {
10
- return { stderr: "deluser: permission denied", exitCode: 1 };
23
+ return { stderr: "deluser: permission denied\n", exitCode: 1 };
11
24
  }
12
25
 
13
- const [username] = args;
26
+ const force =
27
+ args.includes("-f") ||
28
+ args.includes("--force") ||
29
+ args.includes("-y");
30
+ const username = args.find((a) => !a.startsWith("-"));
31
+
14
32
  if (!username) {
15
- return { stderr: "deluser: usage: deluser <username>", exitCode: 1 };
33
+ return {
34
+ stderr: "Usage: deluser [-f] <username>\n",
35
+ exitCode: 1,
36
+ };
37
+ }
38
+
39
+ if (!shell.users.listUsers().includes(username)) {
40
+ return {
41
+ stderr: `deluser: user '${username}' does not exist\n`,
42
+ exitCode: 1,
43
+ };
44
+ }
45
+
46
+ if (username === "root") {
47
+ return {
48
+ stderr: "deluser: cannot remove the root account\n",
49
+ exitCode: 1,
50
+ };
16
51
  }
17
52
 
18
- await shell.users.deleteUser(username);
19
- return { stdout: `deluser: user '${username}' deleted`, exitCode: 0 };
53
+ // Force mode — delete without confirmation
54
+ if (force) {
55
+ await shell.users.deleteUser(username);
56
+ return {
57
+ stdout: `Removing user '${username}' ...\ndeluser: done.\n`,
58
+ exitCode: 0,
59
+ };
60
+ }
61
+
62
+ // Interactive confirmation
63
+ const onPassword = async (
64
+ input: string,
65
+ sh: VirtualShell,
66
+ ): Promise<{ result: CommandResult | null; nextPrompt?: string }> => {
67
+ if (input.trim() !== username) {
68
+ return {
69
+ result: {
70
+ stderr: "deluser: confirmation did not match — user not deleted\n",
71
+ exitCode: 1,
72
+ },
73
+ };
74
+ }
75
+
76
+ await sh.users.deleteUser(username);
77
+ return {
78
+ result: {
79
+ stdout: `Removing user '${username}' ...\ndeluser: done.\n`,
80
+ exitCode: 0,
81
+ },
82
+ };
83
+ };
84
+
85
+ return {
86
+ sudoChallenge: {
87
+ username,
88
+ targetUser: username,
89
+ commandLine: null,
90
+ loginShell: false,
91
+ prompt: `Warning: deleting user '${username}'.\nType the username to confirm: `,
92
+ mode: "confirm",
93
+ onPassword,
94
+ },
95
+ exitCode: 0,
96
+ };
20
97
  },
21
98
  };
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Report filesystem disk space usage.
5
+ * @category system
6
+ * @params ["[-h]"]
7
+ */
3
8
  export const dfCommand: ShellModule = {
4
9
  name: "df",
5
10
  description: "Report filesystem disk space usage",
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
  import { resolvePath } from "./helpers";
4
4
 
5
+ /**
6
+ * Estimate file and directory space usage.
7
+ * @category system
8
+ * @params ["[-h] [-s] [path]"]
9
+ */
5
10
  export const duCommand: ShellModule = {
6
11
  name: "du",
7
12
  description: "Estimate file space usage",
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Set or display shell environment variables for child processes.
5
+ * @category shell
6
+ * @params ["[VAR=value]"]
7
+ */
3
8
  export const exportCommand: ShellModule = {
4
9
  name: "export",
5
10
  description: "Set shell environment variable",
@@ -14,12 +14,15 @@ export const grepCommand: ShellModule = {
14
14
  params: ["[-i] [-v] [-n] [-r] <pattern> [file...]"],
15
15
  run: ({ authUser, shell, cwd, args, stdin }) => {
16
16
  const { flags, positionals } = parseArgs(args, {
17
- flags: ["-i", "-v", "-n", "-r"],
17
+ flags: ["-i", "-v", "-n", "-r", "-c", "-l", "-L", "-q", "--quiet", "--silent"],
18
18
  });
19
- const caseInsensitive = flags.has("-i");
20
- const invertMatch = flags.has("-v");
21
- const showLineNumbers = flags.has("-n");
22
- const recursive = flags.has("-r");
19
+ const caseInsensitive = flags.has("-i");
20
+ const invertMatch = flags.has("-v");
21
+ const showLineNumbers = flags.has("-n");
22
+ const recursive = flags.has("-r");
23
+ const countOnly = flags.has("-c");
24
+ const filesWithMatches = flags.has("-l");
25
+ const quiet = flags.has("-q") || flags.has("--quiet") || flags.has("--silent");
23
26
  const pattern = positionals[0];
24
27
  const files = positionals.slice(1);
25
28
 
@@ -73,7 +76,10 @@ export const grepCommand: ShellModule = {
73
76
 
74
77
  if (files.length === 0) {
75
78
  if (!stdin) return { stdout: "", exitCode: 1 };
76
- results.push(...matchLines(stdin));
79
+ const matched = matchLines(stdin);
80
+ if (countOnly) return { stdout: `${matched.length}\n`, exitCode: matched.length > 0 ? 0 : 1 };
81
+ if (quiet) return { exitCode: matched.length > 0 ? 0 : 1 };
82
+ results.push(...matched);
77
83
  } else {
78
84
  const resolvedPaths = files.flatMap((f) => {
79
85
  const target = resolvePath(cwd, f);
@@ -85,7 +91,14 @@ export const grepCommand: ShellModule = {
85
91
  assertPathAccess(authUser, filePath, "grep");
86
92
  const content = shell.vfs.readFile(filePath);
87
93
  const prefix = resolvedPaths.length > 1 ? `${file}:` : "";
88
- results.push(...matchLines(content, prefix));
94
+ const matched = matchLines(content, prefix);
95
+ if (countOnly) {
96
+ results.push(resolvedPaths.length > 1 ? `${file}:${matched.length}` : String(matched.length));
97
+ } else if (filesWithMatches) {
98
+ if (matched.length > 0) results.push(file);
99
+ } else {
100
+ results.push(...matched);
101
+ }
89
102
  } catch {
90
103
  return {
91
104
  stderr: `grep: ${file}: No such file or directory`,
@@ -96,7 +109,7 @@ export const grepCommand: ShellModule = {
96
109
  }
97
110
 
98
111
  return {
99
- stdout: results.length > 0 ? results.join("\n") : "",
112
+ stdout: results.length > 0 ? `${results.join("\n")}\n` : "",
100
113
  exitCode: results.length > 0 ? 0 : 1,
101
114
  };
102
115
  },
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Print group memberships for a user.
5
+ * @category system
6
+ * @params ["[user]"]
7
+ */
3
8
  export const groupsCommand: ShellModule = {
4
9
  name: "groups",
5
10
  description: "Print group memberships",