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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,eAAO,MAAM,aAAa,EAAE,WAM3B,CAAC"}
|
|
1
|
+
{"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,WAM3B,CAAC"}
|
package/dist/commands/whoami.js
CHANGED
package/dist/commands/xargs.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xargs.d.ts","sourceRoot":"","sources":["../../src/commands/xargs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,eAAO,MAAM,YAAY,EAAE,WAsB1B,CAAC"}
|
|
1
|
+
{"version":3,"file":"xargs.d.ts","sourceRoot":"","sources":["../../src/commands/xargs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,WAsB1B,CAAC"}
|
package/dist/commands/xargs.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { runCommand } from "./runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Build and execute commands from stdin arguments.
|
|
4
|
+
* @category text
|
|
5
|
+
* @params ["[command] [args...]"]
|
|
6
|
+
*/
|
|
2
7
|
export const xargsCommand = {
|
|
3
8
|
name: "xargs",
|
|
4
9
|
description: "Build and execute command lines from stdin",
|
package/dist/self-standalone.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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 } from "node:readline";
|
|
4
5
|
import { makeDefaultEnv, runCommand } from "./commands/runtime";
|
|
6
|
+
import { spawnNanoEditorProcess } from "./modules/shellInteractive";
|
|
5
7
|
import { buildLoginBanner } from "./SSHMimic/loginBanner";
|
|
6
8
|
import { buildPrompt } from "./SSHMimic/prompt";
|
|
7
9
|
import { VirtualShell } from "./VirtualShell";
|
|
@@ -40,9 +42,50 @@ function readLastLogin(username) {
|
|
|
40
42
|
return null;
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
function
|
|
45
|
+
function askHiddenQuestion(rl, promptText) {
|
|
44
46
|
return new Promise((resolve) => {
|
|
45
|
-
|
|
47
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
48
|
+
rl.question(promptText, resolve);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const wasRawMode = Boolean(stdin.isRaw);
|
|
52
|
+
let buffer = "";
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
stdin.off("data", onData);
|
|
55
|
+
if (!wasRawMode) {
|
|
56
|
+
stdin.setRawMode(false);
|
|
57
|
+
}
|
|
58
|
+
rl.resume();
|
|
59
|
+
};
|
|
60
|
+
const finish = (value) => {
|
|
61
|
+
cleanup();
|
|
62
|
+
stdout.write("\n");
|
|
63
|
+
resolve(value);
|
|
64
|
+
};
|
|
65
|
+
const onData = (chunk) => {
|
|
66
|
+
const input = chunk.toString("utf8");
|
|
67
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
68
|
+
const ch = input[index];
|
|
69
|
+
if (ch === "\r" || ch === "\n") {
|
|
70
|
+
finish(buffer);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (ch === "\u007f" || ch === "\b") {
|
|
74
|
+
buffer = buffer.slice(0, -1);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch >= " ") {
|
|
78
|
+
buffer += ch;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
rl.pause();
|
|
83
|
+
stdout.write(promptText);
|
|
84
|
+
if (!wasRawMode) {
|
|
85
|
+
stdin.setRawMode(true);
|
|
86
|
+
}
|
|
87
|
+
stdin.resume();
|
|
88
|
+
stdin.on("data", onData);
|
|
46
89
|
});
|
|
47
90
|
}
|
|
48
91
|
function writeLastLogin(username, from) {
|
|
@@ -52,6 +95,42 @@ function writeLastLogin(username, from) {
|
|
|
52
95
|
}
|
|
53
96
|
virtualShell.vfs.writeFile(`/virtual-env-js/.lastlog/${username}.json`, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
54
97
|
}
|
|
98
|
+
async function flushVfs() {
|
|
99
|
+
await virtualShell.vfs.flushMirror();
|
|
100
|
+
}
|
|
101
|
+
function loadHistory() {
|
|
102
|
+
const historyPath = "/virtual-env-js/.bash_history";
|
|
103
|
+
if (!virtualShell.vfs.exists(historyPath)) {
|
|
104
|
+
virtualShell.vfs.writeFile(historyPath, "");
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return virtualShell.vfs
|
|
108
|
+
.readFile(historyPath)
|
|
109
|
+
.split("\n")
|
|
110
|
+
.map((line) => line.trim())
|
|
111
|
+
.filter((line) => line.length > 0);
|
|
112
|
+
}
|
|
113
|
+
function saveHistory(history) {
|
|
114
|
+
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
115
|
+
virtualShell.vfs.writeFile("/virtual-env-js/.bash_history", data);
|
|
116
|
+
}
|
|
117
|
+
function applySessionState(authUserState, cwdState, result, shellEnvState) {
|
|
118
|
+
let authUser = authUserState;
|
|
119
|
+
let cwd = cwdState;
|
|
120
|
+
if (result.switchUser) {
|
|
121
|
+
authUser = result.switchUser;
|
|
122
|
+
cwd = result.nextCwd ?? `/home/${authUser}`;
|
|
123
|
+
shellEnvState.vars.USER = authUser;
|
|
124
|
+
shellEnvState.vars.LOGNAME = authUser;
|
|
125
|
+
shellEnvState.vars.HOME = `/home/${authUser}`;
|
|
126
|
+
shellEnvState.vars.PWD = cwd;
|
|
127
|
+
}
|
|
128
|
+
else if (result.nextCwd) {
|
|
129
|
+
cwd = result.nextCwd;
|
|
130
|
+
shellEnvState.vars.PWD = cwd;
|
|
131
|
+
}
|
|
132
|
+
return { authUser, cwd };
|
|
133
|
+
}
|
|
55
134
|
virtualShell.addCommand("demo", [], () => {
|
|
56
135
|
return {
|
|
57
136
|
stdout: "This is a demo command. It does nothing useful.",
|
|
@@ -61,6 +140,9 @@ virtualShell.addCommand("demo", [], () => {
|
|
|
61
140
|
async function runReadlineShell() {
|
|
62
141
|
const rl = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
63
142
|
await virtualShell.ensureInitialized();
|
|
143
|
+
let history = loadHistory();
|
|
144
|
+
const rlWithHistory = rl;
|
|
145
|
+
rlWithHistory.history = [...history].reverse();
|
|
64
146
|
const selectedUser = initialUser.trim() || "root";
|
|
65
147
|
const userExists = virtualShell.users.getPasswordHash(selectedUser) !== null;
|
|
66
148
|
if (!userExists) {
|
|
@@ -72,8 +154,161 @@ async function runReadlineShell() {
|
|
|
72
154
|
let cwd = `/home/${authUser}`;
|
|
73
155
|
shellEnv.vars.PWD = cwd;
|
|
74
156
|
const remoteAddress = "localhost";
|
|
157
|
+
const terminalSize = {
|
|
158
|
+
cols: stdout.columns ?? 80,
|
|
159
|
+
rows: stdout.rows ?? 24,
|
|
160
|
+
};
|
|
161
|
+
async function startNanoEditor(targetPath, initialContent, tempPath) {
|
|
162
|
+
if (virtualShell.vfs.exists(targetPath)) {
|
|
163
|
+
await writeFile(tempPath, initialContent, "utf8");
|
|
164
|
+
}
|
|
165
|
+
rl.pause();
|
|
166
|
+
const editor = spawnNanoEditorProcess(tempPath, terminalSize, {
|
|
167
|
+
write: stdout.write.bind(stdout),
|
|
168
|
+
exit: () => undefined,
|
|
169
|
+
end: () => undefined,
|
|
170
|
+
});
|
|
171
|
+
const wasRawMode = Boolean(stdin.isRaw);
|
|
172
|
+
const forwardInput = (chunk) => {
|
|
173
|
+
editor.stdin.write(chunk);
|
|
174
|
+
};
|
|
175
|
+
stdin.resume();
|
|
176
|
+
if (!wasRawMode) {
|
|
177
|
+
stdin.setRawMode(true);
|
|
178
|
+
}
|
|
179
|
+
stdin.on("data", forwardInput);
|
|
180
|
+
await new Promise((resolve) => {
|
|
181
|
+
const cleanup = () => {
|
|
182
|
+
stdin.off("data", forwardInput);
|
|
183
|
+
if (!wasRawMode) {
|
|
184
|
+
stdin.setRawMode(false);
|
|
185
|
+
}
|
|
186
|
+
rl.resume();
|
|
187
|
+
};
|
|
188
|
+
editor.on("error", (error) => {
|
|
189
|
+
cleanup();
|
|
190
|
+
stdout.write(`nano: ${error.message}\r\n`);
|
|
191
|
+
resolve();
|
|
192
|
+
});
|
|
193
|
+
editor.on("close", async () => {
|
|
194
|
+
cleanup();
|
|
195
|
+
rl.write("", { ctrl: true, name: "u" });
|
|
196
|
+
try {
|
|
197
|
+
const updatedContent = await readFile(tempPath, "utf8");
|
|
198
|
+
virtualShell.writeFileAsUser(authUser, targetPath, updatedContent);
|
|
199
|
+
await flushVfs();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Save skipped or temp file missing.
|
|
203
|
+
}
|
|
204
|
+
await unlink(tempPath).catch(() => undefined);
|
|
205
|
+
stdout.write("\r\n");
|
|
206
|
+
resolve();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function handleSudoChallenge(challenge) {
|
|
211
|
+
if (challenge.onPassword) {
|
|
212
|
+
let promptText = challenge.prompt;
|
|
213
|
+
while (true) {
|
|
214
|
+
const typed = await askHiddenQuestion(rl, promptText);
|
|
215
|
+
const step = await challenge.onPassword(typed, virtualShell);
|
|
216
|
+
if (step.result === null) {
|
|
217
|
+
promptText = step.nextPrompt ?? promptText;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
await handleCommandResult(step.result);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const password = await askHiddenQuestion(rl, challenge.prompt);
|
|
225
|
+
if (!virtualShell.users.verifyPassword(challenge.username, password)) {
|
|
226
|
+
process.stderr.write("Sorry, try again.\n");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!challenge.commandLine) {
|
|
230
|
+
authUser = challenge.targetUser;
|
|
231
|
+
cwd = `/home/${authUser}`;
|
|
232
|
+
shellEnv.vars.USER = authUser;
|
|
233
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
234
|
+
shellEnv.vars.HOME = `/home/${authUser}`;
|
|
235
|
+
shellEnv.vars.PWD = cwd;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const runCwd = challenge.loginShell ? `/home/${challenge.targetUser}` : cwd;
|
|
239
|
+
const nestedResult = await runCommand(challenge.commandLine, challenge.targetUser, hostname, "shell", runCwd, virtualShell, undefined, shellEnv);
|
|
240
|
+
await handleCommandResult(nestedResult);
|
|
241
|
+
}
|
|
242
|
+
async function handlePasswordChallenge(challenge) {
|
|
243
|
+
const first = await askHiddenQuestion(rl, challenge.prompt);
|
|
244
|
+
if (challenge.confirmPrompt) {
|
|
245
|
+
const second = await askHiddenQuestion(rl, challenge.confirmPrompt);
|
|
246
|
+
if (second !== first) {
|
|
247
|
+
process.stderr.write("passwords do not match\n");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
switch (challenge.action) {
|
|
252
|
+
case "passwd":
|
|
253
|
+
await virtualShell.users.setPassword(challenge.targetUsername, first);
|
|
254
|
+
stdout.write("passwd: password updated successfully\n");
|
|
255
|
+
break;
|
|
256
|
+
case "adduser":
|
|
257
|
+
if (!challenge.newUsername) {
|
|
258
|
+
process.stderr.write("adduser: missing username\n");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await virtualShell.users.addUser(challenge.newUsername, first);
|
|
262
|
+
stdout.write(`adduser: user '${challenge.newUsername}' created\n`);
|
|
263
|
+
break;
|
|
264
|
+
case "deluser":
|
|
265
|
+
await virtualShell.users.deleteUser(challenge.targetUsername);
|
|
266
|
+
stdout.write(`Removing user '${challenge.targetUsername}' ...\ndeluser: done.\n`);
|
|
267
|
+
break;
|
|
268
|
+
case "su":
|
|
269
|
+
authUser = challenge.targetUsername;
|
|
270
|
+
cwd = `/home/${authUser}`;
|
|
271
|
+
shellEnv.vars.USER = authUser;
|
|
272
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
273
|
+
shellEnv.vars.HOME = `/home/${authUser}`;
|
|
274
|
+
shellEnv.vars.PWD = cwd;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function handleCommandResult(result) {
|
|
279
|
+
if (result.openEditor) {
|
|
280
|
+
await startNanoEditor(result.openEditor.targetPath, result.openEditor.initialContent, result.openEditor.tempPath);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (result.sudoChallenge) {
|
|
284
|
+
await handleSudoChallenge(result.sudoChallenge);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (result.passwordChallenge) {
|
|
288
|
+
await handlePasswordChallenge(result.passwordChallenge);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (result.stdout) {
|
|
292
|
+
stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
|
|
293
|
+
}
|
|
294
|
+
if (result.stderr) {
|
|
295
|
+
process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
|
|
296
|
+
}
|
|
297
|
+
if (result.clearScreen) {
|
|
298
|
+
stdout.write("\u001b[2J\u001b[H");
|
|
299
|
+
console.clear();
|
|
300
|
+
}
|
|
301
|
+
const updatedState = applySessionState(authUser, cwd, result, shellEnv);
|
|
302
|
+
authUser = updatedState.authUser;
|
|
303
|
+
cwd = updatedState.cwd;
|
|
304
|
+
if (result.closeSession) {
|
|
305
|
+
await flushVfs();
|
|
306
|
+
rl.close();
|
|
307
|
+
process.exit(result.exitCode ?? 0);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
75
310
|
if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
|
|
76
|
-
const password = await
|
|
311
|
+
const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
|
|
77
312
|
if (!virtualShell.users.verifyPassword(authUser, password)) {
|
|
78
313
|
process.stderr.write("self-standalone: authentication failed\n");
|
|
79
314
|
process.exit(1);
|
|
@@ -93,43 +328,32 @@ async function runReadlineShell() {
|
|
|
93
328
|
prompt();
|
|
94
329
|
});
|
|
95
330
|
rl.on("close", () => {
|
|
96
|
-
|
|
97
|
-
|
|
331
|
+
void (async () => {
|
|
332
|
+
await flushVfs();
|
|
333
|
+
console.log("");
|
|
334
|
+
process.exit(0);
|
|
335
|
+
})();
|
|
98
336
|
});
|
|
99
337
|
stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
|
|
100
338
|
writeLastLogin(authUser, remoteAddress);
|
|
339
|
+
await flushVfs();
|
|
101
340
|
prompt();
|
|
102
341
|
while (true) {
|
|
103
342
|
const inputLine = await new Promise((resolve) => {
|
|
104
343
|
rl.once("line", (line) => resolve(line));
|
|
105
344
|
});
|
|
106
345
|
rl.pause();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (result.clearScreen) {
|
|
115
|
-
stdout.write("\u001b[2J\u001b[H");
|
|
116
|
-
}
|
|
117
|
-
if (result.switchUser) {
|
|
118
|
-
authUser = result.switchUser;
|
|
119
|
-
cwd = result.nextCwd ?? `/home/${authUser}`;
|
|
120
|
-
shellEnv.vars.USER = authUser;
|
|
121
|
-
shellEnv.vars.LOGNAME = authUser;
|
|
122
|
-
shellEnv.vars.HOME = `/home/${authUser}`;
|
|
123
|
-
shellEnv.vars.PWD = cwd;
|
|
124
|
-
}
|
|
125
|
-
else if (result.nextCwd) {
|
|
126
|
-
cwd = result.nextCwd;
|
|
127
|
-
shellEnv.vars.PWD = cwd;
|
|
128
|
-
}
|
|
129
|
-
if (result.closeSession) {
|
|
130
|
-
rl.close();
|
|
131
|
-
process.exit(result.exitCode ?? 0);
|
|
346
|
+
if (inputLine.trim().length > 0) {
|
|
347
|
+
history.push(inputLine);
|
|
348
|
+
if (history.length > 500) {
|
|
349
|
+
history = history.slice(history.length - 500);
|
|
350
|
+
}
|
|
351
|
+
saveHistory(history);
|
|
352
|
+
rlWithHistory.history = [...history].reverse();
|
|
132
353
|
}
|
|
354
|
+
const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
|
|
355
|
+
await handleCommandResult(result);
|
|
356
|
+
await flushVfs();
|
|
133
357
|
prompt();
|
|
134
358
|
rl.resume();
|
|
135
359
|
}
|
package/dist/types/commands.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface CommandResult {
|
|
|
29
29
|
openHtop?: boolean;
|
|
30
30
|
/** Request sudo password challenge flow. */
|
|
31
31
|
sudoChallenge?: SudoChallenge;
|
|
32
|
+
/** Request a generic password challenge (adduser, passwd). */
|
|
33
|
+
passwordChallenge?: PasswordChallenge;
|
|
32
34
|
}
|
|
33
35
|
/** Deferred sudo challenge metadata returned by sudo command. */
|
|
34
36
|
export interface SudoChallenge {
|
|
@@ -42,6 +44,40 @@ export interface SudoChallenge {
|
|
|
42
44
|
loginShell: boolean;
|
|
43
45
|
/** Prompt text shown before password input. */
|
|
44
46
|
prompt: string;
|
|
47
|
+
/**
|
|
48
|
+
* Challenge mode.
|
|
49
|
+
* - `"sudo"` (default): verify `username`'s password, then run `commandLine`.
|
|
50
|
+
* - `"passwd"`: multi-step new-password flow; `onPassword` handles each step.
|
|
51
|
+
* - `"confirm"`: text confirmation flow (e.g. deluser); `onPassword` receives typed text.
|
|
52
|
+
*/
|
|
53
|
+
mode?: "sudo" | "passwd" | "confirm";
|
|
54
|
+
/**
|
|
55
|
+
* Optional async handler called when the user submits input.
|
|
56
|
+
* Receives the typed text and the shell instance.
|
|
57
|
+
* Returns a `CommandResult` written to the terminal, or `null` to show
|
|
58
|
+
* another prompt (pass `nextPrompt` to change the prompt text).
|
|
59
|
+
*/
|
|
60
|
+
onPassword?: (input: string, shell: import("../VirtualShell").VirtualShell) => Promise<{
|
|
61
|
+
result: CommandResult | null;
|
|
62
|
+
nextPrompt?: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/** Generic password challenge — used by adduser, passwd, deluser. */
|
|
66
|
+
export interface PasswordChallenge {
|
|
67
|
+
/** Lines to print before the first prompt. */
|
|
68
|
+
preamble?: string;
|
|
69
|
+
/** Primary prompt text (e.g. "New password: "). */
|
|
70
|
+
prompt: string;
|
|
71
|
+
/** If set, a second prompt is shown for confirmation. */
|
|
72
|
+
confirmPrompt?: string;
|
|
73
|
+
/** Prompt shown for a destructive confirmation (y/N). */
|
|
74
|
+
confirmText?: string;
|
|
75
|
+
/** Tag identifying what to do with the entered value. */
|
|
76
|
+
action: "adduser" | "passwd" | "deluser" | "su";
|
|
77
|
+
/** Username targeted by the action. */
|
|
78
|
+
targetUsername: string;
|
|
79
|
+
/** For adduser: the new user's username (already validated). */
|
|
80
|
+
newUsername?: string;
|
|
45
81
|
}
|
|
46
82
|
/** State payload used by nano command interactive editor flow. */
|
|
47
83
|
export interface NanoEditorSession {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/types/commands.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC7B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/types/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
* Tokenize a shell command line respecting quoted strings and redirect
|
|
10
|
+
* operators.
|
|
11
|
+
*
|
|
12
|
+
* - Single-quoted content is preserved verbatim.
|
|
13
|
+
* - Double-quoted content is preserved (expansion happens later).
|
|
14
|
+
* - `>`, `>>`, and `<` are emitted as standalone tokens.
|
|
15
|
+
*
|
|
16
|
+
* @param input Raw shell command string.
|
|
17
|
+
* @returns Array of string tokens.
|
|
18
|
+
*/
|
|
19
|
+
export declare function tokenizeCommand(input: string): string[];
|
|
20
|
+
//# sourceMappingURL=tokenize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/utils/tokenize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA0DvD"}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
* Tokenize a shell command line respecting quoted strings and redirect
|
|
10
|
+
* operators.
|
|
11
|
+
*
|
|
12
|
+
* - Single-quoted content is preserved verbatim.
|
|
13
|
+
* - Double-quoted content is preserved (expansion happens later).
|
|
14
|
+
* - `>`, `>>`, and `<` are emitted as standalone tokens.
|
|
15
|
+
*
|
|
16
|
+
* @param input Raw shell command string.
|
|
17
|
+
* @returns Array of string tokens.
|
|
18
|
+
*/
|
|
19
|
+
export function tokenizeCommand(input) {
|
|
20
|
+
const tokens = [];
|
|
21
|
+
let current = "";
|
|
22
|
+
let inQ = false;
|
|
23
|
+
let qChar = "";
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < input.length) {
|
|
26
|
+
const ch = input[i];
|
|
27
|
+
const next = input[i + 1];
|
|
28
|
+
if ((ch === '"' || ch === "'") && !inQ) {
|
|
29
|
+
inQ = true;
|
|
30
|
+
qChar = ch;
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (inQ && ch === qChar) {
|
|
35
|
+
inQ = false;
|
|
36
|
+
qChar = "";
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (inQ) {
|
|
41
|
+
current += ch;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === " ") {
|
|
46
|
+
if (current) {
|
|
47
|
+
tokens.push(current);
|
|
48
|
+
current = "";
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if ((ch === ">" || ch === "<") && !inQ) {
|
|
54
|
+
if (current) {
|
|
55
|
+
tokens.push(current);
|
|
56
|
+
current = "";
|
|
57
|
+
}
|
|
58
|
+
if (ch === ">" && next === ">") {
|
|
59
|
+
tokens.push(">>");
|
|
60
|
+
i += 2;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
tokens.push(ch);
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
current += ch;
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
if (current)
|
|
72
|
+
tokens.push(current);
|
|
73
|
+
return tokens;
|
|
74
|
+
}
|
package/examples/web.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var k=Object.defineProperty;var O=(o,e,t)=>e in o?k(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var u=(o,e,t)=>O(o,typeof e!="symbol"?e+"":e,t);function x(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 w(o){let e=o.trim();if(!e)return{statements:[],isValid:!0};try{return{statements:z(e),isValid:!0}}catch(t){return{statements:[],isValid:!1,error:t.message}}}function z(o){let e=A(o),t=[];for(let r of e){let s={pipeline:{commands:R(r.text.trim()),isValid:!0}};r.op&&(s.op=r.op),t.push(s)}return t}function A(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 R(o){return F(o).map(L)}function F(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 L(o){let e=x(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 I(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 M(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 p(o,e,t=0,r){let n=r??e.HOME??"/home/user";return M(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=I(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 g=o.slice(i+2,d).trim(),P=(await r(g)).replace(/\n$/,"");n+=P,i=d+1;continue}n+=a,i++}o=n}return p(o,e,t)}var _=new TextEncoder,B=new TextDecoder;function j(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 T(o,e,t){let r=new Date().toISOString();return{type:"file",name:o,mode:t,createdAt:r,updatedAt:r,contentBase64:j(e)}}function b(o,e){return o.children.find(t=>t.name===e)}function m(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 V=globalThis;function E(o){return new Promise((e,t)=>{o.addEventListener("success",()=>e(o.result)),o.addEventListener("error",()=>t(o.error))})}var y=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=V.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=b(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=b(s,i);if(!a){let c=W(i,t);m(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=b(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 B.decode(N(t.contentBase64))}writeFile(e,t,r=420){let n=l(e),s=this.ensureDirectory(h(n),493),i=typeof t=="string"?_.encode(t):t,a=T(f(n),i,r);m(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),m(s,i)}copy(e,t){let r=this.getNode(e),n=this.ensureDirectory(h(t),493),s=this.copyNode(r);s.name=f(t),m(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)}},v=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 y(t.vfs),this.registerBuiltins()}register(e){this.commands.set(e.name.toLowerCase(),e);for(let t of e.aliases??[])this.commands.set(t.toLowerCase(),e)}registerBuiltins(){this.register({name:"help",description:"List available web commands",params:[],run:()=>({stdout:`${this.listCommands().join(`
|
|
2
2
|
`)}
|
|
3
3
|
`,exitCode:0})}),this.register({name:"pwd",description:"Print current directory",params:[],run:()=>({stdout:`${this.cwd}
|
|
4
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=p(s,this.env.vars,this.env.lastExitCode,this.env.vars.HOME);return{stdout:r?i:`${i}
|
|
@@ -9,5 +9,5 @@ var P=Object.defineProperty;var A=(o,e,t)=>e in o?P(o,e,{enumerable:!0,configura
|
|
|
9
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
10
|
`),this.vfs.exists("/etc/hosts")||this.vfs.writeFile("/etc/hosts",`127.0.0.1 localhost
|
|
11
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
|
|
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(a=>p(a,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};try{let a=await n.run(i);return a.nextCwd&&(this.cwd=a.nextCwd,this.env.vars.PWD=a.nextCwd),a}catch(a){return{stderr:a instanceof Error?a.message:String(a),exitCode:1}}}};function K(o="typescript-vm",e={}){return new v(o,e)}export{y as IndexedDbMirrorVfs,v as WebShell,K as createWebShell};
|
|
13
13
|
//# sourceMappingURL=web.min.js.map
|
package/package.json
CHANGED
package/src/SSHClient/index.ts
CHANGED
|
@@ -62,11 +62,14 @@ export class SshClient {
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
// Handle async results
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const resolved = result instanceof Promise ? await result : result;
|
|
66
|
+
|
|
67
|
+
// Propagate cwd changes (cd, su, etc.)
|
|
68
|
+
if (resolved.nextCwd && (resolved.exitCode ?? 0) === 0) {
|
|
69
|
+
this.currentCwd = resolved.nextCwd;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
return
|
|
72
|
+
return resolved;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/**
|