typescript-virtual-container 1.0.4 → 1.0.5
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/CHANGELOG.md +11 -0
- package/README.md +50 -0
- package/package.json +1 -1
- package/src/SSHMimic/client.ts +1 -1
- package/src/SSHMimic/exec.ts +1 -1
- package/src/SSHMimic/executor.ts +2 -2
- package/src/SSHMimic/index.ts +14 -18
- package/src/{VirtualFileSystem.ts → VirtualFileSystem/index.ts} +6 -15
- package/src/{SSHMimic → VirtualShell}/commands/cat.ts +2 -1
- package/src/VirtualShell/commands/command-helpers.ts +135 -0
- package/src/{SSHMimic → VirtualShell}/commands/curl.ts +16 -37
- package/src/{SSHMimic → VirtualShell}/commands/echo.ts +10 -2
- package/src/{SSHMimic → VirtualShell}/commands/export.ts +7 -1
- package/src/{SSHMimic → VirtualShell}/commands/grep.ts +15 -8
- package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
- package/src/{SSHMimic → VirtualShell}/commands/index.ts +63 -8
- package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
- package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
- package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
- package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
- package/src/VirtualShell/commands/sh.ts +58 -0
- package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
- package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +16 -26
- package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
- package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
- package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
- package/src/VirtualShell/index.ts +69 -0
- package/src/{SSHMimic → VirtualShell}/shell.ts +3 -3
- package/src/index.ts +8 -0
- package/src/standalone.ts +10 -1
- package/tests/command-helpers.test.ts +40 -0
- package/tests/helpers.test.ts +1 -1
- package/src/SSHMimic/commands/sh.ts +0 -121
- /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/env.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/unset.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/shellParser.ts +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
export const mkdirCommand: ShellModule = {
|
|
@@ -9,7 +10,11 @@ export const mkdirCommand: ShellModule = {
|
|
|
9
10
|
return { stderr: "mkdir: missing operand", exitCode: 1 };
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
for (
|
|
13
|
+
for (let index = 0; index < args.length; index++) {
|
|
14
|
+
const dir = getArg(args, index);
|
|
15
|
+
if (!dir) {
|
|
16
|
+
return { stderr: "mkdir: missing operand", exitCode: 1 };
|
|
17
|
+
}
|
|
13
18
|
const target = resolvePath(cwd, dir);
|
|
14
19
|
assertPathAccess(authUser, target, "mkdir");
|
|
15
20
|
vfs.mkdir(target);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
export const rmCommand: ShellModule = {
|
|
@@ -9,9 +10,15 @@ export const rmCommand: ShellModule = {
|
|
|
9
10
|
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const recursive =
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const recursive = ifFlag(args, ["-r", "-rf", "-fr"]);
|
|
14
|
+
const targets: string[] = [];
|
|
15
|
+
for (let index = 0; ; index += 1) {
|
|
16
|
+
const target = getArg(args, index, { flags: ["-r", "-rf", "-fr"] });
|
|
17
|
+
if (!target) {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
targets.push(target);
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
if (targets.length === 0) {
|
|
17
24
|
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** biome-ignore-all lint/style/useNamingConvention: env variables */
|
|
2
2
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
import { getArg } from "./command-helpers";
|
|
3
4
|
|
|
4
5
|
// Simple in-memory environment variables store
|
|
5
6
|
// In a real implementation, this would be per-session/per-user
|
|
@@ -41,7 +42,12 @@ export const setCommand: ShellModule = {
|
|
|
41
42
|
|
|
42
43
|
// Parse VAR=value format
|
|
43
44
|
const assignments: string[] = [];
|
|
44
|
-
for (
|
|
45
|
+
for (let index = 0; ; index += 1) {
|
|
46
|
+
const arg = getArg(args, index);
|
|
47
|
+
if (!arg) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
if (arg.includes("=")) {
|
|
46
52
|
const [varName, varValue] = arg.split("=", 2);
|
|
47
53
|
if (varName && varValue !== undefined) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CommandContext, ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, getFlag } from "./command-helpers";
|
|
3
|
+
import { runCommand } from "./index";
|
|
4
|
+
|
|
5
|
+
/** Simple shell script executor with basic variable support */
|
|
6
|
+
export const shCommand: ShellModule = {
|
|
7
|
+
name: "sh",
|
|
8
|
+
params: ["-c <script>", "[<file>]"],
|
|
9
|
+
aliases: ["bash"],
|
|
10
|
+
run: async (ctx: CommandContext) => {
|
|
11
|
+
const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
|
|
12
|
+
|
|
13
|
+
// Handle -c option: sh -c "command"
|
|
14
|
+
if (getFlag(args, "-c") && args.length >= 2) {
|
|
15
|
+
const script = getArg(args, 1) ?? "";
|
|
16
|
+
if (!script) {
|
|
17
|
+
return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
18
|
+
}
|
|
19
|
+
const scriptArgs = args.slice(2);
|
|
20
|
+
|
|
21
|
+
// Split by semicolon and newline
|
|
22
|
+
const lines = script
|
|
23
|
+
.split(/[;\n]/)
|
|
24
|
+
.map((line) => line.trim())
|
|
25
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
26
|
+
|
|
27
|
+
let output = "";
|
|
28
|
+
const exitCode = 0;
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
// Simple variable substitution
|
|
32
|
+
let command = line;
|
|
33
|
+
for (let i = 0; i < scriptArgs.length; i++) {
|
|
34
|
+
const arg = scriptArgs[i] ?? "";
|
|
35
|
+
command = command.replaceAll(`$${i}`, arg);
|
|
36
|
+
}
|
|
37
|
+
command = command.replaceAll("$@", scriptArgs.join(" "));
|
|
38
|
+
|
|
39
|
+
// Execute the command
|
|
40
|
+
const result = await Promise.resolve(
|
|
41
|
+
runCommand(command, authUser, hostname, users, mode, cwd, vfs),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (result.stdout) {
|
|
45
|
+
output += `${result.stdout}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (result.stderr) {
|
|
49
|
+
return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { stdout: output.trim(), exitCode };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { stderr: "sh: invalid usage", exitCode: 1 };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
|
|
3
4
|
export const suCommand: ShellModule = {
|
|
4
5
|
name: "su",
|
|
5
6
|
params: ["- <username>"],
|
|
6
7
|
run: ({ authUser, users, args }) => {
|
|
7
|
-
const
|
|
8
|
-
const targetUser = filtered[0];
|
|
8
|
+
const targetUser = getArg(args, 0, { flags: ["-"] });
|
|
9
9
|
|
|
10
10
|
if (!targetUser) {
|
|
11
11
|
return { stderr: "su: missing username", exitCode: 1 };
|
|
@@ -16,7 +16,7 @@ export const suCommand: ShellModule = {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
if (
|
|
19
|
-
!users.verifyPassword(targetUser,
|
|
19
|
+
!users.verifyPassword(targetUser, getArg(args, 1) ?? "") &&
|
|
20
20
|
authUser !== "root"
|
|
21
21
|
) {
|
|
22
22
|
return { stderr: "su: authentication failure", exitCode: 1 };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, getFlag, ifFlag } from "./command-helpers";
|
|
2
3
|
import { runCommand } from "./index";
|
|
3
4
|
|
|
4
5
|
function parseSudoArgs(args: string[]): {
|
|
@@ -6,34 +7,23 @@ function parseSudoArgs(args: string[]): {
|
|
|
6
7
|
loginShell: boolean;
|
|
7
8
|
commandLine: string | null;
|
|
8
9
|
} {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (arg === "-i") {
|
|
17
|
-
loginShell = true;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (arg === "-S") {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (arg === "-u") {
|
|
26
|
-
targetUser = args[index + 1] ?? "root";
|
|
27
|
-
index += 1;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
10
|
+
const loginShell = ifFlag(args, "-i");
|
|
11
|
+
const targetUserValue = getFlag(args, ["-u", "--user"]);
|
|
12
|
+
const targetUser =
|
|
13
|
+
typeof targetUserValue === "string" && targetUserValue.length > 0
|
|
14
|
+
? targetUserValue
|
|
15
|
+
: "root";
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
const commandParts: string[] = [];
|
|
18
|
+
for (let index = 0; ; index += 1) {
|
|
19
|
+
const part = getArg(args, index, {
|
|
20
|
+
flags: ["-i", "-S"],
|
|
21
|
+
flagsWithValue: ["-u", "--user"],
|
|
22
|
+
});
|
|
23
|
+
if (!part) {
|
|
24
|
+
break;
|
|
34
25
|
}
|
|
35
|
-
|
|
36
|
-
commandParts.push(arg);
|
|
26
|
+
commandParts.push(part);
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
export const treeCommand: ShellModule = {
|
|
5
6
|
name: "tree",
|
|
6
7
|
params: ["[path]"],
|
|
7
8
|
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
|
-
const target = resolvePath(cwd, args
|
|
9
|
+
const target = resolvePath(cwd, getArg(args, 0) ?? cwd);
|
|
9
10
|
assertPathAccess(authUser, target, "tree");
|
|
10
11
|
return { stdout: vfs.tree(target), exitCode: 0 };
|
|
11
12
|
},
|
|
@@ -3,10 +3,10 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { ShellModule } from "../../types/commands";
|
|
6
|
+
import { getArg, getFlag, ifFlag } from "./command-helpers";
|
|
6
7
|
import {
|
|
7
8
|
assertPathAccess,
|
|
8
9
|
normalizeTerminalOutput,
|
|
9
|
-
parseOutputPath,
|
|
10
10
|
resolvePath,
|
|
11
11
|
stripUrlFilename,
|
|
12
12
|
} from "./helpers";
|
|
@@ -83,12 +83,29 @@ export const wgetCommand: ShellModule = {
|
|
|
83
83
|
name: "wget",
|
|
84
84
|
params: ["[url]"],
|
|
85
85
|
run: async ({ authUser, vfs, cwd, args }) => {
|
|
86
|
-
const
|
|
86
|
+
const outputPathValue = getFlag(args, [
|
|
87
|
+
"-o",
|
|
88
|
+
"-O",
|
|
89
|
+
"--output",
|
|
90
|
+
"--output-document",
|
|
91
|
+
]);
|
|
92
|
+
const outputPath =
|
|
93
|
+
typeof outputPathValue === "string" && outputPathValue.length > 0
|
|
94
|
+
? outputPathValue
|
|
95
|
+
: null;
|
|
96
|
+
const parserOptions = {
|
|
97
|
+
flagsWithValue: ["-o", "-O", "--output", "--output-document"],
|
|
98
|
+
};
|
|
99
|
+
const inputArgs: string[] = [];
|
|
100
|
+
for (let index = 0; ; index += 1) {
|
|
101
|
+
const arg = getArg(args, index, parserOptions);
|
|
102
|
+
if (!arg) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
inputArgs.push(arg);
|
|
106
|
+
}
|
|
87
107
|
const url = inputArgs[0];
|
|
88
|
-
const isHelpLike =
|
|
89
|
-
(arg) =>
|
|
90
|
-
arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version",
|
|
91
|
-
);
|
|
108
|
+
const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
|
|
92
109
|
|
|
93
110
|
if (!url) {
|
|
94
111
|
return { stderr: "wget: missing URL", exitCode: 1 };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { VirtualUserManager } from "../SSHMimic/users";
|
|
2
|
+
import type { CommandContext, CommandResult } from "../types/commands";
|
|
3
|
+
import type { ShellStream } from "../types/streams";
|
|
4
|
+
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
5
|
+
import { createCustomCommand, registerCommand, runCommand } from "./commands";
|
|
6
|
+
import { startShell } from "./shell";
|
|
7
|
+
|
|
8
|
+
class VirtualShell {
|
|
9
|
+
private vfs: VirtualFileSystem;
|
|
10
|
+
private users: VirtualUserManager;
|
|
11
|
+
private hostname: string;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
vfs: VirtualFileSystem,
|
|
15
|
+
users: VirtualUserManager,
|
|
16
|
+
hostname: string,
|
|
17
|
+
) {
|
|
18
|
+
this.vfs = vfs;
|
|
19
|
+
this.users = users;
|
|
20
|
+
this.hostname = hostname;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
addCommand(
|
|
24
|
+
name: string,
|
|
25
|
+
params: string[],
|
|
26
|
+
callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
|
|
27
|
+
): void {
|
|
28
|
+
const normalized = name.trim().toLowerCase();
|
|
29
|
+
if (normalized.length === 0 || /\s/.test(normalized)) {
|
|
30
|
+
throw new Error("Command name must be non-empty and contain no spaces");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
registerCommand(createCustomCommand(normalized, params, callback));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
executeCommand(rawInput: string, authUser: string, cwd: string): void {
|
|
37
|
+
runCommand(
|
|
38
|
+
rawInput,
|
|
39
|
+
authUser,
|
|
40
|
+
this.hostname,
|
|
41
|
+
this.users,
|
|
42
|
+
"shell",
|
|
43
|
+
cwd,
|
|
44
|
+
this.vfs,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
startInteractiveSession(
|
|
49
|
+
stream: ShellStream,
|
|
50
|
+
authUser: string,
|
|
51
|
+
sessionId: string | null,
|
|
52
|
+
remoteAddress: string,
|
|
53
|
+
terminalSize: { cols: number; rows: number },
|
|
54
|
+
): void {
|
|
55
|
+
// Interactive shell logic
|
|
56
|
+
startShell(
|
|
57
|
+
stream,
|
|
58
|
+
authUser,
|
|
59
|
+
this.vfs!,
|
|
60
|
+
this.hostname,
|
|
61
|
+
this.users!,
|
|
62
|
+
sessionId,
|
|
63
|
+
remoteAddress,
|
|
64
|
+
terminalSize,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { VirtualShell };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
2
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { formatLoginDate } from "../SSHMimic/loginFormat";
|
|
5
|
+
import { buildPrompt } from "../SSHMimic/prompt";
|
|
6
|
+
import type { VirtualUserManager } from "../SSHMimic/users";
|
|
4
7
|
import type { ShellStream } from "../types/streams";
|
|
5
8
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
6
9
|
import { getCommandNames, runCommand } from "./commands";
|
|
7
|
-
import { formatLoginDate } from "./loginFormat";
|
|
8
|
-
import { buildPrompt } from "./prompt";
|
|
9
|
-
import type { VirtualUserManager } from "./users";
|
|
10
10
|
|
|
11
11
|
interface NanoSession {
|
|
12
12
|
kind: "nano" | "htop";
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { SshClient } from "./SSHMimic/client";
|
|
|
2
2
|
import { SshMimic } from "./SSHMimic/index";
|
|
3
3
|
import { VirtualUserManager } from "./SSHMimic/users";
|
|
4
4
|
import VirtualFileSystem from "./VirtualFileSystem";
|
|
5
|
+
import type { VirtualShell } from "./VirtualShell";
|
|
5
6
|
|
|
6
7
|
export type {
|
|
7
8
|
CommandContext,
|
|
@@ -33,4 +34,11 @@ export {
|
|
|
33
34
|
VirtualFileSystem,
|
|
34
35
|
SshMimic as VirtualMachine,
|
|
35
36
|
VirtualUserManager,
|
|
37
|
+
type VirtualShell,
|
|
36
38
|
};
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
getArg,
|
|
42
|
+
getFlag,
|
|
43
|
+
ifFlag,
|
|
44
|
+
} from "./VirtualShell/commands/command-helpers";
|
package/src/standalone.ts
CHANGED
|
@@ -6,7 +6,16 @@ const sshMimic = new VirtualMachine({ port: 2222, hostname: sshHostname });
|
|
|
6
6
|
sshMimic
|
|
7
7
|
.start()
|
|
8
8
|
.then((port: number) => {
|
|
9
|
-
console.
|
|
9
|
+
if (!sshMimic.shell) console.error("Failed to initialize SSH Mimic shell.");
|
|
10
|
+
else {
|
|
11
|
+
console.log(`SSH Mimic initialized. Listening on port ${port}.`);
|
|
12
|
+
sshMimic.shell.addCommand("demo", [], () => {
|
|
13
|
+
return {
|
|
14
|
+
stdout: "This is a demo command. It does nothing useful.",
|
|
15
|
+
exitCode: 0,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
10
19
|
})
|
|
11
20
|
.catch((error: unknown) => {
|
|
12
21
|
console.error("Failed to start SSH Mimic:", error);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
getArg,
|
|
4
|
+
getFlag,
|
|
5
|
+
ifFlag,
|
|
6
|
+
} from "../src/VirtualShell/commands/command-helpers";
|
|
7
|
+
|
|
8
|
+
describe("command-helpers", () => {
|
|
9
|
+
test("ifFlag detects plain and inline flag forms", () => {
|
|
10
|
+
expect(ifFlag(["-l", "docs"], ["-l", "--long"])).toBe(true);
|
|
11
|
+
expect(ifFlag(["--user=root", "whoami"], "--user")).toBe(true);
|
|
12
|
+
expect(ifFlag(["docs"], ["-l", "--long"])).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("getFlag returns value for adjacent and inline forms", () => {
|
|
16
|
+
expect(getFlag(["-u", "root", "id"], ["-u", "--user"])).toBe("root");
|
|
17
|
+
expect(getFlag(["--user=alice", "id"], ["-u", "--user"])).toBe("alice");
|
|
18
|
+
expect(getFlag(["-i", "whoami"], "-i")).toBe("whoami");
|
|
19
|
+
expect(getFlag(["-o"], "-o")).toBe(true);
|
|
20
|
+
expect(getFlag(["pwd"], ["-u", "--user"])).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("getArg skips bool and value flags", () => {
|
|
24
|
+
const args = ["-i", "-u", "root", "sh", "-c", "whoami"];
|
|
25
|
+
const options = { flags: ["-i"], flagsWithValue: ["-u"] };
|
|
26
|
+
|
|
27
|
+
expect(getArg(args, 0, options)).toBe("sh");
|
|
28
|
+
expect(getArg(args, 1, options)).toBe("-c");
|
|
29
|
+
expect(getArg(args, 2, options)).toBe("whoami");
|
|
30
|
+
expect(getArg(args, 3, options)).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("getArg keeps tokens after -- as positional", () => {
|
|
34
|
+
const args = ["-n", "--", "-n", "hello"];
|
|
35
|
+
const options = { flags: ["-n"] };
|
|
36
|
+
|
|
37
|
+
expect(getArg(args, 0, options)).toBe("-n");
|
|
38
|
+
expect(getArg(args, 1, options)).toBe("hello");
|
|
39
|
+
});
|
|
40
|
+
});
|
package/tests/helpers.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { assertPathAccess } from "../src/
|
|
2
|
+
import { assertPathAccess } from "../src/VirtualShell/commands/helpers";
|
|
3
3
|
|
|
4
4
|
describe("assertPathAccess", () => {
|
|
5
5
|
test("blocks non-root access to auth store", () => {
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import type { CommandContext, ShellModule } from "../../types/commands";
|
|
2
|
-
import { runCommand } from "./index";
|
|
3
|
-
|
|
4
|
-
/** Simple shell script executor with basic variable support */
|
|
5
|
-
export const shCommand: ShellModule = {
|
|
6
|
-
name: "sh",
|
|
7
|
-
params: ["-c <script>", "[<file>]"],
|
|
8
|
-
aliases: ["bash"],
|
|
9
|
-
run: async (ctx: CommandContext) => {
|
|
10
|
-
const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
|
|
11
|
-
|
|
12
|
-
// Handle -c option: sh -c "command"
|
|
13
|
-
if (args[0] === "-c" && args.length >= 2) {
|
|
14
|
-
const script = args[1] ?? "";
|
|
15
|
-
if (!script) {
|
|
16
|
-
return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
17
|
-
}
|
|
18
|
-
const scriptArgs = args.slice(2);
|
|
19
|
-
|
|
20
|
-
// Split by semicolon and newline
|
|
21
|
-
const lines = script
|
|
22
|
-
.split(/[;\n]/)
|
|
23
|
-
.map((line) => line.trim())
|
|
24
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
25
|
-
|
|
26
|
-
let output = "";
|
|
27
|
-
let exitCode = 0;
|
|
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);
|
|
35
|
-
}
|
|
36
|
-
command = command.replaceAll("$@", scriptArgs.join(" "));
|
|
37
|
-
|
|
38
|
-
// Execute the command
|
|
39
|
-
const result = await Promise.resolve(
|
|
40
|
-
runCommand(command, authUser, hostname, users, mode, cwd, vfs),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
if (result.stdout) {
|
|
44
|
-
output += `${result.stdout}\n`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (result.stderr) {
|
|
48
|
-
return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
exitCode = result.exitCode ?? 0;
|
|
52
|
-
if (exitCode !== 0) {
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
stdout: output.trimEnd(),
|
|
59
|
-
exitCode,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Handle script file execution: sh <file>
|
|
64
|
-
if (args.length > 0 && args[0]) {
|
|
65
|
-
try {
|
|
66
|
-
const scriptFile = args[0];
|
|
67
|
-
const content = vfs.readFile(scriptFile);
|
|
68
|
-
const scriptArgs = args.slice(1);
|
|
69
|
-
|
|
70
|
-
// Split by newline
|
|
71
|
-
const lines = content
|
|
72
|
-
.split("\n")
|
|
73
|
-
.map((line) => line.trim())
|
|
74
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
75
|
-
|
|
76
|
-
let output = "";
|
|
77
|
-
let exitCode = 0;
|
|
78
|
-
|
|
79
|
-
for (const line of lines) {
|
|
80
|
-
// Simple variable substitution
|
|
81
|
-
let command = line;
|
|
82
|
-
for (let i = 0; i < scriptArgs.length; i++) {
|
|
83
|
-
const arg = scriptArgs[i] ?? "";
|
|
84
|
-
command = command.replaceAll(`$${i}`, arg);
|
|
85
|
-
}
|
|
86
|
-
command = command.replaceAll("$@", scriptArgs.join(" "));
|
|
87
|
-
|
|
88
|
-
// Execute the command
|
|
89
|
-
const result = await Promise.resolve(
|
|
90
|
-
runCommand(command, authUser, hostname, users, mode, cwd, vfs),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
if (result.stdout) {
|
|
94
|
-
output += `${result.stdout}\n`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (result.stderr) {
|
|
98
|
-
return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
exitCode = result.exitCode ?? 0;
|
|
102
|
-
if (exitCode !== 0) {
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
stdout: output.trimEnd(),
|
|
109
|
-
exitCode,
|
|
110
|
-
};
|
|
111
|
-
} catch {
|
|
112
|
-
return {
|
|
113
|
-
stderr: `sh: ${args[0]}: No such file or directory`,
|
|
114
|
-
exitCode: 1,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { stderr: "sh: missing operand or script", exitCode: 1 };
|
|
120
|
-
},
|
|
121
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|