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
package/src/commands/help.ts
CHANGED
|
@@ -1,9 +1,87 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { getCommandModulesPublic } from "./index";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const CATEGORY_ORDER = ["navigation", "files", "text", "archive", "system", "network", "shell", "users", "misc"];
|
|
5
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
6
|
+
navigation: "Navigation",
|
|
7
|
+
files: "Files & Filesystem",
|
|
8
|
+
text: "Text Processing",
|
|
9
|
+
archive: "Archive & Compression",
|
|
10
|
+
system: "System",
|
|
11
|
+
network: "Network",
|
|
12
|
+
shell: "Shell",
|
|
13
|
+
users: "Users & Permissions",
|
|
14
|
+
misc: "Miscellaneous",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function padRight(s: string, n: number): string {
|
|
18
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createHelpCommand(_getNames: () => string[]): ShellModule {
|
|
4
22
|
return {
|
|
5
23
|
name: "help",
|
|
6
|
-
|
|
7
|
-
|
|
24
|
+
description: "Display this help message",
|
|
25
|
+
category: "shell",
|
|
26
|
+
params: ["[command]"],
|
|
27
|
+
run: ({ args }) => {
|
|
28
|
+
const modules = getCommandModulesPublic();
|
|
29
|
+
|
|
30
|
+
// help <command>
|
|
31
|
+
if (args[0]) {
|
|
32
|
+
const mod = modules.find((m) => m.name === args[0] || m.aliases?.includes(args[0]!));
|
|
33
|
+
if (!mod) return { stderr: `help: no help for '${args[0]}'`, exitCode: 1 };
|
|
34
|
+
const aliases = mod.aliases?.length ? ` aliases: ${mod.aliases.join(", ")}\n` : "";
|
|
35
|
+
const params = mod.params.map((p) => ` ${mod.name} ${p}`).join("\n");
|
|
36
|
+
return {
|
|
37
|
+
stdout: [
|
|
38
|
+
`\x1b[1m${mod.name}\x1b[0m — ${mod.description ?? "no description"}`,
|
|
39
|
+
aliases,
|
|
40
|
+
"Usage:",
|
|
41
|
+
params || ` ${mod.name}`,
|
|
42
|
+
].filter(Boolean).join("\n"),
|
|
43
|
+
exitCode: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Full help — grouped by category
|
|
48
|
+
const grouped: Record<string, ShellModule[]> = {};
|
|
49
|
+
for (const mod of modules) {
|
|
50
|
+
const cat = mod.category ?? "misc";
|
|
51
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
52
|
+
grouped[cat]!.push(mod);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
lines.push("\x1b[1mAvailable commands\x1b[0m");
|
|
57
|
+
lines.push("");
|
|
58
|
+
|
|
59
|
+
const cats = [
|
|
60
|
+
...CATEGORY_ORDER.filter((c) => grouped[c]),
|
|
61
|
+
...Object.keys(grouped).filter((c) => !CATEGORY_ORDER.includes(c)),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (const cat of cats) {
|
|
65
|
+
const mods = grouped[cat];
|
|
66
|
+
if (!mods || mods.length === 0) continue;
|
|
67
|
+
lines.push(`\x1b[33m${CATEGORY_LABELS[cat] ?? cat}\x1b[0m`);
|
|
68
|
+
|
|
69
|
+
// Two-column layout
|
|
70
|
+
const sorted = [...mods].sort((a, b) => a.name.localeCompare(b.name));
|
|
71
|
+
for (let i = 0; i < sorted.length; i += 2) {
|
|
72
|
+
const left = sorted[i]!;
|
|
73
|
+
const right = sorted[i + 1];
|
|
74
|
+
const leftStr = ` \x1b[36m${padRight(left.name, 14)}\x1b[0m ${left.description ?? ""}`;
|
|
75
|
+
const rightStr = right
|
|
76
|
+
? ` \x1b[36m${padRight(right.name, 14)}\x1b[0m ${right.description ?? ""}`
|
|
77
|
+
: "";
|
|
78
|
+
lines.push(rightStr ? `${leftStr.padEnd(44)}${rightStr}` : leftStr);
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
lines.push("Type \x1b[1mhelp <command>\x1b[0m for usage details.");
|
|
84
|
+
return { stdout: lines.join("\n"), exitCode: 0 };
|
|
85
|
+
},
|
|
8
86
|
};
|
|
9
87
|
}
|
package/src/commands/hostname.ts
CHANGED
package/src/commands/htop.ts
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const idCommand: ShellModule = {
|
|
4
|
+
name: "id",
|
|
5
|
+
description: "Print user identity",
|
|
6
|
+
category: "system",
|
|
7
|
+
params: ["[user]"],
|
|
8
|
+
run: ({ authUser, shell, args }) => {
|
|
9
|
+
const target = args[0] ?? authUser;
|
|
10
|
+
const uid = target === "root" ? 0 : 1000;
|
|
11
|
+
const gid = uid;
|
|
12
|
+
const isSudo = shell.users.isSudoer(target);
|
|
13
|
+
const groups = isSudo ? `${gid}(${target}),0(root)` : `${gid}(${target})`;
|
|
14
|
+
return { stdout: `uid=${uid}(${target}) gid=${gid}(${target}) groups=${groups}`, exitCode: 0 };
|
|
15
|
+
},
|
|
16
|
+
};
|
package/src/commands/index.ts
CHANGED
|
@@ -1,102 +1,118 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
|
|
1
2
|
import type { VirtualShell } from "../VirtualShell";
|
|
2
3
|
import type {
|
|
3
4
|
CommandContext,
|
|
4
5
|
CommandMode,
|
|
5
6
|
CommandResult,
|
|
7
|
+
ShellEnv,
|
|
6
8
|
ShellModule,
|
|
7
9
|
} from "../types/commands";
|
|
8
10
|
import { adduserCommand } from "./adduser";
|
|
11
|
+
import { awkCommand } from "./awk";
|
|
12
|
+
import { base64Command } from "./base64";
|
|
9
13
|
import { catCommand } from "./cat";
|
|
10
14
|
import { cdCommand } from "./cd";
|
|
15
|
+
import { chmodCommand } from "./chmod";
|
|
11
16
|
import { clearCommand } from "./clear";
|
|
17
|
+
import { cpCommand } from "./cp";
|
|
12
18
|
import { curlCommand } from "./curl";
|
|
19
|
+
import { cutCommand } from "./cut";
|
|
20
|
+
import { dateCommand } from "./date";
|
|
13
21
|
import { deluserCommand } from "./deluser";
|
|
22
|
+
import { dfCommand } from "./df";
|
|
23
|
+
import { diffCommand } from "./diff";
|
|
24
|
+
import { duCommand } from "./du";
|
|
14
25
|
import { echoCommand } from "./echo";
|
|
15
26
|
import { envCommand } from "./env";
|
|
16
27
|
import { exitCommand } from "./exit";
|
|
17
28
|
import { exportCommand } from "./export";
|
|
29
|
+
import { findCommand } from "./find";
|
|
18
30
|
import { grepCommand } from "./grep";
|
|
31
|
+
import { groupsCommand } from "./groups";
|
|
32
|
+
import { gunzipCommand, gzipCommand } from "./gzip";
|
|
33
|
+
import { headCommand } from "./head";
|
|
19
34
|
import { createHelpCommand } from "./help";
|
|
20
35
|
import { hostnameCommand } from "./hostname";
|
|
21
36
|
import { htopCommand } from "./htop";
|
|
37
|
+
import { idCommand } from "./id";
|
|
38
|
+
import { killCommand } from "./kill";
|
|
39
|
+
import { lnCommand } from "./ln";
|
|
22
40
|
import { lsCommand } from "./ls";
|
|
23
41
|
import { mkdirCommand } from "./mkdir";
|
|
42
|
+
import { mvCommand } from "./mv";
|
|
24
43
|
import { nanoCommand } from "./nano";
|
|
25
44
|
import { neofetchCommand } from "./neofetch";
|
|
26
45
|
import { passwdCommand } from "./passwd";
|
|
46
|
+
import { pingCommand } from "./ping";
|
|
47
|
+
import { psCommand } from "./ps";
|
|
27
48
|
import { pwdCommand } from "./pwd";
|
|
28
49
|
import { rmCommand } from "./rm";
|
|
50
|
+
import { sedCommand } from "./sed";
|
|
29
51
|
import { setCommand } from "./set";
|
|
30
52
|
import { shCommand } from "./sh";
|
|
53
|
+
import { sleepCommand } from "./sleep";
|
|
54
|
+
import { sortCommand } from "./sort";
|
|
31
55
|
import { suCommand } from "./su";
|
|
32
56
|
import { sudoCommand } from "./sudo";
|
|
57
|
+
import { tailCommand } from "./tail";
|
|
58
|
+
import { tarCommand } from "./tar";
|
|
59
|
+
import { teeCommand } from "./tee";
|
|
33
60
|
import { touchCommand } from "./touch";
|
|
61
|
+
import { trCommand } from "./tr";
|
|
34
62
|
import { treeCommand } from "./tree";
|
|
63
|
+
import { unameCommand } from "./uname";
|
|
64
|
+
import { uniqCommand } from "./uniq";
|
|
35
65
|
import { unsetCommand } from "./unset";
|
|
66
|
+
import { wcCommand } from "./wc";
|
|
36
67
|
import { wgetCommand } from "./wget";
|
|
37
68
|
import { whoCommand } from "./who";
|
|
38
69
|
import { whoamiCommand } from "./whoami";
|
|
70
|
+
import { xargsCommand } from "./xargs";
|
|
39
71
|
|
|
40
72
|
const BASE_COMMANDS: ShellModule[] = [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
// Navigation
|
|
74
|
+
pwdCommand, cdCommand, lsCommand, treeCommand,
|
|
75
|
+
// Files
|
|
76
|
+
catCommand, touchCommand, rmCommand, mkdirCommand, cpCommand, mvCommand, lnCommand,
|
|
77
|
+
chmodCommand, findCommand,
|
|
78
|
+
// Text processing
|
|
79
|
+
grepCommand, sedCommand, awkCommand, sortCommand, uniqCommand, wcCommand,
|
|
80
|
+
headCommand, tailCommand, cutCommand, trCommand, teeCommand, xargsCommand,
|
|
81
|
+
diffCommand,
|
|
82
|
+
// Archives
|
|
83
|
+
tarCommand, gzipCommand, gunzipCommand, base64Command,
|
|
84
|
+
// System info
|
|
85
|
+
whoamiCommand, whoCommand, hostnameCommand, idCommand, groupsCommand, unameCommand,
|
|
86
|
+
psCommand, killCommand, dfCommand, duCommand, dateCommand, sleepCommand, pingCommand,
|
|
87
|
+
// Shell
|
|
88
|
+
echoCommand, envCommand, exportCommand, setCommand, unsetCommand, shCommand,
|
|
89
|
+
clearCommand, exitCommand,
|
|
90
|
+
// Editors
|
|
91
|
+
nanoCommand, htopCommand,
|
|
92
|
+
// Network
|
|
93
|
+
curlCommand, wgetCommand,
|
|
94
|
+
// Users
|
|
95
|
+
adduserCommand, passwdCommand, deluserCommand, sudoCommand, suCommand,
|
|
96
|
+
// Misc
|
|
54
97
|
neofetchCommand,
|
|
55
|
-
htopCommand,
|
|
56
|
-
adduserCommand,
|
|
57
|
-
passwdCommand,
|
|
58
|
-
deluserCommand,
|
|
59
|
-
sudoCommand,
|
|
60
|
-
suCommand,
|
|
61
|
-
curlCommand,
|
|
62
|
-
envCommand,
|
|
63
|
-
wgetCommand,
|
|
64
|
-
grepCommand,
|
|
65
|
-
exportCommand,
|
|
66
|
-
setCommand,
|
|
67
|
-
unsetCommand,
|
|
68
|
-
shCommand,
|
|
69
|
-
clearCommand,
|
|
70
|
-
exitCommand,
|
|
71
98
|
];
|
|
72
99
|
|
|
73
100
|
const customCommands: ShellModule[] = [];
|
|
74
|
-
|
|
75
|
-
const helpCommand = createHelpCommand(() =>
|
|
76
|
-
getCommandModules().map((cmd) => cmd.name),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
101
|
const commandRegistry = new Map<string, ShellModule>();
|
|
80
102
|
let cachedCommandNames: string[] | null = null;
|
|
81
103
|
|
|
104
|
+
const helpCommand = createHelpCommand(() => getCommandModules().map((cmd) => cmd.name));
|
|
105
|
+
|
|
82
106
|
function buildCache(): void {
|
|
107
|
+
commandRegistry.clear();
|
|
83
108
|
for (const mod of getCommandModules()) {
|
|
84
109
|
commandRegistry.set(mod.name, mod);
|
|
85
|
-
for (const alias of mod.aliases ?? [])
|
|
86
|
-
commandRegistry.set(alias, mod);
|
|
87
|
-
}
|
|
110
|
+
for (const alias of mod.aliases ?? []) commandRegistry.set(alias, mod);
|
|
88
111
|
}
|
|
89
112
|
cachedCommandNames = Array.from(commandRegistry.keys()).sort();
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
function getCommandModules(): ShellModule[] {
|
|
93
|
-
// console.log("Loading command modules...");
|
|
94
|
-
// console.log(
|
|
95
|
-
// `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
|
|
96
|
-
// );
|
|
97
|
-
// console.log(
|
|
98
|
-
// `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
|
|
99
|
-
// );
|
|
100
116
|
return [...BASE_COMMANDS, ...customCommands, helpCommand];
|
|
101
117
|
}
|
|
102
118
|
|
|
@@ -104,23 +120,13 @@ export function registerCommand(module: ShellModule): void {
|
|
|
104
120
|
const normalized: ShellModule = {
|
|
105
121
|
...module,
|
|
106
122
|
name: module.name.trim().toLowerCase(),
|
|
107
|
-
aliases: module.aliases?.map((
|
|
123
|
+
aliases: module.aliases?.map((a) => a.trim().toLowerCase()),
|
|
108
124
|
};
|
|
109
|
-
|
|
110
125
|
const names = [normalized.name, ...(normalized.aliases ?? [])];
|
|
111
|
-
if (names.some((
|
|
112
|
-
throw new Error(
|
|
113
|
-
"Command names and aliases must be non-empty and contain no spaces",
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
for (const name of names) {
|
|
118
|
-
if (commandRegistry.has(name)) {
|
|
119
|
-
throw new Error(`Command '${name}' already exists`);
|
|
120
|
-
}
|
|
121
|
-
commandRegistry.set(name, normalized);
|
|
126
|
+
if (names.some((n) => n.length === 0 || /\s/.test(n))) {
|
|
127
|
+
throw new Error("Command names must be non-empty and contain no spaces");
|
|
122
128
|
}
|
|
123
|
-
|
|
129
|
+
customCommands.push(normalized);
|
|
124
130
|
buildCache();
|
|
125
131
|
}
|
|
126
132
|
|
|
@@ -129,24 +135,20 @@ export function createCustomCommand(
|
|
|
129
135
|
params: string[],
|
|
130
136
|
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
|
|
131
137
|
): ShellModule {
|
|
132
|
-
return {
|
|
133
|
-
name,
|
|
134
|
-
params,
|
|
135
|
-
run,
|
|
136
|
-
};
|
|
138
|
+
return { name, params, run };
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export function getCommandNames(): string[] {
|
|
140
|
-
if (!cachedCommandNames)
|
|
141
|
-
buildCache();
|
|
142
|
-
}
|
|
142
|
+
if (!cachedCommandNames) buildCache();
|
|
143
143
|
return cachedCommandNames!;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
export function getCommandModulesPublic(): ShellModule[] {
|
|
147
|
+
return getCommandModules();
|
|
148
|
+
}
|
|
149
|
+
|
|
146
150
|
export function resolveModule(name: string): ShellModule | undefined {
|
|
147
|
-
if (!cachedCommandNames)
|
|
148
|
-
buildCache();
|
|
149
|
-
}
|
|
151
|
+
if (!cachedCommandNames) buildCache();
|
|
150
152
|
return commandRegistry.get(name.toLowerCase());
|
|
151
153
|
}
|
|
152
154
|
|
|
@@ -156,52 +158,44 @@ function splitArgsRespectingQuotes(input: string): string[] {
|
|
|
156
158
|
let inQuotes = false;
|
|
157
159
|
let quoteChar = "";
|
|
158
160
|
|
|
159
|
-
for (let i = 0; i < input.length; i
|
|
161
|
+
for (let i = 0; i < input.length; i++) {
|
|
160
162
|
const ch = input[i] || "";
|
|
161
163
|
const prev = i > 0 ? input[i - 1] : "";
|
|
162
|
-
|
|
163
164
|
if ((ch === '"' || ch === "'") && prev !== "\\") {
|
|
164
|
-
if (!inQuotes) {
|
|
165
|
-
|
|
166
|
-
quoteChar = ch;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (ch === quoteChar) {
|
|
171
|
-
inQuotes = false;
|
|
172
|
-
quoteChar = "";
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
165
|
+
if (!inQuotes) { inQuotes = true; quoteChar = ch; continue; }
|
|
166
|
+
if (ch === quoteChar) { inQuotes = false; quoteChar = ""; continue; }
|
|
175
167
|
}
|
|
176
|
-
|
|
177
168
|
if (/\s/.test(ch) && !inQuotes) {
|
|
178
|
-
if (current.length > 0) {
|
|
179
|
-
tokens.push(current);
|
|
180
|
-
current = "";
|
|
181
|
-
}
|
|
169
|
+
if (current.length > 0) { tokens.push(current); current = ""; }
|
|
182
170
|
continue;
|
|
183
171
|
}
|
|
184
|
-
|
|
185
172
|
current += ch;
|
|
186
173
|
}
|
|
187
|
-
|
|
188
|
-
if (current.length > 0) {
|
|
189
|
-
tokens.push(current);
|
|
190
|
-
}
|
|
191
|
-
|
|
174
|
+
if (current.length > 0) tokens.push(current);
|
|
192
175
|
return tokens;
|
|
193
176
|
}
|
|
194
177
|
|
|
195
178
|
function parseInput(rawInput: string): { commandName: string; args: string[] } {
|
|
196
179
|
const parts = splitArgsRespectingQuotes(rawInput.trim());
|
|
180
|
+
return { commandName: parts[0]?.toLowerCase() ?? "", args: parts.slice(1) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
|
|
197
184
|
return {
|
|
198
|
-
|
|
199
|
-
|
|
185
|
+
vars: {
|
|
186
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
187
|
+
HOME: `/home/${authUser}`,
|
|
188
|
+
USER: authUser,
|
|
189
|
+
LOGNAME: authUser,
|
|
190
|
+
SHELL: "/bin/sh",
|
|
191
|
+
TERM: "xterm-256color",
|
|
192
|
+
HOSTNAME: hostname,
|
|
193
|
+
PS1: "\\u@\\h:\\w\\$ ",
|
|
194
|
+
},
|
|
195
|
+
lastExitCode: 0,
|
|
200
196
|
};
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
// Internal async function for pipeline execution
|
|
204
|
-
|
|
205
199
|
export async function runCommand(
|
|
206
200
|
rawInput: string,
|
|
207
201
|
authUser: string,
|
|
@@ -210,50 +204,37 @@ export async function runCommand(
|
|
|
210
204
|
cwd: string,
|
|
211
205
|
shell: VirtualShell,
|
|
212
206
|
stdin?: string,
|
|
207
|
+
env?: ShellEnv,
|
|
213
208
|
): Promise<CommandResult> {
|
|
214
209
|
const trimmed = rawInput.trim();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
210
|
+
if (trimmed.length === 0) return { exitCode: 0 };
|
|
211
|
+
|
|
212
|
+
const shellEnv: ShellEnv = env ?? makeDefaultEnv(authUser, hostname);
|
|
213
|
+
|
|
214
|
+
// Detect shell operators
|
|
215
|
+
if (
|
|
216
|
+
/(?<![|&])[|](?![|])/.test(trimmed) ||
|
|
217
|
+
trimmed.includes(">") ||
|
|
218
|
+
trimmed.includes("<") ||
|
|
219
|
+
trimmed.includes("&&") ||
|
|
220
|
+
trimmed.includes("||") ||
|
|
221
|
+
trimmed.includes(";")
|
|
222
|
+
) {
|
|
223
|
+
const { parseScript } = await import("../VirtualShell/shellParser");
|
|
224
|
+
const { executeStatements } = await import("../SSHMimic/executor");
|
|
225
|
+
const script = parseScript(trimmed);
|
|
226
|
+
if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
|
|
232
227
|
try {
|
|
233
|
-
return await
|
|
234
|
-
pipeline,
|
|
235
|
-
authUser,
|
|
236
|
-
hostname,
|
|
237
|
-
mode,
|
|
238
|
-
cwd,
|
|
239
|
-
shell,
|
|
240
|
-
);
|
|
228
|
+
return await executeStatements(script.statements, authUser, hostname, mode, cwd, shell, shellEnv);
|
|
241
229
|
} catch (error: unknown) {
|
|
242
|
-
|
|
243
|
-
error instanceof Error ? error.message : "Pipeline execution failed";
|
|
244
|
-
return { stderr: message, exitCode: 1 };
|
|
230
|
+
return { stderr: error instanceof Error ? error.message : "Execution failed", exitCode: 1 };
|
|
245
231
|
}
|
|
246
232
|
}
|
|
247
233
|
|
|
248
234
|
const { commandName, args } = parseInput(trimmed);
|
|
249
235
|
const mod = resolveModule(commandName);
|
|
250
236
|
|
|
251
|
-
if (!mod) {
|
|
252
|
-
return {
|
|
253
|
-
stderr: `Command '${trimmed}' not found`,
|
|
254
|
-
exitCode: 127,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
237
|
+
if (!mod) return { stderr: `${commandName}: command not found`, exitCode: 127 };
|
|
257
238
|
|
|
258
239
|
try {
|
|
259
240
|
return await mod.run({
|
|
@@ -266,9 +247,9 @@ export async function runCommand(
|
|
|
266
247
|
stdin,
|
|
267
248
|
cwd,
|
|
268
249
|
shell,
|
|
250
|
+
env: shellEnv,
|
|
269
251
|
});
|
|
270
252
|
} catch (error: unknown) {
|
|
271
|
-
|
|
272
|
-
return { stderr: message, exitCode: 1 };
|
|
253
|
+
return { stderr: error instanceof Error ? error.message : "Command failed", exitCode: 1 };
|
|
273
254
|
}
|
|
274
255
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const killCommand: ShellModule = {
|
|
4
|
+
name: "kill",
|
|
5
|
+
description: "Send signal to process",
|
|
6
|
+
category: "system",
|
|
7
|
+
params: ["[-9] <pid>"],
|
|
8
|
+
run: ({ args }) => {
|
|
9
|
+
const pid = args.find((a) => !a.startsWith("-"));
|
|
10
|
+
if (!pid) return { stderr: "kill: no pid specified", exitCode: 1 };
|
|
11
|
+
// In virtual env, we just acknowledge the kill
|
|
12
|
+
return { stdout: ``, exitCode: 0 };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const lnCommand: ShellModule = {
|
|
6
|
+
name: "ln",
|
|
7
|
+
description: "Create links",
|
|
8
|
+
category: "files",
|
|
9
|
+
params: ["[-s] <target> <link_name>"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
11
|
+
const symbolic = ifFlag(args, ["-s", "--symbolic"]);
|
|
12
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
13
|
+
const [targetArg, linkArg] = positionals;
|
|
14
|
+
|
|
15
|
+
if (!targetArg || !linkArg) {
|
|
16
|
+
return { stderr: "ln: missing operand", exitCode: 1 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const linkPath = resolvePath(cwd, linkArg);
|
|
20
|
+
const targetPath = symbolic
|
|
21
|
+
? targetArg // keep relative for symlinks
|
|
22
|
+
: resolvePath(cwd, targetArg);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
assertPathAccess(authUser, linkPath, "ln");
|
|
26
|
+
|
|
27
|
+
if (!symbolic) {
|
|
28
|
+
// Hard link — copy file contents
|
|
29
|
+
const srcPath = resolvePath(cwd, targetArg);
|
|
30
|
+
assertPathAccess(authUser, srcPath, "ln");
|
|
31
|
+
if (!shell.vfs.exists(srcPath)) {
|
|
32
|
+
return {
|
|
33
|
+
stderr: `ln: ${targetArg}: No such file or directory`,
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const content = shell.vfs.readFile(srcPath);
|
|
38
|
+
shell.writeFileAsUser(authUser, linkPath, content);
|
|
39
|
+
} else {
|
|
40
|
+
shell.vfs.symlink(targetPath, linkPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { exitCode: 0 };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
+
return { stderr: `ln: ${msg}`, exitCode: 1 };
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
package/src/commands/ls.ts
CHANGED
|
@@ -28,6 +28,8 @@ function formatDate(date: Date): string {
|
|
|
28
28
|
|
|
29
29
|
export const lsCommand: ShellModule = {
|
|
30
30
|
name: "ls",
|
|
31
|
+
description: "List directory contents",
|
|
32
|
+
category: "navigation",
|
|
31
33
|
params: ["[path]"],
|
|
32
34
|
run: ({ authUser, shell, cwd, args }) => {
|
|
33
35
|
const longFormat = ifFlag(args, ["-l", "--long"]);
|
package/src/commands/mkdir.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const mkdirCommand: ShellModule = {
|
|
6
6
|
name: "mkdir",
|
|
7
|
+
description: "Make directories",
|
|
8
|
+
category: "files",
|
|
7
9
|
params: ["<dir>"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args }) => {
|
|
9
11
|
if (args.length === 0) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const mvCommand: ShellModule = {
|
|
5
|
+
name: "mv",
|
|
6
|
+
description: "Move or rename files",
|
|
7
|
+
category: "files",
|
|
8
|
+
params: ["<source> <dest>"],
|
|
9
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
10
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
11
|
+
const [srcArg, destArg] = positionals;
|
|
12
|
+
|
|
13
|
+
if (!srcArg || !destArg) {
|
|
14
|
+
return { stderr: "mv: missing operand", exitCode: 1 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const srcPath = resolvePath(cwd, srcArg);
|
|
18
|
+
const destPath = resolvePath(cwd, destArg);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
assertPathAccess(authUser, srcPath, "mv");
|
|
22
|
+
assertPathAccess(authUser, destPath, "mv");
|
|
23
|
+
|
|
24
|
+
if (!shell.vfs.exists(srcPath)) {
|
|
25
|
+
return {
|
|
26
|
+
stderr: `mv: ${srcArg}: No such file or directory`,
|
|
27
|
+
exitCode: 1,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If dest is a directory, move into it
|
|
32
|
+
const finalDest =
|
|
33
|
+
shell.vfs.exists(destPath) &&
|
|
34
|
+
shell.vfs.stat(destPath).type === "directory"
|
|
35
|
+
? `${destPath}/${srcArg.split("/").pop()}`
|
|
36
|
+
: destPath;
|
|
37
|
+
|
|
38
|
+
shell.vfs.move(srcPath, finalDest);
|
|
39
|
+
return { exitCode: 0 };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
return { stderr: `mv: ${msg}`, exitCode: 1 };
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
package/src/commands/nano.ts
CHANGED
package/src/commands/neofetch.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { getAllEnvVars } from "./set";
|
|
|
5
5
|
|
|
6
6
|
export const neofetchCommand: ShellModule = {
|
|
7
7
|
name: "neofetch",
|
|
8
|
+
description: "System info display",
|
|
9
|
+
category: "misc",
|
|
8
10
|
params: ["[--off]"],
|
|
9
11
|
run: ({ args, authUser, hostname, shell }) => {
|
|
10
12
|
const env = getAllEnvVars(authUser);
|
package/src/commands/passwd.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { ShellModule } from "../types/commands";
|
|
|
2
2
|
|
|
3
3
|
export const passwdCommand: ShellModule = {
|
|
4
4
|
name: "passwd",
|
|
5
|
+
description: "Change user password",
|
|
6
|
+
category: "users",
|
|
5
7
|
params: ["<username> <password>"],
|
|
6
8
|
run: async ({ authUser, args, shell }) => {
|
|
7
9
|
const [username, password] = args;
|