typescript-virtual-container 1.2.4 → 1.2.6
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 +1056 -1239
- package/benchmark-results.txt +20 -20
- package/dist/SSHMimic/exec.js +2 -2
- package/dist/SSHMimic/executor.d.ts +6 -7
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +77 -60
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +106 -24
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +14 -0
- package/dist/VirtualFileSystem/index.d.ts +115 -88
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +389 -264
- package/dist/VirtualShell/index.d.ts +3 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +4 -6
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +19 -2
- package/dist/VirtualShell/shellParser.d.ts +20 -2
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualShell/shellParser.js +229 -120
- package/dist/VirtualUserManager/index.d.ts +25 -0
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +33 -0
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +2 -0
- package/dist/commands/awk.d.ts +3 -0
- package/dist/commands/awk.d.ts.map +1 -0
- package/dist/commands/awk.js +29 -0
- package/dist/commands/base64.d.ts +3 -0
- package/dist/commands/base64.d.ts.map +1 -0
- package/dist/commands/base64.js +20 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +2 -0
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +2 -0
- package/dist/commands/chmod.d.ts +3 -0
- package/dist/commands/chmod.d.ts.map +1 -0
- package/dist/commands/chmod.js +33 -0
- package/dist/commands/clear.d.ts.map +1 -1
- package/dist/commands/clear.js +4 -1
- package/dist/commands/cp.d.ts +3 -0
- package/dist/commands/cp.d.ts.map +1 -0
- package/dist/commands/cp.js +70 -0
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +2 -0
- package/dist/commands/cut.d.ts +3 -0
- package/dist/commands/cut.d.ts.map +1 -0
- package/dist/commands/cut.js +27 -0
- package/dist/commands/date.d.ts +3 -0
- package/dist/commands/date.d.ts.map +1 -0
- package/dist/commands/date.js +22 -0
- package/dist/commands/deluser.d.ts.map +1 -1
- package/dist/commands/deluser.js +2 -0
- package/dist/commands/df.d.ts +3 -0
- package/dist/commands/df.d.ts.map +1 -0
- package/dist/commands/df.js +16 -0
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +40 -0
- package/dist/commands/du.d.ts +3 -0
- package/dist/commands/du.d.ts.map +1 -0
- package/dist/commands/du.js +39 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +2 -0
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +6 -14
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +11 -21
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +50 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +58 -35
- package/dist/commands/groups.d.ts +3 -0
- package/dist/commands/groups.d.ts.map +1 -0
- package/dist/commands/groups.js +12 -0
- package/dist/commands/gzip.d.ts +4 -0
- package/dist/commands/gzip.d.ts.map +1 -0
- package/dist/commands/gzip.js +40 -0
- package/dist/commands/head.d.ts +3 -0
- package/dist/commands/head.d.ts.map +1 -0
- package/dist/commands/head.js +32 -0
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +75 -3
- package/dist/commands/hostname.d.ts.map +1 -1
- package/dist/commands/hostname.js +2 -0
- package/dist/commands/htop.d.ts.map +1 -1
- package/dist/commands/htop.js +2 -0
- package/dist/commands/id.d.ts +3 -0
- package/dist/commands/id.d.ts.map +1 -0
- package/dist/commands/id.js +14 -0
- package/dist/commands/index.d.ts +5 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +104 -87
- package/dist/commands/kill.d.ts +3 -0
- package/dist/commands/kill.d.ts.map +1 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/ln.d.ts +3 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +44 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +2 -0
- package/dist/commands/mkdir.d.ts.map +1 -1
- package/dist/commands/mkdir.js +2 -0
- package/dist/commands/mv.d.ts +3 -0
- package/dist/commands/mv.d.ts.map +1 -0
- package/dist/commands/mv.js +37 -0
- package/dist/commands/nano.d.ts.map +1 -1
- package/dist/commands/nano.js +2 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +2 -0
- package/dist/commands/passwd.d.ts.map +1 -1
- package/dist/commands/passwd.js +2 -0
- package/dist/commands/ping.d.ts +3 -0
- package/dist/commands/ping.d.ts.map +1 -0
- package/dist/commands/ping.js +18 -0
- package/dist/commands/ps.d.ts +3 -0
- package/dist/commands/ps.d.ts.map +1 -0
- package/dist/commands/ps.js +17 -0
- package/dist/commands/pwd.d.ts.map +1 -1
- package/dist/commands/pwd.js +2 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +2 -0
- package/dist/commands/sed.d.ts +3 -0
- package/dist/commands/sed.d.ts.map +1 -0
- package/dist/commands/sed.js +47 -0
- package/dist/commands/set.d.ts +3 -0
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +19 -46
- package/dist/commands/sh.d.ts +0 -1
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +228 -35
- package/dist/commands/sleep.d.ts +3 -0
- package/dist/commands/sleep.d.ts.map +1 -0
- package/dist/commands/sleep.js +13 -0
- package/dist/commands/sort.d.ts +3 -0
- package/dist/commands/sort.d.ts.map +1 -0
- package/dist/commands/sort.js +37 -0
- package/dist/commands/su.d.ts.map +1 -1
- package/dist/commands/su.js +2 -0
- package/dist/commands/sudo.d.ts.map +1 -1
- package/dist/commands/sudo.js +2 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.d.ts.map +1 -0
- package/dist/commands/tail.js +35 -0
- package/dist/commands/tar.d.ts +3 -0
- package/dist/commands/tar.d.ts.map +1 -0
- package/dist/commands/tar.js +64 -0
- package/dist/commands/tee.d.ts +3 -0
- package/dist/commands/tee.d.ts.map +1 -0
- package/dist/commands/tee.js +29 -0
- package/dist/commands/touch.d.ts.map +1 -1
- package/dist/commands/touch.js +2 -0
- package/dist/commands/tr.d.ts +3 -0
- package/dist/commands/tr.d.ts.map +1 -0
- package/dist/commands/tr.js +24 -0
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +2 -0
- package/dist/commands/uname.d.ts +3 -0
- package/dist/commands/uname.d.ts.map +1 -0
- package/dist/commands/uname.js +21 -0
- package/dist/commands/uniq.d.ts +3 -0
- package/dist/commands/uniq.d.ts.map +1 -0
- package/dist/commands/uniq.js +33 -0
- package/dist/commands/unset.d.ts.map +1 -1
- package/dist/commands/unset.js +6 -10
- package/dist/commands/wc.d.ts +3 -0
- package/dist/commands/wc.d.ts.map +1 -0
- package/dist/commands/wc.js +50 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +2 -0
- package/dist/commands/who.d.ts.map +1 -1
- package/dist/commands/who.js +2 -0
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +2 -0
- package/dist/commands/xargs.d.ts +3 -0
- package/dist/commands/xargs.d.ts.map +1 -0
- package/dist/commands/xargs.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types/commands.d.ts +13 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/pipeline.d.ts +20 -0
- package/dist/types/pipeline.d.ts.map +1 -1
- package/package.json +5 -2
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/exec.ts +2 -2
- package/src/SSHMimic/executor.ts +95 -98
- package/src/SSHMimic/index.ts +138 -57
- package/src/SSHMimic/sftp.ts +15 -0
- package/src/VirtualFileSystem/index.ts +464 -292
- package/src/VirtualShell/index.ts +4 -6
- package/src/VirtualShell/shell.ts +19 -2
- package/src/VirtualShell/shellParser.ts +202 -168
- package/src/VirtualUserManager/index.ts +36 -0
- package/src/commands/adduser.ts +2 -0
- package/src/commands/awk.ts +30 -0
- package/src/commands/base64.ts +18 -0
- package/src/commands/cat.ts +2 -0
- package/src/commands/cd.ts +2 -0
- package/src/commands/chmod.ts +35 -0
- package/src/commands/clear.ts +4 -1
- package/src/commands/cp.ts +78 -0
- package/src/commands/curl.ts +2 -0
- package/src/commands/cut.ts +29 -0
- package/src/commands/date.ts +24 -0
- package/src/commands/deluser.ts +2 -0
- package/src/commands/df.ts +18 -0
- package/src/commands/diff.ts +29 -0
- package/src/commands/du.ts +39 -0
- package/src/commands/echo.ts +2 -0
- package/src/commands/env.ts +6 -16
- package/src/commands/export.ts +11 -24
- package/src/commands/find.ts +63 -0
- package/src/commands/grep.ts +51 -38
- package/src/commands/groups.ts +14 -0
- package/src/commands/gzip.ts +31 -0
- package/src/commands/head.ts +37 -0
- package/src/commands/help.ts +81 -3
- package/src/commands/hostname.ts +2 -0
- package/src/commands/htop.ts +2 -0
- package/src/commands/id.ts +16 -0
- package/src/commands/index.ts +114 -133
- package/src/commands/kill.ts +14 -0
- package/src/commands/ln.ts +49 -0
- package/src/commands/ls.ts +2 -0
- package/src/commands/mkdir.ts +2 -0
- package/src/commands/mv.ts +45 -0
- package/src/commands/nano.ts +2 -0
- package/src/commands/neofetch.ts +2 -0
- package/src/commands/passwd.ts +2 -0
- package/src/commands/ping.ts +20 -0
- package/src/commands/ps.ts +19 -0
- package/src/commands/pwd.ts +2 -0
- package/src/commands/rm.ts +2 -0
- package/src/commands/sed.ts +45 -0
- package/src/commands/set.ts +19 -50
- package/src/commands/sh.ts +192 -43
- package/src/commands/sleep.ts +14 -0
- package/src/commands/sort.ts +37 -0
- package/src/commands/su.ts +2 -0
- package/src/commands/sudo.ts +2 -0
- package/src/commands/tail.ts +39 -0
- package/src/commands/tar.ts +58 -0
- package/src/commands/tee.ts +25 -0
- package/src/commands/touch.ts +2 -0
- package/src/commands/tr.ts +24 -0
- package/src/commands/tree.ts +2 -0
- package/src/commands/uname.ts +20 -0
- package/src/commands/uniq.ts +28 -0
- package/src/commands/unset.ts +5 -12
- package/src/commands/wc.ts +50 -0
- package/src/commands/wget.ts +2 -0
- package/src/commands/who.ts +2 -0
- package/src/commands/whoami.ts +2 -0
- package/src/commands/xargs.ts +17 -0
- package/src/index.ts +1 -0
- package/src/types/commands.ts +14 -0
- package/src/types/pipeline.ts +23 -0
- package/standalone.js +93 -55
- package/standalone.js.map +4 -4
- package/tests/bun-test-shim.ts +1 -0
- package/tests/sftp.test.ts +115 -191
- package/tests/users.test.ts +42 -88
|
@@ -4,7 +4,7 @@ import type { CommandContext, CommandResult } from "../types/commands";
|
|
|
4
4
|
import type { ShellStream } from "../types/streams";
|
|
5
5
|
import type { PerfLogger } from "../utils/perfLogger";
|
|
6
6
|
import { createPerfLogger } from "../utils/perfLogger";
|
|
7
|
-
import VirtualFileSystem from "../VirtualFileSystem";
|
|
7
|
+
import VirtualFileSystem, { type VfsOptions } from "../VirtualFileSystem";
|
|
8
8
|
import { VirtualUserManager } from "../VirtualUserManager";
|
|
9
9
|
import { startShell } from "./shell";
|
|
10
10
|
|
|
@@ -38,7 +38,6 @@ function resolveAutoSudoForNewUsers(): boolean {
|
|
|
38
38
|
* client API.
|
|
39
39
|
*/
|
|
40
40
|
class VirtualShell extends EventEmitter {
|
|
41
|
-
basePath: string = ".";
|
|
42
41
|
vfs: VirtualFileSystem;
|
|
43
42
|
users: VirtualUserManager;
|
|
44
43
|
hostname: string;
|
|
@@ -50,19 +49,18 @@ class VirtualShell extends EventEmitter {
|
|
|
50
49
|
*
|
|
51
50
|
* @param hostname Virtual hostname used for prompts and idents.
|
|
52
51
|
* @param properties Customizable properties shown in `uname -a` and similar commands.
|
|
53
|
-
* @param
|
|
52
|
+
* @param vfsOptions Optional VFS persistence options (mode, snapshotPath).
|
|
54
53
|
*/
|
|
55
54
|
constructor(
|
|
56
55
|
hostname: string,
|
|
57
56
|
properties?: ShellProperties,
|
|
58
|
-
|
|
57
|
+
vfsOptions?: VfsOptions,
|
|
59
58
|
) {
|
|
60
59
|
super();
|
|
61
60
|
perf.mark("constructor");
|
|
62
61
|
this.hostname = hostname;
|
|
63
62
|
this.properties = properties || defaultShellProperties;
|
|
64
|
-
this.
|
|
65
|
-
this.vfs = new VirtualFileSystem(this.basePath);
|
|
63
|
+
this.vfs = new VirtualFileSystem(vfsOptions ?? {});
|
|
66
64
|
this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
|
|
67
65
|
|
|
68
66
|
// Store references to avoid TypeScript "used before assigned" errors
|
|
@@ -2,7 +2,8 @@ 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";
|
|
5
|
+
import { getCommandNames, makeDefaultEnv, runCommand } from "../commands";
|
|
6
|
+
import type { ShellEnv } from "../types/commands";
|
|
6
7
|
import {
|
|
7
8
|
spawnHtopProcess,
|
|
8
9
|
spawnNanoEditorProcess,
|
|
@@ -50,6 +51,7 @@ export function startShell(
|
|
|
50
51
|
let historyIndex: number | null = null;
|
|
51
52
|
let historyDraft = "";
|
|
52
53
|
let cwd = `/home/${authUser}`;
|
|
54
|
+
const shellEnv: ShellEnv = makeDefaultEnv(authUser, hostname);
|
|
53
55
|
let nanoSession: NanoSession | null = null;
|
|
54
56
|
let pendingSudo: PendingSudo | null = null;
|
|
55
57
|
const buildCurrentPrompt = (): string => {
|
|
@@ -62,6 +64,21 @@ export function startShell(
|
|
|
62
64
|
`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`,
|
|
63
65
|
);
|
|
64
66
|
|
|
67
|
+
// Load .bashrc if it exists
|
|
68
|
+
void (async () => {
|
|
69
|
+
const bashrcPath = `/home/${authUser}/.bashrc`;
|
|
70
|
+
if (shell.vfs.exists(bashrcPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const bashrc = shell.vfs.readFile(bashrcPath);
|
|
73
|
+
for (const line of bashrc.split("\n")) {
|
|
74
|
+
const l = line.trim();
|
|
75
|
+
if (!l || l.startsWith("#")) continue;
|
|
76
|
+
await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
|
|
77
|
+
}
|
|
78
|
+
} catch { /* ignore bashrc errors */ }
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
65
82
|
function renderLine(): void {
|
|
66
83
|
const prompt = buildCurrentPrompt();
|
|
67
84
|
stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
|
|
@@ -568,7 +585,7 @@ export function startShell(
|
|
|
568
585
|
|
|
569
586
|
if (line.length > 0) {
|
|
570
587
|
const result = await Promise.resolve(
|
|
571
|
-
runCommand(line, authUser, hostname, "shell", cwd, shell),
|
|
588
|
+
runCommand(line, authUser, hostname, "shell", cwd, shell, undefined, shellEnv),
|
|
572
589
|
);
|
|
573
590
|
|
|
574
591
|
pushHistory(line);
|
|
@@ -1,228 +1,262 @@
|
|
|
1
|
-
import type { Pipeline, PipelineCommand } from "../types/pipeline";
|
|
1
|
+
import type { Pipeline, PipelineCommand, Script, Statement, LogicalOp } from "../types/pipeline";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a shell input line into a Script (sequence of statements connected
|
|
7
|
+
* by && / || / ;). Each statement contains one Pipeline (commands connected
|
|
8
|
+
* by |).
|
|
9
|
+
*/
|
|
10
|
+
export function parseScript(rawInput: string): Script {
|
|
5
11
|
const trimmed = rawInput.trim();
|
|
12
|
+
if (!trimmed) return { statements: [], isValid: true };
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
try {
|
|
15
|
+
const statements = parseStatements(trimmed);
|
|
16
|
+
return { statements, isValid: true };
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return { statements: [], isValid: false, error: (e as Error).message };
|
|
9
19
|
}
|
|
20
|
+
}
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
/** Legacy compat: parse a single pipeline (no &&/||/;) */
|
|
23
|
+
export function parseShellPipeline(rawInput: string): Pipeline {
|
|
24
|
+
const trimmed = rawInput.trim();
|
|
25
|
+
if (!trimmed) return { commands: [], isValid: true };
|
|
26
|
+
try {
|
|
27
|
+
const commands = parsePipeline(trimmed);
|
|
28
|
+
return { commands, isValid: true };
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return { commands: [], isValid: false, error: (e as Error).message };
|
|
19
31
|
}
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
// ── Variable & tilde expansion ────────────────────────────────────────────────
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Expand ~ and $VAR / ${VAR} / ${VAR:-default} / $(cmd placeholder) in a
|
|
38
|
+
* token, given the current env vars and home path.
|
|
39
|
+
* Command substitution $(…) is NOT executed here — it's left as a marker so
|
|
40
|
+
* the executor can handle it.
|
|
41
|
+
*/
|
|
42
|
+
export function expandToken(
|
|
43
|
+
token: string,
|
|
44
|
+
env: Record<string, string>,
|
|
45
|
+
authUser: string,
|
|
46
|
+
lastExitCode = 0,
|
|
47
|
+
): string {
|
|
48
|
+
// tilde expansion
|
|
49
|
+
token = token.replace(/^~(\/|$)/, `/home/${authUser}$1`);
|
|
50
|
+
|
|
51
|
+
// $? special var
|
|
52
|
+
token = token.replace(/\$\?/g, String(lastExitCode));
|
|
53
|
+
// $$ PID (mock)
|
|
54
|
+
token = token.replace(/\$\$/g, "1");
|
|
55
|
+
// $# argc (0 for interactive)
|
|
56
|
+
token = token.replace(/\$#/g, "0");
|
|
57
|
+
|
|
58
|
+
// ${VAR:-default} and ${VAR:+value}
|
|
59
|
+
token = token.replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, name, def) =>
|
|
60
|
+
env[name] ?? def,
|
|
61
|
+
);
|
|
62
|
+
token = token.replace(/\$\{([^}:]+):\+([^}]*)\}/g, (_, name, val) =>
|
|
63
|
+
env[name] ? val : "",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ${VAR}
|
|
67
|
+
token = token.replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? "");
|
|
68
|
+
|
|
69
|
+
// $VAR (greedy: match longest valid identifier)
|
|
70
|
+
token = token.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) =>
|
|
71
|
+
env[name] ?? "",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return token;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Expand glob patterns (*, ?, [abc]) against a list of entries.
|
|
79
|
+
* Returns the original pattern if no match.
|
|
80
|
+
*/
|
|
81
|
+
export function expandGlob(pattern: string, entries: string[]): string[] {
|
|
82
|
+
if (!/[*?[]/.test(pattern)) return [pattern];
|
|
83
|
+
const regex = globToRegex(pattern);
|
|
84
|
+
const matches = entries.filter((e) => regex.test(e));
|
|
85
|
+
return matches.length > 0 ? matches.sort() : [pattern];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function globToRegex(pattern: string): RegExp {
|
|
89
|
+
let re = "^";
|
|
90
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
91
|
+
const c = pattern[i]!;
|
|
92
|
+
if (c === "*") re += ".*";
|
|
93
|
+
else if (c === "?") re += ".";
|
|
94
|
+
else if (c === "[") {
|
|
95
|
+
const close = pattern.indexOf("]", i + 1);
|
|
96
|
+
if (close === -1) re += "\\[";
|
|
97
|
+
else {
|
|
98
|
+
re += `[${pattern.slice(i + 1, close)}]`;
|
|
99
|
+
i = close;
|
|
100
|
+
}
|
|
101
|
+
} else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
102
|
+
}
|
|
103
|
+
return new RegExp(`${re}$`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Internal parser ───────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function parseStatements(input: string): Statement[] {
|
|
109
|
+
// Split by ;, &&, || — respecting quotes and parens
|
|
110
|
+
const segments = splitByLogicalOps(input);
|
|
111
|
+
const statements: Statement[] = [];
|
|
112
|
+
|
|
113
|
+
for (const seg of segments) {
|
|
114
|
+
const commands = parsePipeline(seg.text.trim());
|
|
115
|
+
const stmt: Statement = { pipeline: { commands, isValid: true } };
|
|
116
|
+
if (seg.op) stmt.op = seg.op;
|
|
117
|
+
statements.push(stmt);
|
|
35
118
|
}
|
|
36
119
|
|
|
37
|
-
return
|
|
120
|
+
return statements;
|
|
38
121
|
}
|
|
39
122
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
123
|
+
interface Segment { text: string; op?: LogicalOp }
|
|
124
|
+
|
|
125
|
+
function splitByLogicalOps(input: string): Segment[] {
|
|
126
|
+
const segments: Segment[] = [];
|
|
43
127
|
let current = "";
|
|
44
|
-
let
|
|
45
|
-
let
|
|
128
|
+
let depth = 0; // parens/subshell depth
|
|
129
|
+
let inQ = false;
|
|
130
|
+
let qChar = "";
|
|
46
131
|
let i = 0;
|
|
47
132
|
|
|
133
|
+
const flush = (op?: LogicalOp) => {
|
|
134
|
+
if (current.trim()) segments.push({ text: current, op });
|
|
135
|
+
current = "";
|
|
136
|
+
};
|
|
137
|
+
|
|
48
138
|
while (i < input.length) {
|
|
49
|
-
const ch = input[i]
|
|
50
|
-
|
|
51
|
-
if ((ch === '"' || ch === "'") && (i === 0 || input[i - 1] !== "\\")) {
|
|
52
|
-
if (!inQuotes) {
|
|
53
|
-
inQuotes = true;
|
|
54
|
-
quoteChar = ch;
|
|
55
|
-
} else if (ch === quoteChar) {
|
|
56
|
-
inQuotes = false;
|
|
57
|
-
}
|
|
58
|
-
current += ch;
|
|
59
|
-
i++;
|
|
60
|
-
} else if (ch === "|" && !inQuotes) {
|
|
61
|
-
if (!current.trim()) {
|
|
62
|
-
return {
|
|
63
|
-
tokens: [],
|
|
64
|
-
error: "Syntax error near unexpected token '|'",
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
tokens.push(current.trim());
|
|
68
|
-
current = "";
|
|
69
|
-
i++;
|
|
70
|
-
} else {
|
|
71
|
-
current += ch;
|
|
72
|
-
i++;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
139
|
+
const ch = input[i]!;
|
|
140
|
+
const ch2 = input.slice(i, i + 2);
|
|
75
141
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
error: "Syntax error: unterminated quote",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
142
|
+
if ((ch === '"' || ch === "'") && !inQ) { inQ = true; qChar = ch; current += ch; i++; continue; }
|
|
143
|
+
if (inQ && ch === qChar) { inQ = false; current += ch; i++; continue; }
|
|
144
|
+
if (inQ) { current += ch; i++; continue; }
|
|
82
145
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
error: "Syntax error near unexpected token '|'",
|
|
87
|
-
};
|
|
88
|
-
}
|
|
146
|
+
if (ch === "(") { depth++; current += ch; i++; continue; }
|
|
147
|
+
if (ch === ")") { depth--; current += ch; i++; continue; }
|
|
148
|
+
if (depth > 0) { current += ch; i++; continue; }
|
|
89
149
|
|
|
90
|
-
|
|
150
|
+
if (ch2 === "&&") { flush("&&"); i += 2; continue; }
|
|
151
|
+
if (ch2 === "||") { flush("||"); i += 2; continue; }
|
|
152
|
+
if (ch === ";") { flush(";"); i++; continue; }
|
|
91
153
|
|
|
92
|
-
|
|
154
|
+
current += ch; i++;
|
|
155
|
+
}
|
|
156
|
+
flush();
|
|
157
|
+
return segments;
|
|
93
158
|
}
|
|
94
159
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
error?: string;
|
|
160
|
+
function parsePipeline(input: string): PipelineCommand[] {
|
|
161
|
+
const pipeTokens = splitByPipe(input);
|
|
162
|
+
return pipeTokens.map(parseCommandWithRedirections);
|
|
99
163
|
}
|
|
100
164
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
function splitByPipe(input: string): string[] {
|
|
166
|
+
const tokens: string[] = [];
|
|
167
|
+
let current = "";
|
|
168
|
+
let inQ = false;
|
|
169
|
+
let qChar = "";
|
|
104
170
|
|
|
105
|
-
|
|
106
|
-
|
|
171
|
+
for (let i = 0; i < input.length; i++) {
|
|
172
|
+
const ch = input[i]!;
|
|
173
|
+
if ((ch === '"' || ch === "'") && !inQ) { inQ = true; qChar = ch; current += ch; continue; }
|
|
174
|
+
if (inQ && ch === qChar) { inQ = false; current += ch; continue; }
|
|
175
|
+
if (inQ) { current += ch; continue; }
|
|
176
|
+
|
|
177
|
+
// || was already consumed at statement level, bare | is pipe
|
|
178
|
+
if (ch === "|" && input[i + 1] !== "|") {
|
|
179
|
+
if (!current.trim()) throw new Error("Syntax error near unexpected token '|'");
|
|
180
|
+
tokens.push(current.trim());
|
|
181
|
+
current = "";
|
|
182
|
+
} else {
|
|
183
|
+
current += ch;
|
|
184
|
+
}
|
|
107
185
|
}
|
|
108
186
|
|
|
187
|
+
const tail = current.trim();
|
|
188
|
+
if (!tail && tokens.length > 0) throw new Error("Syntax error near unexpected token '|'");
|
|
189
|
+
if (tail) tokens.push(tail);
|
|
190
|
+
return tokens;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseCommandWithRedirections(token: string): PipelineCommand {
|
|
194
|
+
const parts = tokenizeCommand(token);
|
|
195
|
+
if (parts.length === 0) return { name: "", args: [] };
|
|
196
|
+
|
|
109
197
|
const cmdParts: string[] = [];
|
|
110
198
|
let inputFile: string | undefined;
|
|
111
199
|
let outputFile: string | undefined;
|
|
112
200
|
let appendOutput = false;
|
|
113
|
-
|
|
114
201
|
let i = 0;
|
|
115
|
-
while (i < parts.length) {
|
|
116
|
-
const part = parts[i] as string;
|
|
117
202
|
|
|
203
|
+
while (i < parts.length) {
|
|
204
|
+
const part = parts[i]!;
|
|
118
205
|
if (part === "<") {
|
|
119
206
|
i++;
|
|
120
|
-
if (i >= parts.length)
|
|
121
|
-
return {
|
|
122
|
-
isValid: false,
|
|
123
|
-
error: "Syntax error: expected filename after <",
|
|
124
|
-
};
|
|
125
|
-
}
|
|
207
|
+
if (i >= parts.length) throw new Error("Syntax error: expected filename after <");
|
|
126
208
|
inputFile = parts[i];
|
|
127
209
|
i++;
|
|
128
210
|
} else if (part === ">>") {
|
|
129
211
|
i++;
|
|
130
|
-
if (i >= parts.length)
|
|
131
|
-
|
|
132
|
-
isValid: false,
|
|
133
|
-
error: "Syntax error: expected filename after >>",
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
outputFile = parts[i];
|
|
137
|
-
appendOutput = true;
|
|
138
|
-
i++;
|
|
212
|
+
if (i >= parts.length) throw new Error("Syntax error: expected filename after >>");
|
|
213
|
+
outputFile = parts[i]; appendOutput = true; i++;
|
|
139
214
|
} else if (part === ">") {
|
|
140
215
|
i++;
|
|
141
|
-
if (i >= parts.length)
|
|
142
|
-
|
|
143
|
-
isValid: false,
|
|
144
|
-
error: "Syntax error: expected filename after >",
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
outputFile = parts[i];
|
|
148
|
-
appendOutput = false;
|
|
149
|
-
i++;
|
|
216
|
+
if (i >= parts.length) throw new Error("Syntax error: expected filename after >");
|
|
217
|
+
outputFile = parts[i]; appendOutput = false; i++;
|
|
150
218
|
} else {
|
|
151
|
-
cmdParts.push(part);
|
|
152
|
-
i++;
|
|
219
|
+
cmdParts.push(part); i++;
|
|
153
220
|
}
|
|
154
221
|
}
|
|
155
222
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const name = (cmdParts[0] as string).toLowerCase();
|
|
161
|
-
const args = cmdParts.slice(1);
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
command: {
|
|
165
|
-
name,
|
|
166
|
-
args,
|
|
167
|
-
inputFile,
|
|
168
|
-
outputFile,
|
|
169
|
-
appendOutput,
|
|
170
|
-
},
|
|
171
|
-
isValid: true,
|
|
172
|
-
};
|
|
223
|
+
const name = (cmdParts[0] ?? "").toLowerCase();
|
|
224
|
+
return { name, args: cmdParts.slice(1), inputFile, outputFile, appendOutput };
|
|
173
225
|
}
|
|
174
226
|
|
|
175
|
-
/** Tokenize a command, respecting quotes and handling >> vs > */
|
|
176
227
|
function tokenizeCommand(input: string): string[] {
|
|
177
228
|
const tokens: string[] = [];
|
|
178
229
|
let current = "";
|
|
179
|
-
let
|
|
180
|
-
let
|
|
230
|
+
let inQ = false;
|
|
231
|
+
let qChar = "";
|
|
181
232
|
let i = 0;
|
|
182
233
|
|
|
183
234
|
while (i < input.length) {
|
|
184
|
-
const ch = input[i]
|
|
235
|
+
const ch = input[i]!;
|
|
185
236
|
const next = input[i + 1];
|
|
186
237
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (!inQuotes) {
|
|
190
|
-
inQuotes = true;
|
|
191
|
-
quoteChar = ch;
|
|
192
|
-
} else if (ch === quoteChar) {
|
|
193
|
-
inQuotes = false;
|
|
194
|
-
quoteChar = "";
|
|
195
|
-
} else {
|
|
196
|
-
current += ch;
|
|
197
|
-
}
|
|
198
|
-
i++;
|
|
199
|
-
} else if (ch === " " && !inQuotes) {
|
|
200
|
-
if (current) {
|
|
201
|
-
tokens.push(current);
|
|
202
|
-
current = "";
|
|
203
|
-
}
|
|
204
|
-
i++;
|
|
205
|
-
} else if ((ch === ">" || ch === "<") && !inQuotes) {
|
|
206
|
-
if (current) {
|
|
207
|
-
tokens.push(current);
|
|
208
|
-
current = "";
|
|
209
|
-
}
|
|
210
|
-
if (ch === ">" && next === ">") {
|
|
211
|
-
tokens.push(">>");
|
|
212
|
-
i += 2;
|
|
213
|
-
} else {
|
|
214
|
-
tokens.push(ch);
|
|
215
|
-
i++;
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
current += ch;
|
|
219
|
-
i++;
|
|
238
|
+
if ((ch === '"' || ch === "'") && !inQ) {
|
|
239
|
+
inQ = true; qChar = ch; i++; continue;
|
|
220
240
|
}
|
|
221
|
-
|
|
241
|
+
if (inQ && ch === qChar) {
|
|
242
|
+
inQ = false; qChar = ""; i++; continue;
|
|
243
|
+
}
|
|
244
|
+
if (inQ) { current += ch; i++; continue; }
|
|
222
245
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
246
|
+
if (ch === " ") {
|
|
247
|
+
if (current) { tokens.push(current); current = ""; }
|
|
248
|
+
i++; continue;
|
|
249
|
+
}
|
|
226
250
|
|
|
251
|
+
if ((ch === ">" || ch === "<") && !inQ) {
|
|
252
|
+
if (current) { tokens.push(current); current = ""; }
|
|
253
|
+
if (ch === ">" && next === ">") { tokens.push(">>"); i += 2; }
|
|
254
|
+
else { tokens.push(ch); i++; }
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
current += ch; i++;
|
|
259
|
+
}
|
|
260
|
+
if (current) tokens.push(current);
|
|
227
261
|
return tokens;
|
|
228
262
|
}
|
|
@@ -648,6 +648,42 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
648
648
|
throw new Error("invalid password");
|
|
649
649
|
}
|
|
650
650
|
}
|
|
651
|
+
private readonly authorizedKeys = new Map<string, Array<{ algo: string; data: Buffer }>>();
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Adds an SSH public key for a user, enabling public-key authentication.
|
|
655
|
+
*
|
|
656
|
+
* @param username Target user.
|
|
657
|
+
* @param algo Key algorithm (e.g. "ssh-rsa", "ssh-ed25519").
|
|
658
|
+
* @param data Raw key data as a Buffer (the base64-decoded key bytes).
|
|
659
|
+
*/
|
|
660
|
+
public addAuthorizedKey(username: string, algo: string, data: Buffer): void {
|
|
661
|
+
perf.mark("addAuthorizedKey");
|
|
662
|
+
const keys = this.authorizedKeys.get(username) ?? [];
|
|
663
|
+
keys.push({ algo, data });
|
|
664
|
+
this.authorizedKeys.set(username, keys);
|
|
665
|
+
this.emit("key:add", { username, algo });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Removes all authorized keys for a user.
|
|
670
|
+
*
|
|
671
|
+
* @param username Target user.
|
|
672
|
+
*/
|
|
673
|
+
public removeAuthorizedKeys(username: string): void {
|
|
674
|
+
this.authorizedKeys.delete(username);
|
|
675
|
+
this.emit("key:remove", { username });
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Returns the list of authorized keys for a user.
|
|
680
|
+
* Returns an empty array when no keys are registered.
|
|
681
|
+
*
|
|
682
|
+
* @param username Target user.
|
|
683
|
+
*/
|
|
684
|
+
public getAuthorizedKeys(username: string): Array<{ algo: string; data: Buffer }> {
|
|
685
|
+
return this.authorizedKeys.get(username) ?? [];
|
|
686
|
+
}
|
|
651
687
|
}
|
|
652
688
|
|
|
653
689
|
function normalizeVfsPath(targetPath: string): string {
|
package/src/commands/adduser.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { ShellModule } from "../types/commands";
|
|
|
2
2
|
|
|
3
3
|
export const adduserCommand: ShellModule = {
|
|
4
4
|
name: "adduser",
|
|
5
|
+
description: "Add a new user",
|
|
6
|
+
category: "users",
|
|
5
7
|
params: ["<username> <password>"],
|
|
6
8
|
run: async ({ authUser, shell, args }) => {
|
|
7
9
|
if (authUser !== "root") {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { getFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const awkCommand: ShellModule = {
|
|
5
|
+
name: "awk",
|
|
6
|
+
description: "Pattern scanning and processing language (minimal)",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["[-F <sep>] '<program>' [file]"],
|
|
9
|
+
run: ({ args, stdin }) => {
|
|
10
|
+
const sep = (getFlag(args, ["-F"]) as string | undefined) ?? " ";
|
|
11
|
+
const prog = args.find((a) => !a.startsWith("-") && a !== sep);
|
|
12
|
+
if (!prog) return { stderr: "awk: no program", exitCode: 1 };
|
|
13
|
+
|
|
14
|
+
// Only support print $N and {print $N} patterns
|
|
15
|
+
const printMatch = prog.match(/^\{?\s*print\s+([^}]+)\s*\}?$/);
|
|
16
|
+
if (!printMatch) return { stderr: `awk: unsupported program: ${prog}`, exitCode: 1 };
|
|
17
|
+
|
|
18
|
+
const fields = printMatch[1]!.split(/\s*,\s*/).map((f) => f.trim());
|
|
19
|
+
const lines = (stdin ?? "").split("\n").filter(Boolean);
|
|
20
|
+
const out = lines.map((line) => {
|
|
21
|
+
const parts = line.split(sep === " " ? /\s+/ : sep);
|
|
22
|
+
return fields.map((f) => {
|
|
23
|
+
if (f === "$0") return line;
|
|
24
|
+
const n = parseInt(f.replace("$", ""), 10);
|
|
25
|
+
return Number.isNaN(n) ? f.replace(/"/g, "") : (parts[n - 1] ?? "");
|
|
26
|
+
}).join(sep === " " ? "\t" : sep);
|
|
27
|
+
});
|
|
28
|
+
return { stdout: out.join("\n"), exitCode: 0 };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const base64Command: ShellModule = {
|
|
5
|
+
name: "base64",
|
|
6
|
+
description: "Encode/decode base64",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["[-d] [file]"],
|
|
9
|
+
run: ({ args, stdin }) => {
|
|
10
|
+
const decode = ifFlag(args, ["-d", "--decode"]);
|
|
11
|
+
const input = stdin ?? "";
|
|
12
|
+
if (decode) {
|
|
13
|
+
try { return { stdout: Buffer.from(input.trim(), "base64").toString("utf8"), exitCode: 0 }; }
|
|
14
|
+
catch { return { stderr: "base64: invalid input", exitCode: 1 }; }
|
|
15
|
+
}
|
|
16
|
+
return { stdout: Buffer.from(input).toString("base64"), exitCode: 0 };
|
|
17
|
+
},
|
|
18
|
+
};
|
package/src/commands/cat.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { assertPathAccess, resolveReadablePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const catCommand: ShellModule = {
|
|
6
6
|
name: "cat",
|
|
7
|
+
description: "Concatenate and print files",
|
|
8
|
+
category: "files",
|
|
7
9
|
params: ["<file>"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args }) => {
|
|
9
11
|
const fileArg = getArg(args, 0);
|
package/src/commands/cd.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
3
3
|
|
|
4
4
|
export const cdCommand: ShellModule = {
|
|
5
5
|
name: "cd",
|
|
6
|
+
description: "Change directory",
|
|
7
|
+
category: "navigation",
|
|
6
8
|
params: ["[path]"],
|
|
7
9
|
run: ({ authUser, shell, cwd, args, mode }) => {
|
|
8
10
|
const target = resolvePath(cwd, args[0] ?? "/virtual-env-js");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const chmodCommand: ShellModule = {
|
|
5
|
+
name: "chmod",
|
|
6
|
+
description: "Change file permissions",
|
|
7
|
+
category: "files",
|
|
8
|
+
params: ["<mode> <file>"],
|
|
9
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
10
|
+
const [modeArg, fileArg] = args;
|
|
11
|
+
if (!modeArg || !fileArg) {
|
|
12
|
+
return { stderr: "chmod: missing operand", exitCode: 1 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const filePath = resolvePath(cwd, fileArg);
|
|
16
|
+
try {
|
|
17
|
+
assertPathAccess(authUser, filePath, "chmod");
|
|
18
|
+
if (!shell.vfs.exists(filePath)) {
|
|
19
|
+
return {
|
|
20
|
+
stderr: `chmod: ${fileArg}: No such file or directory`,
|
|
21
|
+
exitCode: 1,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const mode = parseInt(modeArg, 8);
|
|
25
|
+
if (Number.isNaN(mode)) {
|
|
26
|
+
return { stderr: `chmod: invalid mode: ${modeArg}`, exitCode: 1 };
|
|
27
|
+
}
|
|
28
|
+
shell.vfs.chmod(filePath, mode);
|
|
29
|
+
return { exitCode: 0 };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return { stderr: `chmod: ${msg}`, exitCode: 1 };
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|