typescript-virtual-container 1.3.1 → 1.3.3

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 (59) hide show
  1. package/README.md +61 -1
  2. package/builds/self-standalone.js +482 -0
  3. package/builds/self-standalone.js.map +7 -0
  4. package/{standalone-wo-sftp.js → builds/standalone-wo-sftp.js} +144 -153
  5. package/{standalone-wo-sftp.js.map → builds/standalone-wo-sftp.js.map} +4 -4
  6. package/{standalone.js → builds/standalone.js} +61 -70
  7. package/{standalone.js.map → builds/standalone.js.map} +4 -4
  8. package/builds/web-full-api.min.js +13 -0
  9. package/builds/web-full-api.min.js.map +7 -0
  10. package/builds/web-iife.min.js +13 -0
  11. package/builds/web-iife.min.js.map +7 -0
  12. package/builds/web.min.js +13 -0
  13. package/builds/web.min.js.map +7 -0
  14. package/dist/SSHMimic/loginBanner.d.ts +7 -0
  15. package/dist/SSHMimic/loginBanner.d.ts.map +1 -0
  16. package/dist/SSHMimic/loginBanner.js +22 -0
  17. package/dist/VirtualShell/index.d.ts +21 -1
  18. package/dist/VirtualShell/index.d.ts.map +1 -1
  19. package/dist/VirtualShell/index.js +34 -2
  20. package/dist/VirtualShell/shell.d.ts.map +1 -1
  21. package/dist/VirtualShell/shell.js +2 -17
  22. package/dist/self-standalone.d.ts +2 -0
  23. package/dist/self-standalone.d.ts.map +1 -0
  24. package/dist/self-standalone.js +147 -0
  25. package/dist/web-api.d.ts +26 -0
  26. package/dist/web-api.d.ts.map +1 -0
  27. package/dist/web-api.js +46 -0
  28. package/dist/web-full.d.ts +4 -0
  29. package/dist/web-full.d.ts.map +1 -0
  30. package/dist/web-full.js +8 -0
  31. package/dist/web.d.ts +108 -0
  32. package/dist/web.d.ts.map +1 -0
  33. package/dist/web.js +773 -0
  34. package/examples/README.md +81 -3
  35. package/examples/app-iife.js +58 -0
  36. package/examples/app.js +28 -0
  37. package/examples/index-cf.html +27 -0
  38. package/examples/index.html +27 -0
  39. package/examples/server.js +55 -0
  40. package/examples/web-iife.min.js +13 -0
  41. package/examples/web.min.js +13 -0
  42. package/package.json +12 -5
  43. package/polyfills/node_child_process/index.js +2 -0
  44. package/polyfills/node_crypto/index.js +7 -0
  45. package/polyfills/node_events/index.js +9 -0
  46. package/polyfills/node_fs/index.js +8 -0
  47. package/polyfills/node_fs/promises.js +4 -0
  48. package/polyfills/node_os/index.js +9 -0
  49. package/polyfills/node_path/index.js +14 -0
  50. package/polyfills/node_vm/index.js +7 -0
  51. package/polyfills/node_zlib/index.js +3 -0
  52. package/src/SSHMimic/loginBanner.ts +36 -0
  53. package/src/VirtualShell/index.ts +60 -2
  54. package/src/VirtualShell/shell.ts +3 -31
  55. package/src/self-standalone.ts +183 -0
  56. package/src/web-api.ts +62 -0
  57. package/src/web-full.ts +11 -0
  58. package/src/web.ts +930 -0
  59. package/tests/web.test.ts +182 -0
