typescript-virtual-container 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +674 -1504
  3. package/benchmark-results.txt +21 -21
  4. package/builds/self-standalone.js +274 -208
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +201 -149
  7. package/builds/standalone-wo-sftp.js.map +4 -4
  8. package/builds/standalone.js +263 -211
  9. package/builds/standalone.js.map +4 -4
  10. package/builds/web-full-api.min.js +3 -3
  11. package/builds/web-full-api.min.js.map +4 -4
  12. package/builds/web.min.js +2 -2
  13. package/builds/web.min.js.map +4 -4
  14. package/bun.lock +14 -12
  15. package/dist/SSHClient/index.d.ts.map +1 -1
  16. package/dist/SSHClient/index.js +5 -3
  17. package/dist/SSHMimic/executor.d.ts +1 -3
  18. package/dist/SSHMimic/executor.d.ts.map +1 -1
  19. package/dist/SSHMimic/executor.js +20 -22
  20. package/dist/SSHMimic/index.d.ts.map +1 -1
  21. package/dist/SSHMimic/index.js +5 -3
  22. package/dist/SSHMimic/sftp.d.ts.map +1 -1
  23. package/dist/SSHMimic/sftp.js +26 -21
  24. package/dist/VirtualShell/shell.d.ts.map +1 -1
  25. package/dist/VirtualShell/shell.js +25 -3
  26. package/dist/VirtualShell/shellParser.d.ts +1 -8
  27. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  28. package/dist/VirtualShell/shellParser.js +2 -81
  29. package/dist/VirtualUserManager/index.d.ts +7 -1
  30. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  31. package/dist/VirtualUserManager/index.js +47 -19
  32. package/dist/commands/adduser.d.ts +10 -4
  33. package/dist/commands/adduser.d.ts.map +1 -1
  34. package/dist/commands/adduser.js +75 -12
  35. package/dist/commands/alias.d.ts +5 -0
  36. package/dist/commands/alias.d.ts.map +1 -1
  37. package/dist/commands/alias.js +5 -0
  38. package/dist/commands/apt.d.ts +5 -0
  39. package/dist/commands/apt.d.ts.map +1 -1
  40. package/dist/commands/apt.js +5 -0
  41. package/dist/commands/awk.d.ts +10 -8
  42. package/dist/commands/awk.d.ts.map +1 -1
  43. package/dist/commands/awk.js +156 -28
  44. package/dist/commands/cd.d.ts.map +1 -1
  45. package/dist/commands/cd.js +0 -3
  46. package/dist/commands/clear.d.ts +5 -0
  47. package/dist/commands/clear.d.ts.map +1 -1
  48. package/dist/commands/clear.js +5 -0
  49. package/dist/commands/command-helpers.d.ts.map +1 -1
  50. package/dist/commands/command-helpers.js +8 -0
  51. package/dist/commands/declare.d.ts +5 -0
  52. package/dist/commands/declare.d.ts.map +1 -1
  53. package/dist/commands/declare.js +5 -0
  54. package/dist/commands/deluser.d.ts +12 -0
  55. package/dist/commands/deluser.d.ts.map +1 -1
  56. package/dist/commands/deluser.js +72 -6
  57. package/dist/commands/df.d.ts +5 -0
  58. package/dist/commands/df.d.ts.map +1 -1
  59. package/dist/commands/df.js +5 -0
  60. package/dist/commands/du.d.ts +5 -0
  61. package/dist/commands/du.d.ts.map +1 -1
  62. package/dist/commands/du.js +5 -0
  63. package/dist/commands/export.d.ts +5 -0
  64. package/dist/commands/export.d.ts.map +1 -1
  65. package/dist/commands/export.js +5 -0
  66. package/dist/commands/grep.d.ts.map +1 -1
  67. package/dist/commands/grep.js +22 -4
  68. package/dist/commands/groups.d.ts +5 -0
  69. package/dist/commands/groups.d.ts.map +1 -1
  70. package/dist/commands/groups.js +5 -0
  71. package/dist/commands/gzip.d.ts +5 -2
  72. package/dist/commands/gzip.d.ts.map +1 -1
  73. package/dist/commands/gzip.js +48 -28
  74. package/dist/commands/head.d.ts.map +1 -1
  75. package/dist/commands/head.js +12 -3
  76. package/dist/commands/htop.d.ts +5 -0
  77. package/dist/commands/htop.d.ts.map +1 -1
  78. package/dist/commands/htop.js +5 -0
  79. package/dist/commands/kill.d.ts +5 -0
  80. package/dist/commands/kill.d.ts.map +1 -1
  81. package/dist/commands/kill.js +5 -0
  82. package/dist/commands/ln.d.ts +2 -0
  83. package/dist/commands/ln.d.ts.map +1 -1
  84. package/dist/commands/ln.js +22 -0
  85. package/dist/commands/ls.d.ts.map +1 -1
  86. package/dist/commands/ls.js +15 -0
  87. package/dist/commands/lsb-release.d.ts +5 -0
  88. package/dist/commands/lsb-release.d.ts.map +1 -1
  89. package/dist/commands/lsb-release.js +5 -0
  90. package/dist/commands/mkdir.d.ts +5 -0
  91. package/dist/commands/mkdir.d.ts.map +1 -1
  92. package/dist/commands/mkdir.js +5 -0
  93. package/dist/commands/mv.d.ts +5 -0
  94. package/dist/commands/mv.d.ts.map +1 -1
  95. package/dist/commands/mv.js +5 -0
  96. package/dist/commands/nano.d.ts +5 -0
  97. package/dist/commands/nano.d.ts.map +1 -1
  98. package/dist/commands/nano.js +5 -0
  99. package/dist/commands/neofetch.d.ts +5 -0
  100. package/dist/commands/neofetch.d.ts.map +1 -1
  101. package/dist/commands/neofetch.js +8 -5
  102. package/dist/commands/passwd.d.ts +8 -0
  103. package/dist/commands/passwd.d.ts.map +1 -1
  104. package/dist/commands/passwd.js +32 -11
  105. package/dist/commands/ping.d.ts +5 -0
  106. package/dist/commands/ping.d.ts.map +1 -1
  107. package/dist/commands/ping.js +5 -0
  108. package/dist/commands/printf.d.ts +5 -0
  109. package/dist/commands/printf.d.ts.map +1 -1
  110. package/dist/commands/printf.js +43 -12
  111. package/dist/commands/ps.d.ts +5 -0
  112. package/dist/commands/ps.d.ts.map +1 -1
  113. package/dist/commands/ps.js +5 -0
  114. package/dist/commands/read.d.ts +5 -0
  115. package/dist/commands/read.d.ts.map +1 -1
  116. package/dist/commands/read.js +5 -0
  117. package/dist/commands/registry.d.ts.map +1 -1
  118. package/dist/commands/registry.js +4 -1
  119. package/dist/commands/rm.d.ts +5 -0
  120. package/dist/commands/rm.d.ts.map +1 -1
  121. package/dist/commands/rm.js +5 -0
  122. package/dist/commands/runtime.d.ts.map +1 -1
  123. package/dist/commands/runtime.js +1 -57
  124. package/dist/commands/sed.d.ts +5 -0
  125. package/dist/commands/sed.d.ts.map +1 -1
  126. package/dist/commands/sed.js +5 -0
  127. package/dist/commands/set.d.ts +5 -6
  128. package/dist/commands/set.d.ts.map +1 -1
  129. package/dist/commands/set.js +5 -22
  130. package/dist/commands/sh.d.ts +6 -0
  131. package/dist/commands/sh.d.ts.map +1 -1
  132. package/dist/commands/sh.js +6 -0
  133. package/dist/commands/shift.d.ts +10 -0
  134. package/dist/commands/shift.d.ts.map +1 -1
  135. package/dist/commands/shift.js +10 -0
  136. package/dist/commands/sleep.d.ts +5 -0
  137. package/dist/commands/sleep.d.ts.map +1 -1
  138. package/dist/commands/sleep.js +5 -0
  139. package/dist/commands/sort.d.ts +5 -0
  140. package/dist/commands/sort.d.ts.map +1 -1
  141. package/dist/commands/sort.js +5 -0
  142. package/dist/commands/source.d.ts +5 -0
  143. package/dist/commands/source.d.ts.map +1 -1
  144. package/dist/commands/source.js +5 -0
  145. package/dist/commands/stat.d.ts +7 -0
  146. package/dist/commands/stat.d.ts.map +1 -0
  147. package/dist/commands/stat.js +56 -0
  148. package/dist/commands/su.d.ts +13 -0
  149. package/dist/commands/su.d.ts.map +1 -1
  150. package/dist/commands/su.js +45 -14
  151. package/dist/commands/sudo.d.ts.map +1 -1
  152. package/dist/commands/sudo.js +5 -0
  153. package/dist/commands/tail.d.ts +5 -0
  154. package/dist/commands/tail.d.ts.map +1 -1
  155. package/dist/commands/tail.js +15 -3
  156. package/dist/commands/tar.d.ts +5 -0
  157. package/dist/commands/tar.d.ts.map +1 -1
  158. package/dist/commands/tar.js +40 -10
  159. package/dist/commands/tee.d.ts +5 -0
  160. package/dist/commands/tee.d.ts.map +1 -1
  161. package/dist/commands/tee.js +5 -0
  162. package/dist/commands/touch.d.ts +5 -0
  163. package/dist/commands/touch.d.ts.map +1 -1
  164. package/dist/commands/touch.js +5 -0
  165. package/dist/commands/tr.d.ts.map +1 -1
  166. package/dist/commands/tr.js +45 -10
  167. package/dist/commands/tree.d.ts +5 -0
  168. package/dist/commands/tree.d.ts.map +1 -1
  169. package/dist/commands/tree.js +5 -0
  170. package/dist/commands/true.d.ts +10 -0
  171. package/dist/commands/true.d.ts.map +1 -1
  172. package/dist/commands/true.js +10 -0
  173. package/dist/commands/type.d.ts +5 -0
  174. package/dist/commands/type.d.ts.map +1 -1
  175. package/dist/commands/type.js +5 -0
  176. package/dist/commands/uname.d.ts +5 -0
  177. package/dist/commands/uname.d.ts.map +1 -1
  178. package/dist/commands/uname.js +5 -0
  179. package/dist/commands/uniq.d.ts +5 -0
  180. package/dist/commands/uniq.d.ts.map +1 -1
  181. package/dist/commands/uniq.js +5 -0
  182. package/dist/commands/unset.d.ts +5 -0
  183. package/dist/commands/unset.d.ts.map +1 -1
  184. package/dist/commands/unset.js +5 -0
  185. package/dist/commands/uptime.d.ts +5 -0
  186. package/dist/commands/uptime.d.ts.map +1 -1
  187. package/dist/commands/uptime.js +5 -0
  188. package/dist/commands/wc.d.ts +5 -0
  189. package/dist/commands/wc.d.ts.map +1 -1
  190. package/dist/commands/wc.js +5 -0
  191. package/dist/commands/wget.d.ts +5 -0
  192. package/dist/commands/wget.d.ts.map +1 -1
  193. package/dist/commands/wget.js +5 -0
  194. package/dist/commands/who.d.ts +5 -0
  195. package/dist/commands/who.d.ts.map +1 -1
  196. package/dist/commands/who.js +5 -0
  197. package/dist/commands/whoami.d.ts +5 -0
  198. package/dist/commands/whoami.d.ts.map +1 -1
  199. package/dist/commands/whoami.js +5 -0
  200. package/dist/commands/xargs.d.ts +5 -0
  201. package/dist/commands/xargs.d.ts.map +1 -1
  202. package/dist/commands/xargs.js +5 -0
  203. package/dist/self-standalone.js +254 -30
  204. package/dist/types/commands.d.ts +36 -0
  205. package/dist/types/commands.d.ts.map +1 -1
  206. package/dist/utils/tokenize.d.ts +20 -0
  207. package/dist/utils/tokenize.d.ts.map +1 -0
  208. package/dist/utils/tokenize.js +74 -0
  209. package/examples/web.min.js +2 -2
  210. package/package.json +1 -1
  211. package/src/SSHClient/index.ts +6 -3
  212. package/src/SSHMimic/executor.ts +21 -44
  213. package/src/SSHMimic/index.ts +7 -5
  214. package/src/SSHMimic/sftp.ts +28 -21
  215. package/src/VirtualShell/shell.ts +34 -4
  216. package/src/VirtualShell/shellParser.ts +2 -103
  217. package/src/VirtualUserManager/index.ts +44 -20
  218. package/src/commands/adduser.ts +86 -13
  219. package/src/commands/alias.ts +5 -0
  220. package/src/commands/apt.ts +5 -0
  221. package/src/commands/awk.ts +154 -29
  222. package/src/commands/cd.ts +0 -4
  223. package/src/commands/clear.ts +5 -0
  224. package/src/commands/command-helpers.ts +9 -0
  225. package/src/commands/declare.ts +5 -0
  226. package/src/commands/deluser.ts +84 -7
  227. package/src/commands/df.ts +5 -0
  228. package/src/commands/du.ts +5 -0
  229. package/src/commands/export.ts +5 -0
  230. package/src/commands/grep.ts +21 -8
  231. package/src/commands/groups.ts +5 -0
  232. package/src/commands/gzip.ts +54 -28
  233. package/src/commands/head.ts +14 -4
  234. package/src/commands/htop.ts +5 -0
  235. package/src/commands/kill.ts +5 -0
  236. package/src/commands/ln.ts +22 -0
  237. package/src/commands/ls.ts +17 -0
  238. package/src/commands/lsb-release.ts +5 -0
  239. package/src/commands/mkdir.ts +5 -0
  240. package/src/commands/mv.ts +5 -0
  241. package/src/commands/nano.ts +5 -0
  242. package/src/commands/neofetch.ts +8 -6
  243. package/src/commands/passwd.ts +35 -12
  244. package/src/commands/ping.ts +5 -0
  245. package/src/commands/printf.ts +30 -13
  246. package/src/commands/ps.ts +5 -0
  247. package/src/commands/read.ts +5 -0
  248. package/src/commands/registry.ts +4 -1
  249. package/src/commands/rm.ts +5 -0
  250. package/src/commands/runtime.ts +1 -61
  251. package/src/commands/sed.ts +5 -0
  252. package/src/commands/set.ts +5 -24
  253. package/src/commands/sh.ts +9 -3
  254. package/src/commands/shift.ts +10 -0
  255. package/src/commands/sleep.ts +5 -0
  256. package/src/commands/sort.ts +5 -0
  257. package/src/commands/source.ts +5 -0
  258. package/src/commands/stat.ts +61 -0
  259. package/src/commands/su.ts +54 -16
  260. package/src/commands/sudo.ts +5 -0
  261. package/src/commands/tail.ts +17 -3
  262. package/src/commands/tar.ts +38 -15
  263. package/src/commands/tee.ts +5 -0
  264. package/src/commands/touch.ts +5 -0
  265. package/src/commands/tr.ts +54 -10
  266. package/src/commands/tree.ts +5 -0
  267. package/src/commands/true.ts +10 -0
  268. package/src/commands/type.ts +5 -0
  269. package/src/commands/uname.ts +5 -0
  270. package/src/commands/uniq.ts +5 -0
  271. package/src/commands/unset.ts +5 -0
  272. package/src/commands/uptime.ts +5 -0
  273. package/src/commands/wc.ts +5 -0
  274. package/src/commands/wget.ts +5 -0
  275. package/src/commands/who.ts +5 -0
  276. package/src/commands/whoami.ts +5 -0
  277. package/src/commands/xargs.ts +5 -0
  278. package/src/self-standalone.ts +316 -33
  279. package/src/types/commands.ts +37 -0
  280. package/src/utils/tokenize.ts +78 -0
  281. package/builds/web-iife.min.js +0 -13
  282. package/builds/web-iife.min.js.map +0 -7
