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
@@ -8,6 +8,7 @@ import {
8
8
  } from "../modules/linuxRootfs";
9
9
  import type { CommandContext, CommandResult } from "../types/commands";
10
10
  import type { ShellStream } from "../types/streams";
11
+ import type { VfsNodeStats } from "../types/vfs";
11
12
  import type { PerfLogger } from "../utils/perfLogger";
12
13
  import { createPerfLogger } from "../utils/perfLogger";
13
14
  import VirtualFileSystem, { type VfsOptions } from "../VirtualFileSystem";
@@ -40,6 +41,56 @@ export interface ShellProperties {
40
41
  arch: string;
41
42
  }
42
43
 
44
+ export interface VirtualShellVfsLike {
45
+ restoreMirror(): Promise<void>;
46
+ flushMirror(): Promise<void>;
47
+ writeFile(targetPath: string, content: string | Uint8Array): void;
48
+ readFile(targetPath: string): string;
49
+ mkdir(targetPath: string, mode?: number): void;
50
+ exists(targetPath: string): boolean;
51
+ stat(targetPath: string): VfsNodeStats;
52
+ list(targetPath: string): string[];
53
+ remove(targetPath: string, options?: { recursive?: boolean }): void;
54
+ chmod?(targetPath: string, mode: number): void;
55
+ symlink?(targetPath: string, linkPath: string): void;
56
+ getUsageBytes?(targetPath?: string): number;
57
+ }
58
+
59
+ export interface VirtualShellVfsOptions {
60
+ vfsInstance?: VirtualShellVfsLike;
61
+ }
62
+
63
+ function hasVfsInstance(obj: unknown): obj is { vfsInstance: VirtualShellVfsLike } {
64
+ return (
65
+ typeof obj === "object" &&
66
+ obj !== null &&
67
+ "vfsInstance" in obj &&
68
+ isVirtualShellVfsLike((obj as Record<string, unknown>).vfsInstance)
69
+ );
70
+ }
71
+
72
+ function isVirtualShellVfsLike(value: unknown): value is VirtualShellVfsLike {
73
+ if (typeof value !== "object" || value === null) {
74
+ return false;
75
+ }
76
+
77
+ const candidate = value as Record<string, unknown>;
78
+ return (
79
+ typeof candidate.restoreMirror === "function" &&
80
+ typeof candidate.flushMirror === "function" &&
81
+ typeof candidate.writeFile === "function" &&
82
+ typeof candidate.readFile === "function" &&
83
+ typeof candidate.mkdir === "function" &&
84
+ typeof candidate.exists === "function" &&
85
+ typeof candidate.stat === "function" &&
86
+ typeof candidate.list === "function" &&
87
+ typeof candidate.remove === "function" &&
88
+ typeof candidate.copy === "function" &&
89
+ typeof candidate.move === "function" &&
90
+ typeof candidate.touch === "function"
91
+ );
92
+ }
93
+
43
94
  const defaultShellProperties: ShellProperties = {
44
95
  kernel: "1.0.0+itsrealfortune+1-amd64",
45
96
  os: "Fortune GNU/Linux x64",
@@ -104,14 +155,21 @@ class VirtualShell extends EventEmitter {
104
155
  constructor(
105
156
  hostname: string,
106
157
  properties?: ShellProperties,
107
- vfsOptions?: VfsOptions,
158
+ vfsOptionsOrInstance?: VfsOptions | VirtualShellVfsLike | VirtualShellVfsOptions,
108
159
  ) {
109
160
  super();
110
161
  perf.mark("constructor");
111
162
  this.hostname = hostname;
112
163
  this.properties = properties || defaultShellProperties;
113
164
  this.startTime = Date.now();
114
- this.vfs = new VirtualFileSystem(vfsOptions ?? {});
165
+
166
+ if (isVirtualShellVfsLike(vfsOptionsOrInstance)) {
167
+ this.vfs = vfsOptionsOrInstance as unknown as VirtualFileSystem;
168
+ } else if (hasVfsInstance(vfsOptionsOrInstance)) {
169
+ this.vfs = vfsOptionsOrInstance.vfsInstance as unknown as VirtualFileSystem;
170
+ } else {
171
+ this.vfs = new VirtualFileSystem((vfsOptionsOrInstance as VfsOptions) ?? {});
172
+ }
115
173
  this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
116
174
  this.packageManager = new VirtualPackageManager(this.vfs, this.users);
117
175
 
@@ -3,7 +3,6 @@ import { readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import type { ShellProperties, VirtualShell } from ".";
5
5
  import { getCommandNames, makeDefaultEnv, runCommand } from "../commands";
6
- import type { ShellEnv } from "../types/commands";
7
6
  import {
8
7
  spawnHtopProcess,
9
8
  spawnNanoEditorProcess,
@@ -14,8 +13,9 @@ import {
14
13
  type TerminalSize,
15
14
  toTtyLines,
16
15
  } from "../modules/shellRuntime";
17
- import { formatLoginDate } from "../SSHMimic/loginFormat";
16
+ import { buildLoginBanner } from "../SSHMimic/loginBanner";
18
17
  import { buildPrompt } from "../SSHMimic/prompt";
18
+ import type { ShellEnv } from "../types/commands";
19
19
  import type { ShellStream } from "../types/streams";
20
20
  import type VirtualFileSystem from "../VirtualFileSystem";
21
21
 
@@ -411,35 +411,7 @@ export function startShell(
411
411
  function renderLoginBanner(): void {
412
412
  const last = readLastLogin();
413
413
  const nowIso = new Date().toISOString();
414
-
415
- stream.write(
416
- `Linux ${hostname} ${properties.kernel} ${properties.arch}\r\n`,
417
- );
418
- stream.write("\r\n");
419
- stream.write(
420
- "The programs included with the Fortune GNU/Linux system are free software;\r\n",
421
- );
422
- stream.write(
423
- "the exact distribution terms for each program are described in the\r\n",
424
- );
425
- stream.write("individual files in /usr/share/doc/*/copyright.\r\n");
426
- stream.write("\r\n");
427
- stream.write(
428
- "Fortune GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent\r\n",
429
- );
430
- stream.write("permitted by applicable law.\r\n");
431
-
432
- if (last) {
433
- const when = new Date(last.at);
434
- const displayed = Number.isNaN(when.getTime())
435
- ? last.at
436
- : formatLoginDate(when);
437
- stream.write(
438
- `Last login: ${displayed} from ${last.from || "unknown"}\r\n`,
439
- );
440
- }
441
-
442
- stream.write("\r\n");
414
+ stream.write(buildLoginBanner(hostname, properties, last));
443
415
  writeLastLogin(nowIso);
444
416
  }
445
417
 
@@ -0,0 +1,183 @@
1
+ import { basename } from "node:path";
2
+ import { stdin, stdout } from "node:process";
3
+ import { createInterface, type Interface } from "node:readline";
4
+
5
+ import { makeDefaultEnv, runCommand } from "./commands/runtime";
6
+ import { buildLoginBanner, type LoginBannerState } from "./SSHMimic/loginBanner";
7
+ import { buildPrompt } from "./SSHMimic/prompt";
8
+ import { VirtualShell } from "./VirtualShell";
9
+
10
+ const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
11
+ const argv = process.argv.slice(2);
12
+
13
+ function readUserArg(): string {
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const current = argv[index];
16
+ if (current === "--user") {
17
+ const next = argv[index + 1];
18
+ if (!next || next.startsWith("--")) {
19
+ throw new Error("self-standalone: --user requires a value");
20
+ }
21
+ return next;
22
+ }
23
+ if (current?.startsWith("--user=")) {
24
+ return current.slice("--user=".length) || "root";
25
+ }
26
+ }
27
+
28
+ return "root";
29
+ }
30
+
31
+ const initialUser = readUserArg();
32
+ const virtualShell = new VirtualShell(hostname, undefined, {
33
+ mode: "fs",
34
+ snapshotPath: ".vfs",
35
+ });
36
+
37
+ function readLastLogin(username: string): LoginBannerState | null {
38
+ const lastlogPath = `/virtual-env-js/.lastlog/${username}.json`;
39
+ if (!virtualShell.vfs.exists(lastlogPath)) {
40
+ return null;
41
+ }
42
+
43
+ try {
44
+ return JSON.parse(virtualShell.vfs.readFile(lastlogPath)) as LoginBannerState;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function askQuestion(rl: Interface, promptText: string): Promise<string> {
51
+ return new Promise((resolve) => {
52
+ rl.question(promptText, resolve);
53
+ });
54
+ }
55
+
56
+ function writeLastLogin(username: string, from: string): void {
57
+ const dir = "/virtual-env-js/.lastlog";
58
+ if (!virtualShell.vfs.exists(dir)) {
59
+ virtualShell.vfs.mkdir(dir, 0o700);
60
+ }
61
+
62
+ virtualShell.vfs.writeFile(
63
+ `/virtual-env-js/.lastlog/${username}.json`,
64
+ JSON.stringify({ at: new Date().toISOString(), from }),
65
+ );
66
+ }
67
+
68
+ virtualShell.addCommand("demo", [], () => {
69
+ return {
70
+ stdout: "This is a demo command. It does nothing useful.",
71
+ exitCode: 0,
72
+ };
73
+ });
74
+
75
+ async function runReadlineShell() {
76
+ const rl = createInterface({ input: stdin, output: stdout, terminal: true });
77
+ await virtualShell.ensureInitialized();
78
+
79
+ const selectedUser = initialUser.trim() || "root";
80
+ const userExists = virtualShell.users.getPasswordHash(selectedUser) !== null;
81
+ if (!userExists) {
82
+ process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
83
+ process.exit(1);
84
+ }
85
+
86
+ const shellEnv = makeDefaultEnv(selectedUser, hostname);
87
+ let authUser = selectedUser;
88
+ let cwd = `/home/${authUser}`;
89
+ shellEnv.vars.PWD = cwd;
90
+ const remoteAddress = "localhost";
91
+
92
+ if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
93
+ const password = await askQuestion(rl, `Password for ${authUser}: `);
94
+ if (!virtualShell.users.verifyPassword(authUser, password)) {
95
+ process.stderr.write("self-standalone: authentication failed\n");
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ const renderPrompt = (): string => {
101
+ const cwdLabel = cwd === `/home/${authUser}` ? "~" : basename(cwd) || "/";
102
+ return buildPrompt(authUser, hostname, cwdLabel);
103
+ };
104
+
105
+ const prompt = (): void => {
106
+ rl.setPrompt(renderPrompt());
107
+ rl.prompt();
108
+ };
109
+
110
+ rl.on("SIGINT", () => {
111
+ stdout.write("^C\n");
112
+ rl.write("", { ctrl: true, name: "u" });
113
+ prompt();
114
+ });
115
+
116
+ rl.on("close", () => {
117
+ console.log("")
118
+ process.exit(0);
119
+ });
120
+
121
+ stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
122
+ writeLastLogin(authUser, remoteAddress);
123
+ prompt();
124
+
125
+ while (true) {
126
+ const inputLine = await new Promise<string>((resolve) => {
127
+ rl.once("line", (line) => resolve(line));
128
+ });
129
+
130
+ 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");
144
+ }
145
+
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
+ }
157
+
158
+ if (result.closeSession) {
159
+ rl.close();
160
+ process.exit(result.exitCode ?? 0);
161
+ }
162
+
163
+ prompt();
164
+ rl.resume();
165
+ }
166
+ }
167
+
168
+ runReadlineShell().catch((error: unknown) => {
169
+ console.error("Failed to start readline SSH emulation:", error);
170
+ process.exit(1);
171
+ });
172
+
173
+ process.on("uncaughtException", (error) => {
174
+ console.log("Oh my god, something terrible happened: ", error);
175
+ });
176
+
177
+ process.on("unhandledRejection", (error, promise) => {
178
+ console.log(
179
+ " Oh Lord! We forgot to handle a promise rejection here: ",
180
+ promise,
181
+ );
182
+ console.log(" The error was: ", error);
183
+ });
package/src/web-api.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { CommandResult, ShellEnv, } from "./types/commands";
2
+ import { createWebShell } from "./web";
3
+
4
+ /**
5
+ * Browser shim that exposes a VirtualShell-like API backed by the WebShell
6
+ * implementation (IndexedDB mirror). This avoids importing Node-only
7
+ * modules while providing a familiar surface for browser demos.
8
+ */
9
+ class VirtualShellShim {
10
+ private readonly shell: ReturnType<typeof createWebShell>;
11
+
12
+ constructor(hostname = "typescript-vm") {
13
+ this.shell = createWebShell(hostname, {});
14
+ }
15
+
16
+ async ensureInitialized(): Promise<void> {
17
+ await this.shell.ensureInitialized();
18
+ }
19
+
20
+ /** Execute a command line and return the command result. */
21
+ async executeCommandLine(raw: string): Promise<CommandResult> {
22
+ return this.shell.executeCommandLine(raw);
23
+ }
24
+
25
+ /** Alias for executeCommandLine to match some APIs. */
26
+ async run(raw: string): Promise<CommandResult> {
27
+ return this.executeCommandLine(raw);
28
+ }
29
+
30
+ getVfs() {
31
+ return this.shell.vfs;
32
+ }
33
+
34
+ getHostname() {
35
+ return this.shell.hostname;
36
+ }
37
+
38
+ /** Minimal compatibility: write a file as user (no quota checks in browser). */
39
+ writeFileAsUser(_authUser: string, targetPath: string, content: string | Uint8Array) {
40
+ return this.shell.vfs.writeFile(targetPath, content);
41
+ }
42
+
43
+ /** Expose env for simple inspection */
44
+ getEnv(): ShellEnv {
45
+ return this.shell.env as ShellEnv;
46
+ }
47
+
48
+ /** Register a custom command into the web shell. */
49
+ addCommand(name: string, params: string[], callback: (ctx: unknown) => CommandResult | Promise<CommandResult>) {
50
+ // WebShell keeps a private register() method; cast to a minimal register shape.
51
+ const registrable = this.shell as unknown as {
52
+ register(cmd: { name: string; params: string[]; aliases: string[]; description: string; run: (c: unknown) => CommandResult | Promise<CommandResult> }): void;
53
+ };
54
+ registrable.register({ name, params, aliases: [], description: "", run: callback });
55
+ }
56
+ }
57
+
58
+ export function createVirtualShellShim(hostname?: string) {
59
+ return new VirtualShellShim(hostname);
60
+ }
61
+
62
+ export type { VirtualShellShim };
@@ -0,0 +1,11 @@
1
+ import VirtualFileSystem from "./VirtualFileSystem";
2
+ import { VirtualShell } from "./VirtualShell";
3
+
4
+ export function createFullWebShell(hostname = "typescript-vm") {
5
+ // Use the real VirtualFileSystem so the existing shell runtime gets the full
6
+ // API surface it expects. The default persistence mode is in-memory, which
7
+ // keeps this usable in browser bundles without a host filesystem.
8
+ return new VirtualShell(hostname, undefined, new VirtualFileSystem({}));
9
+ }
10
+
11
+ export type { VirtualShell } from "./VirtualShell";