package/dist/web.js ADDED
@@ -0,0 +1,773 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: env vars */
2
+ import { parseScript } from "./VirtualShell/shellParser";
3
+ import { expandAsync, expandSync } from "./utils/expand";
4
+ const textEncoder = new TextEncoder();
5
+ const textDecoder = new TextDecoder();
6
+ function encodeBase64(bytes) {
7
+ let binary = "";
8
+ for (const byte of bytes)
9
+ binary += String.fromCharCode(byte);
10
+ return btoa(binary);
11
+ }
12
+ function decodeBase64(base64) {
13
+ const binary = atob(base64);
14
+ const bytes = new Uint8Array(binary.length);
15
+ for (let index = 0; index < binary.length; index += 1) {
16
+ bytes[index] = binary.charCodeAt(index);
17
+ }
18
+ return bytes;
19
+ }
20
+ function normalizePath(inputPath, cwd = "/") {
21
+ const raw = inputPath.startsWith("/") ? inputPath : `${cwd}/${inputPath}`;
22
+ const parts = raw.split("/");
23
+ const stack = [];
24
+ for (const part of parts) {
25
+ if (!part || part === ".")
26
+ continue;
27
+ if (part === "..") {
28
+ stack.pop();
29
+ continue;
30
+ }
31
+ stack.push(part);
32
+ }
33
+ return `/${stack.join("/")}` || "/";
34
+ }
35
+ function dirname(inputPath) {
36
+ const normalized = normalizePath(inputPath);
37
+ if (normalized === "/")
38
+ return "/";
39
+ const parts = normalized.split("/").filter(Boolean);
40
+ parts.pop();
41
+ return parts.length > 0 ? `/${parts.join("/")}` : "/";
42
+ }
43
+ function basename(inputPath) {
44
+ const normalized = normalizePath(inputPath);
45
+ if (normalized === "/")
46
+ return "/";
47
+ const parts = normalized.split("/").filter(Boolean);
48
+ return parts.at(-1) ?? "/";
49
+ }
50
+ function cloneNode(node) {
51
+ if (node.type === "file") {
52
+ return {
53
+ ...node,
54
+ contentBase64: node.contentBase64,
55
+ };
56
+ }
57
+ return {
58
+ ...node,
59
+ children: node.children.map((child) => cloneNode(child)),
60
+ };
61
+ }
62
+ function makeDirectory(name, mode) {
63
+ const now = new Date().toISOString();
64
+ return {
65
+ type: "directory",
66
+ name,
67
+ mode,
68
+ createdAt: now,
69
+ updatedAt: now,
70
+ children: [],
71
+ };
72
+ }
73
+ function makeFile(name, content, mode) {
74
+ const now = new Date().toISOString();
75
+ return {
76
+ type: "file",
77
+ name,
78
+ mode,
79
+ createdAt: now,
80
+ updatedAt: now,
81
+ contentBase64: encodeBase64(content),
82
+ };
83
+ }
84
+ function findChild(directory, name) {
85
+ return directory.children.find((child) => child.name === name);
86
+ }
87
+ function setChild(directory, node) {
88
+ const index = directory.children.findIndex((child) => child.name === node.name);
89
+ if (index === -1) {
90
+ directory.children.push(node);
91
+ return;
92
+ }
93
+ directory.children[index] = node;
94
+ }
95
+ function removeChild(directory, name) {
96
+ directory.children = directory.children.filter((child) => child.name !== name);
97
+ }
98
+ function splitPath(pathValue) {
99
+ return normalizePath(pathValue).split("/").filter(Boolean);
100
+ }
101
+ const webGlobal = globalThis;
102
+ function promisifyRequest(request) {
103
+ return new Promise((resolve, reject) => {
104
+ request.addEventListener("success", () => resolve(request.result));
105
+ request.addEventListener("error", () => reject(request.error));
106
+ });
107
+ }
108
+ class IndexedDbMirrorVfs {
109
+ databaseName;
110
+ storeName;
111
+ key;
112
+ root;
113
+ constructor(options = {}) {
114
+ this.databaseName = options.databaseName ?? "typescript-virtual-container-web";
115
+ this.storeName = options.storeName ?? "snapshots";
116
+ this.key = options.key ?? "current";
117
+ this.root = makeDirectory("", 0o755);
118
+ }
119
+ async openDatabase() {
120
+ return new Promise((resolve, reject) => {
121
+ const indexedDbFactory = webGlobal.indexedDB;
122
+ if (!indexedDbFactory) {
123
+ reject(new Error("IndexedDB is not available in this environment"));
124
+ return;
125
+ }
126
+ const request = indexedDbFactory.open(this.databaseName, 1);
127
+ request.addEventListener("upgradeneeded", () => {
128
+ const database = request.result;
129
+ if (!database.objectStoreNames.contains(this.storeName)) {
130
+ database.createObjectStore(this.storeName);
131
+ }
132
+ });
133
+ request.addEventListener("success", () => resolve(request.result));
134
+ request.addEventListener("error", () => reject(request.error));
135
+ });
136
+ }
137
+ async readSnapshot() {
138
+ const database = await this.openDatabase();
139
+ try {
140
+ const transaction = database.transaction(this.storeName, "readonly");
141
+ const store = transaction.objectStore(this.storeName);
142
+ const request = store.get(this.key);
143
+ const result = (await promisifyRequest(request));
144
+ if (!result)
145
+ return null;
146
+ return JSON.parse(result);
147
+ }
148
+ finally {
149
+ database.close();
150
+ }
151
+ }
152
+ async writeSnapshot(snapshot) {
153
+ const database = await this.openDatabase();
154
+ try {
155
+ const transaction = database.transaction(this.storeName, "readwrite");
156
+ const store = transaction.objectStore(this.storeName);
157
+ await promisifyRequest(store.put(JSON.stringify(snapshot), this.key));
158
+ await new Promise((resolve, reject) => {
159
+ transaction.addEventListener("complete", () => resolve());
160
+ transaction.addEventListener("error", () => reject(transaction.error));
161
+ transaction.addEventListener("abort", () => reject(transaction.error));
162
+ });
163
+ }
164
+ finally {
165
+ database.close();
166
+ }
167
+ }
168
+ serializeNode(node) {
169
+ if (node.type === "file")
170
+ return { ...node };
171
+ return {
172
+ ...node,
173
+ children: node.children.map((child) => this.serializeNode(child)),
174
+ };
175
+ }
176
+ deserializeNode(node) {
177
+ if (node.type === "file")
178
+ return { ...node };
179
+ return {
180
+ ...node,
181
+ children: node.children.map((child) => this.deserializeNode(child)),
182
+ };
183
+ }
184
+ getNode(targetPath) {
185
+ const normalized = normalizePath(targetPath);
186
+ if (normalized === "/")
187
+ return this.root;
188
+ const parts = splitPath(normalized);
189
+ let current = this.root;
190
+ for (const part of parts) {
191
+ if (current.type !== "directory") {
192
+ throw new Error(`Not a directory: ${normalized}`);
193
+ }
194
+ const child = findChild(current, part);
195
+ if (!child)
196
+ throw new Error(`No such file or directory: ${normalized}`);
197
+ current = child;
198
+ }
199
+ return current;
200
+ }
201
+ ensureDirectory(targetPath, mode) {
202
+ const normalized = normalizePath(targetPath);
203
+ if (normalized === "/")
204
+ return this.root;
205
+ const parts = splitPath(normalized);
206
+ let current = this.root;
207
+ for (const part of parts) {
208
+ const existing = findChild(current, part);
209
+ if (!existing) {
210
+ const created = makeDirectory(part, mode);
211
+ setChild(current, created);
212
+ current = created;
213
+ continue;
214
+ }
215
+ if (existing.type !== "directory") {
216
+ throw new Error(`Cannot create directory '${normalized}': path is a file.`);
217
+ }
218
+ current = existing;
219
+ }
220
+ return current;
221
+ }
222
+ removeNode(targetPath, recursive) {
223
+ const normalized = normalizePath(targetPath);
224
+ if (normalized === "/")
225
+ throw new Error("Cannot remove root directory");
226
+ const parent = this.getNode(dirname(normalized));
227
+ if (parent.type !== "directory")
228
+ throw new Error(`Not a directory: ${dirname(normalized)}`);
229
+ const name = basename(normalized);
230
+ const node = findChild(parent, name);
231
+ if (!node)
232
+ throw new Error(`No such file or directory: ${normalized}`);
233
+ if (node.type === "directory" && node.children.length > 0 && !recursive) {
234
+ throw new Error(`Cannot remove '${normalized}': directory not empty.`);
235
+ }
236
+ removeChild(parent, name);
237
+ }
238
+ copyNode(node) {
239
+ if (node.type === "file") {
240
+ return {
241
+ ...node,
242
+ contentBase64: node.contentBase64,
243
+ };
244
+ }
245
+ return {
246
+ ...node,
247
+ children: node.children.map((child) => this.copyNode(child)),
248
+ };
249
+ }
250
+ async restoreMirror() {
251
+ const snapshot = await this.readSnapshot();
252
+ if (!snapshot)
253
+ return;
254
+ this.root = this.deserializeNode(snapshot.root);
255
+ }
256
+ async flushMirror() {
257
+ await this.writeSnapshot({ root: this.serializeNode(this.root) });
258
+ }
259
+ exists(targetPath) {
260
+ try {
261
+ this.getNode(targetPath);
262
+ return true;
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
268
+ list(targetPath) {
269
+ const node = this.getNode(targetPath);
270
+ if (node.type !== "directory")
271
+ throw new Error(`Not a directory: ${targetPath}`);
272
+ return node.children.map((child) => child.name).sort((a, b) => a.localeCompare(b));
273
+ }
274
+ stat(targetPath) {
275
+ const node = this.getNode(targetPath);
276
+ if (node.type === "file") {
277
+ return {
278
+ type: "file",
279
+ mode: node.mode,
280
+ size: decodeBase64(node.contentBase64).byteLength,
281
+ name: node.name,
282
+ };
283
+ }
284
+ return { type: "directory", mode: node.mode, size: 0, name: node.name };
285
+ }
286
+ readFile(targetPath) {
287
+ const node = this.getNode(targetPath);
288
+ if (node.type !== "file")
289
+ throw new Error(`Is a directory: ${targetPath}`);
290
+ return textDecoder.decode(decodeBase64(node.contentBase64));
291
+ }
292
+ writeFile(targetPath, content, mode = 0o644) {
293
+ const normalized = normalizePath(targetPath);
294
+ const parent = this.ensureDirectory(dirname(normalized), 0o755);
295
+ const bytes = typeof content === "string" ? textEncoder.encode(content) : content;
296
+ const file = makeFile(basename(normalized), bytes, mode);
297
+ setChild(parent, file);
298
+ }
299
+ mkdir(targetPath, mode = 0o755) {
300
+ this.ensureDirectory(targetPath, mode);
301
+ }
302
+ touch(targetPath) {
303
+ if (this.exists(targetPath))
304
+ return;
305
+ this.writeFile(targetPath, "");
306
+ }
307
+ move(fromPath, toPath) {
308
+ const source = this.getNode(fromPath);
309
+ const sourceParent = this.getNode(dirname(fromPath));
310
+ const destinationParent = this.ensureDirectory(dirname(toPath), 0o755);
311
+ if (sourceParent.type !== "directory")
312
+ throw new Error(`Not a directory: ${dirname(fromPath)}`);
313
+ removeChild(sourceParent, basename(fromPath));
314
+ const clone = cloneNode(source);
315
+ clone.name = basename(toPath);
316
+ setChild(destinationParent, clone);
317
+ }
318
+ copy(fromPath, toPath) {
319
+ const source = this.getNode(fromPath);
320
+ const destinationParent = this.ensureDirectory(dirname(toPath), 0o755);
321
+ const clone = this.copyNode(source);
322
+ clone.name = basename(toPath);
323
+ setChild(destinationParent, clone);
324
+ }
325
+ remove(targetPath, options = {}) {
326
+ this.removeNode(targetPath, options.recursive ?? false);
327
+ }
328
+ exportSnapshot() {
329
+ return { root: this.serializeNode(this.root) };
330
+ }
331
+ importSnapshot(snapshot) {
332
+ this.root = this.deserializeNode(snapshot.root);
333
+ }
334
+ }
335
+ class WebShell {
336
+ hostname;
337
+ vfs;
338
+ env;
339
+ cwd;
340
+ commands = new Map();
341
+ initialized = false;
342
+ constructor(hostname, options = {}) {
343
+ this.hostname = hostname;
344
+ this.cwd = options.cwd ?? "/home/root";
345
+ this.env = {
346
+ vars: {
347
+ PATH: "/usr/bin:/bin",
348
+ HOME: "/home/root",
349
+ USER: "root",
350
+ LOGNAME: "root",
351
+ SHELL: "/bin/sh",
352
+ HOSTNAME: hostname,
353
+ PWD: this.cwd,
354
+ },
355
+ lastExitCode: 0,
356
+ };
357
+ this.vfs = new IndexedDbMirrorVfs(options.vfs);
358
+ this.registerBuiltins();
359
+ }
360
+ register(command) {
361
+ this.commands.set(command.name.toLowerCase(), command);
362
+ for (const alias of command.aliases ?? []) {
363
+ this.commands.set(alias.toLowerCase(), command);
364
+ }
365
+ }
366
+ registerBuiltins() {
367
+ this.register({
368
+ name: "help",
369
+ description: "List available web commands",
370
+ params: [],
371
+ run: () => ({ stdout: `${this.listCommands().join("\n")}\n`, exitCode: 0 }),
372
+ });
373
+ this.register({
374
+ name: "pwd",
375
+ description: "Print current directory",
376
+ params: [],
377
+ run: () => ({ stdout: `${this.cwd}\n`, exitCode: 0 }),
378
+ });
379
+ this.register({
380
+ name: "cd",
381
+ description: "Change current directory",
382
+ params: ["[dir]"],
383
+ run: ({ args }) => {
384
+ const target = args[0] ? normalizePath(args[0], this.cwd) : "/home/root";
385
+ if (!this.vfs.exists(target) || this.vfs.stat(target).type !== "directory") {
386
+ return { stderr: `cd: no such file or directory: ${target}`, exitCode: 1 };
387
+ }
388
+ this.cwd = target;
389
+ this.env.vars.PWD = target;
390
+ return { exitCode: 0, nextCwd: target };
391
+ },
392
+ });
393
+ this.register({
394
+ name: "echo",
395
+ description: "Display text",
396
+ params: ["[-n] [-e] [text...]"],
397
+ run: ({ args, stdin }) => {
398
+ const noNewline = args.includes("-n");
399
+ const raw = args.filter((arg) => arg !== "-n" && arg !== "-e" && arg !== "-E");
400
+ const text = raw.length > 0 ? raw.join(" ") : (stdin ?? "");
401
+ const expanded = expandSync(text, this.env.vars, this.env.lastExitCode, this.env.vars.HOME);
402
+ return { stdout: noNewline ? expanded : `${expanded}\n`, exitCode: 0 };
403
+ },
404
+ });
405
+ this.register({
406
+ name: "env",
407
+ description: "Print environment variables",
408
+ params: [],
409
+ run: () => ({ stdout: `${Object.entries(this.env.vars).map(([key, value]) => `${key}=${value}`).join("\n")}\n`, exitCode: 0 }),
410
+ });
411
+ this.register({
412
+ name: "export",
413
+ description: "Set environment variables",
414
+ params: ["KEY=VALUE..."],
415
+ run: ({ args }) => {
416
+ for (const arg of args) {
417
+ const eq = arg.indexOf("=");
418
+ if (eq === -1)
419
+ continue;
420
+ const key = arg.slice(0, eq).trim();
421
+ const value = arg.slice(eq + 1);
422
+ if (key)
423
+ this.env.vars[key] = value;
424
+ }
425
+ return { exitCode: 0 };
426
+ },
427
+ });
428
+ this.register({
429
+ name: "unset",
430
+ description: "Unset environment variables",
431
+ params: ["NAME..."],
432
+ run: ({ args }) => {
433
+ for (const name of args)
434
+ delete this.env.vars[name];
435
+ return { exitCode: 0 };
436
+ },
437
+ });
438
+ this.register({
439
+ name: "mkdir",
440
+ description: "Create directories",
441
+ params: ["[-p] dir..."],
442
+ run: async ({ args }) => {
443
+ const paths = args.filter((arg) => arg !== "-p");
444
+ for (const target of paths) {
445
+ this.vfs.mkdir(normalizePath(target, this.cwd));
446
+ }
447
+ await this.vfs.flushMirror();
448
+ return { exitCode: 0 };
449
+ },
450
+ });
451
+ this.register({
452
+ name: "touch",
453
+ description: "Create files",
454
+ params: ["file..."],
455
+ run: async ({ args }) => {
456
+ for (const target of args) {
457
+ this.vfs.touch(normalizePath(target, this.cwd));
458
+ }
459
+ await this.vfs.flushMirror();
460
+ return { exitCode: 0 };
461
+ },
462
+ });
463
+ this.register({
464
+ name: "rm",
465
+ description: "Remove files or directories",
466
+ params: ["[-r] [-f] path..."],
467
+ run: async ({ args }) => {
468
+ const recursive = args.includes("-r");
469
+ const targets = args.filter((arg) => arg !== "-r" && arg !== "-f");
470
+ for (const target of targets) {
471
+ this.vfs.remove(normalizePath(target, this.cwd), { recursive });
472
+ }
473
+ await this.vfs.flushMirror();
474
+ return { exitCode: 0 };
475
+ },
476
+ });
477
+ this.register({
478
+ name: "cp",
479
+ description: "Copy files or directories",
480
+ params: ["[-r] source destination"],
481
+ run: async ({ args }) => {
482
+ const recursive = args.includes("-r");
483
+ const items = args.filter((arg) => arg !== "-r");
484
+ if (items.length < 2)
485
+ return { stderr: "cp: missing destination file operand", exitCode: 1 };
486
+ const dest = normalizePath(items.at(-1), this.cwd);
487
+ for (const source of items.slice(0, -1)) {
488
+ const sourcePath = normalizePath(source, this.cwd);
489
+ if (!recursive && this.vfs.stat(sourcePath).type === "directory") {
490
+ return { stderr: `cp: -r not specified; omitting directory '${sourcePath}'`, exitCode: 1 };
491
+ }
492
+ this.vfs.copy(sourcePath, dest);
493
+ }
494
+ await this.vfs.flushMirror();
495
+ return { exitCode: 0 };
496
+ },
497
+ });
498
+ this.register({
499
+ name: "mv",
500
+ description: "Move or rename files",
501
+ params: ["source destination"],
502
+ run: async ({ args }) => {
503
+ if (args.length < 2)
504
+ return { stderr: "mv: missing destination file operand", exitCode: 1 };
505
+ const source = normalizePath(args[0], this.cwd);
506
+ const destination = normalizePath(args[1], this.cwd);
507
+ this.vfs.move(source, destination);
508
+ await this.vfs.flushMirror();
509
+ return { exitCode: 0 };
510
+ },
511
+ });
512
+ this.register({
513
+ name: "cat",
514
+ description: "Concatenate files",
515
+ params: ["[file...]"],
516
+ run: ({ args, stdin }) => {
517
+ if (args.length === 0)
518
+ return { stdout: stdin ?? "", exitCode: 0 };
519
+ let output = "";
520
+ for (const source of args) {
521
+ output += this.vfs.readFile(normalizePath(source, this.cwd));
522
+ }
523
+ return { stdout: output, exitCode: 0 };
524
+ },
525
+ });
526
+ this.register({
527
+ name: "ls",
528
+ description: "List files",
529
+ params: ["[path]"],
530
+ run: ({ args }) => {
531
+ const target = normalizePath(args[0] ?? ".", this.cwd);
532
+ const entries = this.vfs.list(target);
533
+ return { stdout: `${entries.join(" ")}\n`, exitCode: 0 };
534
+ },
535
+ });
536
+ this.register({
537
+ name: "tee",
538
+ description: "Read from stdin and write to files",
539
+ params: ["[-a] file..."],
540
+ run: async ({ args, stdin }) => {
541
+ const append = args.includes("-a");
542
+ const targets = args.filter((arg) => arg !== "-a");
543
+ const content = stdin ?? "";
544
+ for (const target of targets) {
545
+ const normalized = normalizePath(target, this.cwd);
546
+ if (append && this.vfs.exists(normalized)) {
547
+ const current = this.vfs.readFile(normalized);
548
+ this.vfs.writeFile(normalized, `${current}${content}`);
549
+ }
550
+ else {
551
+ this.vfs.writeFile(normalized, content);
552
+ }
553
+ }
554
+ await this.vfs.flushMirror();
555
+ return { stdout: content, exitCode: 0 };
556
+ },
557
+ });
558
+ this.register({
559
+ name: "curl",
560
+ description: "Fetch a URL and optionally write to a file",
561
+ params: ["[-o file] URL"],
562
+ run: async ({ args }) => {
563
+ const outputIndex = args.indexOf("-o");
564
+ const outputTarget = outputIndex !== -1 ? args[outputIndex + 1] : undefined;
565
+ const filtered = args.filter((arg, index) => arg !== "-o" && index !== outputIndex + 1);
566
+ const url = filtered.at(-1);
567
+ if (!url)
568
+ return { stderr: "curl: missing URL", exitCode: 2 };
569
+ const response = await fetch(url);
570
+ const body = await response.text();
571
+ if (outputTarget) {
572
+ this.vfs.writeFile(normalizePath(outputTarget, this.cwd), body);
573
+ await this.vfs.flushMirror();
574
+ return { exitCode: response.ok ? 0 : 1 };
575
+ }
576
+ return { stdout: body, exitCode: response.ok ? 0 : 1 };
577
+ },
578
+ });
579
+ this.register({
580
+ name: "wget",
581
+ description: "Fetch a URL and optionally write to a file",
582
+ params: ["[-O file] URL"],
583
+ run: async ({ args }) => {
584
+ const outputIndex = args.indexOf("-O");
585
+ const outputTarget = outputIndex !== -1 ? args[outputIndex + 1] : undefined;
586
+ const filtered = args.filter((arg, index) => arg !== "-O" && index !== outputIndex + 1);
587
+ const url = filtered.at(-1);
588
+ if (!url)
589
+ return { stderr: "wget: missing URL", exitCode: 2 };
590
+ const response = await fetch(url);
591
+ const body = await response.text();
592
+ const filename = outputTarget ?? basename(new URL(url).pathname || "index.html");
593
+ this.vfs.writeFile(normalizePath(filename, this.cwd), body);
594
+ await this.vfs.flushMirror();
595
+ return { exitCode: response.ok ? 0 : 1 };
596
+ },
597
+ });
598
+ this.register({
599
+ name: "true",
600
+ description: "Return success",
601
+ params: [],
602
+ run: () => ({ exitCode: 0 }),
603
+ });
604
+ this.register({
605
+ name: "false",
606
+ description: "Return failure",
607
+ params: [],
608
+ run: () => ({ exitCode: 1 }),
609
+ });
610
+ }
611
+ listCommands() {
612
+ const unique = new Map();
613
+ for (const command of this.commands.values())
614
+ unique.set(command.name, command);
615
+ return Array.from(unique.values())
616
+ .sort((a, b) => a.name.localeCompare(b.name))
617
+ .map((command) => `${command.name}${command.params.length > 0 ? ` ${command.params.join(" ")}` : ""}`);
618
+ }
619
+ resolveCommand(name) {
620
+ return this.commands.get(name.toLowerCase());
621
+ }
622
+ async ensureInitialized() {
623
+ if (this.initialized)
624
+ return;
625
+ await this.vfs.restoreMirror();
626
+ if (!this.vfs.exists("/home"))
627
+ this.vfs.mkdir("/home");
628
+ if (!this.vfs.exists("/home/root")) {
629
+ this.vfs.mkdir("/home/root");
630
+ this.vfs.writeFile("/home/root/README.txt", `Welcome to ${this.hostname}\n`);
631
+ }
632
+ if (!this.vfs.exists("/tmp"))
633
+ this.vfs.mkdir("/tmp");
634
+ if (!this.vfs.exists("/etc"))
635
+ this.vfs.mkdir("/etc");
636
+ if (!this.vfs.exists("/etc/hostname"))
637
+ this.vfs.writeFile("/etc/hostname", `${this.hostname}\n`);
638
+ if (!this.vfs.exists("/etc/hosts")) {
639
+ this.vfs.writeFile("/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n");
640
+ }
641
+ this.initialized = true;
642
+ }
643
+ getCurrentWorkingDirectory() {
644
+ return this.cwd;
645
+ }
646
+ async executeCommandLine(rawInput, persist = true) {
647
+ await this.ensureInitialized();
648
+ const trimmed = rawInput.trim();
649
+ if (!trimmed)
650
+ return { exitCode: 0 };
651
+ const expanded = await expandAsync(trimmed, this.env.vars, this.env.lastExitCode, (subcommand) => this.executeCommandLine(subcommand, false).then((r) => r.stdout ?? ""));
652
+ const script = parseScript(expanded);
653
+ const result = await this.executeStatements(script.statements);
654
+ this.env.lastExitCode = result.exitCode ?? 0;
655
+ if (persist)
656
+ await this.vfs.flushMirror();
657
+ return result;
658
+ }
659
+ async executeStatements(statements) {
660
+ let last = { exitCode: 0 };
661
+ let index = 0;
662
+ while (index < statements.length) {
663
+ const stmt = statements[index];
664
+ last = await this.executePipeline(stmt.pipeline.commands);
665
+ this.env.lastExitCode = last.exitCode ?? 0;
666
+ if (last.closeSession || last.switchUser)
667
+ return last;
668
+ const op = stmt.op;
669
+ if (!op || op === ";") {
670
+ // continue
671
+ }
672
+ else if (op === "&&") {
673
+ if ((last.exitCode ?? 0) !== 0) {
674
+ while (index < statements.length && statements[index]?.op === "&&")
675
+ index += 1;
676
+ }
677
+ }
678
+ else if (op === "||") {
679
+ if ((last.exitCode ?? 0) === 0) {
680
+ while (index < statements.length && statements[index]?.op === "||")
681
+ index += 1;
682
+ }
683
+ }
684
+ index += 1;
685
+ }
686
+ return last;
687
+ }
688
+ async executePipeline(commands) {
689
+ if (commands.length === 0)
690
+ return { exitCode: 0 };
691
+ if (commands.length === 1) {
692
+ return this.executeSingleCommandWithRedirections(commands[0]);
693
+ }
694
+ return this.executePipelineChain(commands);
695
+ }
696
+ async executeSingleCommandWithRedirections(command) {
697
+ let stdin;
698
+ if (command.inputFile) {
699
+ const inputPath = normalizePath(command.inputFile, this.cwd);
700
+ try {
701
+ stdin = this.vfs.readFile(inputPath);
702
+ }
703
+ catch {
704
+ return { stderr: `${command.inputFile}: No such file or directory`, exitCode: 1 };
705
+ }
706
+ }
707
+ const result = await this.executeCommand(command.name, command.args, stdin);
708
+ if (command.outputFile) {
709
+ const outputPath = normalizePath(command.outputFile, this.cwd);
710
+ const output = result.stdout ?? "";
711
+ if (command.appendOutput && this.vfs.exists(outputPath)) {
712
+ const existing = this.vfs.readFile(outputPath);
713
+ this.vfs.writeFile(outputPath, `${existing}${output}`);
714
+ }
715
+ else {
716
+ this.vfs.writeFile(outputPath, output);
717
+ }
718
+ return { ...result, stdout: "" };
719
+ }
720
+ return result;
721
+ }
722
+ async executePipelineChain(commands) {
723
+ let currentOutput = "";
724
+ let exitCode = 0;
725
+ for (let index = 0; index < commands.length; index += 1) {
726
+ const command = commands[index];
727
+ if (index === 0 && command.inputFile) {
728
+ const inputPath = normalizePath(command.inputFile, this.cwd);
729
+ try {
730
+ currentOutput = this.vfs.readFile(inputPath);
731
+ }
732
+ catch {
733
+ return { stderr: `${command.inputFile}: No such file or directory`, exitCode: 1 };
734
+ }
735
+ }
736
+ const result = await this.executeCommand(command.name, command.args, currentOutput);
737
+ currentOutput = result.stdout ?? "";
738
+ exitCode = result.exitCode ?? 0;
739
+ }
740
+ return { stdout: currentOutput, exitCode };
741
+ }
742
+ async executeCommand(name, args, stdin) {
743
+ const command = this.resolveCommand(name);
744
+ if (!command) {
745
+ return { stderr: `${name}: command not found`, exitCode: 127 };
746
+ }
747
+ const expandedArgs = args.map((arg) => expandSync(arg, this.env.vars, this.env.lastExitCode, this.env.vars.HOME));
748
+ const context = {
749
+ args: expandedArgs,
750
+ stdin,
751
+ cwd: this.cwd,
752
+ env: this.env,
753
+ rawInput: `${name} ${args.join(" ")}`.trim(),
754
+ shell: this,
755
+ };
756
+ try {
757
+ const result = await command.run(context);
758
+ if (result.nextCwd) {
759
+ this.cwd = result.nextCwd;
760
+ this.env.vars.PWD = result.nextCwd;
761
+ }
762
+ return result;
763
+ }
764
+ catch (error) {
765
+ const message = error instanceof Error ? error.message : String(error);
766
+ return { stderr: message, exitCode: 1 };
767
+ }
768
+ }
769
+ }
770
+ export { IndexedDbMirrorVfs, WebShell };
771
+ export function createWebShell(hostname = "typescript-vm", options = {}) {
772
+ return new WebShell(hostname, options);
773
+ }