typescript-virtual-container 1.0.1 → 1.0.4
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/.github/workflows/test-battery.yml +53 -6
- package/CHANGELOG.md +40 -0
- package/README.md +11 -29
- package/package.json +1 -1
- package/src/SSHMimic/commands/cat.ts +3 -2
- package/src/SSHMimic/commands/cd.ts +3 -2
- package/src/SSHMimic/commands/curl.ts +9 -3
- package/src/SSHMimic/commands/echo.ts +26 -0
- package/src/SSHMimic/commands/env.ts +22 -0
- package/src/SSHMimic/commands/export.ts +32 -0
- package/src/SSHMimic/commands/grep.ts +81 -0
- package/src/SSHMimic/commands/helpers.ts +26 -0
- package/src/SSHMimic/commands/index.ts +153 -1
- package/src/SSHMimic/commands/ls.ts +3 -2
- package/src/SSHMimic/commands/mkdir.ts +5 -3
- package/src/SSHMimic/commands/nano.ts +3 -2
- package/src/SSHMimic/commands/rm.ts +5 -3
- package/src/SSHMimic/commands/set.ts +67 -0
- package/src/SSHMimic/commands/sh.ts +121 -0
- package/src/SSHMimic/commands/touch.ts +3 -2
- package/src/SSHMimic/commands/tree.ts +3 -2
- package/src/SSHMimic/commands/unset.ts +19 -0
- package/src/SSHMimic/commands/wget.ts +3 -1
- package/src/SSHMimic/executor.ts +201 -0
- package/src/SSHMimic/index.ts +25 -1
- package/src/SSHMimic/shellParser.ts +203 -0
- package/src/SSHMimic/users.ts +16 -7
- package/src/types/commands.ts +2 -0
- package/src/types/pipeline.ts +23 -0
- package/tests/helpers.test.ts +22 -0
- package/tests/users.test.ts +41 -0
- package/tsconfig.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CommandMode,
|
|
3
3
|
CommandOutcome,
|
|
4
|
+
CommandResult,
|
|
4
5
|
ShellModule,
|
|
5
6
|
} from "../../types/commands";
|
|
6
7
|
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
@@ -11,7 +12,11 @@ import { cdCommand } from "./cd";
|
|
|
11
12
|
import { clearCommand } from "./clear";
|
|
12
13
|
import { curlCommand } from "./curl";
|
|
13
14
|
import { deluserCommand } from "./deluser";
|
|
15
|
+
import { echoCommand } from "./echo";
|
|
16
|
+
import { envCommand } from "./env";
|
|
14
17
|
import { exitCommand } from "./exit";
|
|
18
|
+
import { exportCommand } from "./export";
|
|
19
|
+
import { grepCommand } from "./grep";
|
|
15
20
|
import { createHelpCommand } from "./help";
|
|
16
21
|
import { hostnameCommand } from "./hostname";
|
|
17
22
|
import { htopCommand } from "./htop";
|
|
@@ -20,10 +25,13 @@ import { mkdirCommand } from "./mkdir";
|
|
|
20
25
|
import { nanoCommand } from "./nano";
|
|
21
26
|
import { pwdCommand } from "./pwd";
|
|
22
27
|
import { rmCommand } from "./rm";
|
|
28
|
+
import { setCommand } from "./set";
|
|
29
|
+
import { shCommand } from "./sh";
|
|
23
30
|
import { suCommand } from "./su";
|
|
24
31
|
import { sudoCommand } from "./sudo";
|
|
25
32
|
import { touchCommand } from "./touch";
|
|
26
33
|
import { treeCommand } from "./tree";
|
|
34
|
+
import { unsetCommand } from "./unset";
|
|
27
35
|
import { wgetCommand } from "./wget";
|
|
28
36
|
import { whoCommand } from "./who";
|
|
29
37
|
import { whoamiCommand } from "./whoami";
|
|
@@ -36,6 +44,7 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
36
44
|
lsCommand,
|
|
37
45
|
cdCommand,
|
|
38
46
|
catCommand,
|
|
47
|
+
echoCommand,
|
|
39
48
|
mkdirCommand,
|
|
40
49
|
touchCommand,
|
|
41
50
|
rmCommand,
|
|
@@ -47,7 +56,13 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
47
56
|
sudoCommand,
|
|
48
57
|
suCommand,
|
|
49
58
|
curlCommand,
|
|
59
|
+
envCommand,
|
|
50
60
|
wgetCommand,
|
|
61
|
+
grepCommand,
|
|
62
|
+
exportCommand,
|
|
63
|
+
setCommand,
|
|
64
|
+
unsetCommand,
|
|
65
|
+
shCommand,
|
|
51
66
|
clearCommand,
|
|
52
67
|
exitCommand,
|
|
53
68
|
];
|
|
@@ -68,14 +83,134 @@ function resolveModule(name: string): ShellModule | undefined {
|
|
|
68
83
|
);
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
function splitArgsRespectingQuotes(input: string): string[] {
|
|
87
|
+
const tokens: string[] = [];
|
|
88
|
+
let current = "";
|
|
89
|
+
let inQuotes = false;
|
|
90
|
+
let quoteChar = "";
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
93
|
+
const ch = input[i] || "";
|
|
94
|
+
const prev = i > 0 ? input[i - 1] : "";
|
|
95
|
+
|
|
96
|
+
if ((ch === '"' || ch === "'") && prev !== "\\") {
|
|
97
|
+
if (!inQuotes) {
|
|
98
|
+
inQuotes = true;
|
|
99
|
+
quoteChar = ch;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (ch === quoteChar) {
|
|
104
|
+
inQuotes = false;
|
|
105
|
+
quoteChar = "";
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (/\s/.test(ch) && !inQuotes) {
|
|
111
|
+
if (current.length > 0) {
|
|
112
|
+
tokens.push(current);
|
|
113
|
+
current = "";
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
current += ch;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (current.length > 0) {
|
|
122
|
+
tokens.push(current);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
71
128
|
function parseInput(rawInput: string): { commandName: string; args: string[] } {
|
|
72
|
-
const parts = rawInput.trim()
|
|
129
|
+
const parts = splitArgsRespectingQuotes(rawInput.trim());
|
|
73
130
|
return {
|
|
74
131
|
commandName: parts[0]?.toLowerCase() ?? "",
|
|
75
132
|
args: parts.slice(1),
|
|
76
133
|
};
|
|
77
134
|
}
|
|
78
135
|
|
|
136
|
+
// Internal async function for pipeline execution
|
|
137
|
+
async function runCommandInternal(
|
|
138
|
+
rawInput: string,
|
|
139
|
+
authUser: string,
|
|
140
|
+
hostname: string,
|
|
141
|
+
users: VirtualUserManager,
|
|
142
|
+
mode: CommandMode,
|
|
143
|
+
cwd: string,
|
|
144
|
+
vfs: VirtualFileSystem,
|
|
145
|
+
stdin?: string,
|
|
146
|
+
): Promise<CommandResult> {
|
|
147
|
+
// Check if input contains pipes or redirections
|
|
148
|
+
if (
|
|
149
|
+
rawInput.includes("|") ||
|
|
150
|
+
rawInput.includes(">") ||
|
|
151
|
+
rawInput.includes("<")
|
|
152
|
+
) {
|
|
153
|
+
// Use pipeline executor
|
|
154
|
+
const { parseShellPipeline } = await import("../shellParser");
|
|
155
|
+
const { executePipeline } = await import("../executor");
|
|
156
|
+
|
|
157
|
+
const pipeline = parseShellPipeline(rawInput);
|
|
158
|
+
if (!pipeline.isValid) {
|
|
159
|
+
return {
|
|
160
|
+
stderr: pipeline.error || "Syntax error",
|
|
161
|
+
exitCode: 1,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
return await executePipeline(
|
|
167
|
+
pipeline,
|
|
168
|
+
authUser,
|
|
169
|
+
hostname,
|
|
170
|
+
users,
|
|
171
|
+
mode,
|
|
172
|
+
cwd,
|
|
173
|
+
vfs,
|
|
174
|
+
);
|
|
175
|
+
} catch (error: unknown) {
|
|
176
|
+
const message =
|
|
177
|
+
error instanceof Error ? error.message : "Pipeline execution failed";
|
|
178
|
+
return { stderr: message, exitCode: 1 };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Regular command execution
|
|
183
|
+
const { commandName, args } = parseInput(rawInput);
|
|
184
|
+
const mod = resolveModule(commandName);
|
|
185
|
+
|
|
186
|
+
if (!mod) {
|
|
187
|
+
return {
|
|
188
|
+
stderr: `Command '${rawInput}' not found`,
|
|
189
|
+
exitCode: 127,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = mod.run({
|
|
195
|
+
authUser,
|
|
196
|
+
hostname,
|
|
197
|
+
users,
|
|
198
|
+
activeSessions: users.listActiveSessions(),
|
|
199
|
+
rawInput,
|
|
200
|
+
mode,
|
|
201
|
+
args,
|
|
202
|
+
stdin,
|
|
203
|
+
cwd,
|
|
204
|
+
vfs,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return await Promise.resolve(result);
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
const message = error instanceof Error ? error.message : "Command failed";
|
|
210
|
+
return { stderr: message, exitCode: 1 };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
79
214
|
export function runCommand(
|
|
80
215
|
rawInput: string,
|
|
81
216
|
authUser: string,
|
|
@@ -84,6 +219,7 @@ export function runCommand(
|
|
|
84
219
|
mode: CommandMode,
|
|
85
220
|
cwd: string,
|
|
86
221
|
vfs: VirtualFileSystem,
|
|
222
|
+
stdin?: string,
|
|
87
223
|
): CommandOutcome {
|
|
88
224
|
const trimmed = rawInput.trim();
|
|
89
225
|
|
|
@@ -91,6 +227,21 @@ export function runCommand(
|
|
|
91
227
|
return { exitCode: 0 };
|
|
92
228
|
}
|
|
93
229
|
|
|
230
|
+
// Check if input contains pipes or redirections - use async version
|
|
231
|
+
if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
|
|
232
|
+
return runCommandInternal(
|
|
233
|
+
trimmed,
|
|
234
|
+
authUser,
|
|
235
|
+
hostname,
|
|
236
|
+
users,
|
|
237
|
+
mode,
|
|
238
|
+
cwd,
|
|
239
|
+
vfs,
|
|
240
|
+
stdin,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Regular synchronous command execution
|
|
94
245
|
const { commandName, args } = parseInput(trimmed);
|
|
95
246
|
const mod = resolveModule(commandName);
|
|
96
247
|
|
|
@@ -110,6 +261,7 @@ export function runCommand(
|
|
|
110
261
|
rawInput: trimmed,
|
|
111
262
|
mode,
|
|
112
263
|
args,
|
|
264
|
+
stdin,
|
|
113
265
|
cwd,
|
|
114
266
|
vfs,
|
|
115
267
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import { joinListWithType, resolvePath } from "./helpers";
|
|
2
|
+
import { assertPathAccess, joinListWithType, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
function formatPermissions(mode: number, isDirectory: boolean): string {
|
|
5
5
|
const fileType = isDirectory ? "d" : "-";
|
|
@@ -28,10 +28,11 @@ function formatDate(date: Date): string {
|
|
|
28
28
|
export const lsCommand: ShellModule = {
|
|
29
29
|
name: "ls",
|
|
30
30
|
params: ["[path]"],
|
|
31
|
-
run: ({ vfs, cwd, args }) => {
|
|
31
|
+
run: ({ authUser, vfs, cwd, args }) => {
|
|
32
32
|
const longFormat = args.includes("-l") || args.includes("--long");
|
|
33
33
|
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
34
34
|
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
35
|
+
assertPathAccess(authUser, target, "ls");
|
|
35
36
|
const items = vfs.list(target).filter((name) => !name.startsWith("."));
|
|
36
37
|
const rendered = longFormat
|
|
37
38
|
? items
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import { resolvePath } from "./helpers";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
export const mkdirCommand: ShellModule = {
|
|
5
5
|
name: "mkdir",
|
|
6
6
|
params: ["<dir>"],
|
|
7
|
-
run: ({ vfs, cwd, args }) => {
|
|
7
|
+
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
8
|
if (args.length === 0) {
|
|
9
9
|
return { stderr: "mkdir: missing operand", exitCode: 1 };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
for (const dir of args) {
|
|
13
|
-
|
|
13
|
+
const target = resolvePath(cwd, dir);
|
|
14
|
+
assertPathAccess(authUser, target, "mkdir");
|
|
15
|
+
vfs.mkdir(target);
|
|
14
16
|
}
|
|
15
17
|
return { exitCode: 0 };
|
|
16
18
|
},
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
-
import { resolvePath } from "./helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
4
|
|
|
5
5
|
export const nanoCommand: ShellModule = {
|
|
6
6
|
name: "nano",
|
|
7
7
|
params: ["<file>"],
|
|
8
|
-
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
run: ({ authUser, vfs, cwd, args }) => {
|
|
9
9
|
const fileArg = args[0];
|
|
10
10
|
if (!fileArg) {
|
|
11
11
|
return { stderr: "nano: missing file operand", exitCode: 1 };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const targetPath = resolvePath(cwd, fileArg);
|
|
15
|
+
assertPathAccess(authUser, targetPath, "nano");
|
|
15
16
|
const initialContent = vfs.exists(targetPath)
|
|
16
17
|
? vfs.readFile(targetPath)
|
|
17
18
|
: "";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import { resolvePath } from "./helpers";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
export const rmCommand: ShellModule = {
|
|
5
5
|
name: "rm",
|
|
6
6
|
params: ["[-r|-rf] <path>"],
|
|
7
|
-
run: ({ vfs, cwd, args }) => {
|
|
7
|
+
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
8
|
if (args.length === 0) {
|
|
9
9
|
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
10
10
|
}
|
|
@@ -18,7 +18,9 @@ export const rmCommand: ShellModule = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
for (const target of targets) {
|
|
21
|
-
|
|
21
|
+
const resolvedTarget = resolvePath(cwd, target);
|
|
22
|
+
assertPathAccess(authUser, resolvedTarget, "rm");
|
|
23
|
+
vfs.remove(resolvedTarget, { recursive });
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
return { exitCode: 0 };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/useNamingConvention: env variables */
|
|
2
|
+
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
|
|
4
|
+
// Simple in-memory environment variables store
|
|
5
|
+
// In a real implementation, this would be per-session/per-user
|
|
6
|
+
const envVars: Record<string, string> = {
|
|
7
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
8
|
+
HOME: "/home/user",
|
|
9
|
+
SHELL: "/bin/sh",
|
|
10
|
+
TERM: "xterm-256color",
|
|
11
|
+
USER: "user",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getEnvVar(name: string): string | undefined {
|
|
15
|
+
return envVars[name];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setEnvVar(name: string, value: string): void {
|
|
19
|
+
envVars[name] = value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getAllEnvVars(authUser: string): Record<string, string> {
|
|
23
|
+
envVars.USER = authUser;
|
|
24
|
+
envVars.HOME = `/home/${authUser}`;
|
|
25
|
+
return { ...envVars };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const setCommand: ShellModule = {
|
|
29
|
+
name: "set",
|
|
30
|
+
params: ["[VAR=value]"],
|
|
31
|
+
run: ({ args }) => {
|
|
32
|
+
// No arguments: display all environment variables
|
|
33
|
+
if (args.length === 0) {
|
|
34
|
+
const output = Object.entries(envVars)
|
|
35
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
36
|
+
.sort()
|
|
37
|
+
.join("\n");
|
|
38
|
+
|
|
39
|
+
return { stdout: output, exitCode: 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse VAR=value format
|
|
43
|
+
const assignments: string[] = [];
|
|
44
|
+
for (const arg of args) {
|
|
45
|
+
if (arg.includes("=")) {
|
|
46
|
+
const [varName, varValue] = arg.split("=", 2);
|
|
47
|
+
if (varName && varValue !== undefined) {
|
|
48
|
+
setEnvVar(varName, varValue);
|
|
49
|
+
assignments.push(arg);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// If no '=' present, display that specific variable
|
|
53
|
+
const value = getEnvVar(arg);
|
|
54
|
+
if (value !== undefined) {
|
|
55
|
+
assignments.push(`${arg}=${value}`);
|
|
56
|
+
} else {
|
|
57
|
+
assignments.push(`${arg}: not set`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
stdout: assignments.length > 0 ? assignments.join("\n") : "",
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
};
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import { resolvePath } from "./helpers";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
export const touchCommand: ShellModule = {
|
|
5
5
|
name: "touch",
|
|
6
6
|
params: ["<file>"],
|
|
7
|
-
run: ({ vfs, cwd, args }) => {
|
|
7
|
+
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
8
|
if (args.length === 0) {
|
|
9
9
|
return { stderr: "touch: missing file operand", exitCode: 1 };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
for (const file of args) {
|
|
13
13
|
const target = resolvePath(cwd, file);
|
|
14
|
+
assertPathAccess(authUser, target, "touch");
|
|
14
15
|
if (!vfs.exists(target)) {
|
|
15
16
|
vfs.writeFile(target, "");
|
|
16
17
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import { resolvePath } from "./helpers";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
4
|
export const treeCommand: ShellModule = {
|
|
5
5
|
name: "tree",
|
|
6
6
|
params: ["[path]"],
|
|
7
|
-
run: ({ vfs, cwd, args }) => {
|
|
7
|
+
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
8
|
const target = resolvePath(cwd, args[0] ?? cwd);
|
|
9
|
+
assertPathAccess(authUser, target, "tree");
|
|
9
10
|
return { stdout: vfs.tree(target), exitCode: 0 };
|
|
10
11
|
},
|
|
11
12
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { setEnvVar } from "./set";
|
|
3
|
+
|
|
4
|
+
export const unsetCommand: ShellModule = {
|
|
5
|
+
name: "unset",
|
|
6
|
+
params: ["<VAR...>"],
|
|
7
|
+
run: ({ args }) => {
|
|
8
|
+
if (args.length === 0) {
|
|
9
|
+
return { stderr: "unset: missing variable name", exitCode: 1 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Unset (remove) all specified variables
|
|
13
|
+
for (const varName of args) {
|
|
14
|
+
setEnvVar(varName, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { exitCode: 0 };
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import type { ShellModule } from "../../types/commands";
|
|
6
6
|
import {
|
|
7
|
+
assertPathAccess,
|
|
7
8
|
normalizeTerminalOutput,
|
|
8
9
|
parseOutputPath,
|
|
9
10
|
resolvePath,
|
|
@@ -81,7 +82,7 @@ function runHostWget(args: string[]): Promise<{
|
|
|
81
82
|
export const wgetCommand: ShellModule = {
|
|
82
83
|
name: "wget",
|
|
83
84
|
params: ["[url]"],
|
|
84
|
-
run: async ({ vfs, cwd, args }) => {
|
|
85
|
+
run: async ({ authUser, vfs, cwd, args }) => {
|
|
85
86
|
const { outputPath, inputArgs } = parseOutputPath(args);
|
|
86
87
|
const url = inputArgs[0];
|
|
87
88
|
const isHelpLike = inputArgs.some(
|
|
@@ -122,6 +123,7 @@ export const wgetCommand: ShellModule = {
|
|
|
122
123
|
|
|
123
124
|
const content = await readFile(tempFile, "utf8");
|
|
124
125
|
const target = resolvePath(cwd, outputPath ?? stripUrlFilename(url));
|
|
126
|
+
assertPathAccess(authUser, target, "wget");
|
|
125
127
|
vfs.writeFile(target, content);
|
|
126
128
|
|
|
127
129
|
return {
|