typescript-virtual-container 1.0.4 → 1.0.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/CHANGELOG.md +11 -0
- package/README.md +50 -0
- package/modules/neofetch.ts +349 -0
- package/package.json +1 -1
- package/src/SSHMimic/client.ts +1 -1
- package/src/SSHMimic/exec.ts +3 -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 +71 -8
- package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
- package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
- package/src/VirtualShell/commands/neofetch.ts +37 -0
- package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
- package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
- package/src/VirtualShell/commands/sh.ts +68 -0
- package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
- package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +18 -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 +86 -0
- package/src/{SSHMimic → VirtualShell}/shell.ts +21 -14
- package/src/index.ts +8 -0
- package/src/standalone.ts +10 -1
- package/src/types/commands.ts +3 -0
- 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,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,6 @@
|
|
|
1
|
+
import { defaultShellProperties } from "..";
|
|
1
2
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
import { getArg, getFlag, ifFlag } from "./command-helpers";
|
|
2
4
|
import { runCommand } from "./index";
|
|
3
5
|
|
|
4
6
|
function parseSudoArgs(args: string[]): {
|
|
@@ -6,34 +8,23 @@ function parseSudoArgs(args: string[]): {
|
|
|
6
8
|
loginShell: boolean;
|
|
7
9
|
commandLine: string | null;
|
|
8
10
|
} {
|
|
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
|
-
}
|
|
11
|
+
const loginShell = ifFlag(args, "-i");
|
|
12
|
+
const targetUserValue = getFlag(args, ["-u", "--user"]);
|
|
13
|
+
const targetUser =
|
|
14
|
+
typeof targetUserValue === "string" && targetUserValue.length > 0
|
|
15
|
+
? targetUserValue
|
|
16
|
+
: "root";
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
const commandParts: string[] = [];
|
|
19
|
+
for (let index = 0; ; index += 1) {
|
|
20
|
+
const part = getArg(args, index, {
|
|
21
|
+
flags: ["-i", "-S"],
|
|
22
|
+
flagsWithValue: ["-u", "--user"],
|
|
23
|
+
});
|
|
24
|
+
if (!part) {
|
|
25
|
+
break;
|
|
34
26
|
}
|
|
35
|
-
|
|
36
|
-
commandParts.push(arg);
|
|
27
|
+
commandParts.push(part);
|
|
37
28
|
}
|
|
38
29
|
|
|
39
30
|
const commandLine = commandParts.length > 0 ? commandParts.join(" ") : null;
|
|
@@ -72,6 +63,7 @@ export const sudoCommand: ShellModule = {
|
|
|
72
63
|
users,
|
|
73
64
|
mode,
|
|
74
65
|
loginShell ? `/home/${effectiveUser}` : cwd,
|
|
66
|
+
defaultShellProperties,
|
|
75
67
|
vfs,
|
|
76
68
|
);
|
|
77
69
|
}
|
|
@@ -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,86 @@
|
|
|
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
|
+
export interface ShellProperties {
|
|
9
|
+
kernel: string;
|
|
10
|
+
os: "Fortune GNU/Linux x64";
|
|
11
|
+
arch: "x86_64";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const defaultShellProperties: ShellProperties = {
|
|
15
|
+
kernel: "1.0.0+itsrealfortune+1-amd64",
|
|
16
|
+
os: "Fortune GNU/Linux x64",
|
|
17
|
+
arch: "x86_64",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class VirtualShell {
|
|
21
|
+
private vfs: VirtualFileSystem;
|
|
22
|
+
private users: VirtualUserManager;
|
|
23
|
+
private hostname: string;
|
|
24
|
+
public properties: ShellProperties;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
vfs: VirtualFileSystem,
|
|
28
|
+
users: VirtualUserManager,
|
|
29
|
+
hostname: string,
|
|
30
|
+
properties?: ShellProperties,
|
|
31
|
+
) {
|
|
32
|
+
this.vfs = vfs;
|
|
33
|
+
this.users = users;
|
|
34
|
+
this.hostname = hostname;
|
|
35
|
+
this.properties = properties || defaultShellProperties;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
addCommand(
|
|
39
|
+
name: string,
|
|
40
|
+
params: string[],
|
|
41
|
+
callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
|
|
42
|
+
): void {
|
|
43
|
+
const normalized = name.trim().toLowerCase();
|
|
44
|
+
if (normalized.length === 0 || /\s/.test(normalized)) {
|
|
45
|
+
throw new Error("Command name must be non-empty and contain no spaces");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
registerCommand(createCustomCommand(normalized, params, callback));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
executeCommand(rawInput: string, authUser: string, cwd: string): void {
|
|
52
|
+
runCommand(
|
|
53
|
+
rawInput,
|
|
54
|
+
authUser,
|
|
55
|
+
this.hostname,
|
|
56
|
+
this.users,
|
|
57
|
+
"shell",
|
|
58
|
+
cwd,
|
|
59
|
+
this.properties,
|
|
60
|
+
this.vfs,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
startInteractiveSession(
|
|
65
|
+
stream: ShellStream,
|
|
66
|
+
authUser: string,
|
|
67
|
+
sessionId: string | null,
|
|
68
|
+
remoteAddress: string,
|
|
69
|
+
terminalSize: { cols: number; rows: number },
|
|
70
|
+
): void {
|
|
71
|
+
// Interactive shell logic
|
|
72
|
+
startShell(
|
|
73
|
+
this.properties,
|
|
74
|
+
stream,
|
|
75
|
+
authUser,
|
|
76
|
+
this.vfs!,
|
|
77
|
+
this.hostname,
|
|
78
|
+
this.users!,
|
|
79
|
+
sessionId,
|
|
80
|
+
remoteAddress,
|
|
81
|
+
terminalSize,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { VirtualShell };
|
|
@@ -1,12 +1,13 @@
|
|
|
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 { defaultShellProperties, type ShellProperties } from ".";
|
|
5
|
+
import { formatLoginDate } from "../SSHMimic/loginFormat";
|
|
6
|
+
import { buildPrompt } from "../SSHMimic/prompt";
|
|
7
|
+
import type { VirtualUserManager } from "../SSHMimic/users";
|
|
4
8
|
import type { ShellStream } from "../types/streams";
|
|
5
9
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
6
10
|
import { getCommandNames, runCommand } from "./commands";
|
|
7
|
-
import { formatLoginDate } from "./loginFormat";
|
|
8
|
-
import { buildPrompt } from "./prompt";
|
|
9
|
-
import type { VirtualUserManager } from "./users";
|
|
10
11
|
|
|
11
12
|
interface NanoSession {
|
|
12
13
|
kind: "nano" | "htop";
|
|
@@ -41,6 +42,7 @@ function toTtyLines(text: string): string {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function startShell(
|
|
45
|
+
properties: ShellProperties,
|
|
44
46
|
stream: ShellStream,
|
|
45
47
|
authUser: string,
|
|
46
48
|
vfs: VirtualFileSystem,
|
|
@@ -180,6 +182,7 @@ export function startShell(
|
|
|
180
182
|
users,
|
|
181
183
|
"shell",
|
|
182
184
|
runCwd,
|
|
185
|
+
defaultShellProperties,
|
|
183
186
|
vfs,
|
|
184
187
|
),
|
|
185
188
|
);
|
|
@@ -467,20 +470,15 @@ export function startShell(
|
|
|
467
470
|
}
|
|
468
471
|
|
|
469
472
|
function renderLoginBanner(): void {
|
|
470
|
-
// const kernel = os.release();
|
|
471
|
-
// const arch = os.arch();
|
|
472
|
-
|
|
473
|
-
// Our own kernel and arch strings to avoid leaking host info and to provide a more "Linux-like" feel
|
|
474
|
-
const kernel = "5.15.0-1051-azure";
|
|
475
|
-
const arch = "x86_64";
|
|
476
|
-
|
|
477
473
|
const last = readLastLogin();
|
|
478
474
|
const nowIso = new Date().toISOString();
|
|
479
475
|
|
|
480
|
-
stream.write(
|
|
476
|
+
stream.write(
|
|
477
|
+
`Linux ${hostname} ${properties.kernel} ${properties.arch}\r\n`,
|
|
478
|
+
);
|
|
481
479
|
stream.write("\r\n");
|
|
482
480
|
stream.write(
|
|
483
|
-
"The programs included with the
|
|
481
|
+
"The programs included with the Fortune GNU/Linux system are free software;\r\n",
|
|
484
482
|
);
|
|
485
483
|
stream.write(
|
|
486
484
|
"the exact distribution terms for each program are described in the\r\n",
|
|
@@ -488,7 +486,7 @@ export function startShell(
|
|
|
488
486
|
stream.write("individual files in /usr/share/doc/*/copyright.\r\n");
|
|
489
487
|
stream.write("\r\n");
|
|
490
488
|
stream.write(
|
|
491
|
-
"
|
|
489
|
+
"Fortune GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent\r\n",
|
|
492
490
|
);
|
|
493
491
|
stream.write("permitted by applicable law.\r\n");
|
|
494
492
|
|
|
@@ -649,7 +647,16 @@ export function startShell(
|
|
|
649
647
|
|
|
650
648
|
if (line.length > 0) {
|
|
651
649
|
const result = await Promise.resolve(
|
|
652
|
-
runCommand(
|
|
650
|
+
runCommand(
|
|
651
|
+
line,
|
|
652
|
+
authUser,
|
|
653
|
+
hostname,
|
|
654
|
+
users,
|
|
655
|
+
"shell",
|
|
656
|
+
cwd,
|
|
657
|
+
defaultShellProperties,
|
|
658
|
+
vfs,
|
|
659
|
+
),
|
|
653
660
|
);
|
|
654
661
|
|
|
655
662
|
pushHistory(line);
|
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);
|
package/src/types/commands.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
VirtualUserManager,
|
|
7
7
|
} from "../SSHMimic/users";
|
|
8
8
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
9
|
+
import type { ShellProperties } from "../VirtualShell";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Normalized command execution output.
|
|
@@ -76,6 +77,8 @@ export interface CommandContext {
|
|
|
76
77
|
mode: CommandMode;
|
|
77
78
|
/** Tokenized arguments excluding command name. */
|
|
78
79
|
args: string[];
|
|
80
|
+
/** Virtual shell instance. */
|
|
81
|
+
shellProps: ShellProperties;
|
|
79
82
|
/** Optional stdin payload (used by pipes/redirections). */
|
|
80
83
|
stdin?: string;
|
|
81
84
|
/** Current working directory for command execution. */
|
|
@@ -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
|