typescript-virtual-container 1.1.0 → 1.1.1-c
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 +18 -0
- package/README.md +45 -5
- package/dist/SSHClient/index.d.ts +138 -0
- package/dist/SSHClient/index.d.ts.map +1 -0
- package/dist/SSHClient/index.js +216 -0
- package/dist/SSHMimic/exec.d.ts +4 -0
- package/dist/SSHMimic/exec.d.ts.map +1 -0
- package/dist/SSHMimic/exec.js +21 -0
- package/dist/SSHMimic/executor.d.ts +9 -0
- package/dist/SSHMimic/executor.d.ts.map +1 -0
- package/dist/SSHMimic/executor.js +131 -0
- package/dist/SSHMimic/hostKey.d.ts +2 -0
- package/dist/SSHMimic/hostKey.d.ts.map +1 -0
- package/dist/SSHMimic/hostKey.js +17 -0
- package/dist/SSHMimic/index.d.ts +39 -0
- package/dist/SSHMimic/index.d.ts.map +1 -0
- package/dist/SSHMimic/index.js +113 -0
- package/dist/SSHMimic/loginFormat.d.ts +2 -0
- package/dist/SSHMimic/loginFormat.d.ts.map +1 -0
- package/dist/SSHMimic/loginFormat.js +10 -0
- package/dist/SSHMimic/prompt.d.ts +2 -0
- package/dist/SSHMimic/prompt.d.ts.map +1 -0
- package/dist/SSHMimic/prompt.js +9 -0
- package/dist/VirtualFileSystem/archive.d.ts +5 -0
- package/dist/VirtualFileSystem/archive.d.ts.map +1 -0
- package/dist/VirtualFileSystem/archive.js +56 -0
- package/dist/VirtualFileSystem/index.d.ts +131 -0
- package/dist/VirtualFileSystem/index.d.ts.map +1 -0
- package/dist/VirtualFileSystem/index.js +355 -0
- package/dist/VirtualFileSystem/internalTypes.d.ts +18 -0
- package/dist/VirtualFileSystem/internalTypes.d.ts.map +1 -0
- package/dist/VirtualFileSystem/internalTypes.js +0 -0
- package/dist/VirtualFileSystem/path.d.ts +9 -0
- package/dist/VirtualFileSystem/path.d.ts.map +1 -0
- package/dist/VirtualFileSystem/path.js +49 -0
- package/dist/VirtualFileSystem/snapshot.d.ts +5 -0
- package/dist/VirtualFileSystem/snapshot.d.ts.map +1 -0
- package/dist/VirtualFileSystem/snapshot.js +59 -0
- package/dist/VirtualFileSystem/tree.d.ts +3 -0
- package/dist/VirtualFileSystem/tree.d.ts.map +1 -0
- package/dist/VirtualFileSystem/tree.js +19 -0
- package/dist/VirtualShell/index.d.ts +86 -0
- package/dist/VirtualShell/index.d.ts.map +1 -0
- package/dist/VirtualShell/index.js +129 -0
- package/dist/VirtualShell/shell.d.ts +5 -0
- package/dist/VirtualShell/shell.d.ts.map +1 -0
- package/dist/VirtualShell/shell.js +473 -0
- package/dist/VirtualShell/shellParser.d.ts +4 -0
- package/dist/VirtualShell/shellParser.d.ts.map +1 -0
- package/dist/VirtualShell/shellParser.js +207 -0
- package/dist/VirtualUserManager/index.d.ts +168 -0
- package/dist/VirtualUserManager/index.d.ts.map +1 -0
- package/dist/VirtualUserManager/index.js +375 -0
- package/dist/commands/adduser.d.ts +3 -0
- package/dist/commands/adduser.d.ts.map +1 -0
- package/dist/commands/adduser.js +18 -0
- package/dist/commands/cat.d.ts +3 -0
- package/dist/commands/cat.d.ts.map +1 -0
- package/dist/commands/cat.js +15 -0
- package/dist/commands/cd.d.ts +3 -0
- package/dist/commands/cd.d.ts.map +1 -0
- package/dist/commands/cd.js +17 -0
- package/dist/commands/clear.d.ts +3 -0
- package/dist/commands/clear.d.ts.map +1 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/command-helpers.d.ts +23 -0
- package/dist/commands/command-helpers.d.ts.map +1 -0
- package/dist/commands/command-helpers.js +139 -0
- package/dist/commands/curl.d.ts +3 -0
- package/dist/commands/curl.d.ts.map +1 -0
- package/dist/commands/curl.js +44 -0
- package/dist/commands/deluser.d.ts +3 -0
- package/dist/commands/deluser.d.ts.map +1 -0
- package/dist/commands/deluser.js +15 -0
- package/dist/commands/echo.d.ts +3 -0
- package/dist/commands/echo.d.ts.map +1 -0
- package/dist/commands/echo.js +22 -0
- package/dist/commands/env.d.ts +3 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +18 -0
- package/dist/commands/exit.d.ts +3 -0
- package/dist/commands/exit.d.ts.map +1 -0
- package/dist/commands/exit.js +5 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +34 -0
- package/dist/commands/grep.d.ts +3 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +69 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +7 -0
- package/dist/commands/helpers.d.ts +26 -0
- package/dist/commands/helpers.d.ts.map +1 -0
- package/dist/commands/helpers.js +160 -0
- package/dist/commands/hostname.d.ts +3 -0
- package/dist/commands/hostname.d.ts.map +1 -0
- package/dist/commands/hostname.js +5 -0
- package/dist/commands/htop.d.ts +3 -0
- package/dist/commands/htop.d.ts.map +1 -0
- package/dist/commands/htop.js +10 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +212 -0
- package/dist/commands/ls.d.ts +3 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +47 -0
- package/dist/commands/mkdir.d.ts +3 -0
- package/dist/commands/mkdir.d.ts.map +1 -0
- package/dist/commands/mkdir.js +21 -0
- package/dist/commands/nano.d.ts +3 -0
- package/dist/commands/nano.d.ts.map +1 -0
- package/dist/commands/nano.js +27 -0
- package/dist/commands/neofetch.d.ts +3 -0
- package/dist/commands/neofetch.d.ts.map +1 -0
- package/dist/commands/neofetch.js +32 -0
- package/dist/commands/pwd.d.ts +3 -0
- package/dist/commands/pwd.d.ts.map +1 -0
- package/dist/commands/pwd.js +5 -0
- package/dist/commands/rm.d.ts +3 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +29 -0
- package/dist/commands/set.d.ts +7 -0
- package/dist/commands/set.d.ts.map +1 -0
- package/dist/commands/set.js +64 -0
- package/dist/commands/sh.d.ts +4 -0
- package/dist/commands/sh.d.ts.map +1 -0
- package/dist/commands/sh.js +45 -0
- package/dist/commands/su.d.ts +3 -0
- package/dist/commands/su.d.ts.map +1 -0
- package/dist/commands/su.js +24 -0
- package/dist/commands/sudo.d.ts +3 -0
- package/dist/commands/sudo.d.ts.map +1 -0
- package/dist/commands/sudo.js +47 -0
- package/dist/commands/touch.d.ts +3 -0
- package/dist/commands/touch.d.ts.map +1 -0
- package/dist/commands/touch.js +18 -0
- package/dist/commands/tree.d.ts +3 -0
- package/dist/commands/tree.d.ts.map +1 -0
- package/dist/commands/tree.js +11 -0
- package/dist/commands/unset.d.ts +3 -0
- package/dist/commands/unset.d.ts.map +1 -0
- package/dist/commands/unset.js +15 -0
- package/dist/commands/wget.d.ts +3 -0
- package/dist/commands/wget.d.ts.map +1 -0
- package/dist/commands/wget.js +113 -0
- package/dist/commands/who.d.ts +3 -0
- package/dist/commands/who.d.ts.map +1 -0
- package/dist/commands/who.js +15 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/modules/neofetch.d.ts +19 -0
- package/dist/modules/neofetch.d.ts.map +1 -0
- package/dist/modules/neofetch.js +284 -0
- package/dist/modules/shellInteractive.d.ts +6 -0
- package/dist/modules/shellInteractive.d.ts.map +1 -0
- package/dist/modules/shellInteractive.js +26 -0
- package/dist/modules/shellRuntime.d.ts +11 -0
- package/dist/modules/shellRuntime.d.ts.map +1 -0
- package/dist/modules/shellRuntime.js +52 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +25 -0
- package/dist/types/commands.d.ts +89 -0
- package/dist/types/commands.d.ts.map +1 -0
- package/dist/types/commands.js +0 -0
- package/dist/types/pipeline.d.ts +23 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +0 -0
- package/dist/types/streams.d.ts +32 -0
- package/dist/types/streams.d.ts.map +1 -0
- package/dist/types/streams.js +0 -0
- package/dist/types/vfs.d.ts +71 -0
- package/dist/types/vfs.d.ts.map +1 -0
- package/dist/types/vfs.js +0 -0
- package/package.json +4 -2
- package/src/{SSHMimic/client.ts → SSHClient/index.ts} +2 -2
- package/src/SSHMimic/exec.ts +1 -1
- package/src/SSHMimic/executor.ts +8 -8
- package/src/VirtualFileSystem/index.ts +26 -0
- package/src/VirtualShell/index.ts +17 -1
- package/src/VirtualShell/shell.ts +19 -107
- package/src/VirtualShell/shellParser.ts +32 -7
- package/src/VirtualUserManager/index.ts +149 -0
- package/src/{VirtualShell/commands → commands}/adduser.ts +1 -1
- package/src/{VirtualShell/commands → commands}/cat.ts +1 -1
- package/src/{VirtualShell/commands → commands}/cd.ts +1 -1
- package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
- package/src/{VirtualShell/commands → commands}/curl.ts +2 -2
- package/src/{VirtualShell/commands → commands}/deluser.ts +1 -1
- package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
- package/src/{VirtualShell/commands → commands}/env.ts +1 -1
- package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
- package/src/{VirtualShell/commands → commands}/export.ts +1 -1
- package/src/{VirtualShell/commands → commands}/grep.ts +1 -1
- package/src/{VirtualShell/commands → commands}/help.ts +1 -1
- package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
- package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
- package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
- package/src/{VirtualShell/commands → commands}/index.ts +7 -4
- package/src/{VirtualShell/commands → commands}/ls.ts +1 -1
- package/src/{VirtualShell/commands → commands}/mkdir.ts +1 -1
- package/src/{VirtualShell/commands → commands}/nano.ts +1 -1
- package/src/{VirtualShell/commands → commands}/neofetch.ts +2 -2
- package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
- package/src/{VirtualShell/commands → commands}/rm.ts +1 -1
- package/src/{VirtualShell/commands → commands}/set.ts +1 -1
- package/src/{VirtualShell/commands → commands}/sh.ts +1 -1
- package/src/{VirtualShell/commands → commands}/su.ts +1 -1
- package/src/{VirtualShell/commands → commands}/sudo.ts +1 -1
- package/src/{VirtualShell/commands → commands}/touch.ts +2 -2
- package/src/{VirtualShell/commands → commands}/tree.ts +1 -1
- package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
- package/src/{VirtualShell/commands → commands}/wget.ts +2 -2
- package/src/{VirtualShell/commands → commands}/who.ts +2 -2
- package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
- package/src/index.ts +2 -2
- package/{modules → src/modules}/neofetch.ts +56 -51
- package/src/modules/shellInteractive.ts +57 -0
- package/src/modules/shellRuntime.ts +76 -0
- package/tests/command-helpers.test.ts +1 -1
- package/tests/helpers.test.ts +1 -1
- package/tests/users.test.ts +60 -0
- package/tsconfig.json +19 -8
- /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export function shellQuote(value) {
|
|
4
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5
|
+
}
|
|
6
|
+
export function toTtyLines(text) {
|
|
7
|
+
return text
|
|
8
|
+
.replace(/\r\n/g, "\n")
|
|
9
|
+
.replace(/\r/g, "\n")
|
|
10
|
+
.replace(/\n/g, "\r\n");
|
|
11
|
+
}
|
|
12
|
+
export function withTerminalSize(command, terminalSize) {
|
|
13
|
+
const cols = Number.isFinite(terminalSize.cols) && terminalSize.cols > 0
|
|
14
|
+
? Math.floor(terminalSize.cols)
|
|
15
|
+
: 80;
|
|
16
|
+
const rows = Number.isFinite(terminalSize.rows) && terminalSize.rows > 0
|
|
17
|
+
? Math.floor(terminalSize.rows)
|
|
18
|
+
: 24;
|
|
19
|
+
return `stty cols ${cols} rows ${rows} 2>/dev/null; ${command}`;
|
|
20
|
+
}
|
|
21
|
+
export function resolvePath(base, inputPath) {
|
|
22
|
+
if (!inputPath || inputPath.trim() === "" || inputPath === ".") {
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
return inputPath.startsWith("/")
|
|
26
|
+
? path.posix.normalize(inputPath)
|
|
27
|
+
: path.posix.normalize(path.posix.join(base, inputPath));
|
|
28
|
+
}
|
|
29
|
+
export async function collectChildPids(parentPid) {
|
|
30
|
+
try {
|
|
31
|
+
const childrenRaw = await readFile(`/proc/${parentPid}/task/${parentPid}/children`, "utf8");
|
|
32
|
+
const directChildren = childrenRaw
|
|
33
|
+
.trim()
|
|
34
|
+
.split(/\s+/)
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((value) => Number.parseInt(value, 10))
|
|
37
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
38
|
+
const nested = await Promise.all(directChildren.map((pid) => collectChildPids(pid)));
|
|
39
|
+
return [...directChildren, ...nested.flat()];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function getVisibleHtopPidList(rootPid = process.pid) {
|
|
46
|
+
const descendants = await collectChildPids(rootPid);
|
|
47
|
+
const unique = Array.from(new Set(descendants)).sort((a, b) => a - b);
|
|
48
|
+
if (unique.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return unique.join(",");
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { VirtualShell, VirtualSshServer } from ".";
|
|
2
|
+
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
3
|
+
const virtualShell = new VirtualShell(hostname);
|
|
4
|
+
virtualShell.addCommand("demo", [], () => {
|
|
5
|
+
return {
|
|
6
|
+
stdout: "This is a demo command. It does nothing useful.",
|
|
7
|
+
exitCode: 0,
|
|
8
|
+
};
|
|
9
|
+
});
|
|
10
|
+
new VirtualSshServer({
|
|
11
|
+
port: 2222,
|
|
12
|
+
hostname,
|
|
13
|
+
shell: virtualShell,
|
|
14
|
+
})
|
|
15
|
+
.start()
|
|
16
|
+
.then((port) => {
|
|
17
|
+
// if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
|
|
18
|
+
// else {
|
|
19
|
+
console.log(`SSH Mimic initialized. Listening on port ${port}.`);
|
|
20
|
+
// }
|
|
21
|
+
})
|
|
22
|
+
.catch((error) => {
|
|
23
|
+
console.error("Failed to start SSH Mimic:", error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Command invocation mode used by shell runtime. */
|
|
2
|
+
export type CommandMode = "shell" | "exec";
|
|
3
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
+
import type { VirtualActiveSession } from "../VirtualUserManager";
|
|
5
|
+
/**
|
|
6
|
+
* Normalized command execution output.
|
|
7
|
+
*
|
|
8
|
+
* A command can write text, control session lifecycle, request UI state
|
|
9
|
+
* transitions, and update active identity/cwd.
|
|
10
|
+
*/
|
|
11
|
+
export interface CommandResult {
|
|
12
|
+
/** Standard output payload to append in terminal. */
|
|
13
|
+
stdout?: string;
|
|
14
|
+
/** Standard error payload to append in terminal. */
|
|
15
|
+
stderr?: string;
|
|
16
|
+
/** Request full terminal clear before next prompt. */
|
|
17
|
+
clearScreen?: boolean;
|
|
18
|
+
/** Request current shell/exec session close. */
|
|
19
|
+
closeSession?: boolean;
|
|
20
|
+
/** Optional exit code (default behavior handled by caller). */
|
|
21
|
+
exitCode?: number;
|
|
22
|
+
/** Optional cwd to apply for next prompt iteration. */
|
|
23
|
+
nextCwd?: string;
|
|
24
|
+
/** Optional user switch for current session state. */
|
|
25
|
+
switchUser?: string;
|
|
26
|
+
/** Request opening built-in nano editor workflow. */
|
|
27
|
+
openEditor?: NanoEditorSession;
|
|
28
|
+
/** Request opening built-in htop-like screen. */
|
|
29
|
+
openHtop?: boolean;
|
|
30
|
+
/** Request sudo password challenge flow. */
|
|
31
|
+
sudoChallenge?: SudoChallenge;
|
|
32
|
+
}
|
|
33
|
+
/** Deferred sudo challenge metadata returned by sudo command. */
|
|
34
|
+
export interface SudoChallenge {
|
|
35
|
+
/** User currently requesting elevation. */
|
|
36
|
+
username: string;
|
|
37
|
+
/** Target identity for elevated command. */
|
|
38
|
+
targetUser: string;
|
|
39
|
+
/** Command to execute after successful challenge; null for login shell. */
|
|
40
|
+
commandLine: string | null;
|
|
41
|
+
/** True when challenge targets interactive login shell. */
|
|
42
|
+
loginShell: boolean;
|
|
43
|
+
/** Prompt text shown before password input. */
|
|
44
|
+
prompt: string;
|
|
45
|
+
}
|
|
46
|
+
/** State payload used by nano command interactive editor flow. */
|
|
47
|
+
export interface NanoEditorSession {
|
|
48
|
+
/** Final destination path to write when save succeeds. */
|
|
49
|
+
targetPath: string;
|
|
50
|
+
/** Temporary scratch path used while editing. */
|
|
51
|
+
tempPath: string;
|
|
52
|
+
/** Initial editor content shown to user. */
|
|
53
|
+
initialContent: string;
|
|
54
|
+
}
|
|
55
|
+
/** Runtime context object passed to each command module. */
|
|
56
|
+
export interface CommandContext {
|
|
57
|
+
/** Authenticated user currently bound to stream. */
|
|
58
|
+
authUser: string;
|
|
59
|
+
/** Virtual hostname shown in prompt and banners. */
|
|
60
|
+
hostname: string;
|
|
61
|
+
/** Snapshot of currently active user sessions. */
|
|
62
|
+
activeSessions: VirtualActiveSession[];
|
|
63
|
+
/** Original unparsed command line input. */
|
|
64
|
+
rawInput: string;
|
|
65
|
+
/** Invocation mode (interactive shell or direct exec). */
|
|
66
|
+
mode: CommandMode;
|
|
67
|
+
/** Tokenized arguments excluding command name. */
|
|
68
|
+
args: string[];
|
|
69
|
+
/** Virtual shell instance. */
|
|
70
|
+
shell: VirtualShell;
|
|
71
|
+
/** Optional stdin payload (used by pipes/redirections). */
|
|
72
|
+
stdin?: string;
|
|
73
|
+
/** Current working directory for command execution. */
|
|
74
|
+
cwd: string;
|
|
75
|
+
}
|
|
76
|
+
/** Contract implemented by each shell command module. */
|
|
77
|
+
export interface ShellModule {
|
|
78
|
+
/** Primary command name used in CLI. */
|
|
79
|
+
name: string;
|
|
80
|
+
/** Parameter help snippets displayed by help command. */
|
|
81
|
+
params: string[];
|
|
82
|
+
/** Command handler implementation. */
|
|
83
|
+
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
|
|
84
|
+
/** Optional alternative command names. */
|
|
85
|
+
aliases?: string[];
|
|
86
|
+
}
|
|
87
|
+
/** Command return union allowing sync or async handlers. */
|
|
88
|
+
export type CommandOutcome = CommandResult | Promise<CommandResult>;
|
|
89
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +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;CAC9B;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC7B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2DAA2D;IAC3D,UAAU,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IACjC,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC9B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,cAAc,EAAE,oBAAoB,EAAE,CAAC;IACvC,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,8BAA8B;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,GAAG,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrE,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC"}
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Represents a single command in a pipeline. */
|
|
2
|
+
export interface PipelineCommand {
|
|
3
|
+
/** Command name */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Command arguments */
|
|
6
|
+
args: string[];
|
|
7
|
+
/** Input redirection file path (< file) */
|
|
8
|
+
inputFile?: string;
|
|
9
|
+
/** Output redirection file path (> file) */
|
|
10
|
+
outputFile?: string;
|
|
11
|
+
/** Append to output file (>> file) */
|
|
12
|
+
appendOutput?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Represents a parsed shell pipeline */
|
|
15
|
+
export interface Pipeline {
|
|
16
|
+
/** List of commands in the pipeline */
|
|
17
|
+
commands: PipelineCommand[];
|
|
18
|
+
/** Whether this is a valid pipeline */
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
/** Error message if parsing failed */
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/types/pipeline.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC/B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,QAAQ;IACxB,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf"}
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal stream contract used by exec command handlers.
|
|
3
|
+
*/
|
|
4
|
+
export interface ExecStream {
|
|
5
|
+
/** Writes text to stdout channel. */
|
|
6
|
+
write(data: string): void;
|
|
7
|
+
/** Signals output completion. */
|
|
8
|
+
end(): void;
|
|
9
|
+
/** Sets process-like exit code for exec response. */
|
|
10
|
+
exit(code: number): void;
|
|
11
|
+
/** Writable stderr channel. */
|
|
12
|
+
stderr: {
|
|
13
|
+
/** Writes text to stderr channel. */
|
|
14
|
+
write(data: string): void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Minimal interactive stream contract used by shell mode.
|
|
19
|
+
*/
|
|
20
|
+
export interface ShellStream {
|
|
21
|
+
/** Writes text to shell output channel. */
|
|
22
|
+
write(data: string): void;
|
|
23
|
+
/** Sets shell exit code on close. */
|
|
24
|
+
exit(code: number): void;
|
|
25
|
+
/** Ends shell stream. */
|
|
26
|
+
end(): void;
|
|
27
|
+
/** Subscribes to incoming user input chunks. */
|
|
28
|
+
on(event: "data", listener: (chunk: Buffer) => void): void;
|
|
29
|
+
/** Subscribes to stream close event. */
|
|
30
|
+
on(event: "close", listener: () => void): void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=streams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../src/types/streams.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,qCAAqC;IACrC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iCAAiC;IACjC,GAAG,IAAI,IAAI,CAAC;IACZ,qDAAqD;IACrD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,+BAA+B;IAC/B,MAAM,EAAE;QACP,qCAAqC;QACrC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,2CAA2C;IAC3C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,qCAAqC;IACrC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yBAAyB;IACzB,GAAG,IAAI,IAAI,CAAC;IACZ,gDAAgD;IAChD,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,wCAAwC;IACxC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAC/C"}
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Supported virtual node kinds. */
|
|
2
|
+
export type VfsNodeType = "file" | "directory";
|
|
3
|
+
/** Shared metadata fields available on file and directory stats. */
|
|
4
|
+
export interface VfsBaseNode {
|
|
5
|
+
/** Node name without parent path. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Absolute normalized node path. */
|
|
8
|
+
path: string;
|
|
9
|
+
/** POSIX-like mode bits. */
|
|
10
|
+
mode: number;
|
|
11
|
+
/** Node creation timestamp. */
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
/** Last update timestamp. */
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
}
|
|
16
|
+
/** Stat shape returned for file nodes. */
|
|
17
|
+
export interface VfsFileNode extends VfsBaseNode {
|
|
18
|
+
type: "file";
|
|
19
|
+
/** True when file content stored as gzip bytes. */
|
|
20
|
+
compressed: boolean;
|
|
21
|
+
/** Stored byte length (compressed when compressed=true). */
|
|
22
|
+
size: number;
|
|
23
|
+
}
|
|
24
|
+
/** Stat shape returned for directory nodes. */
|
|
25
|
+
export interface VfsDirectoryNode extends VfsBaseNode {
|
|
26
|
+
type: "directory";
|
|
27
|
+
/** Number of direct children in directory. */
|
|
28
|
+
childrenCount: number;
|
|
29
|
+
}
|
|
30
|
+
/** Union of file and directory stat responses. */
|
|
31
|
+
export type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
|
|
32
|
+
/** Optional behavior flags for writeFile operations. */
|
|
33
|
+
export interface WriteFileOptions {
|
|
34
|
+
/** POSIX-like mode to apply on create or overwrite. */
|
|
35
|
+
mode?: number;
|
|
36
|
+
/** Store content compressed with gzip. */
|
|
37
|
+
compress?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Optional behavior flags for remove operations. */
|
|
40
|
+
export interface RemoveOptions {
|
|
41
|
+
/** Allow deleting non-empty directory trees. */
|
|
42
|
+
recursive?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Base snapshot node schema used for archive serialization. */
|
|
45
|
+
export interface VfsSnapshotBaseNode {
|
|
46
|
+
name: string;
|
|
47
|
+
mode: number;
|
|
48
|
+
/** ISO-8601 creation timestamp. */
|
|
49
|
+
createdAt: string;
|
|
50
|
+
/** ISO-8601 update timestamp. */
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
}
|
|
53
|
+
/** Serialized snapshot shape for file nodes. */
|
|
54
|
+
export interface VfsSnapshotFileNode extends VfsSnapshotBaseNode {
|
|
55
|
+
type: "file";
|
|
56
|
+
compressed: boolean;
|
|
57
|
+
/** Base64-encoded raw file bytes. */
|
|
58
|
+
contentBase64: string;
|
|
59
|
+
}
|
|
60
|
+
/** Serialized snapshot shape for directory nodes. */
|
|
61
|
+
export interface VfsSnapshotDirectoryNode extends VfsSnapshotBaseNode {
|
|
62
|
+
type: "directory";
|
|
63
|
+
children: VfsSnapshotNode[];
|
|
64
|
+
}
|
|
65
|
+
/** Union of serialized snapshot node variants. */
|
|
66
|
+
export type VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode;
|
|
67
|
+
/** Top-level serialized filesystem snapshot. */
|
|
68
|
+
export interface VfsSnapshot {
|
|
69
|
+
root: VfsSnapshotDirectoryNode;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=vfs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/types/vfs.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACpE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE7E,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,wBAAwB,CAAC;CAC/B"}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typescript-virtual-container",
|
|
3
3
|
"description": "In-memory SSH server with virtual filesystem and typed programmatic API",
|
|
4
|
-
"
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
5
6
|
"type": "module",
|
|
6
|
-
"version": "1.1.
|
|
7
|
+
"version": "1.1.1c",
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"keywords": [
|
|
9
10
|
"ssh",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"lint": "bunx --bun @biomejs/biome lint ./src",
|
|
21
22
|
"lint:write": "bunx --bun @biomejs/biome lint --write ./src",
|
|
22
23
|
"test": "bunx --bun @biomejs/biome test ./src",
|
|
24
|
+
"build": "tsc --project tsconfig.json",
|
|
23
25
|
"deploy:npm": "npm publish --access public"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { runCommand } from "../commands";
|
|
1
2
|
import type { CommandResult } from "../types/commands";
|
|
2
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
3
|
-
import { runCommand } from "../VirtualShell/commands";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Programmatic client for executing shell commands against a virtual shell.
|
|
@@ -154,7 +154,7 @@ export class SshClient {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
try {
|
|
157
|
-
|
|
157
|
+
this.shell.writeFileAsUser(this.username, path, content);
|
|
158
158
|
return { stdout: `File '${path}' written`, exitCode: 0 };
|
|
159
159
|
} catch (error) {
|
|
160
160
|
return {
|
package/src/SSHMimic/exec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { runCommand } from "../commands";
|
|
1
2
|
import type { ExecStream } from "../types/streams";
|
|
2
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
3
|
-
import { runCommand } from "../VirtualShell/commands";
|
|
4
4
|
|
|
5
5
|
function toTtyLines(text: string): string {
|
|
6
6
|
return text
|
package/src/SSHMimic/executor.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { runCommand as runSingleCommand } from "../commands";
|
|
2
|
+
import { resolvePath } from "../commands/helpers";
|
|
1
3
|
import type { CommandMode, CommandResult } from "../types/commands";
|
|
2
4
|
import type { Pipeline, PipelineCommand } from "../types/pipeline";
|
|
3
5
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
-
import { runCommand as runSingleCommand } from "../VirtualShell/commands";
|
|
5
|
-
import { resolvePath } from "../VirtualShell/commands/helpers";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Execute a parsed pipeline, chaining commands and handling redirections.
|
|
@@ -90,12 +90,12 @@ async function executeSingleCommandWithRedirections(
|
|
|
90
90
|
if (cmd.appendOutput) {
|
|
91
91
|
try {
|
|
92
92
|
const existing = shell.vfs.readFile(outputPath);
|
|
93
|
-
shell.
|
|
93
|
+
shell.writeFileAsUser(authUser, outputPath, existing + output);
|
|
94
94
|
} catch {
|
|
95
|
-
shell.
|
|
95
|
+
shell.writeFileAsUser(authUser, outputPath, output);
|
|
96
96
|
}
|
|
97
97
|
} else {
|
|
98
|
-
shell.
|
|
98
|
+
shell.writeFileAsUser(authUser, outputPath, output);
|
|
99
99
|
}
|
|
100
100
|
return { ...result, stdout: "" };
|
|
101
101
|
} catch {
|
|
@@ -165,12 +165,12 @@ async function executePipelineChain(
|
|
|
165
165
|
if (cmd.appendOutput) {
|
|
166
166
|
try {
|
|
167
167
|
const existing = shell.vfs.readFile(outputPath);
|
|
168
|
-
shell.
|
|
168
|
+
shell.writeFileAsUser(authUser, outputPath, existing + output);
|
|
169
169
|
} catch {
|
|
170
|
-
shell.
|
|
170
|
+
shell.writeFileAsUser(authUser, outputPath, output);
|
|
171
171
|
}
|
|
172
172
|
} else {
|
|
173
|
-
shell.
|
|
173
|
+
shell.writeFileAsUser(authUser, outputPath, output);
|
|
174
174
|
}
|
|
175
175
|
currentOutput = "";
|
|
176
176
|
} catch {
|
|
@@ -24,6 +24,18 @@ class VirtualFileSystem {
|
|
|
24
24
|
private readonly archivePath: string;
|
|
25
25
|
private dirty = false;
|
|
26
26
|
|
|
27
|
+
private computeNodeUsageBytes(node: InternalNode): number {
|
|
28
|
+
if (node.type === "file") {
|
|
29
|
+
return node.content.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let total = 0;
|
|
33
|
+
for (const child of node.children.values()) {
|
|
34
|
+
total += this.computeNodeUsageBytes(child);
|
|
35
|
+
}
|
|
36
|
+
return total;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* Creates a virtual filesystem instance.
|
|
29
41
|
*
|
|
@@ -287,6 +299,20 @@ class VirtualFileSystem {
|
|
|
287
299
|
return renderTree(node, rootLabel);
|
|
288
300
|
}
|
|
289
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Computes total stored file bytes under a path.
|
|
304
|
+
*
|
|
305
|
+
* File usage is based on in-memory stored bytes, including compressed
|
|
306
|
+
* payload size when files are marked as compressed.
|
|
307
|
+
*
|
|
308
|
+
* @param targetPath File or directory path to measure, defaults to root.
|
|
309
|
+
* @returns Total byte usage for file content under target path.
|
|
310
|
+
*/
|
|
311
|
+
public getUsageBytes(targetPath: string = "/"): number {
|
|
312
|
+
const node = getNode(this.root, targetPath);
|
|
313
|
+
return this.computeNodeUsageBytes(node);
|
|
314
|
+
}
|
|
315
|
+
|
|
290
316
|
/**
|
|
291
317
|
* Compresses file content with gzip and flags node as compressed.
|
|
292
318
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createCustomCommand, registerCommand, runCommand } from "../commands";
|
|
2
3
|
import type { CommandContext, CommandResult } from "../types/commands";
|
|
3
4
|
import type { ShellStream } from "../types/streams";
|
|
4
5
|
import VirtualFileSystem from "../VirtualFileSystem";
|
|
5
6
|
import { VirtualUserManager } from "../VirtualUserManager";
|
|
6
|
-
import { createCustomCommand, registerCommand, runCommand } from "./commands";
|
|
7
7
|
import { startShell } from "./shell";
|
|
8
8
|
|
|
9
9
|
export interface ShellProperties {
|
|
@@ -170,6 +170,22 @@ class VirtualShell {
|
|
|
170
170
|
public getHostname(): string {
|
|
171
171
|
return this?.hostname;
|
|
172
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Writes a file on behalf of a user with quota enforcement.
|
|
176
|
+
*
|
|
177
|
+
* @param authUser User performing the write.
|
|
178
|
+
* @param targetPath Destination path.
|
|
179
|
+
* @param content File content.
|
|
180
|
+
*/
|
|
181
|
+
public writeFileAsUser(
|
|
182
|
+
authUser: string,
|
|
183
|
+
targetPath: string,
|
|
184
|
+
content: string | Buffer,
|
|
185
|
+
): void {
|
|
186
|
+
this.users.assertWriteWithinQuota(authUser, targetPath, content);
|
|
187
|
+
this.vfs.writeFile(targetPath, content);
|
|
188
|
+
}
|
|
173
189
|
}
|
|
174
190
|
|
|
175
191
|
export { VirtualShell };
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ShellProperties, VirtualShell } from ".";
|
|
5
|
+
import { getCommandNames, runCommand } from "../commands";
|
|
6
|
+
import {
|
|
7
|
+
spawnHtopProcess,
|
|
8
|
+
spawnNanoEditorProcess,
|
|
9
|
+
} from "../modules/shellInteractive";
|
|
10
|
+
import {
|
|
11
|
+
getVisibleHtopPidList,
|
|
12
|
+
resolvePath,
|
|
13
|
+
type TerminalSize,
|
|
14
|
+
toTtyLines,
|
|
15
|
+
} from "../modules/shellRuntime";
|
|
5
16
|
import { formatLoginDate } from "../SSHMimic/loginFormat";
|
|
6
17
|
import { buildPrompt } from "../SSHMimic/prompt";
|
|
7
18
|
import type { ShellStream } from "../types/streams";
|
|
8
19
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
9
|
-
import { getCommandNames, runCommand } from "./commands";
|
|
10
20
|
|
|
11
21
|
interface NanoSession {
|
|
12
22
|
kind: "nano" | "htop";
|
|
@@ -24,22 +34,6 @@ interface PendingSudo {
|
|
|
24
34
|
buffer: string;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
function shellQuote(value: string): string {
|
|
28
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface TerminalSize {
|
|
32
|
-
cols: number;
|
|
33
|
-
rows: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toTtyLines(text: string): string {
|
|
37
|
-
return text
|
|
38
|
-
.replace(/\r\n/g, "\n")
|
|
39
|
-
.replace(/\r/g, "\n")
|
|
40
|
-
.replace(/\n/g, "\r\n");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
37
|
export function startShell(
|
|
44
38
|
properties: ShellProperties,
|
|
45
39
|
stream: ShellStream,
|
|
@@ -68,60 +62,6 @@ export function startShell(
|
|
|
68
62
|
`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`,
|
|
69
63
|
);
|
|
70
64
|
|
|
71
|
-
async function collectChildPids(parentPid: number): Promise<number[]> {
|
|
72
|
-
try {
|
|
73
|
-
const childrenRaw = await readFile(
|
|
74
|
-
`/proc/${parentPid}/task/${parentPid}/children`,
|
|
75
|
-
"utf8",
|
|
76
|
-
);
|
|
77
|
-
const directChildren = childrenRaw
|
|
78
|
-
.trim()
|
|
79
|
-
.split(/\s+/)
|
|
80
|
-
.filter(Boolean)
|
|
81
|
-
.map((value) => Number.parseInt(value, 10))
|
|
82
|
-
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
83
|
-
|
|
84
|
-
const nested = await Promise.all(
|
|
85
|
-
directChildren.map((pid) => collectChildPids(pid)),
|
|
86
|
-
);
|
|
87
|
-
return [...directChildren, ...nested.flat()];
|
|
88
|
-
} catch {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function getVisibleHtopPidList(): Promise<string | null> {
|
|
94
|
-
const rootPid = process.pid;
|
|
95
|
-
const descendants = await collectChildPids(rootPid);
|
|
96
|
-
const unique = Array.from(new Set(descendants)).sort((a, b) => a - b);
|
|
97
|
-
if (unique.length === 0) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return unique.join(",");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function withTerminalSize(command: string): string {
|
|
105
|
-
const cols =
|
|
106
|
-
Number.isFinite(terminalSize.cols) && terminalSize.cols > 0
|
|
107
|
-
? Math.floor(terminalSize.cols)
|
|
108
|
-
: 80;
|
|
109
|
-
const rows =
|
|
110
|
-
Number.isFinite(terminalSize.rows) && terminalSize.rows > 0
|
|
111
|
-
? Math.floor(terminalSize.rows)
|
|
112
|
-
: 24;
|
|
113
|
-
return `stty cols ${cols} rows ${rows} 2>/dev/null; ${command}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolvePath(base: string, inputPath: string): string {
|
|
117
|
-
if (!inputPath || inputPath.trim() === "" || inputPath === ".") {
|
|
118
|
-
return base;
|
|
119
|
-
}
|
|
120
|
-
return inputPath.startsWith("/")
|
|
121
|
-
? path.posix.normalize(inputPath)
|
|
122
|
-
: path.posix.normalize(path.posix.join(base, inputPath));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
65
|
function renderLine(): void {
|
|
126
66
|
const prompt = buildCurrentPrompt();
|
|
127
67
|
stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
|
|
@@ -236,7 +176,11 @@ export function startShell(
|
|
|
236
176
|
if (activeSession.kind === "nano") {
|
|
237
177
|
try {
|
|
238
178
|
const updatedContent = await readFile(activeSession.tempPath, "utf8");
|
|
239
|
-
shell.
|
|
179
|
+
shell.writeFileAsUser(
|
|
180
|
+
authUser,
|
|
181
|
+
activeSession.targetPath,
|
|
182
|
+
updatedContent,
|
|
183
|
+
);
|
|
240
184
|
await shell.vfs.flushMirror();
|
|
241
185
|
} catch {
|
|
242
186
|
// If temp file does not exist, nano exited without writing.
|
|
@@ -261,23 +205,7 @@ export function startShell(
|
|
|
261
205
|
await writeFile(tempPath, initialContent, "utf8");
|
|
262
206
|
}
|
|
263
207
|
|
|
264
|
-
const
|
|
265
|
-
const editor = spawn("script", ["-qfec", command, "/dev/null"], {
|
|
266
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
267
|
-
env: {
|
|
268
|
-
...process.env,
|
|
269
|
-
// biome-ignore lint/style/useNamingConvention: TERM is an environment variable conventionally in uppercase
|
|
270
|
-
TERM: process.env.TERM ?? "xterm-256color",
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
editor.stdout.on("data", (data: Buffer) => {
|
|
275
|
-
stream.write(data.toString("utf8"));
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
editor.stderr.on("data", (data: Buffer) => {
|
|
279
|
-
stream.write(data.toString("utf8"));
|
|
280
|
-
});
|
|
208
|
+
const editor = spawnNanoEditorProcess(tempPath, terminalSize, stream);
|
|
281
209
|
|
|
282
210
|
editor.on("error", (error: Error) => {
|
|
283
211
|
stream.write(`nano: ${error.message}\r\n`);
|
|
@@ -303,23 +231,7 @@ export function startShell(
|
|
|
303
231
|
return;
|
|
304
232
|
}
|
|
305
233
|
|
|
306
|
-
const
|
|
307
|
-
const monitor = spawn("script", ["-qfec", command, "/dev/null"], {
|
|
308
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
309
|
-
env: {
|
|
310
|
-
...process.env,
|
|
311
|
-
// biome-ignore lint/style/useNamingConvention: TERM is an environment variable conventionally in uppercase
|
|
312
|
-
TERM: process.env.TERM ?? "xterm-256color",
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
monitor.stdout.on("data", (data: Buffer) => {
|
|
317
|
-
stream.write(data.toString("utf8"));
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
monitor.stderr.on("data", (data: Buffer) => {
|
|
321
|
-
stream.write(data.toString("utf8"));
|
|
322
|
-
});
|
|
234
|
+
const monitor = spawnHtopProcess(pidList, terminalSize, stream);
|
|
323
235
|
|
|
324
236
|
monitor.on("error", (error: Error) => {
|
|
325
237
|
stream.write(`htop: ${error.message}\r\n`);
|