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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
type ArgParseOptions = {
|
|
2
|
+
flags?: string[];
|
|
3
|
+
flagsWithValue?: string[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
function toFlagList(flags: string | string[]): string[] {
|
|
7
|
+
return Array.isArray(flags) ? flags : [flags];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function matchFlagToken(
|
|
11
|
+
token: string,
|
|
12
|
+
flag: string,
|
|
13
|
+
): { matched: boolean; inlineValue: string | null } {
|
|
14
|
+
if (token === flag) {
|
|
15
|
+
return { matched: true, inlineValue: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const prefix = `${flag}=`;
|
|
19
|
+
if (token.startsWith(prefix)) {
|
|
20
|
+
return { matched: true, inlineValue: token.slice(prefix.length) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { matched: false, inlineValue: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectPositionals(
|
|
27
|
+
args: string[],
|
|
28
|
+
options: ArgParseOptions = {},
|
|
29
|
+
): string[] {
|
|
30
|
+
const boolFlags = new Set(options.flags ?? []);
|
|
31
|
+
const valueFlags = new Set(options.flagsWithValue ?? []);
|
|
32
|
+
const positionals: string[] = [];
|
|
33
|
+
let passthrough = false;
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
36
|
+
const arg = args[index]!;
|
|
37
|
+
|
|
38
|
+
if (passthrough) {
|
|
39
|
+
positionals.push(arg);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (arg === "--") {
|
|
44
|
+
passthrough = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let consumed = false;
|
|
49
|
+
|
|
50
|
+
for (const flag of boolFlags) {
|
|
51
|
+
const { matched } = matchFlagToken(arg, flag);
|
|
52
|
+
if (matched) {
|
|
53
|
+
consumed = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (consumed) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const flag of valueFlags) {
|
|
63
|
+
const match = matchFlagToken(arg, flag);
|
|
64
|
+
if (!match.matched) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
consumed = true;
|
|
69
|
+
if (match.inlineValue === null && index + 1 < args.length) {
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!consumed) {
|
|
76
|
+
positionals.push(arg);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return positionals;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function ifFlag(args: string[], flags: string | string[]): boolean {
|
|
84
|
+
const allFlags = toFlagList(flags);
|
|
85
|
+
|
|
86
|
+
for (const arg of args) {
|
|
87
|
+
for (const flag of allFlags) {
|
|
88
|
+
if (matchFlagToken(arg, flag).matched) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getFlag(
|
|
98
|
+
args: string[],
|
|
99
|
+
flags: string | string[],
|
|
100
|
+
): string | true | undefined {
|
|
101
|
+
const allFlags = toFlagList(flags);
|
|
102
|
+
|
|
103
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
104
|
+
const arg = args[index]!;
|
|
105
|
+
|
|
106
|
+
for (const flag of allFlags) {
|
|
107
|
+
const match = matchFlagToken(arg, flag);
|
|
108
|
+
if (!match.matched) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (match.inlineValue !== null) {
|
|
113
|
+
return match.inlineValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const next = args[index + 1];
|
|
117
|
+
if (next !== undefined && next !== "--") {
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getArg(
|
|
129
|
+
args: string[],
|
|
130
|
+
index: number,
|
|
131
|
+
options: ArgParseOptions = {},
|
|
132
|
+
): string | undefined {
|
|
133
|
+
const positionals = collectPositionals(args, options);
|
|
134
|
+
return positionals[index];
|
|
135
|
+
}
|
|
@@ -1,43 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
import { getArg, getFlag, ifFlag } from "./command-helpers";
|
|
3
4
|
import {
|
|
4
5
|
assertPathAccess,
|
|
5
6
|
normalizeTerminalOutput,
|
|
6
7
|
resolvePath,
|
|
7
8
|
} from "./helpers";
|
|
8
9
|
|
|
9
|
-
function parseCurlOutputPath(args: string[]): {
|
|
10
|
-
outputPath: string | null;
|
|
11
|
-
inputArgs: string[];
|
|
12
|
-
} {
|
|
13
|
-
const filtered: string[] = [];
|
|
14
|
-
let outputPath: string | null = null;
|
|
15
|
-
|
|
16
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
17
|
-
const arg = args[index]!;
|
|
18
|
-
|
|
19
|
-
if (arg === "-o" || arg === "--output") {
|
|
20
|
-
outputPath = args[index + 1] ?? null;
|
|
21
|
-
index += 1;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (arg.startsWith("-o=")) {
|
|
26
|
-
outputPath = arg.slice(3);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (arg.startsWith("--output=")) {
|
|
31
|
-
outputPath = arg.slice("--output=".length);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
filtered.push(arg);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { outputPath, inputArgs: filtered };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
10
|
function runHostCurl(args: string[]): Promise<{
|
|
42
11
|
stdout: string;
|
|
43
12
|
stderr: string;
|
|
@@ -110,12 +79,22 @@ export const curlCommand: ShellModule = {
|
|
|
110
79
|
name: "curl",
|
|
111
80
|
params: ["[-o file] <url>"],
|
|
112
81
|
run: async ({ authUser, vfs, cwd, args }) => {
|
|
113
|
-
const
|
|
82
|
+
const outputPathValue = getFlag(args, ["-o", "--output"]);
|
|
83
|
+
const outputPath =
|
|
84
|
+
typeof outputPathValue === "string" && outputPathValue.length > 0
|
|
85
|
+
? outputPathValue
|
|
86
|
+
: null;
|
|
87
|
+
const parserOptions = { flagsWithValue: ["-o", "--output"] };
|
|
88
|
+
const inputArgs: string[] = [];
|
|
89
|
+
for (let index = 0; ; index += 1) {
|
|
90
|
+
const arg = getArg(args, index, parserOptions);
|
|
91
|
+
if (!arg) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
inputArgs.push(arg);
|
|
95
|
+
}
|
|
114
96
|
const url = inputArgs[0];
|
|
115
|
-
const isHelpLike =
|
|
116
|
-
(arg) =>
|
|
117
|
-
arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version",
|
|
118
|
-
);
|
|
97
|
+
const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
|
|
119
98
|
|
|
120
99
|
if (!url) {
|
|
121
100
|
return { stderr: "curl: missing URL", exitCode: 1 };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
2
3
|
import { getAllEnvVars } from "./set";
|
|
3
4
|
|
|
4
5
|
function expandEnvVars(input: string, env: Record<string, string>): string {
|
|
@@ -11,8 +12,15 @@ export const echoCommand: ShellModule = {
|
|
|
11
12
|
name: "echo",
|
|
12
13
|
params: ["[options] [text...]"],
|
|
13
14
|
run: ({ args, authUser, stdin }) => {
|
|
14
|
-
const newline = !args
|
|
15
|
-
const filteredArgs =
|
|
15
|
+
const newline = !ifFlag(args, "-n");
|
|
16
|
+
const filteredArgs: string[] = [];
|
|
17
|
+
for (let index = 0; ; index += 1) {
|
|
18
|
+
const value = getArg(args, index, { flags: ["-n"] });
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
filteredArgs.push(value);
|
|
23
|
+
}
|
|
16
24
|
const env = getAllEnvVars(authUser);
|
|
17
25
|
const rawText =
|
|
18
26
|
filteredArgs.length > 0 ? filteredArgs.join(" ") : (stdin ?? "");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
import { getEnvVar, setEnvVar } from "./set";
|
|
3
4
|
|
|
4
5
|
export const exportCommand: ShellModule = {
|
|
@@ -15,7 +16,12 @@ export const exportCommand: ShellModule = {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// Parse VAR=value format
|
|
18
|
-
for (
|
|
19
|
+
for (let index = 0; ; index += 1) {
|
|
20
|
+
const arg = getArg(args, index);
|
|
21
|
+
if (!arg) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
if (arg.includes("=")) {
|
|
20
26
|
const [varName, varValue] = arg.split("=", 2);
|
|
21
27
|
if (varName && varValue !== undefined) {
|
|
@@ -1,25 +1,32 @@
|
|
|
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 grepCommand: ShellModule = {
|
|
5
6
|
name: "grep",
|
|
6
7
|
params: ["[-i] [-v] <pattern> [file...]"],
|
|
7
8
|
run: ({ authUser, vfs, cwd, args, stdin }) => {
|
|
8
|
-
const caseInsensitive = args
|
|
9
|
-
const invertMatch = args
|
|
10
|
-
const
|
|
9
|
+
const caseInsensitive = ifFlag(args, "-i");
|
|
10
|
+
const invertMatch = ifFlag(args, "-v");
|
|
11
|
+
const parserOptions = { flags: ["-i", "-v"] };
|
|
12
|
+
const pattern = getArg(args, 0, parserOptions);
|
|
13
|
+
const files: string[] = [];
|
|
14
|
+
for (let index = 1; ; index += 1) {
|
|
15
|
+
const file = getArg(args, index, parserOptions);
|
|
16
|
+
if (!file) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
files.push(file);
|
|
20
|
+
}
|
|
11
21
|
|
|
12
|
-
if (
|
|
22
|
+
if (!pattern) {
|
|
13
23
|
return { stderr: "grep: no pattern specified", exitCode: 1 };
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
const pattern = filteredArgs[0];
|
|
17
|
-
const files = filteredArgs.slice(1);
|
|
18
|
-
|
|
19
26
|
let regex: RegExp;
|
|
20
27
|
try {
|
|
21
28
|
const flags = caseInsensitive ? "gmi" : "gm";
|
|
22
|
-
regex = new RegExp(pattern
|
|
29
|
+
regex = new RegExp(pattern, flags);
|
|
23
30
|
} catch {
|
|
24
31
|
return { stderr: `grep: invalid regex: ${pattern}`, exitCode: 1 };
|
|
25
32
|
}
|
|
@@ -58,43 +58,6 @@ export function assertPathAccess(
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function parseOutputPath(args: string[]): {
|
|
62
|
-
outputPath: string | null;
|
|
63
|
-
inputArgs: string[];
|
|
64
|
-
} {
|
|
65
|
-
const filtered: string[] = [];
|
|
66
|
-
let outputPath: string | null = null;
|
|
67
|
-
|
|
68
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
69
|
-
const arg = args[index]!;
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
arg === "-o" ||
|
|
73
|
-
arg === "-O" ||
|
|
74
|
-
arg === "--output" ||
|
|
75
|
-
arg === "--output-document"
|
|
76
|
-
) {
|
|
77
|
-
outputPath = args[index + 1] ?? null;
|
|
78
|
-
index += 1;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (arg.startsWith("-o=")) {
|
|
83
|
-
outputPath = arg.slice(3);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (arg.startsWith("-O=")) {
|
|
88
|
-
outputPath = arg.slice(3);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
filtered.push(arg);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { outputPath, inputArgs: filtered };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
61
|
export function stripUrlFilename(url: string): string {
|
|
99
62
|
const cleaned = url.split("?")[0]?.split("#")[0] ?? url;
|
|
100
63
|
const lastPart = cleaned.split("/").filter(Boolean).pop();
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { ShellProperties } from "..";
|
|
2
|
+
import type { VirtualUserManager } from "../../SSHMimic/users";
|
|
1
3
|
import type {
|
|
4
|
+
CommandContext,
|
|
2
5
|
CommandMode,
|
|
3
6
|
CommandOutcome,
|
|
4
7
|
CommandResult,
|
|
5
8
|
ShellModule,
|
|
6
9
|
} from "../../types/commands";
|
|
7
10
|
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
8
|
-
import type { VirtualUserManager } from "../users";
|
|
9
11
|
import { adduserCommand } from "./adduser";
|
|
10
12
|
import { catCommand } from "./cat";
|
|
11
13
|
import { cdCommand } from "./cd";
|
|
@@ -23,6 +25,7 @@ import { htopCommand } from "./htop";
|
|
|
23
25
|
import { lsCommand } from "./ls";
|
|
24
26
|
import { mkdirCommand } from "./mkdir";
|
|
25
27
|
import { nanoCommand } from "./nano";
|
|
28
|
+
import { neofetchCommand } from "./neofetch";
|
|
26
29
|
import { pwdCommand } from "./pwd";
|
|
27
30
|
import { rmCommand } from "./rm";
|
|
28
31
|
import { setCommand } from "./set";
|
|
@@ -50,6 +53,7 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
50
53
|
rmCommand,
|
|
51
54
|
treeCommand,
|
|
52
55
|
nanoCommand,
|
|
56
|
+
neofetchCommand,
|
|
53
57
|
htopCommand,
|
|
54
58
|
adduserCommand,
|
|
55
59
|
deluserCommand,
|
|
@@ -67,18 +71,72 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
67
71
|
exitCommand,
|
|
68
72
|
];
|
|
69
73
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
const customCommands: ShellModule[] = [];
|
|
75
|
+
|
|
76
|
+
const helpCommand = createHelpCommand(() =>
|
|
77
|
+
getCommandModules().map((cmd) => cmd.name),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
function getCommandModules(): ShellModule[] {
|
|
81
|
+
return [...BASE_COMMANDS, ...customCommands, helpCommand];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getTakenCommandNames(modules: ShellModule[]): Set<string> {
|
|
85
|
+
const taken = new Set<string>();
|
|
86
|
+
for (const mod of modules) {
|
|
87
|
+
taken.add(mod.name);
|
|
88
|
+
for (const alias of mod.aliases ?? []) {
|
|
89
|
+
taken.add(alias);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return taken;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function registerCommand(module: ShellModule): void {
|
|
96
|
+
const normalized: ShellModule = {
|
|
97
|
+
...module,
|
|
98
|
+
name: module.name.trim().toLowerCase(),
|
|
99
|
+
aliases: module.aliases?.map((alias) => alias.trim().toLowerCase()),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const names = [normalized.name, ...(normalized.aliases ?? [])];
|
|
103
|
+
if (names.some((name) => name.length === 0 || /\s/.test(name))) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Command names and aliases must be non-empty and contain no spaces",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const takenNames = getTakenCommandNames(getCommandModules());
|
|
110
|
+
const conflict = names.find((name) => takenNames.has(name));
|
|
111
|
+
if (conflict) {
|
|
112
|
+
throw new Error(`Command '${conflict}' already exists`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
customCommands.push(normalized);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createCustomCommand(
|
|
119
|
+
name: string,
|
|
120
|
+
params: string[],
|
|
121
|
+
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
|
|
122
|
+
): ShellModule {
|
|
123
|
+
return {
|
|
124
|
+
name,
|
|
125
|
+
params,
|
|
126
|
+
run,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
74
129
|
|
|
75
130
|
export function getCommandNames(): string[] {
|
|
76
|
-
return
|
|
131
|
+
return getCommandModules().flatMap((cmd) => [
|
|
132
|
+
cmd.name,
|
|
133
|
+
...(cmd.aliases ?? []),
|
|
134
|
+
]);
|
|
77
135
|
}
|
|
78
136
|
|
|
79
137
|
function resolveModule(name: string): ShellModule | undefined {
|
|
80
138
|
const lowered = name.toLowerCase();
|
|
81
|
-
return
|
|
139
|
+
return getCommandModules().find(
|
|
82
140
|
(cmd) => cmd.name === lowered || cmd.aliases?.includes(lowered),
|
|
83
141
|
);
|
|
84
142
|
}
|
|
@@ -141,6 +199,7 @@ async function runCommandInternal(
|
|
|
141
199
|
users: VirtualUserManager,
|
|
142
200
|
mode: CommandMode,
|
|
143
201
|
cwd: string,
|
|
202
|
+
shellProps: ShellProperties,
|
|
144
203
|
vfs: VirtualFileSystem,
|
|
145
204
|
stdin?: string,
|
|
146
205
|
): Promise<CommandResult> {
|
|
@@ -152,7 +211,7 @@ async function runCommandInternal(
|
|
|
152
211
|
) {
|
|
153
212
|
// Use pipeline executor
|
|
154
213
|
const { parseShellPipeline } = await import("../shellParser");
|
|
155
|
-
const { executePipeline } = await import("
|
|
214
|
+
const { executePipeline } = await import("../../SSHMimic/executor");
|
|
156
215
|
|
|
157
216
|
const pipeline = parseShellPipeline(rawInput);
|
|
158
217
|
if (!pipeline.isValid) {
|
|
@@ -199,6 +258,7 @@ async function runCommandInternal(
|
|
|
199
258
|
rawInput,
|
|
200
259
|
mode,
|
|
201
260
|
args,
|
|
261
|
+
shellProps,
|
|
202
262
|
stdin,
|
|
203
263
|
cwd,
|
|
204
264
|
vfs,
|
|
@@ -218,6 +278,7 @@ export function runCommand(
|
|
|
218
278
|
users: VirtualUserManager,
|
|
219
279
|
mode: CommandMode,
|
|
220
280
|
cwd: string,
|
|
281
|
+
shellProps: ShellProperties,
|
|
221
282
|
vfs: VirtualFileSystem,
|
|
222
283
|
stdin?: string,
|
|
223
284
|
): CommandOutcome {
|
|
@@ -236,6 +297,7 @@ export function runCommand(
|
|
|
236
297
|
users,
|
|
237
298
|
mode,
|
|
238
299
|
cwd,
|
|
300
|
+
shellProps,
|
|
239
301
|
vfs,
|
|
240
302
|
stdin,
|
|
241
303
|
);
|
|
@@ -264,6 +326,7 @@ export function runCommand(
|
|
|
264
326
|
stdin,
|
|
265
327
|
cwd,
|
|
266
328
|
vfs,
|
|
329
|
+
shellProps,
|
|
267
330
|
});
|
|
268
331
|
} catch (error: unknown) {
|
|
269
332
|
const message = error instanceof Error ? error.message : "Command failed";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, joinListWithType, resolvePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
function formatPermissions(mode: number, isDirectory: boolean): string {
|
|
@@ -29,8 +30,8 @@ export const lsCommand: ShellModule = {
|
|
|
29
30
|
name: "ls",
|
|
30
31
|
params: ["[path]"],
|
|
31
32
|
run: ({ authUser, vfs, cwd, args }) => {
|
|
32
|
-
const longFormat = args
|
|
33
|
-
const targetArg = args
|
|
33
|
+
const longFormat = ifFlag(args, ["-l", "--long"]);
|
|
34
|
+
const targetArg = getArg(args, 0, { flags: ["-l", "--long"] });
|
|
34
35
|
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
35
36
|
assertPathAccess(authUser, target, "ls");
|
|
36
37
|
const items = vfs.list(target).filter((name) => !name.startsWith("."));
|
|
@@ -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);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildNeofetchOutput } from "../../../modules/neofetch";
|
|
2
|
+
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
import { ifFlag } from "./command-helpers";
|
|
4
|
+
import { getAllEnvVars } from "./set";
|
|
5
|
+
|
|
6
|
+
export const neofetchCommand: ShellModule = {
|
|
7
|
+
name: "neofetch",
|
|
8
|
+
params: ["[--off]"],
|
|
9
|
+
run: ({ args, authUser, hostname, shellProps }) => {
|
|
10
|
+
const env = getAllEnvVars(authUser);
|
|
11
|
+
|
|
12
|
+
if (ifFlag(args, "--help")) {
|
|
13
|
+
return {
|
|
14
|
+
stdout: "Usage: neofetch [--off]",
|
|
15
|
+
exitCode: 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (ifFlag(args, "--off")) {
|
|
20
|
+
return {
|
|
21
|
+
stdout: `${authUser}@${hostname}`,
|
|
22
|
+
exitCode: 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
stdout: buildNeofetchOutput({
|
|
28
|
+
user: authUser,
|
|
29
|
+
host: hostname,
|
|
30
|
+
shell: env.SHELL,
|
|
31
|
+
shellProps: shellProps,
|
|
32
|
+
terminal: env.TERM,
|
|
33
|
+
}),
|
|
34
|
+
exitCode: 0,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -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,68 @@
|
|
|
1
|
+
import { defaultShellProperties } from "..";
|
|
2
|
+
import type { CommandContext, ShellModule } from "../../types/commands";
|
|
3
|
+
import { getArg, getFlag } from "./command-helpers";
|
|
4
|
+
import { runCommand } from "./index";
|
|
5
|
+
|
|
6
|
+
/** Simple shell script executor with basic variable support */
|
|
7
|
+
export const shCommand: ShellModule = {
|
|
8
|
+
name: "sh",
|
|
9
|
+
params: ["-c <script>", "[<file>]"],
|
|
10
|
+
aliases: ["bash"],
|
|
11
|
+
run: async (ctx: CommandContext) => {
|
|
12
|
+
const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
|
|
13
|
+
|
|
14
|
+
// Handle -c option: sh -c "command"
|
|
15
|
+
if (getFlag(args, "-c") && args.length >= 2) {
|
|
16
|
+
const script = getArg(args, 1) ?? "";
|
|
17
|
+
if (!script) {
|
|
18
|
+
return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
19
|
+
}
|
|
20
|
+
const scriptArgs = args.slice(2);
|
|
21
|
+
|
|
22
|
+
// Split by semicolon and newline
|
|
23
|
+
const lines = script
|
|
24
|
+
.split(/[;\n]/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
27
|
+
|
|
28
|
+
let output = "";
|
|
29
|
+
const exitCode = 0;
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
// Simple variable substitution
|
|
33
|
+
let command = line;
|
|
34
|
+
for (let i = 0; i < scriptArgs.length; i++) {
|
|
35
|
+
const arg = scriptArgs[i] ?? "";
|
|
36
|
+
command = command.replaceAll(`$${i}`, arg);
|
|
37
|
+
}
|
|
38
|
+
command = command.replaceAll("$@", scriptArgs.join(" "));
|
|
39
|
+
|
|
40
|
+
// Execute the command
|
|
41
|
+
const result = await Promise.resolve(
|
|
42
|
+
runCommand(
|
|
43
|
+
command,
|
|
44
|
+
authUser,
|
|
45
|
+
hostname,
|
|
46
|
+
users,
|
|
47
|
+
mode,
|
|
48
|
+
cwd,
|
|
49
|
+
defaultShellProperties,
|
|
50
|
+
vfs,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (result.stdout) {
|
|
55
|
+
output += `${result.stdout}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (result.stderr) {
|
|
59
|
+
return { stderr: result.stderr, exitCode: result.exitCode ?? 1 };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { stdout: output.trim(), exitCode };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { stderr: "sh: invalid usage", exitCode: 1 };
|
|
67
|
+
},
|
|
68
|
+
};
|