typescript-virtual-container 1.2.5 → 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 +387 -193
- package/benchmark-results.txt +21 -21
- 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.map +1 -1
- package/dist/SSHMimic/index.js +6 -20
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +14 -0
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +13 -36
- 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.map +1 -1
- 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.map +1 -1
- package/dist/commands/chmod.js +2 -0
- package/dist/commands/clear.d.ts.map +1 -1
- package/dist/commands/clear.js +4 -1
- package/dist/commands/cp.d.ts.map +1 -1
- package/dist/commands/cp.js +2 -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.map +1 -1
- package/dist/commands/find.js +2 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +4 -7
- 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.map +1 -1
- package/dist/commands/head.js +2 -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 +89 -62
- 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.map +1 -1
- package/dist/commands/ln.js +2 -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.map +1 -1
- package/dist/commands/mv.js +2 -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.map +1 -1
- package/dist/commands/tail.js +2 -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.map +1 -1
- package/dist/commands/wc.js +2 -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/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 +1 -1
- package/src/SSHMimic/exec.ts +2 -2
- package/src/SSHMimic/executor.ts +95 -98
- package/src/SSHMimic/index.ts +15 -49
- package/src/SSHMimic/sftp.ts +15 -0
- package/src/VirtualFileSystem/index.ts +27 -75
- package/src/VirtualShell/shell.ts +19 -2
- package/src/VirtualShell/shellParser.ts +202 -168
- package/src/VirtualUserManager/index.ts +2 -7
- 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 +2 -0
- package/src/commands/clear.ts +4 -1
- package/src/commands/cp.ts +2 -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 +2 -0
- package/src/commands/grep.ts +4 -7
- package/src/commands/groups.ts +14 -0
- package/src/commands/gzip.ts +31 -0
- package/src/commands/head.ts +2 -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 +98 -99
- package/src/commands/kill.ts +14 -0
- package/src/commands/ln.ts +2 -0
- package/src/commands/ls.ts +2 -0
- package/src/commands/mkdir.ts +2 -0
- package/src/commands/mv.ts +2 -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 +2 -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 +2 -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/types/commands.ts +14 -0
- package/src/types/pipeline.ts +23 -0
- package/standalone.js +92 -64
- package/standalone.js.map +4 -4
- package/tests/users.test.ts +5 -34
|
@@ -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,10 +648,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
648
648
|
throw new Error("invalid password");
|
|
649
649
|
}
|
|
650
650
|
}
|
|
651
|
-
private readonly authorizedKeys = new Map<
|
|
652
|
-
string,
|
|
653
|
-
Array<{ algo: string; data: Buffer }>
|
|
654
|
-
>();
|
|
651
|
+
private readonly authorizedKeys = new Map<string, Array<{ algo: string; data: Buffer }>>();
|
|
655
652
|
|
|
656
653
|
/**
|
|
657
654
|
* Adds an SSH public key for a user, enabling public-key authentication.
|
|
@@ -684,9 +681,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
684
681
|
*
|
|
685
682
|
* @param username Target user.
|
|
686
683
|
*/
|
|
687
|
-
public getAuthorizedKeys(
|
|
688
|
-
username: string,
|
|
689
|
-
): Array<{ algo: string; data: Buffer }> {
|
|
684
|
+
public getAuthorizedKeys(username: string): Array<{ algo: string; data: Buffer }> {
|
|
690
685
|
return this.authorizedKeys.get(username) ?? [];
|
|
691
686
|
}
|
|
692
687
|
}
|
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");
|
package/src/commands/chmod.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
3
3
|
|
|
4
4
|
export const chmodCommand: ShellModule = {
|
|
5
5
|
name: "chmod",
|
|
6
|
+
description: "Change file permissions",
|
|
7
|
+
category: "files",
|
|
6
8
|
params: ["<mode> <file>"],
|
|
7
9
|
run: ({ authUser, shell, cwd, args }) => {
|
|
8
10
|
const [modeArg, fileArg] = args;
|
package/src/commands/clear.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { ShellModule } from "../types/commands";
|
|
|
2
2
|
|
|
3
3
|
export const clearCommand: ShellModule = {
|
|
4
4
|
name: "clear",
|
|
5
|
+
description: "Clear the terminal screen",
|
|
6
|
+
category: "shell",
|
|
5
7
|
params: [],
|
|
6
|
-
|
|
8
|
+
// clearScreen flag triggers \x1b[2J\x1b[H in the shell layer
|
|
9
|
+
run: () => ({ clearScreen: true, stdout: "", exitCode: 0 }),
|
|
7
10
|
};
|
package/src/commands/cp.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const cpCommand: ShellModule = {
|
|
6
6
|
name: "cp",
|
|
7
|
+
description: "Copy files or directories",
|
|
8
|
+
category: "files",
|
|
7
9
|
params: ["[-r] <source> <dest>"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args }) => {
|
|
9
11
|
const recursive = ifFlag(args, ["-r", "-R", "--recursive"]);
|
package/src/commands/curl.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
|
|
10
10
|
export const curlCommand: ShellModule = {
|
|
11
11
|
name: "curl",
|
|
12
|
+
description: "HTTP client",
|
|
13
|
+
category: "network",
|
|
12
14
|
params: ["[-o file] <url>"],
|
|
13
15
|
run: async ({ authUser, cwd, args, shell }) => {
|
|
14
16
|
const { flagsWithValues, positionals } = parseArgs(args, {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { getFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const cutCommand: ShellModule = {
|
|
5
|
+
name: "cut",
|
|
6
|
+
description: "Remove sections from lines",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["-d <delim> -f <fields> [file]"],
|
|
9
|
+
run: ({ args, stdin }) => {
|
|
10
|
+
const delim = (getFlag(args, ["-d"]) as string | undefined) ?? "\t";
|
|
11
|
+
const fields = (getFlag(args, ["-f"]) as string | undefined) ?? "1";
|
|
12
|
+
const cols = fields.split(",").map((f) => {
|
|
13
|
+
const [a, b] = f.split("-").map(Number);
|
|
14
|
+
return b !== undefined ? { from: (a ?? 1) - 1, to: b - 1 } : { from: (a ?? 1) - 1, to: (a ?? 1) - 1 };
|
|
15
|
+
});
|
|
16
|
+
const lines = (stdin ?? "").split("\n");
|
|
17
|
+
const out = lines.map((line) => {
|
|
18
|
+
const parts = line.split(delim);
|
|
19
|
+
const selected: string[] = [];
|
|
20
|
+
for (const col of cols) {
|
|
21
|
+
for (let i = col.from; i <= Math.min(col.to, parts.length - 1); i++) {
|
|
22
|
+
selected.push(parts[i] ?? "");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return selected.join(delim);
|
|
26
|
+
});
|
|
27
|
+
return { stdout: out.join("\n"), exitCode: 0 };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const dateCommand: ShellModule = {
|
|
4
|
+
name: "date",
|
|
5
|
+
description: "Print current date and time",
|
|
6
|
+
category: "system",
|
|
7
|
+
params: ["[+format]"],
|
|
8
|
+
run: ({ args }) => {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const fmt = args[0];
|
|
11
|
+
if (fmt?.startsWith("+")) {
|
|
12
|
+
const f = fmt.slice(1)
|
|
13
|
+
.replace("%Y", String(now.getFullYear()))
|
|
14
|
+
.replace("%m", String(now.getMonth() + 1).padStart(2, "0"))
|
|
15
|
+
.replace("%d", String(now.getDate()).padStart(2, "0"))
|
|
16
|
+
.replace("%H", String(now.getHours()).padStart(2, "0"))
|
|
17
|
+
.replace("%M", String(now.getMinutes()).padStart(2, "0"))
|
|
18
|
+
.replace("%S", String(now.getSeconds()).padStart(2, "0"))
|
|
19
|
+
.replace("%s", String(Math.floor(now.getTime() / 1000)));
|
|
20
|
+
return { stdout: f, exitCode: 0 };
|
|
21
|
+
}
|
|
22
|
+
return { stdout: now.toString(), exitCode: 0 };
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/commands/deluser.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { ShellModule } from "../types/commands";
|
|
|
2
2
|
|
|
3
3
|
export const deluserCommand: ShellModule = {
|
|
4
4
|
name: "deluser",
|
|
5
|
+
description: "Delete a user",
|
|
6
|
+
category: "users",
|
|
5
7
|
params: ["<username>"],
|
|
6
8
|
run: async ({ authUser, args, shell }) => {
|
|
7
9
|
if (authUser !== "root") {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const dfCommand: ShellModule = {
|
|
4
|
+
name: "df",
|
|
5
|
+
description: "Report filesystem disk space usage",
|
|
6
|
+
category: "system",
|
|
7
|
+
params: ["[-h]"],
|
|
8
|
+
run: ({ shell }) => {
|
|
9
|
+
const bytes = shell.vfs.getUsageBytes();
|
|
10
|
+
const used = (bytes / 1024).toFixed(0);
|
|
11
|
+
const total = "1048576"; // 1GB virtual
|
|
12
|
+
const avail = String(Number(total) - Number(used));
|
|
13
|
+
const pct = Math.round((Number(used) / Number(total)) * 100);
|
|
14
|
+
const hdr = "Filesystem 1K-blocks Used Available Use% Mounted on";
|
|
15
|
+
const row = `virtual-fs ${total.padStart(9)} ${used.padStart(7)} ${avail.padStart(9)} ${pct}% /`;
|
|
16
|
+
return { stdout: `${hdr}\n${row}`, exitCode: 0 };
|
|
17
|
+
},
|
|
18
|
+
};
|