typescript-virtual-container 1.2.4 → 1.2.6

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 (267) hide show
  1. package/README.md +1056 -1239
  2. package/benchmark-results.txt +20 -20
  3. package/dist/SSHMimic/exec.js +2 -2
  4. package/dist/SSHMimic/executor.d.ts +6 -7
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +77 -60
  7. package/dist/SSHMimic/index.d.ts +19 -2
  8. package/dist/SSHMimic/index.d.ts.map +1 -1
  9. package/dist/SSHMimic/index.js +106 -24
  10. package/dist/SSHMimic/sftp.d.ts.map +1 -1
  11. package/dist/SSHMimic/sftp.js +14 -0
  12. package/dist/VirtualFileSystem/index.d.ts +115 -88
  13. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  14. package/dist/VirtualFileSystem/index.js +389 -264
  15. package/dist/VirtualShell/index.d.ts +3 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +4 -6
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +19 -2
  20. package/dist/VirtualShell/shellParser.d.ts +20 -2
  21. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  22. package/dist/VirtualShell/shellParser.js +229 -120
  23. package/dist/VirtualUserManager/index.d.ts +25 -0
  24. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  25. package/dist/VirtualUserManager/index.js +33 -0
  26. package/dist/commands/adduser.d.ts.map +1 -1
  27. package/dist/commands/adduser.js +2 -0
  28. package/dist/commands/awk.d.ts +3 -0
  29. package/dist/commands/awk.d.ts.map +1 -0
  30. package/dist/commands/awk.js +29 -0
  31. package/dist/commands/base64.d.ts +3 -0
  32. package/dist/commands/base64.d.ts.map +1 -0
  33. package/dist/commands/base64.js +20 -0
  34. package/dist/commands/cat.d.ts.map +1 -1
  35. package/dist/commands/cat.js +2 -0
  36. package/dist/commands/cd.d.ts.map +1 -1
  37. package/dist/commands/cd.js +2 -0
  38. package/dist/commands/chmod.d.ts +3 -0
  39. package/dist/commands/chmod.d.ts.map +1 -0
  40. package/dist/commands/chmod.js +33 -0
  41. package/dist/commands/clear.d.ts.map +1 -1
  42. package/dist/commands/clear.js +4 -1
  43. package/dist/commands/cp.d.ts +3 -0
  44. package/dist/commands/cp.d.ts.map +1 -0
  45. package/dist/commands/cp.js +70 -0
  46. package/dist/commands/curl.d.ts.map +1 -1
  47. package/dist/commands/curl.js +2 -0
  48. package/dist/commands/cut.d.ts +3 -0
  49. package/dist/commands/cut.d.ts.map +1 -0
  50. package/dist/commands/cut.js +27 -0
  51. package/dist/commands/date.d.ts +3 -0
  52. package/dist/commands/date.d.ts.map +1 -0
  53. package/dist/commands/date.js +22 -0
  54. package/dist/commands/deluser.d.ts.map +1 -1
  55. package/dist/commands/deluser.js +2 -0
  56. package/dist/commands/df.d.ts +3 -0
  57. package/dist/commands/df.d.ts.map +1 -0
  58. package/dist/commands/df.js +16 -0
  59. package/dist/commands/diff.d.ts +3 -0
  60. package/dist/commands/diff.d.ts.map +1 -0
  61. package/dist/commands/diff.js +40 -0
  62. package/dist/commands/du.d.ts +3 -0
  63. package/dist/commands/du.d.ts.map +1 -0
  64. package/dist/commands/du.js +39 -0
  65. package/dist/commands/echo.d.ts.map +1 -1
  66. package/dist/commands/echo.js +2 -0
  67. package/dist/commands/env.d.ts.map +1 -1
  68. package/dist/commands/env.js +6 -14
  69. package/dist/commands/export.d.ts.map +1 -1
  70. package/dist/commands/export.js +11 -21
  71. package/dist/commands/find.d.ts +3 -0
  72. package/dist/commands/find.d.ts.map +1 -0
  73. package/dist/commands/find.js +50 -0
  74. package/dist/commands/grep.d.ts.map +1 -1
  75. package/dist/commands/grep.js +58 -35
  76. package/dist/commands/groups.d.ts +3 -0
  77. package/dist/commands/groups.d.ts.map +1 -0
  78. package/dist/commands/groups.js +12 -0
  79. package/dist/commands/gzip.d.ts +4 -0
  80. package/dist/commands/gzip.d.ts.map +1 -0
  81. package/dist/commands/gzip.js +40 -0
  82. package/dist/commands/head.d.ts +3 -0
  83. package/dist/commands/head.d.ts.map +1 -0
  84. package/dist/commands/head.js +32 -0
  85. package/dist/commands/help.d.ts +1 -1
  86. package/dist/commands/help.d.ts.map +1 -1
  87. package/dist/commands/help.js +75 -3
  88. package/dist/commands/hostname.d.ts.map +1 -1
  89. package/dist/commands/hostname.js +2 -0
  90. package/dist/commands/htop.d.ts.map +1 -1
  91. package/dist/commands/htop.js +2 -0
  92. package/dist/commands/id.d.ts +3 -0
  93. package/dist/commands/id.d.ts.map +1 -0
  94. package/dist/commands/id.js +14 -0
  95. package/dist/commands/index.d.ts +5 -2
  96. package/dist/commands/index.d.ts.map +1 -1
  97. package/dist/commands/index.js +104 -87
  98. package/dist/commands/kill.d.ts +3 -0
  99. package/dist/commands/kill.d.ts.map +1 -0
  100. package/dist/commands/kill.js +13 -0
  101. package/dist/commands/ln.d.ts +3 -0
  102. package/dist/commands/ln.d.ts.map +1 -0
  103. package/dist/commands/ln.js +44 -0
  104. package/dist/commands/ls.d.ts.map +1 -1
  105. package/dist/commands/ls.js +2 -0
  106. package/dist/commands/mkdir.d.ts.map +1 -1
  107. package/dist/commands/mkdir.js +2 -0
  108. package/dist/commands/mv.d.ts +3 -0
  109. package/dist/commands/mv.d.ts.map +1 -0
  110. package/dist/commands/mv.js +37 -0
  111. package/dist/commands/nano.d.ts.map +1 -1
  112. package/dist/commands/nano.js +2 -0
  113. package/dist/commands/neofetch.d.ts.map +1 -1
  114. package/dist/commands/neofetch.js +2 -0
  115. package/dist/commands/passwd.d.ts.map +1 -1
  116. package/dist/commands/passwd.js +2 -0
  117. package/dist/commands/ping.d.ts +3 -0
  118. package/dist/commands/ping.d.ts.map +1 -0
  119. package/dist/commands/ping.js +18 -0
  120. package/dist/commands/ps.d.ts +3 -0
  121. package/dist/commands/ps.d.ts.map +1 -0
  122. package/dist/commands/ps.js +17 -0
  123. package/dist/commands/pwd.d.ts.map +1 -1
  124. package/dist/commands/pwd.js +2 -0
  125. package/dist/commands/rm.d.ts.map +1 -1
  126. package/dist/commands/rm.js +2 -0
  127. package/dist/commands/sed.d.ts +3 -0
  128. package/dist/commands/sed.d.ts.map +1 -0
  129. package/dist/commands/sed.js +47 -0
  130. package/dist/commands/set.d.ts +3 -0
  131. package/dist/commands/set.d.ts.map +1 -1
  132. package/dist/commands/set.js +19 -46
  133. package/dist/commands/sh.d.ts +0 -1
  134. package/dist/commands/sh.d.ts.map +1 -1
  135. package/dist/commands/sh.js +228 -35
  136. package/dist/commands/sleep.d.ts +3 -0
  137. package/dist/commands/sleep.d.ts.map +1 -0
  138. package/dist/commands/sleep.js +13 -0
  139. package/dist/commands/sort.d.ts +3 -0
  140. package/dist/commands/sort.d.ts.map +1 -0
  141. package/dist/commands/sort.js +37 -0
  142. package/dist/commands/su.d.ts.map +1 -1
  143. package/dist/commands/su.js +2 -0
  144. package/dist/commands/sudo.d.ts.map +1 -1
  145. package/dist/commands/sudo.js +2 -0
  146. package/dist/commands/tail.d.ts +3 -0
  147. package/dist/commands/tail.d.ts.map +1 -0
  148. package/dist/commands/tail.js +35 -0
  149. package/dist/commands/tar.d.ts +3 -0
  150. package/dist/commands/tar.d.ts.map +1 -0
  151. package/dist/commands/tar.js +64 -0
  152. package/dist/commands/tee.d.ts +3 -0
  153. package/dist/commands/tee.d.ts.map +1 -0
  154. package/dist/commands/tee.js +29 -0
  155. package/dist/commands/touch.d.ts.map +1 -1
  156. package/dist/commands/touch.js +2 -0
  157. package/dist/commands/tr.d.ts +3 -0
  158. package/dist/commands/tr.d.ts.map +1 -0
  159. package/dist/commands/tr.js +24 -0
  160. package/dist/commands/tree.d.ts.map +1 -1
  161. package/dist/commands/tree.js +2 -0
  162. package/dist/commands/uname.d.ts +3 -0
  163. package/dist/commands/uname.d.ts.map +1 -0
  164. package/dist/commands/uname.js +21 -0
  165. package/dist/commands/uniq.d.ts +3 -0
  166. package/dist/commands/uniq.d.ts.map +1 -0
  167. package/dist/commands/uniq.js +33 -0
  168. package/dist/commands/unset.d.ts.map +1 -1
  169. package/dist/commands/unset.js +6 -10
  170. package/dist/commands/wc.d.ts +3 -0
  171. package/dist/commands/wc.d.ts.map +1 -0
  172. package/dist/commands/wc.js +50 -0
  173. package/dist/commands/wget.d.ts.map +1 -1
  174. package/dist/commands/wget.js +2 -0
  175. package/dist/commands/who.d.ts.map +1 -1
  176. package/dist/commands/who.js +2 -0
  177. package/dist/commands/whoami.d.ts.map +1 -1
  178. package/dist/commands/whoami.js +2 -0
  179. package/dist/commands/xargs.d.ts +3 -0
  180. package/dist/commands/xargs.d.ts.map +1 -0
  181. package/dist/commands/xargs.js +16 -0
  182. package/dist/index.d.ts +1 -0
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/types/commands.d.ts +13 -0
  185. package/dist/types/commands.d.ts.map +1 -1
  186. package/dist/types/pipeline.d.ts +20 -0
  187. package/dist/types/pipeline.d.ts.map +1 -1
  188. package/package.json +5 -2
  189. package/scripts/publish-package.sh +70 -0
  190. package/src/SSHMimic/exec.ts +2 -2
  191. package/src/SSHMimic/executor.ts +95 -98
  192. package/src/SSHMimic/index.ts +138 -57
  193. package/src/SSHMimic/sftp.ts +15 -0
  194. package/src/VirtualFileSystem/index.ts +464 -292
  195. package/src/VirtualShell/index.ts +4 -6
  196. package/src/VirtualShell/shell.ts +19 -2
  197. package/src/VirtualShell/shellParser.ts +202 -168
  198. package/src/VirtualUserManager/index.ts +36 -0
  199. package/src/commands/adduser.ts +2 -0
  200. package/src/commands/awk.ts +30 -0
  201. package/src/commands/base64.ts +18 -0
  202. package/src/commands/cat.ts +2 -0
  203. package/src/commands/cd.ts +2 -0
  204. package/src/commands/chmod.ts +35 -0
  205. package/src/commands/clear.ts +4 -1
  206. package/src/commands/cp.ts +78 -0
  207. package/src/commands/curl.ts +2 -0
  208. package/src/commands/cut.ts +29 -0
  209. package/src/commands/date.ts +24 -0
  210. package/src/commands/deluser.ts +2 -0
  211. package/src/commands/df.ts +18 -0
  212. package/src/commands/diff.ts +29 -0
  213. package/src/commands/du.ts +39 -0
  214. package/src/commands/echo.ts +2 -0
  215. package/src/commands/env.ts +6 -16
  216. package/src/commands/export.ts +11 -24
  217. package/src/commands/find.ts +63 -0
  218. package/src/commands/grep.ts +51 -38
  219. package/src/commands/groups.ts +14 -0
  220. package/src/commands/gzip.ts +31 -0
  221. package/src/commands/head.ts +37 -0
  222. package/src/commands/help.ts +81 -3
  223. package/src/commands/hostname.ts +2 -0
  224. package/src/commands/htop.ts +2 -0
  225. package/src/commands/id.ts +16 -0
  226. package/src/commands/index.ts +114 -133
  227. package/src/commands/kill.ts +14 -0
  228. package/src/commands/ln.ts +49 -0
  229. package/src/commands/ls.ts +2 -0
  230. package/src/commands/mkdir.ts +2 -0
  231. package/src/commands/mv.ts +45 -0
  232. package/src/commands/nano.ts +2 -0
  233. package/src/commands/neofetch.ts +2 -0
  234. package/src/commands/passwd.ts +2 -0
  235. package/src/commands/ping.ts +20 -0
  236. package/src/commands/ps.ts +19 -0
  237. package/src/commands/pwd.ts +2 -0
  238. package/src/commands/rm.ts +2 -0
  239. package/src/commands/sed.ts +45 -0
  240. package/src/commands/set.ts +19 -50
  241. package/src/commands/sh.ts +192 -43
  242. package/src/commands/sleep.ts +14 -0
  243. package/src/commands/sort.ts +37 -0
  244. package/src/commands/su.ts +2 -0
  245. package/src/commands/sudo.ts +2 -0
  246. package/src/commands/tail.ts +39 -0
  247. package/src/commands/tar.ts +58 -0
  248. package/src/commands/tee.ts +25 -0
  249. package/src/commands/touch.ts +2 -0
  250. package/src/commands/tr.ts +24 -0
  251. package/src/commands/tree.ts +2 -0
  252. package/src/commands/uname.ts +20 -0
  253. package/src/commands/uniq.ts +28 -0
  254. package/src/commands/unset.ts +5 -12
  255. package/src/commands/wc.ts +50 -0
  256. package/src/commands/wget.ts +2 -0
  257. package/src/commands/who.ts +2 -0
  258. package/src/commands/whoami.ts +2 -0
  259. package/src/commands/xargs.ts +17 -0
  260. package/src/index.ts +1 -0
  261. package/src/types/commands.ts +14 -0
  262. package/src/types/pipeline.ts +23 -0
  263. package/standalone.js +93 -55
  264. package/standalone.js.map +4 -4
  265. package/tests/bun-test-shim.ts +1 -0
  266. package/tests/sftp.test.ts +115 -191
  267. package/tests/users.test.ts +42 -88
