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.
@@ -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().split(/\s+/).filter(Boolean);
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
- vfs.mkdir(resolvePath(cwd, dir));
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
- vfs.remove(resolvePath(cwd, target), { recursive });
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 {