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
package/src/commands/sh.ts
CHANGED
|
@@ -1,57 +1,206 @@
|
|
|
1
|
-
import type { CommandContext, ShellModule } from "../types/commands";
|
|
2
|
-
import { getArg,
|
|
1
|
+
import type { CommandContext, CommandResult, ShellModule } from "../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
3
|
+
import { resolvePath } from "./helpers";
|
|
3
4
|
import { runCommand } from "./index";
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
/** Expand $VAR and ${VAR:-default} in a line using the current env */
|
|
7
|
+
function expandVars(line: string, env: Record<string, string>, lastExit: number): string {
|
|
8
|
+
return line
|
|
9
|
+
.replace(/\$\?/g, String(lastExit))
|
|
10
|
+
.replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, n, d) => env[n] ?? d)
|
|
11
|
+
.replace(/\$\{([^}]+)\}/g, (_, n) => env[n] ?? "")
|
|
12
|
+
.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, n) => env[n] ?? "")
|
|
13
|
+
.replace(/^~(\/|$)/, `${env.HOME ?? "/home/user"}$1`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Block =
|
|
17
|
+
| { type: "if"; cond: string; then: string[]; elif: Array<{ cond: string; body: string[] }>; else_: string[] }
|
|
18
|
+
| { type: "for"; var: string; list: string; body: string[] }
|
|
19
|
+
| { type: "while"; cond: string; body: string[] }
|
|
20
|
+
| { type: "cmd"; line: string };
|
|
21
|
+
|
|
22
|
+
/** Very small shell interpreter: supports if/elif/else/fi, for/do/done, while/do/done */
|
|
23
|
+
function parseBlocks(lines: string[]): Block[] {
|
|
24
|
+
const blocks: Block[] = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < lines.length) {
|
|
27
|
+
const line = lines[i]!.trim();
|
|
28
|
+
if (!line || line.startsWith("#")) { i++; continue; }
|
|
29
|
+
|
|
30
|
+
if (line.startsWith("if ") || line === "if") {
|
|
31
|
+
const cond = line.replace(/^if\s+/, "").replace(/;\s*then\s*$/, "").trim();
|
|
32
|
+
const thenLines: string[] = [];
|
|
33
|
+
const elifBlocks: Array<{ cond: string; body: string[] }> = [];
|
|
34
|
+
const elseLines: string[] = [];
|
|
35
|
+
let section: "then" | "elif" | "else" = "then";
|
|
36
|
+
let elifCond = "";
|
|
37
|
+
i++;
|
|
38
|
+
while (i < lines.length && lines[i]?.trim() !== "fi") {
|
|
39
|
+
const l = lines[i]!.trim();
|
|
40
|
+
if (l.startsWith("elif ")) { section = "elif"; elifCond = l.replace(/^elif\s+/, "").replace(/;\s*then\s*$/, "").trim(); elifBlocks.push({ cond: elifCond, body: [] }); }
|
|
41
|
+
else if (l === "else") { section = "else"; }
|
|
42
|
+
else if (l !== "then") {
|
|
43
|
+
if (section === "then") thenLines.push(l);
|
|
44
|
+
else if (section === "elif" && elifBlocks.length > 0) elifBlocks[elifBlocks.length - 1]!.body.push(l);
|
|
45
|
+
else elseLines.push(l);
|
|
46
|
+
}
|
|
47
|
+
i++;
|
|
17
48
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
// Simple variable substitution
|
|
31
|
-
let command = line;
|
|
32
|
-
for (let i = 0; i < scriptArgs.length; i++) {
|
|
33
|
-
const arg = scriptArgs[i] ?? "";
|
|
34
|
-
command = command.replaceAll(`$${i}`, arg);
|
|
49
|
+
blocks.push({ type: "if", cond, then: thenLines, elif: elifBlocks, else_: elseLines });
|
|
50
|
+
} else if (line.startsWith("for ")) {
|
|
51
|
+
const m = line.match(/^for\s+(\w+)\s+in\s+(.+?)(?:\s*;\s*do)?$/);
|
|
52
|
+
if (m) {
|
|
53
|
+
const body: string[] = [];
|
|
54
|
+
i++;
|
|
55
|
+
while (i < lines.length && lines[i]?.trim() !== "done") {
|
|
56
|
+
const l = lines[i]!.trim();
|
|
57
|
+
if (l !== "do") body.push(l);
|
|
58
|
+
i++;
|
|
35
59
|
}
|
|
36
|
-
|
|
60
|
+
blocks.push({ type: "for", var: m[1]!, list: m[2]!, body });
|
|
61
|
+
} else { blocks.push({ type: "cmd", line }); }
|
|
62
|
+
} else if (line.startsWith("while ")) {
|
|
63
|
+
const cond = line.replace(/^while\s+/, "").replace(/;\s*do\s*$/, "").trim();
|
|
64
|
+
const body: string[] = [];
|
|
65
|
+
i++;
|
|
66
|
+
while (i < lines.length && lines[i]?.trim() !== "done") {
|
|
67
|
+
const l = lines[i]!.trim();
|
|
68
|
+
if (l !== "do") body.push(l);
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
blocks.push({ type: "while", cond, body });
|
|
72
|
+
} else {
|
|
73
|
+
blocks.push({ type: "cmd", line });
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
return blocks;
|
|
78
|
+
}
|
|
37
79
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean> {
|
|
81
|
+
const expanded = expandVars(cond, ctx.env.vars, ctx.env.lastExitCode);
|
|
82
|
+
// test -f / test -d / [ ... ]
|
|
83
|
+
const testMatch = expanded.match(/^\[?\s*(.+?)\s*\]?$/);
|
|
84
|
+
if (testMatch) {
|
|
85
|
+
const expr = testMatch[1]!;
|
|
86
|
+
// -f file
|
|
87
|
+
const fTest = expr.match(/^-([fdeznr])\s+(.+)$/);
|
|
88
|
+
if (fTest) {
|
|
89
|
+
const [, flag, arg] = fTest;
|
|
90
|
+
const p = resolvePath(ctx.cwd, arg!);
|
|
91
|
+
if (flag === "f") return ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "file";
|
|
92
|
+
if (flag === "d") return ctx.shell.vfs.exists(p) && ctx.shell.vfs.stat(p).type === "directory";
|
|
93
|
+
if (flag === "e") return ctx.shell.vfs.exists(p);
|
|
94
|
+
if (flag === "z") return (arg ?? "").length === 0;
|
|
95
|
+
if (flag === "n") return (arg ?? "").length > 0;
|
|
96
|
+
}
|
|
97
|
+
// string comparison
|
|
98
|
+
const cmpMatch = expr.match(/^"?([^"]*)"?\s*(==|!=|=|<|>)\s*"?([^"]*)"?$/);
|
|
99
|
+
if (cmpMatch) {
|
|
100
|
+
const [, a, op, b] = cmpMatch;
|
|
101
|
+
if (op === "==" || op === "=") return a === b;
|
|
102
|
+
if (op === "!=") return a !== b;
|
|
103
|
+
}
|
|
104
|
+
// numeric
|
|
105
|
+
const numMatch = expr.match(/^(\S+)\s+(-eq|-ne|-lt|-le|-gt|-ge)\s+(\S+)$/);
|
|
106
|
+
if (numMatch) {
|
|
107
|
+
const [, a, op, b] = numMatch;
|
|
108
|
+
const na = Number(a), nb = Number(b);
|
|
109
|
+
if (op === "-eq") return na === nb;
|
|
110
|
+
if (op === "-ne") return na !== nb;
|
|
111
|
+
if (op === "-lt") return na < nb;
|
|
112
|
+
if (op === "-le") return na <= nb;
|
|
113
|
+
if (op === "-gt") return na > nb;
|
|
114
|
+
if (op === "-ge") return na >= nb;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// fallback: run command and check exit code
|
|
118
|
+
const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
|
|
119
|
+
return (r.exitCode ?? 0) === 0;
|
|
120
|
+
}
|
|
42
121
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
122
|
+
async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandResult> {
|
|
123
|
+
let lastResult: CommandResult = { exitCode: 0 };
|
|
124
|
+
let output = "";
|
|
46
125
|
|
|
47
|
-
|
|
48
|
-
|
|
126
|
+
for (const block of blocks) {
|
|
127
|
+
if (block.type === "cmd") {
|
|
128
|
+
const expanded = expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode);
|
|
129
|
+
const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
|
|
130
|
+
ctx.env.lastExitCode = r.exitCode ?? 0;
|
|
131
|
+
if (r.stdout) output += `${r.stdout}\n`;
|
|
132
|
+
if (r.stderr) return { ...r, stdout: output.trim() };
|
|
133
|
+
lastResult = r;
|
|
134
|
+
} else if (block.type === "if") {
|
|
135
|
+
let ran = false;
|
|
136
|
+
if (await evalCondition(block.cond, ctx)) {
|
|
137
|
+
const sub = await runBlocks(parseBlocks(block.then), ctx);
|
|
138
|
+
if (sub.stdout) output += `${sub.stdout}\n`;
|
|
139
|
+
ran = true;
|
|
140
|
+
} else {
|
|
141
|
+
for (const elif of block.elif) {
|
|
142
|
+
if (await evalCondition(elif.cond, ctx)) {
|
|
143
|
+
const sub = await runBlocks(parseBlocks(elif.body), ctx);
|
|
144
|
+
if (sub.stdout) output += `${sub.stdout}\n`;
|
|
145
|
+
ran = true; break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!ran && block.else_.length > 0) {
|
|
149
|
+
const sub = await runBlocks(parseBlocks(block.else_), ctx);
|
|
150
|
+
if (sub.stdout) output += `${sub.stdout}\n`;
|
|
49
151
|
}
|
|
50
152
|
}
|
|
153
|
+
} else if (block.type === "for") {
|
|
154
|
+
const listExpanded = expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode);
|
|
155
|
+
const items = listExpanded.trim().split(/\s+/);
|
|
156
|
+
for (const item of items) {
|
|
157
|
+
ctx.env.vars[block.var] = item;
|
|
158
|
+
const sub = await runBlocks(parseBlocks(block.body), ctx);
|
|
159
|
+
if (sub.stdout) output += `${sub.stdout}\n`;
|
|
160
|
+
if (sub.closeSession) return sub;
|
|
161
|
+
}
|
|
162
|
+
} else if (block.type === "while") {
|
|
163
|
+
let iterations = 0;
|
|
164
|
+
while (iterations < 1000 && await evalCondition(block.cond, ctx)) {
|
|
165
|
+
const sub = await runBlocks(parseBlocks(block.body), ctx);
|
|
166
|
+
if (sub.stdout) output += `${sub.stdout}\n`;
|
|
167
|
+
if (sub.closeSession) return sub;
|
|
168
|
+
iterations++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { ...lastResult, stdout: output.trim() || lastResult.stdout };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const shCommand: ShellModule = {
|
|
176
|
+
name: "sh",
|
|
177
|
+
aliases: ["bash"],
|
|
178
|
+
description: "Execute shell script or command",
|
|
179
|
+
category: "shell",
|
|
180
|
+
params: ["-c <script>", "[<file>]"],
|
|
181
|
+
run: async (ctx: CommandContext) => {
|
|
182
|
+
const { args, authUser, shell, cwd } = ctx;
|
|
183
|
+
|
|
184
|
+
// sh -c "inline script"
|
|
185
|
+
if (ifFlag(args, "-c")) {
|
|
186
|
+
const script = getArg(args, 1) ?? "";
|
|
187
|
+
if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
188
|
+
const lines = script.split(/[;\n]/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
189
|
+
const blocks = parseBlocks(lines);
|
|
190
|
+
return runBlocks(blocks, ctx);
|
|
191
|
+
}
|
|
51
192
|
|
|
52
|
-
|
|
193
|
+
// sh <file>
|
|
194
|
+
const fileArg = args[0];
|
|
195
|
+
if (fileArg) {
|
|
196
|
+
const p = resolvePath(cwd, fileArg);
|
|
197
|
+
if (!shell.vfs.exists(p)) return { stderr: `sh: ${fileArg}: No such file or directory`, exitCode: 1 };
|
|
198
|
+
const content = shell.vfs.readFile(p);
|
|
199
|
+
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
200
|
+
const blocks = parseBlocks(lines);
|
|
201
|
+
return runBlocks(blocks, ctx);
|
|
53
202
|
}
|
|
54
203
|
|
|
55
|
-
return { stderr: "sh: invalid usage", exitCode: 1 };
|
|
204
|
+
return { stderr: "sh: invalid usage. Use: sh -c 'cmd' or sh <file>", exitCode: 1 };
|
|
56
205
|
},
|
|
57
206
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const sleepCommand: ShellModule = {
|
|
4
|
+
name: "sleep",
|
|
5
|
+
description: "Delay execution",
|
|
6
|
+
category: "system",
|
|
7
|
+
params: ["<seconds>"],
|
|
8
|
+
run: async ({ args }) => {
|
|
9
|
+
const secs = parseFloat(args[0] ?? "1");
|
|
10
|
+
if (Number.isNaN(secs) || secs < 0) return { stderr: "sleep: invalid time", exitCode: 1 };
|
|
11
|
+
await new Promise((r) => setTimeout(r, secs * 1000));
|
|
12
|
+
return { exitCode: 0 };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const sortCommand: ShellModule = {
|
|
6
|
+
name: "sort",
|
|
7
|
+
description: "Sort lines of text",
|
|
8
|
+
category: "text",
|
|
9
|
+
params: ["[-r] [-n] [-u] [-k <col>] [file...]"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
11
|
+
const reverse = ifFlag(args, ["-r"]);
|
|
12
|
+
const numeric = ifFlag(args, ["-n"]);
|
|
13
|
+
const unique = ifFlag(args, ["-u"]);
|
|
14
|
+
const files = args.filter((a) => !a.startsWith("-"));
|
|
15
|
+
|
|
16
|
+
const getContent = (): string => {
|
|
17
|
+
if (files.length > 0) {
|
|
18
|
+
return files.map((f) => {
|
|
19
|
+
try {
|
|
20
|
+
assertPathAccess(authUser, resolvePath(cwd, f), "sort");
|
|
21
|
+
return shell.vfs.readFile(resolvePath(cwd, f));
|
|
22
|
+
} catch { return ""; }
|
|
23
|
+
}).join("\n");
|
|
24
|
+
}
|
|
25
|
+
return stdin ?? "";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const lines = getContent().split("\n").filter(Boolean);
|
|
29
|
+
const sorted = [...lines].sort((a, b) => {
|
|
30
|
+
if (numeric) return Number(a) - Number(b);
|
|
31
|
+
return a.localeCompare(b);
|
|
32
|
+
});
|
|
33
|
+
const result = reverse ? sorted.reverse() : sorted;
|
|
34
|
+
const out = unique ? [...new Set(result)] : result;
|
|
35
|
+
return { stdout: out.join("\n"), exitCode: 0 };
|
|
36
|
+
},
|
|
37
|
+
};
|
package/src/commands/su.ts
CHANGED
package/src/commands/sudo.ts
CHANGED
|
@@ -21,6 +21,8 @@ function parseSudoArgs(args: string[]): {
|
|
|
21
21
|
}
|
|
22
22
|
export const sudoCommand: ShellModule = {
|
|
23
23
|
name: "sudo",
|
|
24
|
+
description: "Execute as superuser",
|
|
25
|
+
category: "users",
|
|
24
26
|
params: ["<command...>"],
|
|
25
27
|
run: async ({ authUser, hostname, mode, cwd, shell, args }) => {
|
|
26
28
|
const { targetUser, loginShell, commandLine } = parseSudoArgs(args);
|
package/src/commands/tail.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const tailCommand: ShellModule = {
|
|
6
6
|
name: "tail",
|
|
7
|
+
description: "Output last lines",
|
|
8
|
+
category: "text",
|
|
7
9
|
params: ["[-n <lines>] [file...]"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
9
11
|
const nArg = getFlag(args, ["-n"]);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const tarCommand: ShellModule = {
|
|
6
|
+
name: "tar",
|
|
7
|
+
description: "Archive utility",
|
|
8
|
+
category: "archive",
|
|
9
|
+
params: ["[-czf|-xzf|-tf] <archive> [files...]"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
11
|
+
const create = ifFlag(args, ["-c"]);
|
|
12
|
+
const extract = ifFlag(args, ["-x"]);
|
|
13
|
+
const list = ifFlag(args, ["-t"]);
|
|
14
|
+
const fFlag = args.findIndex((a) => a.includes("f"));
|
|
15
|
+
const archiveName = fFlag !== -1 ? args[fFlag + 1] : args.find((a) => a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz"));
|
|
16
|
+
|
|
17
|
+
if (!archiveName) return { stderr: "tar: no archive specified", exitCode: 1 };
|
|
18
|
+
const archivePath = resolvePath(cwd, archiveName);
|
|
19
|
+
|
|
20
|
+
if (create) {
|
|
21
|
+
const fileArgs = args.filter((a) => !a.startsWith("-") && a !== archiveName);
|
|
22
|
+
const entries: Record<string, string> = {};
|
|
23
|
+
for (const f of fileArgs) {
|
|
24
|
+
const p = resolvePath(cwd, f);
|
|
25
|
+
try {
|
|
26
|
+
const stat = shell.vfs.stat(p);
|
|
27
|
+
if (stat.type === "file") entries[f] = shell.vfs.readFile(p);
|
|
28
|
+
else {
|
|
29
|
+
const walk = (dir: string, prefix: string) => {
|
|
30
|
+
for (const e of shell.vfs.list(dir)) {
|
|
31
|
+
const full = `${dir}/${e}`, rel = `${prefix}/${e}`;
|
|
32
|
+
const s = shell.vfs.stat(full);
|
|
33
|
+
if (s.type === "file") entries[rel] = shell.vfs.readFile(full);
|
|
34
|
+
else walk(full, rel);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
walk(p, f);
|
|
38
|
+
}
|
|
39
|
+
} catch { return { stderr: `tar: ${f}: No such file or directory`, exitCode: 1 }; }
|
|
40
|
+
}
|
|
41
|
+
shell.writeFileAsUser(authUser, archivePath, JSON.stringify(entries));
|
|
42
|
+
return { exitCode: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (list || extract) {
|
|
46
|
+
let entries: Record<string, string>;
|
|
47
|
+
try { entries = JSON.parse(shell.vfs.readFile(archivePath)); }
|
|
48
|
+
catch { return { stderr: `tar: ${archiveName}: cannot open archive`, exitCode: 1 }; }
|
|
49
|
+
if (list) return { stdout: Object.keys(entries).join("\n"), exitCode: 0 };
|
|
50
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
51
|
+
shell.writeFileAsUser(authUser, resolvePath(cwd, name), content);
|
|
52
|
+
}
|
|
53
|
+
return { exitCode: 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { stderr: "tar: must specify -c, -x, or -t", exitCode: 1 };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const teeCommand: ShellModule = {
|
|
6
|
+
name: "tee",
|
|
7
|
+
description: "Read stdin, write to stdout and files",
|
|
8
|
+
category: "text",
|
|
9
|
+
params: ["[-a] <file...>"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
11
|
+
const append = ifFlag(args, ["-a"]);
|
|
12
|
+
const files = args.filter((a) => !a.startsWith("-"));
|
|
13
|
+
const input = stdin ?? "";
|
|
14
|
+
for (const f of files) {
|
|
15
|
+
const p = resolvePath(cwd, f);
|
|
16
|
+
if (append) {
|
|
17
|
+
const existing = (() => { try { return shell.vfs.readFile(p); } catch { return ""; } })();
|
|
18
|
+
shell.writeFileAsUser(authUser, p, existing + input);
|
|
19
|
+
} else {
|
|
20
|
+
shell.writeFileAsUser(authUser, p, input);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { stdout: input, exitCode: 0 };
|
|
24
|
+
},
|
|
25
|
+
};
|
package/src/commands/touch.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
3
3
|
|
|
4
4
|
export const touchCommand: ShellModule = {
|
|
5
5
|
name: "touch",
|
|
6
|
+
description: "Create or update files",
|
|
7
|
+
category: "files",
|
|
6
8
|
params: ["<file>"],
|
|
7
9
|
run: ({ authUser, shell, cwd, args }) => {
|
|
8
10
|
if (args.length === 0) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const trCommand: ShellModule = {
|
|
5
|
+
name: "tr",
|
|
6
|
+
description: "Translate or delete characters",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["[-d] <set1> [set2]"],
|
|
9
|
+
run: ({ args, stdin }) => {
|
|
10
|
+
const del = ifFlag(args, ["-d"]);
|
|
11
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
12
|
+
const set1 = positionals[0] ?? "";
|
|
13
|
+
const set2 = positionals[1] ?? "";
|
|
14
|
+
let input = stdin ?? "";
|
|
15
|
+
if (del) {
|
|
16
|
+
for (const c of set1) input = input.split(c).join("");
|
|
17
|
+
} else if (set2) {
|
|
18
|
+
for (let i = 0; i < set1.length; i++) {
|
|
19
|
+
input = input.split(set1[i]!).join(set2[i] ?? set2[set2.length - 1] ?? "");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { stdout: input, exitCode: 0 };
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/commands/tree.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const treeCommand: ShellModule = {
|
|
6
6
|
name: "tree",
|
|
7
|
+
description: "Display directory tree",
|
|
8
|
+
category: "navigation",
|
|
7
9
|
params: ["[path]"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args }) => {
|
|
9
11
|
const target = resolvePath(cwd, getArg(args, 0) ?? cwd);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const unameCommand: ShellModule = {
|
|
5
|
+
name: "uname",
|
|
6
|
+
description: "Print system information",
|
|
7
|
+
category: "system",
|
|
8
|
+
params: ["[-a] [-s] [-r] [-m]"],
|
|
9
|
+
run: ({ shell, args }) => {
|
|
10
|
+
const all = ifFlag(args, ["-a"]);
|
|
11
|
+
const sysname = "Linux";
|
|
12
|
+
const release = shell.properties?.kernel ?? "5.15.0";
|
|
13
|
+
const machine = shell.properties?.arch ?? "x86_64";
|
|
14
|
+
const hostname = shell.hostname;
|
|
15
|
+
if (all) return { stdout: `${sysname} ${hostname} ${release} #1 SMP ${machine} GNU/Linux`, exitCode: 0 };
|
|
16
|
+
if (ifFlag(args, ["-r"])) return { stdout: release, exitCode: 0 };
|
|
17
|
+
if (ifFlag(args, ["-m"])) return { stdout: machine, exitCode: 0 };
|
|
18
|
+
return { stdout: sysname, exitCode: 0 };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const uniqCommand: ShellModule = {
|
|
5
|
+
name: "uniq",
|
|
6
|
+
description: "Report or filter out repeated lines",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["[-c] [-d] [-u] [file]"],
|
|
9
|
+
run: ({ args, stdin }) => {
|
|
10
|
+
const count = ifFlag(args, ["-c"]);
|
|
11
|
+
const dupOnly = ifFlag(args, ["-d"]);
|
|
12
|
+
const uniqOnly = ifFlag(args, ["-u"]);
|
|
13
|
+
const lines = (stdin ?? "").split("\n");
|
|
14
|
+
const out: string[] = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < lines.length) {
|
|
17
|
+
let j = i;
|
|
18
|
+
while (j < lines.length && lines[j] === lines[i]) j++;
|
|
19
|
+
const n = j - i;
|
|
20
|
+
const line = lines[i]!;
|
|
21
|
+
if (dupOnly && n === 1) { i = j; continue; }
|
|
22
|
+
if (uniqOnly && n > 1) { i = j; continue; }
|
|
23
|
+
out.push(count ? `${String(n).padStart(4)} ${line}` : line);
|
|
24
|
+
i = j;
|
|
25
|
+
}
|
|
26
|
+
return { stdout: out.join("\n"), exitCode: 0 };
|
|
27
|
+
},
|
|
28
|
+
};
|
package/src/commands/unset.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
-
import { setEnvVar } from "./set";
|
|
3
2
|
|
|
4
3
|
export const unsetCommand: ShellModule = {
|
|
5
4
|
name: "unset",
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Unset (remove) all specified variables
|
|
13
|
-
for (const varName of args) {
|
|
14
|
-
setEnvVar(varName, "");
|
|
15
|
-
}
|
|
16
|
-
|
|
5
|
+
description: "Remove shell variable",
|
|
6
|
+
category: "shell",
|
|
7
|
+
params: ["<VAR>"],
|
|
8
|
+
run: ({ args, env }) => {
|
|
9
|
+
for (const name of args) delete env.vars[name];
|
|
17
10
|
return { exitCode: 0 };
|
|
18
11
|
},
|
|
19
12
|
};
|
package/src/commands/wc.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const wcCommand: ShellModule = {
|
|
6
6
|
name: "wc",
|
|
7
|
+
description: "Count words/lines/bytes",
|
|
8
|
+
category: "text",
|
|
7
9
|
params: ["[-l] [-w] [-c] [file...]"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
9
11
|
const lines = ifFlag(args, ["-l"]);
|
package/src/commands/wget.ts
CHANGED
|
@@ -82,6 +82,8 @@ function runHostWget(args: string[]): Promise<{
|
|
|
82
82
|
|
|
83
83
|
export const wgetCommand: ShellModule = {
|
|
84
84
|
name: "wget",
|
|
85
|
+
description: "File downloader",
|
|
86
|
+
category: "network",
|
|
85
87
|
params: ["[url]"],
|
|
86
88
|
run: async ({ authUser, cwd, args, shell }) => {
|
|
87
89
|
const { flagsWithValues, positionals } = parseArgs(args, {
|
package/src/commands/who.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { ShellModule } from "../types/commands";
|
|
|
3
3
|
|
|
4
4
|
export const whoCommand: ShellModule = {
|
|
5
5
|
name: "who",
|
|
6
|
+
description: "Show active sessions",
|
|
7
|
+
category: "system",
|
|
6
8
|
params: [],
|
|
7
9
|
run: ({ shell }) => {
|
|
8
10
|
const lines = shell.users.listActiveSessions().map((session) => {
|
package/src/commands/whoami.ts
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { runCommand } from "./index";
|
|
3
|
+
|
|
4
|
+
export const xargsCommand: ShellModule = {
|
|
5
|
+
name: "xargs",
|
|
6
|
+
description: "Build and execute command lines from stdin",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["[command] [args...]"],
|
|
9
|
+
run: async ({ authUser, hostname, mode, cwd, args, stdin, shell, env }) => {
|
|
10
|
+
const baseCmd = args[0] ?? "echo";
|
|
11
|
+
const extraArgs = args.slice(1);
|
|
12
|
+
const items = (stdin ?? "").trim().split(/\s+/).filter(Boolean);
|
|
13
|
+
if (items.length === 0) return { exitCode: 0 };
|
|
14
|
+
const fullCmd = [baseCmd, ...extraArgs, ...items].join(" ");
|
|
15
|
+
return runCommand(fullCmd, authUser, hostname, mode, cwd, shell, undefined, env);
|
|
16
|
+
},
|
|
17
|
+
};
|
package/src/types/commands.ts
CHANGED
|
@@ -57,6 +57,14 @@ export interface NanoEditorSession {
|
|
|
57
57
|
initialContent: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Per-session shell environment (variables, last exit code). */
|
|
61
|
+
export interface ShellEnv {
|
|
62
|
+
/** Environment variables visible to commands. */
|
|
63
|
+
vars: Record<string, string>;
|
|
64
|
+
/** Exit status of the last executed command. */
|
|
65
|
+
lastExitCode: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
/** Runtime context object passed to each command module. */
|
|
61
69
|
export interface CommandContext {
|
|
62
70
|
/** Authenticated user currently bound to stream. */
|
|
@@ -77,6 +85,8 @@ export interface CommandContext {
|
|
|
77
85
|
stdin?: string;
|
|
78
86
|
/** Current working directory for command execution. */
|
|
79
87
|
cwd: string;
|
|
88
|
+
/** Per-session environment available to command modules. */
|
|
89
|
+
env: ShellEnv;
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/** Contract implemented by each shell command module. */
|
|
@@ -89,6 +99,10 @@ export interface ShellModule {
|
|
|
89
99
|
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
|
|
90
100
|
/** Optional alternative command names. */
|
|
91
101
|
aliases?: string[];
|
|
102
|
+
/** Short description shown in `help`. */
|
|
103
|
+
description?: string;
|
|
104
|
+
/** Category used for grouped help output. */
|
|
105
|
+
category?: string;
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
/** Command return union allowing sync or async handlers. */
|