@@ -1,10 +1,13 @@
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
1
2
  import { basename } from "node:path";
2
3
  import { stdin, stdout } from "node:process";
3
4
  import { createInterface, type Interface } from "node:readline";
4
5
 
5
6
  import { makeDefaultEnv, runCommand } from "./commands/runtime";
7
+ import { spawnNanoEditorProcess } from "./modules/shellInteractive";
6
8
  import { buildLoginBanner, type LoginBannerState } from "./SSHMimic/loginBanner";
7
9
  import { buildPrompt } from "./SSHMimic/prompt";
10
+ import type { CommandResult, PasswordChallenge, SudoChallenge } from "./types/commands";
8
11
  import { VirtualShell } from "./VirtualShell";
9
12
 
10
13
  const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
@@ -47,9 +50,55 @@ function readLastLogin(username: string): LoginBannerState | null {
47
50
  }
48
51
  }
49
52
 
50
- function askQuestion(rl: Interface, promptText: string): Promise<string> {
53
+ function askHiddenQuestion(rl: Interface, promptText: string): Promise<string> {
51
54
  return new Promise((resolve) => {
52
- rl.question(promptText, resolve);
55
+ if (!stdin.isTTY || !stdout.isTTY) {
56
+ rl.question(promptText, resolve);
57
+ return;
58
+ }
59
+
60
+ const wasRawMode = Boolean(stdin.isRaw);
61
+ let buffer = "";
62
+
63
+ const cleanup = (): void => {
64
+ stdin.off("data", onData);
65
+ if (!wasRawMode) {
66
+ stdin.setRawMode(false);
67
+ }
68
+ rl.resume();
69
+ };
70
+
71
+ const finish = (value: string): void => {
72
+ cleanup();
73
+ stdout.write("\n");
74
+ resolve(value);
75
+ };
76
+
77
+ const onData = (chunk: Buffer): void => {
78
+ const input = chunk.toString("utf8");
79
+ for (let index = 0; index < input.length; index += 1) {
80
+ const ch = input[index]!;
81
+ if (ch === "\r" || ch === "\n") {
82
+ finish(buffer);
83
+ return;
84
+ }
85
+ if (ch === "\u007f" || ch === "\b") {
86
+ buffer = buffer.slice(0, -1);
87
+ continue;
88
+ }
89
+ if (ch >= " ") {
90
+ buffer += ch;
91
+ }
92
+ }
93
+ };
94
+
95
+ rl.pause();
96
+ stdout.write(promptText);
97
+ if (!wasRawMode) {
98
+ stdin.setRawMode(true);
99
+ }
100
+ stdin.resume();
101
+ stdin.on("data", onData);
53
102
  });
54
103
  }
55
104
 
@@ -65,6 +114,53 @@ function writeLastLogin(username: string, from: string): void {
65
114
  );
66
115
  }
