typescript-virtual-container 1.3.4 → 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.
- package/.vscode/settings.json +0 -1
- package/README.md +674 -1504
- package/benchmark-results.txt +21 -21
- package/builds/self-standalone.js +274 -208
- package/builds/self-standalone.js.map +4 -4
- package/builds/standalone-wo-sftp.js +201 -149
- package/builds/standalone-wo-sftp.js.map +4 -4
- package/builds/standalone.js +263 -211
- package/builds/standalone.js.map +4 -4
- package/builds/web-full-api.min.js +3 -3
- package/builds/web-full-api.min.js.map +4 -4
- package/builds/web.min.js +2 -2
- package/builds/web.min.js.map +4 -4
- package/bun.lock +14 -12
- package/dist/SSHClient/index.d.ts.map +1 -1
- package/dist/SSHClient/index.js +5 -3
- package/dist/SSHMimic/executor.d.ts +1 -3
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +20 -22
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +5 -3
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +26 -21
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +25 -3
- package/dist/VirtualShell/shellParser.d.ts +1 -8
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualShell/shellParser.js +2 -81
- package/dist/VirtualUserManager/index.d.ts +7 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +47 -16
- package/dist/commands/adduser.d.ts +10 -4
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +75 -12
- package/dist/commands/alias.d.ts +5 -0
- package/dist/commands/alias.d.ts.map +1 -1
- package/dist/commands/alias.js +5 -0
- package/dist/commands/apt.d.ts +5 -0
- package/dist/commands/apt.d.ts.map +1 -1
- package/dist/commands/apt.js +5 -0
- package/dist/commands/awk.d.ts +10 -8
- package/dist/commands/awk.d.ts.map +1 -1
- package/dist/commands/awk.js +156 -28
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +0 -3
- package/dist/commands/clear.d.ts +5 -0
- package/dist/commands/clear.d.ts.map +1 -1
- package/dist/commands/clear.js +5 -0
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +8 -0
- package/dist/commands/declare.d.ts +5 -0
- package/dist/commands/declare.d.ts.map +1 -1
- package/dist/commands/declare.js +5 -0
- package/dist/commands/deluser.d.ts +12 -0
- package/dist/commands/deluser.d.ts.map +1 -1
- package/dist/commands/deluser.js +72 -6
- package/dist/commands/df.d.ts +5 -0
- package/dist/commands/df.d.ts.map +1 -1
- package/dist/commands/df.js +5 -0
- package/dist/commands/du.d.ts +5 -0
- package/dist/commands/du.d.ts.map +1 -1
- package/dist/commands/du.js +5 -0
- package/dist/commands/export.d.ts +5 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +5 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +22 -4
- package/dist/commands/groups.d.ts +5 -0
- package/dist/commands/groups.d.ts.map +1 -1
- package/dist/commands/groups.js +5 -0
- package/dist/commands/gzip.d.ts +5 -2
- package/dist/commands/gzip.d.ts.map +1 -1
- package/dist/commands/gzip.js +48 -28
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +12 -3
- package/dist/commands/htop.d.ts +5 -0
- package/dist/commands/htop.d.ts.map +1 -1
- package/dist/commands/htop.js +5 -0
- package/dist/commands/kill.d.ts +5 -0
- package/dist/commands/kill.d.ts.map +1 -1
- package/dist/commands/kill.js +5 -0
- package/dist/commands/ln.d.ts +2 -0
- package/dist/commands/ln.d.ts.map +1 -1
- package/dist/commands/ln.js +22 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +15 -0
- package/dist/commands/lsb-release.d.ts +5 -0
- package/dist/commands/lsb-release.d.ts.map +1 -1
- package/dist/commands/lsb-release.js +5 -0
- package/dist/commands/mkdir.d.ts +5 -0
- package/dist/commands/mkdir.d.ts.map +1 -1
- package/dist/commands/mkdir.js +5 -0
- package/dist/commands/mv.d.ts +5 -0
- package/dist/commands/mv.d.ts.map +1 -1
- package/dist/commands/mv.js +5 -0
- package/dist/commands/nano.d.ts +5 -0
- package/dist/commands/nano.d.ts.map +1 -1
- package/dist/commands/nano.js +5 -0
- package/dist/commands/neofetch.d.ts +5 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +8 -5
- package/dist/commands/passwd.d.ts +8 -0
- package/dist/commands/passwd.d.ts.map +1 -1
- package/dist/commands/passwd.js +32 -11
- package/dist/commands/ping.d.ts +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -0
- package/dist/commands/printf.d.ts +5 -0
- package/dist/commands/printf.d.ts.map +1 -1
- package/dist/commands/printf.js +43 -12
- package/dist/commands/ps.d.ts +5 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +5 -0
- package/dist/commands/read.d.ts +5 -0
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +5 -0
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +4 -1
- package/dist/commands/rm.d.ts +5 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +5 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +1 -57
- package/dist/commands/sed.d.ts +5 -0
- package/dist/commands/sed.d.ts.map +1 -1
- package/dist/commands/sed.js +5 -0
- package/dist/commands/set.d.ts +5 -6
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +5 -22
- package/dist/commands/sh.d.ts +6 -0
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +6 -0
- package/dist/commands/shift.d.ts +10 -0
- package/dist/commands/shift.d.ts.map +1 -1
- package/dist/commands/shift.js +10 -0
- package/dist/commands/sleep.d.ts +5 -0
- package/dist/commands/sleep.d.ts.map +1 -1
- package/dist/commands/sleep.js +5 -0
- package/dist/commands/sort.d.ts +5 -0
- package/dist/commands/sort.d.ts.map +1 -1
- package/dist/commands/sort.js +5 -0
- package/dist/commands/source.d.ts +5 -0
- package/dist/commands/source.d.ts.map +1 -1
- package/dist/commands/source.js +5 -0
- package/dist/commands/stat.d.ts +7 -0
- package/dist/commands/stat.d.ts.map +1 -0
- package/dist/commands/stat.js +56 -0
- package/dist/commands/su.d.ts +13 -0
- package/dist/commands/su.d.ts.map +1 -1
- package/dist/commands/su.js +45 -14
- package/dist/commands/sudo.d.ts.map +1 -1
- package/dist/commands/sudo.js +5 -0
- package/dist/commands/tail.d.ts +5 -0
- package/dist/commands/tail.d.ts.map +1 -1
- package/dist/commands/tail.js +15 -3
- package/dist/commands/tar.d.ts +5 -0
- package/dist/commands/tar.d.ts.map +1 -1
- package/dist/commands/tar.js +40 -10
- package/dist/commands/tee.d.ts +5 -0
- package/dist/commands/tee.d.ts.map +1 -1
- package/dist/commands/tee.js +5 -0
- package/dist/commands/touch.d.ts +5 -0
- package/dist/commands/touch.d.ts.map +1 -1
- package/dist/commands/touch.js +5 -0
- package/dist/commands/tr.d.ts.map +1 -1
- package/dist/commands/tr.js +45 -10
- package/dist/commands/tree.d.ts +5 -0
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +5 -0
- package/dist/commands/true.d.ts +10 -0
- package/dist/commands/true.d.ts.map +1 -1
- package/dist/commands/true.js +10 -0
- package/dist/commands/type.d.ts +5 -0
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +5 -0
- package/dist/commands/uname.d.ts +5 -0
- package/dist/commands/uname.d.ts.map +1 -1
- package/dist/commands/uname.js +5 -0
- package/dist/commands/uniq.d.ts +5 -0
- package/dist/commands/uniq.d.ts.map +1 -1
- package/dist/commands/uniq.js +5 -0
- package/dist/commands/unset.d.ts +5 -0
- package/dist/commands/unset.d.ts.map +1 -1
- package/dist/commands/unset.js +5 -0
- package/dist/commands/uptime.d.ts +5 -0
- package/dist/commands/uptime.d.ts.map +1 -1
- package/dist/commands/uptime.js +5 -0
- package/dist/commands/wc.d.ts +5 -0
- package/dist/commands/wc.d.ts.map +1 -1
- package/dist/commands/wc.js +5 -0
- package/dist/commands/wget.d.ts +5 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +5 -0
- package/dist/commands/who.d.ts +5 -0
- package/dist/commands/who.d.ts.map +1 -1
- package/dist/commands/who.js +5 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +5 -0
- package/dist/commands/xargs.d.ts +5 -0
- package/dist/commands/xargs.d.ts.map +1 -1
- package/dist/commands/xargs.js +5 -0
- package/dist/self-standalone.js +254 -30
- package/dist/types/commands.d.ts +36 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/tokenize.d.ts +20 -0
- package/dist/utils/tokenize.d.ts.map +1 -0
- package/dist/utils/tokenize.js +74 -0
- package/examples/web.min.js +2 -2
- package/package.json +1 -1
- package/src/SSHClient/index.ts +6 -3
- package/src/SSHMimic/executor.ts +21 -44
- package/src/SSHMimic/index.ts +7 -5
- package/src/SSHMimic/sftp.ts +28 -21
- package/src/VirtualShell/shell.ts +34 -4
- package/src/VirtualShell/shellParser.ts +2 -103
- package/src/VirtualUserManager/index.ts +43 -19
- package/src/commands/adduser.ts +86 -13
- package/src/commands/alias.ts +5 -0
- package/src/commands/apt.ts +5 -0
- package/src/commands/awk.ts +154 -29
- package/src/commands/cd.ts +0 -4
- package/src/commands/clear.ts +5 -0
- package/src/commands/command-helpers.ts +9 -0
- package/src/commands/declare.ts +5 -0
- package/src/commands/deluser.ts +84 -7
- package/src/commands/df.ts +5 -0
- package/src/commands/du.ts +5 -0
- package/src/commands/export.ts +5 -0
- package/src/commands/grep.ts +21 -8
- package/src/commands/groups.ts +5 -0
- package/src/commands/gzip.ts +54 -28
- package/src/commands/head.ts +14 -4
- package/src/commands/htop.ts +5 -0
- package/src/commands/kill.ts +5 -0
- package/src/commands/ln.ts +22 -0
- package/src/commands/ls.ts +17 -0
- package/src/commands/lsb-release.ts +5 -0
- package/src/commands/mkdir.ts +5 -0
- package/src/commands/mv.ts +5 -0
- package/src/commands/nano.ts +5 -0
- package/src/commands/neofetch.ts +8 -6
- package/src/commands/passwd.ts +35 -12
- package/src/commands/ping.ts +5 -0
- package/src/commands/printf.ts +30 -13
- package/src/commands/ps.ts +5 -0
- package/src/commands/read.ts +5 -0
- package/src/commands/registry.ts +4 -1
- package/src/commands/rm.ts +5 -0
- package/src/commands/runtime.ts +1 -61
- package/src/commands/sed.ts +5 -0
- package/src/commands/set.ts +5 -24
- package/src/commands/sh.ts +9 -3
- package/src/commands/shift.ts +10 -0
- package/src/commands/sleep.ts +5 -0
- package/src/commands/sort.ts +5 -0
- package/src/commands/source.ts +5 -0
- package/src/commands/stat.ts +61 -0
- package/src/commands/su.ts +54 -16
- package/src/commands/sudo.ts +5 -0
- package/src/commands/tail.ts +17 -3
- package/src/commands/tar.ts +38 -15
- package/src/commands/tee.ts +5 -0
- package/src/commands/touch.ts +5 -0
- package/src/commands/tr.ts +54 -10
- package/src/commands/tree.ts +5 -0
- package/src/commands/true.ts +10 -0
- package/src/commands/type.ts +5 -0
- package/src/commands/uname.ts +5 -0
- package/src/commands/uniq.ts +5 -0
- package/src/commands/unset.ts +5 -0
- package/src/commands/uptime.ts +5 -0
- package/src/commands/wc.ts +5 -0
- package/src/commands/wget.ts +5 -0
- package/src/commands/who.ts +5 -0
- package/src/commands/whoami.ts +5 -0
- package/src/commands/xargs.ts +5 -0
- package/src/self-standalone.ts +316 -33
- package/src/types/commands.ts +37 -0
- package/src/utils/tokenize.ts +78 -0
- package/builds/web-iife.min.js +0 -13
- package/builds/web-iife.min.js.map +0 -7
package/src/self-standalone.ts
CHANGED
|
@@ -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
|
|
53
|
+
function askHiddenQuestion(rl: Interface, promptText: string): Promise<string> {
|
|
51
54
|
return new Promise((resolve) => {
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
159
|
-
rl.close();
|
|
160
|
-
process.exit(result.exitCode ?? 0);
|
|
161
|
-
}
|
|
444
|
+
await flushVfs();
|
|
162
445
|
|
|
163
446
|
prompt();
|
|
164
447
|
rl.resume();
|
package/src/types/commands.ts
CHANGED
|
@@ -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
|
+
}
|
package/builds/web-iife.min.js
DELETED
|
@@ -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
|