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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +50 -0
  3. package/modules/neofetch.ts +349 -0
  4. package/package.json +1 -1
  5. package/src/SSHMimic/client.ts +1 -1
  6. package/src/SSHMimic/exec.ts +3 -1
  7. package/src/SSHMimic/executor.ts +2 -2
  8. package/src/SSHMimic/index.ts +14 -18
  9. package/src/{VirtualFileSystem.ts → VirtualFileSystem/index.ts} +6 -15
  10. package/src/{SSHMimic → VirtualShell}/commands/cat.ts +2 -1
  11. package/src/VirtualShell/commands/command-helpers.ts +135 -0
  12. package/src/{SSHMimic → VirtualShell}/commands/curl.ts +16 -37
  13. package/src/{SSHMimic → VirtualShell}/commands/echo.ts +10 -2
  14. package/src/{SSHMimic → VirtualShell}/commands/export.ts +7 -1
  15. package/src/{SSHMimic → VirtualShell}/commands/grep.ts +15 -8
  16. package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
  17. package/src/{SSHMimic → VirtualShell}/commands/index.ts +71 -8
  18. package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
  19. package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
  20. package/src/VirtualShell/commands/neofetch.ts +37 -0
  21. package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
  22. package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
  23. package/src/VirtualShell/commands/sh.ts +68 -0
  24. package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
  25. package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +18 -26
  26. package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
  27. package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
  28. package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
  29. package/src/VirtualShell/index.ts +86 -0
  30. package/src/{SSHMimic → VirtualShell}/shell.ts +21 -14
  31. package/src/index.ts +8 -0
  32. package/src/standalone.ts +10 -1
  33. package/src/types/commands.ts +3 -0
  34. package/tests/command-helpers.test.ts +40 -0
  35. package/tests/helpers.test.ts +1 -1
  36. package/src/SSHMimic/commands/sh.ts +0 -121
  37. /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
  38. /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
  39. /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
  40. /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
  41. /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
  42. /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
  43. /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
  44. /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
  45. /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
  46. /package/src/{SSHMimic → VirtualShell}/commands/env.ts +0 -0
  47. /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
  48. /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
  49. /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
  50. /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
  51. /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
  52. /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
  53. /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
  54. /package/src/{SSHMimic → VirtualShell}/commands/unset.ts +0 -0
  55. /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
  56. /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 { outputPath, inputArgs } = parseCurlOutputPath(args);
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 = inputArgs.some(
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.includes("-n");
15
- const filteredArgs = args.filter((arg) => arg !== "-n");
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 (const arg of args) {
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.includes("-i");
9
- const invertMatch = args.includes("-v");
10
- const filteredArgs = args.filter((arg) => arg !== "-i" && arg !== "-v");
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 (filteredArgs.length === 0) {
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 as string, flags);
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 COMMANDS: ShellModule[] = [
71
- ...BASE_COMMANDS,
72
- createHelpCommand(() => COMMANDS.map((cmd) => cmd.name)),
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 COMMANDS.flatMap((cmd) => [cmd.name, ...(cmd.aliases ?? [])]);
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 COMMANDS.find(
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("../executor");
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.includes("-l") || args.includes("--long");
33
- const targetArg = args.find((arg) => !arg.startsWith("-"));
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 (const dir of args) {
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
- args.includes("-r") || args.includes("-rf") || args.includes("-fr");
14
- const targets = args.filter((arg) => !arg.startsWith("-"));
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 (const arg of args) {
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
+ };