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
@@ -0,0 +1,41 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+
4
+ export const readCommand: ShellModule = {
5
+ name: "read",
6
+ description: "Read a line from stdin into variables",
7
+ category: "shell",
8
+ params: ["[-r] [-p prompt] <var...>"],
9
+ run: ({ args, stdin, env }) => {
10
+ const _promptIdx = args.indexOf("-p");
11
+ const varNames = args.filter(
12
+ (a, i) => a !== "-r" && a !== "-p" && args[i - 1] !== "-p",
13
+ );
14
+
15
+ // In non-interactive context, read from stdin pipe
16
+ const input = (stdin ?? "").split("\n")[0] ?? "";
17
+ const line = ifFlag(args, ["-r"])
18
+ ? input
19
+ : input.replace(/\\(?:\r?\n|.)/g, (m) =>
20
+ m[1] === "\n" || m[1] === "\r" ? "" : m[1]!,
21
+ );
22
+
23
+ if (!env) return { exitCode: 0 };
24
+
25
+ if (varNames.length === 0) {
26
+ // No var names: store into REPLY
27
+ env.vars.REPLY = line;
28
+ } else if (varNames.length === 1) {
29
+ env.vars[varNames[0]!] = line;
30
+ } else {
31
+ // Split on whitespace, last var gets remainder
32
+ const parts = line.split(/\s+/);
33
+ for (let i = 0; i < varNames.length; i++) {
34
+ env.vars[varNames[i]!] =
35
+ i < varNames.length - 1 ? (parts[i] ?? "") : parts.slice(i).join(" ");
36
+ }
37
+ }
38
+
39
+ return { exitCode: 0 };
40
+ },
41
+ };
@@ -0,0 +1,244 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
2
+ import type { CommandContext, CommandResult, ShellModule } from "../types/commands";
3
+ import { adduserCommand } from "./adduser";
4
+ import { aliasCommand, unaliasCommand } from "./alias";
5
+ import { aptCacheCommand, aptCommand } from "./apt";
6
+ import { awkCommand } from "./awk";
7
+ import { base64Command } from "./base64";
8
+ import { catCommand } from "./cat";
9
+ import { cdCommand } from "./cd";
10
+ import { chmodCommand } from "./chmod";
11
+ import { clearCommand } from "./clear";
12
+ import { cpCommand } from "./cp";
13
+ import { curlCommand } from "./curl";
14
+ import { cutCommand } from "./cut";
15
+ import { dateCommand } from "./date";
16
+ import { declareCommand } from "./declare";
17
+ import { deluserCommand } from "./deluser";
18
+ import { dfCommand } from "./df";
19
+ import { diffCommand } from "./diff";
20
+ import { dpkgCommand, dpkgQueryCommand } from "./dpkg";
21
+ import { duCommand } from "./du";
22
+ import { echoCommand } from "./echo";
23
+ import { envCommand } from "./env";
24
+ import { exitCommand } from "./exit";
25
+ import { exportCommand } from "./export";
26
+ import { findCommand } from "./find";
27
+ import { freeCommand } from "./free";
28
+ import { grepCommand } from "./grep";
29
+ import { groupsCommand } from "./groups";
30
+ import { gunzipCommand, gzipCommand } from "./gzip";
31
+ import { headCommand } from "./head";
32
+ import { createHelpCommand } from "./help";
33
+ import { historyCommand } from "./history";
34
+ import { hostnameCommand } from "./hostname";
35
+ import { htopCommand } from "./htop";
36
+ import { idCommand } from "./id";
37
+ import { killCommand } from "./kill";
38
+ import { lnCommand } from "./ln";
39
+ import { lsCommand } from "./ls";
40
+ import { lsbReleaseCommand } from "./lsb-release";
41
+ import { manCommand } from "./man";
42
+ import { mkdirCommand } from "./mkdir";
43
+ import { mvCommand } from "./mv";
44
+ import { nanoCommand } from "./nano";
45
+ import { neofetchCommand } from "./neofetch";
46
+ import { nodeCommand } from "./node";
47
+ import { npmCommand, npxCommand } from "./npm";
48
+ import { passwdCommand } from "./passwd";
49
+ import { pingCommand } from "./ping";
50
+ import { printfCommand } from "./printf";
51
+ import { psCommand } from "./ps";
52
+ import { pwdCommand } from "./pwd";
53
+ import { python3Command } from "./python";
54
+ import { readCommand } from "./read";
55
+ import { rmCommand } from "./rm";
56
+ import { sedCommand } from "./sed";
57
+ import { setCommand } from "./set";
58
+ import { shCommand } from "./sh";
59
+ import { returnCommand, shiftCommand, trapCommand } from "./shift";
60
+ import { sleepCommand } from "./sleep";
61
+ import { sortCommand } from "./sort";
62
+ import { sourceCommand } from "./source";
63
+ import { suCommand } from "./su";
64
+ import { sudoCommand } from "./sudo";
65
+ import { tailCommand } from "./tail";
66
+ import { tarCommand } from "./tar";
67
+ import { teeCommand } from "./tee";
68
+ import { testCommand } from "./test";
69
+ import { touchCommand } from "./touch";
70
+ import { trCommand } from "./tr";
71
+ import { treeCommand } from "./tree";
72
+ import { falseCommand, trueCommand } from "./true";
73
+ import { typeCommand } from "./type";
74
+ import { unameCommand } from "./uname";
75
+ import { uniqCommand } from "./uniq";
76
+ import { unsetCommand } from "./unset";
77
+ import { uptimeCommand } from "./uptime";
78
+ import { wcCommand } from "./wc";
79
+ import { wgetCommand } from "./wget";
80
+ import { whichCommand } from "./which";
81
+ import { whoCommand } from "./who";
82
+ import { whoamiCommand } from "./whoami";
83
+ import { xargsCommand } from "./xargs";
84
+
85
+ const BASE_COMMANDS: ShellModule[] = [
86
+ // Navigation
87
+ pwdCommand,
88
+ cdCommand,
89
+ lsCommand,
90
+ treeCommand,
91
+ // Files
92
+ catCommand,
93
+ touchCommand,
94
+ rmCommand,
95
+ mkdirCommand,
96
+ cpCommand,
97
+ mvCommand,
98
+ lnCommand,
99
+ chmodCommand,
100
+ findCommand,
101
+ // Text processing
102
+ grepCommand,
103
+ sedCommand,
104
+ awkCommand,
105
+ sortCommand,
106
+ uniqCommand,
107
+ wcCommand,
108
+ headCommand,
109
+ tailCommand,
110
+ cutCommand,
111
+ trCommand,
112
+ teeCommand,
113
+ xargsCommand,
114
+ diffCommand,
115
+ // Archives
116
+ tarCommand,
117
+ gzipCommand,
118
+ gunzipCommand,
119
+ base64Command,
120
+ // System info
121
+ whoamiCommand,
122
+ whoCommand,
123
+ hostnameCommand,
124
+ idCommand,
125
+ groupsCommand,
126
+ unameCommand,
127
+ psCommand,
128
+ killCommand,
129
+ dfCommand,
130
+ duCommand,
131
+ dateCommand,
132
+ sleepCommand,
133
+ pingCommand,
134
+ // Shell
135
+ echoCommand,
136
+ envCommand,
137
+ exportCommand,
138
+ setCommand,
139
+ unsetCommand,
140
+ shCommand,
141
+ clearCommand,
142
+ exitCommand,
143
+ // Editors
144
+ nanoCommand,
145
+ htopCommand,
146
+ // Network
147
+ curlCommand,
148
+ wgetCommand,
149
+ // Users
150
+ adduserCommand,
151
+ passwdCommand,
152
+ deluserCommand,
153
+ sudoCommand,
154
+ suCommand,
155
+ // Misc
156
+ neofetchCommand,
157
+ // Package management
158
+ aptCommand,
159
+ aptCacheCommand,
160
+ dpkgCommand,
161
+ dpkgQueryCommand,
162
+ // Shell (extended)
163
+ whichCommand,
164
+ typeCommand,
165
+ manCommand,
166
+ aliasCommand,
167
+ unaliasCommand,
168
+ testCommand,
169
+ sourceCommand,
170
+ historyCommand,
171
+ printfCommand,
172
+ readCommand,
173
+ declareCommand,
174
+ shiftCommand,
175
+ trapCommand,
176
+ returnCommand,
177
+ trueCommand,
178
+ falseCommand,
179
+ npmCommand,
180
+ npxCommand,
181
+ nodeCommand,
182
+ python3Command,
183
+ // System (extended)
184
+ uptimeCommand,
185
+ freeCommand,
186
+ lsbReleaseCommand,
187
+ ];
188
+
189
+ const customCommands: ShellModule[] = [];
190
+ const commandRegistry = new Map<string, ShellModule>();
191
+ let cachedCommandNames: string[] | null = null;
192
+
193
+ const helpCommand = createHelpCommand(() =>
194
+ getCommandModules().map((cmd) => cmd.name),
195
+ );
196
+
197
+ function buildCache(): void {
198
+ commandRegistry.clear();
199
+ for (const mod of getCommandModules()) {
200
+ commandRegistry.set(mod.name, mod);
201
+ for (const alias of mod.aliases ?? []) commandRegistry.set(alias, mod);
202
+ }
203
+ cachedCommandNames = Array.from(commandRegistry.keys()).sort();
204
+ }
205
+
206
+ function getCommandModules(): ShellModule[] {
207
+ return [...BASE_COMMANDS, ...customCommands, helpCommand];
208
+ }
209
+
210
+ export function registerCommand(module: ShellModule): void {
211
+ const normalized: ShellModule = {
212
+ ...module,
213
+ name: module.name.trim().toLowerCase(),
214
+ aliases: module.aliases?.map((a) => a.trim().toLowerCase()),
215
+ };
216
+ const names = [normalized.name, ...(normalized.aliases ?? [])];
217
+ if (names.some((n) => n.length === 0 || /\s/.test(n))) {
218
+ throw new Error("Command names must be non-empty and contain no spaces");
219
+ }
220
+ customCommands.push(normalized);
221
+ buildCache();
222
+ }
223
+
224
+ export function createCustomCommand(
225
+ name: string,
226
+ params: string[],
227
+ run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
228
+ ): ShellModule {
229
+ return { name, params, run };
230
+ }
231
+
232
+ export function getCommandNames(): string[] {
233
+ if (!cachedCommandNames) buildCache();
234
+ return cachedCommandNames!;
235
+ }
236
+
237
+ export function getCommandModulesPublic(): ShellModule[] {
238
+ return getCommandModules();
239
+ }
240
+
241
+ export function resolveModule(name: string): ShellModule | undefined {
242
+ if (!cachedCommandNames) buildCache();
243
+ return commandRegistry.get(name.toLowerCase());
244
+ }
@@ -0,0 +1,353 @@
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
+ CommandMode,
7
+ CommandResult,
8
+ ShellEnv,
9
+ } from "../types/commands";
10
+ import { expandAsync } from "../utils/expand";
11
+ import { resolveModule } from "./registry";
12
+
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
+ export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
75
+ return {
76
+ vars: {
77
+ PATH: "/usr/local/bin:/usr/bin:/bin",
78
+ HOME: `/home/${authUser}`,
79
+ USER: authUser,
80
+ LOGNAME: authUser,
81
+ SHELL: "/bin/sh",
82
+ TERM: "xterm-256color",
83
+ HOSTNAME: hostname,
84
+ PS1: "\\u@\\h:\\w\\$ ",
85
+ },
86
+ lastExitCode: 0,
87
+ };
88
+ }
89
+
90
+ function resolveVfsBinary(
91
+ name: string,
92
+ env: ShellEnv,
93
+ shell: VirtualShell,
94
+ authUser: string,
95
+ ): string | null {
96
+ if (name.startsWith("/")) {
97
+ if (!shell.vfs.exists(name)) return null;
98
+ try {
99
+ const st = shell.vfs.stat(name);
100
+ if (st.type !== "file") return null;
101
+ if (!(st.mode & 0o111)) return null;
102
+ if (
103
+ (name.startsWith("/sbin/") || name.startsWith("/usr/sbin/")) &&
104
+ authUser !== "root"
105
+ )
106
+ return null;
107
+ return name;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ const pathDirs = (env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin").split(":");
114
+ for (const dir of pathDirs) {
115
+ if ((dir === "/sbin" || dir === "/usr/sbin") && authUser !== "root")
116
+ continue;
117
+ const full = `${dir}/${name}`;
118
+ if (!shell.vfs.exists(full)) continue;
119
+ try {
120
+ const st = shell.vfs.stat(full);
121
+ if (st.type !== "file") continue;
122
+ if (!(st.mode & 0o111)) continue;
123
+ return full;
124
+ } catch {}
125
+ }
126
+ return null;
127
+ }
128
+
129
+ export async function runCommandDirect(
130
+ name: string,
131
+ args: string[],
132
+ authUser: string,
133
+ hostname: string,
134
+ mode: CommandMode,
135
+ cwd: string,
136
+ shell: VirtualShell,
137
+ stdin: string | undefined,
138
+ env: ShellEnv,
139
+ ): Promise<CommandResult> {
140
+ const aliasVal = env.vars[`__alias_${name}`];
141
+ if (aliasVal) {
142
+ return runCommand(
143
+ `${aliasVal} ${args.join(" ")}`,
144
+ authUser,
145
+ hostname,
146
+ mode,
147
+ cwd,
148
+ shell,
149
+ stdin,
150
+ env,
151
+ );
152
+ }
153
+
154
+ const mod = resolveModule(name);
155
+ if (!mod) {
156
+ const vfsBinary = resolveVfsBinary(name, env, shell, authUser);
157
+ if (vfsBinary) {
158
+ const stubContent = shell.vfs.readFile(vfsBinary);
159
+ const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
160
+ if (builtinMatch) {
161
+ const builtinMod = resolveModule(builtinMatch[1]!);
162
+ if (builtinMod) {
163
+ return await builtinMod.run({
164
+ authUser,
165
+ hostname,
166
+ activeSessions: shell.users.listActiveSessions(),
167
+ rawInput: [name, ...args].join(" "),
168
+ mode,
169
+ args,
170
+ stdin,
171
+ cwd,
172
+ shell,
173
+ env,
174
+ });
175
+ }
176
+ }
177
+ const shMod = resolveModule("sh");
178
+ if (shMod) {
179
+ return await shMod.run({
180
+ authUser,
181
+ hostname,
182
+ activeSessions: shell.users.listActiveSessions(),
183
+ rawInput: `sh -c ${JSON.stringify(stubContent)}`,
184
+ mode,
185
+ args: ["-c", stubContent, "--", ...args],
186
+ stdin,
187
+ cwd,
188
+ shell,
189
+ env,
190
+ });
191
+ }
192
+ }
193
+ return { stderr: `${name}: command not found`, exitCode: 127 };
194
+ }
195
+
196
+ try {
197
+ return await mod.run({
198
+ authUser,
199
+ hostname,
200
+ activeSessions: shell.users.listActiveSessions(),
201
+ rawInput: [name, ...args].join(" "),
202
+ mode,
203
+ args,
204
+ stdin,
205
+ cwd,
206
+ shell,
207
+ env,
208
+ });
209
+ } catch (error: unknown) {
210
+ return {
211
+ stderr: error instanceof Error ? error.message : "Command failed",
212
+ exitCode: 1,
213
+ };
214
+ }
215
+ }
216
+
217
+ export async function runCommand(
218
+ rawInput: string,
219
+ authUser: string,
220
+ hostname: string,
221
+ mode: CommandMode,
222
+ cwd: string,
223
+ shell: VirtualShell,
224
+ stdin?: string,
225
+ env?: ShellEnv,
226
+ ): Promise<CommandResult> {
227
+ const trimmed = rawInput.trim();
228
+ if (trimmed.length === 0) return { exitCode: 0 };
229
+
230
+ const shellEnv: ShellEnv = env ?? makeDefaultEnv(authUser, hostname);
231
+
232
+ const rawTokens = tokenizeCommand(trimmed);
233
+ const rawFirstWord = rawTokens[0]?.toLowerCase() ?? "";
234
+ const aliasVal = shellEnv.vars[`__alias_${rawFirstWord}`];
235
+ const aliasExpanded = aliasVal
236
+ ? trimmed.replace(rawFirstWord, aliasVal)
237
+ : trimmed;
238
+
239
+ const hasOperators =
240
+ /(?<![|&])[|](?![|])/.test(aliasExpanded) ||
241
+ aliasExpanded.includes(">") ||
242
+ aliasExpanded.includes("<") ||
243
+ aliasExpanded.includes("&&") ||
244
+ aliasExpanded.includes("||") ||
245
+ aliasExpanded.includes(";");
246
+
247
+ if (hasOperators) {
248
+ const script = parseScript(aliasExpanded);
249
+ if (!script.isValid)
250
+ return { stderr: script.error || "Syntax error", exitCode: 1 };
251
+ try {
252
+ return await executeStatements(
253
+ script.statements,
254
+ authUser,
255
+ hostname,
256
+ mode,
257
+ cwd,
258
+ shell,
259
+ shellEnv,
260
+ );
261
+ } catch (error: unknown) {
262
+ return {
263
+ stderr: error instanceof Error ? error.message : "Execution failed",
264
+ exitCode: 1,
265
+ };
266
+ }
267
+ }
268
+
269
+ const expanded = await expandAsync(
270
+ aliasExpanded,
271
+ shellEnv.vars,
272
+ shellEnv.lastExitCode,
273
+ (sub) =>
274
+ runCommand(
275
+ sub,
276
+ authUser,
277
+ hostname,
278
+ mode,
279
+ cwd,
280
+ shell,
281
+ undefined,
282
+ shellEnv,
283
+ ).then((r) => r.stdout ?? ""),
284
+ );
285
+
286
+ const parts = tokenizeCommand(expanded.trim());
287
+ const commandName = parts[0]?.toLowerCase() ?? "";
288
+ const args = parts.slice(1);
289
+ const mod = resolveModule(commandName);
290
+
291
+ if (!mod) {
292
+ const vfsBinary = resolveVfsBinary(commandName, shellEnv, shell, authUser);
293
+ if (vfsBinary) {
294
+ const stubContent = shell.vfs.readFile(vfsBinary);
295
+ const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
296
+ if (builtinMatch) {
297
+ const builtinName = builtinMatch[1]!;
298
+ const builtinMod = resolveModule(builtinName);
299
+ if (builtinMod) {
300
+ return await builtinMod.run({
301
+ authUser,
302
+ hostname,
303
+ activeSessions: shell.users.listActiveSessions(),
304
+ rawInput: [commandName, ...args].join(" "),
305
+ mode,
306
+ args,
307
+ stdin,
308
+ cwd,
309
+ shell,
310
+ env: shellEnv,
311
+ });
312
+ }
313
+ }
314
+ const shMod = resolveModule("sh");
315
+ if (shMod) {
316
+ return await shMod.run({
317
+ authUser,
318
+ hostname,
319
+ activeSessions: shell.users.listActiveSessions(),
320
+ rawInput: `sh -c ${JSON.stringify(stubContent)}`,
321
+ mode,
322
+ args: ["-c", stubContent, "--", ...args],
323
+ stdin,
324
+ cwd,
325
+ shell,
326
+ env: shellEnv,
327
+ });
328
+ }
329
+ }
330
+
331
+ return { stderr: `${commandName}: command not found`, exitCode: 127 };
332
+ }
333
+
334
+ try {
335
+ return await mod.run({
336
+ authUser,
337
+ hostname,
338
+ activeSessions: shell.users.listActiveSessions(),
339
+ rawInput: expanded,
340
+ mode,
341
+ args,
342
+ stdin,
343
+ cwd,
344
+ shell,
345
+ env: shellEnv,
346
+ });
347
+ } catch (error: unknown) {
348
+ return {
349
+ stderr: error instanceof Error ? error.message : "Command failed",
350
+ exitCode: 1,
351
+ };
352
+ }
353
+ }
@@ -9,7 +9,9 @@ export const sedCommand: ShellModule = {
9
9
  params: ["-e <expr> [file]", "s/pattern/replace/[g]"],
10
10
  run: ({ authUser, shell, cwd, args, stdin }) => {
11
11
  const inPlace = ifFlag(args, ["-i"]);
12
- const expr = (getFlag(args, ["-e"]) as string | undefined) ?? args.find((a) => !a.startsWith("-"));
12
+ const expr =
13
+ (getFlag(args, ["-e"]) as string | undefined) ??
14
+ args.find((a) => !a.startsWith("-"));
13
15
  const fileArg = args.filter((a) => !a.startsWith("-") && a !== expr).pop();
14
16
 
15
17
  if (!expr) return { stderr: "sed: no expression", exitCode: 1 };
@@ -17,22 +19,38 @@ export const sedCommand: ShellModule = {
17
19
  let content = stdin ?? "";
18
20
  if (fileArg) {
19
21
  const p = resolvePath(cwd, fileArg);
20
- try { content = shell.vfs.readFile(p); } catch { return { stderr: `sed: ${fileArg}: No such file or directory`, exitCode: 1 }; }
22
+ try {
23
+ content = shell.vfs.readFile(p);
24
+ } catch {
25
+ return {
26
+ stderr: `sed: ${fileArg}: No such file or directory`,
27
+ exitCode: 1,
28
+ };
29
+ }
21
30
  }
22
31
 
23
32
  // Parse s/from/to/[g]
24
33
  const sMatch = expr.match(/^s([^a-zA-Z0-9])(.+?)\1(.*?)\1([gi]*)$/);
25
- if (!sMatch) return { stderr: `sed: unrecognized command: ${expr}`, exitCode: 1 };
34
+ if (!sMatch)
35
+ return { stderr: `sed: unrecognized command: ${expr}`, exitCode: 1 };
26
36
 
27
37
  const [, , from, to, flags] = sMatch;
28
- const regexFlags = (flags ?? "").includes("i") ? "gi" : (flags ?? "").includes("g") ? "g" : "";
38
+ const regexFlags = (flags ?? "").includes("i")
39
+ ? "gi"
40
+ : (flags ?? "").includes("g")
41
+ ? "g"
42
+ : "";
29
43
  let regex: RegExp;
30
- try { regex = new RegExp(from!, regexFlags || ""); }
31
- catch (_e) { return { stderr: `sed: invalid regex: ${from}`, exitCode: 1 }; }
44
+ try {
45
+ regex = new RegExp(from!, regexFlags || "");
46
+ } catch (_e) {
47
+ return { stderr: `sed: invalid regex: ${from}`, exitCode: 1 };
48
+ }
32
49
 
33
- const result = (flags ?? "").includes("g") || regexFlags.includes("g")
34
- ? content.replace(regex, to ?? "")
35
- : content.replace(regex, to ?? "");
50
+ const result =
51
+ (flags ?? "").includes("g") || regexFlags.includes("g")
52
+ ? content.replace(regex, to ?? "")
53
+ : content.replace(regex, to ?? "");
36
54
 
37
55
  if (inPlace && fileArg) {
38
56
  const p = resolvePath(cwd, fileArg);