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
@@ -2,54 +2,80 @@ import type { ShellModule } from "../types/commands";
2
2
  import { resolvePath } from "./helpers";
3
3
 
4
4
  /**
5
- * Compress files using gzip (stores in VFS as compressed content).
5
+ * Compress files using gzip renames file to `<file>.gz`, removes original.
6
6
  * @category archive
7
- * @params ["<file>"]
8
7
  */
9
8
  export const gzipCommand: ShellModule = {
10
9
  name: "gzip",
11
10
  description: "Compress files",
12
11
  category: "archive",
13
- params: ["<file>"],
12
+ params: ["[-k] [-d] <file>"],
14
13
  run: ({ shell, cwd, args }) => {
15
- const file = args[0];
16
- if (!file) return { stderr: "gzip: no file specified", exitCode: 1 };
14
+ const keepOrig = args.includes("-k") || args.includes("--keep");
15
+ const decompress = args.includes("-d");
16
+ const file = args.find((a) => !a.startsWith("-"));
17
+ if (!file) return { stderr: "gzip: no file specified\n", exitCode: 1 };
18
+
17
19
  const p = resolvePath(cwd, file);
18
- try {
19
- shell.vfs.compressFile(p);
20
+
21
+ if (decompress) {
22
+ // gzip -d = gunzip
23
+ if (!file.endsWith(".gz")) {
24
+ return { stderr: `gzip: ${file}: unknown suffix -- ignored\n`, exitCode: 1 };
25
+ }
26
+ if (!shell.vfs.exists(p)) {
27
+ return { stderr: `gzip: ${file}: No such file or directory\n`, exitCode: 1 };
28
+ }
29
+ const content = shell.vfs.readFile(p);
30
+ const dest = p.slice(0, -3);
31
+ shell.vfs.writeFile(dest, content);
32
+ if (!keepOrig) shell.vfs.remove(p);
20
33
  return { exitCode: 0 };
21
- } catch {
22
- return {
23
- stderr: `gzip: ${file}: No such file or directory`,
24
- exitCode: 1,
25
- };
26
34
  }
35
+
36
+ if (!shell.vfs.exists(p)) {
37
+ return { stderr: `gzip: ${file}: No such file or directory\n`, exitCode: 1 };
38
+ }
39
+ if (file.endsWith(".gz")) {
40
+ return { stderr: `gzip: ${file}: already has .gz suffix -- unchanged\n`, exitCode: 1 };
41
+ }
42
+
43
+ const rawContent = shell.vfs.readFileRaw(p);
44
+ const gzPath = `${p}.gz`;
45
+ shell.vfs.writeFile(gzPath, rawContent, { compress: true });
46
+ if (!keepOrig) shell.vfs.remove(p);
47
+ return { exitCode: 0 };
27
48
  },
28
49
  };
29
50
 
51
+ /**
52
+ * Decompress gzip files — renames `<file>.gz` to `<file>`, removes original.
53
+ * @category archive
54
+ */
30
55
  export const gunzipCommand: ShellModule = {
31
- /**
32
- * Decompress gzip files (or zcat alias).
33
- * @category archive
34
- * @params ["<file>"]
35
- */
36
56
  name: "gunzip",
37
57
  description: "Decompress files",
38
58
  category: "archive",
39
- params: ["<file>"],
40
59
  aliases: ["zcat"],
60
+ params: ["[-k] <file>"],
41
61
  run: ({ shell, cwd, args }) => {
42
- const file = args[0];
43
- if (!file) return { stderr: "gunzip: no file specified", exitCode: 1 };
62
+ const keepOrig = args.includes("-k") || args.includes("--keep");
63
+ const file = args.find((a) => !a.startsWith("-"));
64
+ if (!file) return { stderr: "gunzip: no file specified\n", exitCode: 1 };
65
+
44
66
  const p = resolvePath(cwd, file);
45
- try {
46
- shell.vfs.decompressFile(p);
47
- return { exitCode: 0 };
48
- } catch {
49
- return {
50
- stderr: `gunzip: ${file}: No such file or directory`,
51
- exitCode: 1,
52
- };
67
+
68
+ if (!shell.vfs.exists(p)) {
69
+ return { stderr: `gunzip: ${file}: No such file or directory\n`, exitCode: 1 };
70
+ }
71
+ if (!file.endsWith(".gz")) {
72
+ return { stderr: `gunzip: ${file}: unknown suffix -- ignored\n`, exitCode: 1 };
53
73
  }
74
+
75
+ const content = shell.vfs.readFile(p);
76
+ const dest = p.slice(0, -3);
77
+ shell.vfs.writeFile(dest, content);
78
+ if (!keepOrig) shell.vfs.remove(p);
79
+ return { exitCode: 0 };
54
80
  },
55
81
  };
@@ -14,11 +14,21 @@ export const headCommand: ShellModule = {
14
14
  params: ["[-n <lines>] [file...]"],
15
15
  run: ({ authUser, shell, cwd, args, stdin }) => {
16
16
  const nArg = getFlag(args, ["-n"]);
17
- const n = typeof nArg === "string" ? parseInt(nArg, 10) : 10;
18
- const positionals = args.filter((a) => !a.startsWith("-") && a !== nArg);
17
+ // Support both -n N and -N shorthand (head -2, head -10)
18
+ const shortN = args.find((a) => /^-\d+$/.test(a));
19
+ const n = typeof nArg === "string"
20
+ ? parseInt(nArg, 10)
21
+ : shortN ? parseInt(shortN.slice(1), 10) : 10;
22
+ const positionals = args.filter(
23
+ (a) => !a.startsWith("-") && a !== nArg && a !== String(n),
24
+ );
19
25
 
20
- const take = (content: string) =>
21
- content.split("\n").slice(0, n).join("\n");
26
+ const take = (content: string) => {
27
+ const lines = content.split("\n");
28
+ // Preserve trailing newline
29
+ const sliced = lines.slice(0, n);
30
+ return sliced.join("\n") + (content.endsWith("\n") && sliced.length === lines.slice(0, n).length ? "\n" : "");
31
+ };
22
32
 
23
33
  if (positionals.length === 0) {
24
34
  return { stdout: take(stdin ?? ""), exitCode: 0 };
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Interactive system monitor (requires terminal interaction).
5
+ * @category system
6
+ * @params []
7
+ */
3
8
  export const htopCommand: ShellModule = {
4
9
  name: "htop",
5
10
  description: "System monitor",
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Send a signal to a process by PID.
5
+ * @category system
6
+ * @params ["[-9] <pid>"]
7
+ */
3
8
  export const killCommand: ShellModule = {
4
9
  name: "kill",
5
10
  description: "Send signal to process",
@@ -47,3 +47,25 @@ export const lnCommand: ShellModule = {
47
47
  }
48
48
  },
49
49
  };
50
+
51
+ /** Shell command: print the value of a symbolic link. */
52
+ export const readlinkCommand: ShellModule = {
53
+ name: "readlink",
54
+ description: "Print resolved path of symbolic link",
55
+ category: "files",
56
+ params: ["[-f] <path>"],
57
+ run: ({ shell, cwd, args }) => {
58
+ const follow = args.includes("-f") || args.includes("-e");
59
+ const target = args.find((a) => !a.startsWith("-"));
60
+ if (!target) return { stderr: "readlink: missing operand\n", exitCode: 1 };
61
+ const p = resolvePath(cwd, target);
62
+ if (!shell.vfs.exists(p)) {
63
+ return { stderr: `readlink: ${target}: No such file or directory\n`, exitCode: 1 };
64
+ }
65
+ if (!shell.vfs.isSymlink(p)) {
66
+ return { stderr: `readlink: ${target}: not a symbolic link\n`, exitCode: 1 };
67
+ }
68
+ const resolved = shell.vfs.resolveSymlink(follow ? p : p);
69
+ return { stdout: `${resolved}\n`, exitCode: 0 };
70
+ },
71
+ };
@@ -39,6 +39,23 @@ export const lsCommand: ShellModule = {
39
39
  });
40
40
  const target = resolvePath(cwd, targetArg ?? cwd);
41
41
  assertPathAccess(authUser, target, "ls");
42
+
43
+ // If target is a file, show its info directly (ls -l /etc/passwd)
44
+ if (shell.vfs.exists(target)) {
45
+ const st = shell.vfs.stat(target);
46
+ if (st.type === "file" || shell.vfs.isSymlink(target)) {
47
+ if (longFormat) {
48
+ const name = target.split("/").pop() ?? target;
49
+ const size = st.type === "file" ? st.size : 0;
50
+ return {
51
+ stdout: `${formatPermissions(st.mode, false)} 1 root root ${size} ${formatDate(st.updatedAt)} ${name}\n`,
52
+ exitCode: 0,
53
+ };
54
+ }
55
+ return { stdout: target.split("/").pop() ?? target, exitCode: 0 };
56
+ }
57
+ }
58
+
42
59
  const items = shell.vfs
43
60
  .list(target)
44
61
  .filter((name) => showHidden || !name.startsWith("."));
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
 
4
+ /**
5
+ * Print Linux distribution information.
6
+ * @category system
7
+ * @params ["[-a] [-i] [-d] [-r] [-c]"]
8
+ */
4
9
  export const lsbReleaseCommand: ShellModule = {
5
10
  name: "lsb_release",
6
11
  description: "Print distribution-specific information",
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
2
2
  import { getArg } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
+ /**
6
+ * Create one or more directories.
7
+ * @category files
8
+ * @params ["<dir>"]
9
+ */
5
10
  export const mkdirCommand: ShellModule = {
6
11
  name: "mkdir",
7
12
  description: "Make directories",
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { assertPathAccess, resolvePath } from "./helpers";
3
3
 
4
+ /**
5
+ * Move or rename files and directories.
6
+ * @category files
7
+ * @params ["<source> <dest>"]
8
+ */
4
9
  export const mvCommand: ShellModule = {
5
10
  name: "mv",
6
11
  description: "Move or rename files",
@@ -2,6 +2,11 @@ import * as path from "node:path";
2
2
  import type { ShellModule } from "../types/commands";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
+ /**
6
+ * Simple text editor for editing files.
7
+ * @category files
8
+ * @params ["<file>"]
9
+ */
5
10
  export const nanoCommand: ShellModule = {
6
11
  name: "nano",
7
12
  description: "Text editor",
@@ -1,16 +1,18 @@
1
1
  import { buildNeofetchOutput } from "../modules/neofetch";
2
2
  import type { ShellModule } from "../types/commands";
3
3
  import { ifFlag } from "./command-helpers";
4
- import { getAllEnvVars } from "./set";
5
4
 
5
+ /**
6
+ * Display system information in a decorative format.
7
+ * @category system
8
+ * @params ["[--off]"]
9
+ */
6
10
  export const neofetchCommand: ShellModule = {
7
11
  name: "neofetch",
8
12
  description: "System info display",
9
13
  category: "system",
10
14
  params: ["[--off]"],
11
- run: ({ args, authUser, hostname, shell }) => {
12
- const env = getAllEnvVars(authUser);
13
-
15
+ run: ({ args, authUser, hostname, shell, env }) => {
14
16
  if (ifFlag(args, "--help")) {
15
17
  return {
16
18
  stdout: "Usage: neofetch [--off]",
@@ -29,9 +31,9 @@ export const neofetchCommand: ShellModule = {
29
31
  stdout: buildNeofetchOutput({
30
32
  user: authUser,
31
33
  host: hostname,
32
- shell: env.SHELL,
34
+ shell: env.vars.SHELL,
33
35
  shellProps: shell.properties,
34
- terminal: env.TERM,
36
+ terminal: env.vars.TERM,
35
37
  uptimeSeconds: Math.floor((Date.now() - shell.startTime) / 1000),
36
38
  packages: (() => {
37
39
  const count = shell.packageManager?.installedCount() ?? 0;
@@ -1,26 +1,49 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * `passwd [username]` — change a virtual user's password.
5
+ *
6
+ * - Root can change any user's password.
7
+ * - Non-root can only change their own password.
8
+ * - Interactive: emits `passwordChallenge` for hidden-input prompting.
9
+ * - Non-interactive: reads new password from stdin first line.
10
+ */
3
11
  export const passwdCommand: ShellModule = {
4
12
  name: "passwd",
5
13
  description: "Change user password",
6
14
  category: "users",
7
- params: ["<username> <password>"],
8
- run: async ({ authUser, args, shell }) => {
9
- const [username, password] = args;
10
- if (!username || !password) {
11
- return {
12
- stderr: "passwd: usage: passwd <username> <password>",
13
- exitCode: 1,
14
- };
15
- }
15
+ params: ["[username]"],
16
+ run: async ({ authUser, args, shell, stdin }) => {
17
+ const targetUser = args[0] ?? authUser;
16
18
 
17
- if (authUser !== "root" && authUser !== username) {
19
+ // Permission check
20
+ if (authUser !== "root" && authUser !== targetUser) {
18
21
  return { stderr: "passwd: permission denied", exitCode: 1 };
19
22
  }
20
23
 
21
- await shell.users.setPassword(username, password);
24
+ // Target must exist
25
+ if (!shell.users.listUsers().includes(targetUser)) {
26
+ return { stderr: `passwd: user '${targetUser}' does not exist`, exitCode: 1 };
27
+ }
28
+
29
+ // Non-interactive: read new password from stdin
30
+ if (stdin !== undefined && stdin.trim().length > 0) {
31
+ const password = stdin.trim().split("\n")[0]!;
32
+ await shell.users.setPassword(targetUser, password);
33
+ return {
34
+ stdout: `passwd: password updated successfully\n`,
35
+ exitCode: 0,
36
+ };
37
+ }
38
+
39
+ // Interactive: emit password challenge
22
40
  return {
23
- stdout: `passwd: password updated for '${username}'`,
41
+ passwordChallenge: {
42
+ prompt: "New password: ",
43
+ confirmPrompt: "Retype new password: ",
44
+ action: "passwd" as const,
45
+ targetUsername: targetUser,
46
+ },
24
47
  exitCode: 0,
25
48
  };
26
49
  },
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { parseArgs } from "./command-helpers";
3
3
 
4
+ /**
5
+ * Send ICMP ECHO_REQUEST packets (mock implementation).
6
+ * @category network
7
+ * @params ["[-c <count>] <host>"]
8
+ */
4
9
  export const pingCommand: ShellModule = {
5
10
  name: "ping",
6
11
  description: "Send ICMP ECHO_REQUEST (mock)",
@@ -50,35 +50,47 @@ function renderPrintf(fmt: string, args: string[]): string {
50
50
  }
51
51
  }
52
52
  if (fmt[i] === "%" && i + 1 < fmt.length) {
53
- // Optional width/precision: %[-][width][.prec]spec
54
53
  let j = i + 1;
55
- if (fmt[j] === "-") j++;
56
- while (j < fmt.length && /\d/.test(fmt[j]!)) j++;
54
+ let leftAlign = false; if (fmt[j] === "-") { leftAlign = true; j++; }
55
+ let zeroPad = false; if (fmt[j] === "0") { zeroPad = true; j++; }
56
+ let width = 0;
57
+ while (j < fmt.length && /\d/.test(fmt[j]!)) { width = width * 10 + parseInt(fmt[j]!, 10); j++; }
58
+ let precision = -1;
57
59
  if (fmt[j] === ".") {
58
- j++;
59
- while (j < fmt.length && /\d/.test(fmt[j]!)) j++;
60
+ j++; precision = 0;
61
+ while (j < fmt.length && /\d/.test(fmt[j]!)) { precision = precision * 10 + parseInt(fmt[j]!, 10); j++; }
60
62
  }
61
63
  const spec = fmt[j];
62
64
  const arg = args[argIdx++] ?? "";
65
+ const pad = (s: string, ch = " "): string => {
66
+ if (width <= 0 || s.length >= width) return s;
67
+ const fill = ch.repeat(width - s.length);
68
+ return leftAlign ? s + fill : fill + s;
69
+ };
63
70
  switch (spec) {
64
- case "s":
65
- out += arg;
71
+ case "s": {
72
+ let val = String(arg);
73
+ if (precision >= 0) val = val.slice(0, precision);
74
+ out += pad(val);
66
75
  break;
76
+ }
67
77
  case "d":
68
78
  case "i":
69
- out += String(parseInt(arg, 10) || 0);
79
+ out += pad(String(parseInt(arg, 10) || 0), zeroPad ? "0" : " ");
70
80
  break;
71
- case "f":
72
- out += String(parseFloat(arg) || 0);
81
+ case "f": {
82
+ const prec = precision >= 0 ? precision : 6;
83
+ out += pad((parseFloat(arg) || 0).toFixed(prec));
73
84
  break;
85
+ }
74
86
  case "o":
75
- out += (parseInt(arg, 10) || 0).toString(8);
87
+ out += pad((parseInt(arg, 10) || 0).toString(8), zeroPad ? "0" : " ");
76
88
  break;
77
89
  case "x":
78
- out += (parseInt(arg, 10) || 0).toString(16);
90
+ out += pad((parseInt(arg, 10) || 0).toString(16), zeroPad ? "0" : " ");
79
91
  break;
80
92
  case "X":
81
- out += (parseInt(arg, 10) || 0).toString(16).toUpperCase();
93
+ out += pad((parseInt(arg, 10) || 0).toString(16).toUpperCase(), zeroPad ? "0" : " ");
82
94
  break;
83
95
  case "%":
84
96
  out += "%";
@@ -98,6 +110,11 @@ function renderPrintf(fmt: string, args: string[]): string {
98
110
  return out;
99
111
  }
100
112
 
113
+ /**
114
+ * Format and print data to stdout.
115
+ * @category shell
116
+ * @params ["<format> [args...]"]
117
+ */
101
118
  export const printfCommand: ShellModule = {
102
119
  name: "printf",
103
120
  description: "Format and print data",
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
 
4
+ /**
5
+ * Report process status with various formatting options.
6
+ * @category system
7
+ * @params ["[-a] [-u] [-x] [aux]"]
8
+ */
4
9
  export const psCommand: ShellModule = {
5
10
  name: "ps",
6
11
  description: "Report process status",
@@ -1,6 +1,11 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { ifFlag } from "./command-helpers";
3
3
 
4
+ /**
5
+ * Read a line from stdin into one or more variables.
6
+ * @category shell
7
+ * @params ["[-r] [-p prompt] <var...>"]
8
+ */
4
9
  export const readCommand: ShellModule = {
5
10
  name: "read",
6
11
  description: "Read a line from stdin into variables",
@@ -35,7 +35,8 @@ import { hostnameCommand } from "./hostname";
35
35
  import { htopCommand } from "./htop";
36
36
  import { idCommand } from "./id";
37
37
  import { killCommand } from "./kill";
38
- import { lnCommand } from "./ln";
38
+ import { lnCommand, readlinkCommand } from "./ln";
39
+ import { statCommand } from "./stat";
39
40
  import { lsCommand } from "./ls";
40
41
  import { lsbReleaseCommand } from "./lsb-release";
41
42
  import { manCommand } from "./man";
@@ -96,7 +97,9 @@ const BASE_COMMANDS: ShellModule[] = [
96
97
  cpCommand,
97
98
  mvCommand,
98
99
  lnCommand,
100
+ readlinkCommand,
99
101
  chmodCommand,
102
+ statCommand,
100
103
  findCommand,
101
104
  // Text processing
102
105
  grepCommand,
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
2
2
  import { getArg, ifFlag } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
+ /**
6
+ * Remove files or directories from the filesystem.
7
+ * @category files
8
+ * @params ["[-r|-rf] <path>"]
9
+ */
5
10
  export const rmCommand: ShellModule = {
6
11
  name: "rm",
7
12
  description: "Remove files or directories",
@@ -8,69 +8,9 @@ import type {
8
8
  ShellEnv,
9
9
  } from "../types/commands";
10
10
  import { expandAsync } from "../utils/expand";
11
+ import { tokenizeCommand } from "../utils/tokenize";
11
12
  import { resolveModule } from "./registry";
12
13
 
13
- // ── Tokenize command input respecting quotes ──────────────────────────────────
14
- function tokenizeCommand(input: string): string[] {
15
- const tokens: string[] = [];
16
- let current = "";
17
- let inQ = false;
18
- let qChar = "";
19
- let i = 0;
20
-
21
- while (i < input.length) {
22
- const ch = input[i]!;
23
- const next = input[i + 1];
24
-
25
- if ((ch === '"' || ch === "'") && !inQ) {
26
- inQ = true;
27
- qChar = ch;
28
- i++;
29
- continue;
30
- }
31
- if (inQ && ch === qChar) {
32
- inQ = false;
33
- qChar = "";
34
- i++;
35
- continue;
36
- }
37
- if (inQ) {
38
- current += ch;
39
- i++;
40
- continue;
41
- }
42
-
43
- if (ch === " ") {
44
- if (current) {
45
- tokens.push(current);
46
- current = "";
47
- }
48
- i++;
49
- continue;
50
- }
51
-
52
- if ((ch === ">" || ch === "<") && !inQ) {
53
- if (current) {
54
- tokens.push(current);
55
- current = "";
56
- }
57
- if (ch === ">" && next === ">") {
58
- tokens.push(">>");
59
- i += 2;
60
- } else {
61
- tokens.push(ch);
62
- i++;
63
- }
64
- continue;
65
- }
66
-
67
- current += ch;
68
- i++;
69
- }
70
- if (current) tokens.push(current);
71
- return tokens;
72
- }
73
-
74
14
  export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
75
15
  return {
76
16
  vars: {
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
2
2
  import { getFlag, ifFlag } from "./command-helpers";
3
3
  import { resolvePath } from "./helpers";
4
4
 
5
+ /**
6
+ * Stream editor for filtering and transforming text lines.
7
+ * @category text
8
+ * @params ["-e <expr> [file]", "s/pattern/replace/[g]"]
9
+ */
5
10
  export const sedCommand: ShellModule = {
6
11
  name: "sed",
7
12
  description: "Stream editor for filtering and transforming text",
@@ -1,30 +1,11 @@
1
1
  /** biome-ignore-all lint/style/useNamingConvention: env variables */
2
2
  import type { ShellModule } from "../types/commands";
3
3
 
4
- // Legacy global store kept for compatibility with older callers
5
- const _globalEnv: Record<string, string> = {
6
- PATH: "/usr/local/bin:/usr/bin:/bin",
7
- HOME: "/home/user",
8
- SHELL: "/bin/sh",
9
- TERM: "xterm-256color",
10
- USER: "user",
11
- };
12
-
13
- /** @deprecated use env.vars from CommandContext */
14
- export function getEnvVar(name: string): string | undefined {
15
- return _globalEnv[name];
16
- }
17
- /** @deprecated use env.vars from CommandContext */
18
- export function setEnvVar(name: string, value: string): void {
19
- _globalEnv[name] = value;
20
- }
21
- /** @deprecated use env.vars from CommandContext */
22
- export function getAllEnvVars(authUser: string): Record<string, string> {
23
- _globalEnv.USER = authUser;
24
- _globalEnv.HOME = `/home/${authUser}`;
25
- return { ..._globalEnv };
26
- }
27
-
4
+ /**
5
+ * Display or set shell variables and options.
6
+ * @category shell
7
+ * @params ["[VAR=value]"]
8
+ */
28
9
  export const setCommand: ShellModule = {
29
10
  name: "set",
30
11
  description: "Display or set shell variables",
@@ -1,7 +1,7 @@
1
1
  import type {
2
- CommandContext,
3
- CommandResult,
4
- ShellModule,
2
+ CommandContext,
3
+ CommandResult,
4
+ ShellModule,
5
5
  } from "../types/commands";
6
6
  import { expandAsync } from "../utils/expand";
7
7
  import { ifFlag } from "./command-helpers";
@@ -286,6 +286,12 @@ async function runBlocks(
286
286
  return { ...lastResult, stdout: output.trim() || lastResult.stdout };
287
287
  }
288
288
 
289
+ /**
290
+ * Execute shell scripts or commands with a minimal shell interpreter.
291
+ * Supports if/elif/else, for loops, while loops, and variable expansion.
292
+ * @category shell
293
+ * @params ["-c <script>", "[<file>]"]
294
+ */
289
295
  export const shCommand: ShellModule = {
290
296
  name: "sh",
291
297
  aliases: ["bash"],