typescript-virtual-container 1.2.9 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +141 -50
  3. package/biome.json +7 -0
  4. package/dist/SSHMimic/exec.d.ts.map +1 -1
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +32 -16
  7. package/dist/SSHMimic/index.d.ts.map +1 -1
  8. package/dist/SSHMimic/index.js +20 -6
  9. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/binaryPack.js +29 -6
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +36 -13
  13. package/dist/VirtualPackageManager/index.d.ts.map +1 -1
  14. package/dist/VirtualPackageManager/index.js +192 -43
  15. package/dist/VirtualShell/index.d.ts +10 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +18 -7
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +3 -1
  20. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  21. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  22. package/dist/commands/adduser.d.ts +6 -0
  23. package/dist/commands/adduser.d.ts.map +1 -1
  24. package/dist/commands/adduser.js +6 -0
  25. package/dist/commands/alias.d.ts +5 -0
  26. package/dist/commands/alias.d.ts.map +1 -1
  27. package/dist/commands/alias.js +5 -0
  28. package/dist/commands/apt.d.ts +5 -0
  29. package/dist/commands/apt.d.ts.map +1 -1
  30. package/dist/commands/apt.js +32 -9
  31. package/dist/commands/awk.d.ts +11 -0
  32. package/dist/commands/awk.d.ts.map +1 -1
  33. package/dist/commands/awk.js +15 -2
  34. package/dist/commands/base64.d.ts +5 -0
  35. package/dist/commands/base64.d.ts.map +1 -1
  36. package/dist/commands/base64.js +9 -1
  37. package/dist/commands/cat.d.ts +5 -0
  38. package/dist/commands/cat.d.ts.map +1 -1
  39. package/dist/commands/cat.js +10 -2
  40. package/dist/commands/cd.d.ts +5 -0
  41. package/dist/commands/cd.d.ts.map +1 -1
  42. package/dist/commands/cd.js +5 -0
  43. package/dist/commands/chmod.d.ts +5 -0
  44. package/dist/commands/chmod.d.ts.map +1 -1
  45. package/dist/commands/chmod.js +5 -0
  46. package/dist/commands/cp.d.ts +5 -0
  47. package/dist/commands/cp.d.ts.map +1 -1
  48. package/dist/commands/cp.js +5 -0
  49. package/dist/commands/curl.d.ts +5 -0
  50. package/dist/commands/curl.d.ts.map +1 -1
  51. package/dist/commands/curl.js +34 -6
  52. package/dist/commands/cut.d.ts +5 -0
  53. package/dist/commands/cut.d.ts.map +1 -1
  54. package/dist/commands/cut.js +8 -1
  55. package/dist/commands/date.d.ts +5 -0
  56. package/dist/commands/date.d.ts.map +1 -1
  57. package/dist/commands/date.js +7 -1
  58. package/dist/commands/declare.d.ts +3 -0
  59. package/dist/commands/declare.d.ts.map +1 -0
  60. package/dist/commands/declare.js +39 -0
  61. package/dist/commands/diff.d.ts +5 -0
  62. package/dist/commands/diff.d.ts.map +1 -1
  63. package/dist/commands/diff.js +5 -0
  64. package/dist/commands/dpkg.d.ts +5 -0
  65. package/dist/commands/dpkg.d.ts.map +1 -1
  66. package/dist/commands/dpkg.js +24 -7
  67. package/dist/commands/du.d.ts.map +1 -1
  68. package/dist/commands/du.js +8 -2
  69. package/dist/commands/echo.d.ts +5 -0
  70. package/dist/commands/echo.d.ts.map +1 -1
  71. package/dist/commands/echo.js +13 -4
  72. package/dist/commands/env.d.ts +5 -0
  73. package/dist/commands/env.d.ts.map +1 -1
  74. package/dist/commands/env.js +11 -1
  75. package/dist/commands/exit.d.ts +5 -0
  76. package/dist/commands/exit.d.ts.map +1 -1
  77. package/dist/commands/exit.js +12 -2
  78. package/dist/commands/export.d.ts.map +1 -1
  79. package/dist/commands/export.js +3 -1
  80. package/dist/commands/find.d.ts +5 -0
  81. package/dist/commands/find.d.ts.map +1 -1
  82. package/dist/commands/find.js +5 -0
  83. package/dist/commands/free.d.ts +5 -0
  84. package/dist/commands/free.d.ts.map +1 -1
  85. package/dist/commands/free.js +5 -0
  86. package/dist/commands/grep.d.ts +5 -0
  87. package/dist/commands/grep.d.ts.map +1 -1
  88. package/dist/commands/grep.js +12 -2
  89. package/dist/commands/gzip.d.ts +5 -0
  90. package/dist/commands/gzip.d.ts.map +1 -1
  91. package/dist/commands/gzip.js +18 -2
  92. package/dist/commands/head.d.ts +5 -0
  93. package/dist/commands/head.d.ts.map +1 -1
  94. package/dist/commands/head.js +5 -0
  95. package/dist/commands/help.d.ts.map +1 -1
  96. package/dist/commands/help.js +98 -45
  97. package/dist/commands/history.d.ts +5 -0
  98. package/dist/commands/history.d.ts.map +1 -1
  99. package/dist/commands/history.js +5 -0
  100. package/dist/commands/hostname.d.ts +5 -0
  101. package/dist/commands/hostname.d.ts.map +1 -1
  102. package/dist/commands/hostname.js +5 -0
  103. package/dist/commands/id.d.ts.map +1 -1
  104. package/dist/commands/id.js +4 -1
  105. package/dist/commands/index.d.ts +2 -17
  106. package/dist/commands/index.d.ts.map +1 -1
  107. package/dist/commands/index.js +2 -340
  108. package/dist/commands/ls.d.ts.map +1 -1
  109. package/dist/commands/ls.js +3 -1
  110. package/dist/commands/lsb-release.d.ts.map +1 -1
  111. package/dist/commands/lsb-release.js +8 -2
  112. package/dist/commands/nano.js +1 -1
  113. package/dist/commands/neofetch.js +1 -1
  114. package/dist/commands/node.d.ts +9 -0
  115. package/dist/commands/node.d.ts.map +1 -0
  116. package/dist/commands/node.js +316 -0
  117. package/dist/commands/npm.d.ts +19 -0
  118. package/dist/commands/npm.d.ts.map +1 -0
  119. package/dist/commands/npm.js +109 -0
  120. package/dist/commands/ping.d.ts.map +1 -1
  121. package/dist/commands/ping.js +3 -1
  122. package/dist/commands/printf.d.ts +3 -0
  123. package/dist/commands/printf.d.ts.map +1 -0
  124. package/dist/commands/printf.js +113 -0
  125. package/dist/commands/ps.d.ts.map +1 -1
  126. package/dist/commands/ps.js +4 -1
  127. package/dist/commands/python.d.ts +30 -0
  128. package/dist/commands/python.d.ts.map +1 -0
  129. package/dist/commands/python.js +2058 -0
  130. package/dist/commands/read.d.ts +3 -0
  131. package/dist/commands/read.d.ts.map +1 -0
  132. package/dist/commands/read.js +34 -0
  133. package/dist/commands/registry.d.ts +8 -0
  134. package/dist/commands/registry.d.ts.map +1 -0
  135. package/dist/commands/registry.js +229 -0
  136. package/dist/commands/runtime.d.ts +6 -0
  137. package/dist/commands/runtime.d.ts.map +1 -0
  138. package/dist/commands/runtime.js +280 -0
  139. package/dist/commands/sed.d.ts.map +1 -1
  140. package/dist/commands/sed.js +11 -3
  141. package/dist/commands/set.d.ts.map +1 -1
  142. package/dist/commands/set.js +9 -3
  143. package/dist/commands/sh.d.ts.map +1 -1
  144. package/dist/commands/sh.js +57 -36
  145. package/dist/commands/shift.d.ts +5 -0
  146. package/dist/commands/shift.d.ts.map +1 -0
  147. package/dist/commands/shift.js +52 -0
  148. package/dist/commands/sleep.d.ts.map +1 -1
  149. package/dist/commands/sort.d.ts.map +1 -1
  150. package/dist/commands/sort.js +4 -2
  151. package/dist/commands/source.d.ts.map +1 -1
  152. package/dist/commands/source.js +5 -2
  153. package/dist/commands/sudo.js +1 -1
  154. package/dist/commands/tar.d.ts.map +1 -1
  155. package/dist/commands/tar.js +11 -3
  156. package/dist/commands/tee.d.ts.map +1 -1
  157. package/dist/commands/tee.js +8 -6
  158. package/dist/commands/test.d.ts.map +1 -1
  159. package/dist/commands/test.js +46 -24
  160. package/dist/commands/tr.d.ts.map +1 -1
  161. package/dist/commands/tr.js +3 -1
  162. package/dist/commands/true.d.ts +4 -0
  163. package/dist/commands/true.d.ts.map +1 -0
  164. package/dist/commands/true.js +14 -0
  165. package/dist/commands/type.d.ts.map +1 -1
  166. package/dist/commands/type.js +1 -1
  167. package/dist/commands/uname.d.ts.map +1 -1
  168. package/dist/commands/uname.js +4 -1
  169. package/dist/commands/uniq.d.ts.map +1 -1
  170. package/dist/commands/uptime.d.ts.map +1 -1
  171. package/dist/commands/uptime.js +4 -1
  172. package/dist/commands/wget.d.ts.map +1 -1
  173. package/dist/commands/wget.js +32 -7
  174. package/dist/commands/which.d.ts.map +1 -1
  175. package/dist/commands/xargs.d.ts.map +1 -1
  176. package/dist/commands/xargs.js +1 -1
  177. package/dist/index.d.ts +15 -14
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +9 -9
  180. package/dist/modules/linuxRootfs.d.ts +18 -1
  181. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  182. package/dist/modules/linuxRootfs.js +160 -17
  183. package/dist/standalone-wo-sftp.d.ts +2 -0
  184. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  185. package/dist/standalone-wo-sftp.js +30 -0
  186. package/dist/utils/expand.d.ts +50 -0
  187. package/dist/utils/expand.d.ts.map +1 -0
  188. package/dist/utils/expand.js +183 -0
  189. package/dist/utils/vfsDiff.d.ts +90 -0
  190. package/dist/utils/vfsDiff.d.ts.map +1 -0
  191. package/dist/utils/vfsDiff.js +177 -0
  192. package/package.json +2 -1
  193. package/src/SSHMimic/exec.ts +10 -1
  194. package/src/SSHMimic/executor.ts +104 -18
  195. package/src/SSHMimic/index.ts +49 -15
  196. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  197. package/src/VirtualFileSystem/index.ts +78 -28
  198. package/src/VirtualPackageManager/index.ts +208 -49
  199. package/src/VirtualShell/index.ts +35 -7
  200. package/src/VirtualShell/shell.ts +23 -3
  201. package/src/VirtualShell/shellParser.ts +134 -36
  202. package/src/VirtualUserManager/index.ts +7 -2
  203. package/src/commands/adduser.ts +6 -0
  204. package/src/commands/alias.ts +5 -1
  205. package/src/commands/apt.ts +47 -17
  206. package/src/commands/awk.ts +20 -6
  207. package/src/commands/base64.ts +13 -2
  208. package/src/commands/cat.ts +13 -5
  209. package/src/commands/cd.ts +5 -0
  210. package/src/commands/chmod.ts +5 -0
  211. package/src/commands/cp.ts +5 -0
  212. package/src/commands/curl.ts +56 -12
  213. package/src/commands/cut.ts +8 -1
  214. package/src/commands/date.ts +7 -1
  215. package/src/commands/declare.ts +44 -0
  216. package/src/commands/diff.ts +17 -3
  217. package/src/commands/dpkg.ts +33 -11
  218. package/src/commands/du.ts +17 -5
  219. package/src/commands/echo.ts +22 -9
  220. package/src/commands/env.ts +11 -1
  221. package/src/commands/exit.ts +12 -2
  222. package/src/commands/export.ts +3 -1
  223. package/src/commands/find.ts +5 -0
  224. package/src/commands/free.ts +9 -2
  225. package/src/commands/grep.ts +12 -2
  226. package/src/commands/gzip.ts +28 -4
  227. package/src/commands/head.ts +5 -0
  228. package/src/commands/help.ts +121 -47
  229. package/src/commands/history.ts +7 -2
  230. package/src/commands/hostname.ts +5 -0
  231. package/src/commands/id.ts +4 -1
  232. package/src/commands/index.ts +9 -360
  233. package/src/commands/ls.ts +5 -3
  234. package/src/commands/lsb-release.ts +8 -2
  235. package/src/commands/nano.ts +1 -1
  236. package/src/commands/neofetch.ts +1 -1
  237. package/src/commands/node.ts +341 -0
  238. package/src/commands/npm.ts +132 -0
  239. package/src/commands/ping.ts +6 -2
  240. package/src/commands/printf.ts +112 -0
  241. package/src/commands/ps.ts +21 -9
  242. package/src/commands/python.ts +2229 -0
  243. package/src/commands/read.ts +41 -0
  244. package/src/commands/registry.ts +244 -0
  245. package/src/commands/runtime.ts +353 -0
  246. package/src/commands/sed.ts +27 -9
  247. package/src/commands/set.ts +9 -3
  248. package/src/commands/sh.ts +159 -55
  249. package/src/commands/shift.ts +53 -0
  250. package/src/commands/sleep.ts +2 -1
  251. package/src/commands/sort.ts +10 -6
  252. package/src/commands/source.ts +15 -3
  253. package/src/commands/sudo.ts +1 -1
  254. package/src/commands/tar.ts +28 -7
  255. package/src/commands/tee.ts +7 -1
  256. package/src/commands/test.ts +61 -26
  257. package/src/commands/tr.ts +3 -1
  258. package/src/commands/true.ts +17 -0
  259. package/src/commands/type.ts +6 -3
  260. package/src/commands/uname.ts +5 -1
  261. package/src/commands/uniq.ts +8 -2
  262. package/src/commands/uptime.ts +4 -1
  263. package/src/commands/wget.ts +51 -12
  264. package/src/commands/which.ts +5 -2
  265. package/src/commands/xargs.ts +11 -2
  266. package/src/index.ts +23 -24
  267. package/src/modules/linuxRootfs.ts +233 -30
  268. package/src/standalone-wo-sftp.ts +38 -0
  269. package/src/utils/expand.ts +238 -0
  270. package/src/utils/vfsDiff.ts +275 -0
  271. package/standalone-wo-sftp.js +507 -0
  272. package/standalone-wo-sftp.js.map +7 -0
  273. package/standalone.js +253 -191
  274. package/standalone.js.map +4 -4
  275. package/tests/bun-test-shim.ts +9 -1
  276. package/tests/command-helpers.test.ts +1 -5
  277. package/tests/new-features.test.ts +415 -5
  278. package/tests/parser-executor.test.ts +27 -27
  279. package/tests/sftp.test.ts +122 -42
  280. package/tests/users.test.ts +23 -5
  281. package/CHANGELOG.md +0 -150
