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/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
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const cpCommand: ShellModule = {
|
|
6
|
+
name: "cp",
|
|
7
|
+
description: "Copy files or directories",
|
|
8
|
+
category: "files",
|
|
9
|
+
params: ["[-r] <source> <dest>"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
11
|
+
const recursive = ifFlag(args, ["-r", "-R", "--recursive"]);
|
|
12
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
13
|
+
const [srcArg, destArg] = positionals;
|
|
14
|
+
|
|
15
|
+
if (!srcArg || !destArg) {
|
|
16
|
+
return { stderr: "cp: missing operand", exitCode: 1 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const srcPath = resolvePath(cwd, srcArg);
|
|
20
|
+
const destPath = resolvePath(cwd, destArg);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
assertPathAccess(authUser, srcPath, "cp");
|
|
24
|
+
assertPathAccess(authUser, destPath, "cp");
|
|
25
|
+
|
|
26
|
+
if (!shell.vfs.exists(srcPath)) {
|
|
27
|
+
return {
|
|
28
|
+
stderr: `cp: ${srcArg}: No such file or directory`,
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const srcStat = shell.vfs.stat(srcPath);
|
|
34
|
+
|
|
35
|
+
if (srcStat.type === "directory") {
|
|
36
|
+
if (!recursive) {
|
|
37
|
+
return {
|
|
38
|
+
stderr: `cp: ${srcArg}: is a directory (use -r)`,
|
|
39
|
+
exitCode: 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const copyDir = (from: string, to: string) => {
|
|
43
|
+
shell.vfs.mkdir(to, 0o755);
|
|
44
|
+
for (const entry of shell.vfs.list(from)) {
|
|
45
|
+
const fromEntry = `${from}/${entry}`;
|
|
46
|
+
const toEntry = `${to}/${entry}`;
|
|
47
|
+
const stat = shell.vfs.stat(fromEntry);
|
|
48
|
+
if (stat.type === "directory") {
|
|
49
|
+
copyDir(fromEntry, toEntry);
|
|
50
|
+
} else {
|
|
51
|
+
const content = shell.vfs.readFileRaw(fromEntry);
|
|
52
|
+
shell.writeFileAsUser(authUser, toEntry, content);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const finalDest =
|
|
57
|
+
shell.vfs.exists(destPath) &&
|
|
58
|
+
shell.vfs.stat(destPath).type === "directory"
|
|
59
|
+
? `${destPath}/${srcArg.split("/").pop()}`
|
|
60
|
+
: destPath;
|
|
61
|
+
copyDir(srcPath, finalDest);
|
|
62
|
+
} else {
|
|
63
|
+
const finalDest =
|
|
64
|
+
shell.vfs.exists(destPath) &&
|
|
65
|
+
shell.vfs.stat(destPath).type === "directory"
|
|
66
|
+
? `${destPath}/${srcArg.split("/").pop()}`
|
|
67
|
+
: destPath;
|
|
68
|
+
const content = shell.vfs.readFileRaw(srcPath);
|
|
69
|
+
shell.writeFileAsUser(authUser, finalDest, content);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { exitCode: 0 };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
return { stderr: `cp: ${msg}`, exitCode: 1 };
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
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
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const diffCommand: ShellModule = {
|
|
5
|
+
name: "diff",
|
|
6
|
+
description: "Compare files line by line",
|
|
7
|
+
category: "text",
|
|
8
|
+
params: ["<file1> <file2>"],
|
|
9
|
+
run: ({ shell, cwd, args }) => {
|
|
10
|
+
const [f1, f2] = args;
|
|
11
|
+
if (!f1 || !f2) return { stderr: "diff: missing operand", exitCode: 1 };
|
|
12
|
+
const p1 = resolvePath(cwd, f1);
|
|
13
|
+
const p2 = resolvePath(cwd, f2);
|
|
14
|
+
let a: string[], b: string[];
|
|
15
|
+
try { a = shell.vfs.readFile(p1).split("\n"); } catch { return { stderr: `diff: ${f1}: No such file or directory`, exitCode: 2 }; }
|
|
16
|
+
try { b = shell.vfs.readFile(p2).split("\n"); } catch { return { stderr: `diff: ${f2}: No such file or directory`, exitCode: 2 }; }
|
|
17
|
+
|
|
18
|
+
const out: string[] = [];
|
|
19
|
+
const max = Math.max(a.length, b.length);
|
|
20
|
+
for (let i = 0; i < max; i++) {
|
|
21
|
+
const la = a[i]; const lb = b[i];
|
|
22
|
+
if (la !== lb) {
|
|
23
|
+
if (la !== undefined) out.push(`< ${la}`);
|
|
24
|
+
if (lb !== undefined) out.push(`> ${lb}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { stdout: out.join("\n"), exitCode: out.length > 0 ? 1 : 0 };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const duCommand: ShellModule = {
|
|
6
|
+
name: "du",
|
|
7
|
+
description: "Estimate file space usage",
|
|
8
|
+
category: "system",
|
|
9
|
+
params: ["[-h] [-s] [path]"],
|
|
10
|
+
run: ({ shell, cwd, args }) => {
|
|
11
|
+
const human = ifFlag(args, ["-h"]);
|
|
12
|
+
const summary = ifFlag(args, ["-s"]);
|
|
13
|
+
const target = args.find((a) => !a.startsWith("-")) ?? ".";
|
|
14
|
+
const p = resolvePath(cwd, target);
|
|
15
|
+
|
|
16
|
+
const fmt = (b: number) => human ? `${(b / 1024).toFixed(1)}K` : String(Math.ceil(b / 1024));
|
|
17
|
+
|
|
18
|
+
if (!shell.vfs.exists(p)) return { stderr: `du: ${target}: No such file or directory`, exitCode: 1 };
|
|
19
|
+
|
|
20
|
+
if (summary || shell.vfs.stat(p).type === "file") {
|
|
21
|
+
return { stdout: `${fmt(shell.vfs.getUsageBytes(p))}\t${target}`, exitCode: 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
const walk = (dir: string, rel: string) => {
|
|
26
|
+
let total = 0;
|
|
27
|
+
for (const e of shell.vfs.list(dir)) {
|
|
28
|
+
const full = `${dir}/${e}`, r = `${rel}/${e}`;
|
|
29
|
+
const st = shell.vfs.stat(full);
|
|
30
|
+
if (st.type === "directory") total += walk(full, r);
|
|
31
|
+
else { total += st.size; if (!summary) lines.push(`${fmt(st.size)}\t${r}`); }
|
|
32
|
+
}
|
|
33
|
+
lines.push(`${fmt(total)}\t${rel}`);
|
|
34
|
+
return total;
|
|
35
|
+
};
|
|
36
|
+
walk(p, target);
|
|
37
|
+
return { stdout: lines.join("\n"), exitCode: 0 };
|
|
38
|
+
},
|
|
39
|
+
};
|
package/src/commands/echo.ts
CHANGED
|
@@ -10,6 +10,8 @@ function expandEnvVars(input: string, env: Record<string, string>): string {
|
|
|
10
10
|
|
|
11
11
|
export const echoCommand: ShellModule = {
|
|
12
12
|
name: "echo",
|
|
13
|
+
description: "Display text",
|
|
14
|
+
category: "shell",
|
|
13
15
|
params: ["[options] [text...]"],
|
|
14
16
|
run: ({ args, authUser, stdin }) => {
|
|
15
17
|
const { flags, positionals } = parseArgs(args, { flags: ["-n"] });
|
package/src/commands/env.ts
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
-
import { getAllEnvVars } from "./set";
|
|
3
2
|
|
|
4
3
|
export const envCommand: ShellModule = {
|
|
5
4
|
name: "env",
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const envVarsOutput = Object.entries(allVars)
|
|
13
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
14
|
-
.sort()
|
|
15
|
-
.join("\n");
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
stdout: envVarsOutput,
|
|
19
|
-
exitCode: 0,
|
|
20
|
-
};
|
|
5
|
+
description: "Print environment variables",
|
|
6
|
+
category: "shell",
|
|
7
|
+
params: [],
|
|
8
|
+
run: ({ env, authUser }) => {
|
|
9
|
+
const vars = { ...env.vars, USER: authUser, HOME: `/home/${authUser}` };
|
|
10
|
+
return { stdout: Object.entries(vars).map(([k, v]) => `${k}=${v}`).join("\n"), exitCode: 0 };
|
|
21
11
|
},
|
|
22
12
|
};
|
package/src/commands/export.ts
CHANGED
|
@@ -1,38 +1,25 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
-
import { getArg } from "./command-helpers";
|
|
3
|
-
import { getEnvVar, setEnvVar } from "./set";
|
|
4
2
|
|
|
5
3
|
export const exportCommand: ShellModule = {
|
|
6
4
|
name: "export",
|
|
5
|
+
description: "Set shell environment variable",
|
|
6
|
+
category: "shell",
|
|
7
7
|
params: ["[VAR=value]"],
|
|
8
|
-
run: ({ args }) => {
|
|
9
|
-
// export VAR=value or export VAR (to make it available to child processes)
|
|
8
|
+
run: ({ args, env }) => {
|
|
10
9
|
if (args.length === 0) {
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
stdout: "# export command - sets variables for child processes",
|
|
14
|
-
exitCode: 0,
|
|
15
|
-
};
|
|
10
|
+
const out = Object.entries(env.vars).map(([k, v]) => `declare -x ${k}="${v}"`).join("\n");
|
|
11
|
+
return { stdout: out, exitCode: 0 };
|
|
16
12
|
}
|
|
17
|
-
|
|
18
|
-
// Parse VAR=value format
|
|
19
|
-
for (let index = 0; ; index += 1) {
|
|
20
|
-
const arg = getArg(args, index);
|
|
21
|
-
if (!arg) {
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
|
|
13
|
+
for (const arg of args) {
|
|
25
14
|
if (arg.includes("=")) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
const eq = arg.indexOf("=");
|
|
16
|
+
const name = arg.slice(0, eq);
|
|
17
|
+
const value = arg.slice(eq + 1);
|
|
18
|
+
env.vars[name] = value;
|
|
30
19
|
} else {
|
|
31
|
-
//
|
|
32
|
-
setEnvVar(arg, getEnvVar(arg) || "");
|
|
20
|
+
// mark existing as exported (already is)
|
|
33
21
|
}
|
|
34
22
|
}
|
|
35
|
-
|
|
36
23
|
return { exitCode: 0 };
|
|
37
24
|
},
|
|
38
25
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { getFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const findCommand: ShellModule = {
|
|
6
|
+
name: "find",
|
|
7
|
+
description: "Search for files",
|
|
8
|
+
category: "files",
|
|
9
|
+
params: ["[path] [-name <pattern>] [-type f|d]"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
11
|
+
const namePattern = getFlag(args, ["-name"]);
|
|
12
|
+
const typeFilter = getFlag(args, ["-type"]);
|
|
13
|
+
const positionals = args.filter(
|
|
14
|
+
(a) => !a.startsWith("-") && a !== namePattern && a !== typeFilter,
|
|
15
|
+
);
|
|
16
|
+
const rootArg = positionals[0] ?? ".";
|
|
17
|
+
const rootPath = resolvePath(cwd, rootArg);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
assertPathAccess(authUser, rootPath, "find");
|
|
21
|
+
if (!shell.vfs.exists(rootPath)) {
|
|
22
|
+
return {
|
|
23
|
+
stderr: `find: ${rootArg}: No such file or directory`,
|
|
24
|
+
exitCode: 1,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return { stderr: `find: ${msg}`, exitCode: 1 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const nameRegex = namePattern
|
|
33
|
+
? new RegExp(
|
|
34
|
+
`^${(namePattern as string).replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".")}$`,
|
|
35
|
+
)
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
const results: string[] = [];
|
|
39
|
+
const walk = (currentPath: string, display: string) => {
|
|
40
|
+
const stat = shell.vfs.stat(currentPath);
|
|
41
|
+
|
|
42
|
+
const matchesType =
|
|
43
|
+
!typeFilter ||
|
|
44
|
+
(typeFilter === "f" && stat.type === "file") ||
|
|
45
|
+
(typeFilter === "d" && stat.type === "directory");
|
|
46
|
+
const matchesName =
|
|
47
|
+
!nameRegex || nameRegex.test(currentPath.split("/").pop() ?? "");
|
|
48
|
+
|
|
49
|
+
if (matchesType && matchesName) results.push(display);
|
|
50
|
+
|
|
51
|
+
if (stat.type === "directory") {
|
|
52
|
+
for (const entry of shell.vfs.list(currentPath)) {
|
|
53
|
+
const full = `${currentPath}/${entry}`;
|
|
54
|
+
const disp = `${display}/${entry}`;
|
|
55
|
+
walk(full, disp);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
walk(rootPath, rootArg);
|
|
61
|
+
return { stdout: results.join("\n"), exitCode: 0 };
|
|
62
|
+
},
|
|
63
|
+
};
|
package/src/commands/grep.ts
CHANGED
|
@@ -4,11 +4,15 @@ import { assertPathAccess, resolvePath } from "./helpers";
|
|
|
4
4
|
|
|
5
5
|
export const grepCommand: ShellModule = {
|
|
6
6
|
name: "grep",
|
|
7
|
-
|
|
7
|
+
description: "Search text patterns",
|
|
8
|
+
category: "text",
|
|
9
|
+
params: ["[-i] [-v] [-n] [-r] <pattern> [file...]"],
|
|
8
10
|
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
9
|
-
const { flags, positionals } = parseArgs(args, { flags: ["-i", "-v"] });
|
|
11
|
+
const { flags, positionals } = parseArgs(args, { flags: ["-i", "-v", "-n", "-r"] });
|
|
10
12
|
const caseInsensitive = flags.has("-i");
|
|
11
13
|
const invertMatch = flags.has("-v");
|
|
14
|
+
const showLineNumbers = flags.has("-n");
|
|
15
|
+
const recursive = flags.has("-r");
|
|
12
16
|
const pattern = positionals[0];
|
|
13
17
|
const files = positionals.slice(1);
|
|
14
18
|
|
|
@@ -18,57 +22,66 @@ export const grepCommand: ShellModule = {
|
|
|
18
22
|
|
|
19
23
|
let regex: RegExp;
|
|
20
24
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
// No "g" flag — avoids the stateful lastIndex problem with regex.test()
|
|
26
|
+
const regexFlags = caseInsensitive ? "mi" : "m";
|
|
27
|
+
regex = new RegExp(pattern, regexFlags);
|
|
23
28
|
} catch {
|
|
24
29
|
return { stderr: `grep: invalid regex: ${pattern}`, exitCode: 1 };
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const lines = stdin.split("\n");
|
|
35
|
-
for (const line of lines) {
|
|
36
|
-
regex.lastIndex = 0;
|
|
32
|
+
const matchLines = (content: string, prefix = ""): string[] => {
|
|
33
|
+
const lines = content.split("\n");
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i] ?? "";
|
|
37
37
|
const matches = regex.test(line);
|
|
38
38
|
const shouldInclude = invertMatch ? !matches : matches;
|
|
39
|
-
|
|
40
39
|
if (shouldInclude) {
|
|
41
|
-
|
|
40
|
+
const lineLabel = showLineNumbers ? `${i + 1}:` : "";
|
|
41
|
+
out.push(`${prefix}${lineLabel}${line}`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const readPaths = (base: string): string[] => {
|
|
48
|
+
if (!shell.vfs.exists(base)) return [];
|
|
49
|
+
const stat = shell.vfs.stat(base);
|
|
50
|
+
if (stat.type === "file") return [base];
|
|
51
|
+
if (!recursive) return [];
|
|
52
|
+
const paths: string[] = [];
|
|
53
|
+
const walk = (dir: string) => {
|
|
54
|
+
for (const entry of shell.vfs.list(dir)) {
|
|
55
|
+
const full = `${dir}/${entry}`;
|
|
56
|
+
const s = shell.vfs.stat(full);
|
|
57
|
+
if (s.type === "file") paths.push(full);
|
|
58
|
+
else walk(full);
|
|
59
|
+
}
|
|
48
60
|
};
|
|
49
|
-
|
|
61
|
+
walk(base);
|
|
62
|
+
return paths;
|
|
63
|
+
};
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
const target = resolvePath(cwd, file);
|
|
53
|
-
try {
|
|
54
|
-
assertPathAccess(authUser, target, "grep");
|
|
55
|
-
const content = shell.vfs.readFile(target);
|
|
56
|
-
const lines = content.split("\n");
|
|
65
|
+
const results: string[] = [];
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
if (!stdin) return { stdout: "", exitCode: 1 };
|
|
69
|
+
results.push(...matchLines(stdin));
|
|
70
|
+
} else {
|
|
71
|
+
const resolvedPaths = files.flatMap((f) => {
|
|
72
|
+
const target = resolvePath(cwd, f);
|
|
73
|
+
return readPaths(target).map((p) => ({ file: f, path: p }));
|
|
74
|
+
});
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
for (const { file, path: filePath } of resolvedPaths) {
|
|
77
|
+
try {
|
|
78
|
+
assertPathAccess(authUser, filePath, "grep");
|
|
79
|
+
const content = shell.vfs.readFile(filePath);
|
|
80
|
+
const prefix = resolvedPaths.length > 1 ? `${file}:` : "";
|
|
81
|
+
results.push(...matchLines(content, prefix));
|
|
82
|
+
} catch {
|
|
83
|
+
return { stderr: `grep: ${file}: No such file or directory`, exitCode: 1 };
|
|
66
84
|
}
|
|
67
|
-
} catch {
|
|
68
|
-
return {
|
|
69
|
-
stderr: `grep: ${file}: No such file or directory`,
|
|
70
|
-
exitCode: 1,
|
|
71
|
-
};
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const groupsCommand: ShellModule = {
|
|
4
|
+
name: "groups",
|
|
5
|
+
description: "Print group memberships",
|
|
6
|
+
category: "system",
|
|
7
|
+
params: ["[user]"],
|
|
8
|
+
run: ({ authUser, shell, args }) => {
|
|
9
|
+
const target = args[0] ?? authUser;
|
|
10
|
+
const isSudo = shell.users.isSudoer(target);
|
|
11
|
+
const grps = isSudo ? `${target} sudo root` : target;
|
|
12
|
+
return { stdout: grps, exitCode: 0 };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const gzipCommand: ShellModule = {
|
|
5
|
+
name: "gzip",
|
|
6
|
+
description: "Compress files",
|
|
7
|
+
category: "archive",
|
|
8
|
+
params: ["<file>"],
|
|
9
|
+
run: ({ shell, cwd, args }) => {
|
|
10
|
+
const file = args[0];
|
|
11
|
+
if (!file) return { stderr: "gzip: no file specified", exitCode: 1 };
|
|
12
|
+
const p = resolvePath(cwd, file);
|
|
13
|
+
try { shell.vfs.compressFile(p); return { exitCode: 0 }; }
|
|
14
|
+
catch { return { stderr: `gzip: ${file}: No such file or directory`, exitCode: 1 }; }
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const gunzipCommand: ShellModule = {
|
|
19
|
+
name: "gunzip",
|
|
20
|
+
description: "Decompress files",
|
|
21
|
+
category: "archive",
|
|
22
|
+
params: ["<file>"],
|
|
23
|
+
aliases: ["zcat"],
|
|
24
|
+
run: ({ shell, cwd, args }) => {
|
|
25
|
+
const file = args[0];
|
|
26
|
+
if (!file) return { stderr: "gunzip: no file specified", exitCode: 1 };
|
|
27
|
+
const p = resolvePath(cwd, file);
|
|
28
|
+
try { shell.vfs.decompressFile(p); return { exitCode: 0 }; }
|
|
29
|
+
catch { return { stderr: `gunzip: ${file}: No such file or directory`, exitCode: 1 }; }
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { getFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const headCommand: ShellModule = {
|
|
6
|
+
name: "head",
|
|
7
|
+
description: "Output first lines",
|
|
8
|
+
category: "text",
|
|
9
|
+
params: ["[-n <lines>] [file...]"],
|
|
10
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
11
|
+
const nArg = getFlag(args, ["-n"]);
|
|
12
|
+
const n = typeof nArg === "string" ? parseInt(nArg, 10) : 10;
|
|
13
|
+
const positionals = args.filter((a) => !a.startsWith("-") && a !== nArg);
|
|
14
|
+
|
|
15
|
+
const take = (content: string) =>
|
|
16
|
+
content.split("\n").slice(0, n).join("\n");
|
|
17
|
+
|
|
18
|
+
if (positionals.length === 0) {
|
|
19
|
+
return { stdout: take(stdin ?? ""), exitCode: 0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const results: string[] = [];
|
|
23
|
+
for (const file of positionals) {
|
|
24
|
+
const filePath = resolvePath(cwd, file);
|
|
25
|
+
try {
|
|
26
|
+
assertPathAccess(authUser, filePath, "head");
|
|
27
|
+
results.push(take(shell.vfs.readFile(filePath)));
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
stderr: `head: ${file}: No such file or directory`,
|
|
31
|
+
exitCode: 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { stdout: results.join("\n"), exitCode: 0 };
|
|
36
|
+
},
|
|
37
|
+
};
|