typescript-virtual-container 1.3.1 → 1.3.2
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.
- package/README.md +61 -1
- package/builds/self-standalone.js +481 -0
- package/builds/self-standalone.js.map +7 -0
- package/{standalone-wo-sftp.js → builds/standalone-wo-sftp.js} +137 -137
- package/{standalone-wo-sftp.js.map → builds/standalone-wo-sftp.js.map} +4 -4
- package/{standalone.js → builds/standalone.js} +176 -176
- package/{standalone.js.map → builds/standalone.js.map} +4 -4
- package/builds/web-full-api.min.js +13 -0
- package/builds/web-full-api.min.js.map +7 -0
- package/builds/web-iife.min.js +13 -0
- package/builds/web-iife.min.js.map +7 -0
- package/builds/web.min.js +13 -0
- package/builds/web.min.js.map +7 -0
- package/dist/SSHMimic/loginBanner.d.ts +7 -0
- package/dist/SSHMimic/loginBanner.d.ts.map +1 -0
- package/dist/SSHMimic/loginBanner.js +22 -0
- package/dist/VirtualShell/index.d.ts +21 -1
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +34 -2
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +2 -17
- package/dist/self-standalone.d.ts +2 -0
- package/dist/self-standalone.d.ts.map +1 -0
- package/dist/self-standalone.js +147 -0
- package/dist/web-api.d.ts +26 -0
- package/dist/web-api.d.ts.map +1 -0
- package/dist/web-api.js +46 -0
- package/dist/web-full.d.ts +4 -0
- package/dist/web-full.d.ts.map +1 -0
- package/dist/web-full.js +8 -0
- package/dist/web.d.ts +108 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +773 -0
- package/examples/README.md +81 -3
- package/examples/app-iife.js +58 -0
- package/examples/app.js +28 -0
- package/examples/index-cf.html +27 -0
- package/examples/index.html +27 -0
- package/examples/server.js +55 -0
- package/examples/web-iife.min.js +13 -0
- package/examples/web.min.js +13 -0
- package/package.json +10 -3
- package/polyfills/node:child_process/index.js +2 -0
- package/polyfills/node:crypto/index.js +7 -0
- package/polyfills/node:events/index.js +9 -0
- package/polyfills/node:fs/index.js +8 -0
- package/polyfills/node:fs/promises.js +4 -0
- package/polyfills/node:os/index.js +9 -0
- package/polyfills/node:path/index.js +14 -0
- package/polyfills/node:vm/index.js +7 -0
- package/polyfills/node:zlib/index.js +3 -0
- package/src/SSHMimic/loginBanner.ts +36 -0
- package/src/VirtualShell/index.ts +60 -2
- package/src/VirtualShell/shell.ts +3 -31
- package/src/self-standalone.ts +183 -0
- package/src/web-api.ts +62 -0
- package/src/web-full.ts +11 -0
- package/src/web.ts +930 -0
- package/tests/web.test.ts +182 -0
|
@@ -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 {
|
|
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 };
|
package/src/web-full.ts
ADDED
|
@@ -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";
|