@@ -1,78 +1,152 @@
1
1
  import type { ShellModule } from "../types/commands";
2
- import { getCommandModulesPublic } from "./index";
2
+ import { getCommandModulesPublic } from "./registry";
3
+
4
+ // ─── category config ──────────────────────────────────────────────────────────
5
+
6
+ const CATEGORY_ORDER = [
7
+ "navigation",
8
+ "files",
9
+ "text",
10
+ "archive",
11
+ "system",
12
+ "package",
13
+ "network",
14
+ "shell",
15
+ "users",
16
+ "misc",
17
+ ];
3
18
 
4
- const CATEGORY_ORDER = ["navigation", "files", "text", "archive", "system", "network", "shell", "users", "misc"];
5
19
  const CATEGORY_LABELS: Record<string, string> = {
6
20
  navigation: "Navigation",
7
21
  files: "Files & Filesystem",
8
22
  text: "Text Processing",
9
23
  archive: "Archive & Compression",
10
24
  system: "System",
25
+ package: "Package Management",
11
26
  network: "Network",
12
- shell: "Shell",
27
+ shell: "Shell & Scripting",
13
28
  users: "Users & Permissions",
14
29
  misc: "Miscellaneous",
15
30
  };
16
31
 
17
- function padRight(s: string, n: number): string {
32
+ // ─── formatting helpers ───────────────────────────────────────────────────────
33
+
34
+ const BOLD = "\x1b[1m";
35
+ const RESET = "\x1b[0m";
36
+ const CYAN = "\x1b[36m";
37
+ const YLW = "\x1b[33m";
38
+ const DIM = "\x1b[2m";
39
+ const GREEN = "\x1b[32m";
40
+
41
+ function pad(s: string, n: number): string {
18
42
  return s.length >= n ? s : s + " ".repeat(n - s.length);
19
43
  }
20
44
 
45
+ function formatCmdLine(mod: ShellModule): string {
46
+ const aliases = mod.aliases?.length
47
+ ? ` ${DIM}(${mod.aliases.join(", ")})${RESET}`
48
+ : "";
49
+ return ` ${CYAN}${pad(mod.name, 16)}${RESET}${aliases}${pad("", mod.aliases?.length ? 0 : 0)} ${mod.description ?? ""}`;
50
+ }
51
+
52
+ // ─── full grouped listing ─────────────────────────────────────────────────────
53
+
54
+ function renderFull(modules: ShellModule[]): string {
55
+ const grouped: Record<string, ShellModule[]> = {};
56
+ for (const mod of modules) {
57
+ const cat = mod.category ?? "misc";
58
+ if (!grouped[cat]) grouped[cat] = [];
59
+ grouped[cat]!.push(mod);
60
+ }
61
+
62
+ const lines: string[] = [
63
+ `${BOLD}Available commands${RESET}`,
64
+ `${DIM}Type 'help <command>' for detailed usage.${RESET}`,
65
+ "",
66
+ ];
67
+
68
+ const cats = [
69
+ ...CATEGORY_ORDER.filter((c) => grouped[c]),
70
+ ...Object.keys(grouped)
71
+ .filter((c) => !CATEGORY_ORDER.includes(c))
72
+ .sort(),
73
+ ];
74
+
75
+ for (const cat of cats) {
76
+ const mods = grouped[cat];
77
+ if (!mods?.length) continue;
78
+
79
+ lines.push(`${YLW}${CATEGORY_LABELS[cat] ?? cat}${RESET}`);
80
+ const sorted = [...mods].sort((a, b) => a.name.localeCompare(b.name));
81
+ for (const mod of sorted) {
82
+ lines.push(formatCmdLine(mod));
83
+ }
84
+ lines.push("");
85
+ }
86
+
87
+ const total = modules.length;
88
+ lines.push(`${DIM}${total} commands available.${RESET}`);
89
+
90
+ return lines.join("\n");
91
+ }
92
+
93
+ // ─── single-command detail ────────────────────────────────────────────────────
94
+
95
+ function renderDetail(mod: ShellModule): string {
96
+ const lines: string[] = [];
97
+
98
+ lines.push(
99
+ `${BOLD}${mod.name}${RESET} — ${mod.description ?? "no description"}`,
100
+ );
101
+
102
+ if (mod.aliases?.length) {
103
+ lines.push(`${DIM}Aliases: ${mod.aliases.join(", ")}${RESET}`);
104
+ }
105
+
106
+ lines.push("");
107
+ lines.push(`${GREEN}Usage:${RESET}`);
108
+ if (mod.params.length) {
109
+ for (const p of mod.params) {
110
+ lines.push(` ${mod.name} ${p}`);
111
+ }
112
+ } else {
113
+ lines.push(` ${mod.name}`);
114
+ }
115
+
116
+ const catLabel =
117
+ CATEGORY_LABELS[mod.category ?? "misc"] ?? mod.category ?? "misc";
118
+ lines.push("");
119
+ lines.push(`${DIM}Category: ${catLabel}${RESET}`);
120
+
121
+ return lines.join("\n");
122
+ }
123
+
124
+ // ─── export ───────────────────────────────────────────────────────────────────
125
+
21
126
  export function createHelpCommand(_getNames: () => string[]): ShellModule {
22
127
  return {
23
128
  name: "help",
24
- description: "Display this help message",
129
+ description: "List all commands, or show usage for a specific command",
25
130
  category: "shell",
26
131
  params: ["[command]"],
27
132
  run: ({ args }) => {
28
133
  const modules = getCommandModulesPublic();
29
134
 
30
- // help <command>
31
135
  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
- const sorted = [...mods].sort((a, b) => a.name.localeCompare(b.name));
70
- for (const mod of sorted) {
71
- lines.push(` \x1b[36m${padRight(mod.name, 14)}\x1b[0m ${mod.description ?? ""}`);
136
+ const target = args[0].toLowerCase();
137
+ const mod = modules.find(
138
+ (m) => m.name === target || m.aliases?.includes(target),
139
+ );
140
+ if (!mod) {
141
+ return {
142
+ stderr: `help: no help entry for '${args[0]}'`,
143
+ exitCode: 1,
144
+ };
72
145
  }
146
+ return { stdout: renderDetail(mod), exitCode: 0 };
73
147
  }
74
148
 
75
- return { stdout: lines.join("\n"), exitCode: 0 };
149
+ return { stdout: renderFull(modules), exitCode: 0 };
76
150
  },
77
151
  };
78
152
  }
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Display persisted command history for the session (from VFS).
5
+ * @category shell
6
+ * @params ["[n]"]
7
+ */
3
8
  export const historyCommand: ShellModule = {
4
9
  name: "history",
5
10
  description: "Display command history",
@@ -20,8 +25,8 @@ export const historyCommand: ShellModule = {
20
25
  const slice = n && !Number.isNaN(n) ? lines.slice(-n) : lines;
21
26
 
22
27
  const offset = lines.length - slice.length + 1;
23
- const numbered = slice.map((line, i) =>
24
- `${String(offset + i).padStart(5)} ${line}`
28
+ const numbered = slice.map(
29
+ (line, i) => `${String(offset + i).padStart(5)} ${line}`,
25
30
  );
26
31
 
27
32
  return { stdout: numbered.join("\n"), exitCode: 0 };
@@ -1,5 +1,10 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
 
3
+ /**
4
+ * Print the configured hostname for the virtual shell.
5
+ * @category system
6
+ * @params []
7
+ */
3
8
  export const hostnameCommand: ShellModule = {
4
9
  name: "hostname",
5
10
  description: "Print hostname",
@@ -11,6 +11,9 @@ export const idCommand: ShellModule = {
11
11
  const gid = uid;
12
12
  const isSudo = shell.users.isSudoer(target);
13
13
  const groups = isSudo ? `${gid}(${target}),0(root)` : `${gid}(${target})`;
14
- return { stdout: `uid=${uid}(${target}) gid=${gid}(${target}) groups=${groups}`, exitCode: 0 };
14
+ return {
15
+ stdout: `uid=${uid}(${target}) gid=${gid}(${target}) groups=${groups}`,
16
+ exitCode: 0,
17
+ };
15
18
  },
16
19
  };
@@ -1,360 +1,9 @@
1
- /** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
2
- import { executeStatements } from "../SSHMimic/executor";
3
- import type { VirtualShell } from "../VirtualShell";
4
- import { parseScript } from "../VirtualShell/shellParser";
5
- import type {
6
- CommandContext,
7
- CommandMode,
8
- CommandResult,
9
- ShellEnv,
10
- ShellModule,
11
- } from "../types/commands";
12
- import { adduserCommand } from "./adduser";
13
- import { aliasCommand, unaliasCommand } from "./alias";
14
- import { testCommand } from "./test";
15
- import { sourceCommand } from "./source";
16
- import { historyCommand } from "./history";
17
- import { aptCacheCommand, aptCommand } from "./apt";
18
- import { awkCommand } from "./awk";
19
- import { base64Command } from "./base64";
20
- import { catCommand } from "./cat";
21
- import { cdCommand } from "./cd";
22
- import { chmodCommand } from "./chmod";
23
- import { clearCommand } from "./clear";
24
- import { cpCommand } from "./cp";
25
- import { curlCommand } from "./curl";
26
- import { cutCommand } from "./cut";
27
- import { dateCommand } from "./date";
28
- import { deluserCommand } from "./deluser";
29
- import { dfCommand } from "./df";
30
- import { diffCommand } from "./diff";
31
- import { dpkgCommand, dpkgQueryCommand } from "./dpkg";
32
- import { duCommand } from "./du";
33
- import { echoCommand } from "./echo";
34
- import { envCommand } from "./env";
35
- import { exitCommand } from "./exit";
36
- import { exportCommand } from "./export";
37
- import { findCommand } from "./find";
38
- import { freeCommand } from "./free";
39
- import { grepCommand } from "./grep";
40
- import { groupsCommand } from "./groups";
41
- import { gunzipCommand, gzipCommand } from "./gzip";
42
- import { headCommand } from "./head";
43
- import { createHelpCommand } from "./help";
44
- import { hostnameCommand } from "./hostname";
45
- import { htopCommand } from "./htop";
46
- import { idCommand } from "./id";
47
- import { killCommand } from "./kill";
48
- import { lnCommand } from "./ln";
49
- import { lsCommand } from "./ls";
50
- import { lsbReleaseCommand } from "./lsb-release";
51
- import { manCommand } from "./man";
52
- import { mkdirCommand } from "./mkdir";
53
- import { mvCommand } from "./mv";
54
- import { nanoCommand } from "./nano";
55
- import { neofetchCommand } from "./neofetch";
56
- import { passwdCommand } from "./passwd";
57
- import { pingCommand } from "./ping";
58
- import { psCommand } from "./ps";
59
- import { pwdCommand } from "./pwd";
60
- import { rmCommand } from "./rm";
61
- import { sedCommand } from "./sed";
62
- import { setCommand } from "./set";
63
- import { shCommand } from "./sh";
64
- import { sleepCommand } from "./sleep";
65
- import { sortCommand } from "./sort";
66
- import { suCommand } from "./su";
67
- import { sudoCommand } from "./sudo";
68
- import { tailCommand } from "./tail";
69
- import { tarCommand } from "./tar";
70
- import { teeCommand } from "./tee";
71
- import { touchCommand } from "./touch";
72
- import { trCommand } from "./tr";
73
- import { treeCommand } from "./tree";
74
- import { typeCommand } from "./type";
75
- import { unameCommand } from "./uname";
76
- import { uniqCommand } from "./uniq";
77
- import { unsetCommand } from "./unset";
78
- import { uptimeCommand } from "./uptime";
79
- import { wcCommand } from "./wc";
80
- import { wgetCommand } from "./wget";
81
- import { whichCommand } from "./which";
82
- import { whoCommand } from "./who";
83
- import { whoamiCommand } from "./whoami";
84
- import { xargsCommand } from "./xargs";
85
-
86
- const BASE_COMMANDS: ShellModule[] = [
87
- // Navigation
88
- pwdCommand, cdCommand, lsCommand, treeCommand,
89
- // Files
90
- catCommand, touchCommand, rmCommand, mkdirCommand, cpCommand, mvCommand, lnCommand,
91
- chmodCommand, findCommand,
92
- // Text processing
93
- grepCommand, sedCommand, awkCommand, sortCommand, uniqCommand, wcCommand,
94
- headCommand, tailCommand, cutCommand, trCommand, teeCommand, xargsCommand,
95
- diffCommand,
96
- // Archives
97
- tarCommand, gzipCommand, gunzipCommand, base64Command,
98
- // System info
99
- whoamiCommand, whoCommand, hostnameCommand, idCommand, groupsCommand, unameCommand,
100
- psCommand, killCommand, dfCommand, duCommand, dateCommand, sleepCommand, pingCommand,
101
- // Shell
102
- echoCommand, envCommand, exportCommand, setCommand, unsetCommand, shCommand,
103
- clearCommand, exitCommand,
104
- // Editors
105
- nanoCommand, htopCommand,
106
- // Network
107
- curlCommand, wgetCommand,
108
- // Users
109
- adduserCommand, passwdCommand, deluserCommand, sudoCommand, suCommand,
110
- // Misc
111
- neofetchCommand,
112
- // Package management
113
- aptCommand, aptCacheCommand, dpkgCommand, dpkgQueryCommand,
114
- // Shell (extended)
115
- whichCommand, typeCommand, manCommand, aliasCommand, unaliasCommand,
116
- testCommand, sourceCommand, historyCommand,
117
- // System (extended)
118
- uptimeCommand, freeCommand, lsbReleaseCommand,
119
- ];
120
-
121
- const customCommands: ShellModule[] = [];
122
- const commandRegistry = new Map<string, ShellModule>();
123
- let cachedCommandNames: string[] | null = null;
124
-
125
- const helpCommand = createHelpCommand(() => getCommandModules().map((cmd) => cmd.name));
126
-
127
- function buildCache(): void {
128
- commandRegistry.clear();
129
- for (const mod of getCommandModules()) {
130
- commandRegistry.set(mod.name, mod);
131
- for (const alias of mod.aliases ?? []) commandRegistry.set(alias, mod);
132
- }
133
- cachedCommandNames = Array.from(commandRegistry.keys()).sort();
134
- }
135
-
136
- function getCommandModules(): ShellModule[] {
137
- return [...BASE_COMMANDS, ...customCommands, helpCommand];
138
- }
139
-
140
- export function registerCommand(module: ShellModule): void {
141
- const normalized: ShellModule = {
142
- ...module,
143
- name: module.name.trim().toLowerCase(),
144
- aliases: module.aliases?.map((a) => a.trim().toLowerCase()),
145
- };
146
- const names = [normalized.name, ...(normalized.aliases ?? [])];
147
- if (names.some((n) => n.length === 0 || /\s/.test(n))) {
148
- throw new Error("Command names must be non-empty and contain no spaces");
149
- }
150
- customCommands.push(normalized);
151
- buildCache();
152
- }
153
-
154
- export function createCustomCommand(
155
- name: string,
156
- params: string[],
157
- run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
158
- ): ShellModule {
159
- return { name, params, run };
160
- }
161
-
162
- export function getCommandNames(): string[] {
163
- if (!cachedCommandNames) buildCache();
164
- return cachedCommandNames!;
165
- }
166
-
167
- export function getCommandModulesPublic(): ShellModule[] {
168
- return getCommandModules();
169
- }
170
-
171
- export function resolveModule(name: string): ShellModule | undefined {
172
- if (!cachedCommandNames) buildCache();
173
- return commandRegistry.get(name.toLowerCase());
174
- }
175
-
176
- function splitArgsRespectingQuotes(input: string): string[] {
177
- const tokens: string[] = [];
178
- let current = "";
179
- let inQuotes = false;
180
- let quoteChar = "";
181
-
182
- for (let i = 0; i < input.length; i++) {
183
- const ch = input[i] || "";
184
- const prev = i > 0 ? input[i - 1] : "";
185
- if ((ch === '"' || ch === "'") && prev !== "\\") {
186
- if (!inQuotes) { inQuotes = true; quoteChar = ch; continue; }
187
- if (ch === quoteChar) { inQuotes = false; quoteChar = ""; continue; }
188
- }
189
- if (/\s/.test(ch) && !inQuotes) {
190
- if (current.length > 0) { tokens.push(current); current = ""; }
191
- continue;
192
- }
193
- current += ch;
194
- }
195
- if (current.length > 0) tokens.push(current);
196
- return tokens;
197
- }
198
-
199
- function parseInput(rawInput: string): { commandName: string; args: string[] } {
200
- const parts = splitArgsRespectingQuotes(rawInput.trim());
201
- return { commandName: parts[0]?.toLowerCase() ?? "", args: parts.slice(1) };
202
- }
203
-
204
- export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
205
- return {
206
- vars: {
207
- PATH: "/usr/local/bin:/usr/bin:/bin",
208
- HOME: `/home/${authUser}`,
209
- USER: authUser,
210
- LOGNAME: authUser,
211
- SHELL: "/bin/sh",
212
- TERM: "xterm-256color",
213
- HOSTNAME: hostname,
214
- PS1: "\\u@\\h:\\w\\$ ",
215
- },
216
- lastExitCode: 0,
217
- };
218
- }
219
-
220
- /**
221
- * Execute a pre-parsed command directly by name and argument list.
222
- *
223
- * Unlike `runCommand`, this function does NOT re-join name+args into a string
224
- * and re-parse — so arguments that contain special characters (`;`, `|`, `>`,
225
- * quotes) are passed through verbatim. Use this from the pipeline executor.
226
- */
227
- export async function runCommandDirect(
228
- name: string,
229
- args: string[],
230
- authUser: string,
231
- hostname: string,
232
- mode: CommandMode,
233
- cwd: string,
234
- shell: VirtualShell,
235
- stdin: string | undefined,
236
- env: ShellEnv,
237
- ): Promise<CommandResult> {
238
- // Alias expansion on the command name
239
- const aliasVal = env.vars[`__alias_${name}`];
240
- if (aliasVal) {
241
- // Alias may expand to a multi-word command — re-route through runCommand
242
- return runCommand(`${aliasVal} ${args.join(" ")}`, authUser, hostname, mode, cwd, shell, stdin, env);
243
- }
244
-
245
- const mod = resolveModule(name);
246
- if (!mod) return { stderr: `${name}: command not found`, exitCode: 127 };
247
-
248
- try {
249
- return await mod.run({
250
- authUser,
251
- hostname,
252
- activeSessions: shell.users.listActiveSessions(),
253
- rawInput: [name, ...args].join(" "),
254
- mode,
255
- args,
256
- stdin,
257
- cwd,
258
- shell,
259
- env,
260
- });
261
- } catch (error: unknown) {
262
- return { stderr: error instanceof Error ? error.message : "Command failed", exitCode: 1 };
263
- }
264
- }
265
-
266
- export async function runCommand(
267
- rawInput: string,
268
- authUser: string,
269
- hostname: string,
270
- mode: CommandMode,
271
- cwd: string,
272
- shell: VirtualShell,
273
- stdin?: string,
274
- env?: ShellEnv,
275
- ): Promise<CommandResult> {
276
- const trimmed = rawInput.trim();
277
- if (trimmed.length === 0) return { exitCode: 0 };
278
-
279
- const shellEnv: ShellEnv = env ?? makeDefaultEnv(authUser, hostname);
280
-
281
- // ── $(cmd) command substitution ──────────────────────────────────────────
282
- let expanded = trimmed;
283
- if (expanded.includes("$(")) {
284
- // Only substitute $(…) that are NOT inside single quotes
285
- // Strategy: walk char by char, track single-quote state
286
- let result = "";
287
- let inSingle = false;
288
- let i = 0;
289
- while (i < expanded.length) {
290
- const ch = expanded[i]!;
291
- if (ch === "'" && !inSingle) { inSingle = true; result += ch; i++; continue; }
292
- if (ch === "'" && inSingle) { inSingle = false; result += ch; i++; continue; }
293
- if (!inSingle && ch === "$" && expanded[i + 1] === "(") {
294
- // Find matching closing )
295
- let depth = 0;
296
- let j = i + 1;
297
- while (j < expanded.length) {
298
- if (expanded[j] === "(") depth++;
299
- else if (expanded[j] === ")") { depth--; if (depth === 0) break; }
300
- j++;
301
- }
302
- const sub = expanded.slice(i + 2, j).trim();
303
- const subResult = await runCommand(sub, authUser, hostname, mode, cwd, shell, undefined, shellEnv);
304
- const subOut = (subResult.stdout ?? "").replace(/\n$/, "");
305
- result += subOut;
306
- i = j + 1;
307
- continue;
308
- }
309
- result += ch; i++;
310
- }
311
- expanded = result;
312
- }
313
-
314
- // ── alias expansion ───────────────────────────────────────────────────────
315
- const firstWord = expanded.split(/\s+/)[0] ?? "";
316
- const aliasVal = shellEnv.vars[`__alias_${firstWord}`];
317
- if (aliasVal) {
318
- expanded = expanded.replace(firstWord, aliasVal);
319
- }
320
-
321
- // Detect shell operators
322
- if (
323
- /(?<![|&])[|](?![|])/.test(expanded) ||
324
- expanded.includes(">") ||
325
- expanded.includes("<") ||
326
- expanded.includes("&&") ||
327
- expanded.includes("||") ||
328
- expanded.includes(";")
329
- ) {
330
- const script = parseScript(expanded);
331
- if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
332
- try {
333
- return await executeStatements(script.statements, authUser, hostname, mode, cwd, shell, shellEnv);
334
- } catch (error: unknown) {
335
- return { stderr: error instanceof Error ? error.message : "Execution failed", exitCode: 1 };
336
- }
337
- }
338
-
339
- const { commandName, args } = parseInput(expanded);
340
- const mod = resolveModule(commandName);
341
-
342
- if (!mod) return { stderr: `${commandName}: command not found`, exitCode: 127 };
343
-
344
- try {
345
- return await mod.run({
346
- authUser,
347
- hostname,
348
- activeSessions: shell.users.listActiveSessions(),
349
- rawInput: expanded,
350
- mode,
351
- args,
352
- stdin,
353
- cwd,
354
- shell,
355
- env: shellEnv,
356
- });
357
- } catch (error: unknown) {
358
- return { stderr: error instanceof Error ? error.message : "Command failed", exitCode: 1 };
359
- }
360
- }
1
+ export {
2
+ createCustomCommand,
3
+ getCommandModulesPublic,
4
+ getCommandNames,
5
+ registerCommand,
6
+ resolveModule
7
+ } from "./registry";
8
+
9
+ export { makeDefaultEnv, runCommand, runCommandDirect } from "./runtime";
@@ -32,9 +32,11 @@ export const lsCommand: ShellModule = {
32
32
  category: "navigation",
33
33
  params: ["[-la] [path]"],
34
34
  run: ({ authUser, shell, cwd, args }) => {
35
- const longFormat = ifFlag(args, ["-l", "--long"]);
36
- const showHidden = ifFlag(args, ["-a", "--all"]);
37
- const targetArg = getArg(args, 0, { flags: ["-l", "--long", "-a", "--all", "-la", "-al"] });
35
+ const longFormat = ifFlag(args, ["-l", "--long"]);
36
+ const showHidden = ifFlag(args, ["-a", "--all"]);
37
+ const targetArg = getArg(args, 0, {
38
+ flags: ["-l", "--long", "-a", "--all", "-la", "-al"],
39
+ });
38
40
  const target = resolvePath(cwd, targetArg ?? cwd);
39
41
  assertPathAccess(authUser, target, "ls");
40
42
  const items = shell.vfs
@@ -15,11 +15,17 @@ export const lsbReleaseCommand: ShellModule = {
15
15
  const content = shell.vfs.readFile("/etc/os-release");
16
16
  for (const line of content.split("\n")) {
17
17
  if (line.startsWith("PRETTY_NAME="))
18
- osName = line.slice("PRETTY_NAME=".length).replace(/^"|"$/g, "").trim();
18
+ osName = line
19
+ .slice("PRETTY_NAME=".length)
20
+ .replace(/^"|"$/g, "")
21
+ .trim();
19
22
  if (line.startsWith("VERSION_CODENAME="))
20
23
  codename = line.slice("VERSION_CODENAME=".length).trim();
21
24
  if (line.startsWith("VERSION_ID="))
22
- version = line.slice("VERSION_ID=".length).replace(/^"|"$/g, "").trim();
25
+ version = line
26
+ .slice("VERSION_ID=".length)
27
+ .replace(/^"|"$/g, "")
28
+ .trim();
23
29
  }
24
30
  } catch {}
25
31
 
@@ -5,7 +5,7 @@ import { assertPathAccess, resolvePath } from "./helpers";
5
5
  export const nanoCommand: ShellModule = {
6
6
  name: "nano",
7
7
  description: "Text editor",
8
- category: "shell",
8
+ category: "files",
9
9
  params: ["<file>"],
10
10
  run: ({ authUser, shell, cwd, args }) => {
11
11
  const fileArg = args[0];
@@ -6,7 +6,7 @@ import { getAllEnvVars } from "./set";
6
6
  export const neofetchCommand: ShellModule = {
7
7
  name: "neofetch",
8
8
  description: "System info display",
9
- category: "misc",
9
+ category: "system",
10
10
  params: ["[--off]"],
11
11
  run: ({ args, authUser, hostname, shell }) => {
12
12
  const env = getAllEnvVars(authUser);