typescript-virtual-container 1.5.5 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -35
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/index.d.ts +5 -1
- package/dist/SSHMimic/index.js +27 -3
- package/dist/SSHMimic/scp.d.ts +34 -0
- package/dist/SSHMimic/scp.js +285 -0
- package/dist/SSHMimic/sftp.d.ts +53 -3
- package/dist/SSHMimic/sftp.js +9 -3
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
- package/dist/VirtualFileSystem/binaryPack.js +37 -1
- package/dist/VirtualFileSystem/index.d.ts +7 -0
- package/dist/VirtualFileSystem/index.js +67 -27
- package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
- package/dist/VirtualFileSystem/path.d.ts +5 -0
- package/dist/VirtualFileSystem/path.js +24 -11
- package/dist/VirtualPackageManager/index.d.ts +4 -2
- package/dist/VirtualPackageManager/index.js +24 -4
- package/dist/VirtualShell/index.d.ts +4 -0
- package/dist/VirtualShell/index.js +1 -7
- package/dist/VirtualShell/shell.js +40 -10
- package/dist/VirtualShell/shellParser.js +1 -22
- package/dist/commands/awk.d.ts +6 -11
- package/dist/commands/awk.js +462 -109
- package/dist/commands/bzip2.d.ts +11 -0
- package/dist/commands/bzip2.js +91 -0
- package/dist/commands/exit.js +1 -1
- package/dist/commands/find.d.ts +2 -2
- package/dist/commands/find.js +209 -37
- package/dist/commands/helpers.d.ts +0 -20
- package/dist/commands/helpers.js +0 -97
- package/dist/commands/lsof.d.ts +6 -0
- package/dist/commands/lsof.js +30 -0
- package/dist/commands/perl.d.ts +6 -0
- package/dist/commands/perl.js +76 -0
- package/dist/commands/python.js +5 -2
- package/dist/commands/registry.js +19 -1
- package/dist/commands/runtime.js +65 -87
- package/dist/commands/sed.d.ts +2 -2
- package/dist/commands/sed.js +216 -34
- package/dist/commands/sh.js +42 -0
- package/dist/commands/strace.d.ts +6 -0
- package/dist/commands/strace.js +26 -0
- package/dist/commands/tar.d.ts +2 -1
- package/dist/commands/tar.js +138 -52
- package/dist/commands/test.js +2 -2
- package/dist/commands/zip.d.ts +11 -0
- package/dist/commands/zip.js +232 -0
- package/dist/modules/linuxRootfs.js +1 -4
- package/dist/modules/neofetch.js +2 -2
- package/dist/types/commands.d.ts +4 -0
- package/dist/utils/argv.d.ts +6 -0
- package/dist/utils/argv.js +32 -0
- package/dist/utils/expand.d.ts +5 -2
- package/dist/utils/expand.js +112 -45
- package/dist/utils/glob.d.ts +6 -0
- package/dist/utils/glob.js +34 -0
- package/dist/utils/tokenize.js +13 -13
- package/package.json +9 -7
- package/dist/self-standalone.d.ts +0 -1
- package/dist/self-standalone.js +0 -444
- package/dist/standalone-wo-sftp.d.ts +0 -1
- package/dist/standalone-wo-sftp.js +0 -30
- package/dist/standalone.d.ts +0 -1
- package/dist/standalone.js +0 -61
package/dist/self-standalone.js
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { basename } from "node:path";
|
|
4
|
-
import { stdin, stdout } from "node:process";
|
|
5
|
-
import { createInterface } from "node:readline";
|
|
6
|
-
import { getCommandNames } from "./commands/registry";
|
|
7
|
-
import { makeDefaultEnv, runCommand, userHome } from "./commands/runtime";
|
|
8
|
-
import { spawnNanoEditorProcess } from "./modules/shellInteractive";
|
|
9
|
-
import { resolvePath } from "./modules/shellRuntime";
|
|
10
|
-
import { buildLoginBanner } from "./SSHMimic/loginBanner";
|
|
11
|
-
import { buildPrompt } from "./SSHMimic/prompt";
|
|
12
|
-
import { VirtualShell } from "./VirtualShell";
|
|
13
|
-
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
14
|
-
const argv = process.argv.slice(2);
|
|
15
|
-
// ── CLI args ──────────────────────────────────────────────────────────────────
|
|
16
|
-
console.clear();
|
|
17
|
-
function readUserArg() {
|
|
18
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
-
const current = argv[index];
|
|
20
|
-
if (current === "--user") {
|
|
21
|
-
const next = argv[index + 1];
|
|
22
|
-
if (!next || next.startsWith("--")) {
|
|
23
|
-
throw new Error("self-standalone: --user requires a value");
|
|
24
|
-
}
|
|
25
|
-
return next;
|
|
26
|
-
}
|
|
27
|
-
if (current?.startsWith("--user=")) {
|
|
28
|
-
return current.slice("--user=".length) || "root";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return "root";
|
|
32
|
-
}
|
|
33
|
-
const initialUser = readUserArg();
|
|
34
|
-
const virtualShell = new VirtualShell(hostname, undefined, {
|
|
35
|
-
mode: "fs",
|
|
36
|
-
snapshotPath: ".vfs",
|
|
37
|
-
});
|
|
38
|
-
// ── VFS helpers ───────────────────────────────────────────────────────────────
|
|
39
|
-
function readLastLogin(username) {
|
|
40
|
-
const lastlogPath = `/home/${username}/.lastlog`;
|
|
41
|
-
if (!virtualShell.vfs.exists(lastlogPath))
|
|
42
|
-
return null;
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(virtualShell.vfs.readFile(lastlogPath));
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function writeLastLogin(username, from) {
|
|
51
|
-
virtualShell.vfs.writeFile(`/home/${username}/.lastlog`, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
52
|
-
}
|
|
53
|
-
async function flushVfs() {
|
|
54
|
-
await virtualShell.vfs.stopAutoFlush();
|
|
55
|
-
}
|
|
56
|
-
function loadHistory(authUser) {
|
|
57
|
-
const historyPath = `${userHome(authUser)}/.bash_history`;
|
|
58
|
-
if (!virtualShell.vfs.exists(historyPath)) {
|
|
59
|
-
virtualShell.vfs.writeFile(historyPath, "");
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
return virtualShell.vfs
|
|
63
|
-
.readFile(historyPath)
|
|
64
|
-
.split("\n")
|
|
65
|
-
.map((line) => line.trim())
|
|
66
|
-
.filter((line) => line.length > 0);
|
|
67
|
-
}
|
|
68
|
-
function saveHistory(history, authUser) {
|
|
69
|
-
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
70
|
-
virtualShell.vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
|
|
71
|
-
}
|
|
72
|
-
// ── Tab completion ────────────────────────────────────────────────────────────
|
|
73
|
-
function listPathCompletions(vfs, cwd, prefix) {
|
|
74
|
-
const slashIndex = prefix.lastIndexOf("/");
|
|
75
|
-
const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
|
|
76
|
-
const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
|
|
77
|
-
const basePath = resolvePath(cwd, dirPart || ".");
|
|
78
|
-
try {
|
|
79
|
-
return vfs
|
|
80
|
-
.list(basePath)
|
|
81
|
-
.filter((e) => !e.startsWith(".") && e.startsWith(namePart))
|
|
82
|
-
.map((e) => {
|
|
83
|
-
const fullPath = path.posix.join(basePath, e);
|
|
84
|
-
const st = vfs.stat(fullPath);
|
|
85
|
-
return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
|
|
86
|
-
})
|
|
87
|
-
.sort();
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function makeCompleter(getState) {
|
|
94
|
-
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
95
|
-
return (line, cb) => {
|
|
96
|
-
const { cwd } = getState();
|
|
97
|
-
// Extract the token under/before cursor (last whitespace-separated word)
|
|
98
|
-
const token = line.split(/\s+/).at(-1) ?? "";
|
|
99
|
-
const isFirstToken = line.trimStart() === token;
|
|
100
|
-
const cmdHits = isFirstToken ? commandNames.filter((n) => n.startsWith(token)) : [];
|
|
101
|
-
const pathHits = listPathCompletions(virtualShell.vfs, cwd, token);
|
|
102
|
-
const hits = Array.from(new Set([...cmdHits, ...pathHits])).sort();
|
|
103
|
-
cb(null, [hits, token]);
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
// ── Hidden password input ─────────────────────────────────────────────────────
|
|
107
|
-
function askHiddenQuestion(rl, promptText) {
|
|
108
|
-
return new Promise((resolve) => {
|
|
109
|
-
if (!stdin.isTTY || !stdout.isTTY) {
|
|
110
|
-
rl.question(promptText, resolve);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const wasRawMode = Boolean(stdin.isRaw);
|
|
114
|
-
let buffer = "";
|
|
115
|
-
const cleanup = () => {
|
|
116
|
-
stdin.off("data", onData);
|
|
117
|
-
if (!wasRawMode)
|
|
118
|
-
stdin.setRawMode(false);
|
|
119
|
-
};
|
|
120
|
-
const finish = (value) => {
|
|
121
|
-
cleanup();
|
|
122
|
-
stdout.write("\n");
|
|
123
|
-
resolve(value);
|
|
124
|
-
};
|
|
125
|
-
const onData = (chunk) => {
|
|
126
|
-
const input = chunk.toString("utf8");
|
|
127
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
128
|
-
const ch = input[i];
|
|
129
|
-
if (ch === "\r" || ch === "\n") {
|
|
130
|
-
finish(buffer);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (ch === "\u007f" || ch === "\b") {
|
|
134
|
-
buffer = buffer.slice(0, -1);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (ch >= " ")
|
|
138
|
-
buffer += ch;
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
// Pause readline so it doesn't eat our raw keystrokes
|
|
142
|
-
rl.pause();
|
|
143
|
-
stdout.write(promptText);
|
|
144
|
-
if (!wasRawMode)
|
|
145
|
-
stdin.setRawMode(true);
|
|
146
|
-
stdin.resume();
|
|
147
|
-
stdin.on("data", onData);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
// ── Session state helper ──────────────────────────────────────────────────────
|
|
151
|
-
function applySessionState(authUserState, cwdState, result, shellEnvState) {
|
|
152
|
-
let authUser = authUserState;
|
|
153
|
-
let cwd = cwdState;
|
|
154
|
-
if (result.switchUser) {
|
|
155
|
-
authUser = result.switchUser;
|
|
156
|
-
cwd = result.nextCwd ?? userHome(authUser);
|
|
157
|
-
shellEnvState.vars.USER = authUser;
|
|
158
|
-
shellEnvState.vars.LOGNAME = authUser;
|
|
159
|
-
shellEnvState.vars.HOME = userHome(authUser);
|
|
160
|
-
shellEnvState.vars.PWD = cwd;
|
|
161
|
-
}
|
|
162
|
-
else if (result.nextCwd) {
|
|
163
|
-
cwd = result.nextCwd;
|
|
164
|
-
shellEnvState.vars.PWD = cwd;
|
|
165
|
-
}
|
|
166
|
-
return { authUser, cwd };
|
|
167
|
-
}
|
|
168
|
-
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
169
|
-
virtualShell.addCommand("demo", [], () => ({
|
|
170
|
-
stdout: "This is a demo command. It does nothing useful.",
|
|
171
|
-
exitCode: 0,
|
|
172
|
-
}));
|
|
173
|
-
// ── Main shell ────────────────────────────────────────────────────────────────
|
|
174
|
-
async function runReadlineShell() {
|
|
175
|
-
await virtualShell.ensureInitialized();
|
|
176
|
-
const selectedUser = initialUser.trim() || "root";
|
|
177
|
-
if (virtualShell.users.getPasswordHash(selectedUser) === null) {
|
|
178
|
-
process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
// Ensure home dir and README.txt exist (mirrors SSHMimic/VirtualUserManager behaviour)
|
|
182
|
-
const homePath = selectedUser === "root" ? "/root" : userHome(selectedUser);
|
|
183
|
-
if (!virtualShell.vfs.exists(homePath)) {
|
|
184
|
-
virtualShell.vfs.mkdir(homePath, selectedUser === "root" ? 0o700 : 0o755);
|
|
185
|
-
}
|
|
186
|
-
const readmePath = `${homePath}/README.txt`;
|
|
187
|
-
if (!virtualShell.vfs.exists(readmePath)) {
|
|
188
|
-
virtualShell.vfs.writeFile(readmePath, `Welcome to ${hostname}\n`);
|
|
189
|
-
await virtualShell.vfs.stopAutoFlush();
|
|
190
|
-
}
|
|
191
|
-
const shellEnv = makeDefaultEnv(selectedUser, hostname);
|
|
192
|
-
let authUser = selectedUser;
|
|
193
|
-
let cwd = userHome(authUser);
|
|
194
|
-
shellEnv.vars.PWD = cwd;
|
|
195
|
-
const remoteAddress = "localhost";
|
|
196
|
-
const terminalSize = { cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 };
|
|
197
|
-
let history = loadHistory(authUser);
|
|
198
|
-
// completer reads cwd via closure — always current
|
|
199
|
-
const rl = createInterface({
|
|
200
|
-
input: stdin,
|
|
201
|
-
output: stdout,
|
|
202
|
-
terminal: true,
|
|
203
|
-
completer: makeCompleter(() => ({ cwd })),
|
|
204
|
-
});
|
|
205
|
-
// Sync readline's internal history with our VFS history
|
|
206
|
-
const rlWithHistory = rl;
|
|
207
|
-
rlWithHistory.history = [...history].reverse();
|
|
208
|
-
// ── nano editor ────────────────────────────────────────────────────────────
|
|
209
|
-
async function startNanoEditor(targetPath, initialContent, tempPath) {
|
|
210
|
-
if (virtualShell.vfs.exists(targetPath)) {
|
|
211
|
-
await writeFile(tempPath, initialContent, "utf8");
|
|
212
|
-
}
|
|
213
|
-
rl.pause();
|
|
214
|
-
const editor = spawnNanoEditorProcess(tempPath, terminalSize, {
|
|
215
|
-
write: stdout.write.bind(stdout),
|
|
216
|
-
exit: () => undefined,
|
|
217
|
-
end: () => undefined,
|
|
218
|
-
});
|
|
219
|
-
const wasRawMode = Boolean(stdin.isRaw);
|
|
220
|
-
const forwardInput = (chunk) => { editor.stdin.write(chunk); };
|
|
221
|
-
stdin.resume();
|
|
222
|
-
if (!wasRawMode)
|
|
223
|
-
stdin.setRawMode(true);
|
|
224
|
-
stdin.on("data", forwardInput);
|
|
225
|
-
await new Promise((resolve) => {
|
|
226
|
-
const cleanup = () => {
|
|
227
|
-
stdin.off("data", forwardInput);
|
|
228
|
-
if (!wasRawMode)
|
|
229
|
-
stdin.setRawMode(false);
|
|
230
|
-
rl.resume();
|
|
231
|
-
};
|
|
232
|
-
editor.on("error", (error) => {
|
|
233
|
-
cleanup();
|
|
234
|
-
stdout.write(`nano: ${error.message}\r\n`);
|
|
235
|
-
resolve();
|
|
236
|
-
});
|
|
237
|
-
editor.on("close", async () => {
|
|
238
|
-
cleanup();
|
|
239
|
-
rl.write("", { ctrl: true, name: "u" });
|
|
240
|
-
try {
|
|
241
|
-
const updatedContent = await readFile(tempPath, "utf8");
|
|
242
|
-
virtualShell.writeFileAsUser(authUser, targetPath, updatedContent);
|
|
243
|
-
await flushVfs();
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
// save skipped or temp file missing
|
|
247
|
-
}
|
|
248
|
-
await unlink(tempPath).catch(() => undefined);
|
|
249
|
-
stdout.write("\r\n");
|
|
250
|
-
resolve();
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
// ── challenge handlers ─────────────────────────────────────────────────────
|
|
255
|
-
async function handleSudoChallenge(challenge) {
|
|
256
|
-
if (challenge.onPassword) {
|
|
257
|
-
let promptText = challenge.prompt;
|
|
258
|
-
while (true) {
|
|
259
|
-
const typed = await askHiddenQuestion(rl, promptText);
|
|
260
|
-
const step = await challenge.onPassword(typed, virtualShell);
|
|
261
|
-
if (step.result === null) {
|
|
262
|
-
promptText = step.nextPrompt ?? promptText;
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
await handleCommandResult(step.result);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const password = await askHiddenQuestion(rl, challenge.prompt);
|
|
270
|
-
if (!virtualShell.users.verifyPassword(challenge.username, password)) {
|
|
271
|
-
process.stderr.write("Sorry, try again.\n");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (!challenge.commandLine) {
|
|
275
|
-
authUser = challenge.targetUser;
|
|
276
|
-
cwd = userHome(authUser);
|
|
277
|
-
shellEnv.vars.USER = authUser;
|
|
278
|
-
shellEnv.vars.LOGNAME = authUser;
|
|
279
|
-
shellEnv.vars.HOME = userHome(authUser);
|
|
280
|
-
shellEnv.vars.PWD = cwd;
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const runCwd = challenge.loginShell ? userHome(challenge.targetUser) : cwd;
|
|
284
|
-
const nestedResult = await runCommand(challenge.commandLine, challenge.targetUser, hostname, "shell", runCwd, virtualShell, undefined, shellEnv);
|
|
285
|
-
await handleCommandResult(nestedResult);
|
|
286
|
-
}
|
|
287
|
-
async function handlePasswordChallenge(challenge) {
|
|
288
|
-
const first = await askHiddenQuestion(rl, challenge.prompt);
|
|
289
|
-
if (challenge.confirmPrompt) {
|
|
290
|
-
const second = await askHiddenQuestion(rl, challenge.confirmPrompt);
|
|
291
|
-
if (second !== first) {
|
|
292
|
-
process.stderr.write("passwords do not match\n");
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
switch (challenge.action) {
|
|
297
|
-
case "passwd":
|
|
298
|
-
await virtualShell.users.setPassword(challenge.targetUsername, first);
|
|
299
|
-
stdout.write("passwd: password updated successfully\n");
|
|
300
|
-
break;
|
|
301
|
-
case "adduser":
|
|
302
|
-
if (!challenge.newUsername) {
|
|
303
|
-
process.stderr.write("adduser: missing username\n");
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
await virtualShell.users.addUser(challenge.newUsername, first);
|
|
307
|
-
stdout.write(`adduser: user '${challenge.newUsername}' created\n`);
|
|
308
|
-
break;
|
|
309
|
-
case "deluser":
|
|
310
|
-
await virtualShell.users.deleteUser(challenge.targetUsername);
|
|
311
|
-
stdout.write(`Removing user '${challenge.targetUsername}' ...\ndeluser: done.\n`);
|
|
312
|
-
break;
|
|
313
|
-
case "su":
|
|
314
|
-
authUser = challenge.targetUsername;
|
|
315
|
-
cwd = userHome(authUser);
|
|
316
|
-
shellEnv.vars.USER = authUser;
|
|
317
|
-
shellEnv.vars.LOGNAME = authUser;
|
|
318
|
-
shellEnv.vars.HOME = userHome(authUser);
|
|
319
|
-
shellEnv.vars.PWD = cwd;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
// handleCommandResult must be declared before the "line" handler
|
|
324
|
-
async function handleCommandResult(result) {
|
|
325
|
-
if (result.openEditor) {
|
|
326
|
-
await startNanoEditor(result.openEditor.targetPath, result.openEditor.initialContent, result.openEditor.tempPath);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
if (result.sudoChallenge) {
|
|
330
|
-
await handleSudoChallenge(result.sudoChallenge);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (result.passwordChallenge) {
|
|
334
|
-
await handlePasswordChallenge(result.passwordChallenge);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (result.clearScreen) {
|
|
338
|
-
stdout.write("\u001b[2J\u001b[H");
|
|
339
|
-
console.clear();
|
|
340
|
-
}
|
|
341
|
-
if (result.stdout) {
|
|
342
|
-
stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
|
|
343
|
-
}
|
|
344
|
-
if (result.stderr) {
|
|
345
|
-
process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
|
|
346
|
-
}
|
|
347
|
-
const updated = applySessionState(authUser, cwd, result, shellEnv);
|
|
348
|
-
authUser = updated.authUser;
|
|
349
|
-
cwd = updated.cwd;
|
|
350
|
-
if (result.closeSession) {
|
|
351
|
-
await flushVfs();
|
|
352
|
-
rl.close();
|
|
353
|
-
process.exit(result.exitCode ?? 0);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// ── Prompt helper ──────────────────────────────────────────────────────────
|
|
357
|
-
const renderPrompt = () => {
|
|
358
|
-
const cwdLabel = cwd === userHome(authUser) ? "~" : basename(cwd) || "/";
|
|
359
|
-
return buildPrompt(authUser, hostname, cwdLabel);
|
|
360
|
-
};
|
|
361
|
-
const prompt = () => {
|
|
362
|
-
rl.setPrompt(renderPrompt());
|
|
363
|
-
rl.prompt();
|
|
364
|
-
};
|
|
365
|
-
// ── Auth (password gate) ───────────────────────────────────────────────────
|
|
366
|
-
if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
|
|
367
|
-
const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
|
|
368
|
-
if (!virtualShell.users.verifyPassword(authUser, password)) {
|
|
369
|
-
process.stderr.write("self-standalone: authentication failed\n");
|
|
370
|
-
process.exit(1);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
// ── Login banner ───────────────────────────────────────────────────────────
|
|
374
|
-
stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
|
|
375
|
-
writeLastLogin(authUser, remoteAddress);
|
|
376
|
-
await flushVfs();
|
|
377
|
-
// ── Event-driven line handler (enables completer) ──────────────────────────
|
|
378
|
-
//
|
|
379
|
-
// Key insight: readline's completer only fires when readline itself owns
|
|
380
|
-
// stdin (i.e. rl is not paused). We use the event-driven "line" pattern
|
|
381
|
-
// instead of a while(true)+rl.once("line") loop so readline stays active
|
|
382
|
-
// between commands. We pause only while awaiting async work, then resume
|
|
383
|
-
// immediately before re-prompting so the next Tab press is caught.
|
|
384
|
-
let busy = false;
|
|
385
|
-
rl.on("line", async (inputLine) => {
|
|
386
|
-
if (busy)
|
|
387
|
-
return; // shouldn't happen but guard re-entrancy
|
|
388
|
-
busy = true;
|
|
389
|
-
rl.pause();
|
|
390
|
-
const trimmed = inputLine.trim();
|
|
391
|
-
if (trimmed.length > 0) {
|
|
392
|
-
history.push(inputLine);
|
|
393
|
-
if (history.length > 500)
|
|
394
|
-
history = history.slice(history.length - 500);
|
|
395
|
-
saveHistory(history, authUser);
|
|
396
|
-
rlWithHistory.history = [...history].reverse();
|
|
397
|
-
}
|
|
398
|
-
const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
|
|
399
|
-
await handleCommandResult(result);
|
|
400
|
-
await flushVfs();
|
|
401
|
-
busy = false;
|
|
402
|
-
// Resume before prompt so readline can handle Tab on the next input
|
|
403
|
-
rl.resume();
|
|
404
|
-
prompt();
|
|
405
|
-
});
|
|
406
|
-
rl.on("SIGINT", () => {
|
|
407
|
-
stdout.write("^C\n");
|
|
408
|
-
rl.write("", { ctrl: true, name: "u" });
|
|
409
|
-
prompt();
|
|
410
|
-
});
|
|
411
|
-
rl.on("close", () => {
|
|
412
|
-
void flushVfs().then(() => {
|
|
413
|
-
console.log("");
|
|
414
|
-
process.exit(0);
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
// Initial prompt — readline is already active, completer live from first keystroke
|
|
418
|
-
prompt();
|
|
419
|
-
}
|
|
420
|
-
runReadlineShell().catch((error) => {
|
|
421
|
-
console.error("Failed to start readline SSH emulation:", error);
|
|
422
|
-
process.exit(1);
|
|
423
|
-
});
|
|
424
|
-
// ── Graceful shutdown (process-level) ────────────────────────────────────────
|
|
425
|
-
let _shuttingDown = false;
|
|
426
|
-
async function _gracefulShutdown(signal) {
|
|
427
|
-
if (_shuttingDown)
|
|
428
|
-
return;
|
|
429
|
-
_shuttingDown = true;
|
|
430
|
-
process.stdout.write(`\n[${signal}] Saving VFS...\n`);
|
|
431
|
-
try {
|
|
432
|
-
await virtualShell.vfs.stopAutoFlush();
|
|
433
|
-
}
|
|
434
|
-
catch { }
|
|
435
|
-
process.exit(0);
|
|
436
|
-
}
|
|
437
|
-
process.on("SIGTERM", () => { void _gracefulShutdown("SIGTERM"); });
|
|
438
|
-
process.on("beforeExit", () => { void virtualShell.vfs.stopAutoFlush(); });
|
|
439
|
-
process.on("uncaughtException", (error) => {
|
|
440
|
-
console.error("Uncaught exception:", error);
|
|
441
|
-
});
|
|
442
|
-
process.on("unhandledRejection", (error, promise) => {
|
|
443
|
-
console.error("Unhandled rejection at:", promise, "error:", error);
|
|
444
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { SshMimic } from "./SSHMimic/index";
|
|
2
|
-
import { VirtualShell } from "./VirtualShell";
|
|
3
|
-
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
4
|
-
const virtualShell = new VirtualShell(hostname, undefined, {
|
|
5
|
-
mode: "fs",
|
|
6
|
-
snapshotPath: ".vfs",
|
|
7
|
-
});
|
|
8
|
-
virtualShell.addCommand("demo", [], () => {
|
|
9
|
-
return {
|
|
10
|
-
stdout: "This is a demo command. It does nothing useful.",
|
|
11
|
-
exitCode: 0,
|
|
12
|
-
};
|
|
13
|
-
});
|
|
14
|
-
new SshMimic({
|
|
15
|
-
port: 2222,
|
|
16
|
-
hostname,
|
|
17
|
-
shell: virtualShell,
|
|
18
|
-
})
|
|
19
|
-
.start()
|
|
20
|
-
.catch((error) => {
|
|
21
|
-
console.error("Failed to start SSH Mimic:", error);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
});
|
|
24
|
-
process.on("uncaughtException", (error) => {
|
|
25
|
-
console.log("Oh my god, something terrible happened: ", error);
|
|
26
|
-
});
|
|
27
|
-
process.on("unhandledRejection", (error, promise) => {
|
|
28
|
-
console.log(" Oh Lord! We forgot to handle a promise rejection here: ", promise);
|
|
29
|
-
console.log(" The error was: ", error);
|
|
30
|
-
});
|
package/dist/standalone.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/standalone.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
|
|
2
|
-
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
3
|
-
const virtualShell = new VirtualShell(hostname, undefined, {
|
|
4
|
-
mode: "fs",
|
|
5
|
-
snapshotPath: ".vfs",
|
|
6
|
-
});
|
|
7
|
-
virtualShell.addCommand("demo", [], () => {
|
|
8
|
-
return {
|
|
9
|
-
stdout: "This is a demo command. It does nothing useful.",
|
|
10
|
-
exitCode: 0,
|
|
11
|
-
};
|
|
12
|
-
});
|
|
13
|
-
new VirtualSshServer({
|
|
14
|
-
port: 2222,
|
|
15
|
-
hostname,
|
|
16
|
-
shell: virtualShell,
|
|
17
|
-
})
|
|
18
|
-
.start()
|
|
19
|
-
.catch((error) => {
|
|
20
|
-
console.error("Failed to start SSH Mimic:", error);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
});
|
|
23
|
-
new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
|
|
24
|
-
.start()
|
|
25
|
-
.catch((error) => {
|
|
26
|
-
console.error("Failed to start SFTP Mimic:", error);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
});
|
|
29
|
-
// ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
30
|
-
// On SIGINT / SIGTERM: flush the WAL journal to a full checkpoint before exit.
|
|
31
|
-
// A kill -9 or OOM crash is unrecoverable here, but the WAL journal on disk
|
|
32
|
-
// guarantees all writes since the last checkpoint are replayed on next start.
|
|
33
|
-
let isShuttingDown = false;
|
|
34
|
-
async function gracefulShutdown(signal) {
|
|
35
|
-
if (isShuttingDown)
|
|
36
|
-
return;
|
|
37
|
-
isShuttingDown = true;
|
|
38
|
-
console.log(`\n[${signal}] Flushing VFS checkpoint before exit...`);
|
|
39
|
-
try {
|
|
40
|
-
await virtualShell.vfs.stopAutoFlush();
|
|
41
|
-
console.log("[shutdown] Checkpoint written. Goodbye.");
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
console.error("[shutdown] Flush failed:", err);
|
|
45
|
-
}
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
process.on("SIGINT", () => { void gracefulShutdown("SIGINT"); });
|
|
49
|
-
process.on("SIGTERM", () => { void gracefulShutdown("SIGTERM"); });
|
|
50
|
-
process.on("beforeExit", () => { void virtualShell.vfs.stopAutoFlush(); });
|
|
51
|
-
process.on("uncaughtException", (error) => {
|
|
52
|
-
console.debug("Oh my god, something terrible happened: ", error);
|
|
53
|
-
});
|
|
54
|
-
process.on("unhandledRejection", (error, promise) => {
|
|
55
|
-
console.debug(" Oh Lord! We forgot to handle a promise rejection here: ", promise);
|
|
56
|
-
console.debug(" The error was: ", error);
|
|
57
|
-
});
|
|
58
|
-
setInterval(() => {
|
|
59
|
-
const rss = process.memoryUsage().rss; // Just keep the event loop alive and prevent exit
|
|
60
|
-
console.debug(`Current memory usage: ${Math.round(rss / 1024 / 1024)} MB`);
|
|
61
|
-
}, 1000);
|