@@ -1,9 +1,87 @@
1
1
  import type { ShellModule } from "../types/commands";
2
+ import { getCommandModulesPublic } from "./index";
2
3
 
3
- export function createHelpCommand(getNames: () => string[]): ShellModule {
4
+ const CATEGORY_ORDER = ["navigation", "files", "text", "archive", "system", "network", "shell", "users", "misc"];
5
+ const CATEGORY_LABELS: Record<string, string> = {
6
+ navigation: "Navigation",
7
+ files: "Files & Filesystem",
8
+ text: "Text Processing",
9
+ archive: "Archive & Compression",
10
+ system: "System",
11
+ network: "Network",
12
+ shell: "Shell",
13
+ users: "Users & Permissions",
14
+ misc: "Miscellaneous",
15
+ };
16
+
17
+ function padRight(s: string, n: number): string {
18
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
19
+ }
20
+
21
+ export function createHelpCommand(_getNames: () => string[]): ShellModule {
4
22
  return {
5
23
  name: "help",
6
- params: [],
7
- run: () => ({ stdout: `Builtins: ${getNames().join(" ")}`, exitCode: 0 }),
24
+ description: "Display this help message",
25
+ category: "shell",
26
+ params: ["[command]"],
27
+ run: ({ args }) => {
28
+ const modules = getCommandModulesPublic();
29
+
30
+ // help <command>
31
+ if (args[0]) {
32
+ const mod = modules.find((m) => m.name === args[0] || m.aliases?.includes(args[0]!));
33
+ if (!mod) return { stderr: `help: no help for '${args[0]}'`, exitCode: 1 };
34
+ const aliases = mod.aliases?.length ? ` aliases: ${mod.aliases.join(", ")}\n` : "";
35
+ const params = mod.params.map((p) => ` ${mod.name} ${p}`).join("\n");
36
+ return {
37
+ stdout: [
38
+ `\x1b[1m${mod.name}\x1b[0m — ${mod.description ?? "no description"}`,
39
+ aliases,
40
+ "Usage:",
41
+ params || ` ${mod.name}`,
42
+ ].filter(Boolean).join("\n"),
43
+ exitCode: 0,
44
+ };
45
+ }
46
+
47
+ // Full help — grouped by category
48
+ const grouped: Record<string, ShellModule[]> = {};
49
+ for (const mod of modules) {
50
+ const cat = mod.category ?? "misc";
51
+ if (!grouped[cat]) grouped[cat] = [];
52
+ grouped[cat]!.push(mod);
53
+ }
54
+
55
+ const lines: string[] = [];
56
+ lines.push("\x1b[1mAvailable commands\x1b[0m");
57
+ lines.push("");
58
+
59
+ const cats = [
60
+ ...CATEGORY_ORDER.filter((c) => grouped[c]),
61
+ ...Object.keys(grouped).filter((c) => !CATEGORY_ORDER.includes(c)),
62
+ ];
63
+
64
+ for (const cat of cats) {
65
+ const mods = grouped[cat];
66
+ if (!mods || mods.length === 0) continue;
67
+ lines.push(`\x1b[33m${CATEGORY_LABELS[cat] ?? cat}\x1b[0m`);
68
+
69
+ // Two-column layout
70
+ const sorted = [...mods].sort((a, b) => a.name.localeCompare(b.name));
71
+ for (let i = 0; i < sorted.length; i += 2) {
72
+ const left = sorted[i]!;
73
+ const right = sorted[i + 1];
74
+ const leftStr = ` \x1b[36m${padRight(left.name, 14)}\x1b[0m ${left.description ?? ""}`;
75
+ const rightStr = right
76
+ ? ` \x1b[36m${padRight(right.name, 14)}\x1b[0m ${right.description ?? ""}`
77
+ : "";
78
+ lines.push(rightStr ? `${leftStr.padEnd(44)}${rightStr}` : leftStr);
79
+ }
80
+ lines.push("");
81
+ }
82
+
83
+ lines.push("Type \x1b[1mhelp <command>\x1b[0m for usage details.");
84
+ return { stdout: lines.join("\n"), exitCode: 0 };
85
+ },
8
86
  };
9
87
  }
@@ -2,6 +2,8 @@ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const hostnameCommand: ShellModule = {
4
4
  name: "hostname",
5
+ description: "Print hostname",
6
+ category: "system",
5
7
  params: [],
6
8
  run: ({ hostname }) => ({ stdout: hostname, exitCode: 0 }),
7
9
  };
@@ -2,6 +2,8 @@ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const htopCommand: ShellModule = {
4
4
  name: "htop",
5
+ description: "System monitor",
6
+ category: "system",
5
7
  params: [],
6
8
  run: ({ mode }) => {
7
9
  if (mode === "exec") {
@@ -0,0 +1,16 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ export const idCommand: ShellModule = {
4
+ name: "id",
5
+ description: "Print user identity",
6
+ category: "system",
7
+ params: ["[user]"],
8
+ run: ({ authUser, shell, args }) => {
9
+ const target = args[0] ?? authUser;
10
+ const uid = target === "root" ? 0 : 1000;
11
+ const gid = uid;
12
+ const isSudo = shell.users.isSudoer(target);
13
+ const groups = isSudo ? `${gid}(${target}),0(root)` : `${gid}(${target})`;
14
+ return { stdout: `uid=${uid}(${target}) gid=${gid}(${target}) groups=${groups}`, exitCode: 0 };
15
+ },
16
+ };
@@ -1,102 +1,118 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
1
2
  import type { VirtualShell } from "../VirtualShell";
2
3
  import type {
3
4
  CommandContext,
4
5
  CommandMode,
5
6
  CommandResult,
7
+ ShellEnv,
6
8
  ShellModule,
7
9
  } from "../types/commands";
8
10
  import { adduserCommand } from "./adduser";
11
+ import { awkCommand } from "./awk";
12
+ import { base64Command } from "./base64";
9
13
  import { catCommand } from "./cat";
10
14
  import { cdCommand } from "./cd";
15
+ import { chmodCommand } from "./chmod";
11
16
  import { clearCommand } from "./clear";
17
+ import { cpCommand } from "./cp";
12
18
  import { curlCommand } from "./curl";
19
+ import { cutCommand } from "./cut";
20
+ import { dateCommand } from "./date";
13
21
  import { deluserCommand } from "./deluser";
22
+ import { dfCommand } from "./df";
23
+ import { diffCommand } from "./diff";
24
+ import { duCommand } from "./du";
14
25
  import { echoCommand } from "./echo";
15
26
  import { envCommand } from "./env";
16
27
  import { exitCommand } from "./exit";
17
28
  import { exportCommand } from "./export";
29
+ import { findCommand } from "./find";
18
30
  import { grepCommand } from "./grep";
31
+ import { groupsCommand } from "./groups";
32
+ import { gunzipCommand, gzipCommand } from "./gzip";
33
+ import { headCommand } from "./head";
19
34
  import { createHelpCommand } from "./help";
20
35
  import { hostnameCommand } from "./hostname";
21
36
  import { htopCommand } from "./htop";
37
+ import { idCommand } from "./id";
38
+ import { killCommand } from "./kill";
39
+ import { lnCommand } from "./ln";
22
40
  import { lsCommand } from "./ls";
23
41
  import { mkdirCommand } from "./mkdir";
42
+ import { mvCommand } from "./mv";
24
43
  import { nanoCommand } from "./nano";
25
44
  import { neofetchCommand } from "./neofetch";
26
45
  import { passwdCommand } from "./passwd";
46
+ import { pingCommand } from "./ping";
47
+ import { psCommand } from "./ps";
27
48
  import { pwdCommand } from "./pwd";
28
49
  import { rmCommand } from "./rm";
50
+ import { sedCommand } from "./sed";
29
51
  import { setCommand } from "./set";
30
52
  import { shCommand } from "./sh";
53
+ import { sleepCommand } from "./sleep";
54
+ import { sortCommand } from "./sort";
31
55
  import { suCommand } from "./su";
32
56
  import { sudoCommand } from "./sudo";
57
+ import { tailCommand } from "./tail";
58
+ import { tarCommand } from "./tar";
59
+ import { teeCommand } from "./tee";
33
60
  import { touchCommand } from "./touch";
61
+ import { trCommand } from "./tr";
34
62
  import { treeCommand } from "./tree";
63
+ import { unameCommand } from "./uname";
64
+ import { uniqCommand } from "./uniq";
35
65
  import { unsetCommand } from "./unset";
66
+ import { wcCommand } from "./wc";
36
67
  import { wgetCommand } from "./wget";
37
68
  import { whoCommand } from "./who";
38
69
  import { whoamiCommand } from "./whoami";
70
+ import { xargsCommand } from "./xargs";
39
71
 
40
72
  const BASE_COMMANDS: ShellModule[] = [
41
- pwdCommand,
42
- whoamiCommand,
43
- whoCommand,
44
- hostnameCommand,
45
- lsCommand,
46
- cdCommand,
47
- catCommand,
48
- echoCommand,
49
- mkdirCommand,
50
- touchCommand,
51
- rmCommand,
52
- treeCommand,
53
- nanoCommand,
73
+ // Navigation
74
+ pwdCommand, cdCommand, lsCommand, treeCommand,
75
+ // Files
76
+ catCommand, touchCommand, rmCommand, mkdirCommand, cpCommand, mvCommand, lnCommand,
77
+ chmodCommand, findCommand,
78
+ // Text processing
79
+ grepCommand, sedCommand, awkCommand, sortCommand, uniqCommand, wcCommand,
80
+ headCommand, tailCommand, cutCommand, trCommand, teeCommand, xargsCommand,
81
+ diffCommand,
82
+ // Archives
83
+ tarCommand, gzipCommand, gunzipCommand, base64Command,
84
+ // System info
85
+ whoamiCommand, whoCommand, hostnameCommand, idCommand, groupsCommand, unameCommand,
86
+ psCommand, killCommand, dfCommand, duCommand, dateCommand, sleepCommand, pingCommand,
87
+ // Shell
88
+ echoCommand, envCommand, exportCommand, setCommand, unsetCommand, shCommand,
89
+ clearCommand, exitCommand,
90
+ // Editors
91
+ nanoCommand, htopCommand,
92
+ // Network
93
+ curlCommand, wgetCommand,
94
+ // Users
95
+ adduserCommand, passwdCommand, deluserCommand, sudoCommand, suCommand,
96
+ // Misc
54
97
  neofetchCommand,
55
- htopCommand,
56
- adduserCommand,
57
- passwdCommand,
58
- deluserCommand,
59
- sudoCommand,
60
- suCommand,
61
- curlCommand,
62
- envCommand,
63
- wgetCommand,
64
- grepCommand,
65
- exportCommand,
66
- setCommand,
67
- unsetCommand,
68
- shCommand,
69
- clearCommand,
70
- exitCommand,
71
98
  ];
72
99
 
73
100
  const customCommands: ShellModule[] = [];
74
-
75
- const helpCommand = createHelpCommand(() =>
76
- getCommandModules().map((cmd) => cmd.name),
77
- );
78
-
79
101
  const commandRegistry = new Map<string, ShellModule>();
80
102
  let cachedCommandNames: string[] | null = null;
81
103
 
104
+ const helpCommand = createHelpCommand(() => getCommandModules().map((cmd) => cmd.name));
105
+
82
106
  function buildCache(): void {
107
+ commandRegistry.clear();
83
108
  for (const mod of getCommandModules()) {
84
109
  commandRegistry.set(mod.name, mod);
85
- for (const alias of mod.aliases ?? []) {
86
- commandRegistry.set(alias, mod);
87
- }
110
+ for (const alias of mod.aliases ?? []) commandRegistry.set(alias, mod);
88
111
  }
89
112
  cachedCommandNames = Array.from(commandRegistry.keys()).sort();
90
113
  }
91
114
 
92
115
  function getCommandModules(): ShellModule[] {
93
- // console.log("Loading command modules...");
94
- // console.log(
95
- // `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
96
- // );
97
- // console.log(
98
- // `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
99
- // );
100
116
  return [...BASE_COMMANDS, ...customCommands, helpCommand];
101
117
  }
102
118
 
@@ -104,23 +120,13 @@ export function registerCommand(module: ShellModule): void {
104
120
  const normalized: ShellModule = {
105
121
  ...module,
106
122
  name: module.name.trim().toLowerCase(),
107
- aliases: module.aliases?.map((alias) => alias.trim().toLowerCase()),
123
+ aliases: module.aliases?.map((a) => a.trim().toLowerCase()),
108
124
  };
109
-
110
125
  const names = [normalized.name, ...(normalized.aliases ?? [])];
111
- if (names.some((name) => name.length === 0 || /\s/.test(name))) {
112
- throw new Error(
113
- "Command names and aliases must be non-empty and contain no spaces",
114
- );
115
- }
116
-
117
- for (const name of names) {
118
- if (commandRegistry.has(name)) {
119
- throw new Error(`Command '${name}' already exists`);
120
- }
121
- commandRegistry.set(name, normalized);
126
+ if (names.some((n) => n.length === 0 || /\s/.test(n))) {
127
+ throw new Error("Command names must be non-empty and contain no spaces");
122
128
  }
123
-
129
+ customCommands.push(normalized);
124
130
  buildCache();
125
131
  }
126
132
 
@@ -129,24 +135,20 @@ export function createCustomCommand(
129
135
  params: string[],
130
136
  run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
131
137
  ): ShellModule {
132
- return {
133
- name,
134
- params,
135
- run,
136
- };
138
+ return { name, params, run };
137
139
  }
138
140
 
139
141
  export function getCommandNames(): string[] {
140
- if (!cachedCommandNames) {
141
- buildCache();
142
- }
142
+ if (!cachedCommandNames) buildCache();
143
143
  return cachedCommandNames!;
144
144
  }
145
145
 
146
+ export function getCommandModulesPublic(): ShellModule[] {
147
+ return getCommandModules();
148
+ }
149
+
146
150
  export function resolveModule(name: string): ShellModule | undefined {
147
- if (!cachedCommandNames) {
148
- buildCache();
149
- }
151
+ if (!cachedCommandNames) buildCache();
150
152
  return commandRegistry.get(name.toLowerCase());
151
153
  }
152
154
 
@@ -156,52 +158,44 @@ function splitArgsRespectingQuotes(input: string): string[] {
156
158
  let inQuotes = false;
157
159
  let quoteChar = "";
158
160
 
159
- for (let i = 0; i < input.length; i += 1) {
161
+ for (let i = 0; i < input.length; i++) {
160
162
  const ch = input[i] || "";
161
163
  const prev = i > 0 ? input[i - 1] : "";
162
-
163
164
  if ((ch === '"' || ch === "'") && prev !== "\\") {
164
- if (!inQuotes) {
165
- inQuotes = true;
166
- quoteChar = ch;
167
- continue;
168
- }
169
-
170
- if (ch === quoteChar) {
171
- inQuotes = false;
172
- quoteChar = "";
173
- continue;
174
- }
165
+ if (!inQuotes) { inQuotes = true; quoteChar = ch; continue; }
166
+ if (ch === quoteChar) { inQuotes = false; quoteChar = ""; continue; }
175
167
  }
176
-
177
168
  if (/\s/.test(ch) && !inQuotes) {
178
- if (current.length > 0) {
179
- tokens.push(current);
180
- current = "";
181
- }
169
+ if (current.length > 0) { tokens.push(current); current = ""; }
182
170
  continue;
183
171
  }
184
-
185
172
  current += ch;
186
173
  }
187
-
188
- if (current.length > 0) {
189
- tokens.push(current);
190
- }
191
-
174
+ if (current.length > 0) tokens.push(current);
192
175
  return tokens;
193
176
  }
194
177
 
195
178
  function parseInput(rawInput: string): { commandName: string; args: string[] } {
196
179
  const parts = splitArgsRespectingQuotes(rawInput.trim());
180
+ return { commandName: parts[0]?.toLowerCase() ?? "", args: parts.slice(1) };
181
+ }
182
+
183
+ export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
197
184
  return {
198
- commandName: parts[0]?.toLowerCase() ?? "",
199
- args: parts.slice(1),
185
+ vars: {
186
+ PATH: "/usr/local/bin:/usr/bin:/bin",
187
+ HOME: `/home/${authUser}`,
188
+ USER: authUser,
189
+ LOGNAME: authUser,
190
+ SHELL: "/bin/sh",
191
+ TERM: "xterm-256color",
192
+ HOSTNAME: hostname,
193
+ PS1: "\\u@\\h:\\w\\$ ",
194
+ },
195
+ lastExitCode: 0,
200
196
  };
201
197
  }
202
198
 
203
- // Internal async function for pipeline execution
204
-
205
199
  export async function runCommand(
206
200
  rawInput: string,
207
201
  authUser: string,
@@ -210,50 +204,37 @@ export async function runCommand(
210
204
  cwd: string,
211
205
  shell: VirtualShell,
212
206
  stdin?: string,
207
+ env?: ShellEnv,
213
208
  ): Promise<CommandResult> {
214
209
  const trimmed = rawInput.trim();
215
-
216
- if (trimmed.length === 0) {
217
- return { exitCode: 0 };
218
- }
219
-
220
- if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
221
- const { parseShellPipeline } = await import("../VirtualShell/shellParser");
222
- const { executePipeline } = await import("../SSHMimic/executor");
223
-
224
- const pipeline = parseShellPipeline(trimmed);
225
- if (!pipeline.isValid) {
226
- return {
227
- stderr: pipeline.error || "Syntax error",
228
- exitCode: 1,
229
- };
230
- }
231
-
210
+ if (trimmed.length === 0) return { exitCode: 0 };
211
+
212
+ const shellEnv: ShellEnv = env ?? makeDefaultEnv(authUser, hostname);
213
+
214
+ // Detect shell operators
215
+ if (
216
+ /(?<![|&])[|](?![|])/.test(trimmed) ||
217
+ trimmed.includes(">") ||
218
+ trimmed.includes("<") ||
219
+ trimmed.includes("&&") ||
220
+ trimmed.includes("||") ||
221
+ trimmed.includes(";")
222
+ ) {
223
+ const { parseScript } = await import("../VirtualShell/shellParser");
224
+ const { executeStatements } = await import("../SSHMimic/executor");
225
+ const script = parseScript(trimmed);
226
+ if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
232
227
  try {
233
- return await executePipeline(
234
- pipeline,
235
- authUser,
236
- hostname,
237
- mode,
238
- cwd,
239
- shell,
240
- );
228
+ return await executeStatements(script.statements, authUser, hostname, mode, cwd, shell, shellEnv);
241
229
  } catch (error: unknown) {
242
- const message =
243
- error instanceof Error ? error.message : "Pipeline execution failed";
244
- return { stderr: message, exitCode: 1 };
230
+ return { stderr: error instanceof Error ? error.message : "Execution failed", exitCode: 1 };
245
231
  }
246
232
  }
247
233
 
248
234
  const { commandName, args } = parseInput(trimmed);
249
235
  const mod = resolveModule(commandName);
250
236
 
251
- if (!mod) {
252
- return {
253
- stderr: `Command '${trimmed}' not found`,
254
- exitCode: 127,
255
- };
256
- }
237
+ if (!mod) return { stderr: `${commandName}: command not found`, exitCode: 127 };
257
238
 
258
239
  try {
259
240
  return await mod.run({
@@ -266,9 +247,9 @@ export async function runCommand(
266
247
  stdin,
267
248
  cwd,
268
249
  shell,
250
+ env: shellEnv,
269
251
  });
270
252
  } catch (error: unknown) {
271
- const message = error instanceof Error ? error.message : "Command failed";
272
- return { stderr: message, exitCode: 1 };
253
+ return { stderr: error instanceof Error ? error.message : "Command failed", exitCode: 1 };
273
254
  }
274
255
  }
@@ -0,0 +1,14 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ export const killCommand: ShellModule = {
4
+ name: "kill",
5
+ description: "Send signal to process",
6
+ category: "system",
7
+ params: ["[-9] <pid>"],
8
+ run: ({ args }) => {
9
+ const pid = args.find((a) => !a.startsWith("-"));
10
+ if (!pid) return { stderr: "kill: no pid specified", exitCode: 1 };
11
+ // In virtual env, we just acknowledge the kill
12
+ return { stdout: ``, exitCode: 0 };
13
+ },
14
+ };
@@ -0,0 +1,49 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+ import { assertPathAccess, resolvePath } from "./helpers";
4
+
5
+ export const lnCommand: ShellModule = {
6
+ name: "ln",
7
+ description: "Create links",
8
+ category: "files",
9
+ params: ["[-s] <target> <link_name>"],
10
+ run: ({ authUser, shell, cwd, args }) => {
11
+ const symbolic = ifFlag(args, ["-s", "--symbolic"]);
12
+ const positionals = args.filter((a) => !a.startsWith("-"));
13
+ const [targetArg, linkArg] = positionals;
14
+
15
+ if (!targetArg || !linkArg) {
16
+ return { stderr: "ln: missing operand", exitCode: 1 };
17
+ }
18
+
19
+ const linkPath = resolvePath(cwd, linkArg);
20
+ const targetPath = symbolic
21
+ ? targetArg // keep relative for symlinks
22
+ : resolvePath(cwd, targetArg);
23
+
24
+ try {
25
+ assertPathAccess(authUser, linkPath, "ln");
26
+
27
+ if (!symbolic) {
28
+ // Hard link — copy file contents
29
+ const srcPath = resolvePath(cwd, targetArg);
30
+ assertPathAccess(authUser, srcPath, "ln");
31
+ if (!shell.vfs.exists(srcPath)) {
32
+ return {
33
+ stderr: `ln: ${targetArg}: No such file or directory`,
34
+ exitCode: 1,
35
+ };
36
+ }
37
+ const content = shell.vfs.readFile(srcPath);
38
+ shell.writeFileAsUser(authUser, linkPath, content);
39
+ } else {
40
+ shell.vfs.symlink(targetPath, linkPath);
41
+ }
42
+
43
+ return { exitCode: 0 };
44
+ } catch (err) {
45
+ const msg = err instanceof Error ? err.message : String(err);
46
+ return { stderr: `ln: ${msg}`, exitCode: 1 };
47
+ }
48
+ },
49
+ };
@@ -28,6 +28,8 @@ function formatDate(date: Date): string {
28
28
 
29
29
  export const lsCommand: ShellModule = {
30
30
  name: "ls",
31
+ description: "List directory contents",
32
+ category: "navigation",
31
33
  params: ["[path]"],
32
34
  run: ({ authUser, shell, cwd, args }) => {
33
35
  const longFormat = ifFlag(args, ["-l", "--long"]);
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const mkdirCommand: ShellModule = {
6
6
  name: "mkdir",
7
+ description: "Make directories",
8
+ category: "files",
7
9
  params: ["<dir>"],
8
10
  run: ({ authUser, shell, cwd, args }) => {
9
11
  if (args.length === 0) {
@@ -0,0 +1,45 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { assertPathAccess, resolvePath } from "./helpers";
3
+
4
+ export const mvCommand: ShellModule = {
5
+ name: "mv",
6
+ description: "Move or rename files",
7
+ category: "files",
8
+ params: ["<source> <dest>"],
9
+ run: ({ authUser, shell, cwd, args }) => {
10
+ const positionals = args.filter((a) => !a.startsWith("-"));
11
+ const [srcArg, destArg] = positionals;
12
+
13
+ if (!srcArg || !destArg) {
14
+ return { stderr: "mv: missing operand", exitCode: 1 };
15
+ }
16
+
17
+ const srcPath = resolvePath(cwd, srcArg);
18
+ const destPath = resolvePath(cwd, destArg);
19
+
20
+ try {
21
+ assertPathAccess(authUser, srcPath, "mv");
22
+ assertPathAccess(authUser, destPath, "mv");
23
+
24
+ if (!shell.vfs.exists(srcPath)) {
25
+ return {
26
+ stderr: `mv: ${srcArg}: No such file or directory`,
27
+ exitCode: 1,
28
+ };
29
+ }
30
+
31
+ // If dest is a directory, move into it
32
+ const finalDest =
33
+ shell.vfs.exists(destPath) &&
34
+ shell.vfs.stat(destPath).type === "directory"
35
+ ? `${destPath}/${srcArg.split("/").pop()}`
36
+ : destPath;
37
+
38
+ shell.vfs.move(srcPath, finalDest);
39
+ return { exitCode: 0 };
40
+ } catch (err) {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ return { stderr: `mv: ${msg}`, exitCode: 1 };
43
+ }
44
+ },
45
+ };
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const nanoCommand: ShellModule = {
6
6
  name: "nano",
7
+ description: "Text editor",
8
+ category: "shell",
7
9
  params: ["<file>"],
8
10
  run: ({ authUser, shell, cwd, args }) => {
9
11
  const fileArg = args[0];
@@ -5,6 +5,8 @@ import { getAllEnvVars } from "./set";
5
5
 
6
6
  export const neofetchCommand: ShellModule = {
7
7
  name: "neofetch",
8
+ description: "System info display",
9
+ category: "misc",
8
10
  params: ["[--off]"],
9
11
  run: ({ args, authUser, hostname, shell }) => {
10
12
  const env = getAllEnvVars(authUser);
@@ -2,6 +2,8 @@ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const passwdCommand: ShellModule = {
4
4
  name: "passwd",
5
+ description: "Change user password",
6
+ category: "users",
5
7
  params: ["<username> <password>"],
6
8
  run: async ({ authUser, args, shell }) => {
7
9
  const [username, password] = args;