67
116
 
117
+ async function flushVfs(): Promise<void> {
118
+ await virtualShell.vfs.flushMirror();
119
+ }
120
+
121
+ function loadHistory(): string[] {
122
+ const historyPath = "/virtual-env-js/.bash_history";
123
+ if (!virtualShell.vfs.exists(historyPath)) {
124
+ virtualShell.vfs.writeFile(historyPath, "");
125
+ return [];
126
+ }
127
+
128
+ return virtualShell.vfs
129
+ .readFile(historyPath)
130
+ .split("\n")
131
+ .map((line) => line.trim())
132
+ .filter((line) => line.length > 0);
133
+ }
134
+
135
+ function saveHistory(history: string[]): void {
136
+ const data = history.length > 0 ? `${history.join("\n")}\n` : "";
137
+ virtualShell.vfs.writeFile("/virtual-env-js/.bash_history", data);
138
+ }
139
+
140
+ function applySessionState(
141
+ authUserState: string,
142
+ cwdState: string,
143
+ result: CommandResult,
144
+ shellEnvState: ReturnType<typeof makeDefaultEnv>,
145
+ ): { authUser: string; cwd: string } {
146
+ let authUser = authUserState;
147
+ let cwd = cwdState;
148
+
149
+ if (result.switchUser) {
150
+ authUser = result.switchUser;
151
+ cwd = result.nextCwd ?? `/home/${authUser}`;
152
+ shellEnvState.vars.USER = authUser;
153
+ shellEnvState.vars.LOGNAME = authUser;
154
+ shellEnvState.vars.HOME = `/home/${authUser}`;
155
+ shellEnvState.vars.PWD = cwd;
156
+ } else if (result.nextCwd) {
157
+ cwd = result.nextCwd;
158
+ shellEnvState.vars.PWD = cwd;
159
+ }
160
+
161
+ return { authUser, cwd };
162
+ }
163
+
68
164
  virtualShell.addCommand("demo", [], () => {
69
165
  return {
70
166
  stdout: "This is a demo command. It does nothing useful.",
@@ -75,6 +171,9 @@ virtualShell.addCommand("demo", [], () => {
75
171
  async function runReadlineShell() {
76
172
  const rl = createInterface({ input: stdin, output: stdout, terminal: true });
77
173
  await virtualShell.ensureInitialized();
174
+ let history = loadHistory();
175
+ const rlWithHistory = rl as Interface & { history: string[] };
176
+ rlWithHistory.history = [...history].reverse();
78
177
 
79
178
  const selectedUser = initialUser.trim() || "root";
80
179
  const userExists = virtualShell.users.getPasswordHash(selectedUser) !== null;
@@ -88,9 +187,207 @@ async function runReadlineShell() {
88
187
  let cwd = `/home/${authUser}`;
89
188
  shellEnv.vars.PWD = cwd;
90
189
  const remoteAddress = "localhost";
190
+ const terminalSize = {
191
+ cols: stdout.columns ?? 80,
192
+ rows: stdout.rows ?? 24,
193
+ };
194
+
195
+ async function startNanoEditor(
196
+ targetPath: string,
197
+ initialContent: string,
198
+ tempPath: string,
199
+ ): Promise<void> {
200
+ if (virtualShell.vfs.exists(targetPath)) {
201
+ await writeFile(tempPath, initialContent, "utf8");
202
+ }
203
+
204
+ rl.pause();
205
+ const editor = spawnNanoEditorProcess(
206
+ tempPath,
207
+ terminalSize,
208
+ {
209
+ write: stdout.write.bind(stdout),
210
+ exit: () => undefined,
211
+ end: () => undefined,
212
+ } as unknown as Parameters<typeof spawnNanoEditorProcess>[2],
213
+ );
214
+
215
+ const wasRawMode = Boolean(stdin.isRaw);
216
+ const forwardInput = (chunk: Buffer): void => {
217
+ editor.stdin.write(chunk);
218
+ };
219
+
220
+ stdin.resume();
221
+ if (!wasRawMode) {
222
+ stdin.setRawMode(true);
223
+ }
224
+ stdin.on("data", forwardInput);
225
+
226
+ await new Promise<void>((resolve) => {
227
+ const cleanup = (): void => {
228
+ stdin.off("data", forwardInput);
229
+ if (!wasRawMode) {
230
+ stdin.setRawMode(false);
231
+ }
232
+ rl.resume();
233
+ };
234
+
235
+ editor.on("error", (error: Error) => {
236
+ cleanup();
237
+ stdout.write(`nano: ${error.message}\r\n`);
238
+ resolve();
239
+ });
240
+
241
+ editor.on("close", async () => {
242
+ cleanup();
243
+ rl.write("", { ctrl: true, name: "u" });
244
+ try {
245
+ const updatedContent = await readFile(tempPath, "utf8");
246
+ virtualShell.writeFileAsUser(authUser, targetPath, updatedContent);
247
+ await flushVfs();
248
+ } catch {
249
+ // Save skipped or temp file missing.
250
+ }
251
+
252
+ await unlink(tempPath).catch(() => undefined);
253
+ stdout.write("\r\n");
254
+ resolve();
255
+ });
256
+ });
257
+ }
258
+
259
+ async function handleSudoChallenge(challenge: SudoChallenge): Promise<void> {
260
+ if (challenge.onPassword) {
261
+ let promptText = challenge.prompt;
262
+ while (true) {
263
+ const typed = await askHiddenQuestion(rl, promptText);
264
+ const step = await challenge.onPassword(typed, virtualShell);
265
+ if (step.result === null) {
266
+ promptText = step.nextPrompt ?? promptText;
267
+ continue;
268
+ }
269
+
270
+ await handleCommandResult(step.result);
271
+ return;
272
+ }
273
+ }
274
+
275
+ const password = await askHiddenQuestion(rl, challenge.prompt);
276
+ if (!virtualShell.users.verifyPassword(challenge.username, password)) {
277
+ process.stderr.write("Sorry, try again.\n");
278
+ return;
279
+ }
280
+
281
+ if (!challenge.commandLine) {
282
+ authUser = challenge.targetUser;
283
+ cwd = `/home/${authUser}`;
284
+ shellEnv.vars.USER = authUser;
285
+ shellEnv.vars.LOGNAME = authUser;
286
+ shellEnv.vars.HOME = `/home/${authUser}`;
287
+ shellEnv.vars.PWD = cwd;
288
+ return;
289
+ }
290
+
291
+ const runCwd = challenge.loginShell ? `/home/${challenge.targetUser}` : cwd;
292
+ const nestedResult = await runCommand(
293
+ challenge.commandLine,
294
+ challenge.targetUser,
295
+ hostname,
296
+ "shell",
297
+ runCwd,
298
+ virtualShell,
299
+ undefined,
300
+ shellEnv,
301
+ );
302
+ await handleCommandResult(nestedResult);
303
+ }
304
+
305
+ async function handlePasswordChallenge(
306
+ challenge: PasswordChallenge,
307
+ ): Promise<void> {
308
+ const first = await askHiddenQuestion(rl, challenge.prompt);
309
+ if (challenge.confirmPrompt) {
310
+ const second = await askHiddenQuestion(rl, challenge.confirmPrompt);
311
+ if (second !== first) {
312
+ process.stderr.write("passwords do not match\n");
313
+ return;
314
+ }
315
+ }
316
+
317
+ switch (challenge.action) {
318
+ case "passwd":
319
+ await virtualShell.users.setPassword(challenge.targetUsername, first);
320
+ stdout.write("passwd: password updated successfully\n");
321
+ break;
322
+ case "adduser":
323
+ if (!challenge.newUsername) {
324
+ process.stderr.write("adduser: missing username\n");
325
+ return;
326
+ }
327
+ await virtualShell.users.addUser(challenge.newUsername, first);
328
+ stdout.write(`adduser: user '${challenge.newUsername}' created\n`);
329
+ break;
330
+ case "deluser":
331
+ await virtualShell.users.deleteUser(challenge.targetUsername);
332
+ stdout.write(`Removing user '${challenge.targetUsername}' ...\ndeluser: done.\n`);
333
+ break;
334
+ case "su":
335
+ authUser = challenge.targetUsername;
336
+ cwd = `/home/${authUser}`;
337
+ shellEnv.vars.USER = authUser;
338
+ shellEnv.vars.LOGNAME = authUser;
339
+ shellEnv.vars.HOME = `/home/${authUser}`;
340
+ shellEnv.vars.PWD = cwd;
341
+ break;
342
+ }
343
+ }
344
+
345
+ async function handleCommandResult(result: CommandResult): Promise<void> {
346
+ if (result.openEditor) {
347
+ await startNanoEditor(
348
+ result.openEditor.targetPath,
349
+ result.openEditor.initialContent,
350
+ result.openEditor.tempPath,
351
+ );
352
+ return;
353
+ }
354
+
355
+ if (result.sudoChallenge) {
356
+ await handleSudoChallenge(result.sudoChallenge);
357
+ return;
358
+ }
359
+
360
+ if (result.passwordChallenge) {
361
+ await handlePasswordChallenge(result.passwordChallenge);
362
+ return;
363
+ }
364
+
365
+ if (result.stdout) {
366
+ stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
367
+ }
368
+
369
+ if (result.stderr) {
370
+ process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
371
+ }
372
+
373
+ if (result.clearScreen) {
374
+ stdout.write("\u001b[2J\u001b[H");
375
+ console.clear();
376
+ }
377
+
378
+ const updatedState = applySessionState(authUser, cwd, result, shellEnv);
379
+ authUser = updatedState.authUser;
380
+ cwd = updatedState.cwd;
381
+
382
+ if (result.closeSession) {
383
+ await flushVfs();
384
+ rl.close();
385
+ process.exit(result.exitCode ?? 0);
386
+ }
387
+ }
91
388
 
92
389
  if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
93
- const password = await askQuestion(rl, `Password for ${authUser}: `);
390
+ const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
94
391
  if (!virtualShell.users.verifyPassword(authUser, password)) {
95
392
  process.stderr.write("self-standalone: authentication failed\n");
96
393
  process.exit(1);
@@ -114,12 +411,16 @@ async function runReadlineShell() {
114
411
  });
115
412
 
116
413
  rl.on("close", () => {
117
- console.log("")
118
- process.exit(0);
414
+ void (async () => {
415
+ await flushVfs();
416
+ console.log("");
417
+ process.exit(0);
418
+ })();
119
419
  });
120
420
 
121
421
  stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
122
422
  writeLastLogin(authUser, remoteAddress);
423
+ await flushVfs();
123
424
  prompt();
124
425
 
125
426
  while (true) {
@@ -128,37 +429,19 @@ async function runReadlineShell() {
128
429
  });
129
430
 
130
431
  rl.pause();
131
-
132
- const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
133
-
134
- if (result.stdout) {
135
- stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
136
- }
137
-
138
- if (result.stderr) {
139
- process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
140
- }
141
-
142
- if (result.clearScreen) {
143
- stdout.write("\u001b[2J\u001b[H");
432
+ if (inputLine.trim().length > 0) {
433
+ history.push(inputLine);
434
+ if (history.length > 500) {
435
+ history = history.slice(history.length - 500);
436
+ }
437
+ saveHistory(history);
438
+ rlWithHistory.history = [...history].reverse();
144
439
  }
145
440
 
146
- if (result.switchUser) {
147
- authUser = result.switchUser;
148
- cwd = result.nextCwd ?? `/home/${authUser}`;
149
- shellEnv.vars.USER = authUser;
150
- shellEnv.vars.LOGNAME = authUser;
151
- shellEnv.vars.HOME = `/home/${authUser}`;
152
- shellEnv.vars.PWD = cwd;
153
- } else if (result.nextCwd) {
154
- cwd = result.nextCwd;
155
- shellEnv.vars.PWD = cwd;
156
- }
441
+ const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
442
+ await handleCommandResult(result);
157
443
 
158
- if (result.closeSession) {
159
- rl.close();
160
- process.exit(result.exitCode ?? 0);
161
- }
444
+ await flushVfs();
162
445
 
163
446
  prompt();
164
447
  rl.resume();
@@ -31,6 +31,8 @@ export interface CommandResult {
31
31
  openHtop?: boolean;
32
32
  /** Request sudo password challenge flow. */
33
33
  sudoChallenge?: SudoChallenge;
34
+ /** Request a generic password challenge (adduser, passwd). */
35
+ passwordChallenge?: PasswordChallenge;
34
36
  }
35
37
 
36
38
  /** Deferred sudo challenge metadata returned by sudo command. */
@@ -45,6 +47,41 @@ export interface SudoChallenge {
45
47
  loginShell: boolean;
46
48
  /** Prompt text shown before password input. */
47
49
  prompt: string;
50
+ /**
51
+ * Challenge mode.
52
+ * - `"sudo"` (default): verify `username`'s password, then run `commandLine`.
53
+ * - `"passwd"`: multi-step new-password flow; `onPassword` handles each step.
54
+ * - `"confirm"`: text confirmation flow (e.g. deluser); `onPassword` receives typed text.
55
+ */
56
+ mode?: "sudo" | "passwd" | "confirm";
57
+ /**
58
+ * Optional async handler called when the user submits input.
59
+ * Receives the typed text and the shell instance.
60
+ * Returns a `CommandResult` written to the terminal, or `null` to show
61
+ * another prompt (pass `nextPrompt` to change the prompt text).
62
+ */
63
+ onPassword?: (input: string, shell: import("../VirtualShell").VirtualShell) => Promise<{
64
+ result: CommandResult | null;
65
+ nextPrompt?: string;
66
+ }>;
67
+ }
68
+
69
+ /** Generic password challenge — used by adduser, passwd, deluser. */
70
+ export interface PasswordChallenge {
71
+ /** Lines to print before the first prompt. */
72
+ preamble?: string;
73
+ /** Primary prompt text (e.g. "New password: "). */
74
+ prompt: string;
75
+ /** If set, a second prompt is shown for confirmation. */
76
+ confirmPrompt?: string;
77
+ /** Prompt shown for a destructive confirmation (y/N). */
78
+ confirmText?: string;
79
+ /** Tag identifying what to do with the entered value. */
80
+ action: "adduser" | "passwd" | "deluser" | "su";
81
+ /** Username targeted by the action. */
82
+ targetUsername: string;
83
+ /** For adduser: the new user's username (already validated). */
84
+ newUsername?: string;
48
85
  }
49
86
 
50
87
  /** State payload used by nano command interactive editor flow. */
@@ -0,0 +1,78 @@
1
+ /**
2
+ * tokenize.ts
3
+ *
4
+ * Shared shell tokenizer used by `shellParser.ts` and `runtime.ts`.
5
+ * Splits a shell input string into tokens respecting single and double
6
+ * quotes, and separates `>`, `>>`, `<` as standalone redirect tokens.
7
+ */
8
+
9
+ /**
10
+ * Tokenize a shell command line respecting quoted strings and redirect
11
+ * operators.
12
+ *
13
+ * - Single-quoted content is preserved verbatim.
14
+ * - Double-quoted content is preserved (expansion happens later).
15
+ * - `>`, `>>`, and `<` are emitted as standalone tokens.
16
+ *
17
+ * @param input Raw shell command string.
18
+ * @returns Array of string tokens.
19
+ */
20
+ export function tokenizeCommand(input: string): string[] {
21
+ const tokens: string[] = [];
22
+ let current = "";
23
+ let inQ = false;
24
+ let qChar = "";
25
+ let i = 0;
26
+
27
+ while (i < input.length) {
28
+ const ch = input[i]!;
29
+ const next = input[i + 1];
30
+
31
+ if ((ch === '"' || ch === "'") && !inQ) {
32
+ inQ = true;
33
+ qChar = ch;
34
+ i++;
35
+ continue;
36
+ }
37
+ if (inQ && ch === qChar) {
38
+ inQ = false;
39
+ qChar = "";
40
+ i++;
41
+ continue;
42
+ }
43
+ if (inQ) {
44
+ current += ch;
45
+ i++;
46
+ continue;
47
+ }
48
+
49
+ if (ch === " ") {
50
+ if (current) {
51
+ tokens.push(current);
52
+ current = "";
53
+ }
54
+ i++;
55
+ continue;
56
+ }
57
+
58
+ if ((ch === ">" || ch === "<") && !inQ) {
59
+ if (current) {
60
+ tokens.push(current);
61
+ current = "";
62
+ }
63
+ if (ch === ">" && next === ">") {
64
+ tokens.push(">>");
65
+ i += 2;
66
+ } else {
67
+ tokens.push(ch);
68
+ i++;
69
+ }
70
+ continue;
71
+ }
72
+
73
+ current += ch;
74
+ i++;
75
+ }
76
+ if (current) tokens.push(current);
77
+ return tokens;
78
+ }
@@ -1,13 +0,0 @@
1
- "use strict";var WebShellLib=(()=>{var p=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var R=(o,e,t)=>e in o?p(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var F=(o,e)=>{for(var t in e)p(o,t,{get:e[t],enumerable:!0})},L=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of z(e))!O.call(o,n)&&n!==t&&p(o,n,{get:()=>e[n],enumerable:!(r=A(e,n))||r.enumerable});return o};var _=o=>L(p({},"__esModule",{value:!0}),o);var u=(o,e,t)=>R(o,typeof e!="symbol"?e+"":e,t);var K={};F(K,{IndexedDbMirrorVfs:()=>b,WebShell:()=>y,createWebShell:()=>J});function w(o){let e=o.trim();if(!e)return{statements:[],isValid:!0};try{return{statements:I(e),isValid:!0}}catch(t){return{statements:[],isValid:!1,error:t.message}}}function I(o){let e=k(o),t=[];for(let r of e){let s={pipeline:{commands:M(r.text.trim()),isValid:!0}};r.op&&(s.op=r.op),t.push(s)}return t}function k(o){let e=[],t="",r=0,n=!1,s="",i=0,a=c=>{t.trim()&&e.push({text:t,op:c}),t=""};for(;i<o.length;){let c=o[i],d=o.slice(i,i+2);if((c==='"'||c==="'")&&!n){n=!0,s=c,t+=c,i++;continue}if(n&&c===s){n=!1,t+=c,i++;continue}if(n){t+=c,i++;continue}if(c==="("){r++,t+=c,i++;continue}if(c===")"){r--,t+=c,i++;continue}if(r>0){t+=c,i++;continue}if(d==="&&"){a("&&"),i+=2;continue}if(d==="||"){a("||"),i+=2;continue}if(c===";"){a(";"),i++;continue}t+=c,i++}return a(),e}function M(o){return B(o).map(T)}function B(o){let e=[],t="",r=!1,n="";for(let i=0;i<o.length;i++){let a=o[i];if((a==='"'||a==="'")&&!r){r=!0,n=a,t+=a;continue}if(r&&a===n){r=!1,t+=a;continue}if(r){t+=a;continue}if(a==="|"&&o[i+1]!=="|"){if(!t.trim())throw new Error("Syntax error near unexpected token '|'");e.push(t.trim()),t=""}else t+=a}let s=t.trim();if(!s&&e.length>0)throw new Error("Syntax error near unexpected token '|'");return s&&e.push(s),e}function T(o){let e=j(o);if(e.length===0)return{name:"",args:[]};let t=[],r,n,s=!1,i=0;for(;i<e.length;){let c=e[i];if(c==="<"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after <");r=e[i],i++}else if(c===">>"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after >>");n=e[i],s=!0,i++}else if(c===">"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after >");n=e[i],s=!1,i++}else t.push(c),i++}return{name:(t[0]??"").toLowerCase(),args:t.slice(1),inputFile:r,outputFile:n,appendOutput:s}}function j(o){let e=[],t="",r=!1,n="",s=0;for(;s<o.length;){let i=o[s],a=o[s+1];if((i==='"'||i==="'")&&!r){r=!0,n=i,s++;continue}if(r&&i===n){r=!1,n="",s++;continue}if(r){t+=i,s++;continue}if(i===" "){t&&(e.push(t),t=""),s++;continue}if((i===">"||i==="<")&&!r){t&&(e.push(t),t=""),i===">"&&a===">"?(e.push(">>"),s+=2):(e.push(i),s++);continue}t+=i,s++}return t&&e.push(t),e}function V(o,e){let t=o.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g,(r,n)=>{let s=e[n];return s!==void 0&&s!==""?s:"0"});if(!/^[\d\s+\-*/%()^!&|<>=,. ]+$/.test(t))return NaN;try{let r=Function(`"use strict"; return (${t.replace(/\*\*/g,"**")});`)();return typeof r=="number"?Math.trunc(r):NaN}catch{return NaN}}function U(o,e){let t=[],r=0;for(;r<o.length;){let n=o.indexOf("'",r);if(n===-1){t.push(e(o.slice(r)));break}t.push(e(o.slice(r,n)));let s=o.indexOf("'",n+1);if(s===-1){t.push(o.slice(n));break}t.push(o.slice(n,s+1)),r=s+1}return t.join("")}function m(o,e,t=0,r){let n=r??e.HOME??"/home/user";return U(o,s=>{let i=s;return i=i.replace(/(^|[\s:])~(\/|$)/g,(a,c,d)=>`${c}${n}${d}`),i=i.replace(/\$\?/g,String(t)),i=i.replace(/\$\$/g,"1"),i=i.replace(/\$#/g,"0"),i=i.replace(/\$\(\(([^)]+(?:\([^)]*\)[^)]*)*)\)\)/g,(a,c)=>{let d=V(c,e);return Number.isNaN(d)?"0":String(d)}),i=i.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,c)=>String((e[c]??"").length)),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):-([^}]*)\}/g,(a,c,d)=>e[c]!==void 0&&e[c]!==""?e[c]:d),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):=([^}]*)\}/g,(a,c,d)=>((e[c]===void 0||e[c]==="")&&(e[c]=d),e[c])),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g,(a,c,d)=>e[c]!==void 0&&e[c]!==""?d:""),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,c)=>e[c]??""),i=i.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g,(a,c)=>e[c]??""),i})}async function C(o,e,t,r){if(o.includes("$(")){let n="",s=!1,i=0;for(;i<o.length;){let a=o[i];if(a==="'"&&!s){s=!0,n+=a,i++;continue}if(a==="'"&&s){s=!1,n+=a,i++;continue}if(!s&&a==="$"&&o[i+1]==="("){if(o[i+2]==="("){n+=a,i++;continue}let c=0,d=i+1;for(;d<o.length;){if(o[d]==="(")c++;else if(o[d]===")"&&(c--,c===0))break;d++}let x=o.slice(i+2,d).trim(),P=(await r(x)).replace(/\n$/,"");n+=P,i=d+1;continue}n+=a,i++}o=n}return m(o,e,t)}var Z=new TextEncoder,q=new TextDecoder;function H(o){let e="";for(let t of o)e+=String.fromCharCode(t);return btoa(e)}function N(o){let e=atob(o),t=new Uint8Array(e.length);for(let r=0;r<e.length;r+=1)t[r]=e.charCodeAt(r);return t}function l(o,e="/"){let r=(o.startsWith("/")?o:`${e}/${o}`).split("/"),n=[];for(let s of r)if(!(!s||s===".")){if(s===".."){n.pop();continue}n.push(s)}return`/${n.join("/")}`||"/"}function h(o){let e=l(o);if(e==="/")return"/";let t=e.split("/").filter(Boolean);return t.pop(),t.length>0?`/${t.join("/")}`:"/"}function f(o){let e=l(o);return e==="/"?"/":e.split("/").filter(Boolean).at(-1)??"/"}function D(o){return o.type==="file"?{...o,contentBase64:o.contentBase64}:{...o,children:o.children.map(e=>D(e))}}function W(o,e){let t=new Date().toISOString();return{type:"directory",name:o,mode:e,createdAt:t,updatedAt:t,children:[]}}function Q(o,e,t){let r=new Date().toISOString();return{type:"file",name:o,mode:t,createdAt:r,updatedAt:r,contentBase64:H(e)}}function v(o,e){return o.children.find(t=>t.name===e)}function g(o,e){let t=o.children.findIndex(r=>r.name===e.name);if(t===-1){o.children.push(e);return}o.children[t]=e}function S(o,e){o.children=o.children.filter(t=>t.name!==e)}function $(o){return l(o).split("/").filter(Boolean)}var G=globalThis;function E(o){return new Promise((e,t)=>{o.addEventListener("success",()=>e(o.result)),o.addEventListener("error",()=>t(o.error))})}var b=class{constructor(e={}){u(this,"databaseName");u(this,"storeName");u(this,"key");u(this,"root");this.databaseName=e.databaseName??"typescript-virtual-container-web",this.storeName=e.storeName??"snapshots",this.key=e.key??"current",this.root=W("",493)}async openDatabase(){return new Promise((e,t)=>{let r=G.indexedDB;if(!r){t(new Error("IndexedDB is not available in this environment"));return}let n=r.open(this.databaseName,1);n.addEventListener("upgradeneeded",()=>{let s=n.result;s.objectStoreNames.contains(this.storeName)||s.createObjectStore(this.storeName)}),n.addEventListener("success",()=>e(n.result)),n.addEventListener("error",()=>t(n.error))})}async readSnapshot(){let e=await this.openDatabase();try{let n=e.transaction(this.storeName,"readonly").objectStore(this.storeName).get(this.key),s=await E(n);return s?JSON.parse(s):null}finally{e.close()}}async writeSnapshot(e){let t=await this.openDatabase();try{let r=t.transaction(this.storeName,"readwrite"),n=r.objectStore(this.storeName);await E(n.put(JSON.stringify(e),this.key)),await new Promise((s,i)=>{r.addEventListener("complete",()=>s()),r.addEventListener("error",()=>i(r.error)),r.addEventListener("abort",()=>i(r.error))})}finally{t.close()}}serializeNode(e){return e.type==="file"?{...e}:{...e,children:e.children.map(t=>this.serializeNode(t))}}deserializeNode(e){return e.type==="file"?{...e}:{...e,children:e.children.map(t=>this.deserializeNode(t))}}getNode(e){let t=l(e);if(t==="/")return this.root;let r=$(t),n=this.root;for(let s of r){if(n.type!=="directory")throw new Error(`Not a directory: ${t}`);let i=v(n,s);if(!i)throw new Error(`No such file or directory: ${t}`);n=i}return n}ensureDirectory(e,t){let r=l(e);if(r==="/")return this.root;let n=$(r),s=this.root;for(let i of n){let a=v(s,i);if(!a){let c=W(i,t);g(s,c),s=c;continue}if(a.type!=="directory")throw new Error(`Cannot create directory '${r}': path is a file.`);s=a}return s}removeNode(e,t){let r=l(e);if(r==="/")throw new Error("Cannot remove root directory");let n=this.getNode(h(r));if(n.type!=="directory")throw new Error(`Not a directory: ${h(r)}`);let s=f(r),i=v(n,s);if(!i)throw new Error(`No such file or directory: ${r}`);if(i.type==="directory"&&i.children.length>0&&!t)throw new Error(`Cannot remove '${r}': directory not empty.`);S(n,s)}copyNode(e){return e.type==="file"?{...e,contentBase64:e.contentBase64}:{...e,children:e.children.map(t=>this.copyNode(t))}}async restoreMirror(){let e=await this.readSnapshot();e&&(this.root=this.deserializeNode(e.root))}async flushMirror(){await this.writeSnapshot({root:this.serializeNode(this.root)})}exists(e){try{return this.getNode(e),!0}catch{return!1}}list(e){let t=this.getNode(e);if(t.type!=="directory")throw new Error(`Not a directory: ${e}`);return t.children.map(r=>r.name).sort((r,n)=>r.localeCompare(n))}stat(e){let t=this.getNode(e);return t.type==="file"?{type:"file",mode:t.mode,size:N(t.contentBase64).byteLength,name:t.name}:{type:"directory",mode:t.mode,size:0,name:t.name}}readFile(e){let t=this.getNode(e);if(t.type!=="file")throw new Error(`Is a directory: ${e}`);return q.decode(N(t.contentBase64))}writeFile(e,t,r=420){let n=l(e),s=this.ensureDirectory(h(n),493),i=typeof t=="string"?Z.encode(t):t,a=Q(f(n),i,r);g(s,a)}mkdir(e,t=493){this.ensureDirectory(e,t)}touch(e){this.exists(e)||this.writeFile(e,"")}move(e,t){let r=this.getNode(e),n=this.getNode(h(e)),s=this.ensureDirectory(h(t),493);if(n.type!=="directory")throw new Error(`Not a directory: ${h(e)}`);S(n,f(e));let i=D(r);i.name=f(t),g(s,i)}copy(e,t){let r=this.getNode(e),n=this.ensureDirectory(h(t),493),s=this.copyNode(r);s.name=f(t),g(n,s)}remove(e,t={}){this.removeNode(e,t.recursive??!1)}exportSnapshot(){return{root:this.serializeNode(this.root)}}importSnapshot(e){this.root=this.deserializeNode(e.root)}},y=class{constructor(e,t={}){u(this,"hostname");u(this,"vfs");u(this,"env");u(this,"cwd");u(this,"commands",new Map);u(this,"initialized",!1);this.hostname=e,this.cwd=t.cwd??"/home/root",this.env={vars:{PATH:"/usr/bin:/bin",HOME:"/home/root",USER:"root",LOGNAME:"root",SHELL:"/bin/sh",HOSTNAME:e,PWD:this.cwd},lastExitCode:0},this.vfs=new b(t.vfs),this.registerBuiltins()}register(e){this.commands.set(e.name,e);for(let t of e.aliases??[])this.commands.set(t,e)}registerBuiltins(){this.register({name:"help",description:"List available web commands",params:[],run:()=>({stdout:`${this.listCommands().join(`
2
- `)}
3
- `,exitCode:0})}),this.register({name:"pwd",description:"Print current directory",params:[],run:()=>({stdout:`${this.cwd}
4
- `,exitCode:0})}),this.register({name:"cd",description:"Change current directory",params:["[dir]"],run:({args:e})=>{let t=e[0]?l(e[0],this.cwd):"/home/root";return!this.vfs.exists(t)||this.vfs.stat(t).type!=="directory"?{stderr:`cd: no such file or directory: ${t}`,exitCode:1}:(this.cwd=t,this.env.vars.PWD=t,{exitCode:0,nextCwd:t})}}),this.register({name:"echo",description:"Display text",params:["[-n] [-e] [text...]"],run:({args:e,stdin:t})=>{let r=e.includes("-n"),n=e.filter(a=>a!=="-n"&&a!=="-e"&&a!=="-E"),s=n.length>0?n.join(" "):t??"",i=m(s,this.env.vars,this.env.lastExitCode,this.env.vars.HOME);return{stdout:r?i:`${i}
5
- `,exitCode:0}}}),this.register({name:"env",description:"Print environment variables",params:[],run:()=>({stdout:`${Object.entries(this.env.vars).map(([e,t])=>`${e}=${t}`).join(`
6
- `)}
7
- `,exitCode:0})}),this.register({name:"export",description:"Set environment variables",params:["KEY=VALUE..."],run:({args:e})=>{for(let t of e){let r=t.indexOf("=");if(r===-1)continue;let n=t.slice(0,r).trim(),s=t.slice(r+1);n&&(this.env.vars[n]=s)}return{exitCode:0}}}),this.register({name:"unset",description:"Unset environment variables",params:["NAME..."],run:({args:e})=>{for(let t of e)delete this.env.vars[t];return{exitCode:0}}}),this.register({name:"mkdir",description:"Create directories",params:["[-p] dir..."],run:async({args:e})=>{let t=e.filter(r=>r!=="-p");for(let r of t)this.vfs.mkdir(l(r,this.cwd));return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"touch",description:"Create files",params:["file..."],run:async({args:e})=>{for(let t of e)this.vfs.touch(l(t,this.cwd));return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"rm",description:"Remove files or directories",params:["[-r] [-f] path..."],run:async({args:e})=>{let t=e.includes("-r"),r=e.filter(n=>n!=="-r"&&n!=="-f");for(let n of r)this.vfs.remove(l(n,this.cwd),{recursive:t});return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"cp",description:"Copy files or directories",params:["[-r] source destination"],run:async({args:e})=>{let t=e.includes("-r"),r=e.filter(s=>s!=="-r");if(r.length<2)return{stderr:"cp: missing destination file operand",exitCode:1};let n=l(r.at(-1),this.cwd);for(let s of r.slice(0,-1)){let i=l(s,this.cwd);if(!t&&this.vfs.stat(i).type==="directory")return{stderr:`cp: -r not specified; omitting directory '${i}'`,exitCode:1};this.vfs.copy(i,n)}return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"mv",description:"Move or rename files",params:["source destination"],run:async({args:e})=>{if(e.length<2)return{stderr:"mv: missing destination file operand",exitCode:1};let t=l(e[0],this.cwd),r=l(e[1],this.cwd);return this.vfs.move(t,r),await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"cat",description:"Concatenate files",params:["[file...]"],run:({args:e,stdin:t})=>{if(e.length===0)return{stdout:t??"",exitCode:0};let r="";for(let n of e)r+=this.vfs.readFile(l(n,this.cwd));return{stdout:r,exitCode:0}}}),this.register({name:"ls",description:"List files",params:["[path]"],run:({args:e})=>{let t=l(e[0]??".",this.cwd);return{stdout:`${this.vfs.list(t).join(" ")}
8
- `,exitCode:0}}}),this.register({name:"tee",description:"Read from stdin and write to files",params:["[-a] file..."],run:async({args:e,stdin:t})=>{let r=e.includes("-a"),n=e.filter(i=>i!=="-a"),s=t??"";for(let i of n){let a=l(i,this.cwd);if(r&&this.vfs.exists(a)){let c=this.vfs.readFile(a);this.vfs.writeFile(a,`${c}${s}`)}else this.vfs.writeFile(a,s)}return await this.vfs.flushMirror(),{stdout:s,exitCode:0}}}),this.register({name:"curl",description:"Fetch a URL and optionally write to a file",params:["[-o file] URL"],run:async({args:e})=>{let t=e.indexOf("-o"),r=t!==-1?e[t+1]:void 0,s=e.filter((c,d)=>c!=="-o"&&d!==t+1).at(-1);if(!s)return{stderr:"curl: missing URL",exitCode:2};let i=await fetch(s),a=await i.text();return r?(this.vfs.writeFile(l(r,this.cwd),a),await this.vfs.flushMirror(),{exitCode:i.ok?0:1}):{stdout:a,exitCode:i.ok?0:1}}}),this.register({name:"wget",description:"Fetch a URL and optionally write to a file",params:["[-O file] URL"],run:async({args:e})=>{let t=e.indexOf("-O"),r=t!==-1?e[t+1]:void 0,s=e.filter((d,x)=>d!=="-O"&&x!==t+1).at(-1);if(!s)return{stderr:"wget: missing URL",exitCode:2};let i=await fetch(s),a=await i.text(),c=r??f(new URL(s).pathname||"index.html");return this.vfs.writeFile(l(c,this.cwd),a),await this.vfs.flushMirror(),{exitCode:i.ok?0:1}}}),this.register({name:"true",description:"Return success",params:[],run:()=>({exitCode:0})}),this.register({name:"false",description:"Return failure",params:[],run:()=>({exitCode:1})})}listCommands(){let e=new Map;for(let t of this.commands.values())e.set(t.name,t);return Array.from(e.values()).sort((t,r)=>t.name.localeCompare(r.name)).map(t=>`${t.name}${t.params.length>0?` ${t.params.join(" ")}`:""}`)}resolveCommand(e){return this.commands.get(e.toLowerCase())}async ensureInitialized(){this.initialized||(await this.vfs.restoreMirror(),this.vfs.exists("/home")||this.vfs.mkdir("/home"),this.vfs.exists("/home/root")||(this.vfs.mkdir("/home/root"),this.vfs.writeFile("/home/root/README.txt",`Welcome to ${this.hostname}
9
- `)),this.vfs.exists("/tmp")||this.vfs.mkdir("/tmp"),this.vfs.exists("/etc")||this.vfs.mkdir("/etc"),this.vfs.exists("/etc/hostname")||this.vfs.writeFile("/etc/hostname",`${this.hostname}
10
- `),this.vfs.exists("/etc/hosts")||this.vfs.writeFile("/etc/hosts",`127.0.0.1 localhost
11
- ::1 localhost
12
- `),this.initialized=!0)}getCurrentWorkingDirectory(){return this.cwd}async executeCommandLine(e,t=!0){await this.ensureInitialized();let r=e.trim();if(!r)return{exitCode:0};let n=await C(r,this.env.vars,this.env.lastExitCode,a=>this.executeCommandLine(a,!1).then(c=>c.stdout??"")),s=w(n),i=await this.executeStatements(s.statements);return this.env.lastExitCode=i.exitCode??0,t&&await this.vfs.flushMirror(),i}async executeStatements(e){let t={exitCode:0},r=0;for(;r<e.length;){let n=e[r];if(t=await this.executePipeline(n.pipeline.commands),this.env.lastExitCode=t.exitCode??0,t.closeSession||t.switchUser)return t;let s=n.op;if(!(!s||s===";")){if(s==="&&"){if((t.exitCode??0)!==0)for(;r<e.length&&e[r]?.op==="&&";)r+=1}else if(s==="||"&&(t.exitCode??0)===0)for(;r<e.length&&e[r]?.op==="||";)r+=1}r+=1}return t}async executePipeline(e){return e.length===0?{exitCode:0}:e.length===1?this.executeSingleCommandWithRedirections(e[0]):this.executePipelineChain(e)}async executeSingleCommandWithRedirections(e){let t;if(e.inputFile){let n=l(e.inputFile,this.cwd);try{t=this.vfs.readFile(n)}catch{return{stderr:`${e.inputFile}: No such file or directory`,exitCode:1}}}let r=await this.executeCommand(e.name,e.args,t);if(e.outputFile){let n=l(e.outputFile,this.cwd),s=r.stdout??"";if(e.appendOutput&&this.vfs.exists(n)){let i=this.vfs.readFile(n);this.vfs.writeFile(n,`${i}${s}`)}else this.vfs.writeFile(n,s);return{...r,stdout:""}}return r}async executePipelineChain(e){let t="",r=0;for(let n=0;n<e.length;n+=1){let s=e[n];if(n===0&&s.inputFile){let a=l(s.inputFile,this.cwd);try{t=this.vfs.readFile(a)}catch{return{stderr:`${s.inputFile}: No such file or directory`,exitCode:1}}}let i=await this.executeCommand(s.name,s.args,t);t=i.stdout??"",r=i.exitCode??0}return{stdout:t,exitCode:r}}async executeCommand(e,t,r){let n=this.resolveCommand(e);if(!n)return{stderr:`${e}: command not found`,exitCode:127};let i={args:t.map(c=>m(c,this.env.vars,this.env.lastExitCode,this.env.vars.HOME)),stdin:r,cwd:this.cwd,env:this.env,rawInput:`${e} ${t.join(" ")}`.trim(),shell:this},a=await n.run(i);return a.nextCwd&&(this.cwd=a.nextCwd,this.env.vars.PWD=a.nextCwd),a}};function J(o="typescript-vm",e={}){return new y(o,e)}return _(K);})();
13
- //# sourceMappingURL=web-iife.min.js.map