typescript-virtual-container 1.2.7 → 1.2.9

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 (130) hide show
  1. package/README.md +457 -42
  2. package/dist/SSHMimic/executor.js +3 -5
  3. package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
  4. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
  5. package/dist/VirtualFileSystem/binaryPack.js +193 -0
  6. package/dist/VirtualFileSystem/index.d.ts +7 -5
  7. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  8. package/dist/VirtualFileSystem/index.js +20 -9
  9. package/dist/VirtualPackageManager/index.d.ts +202 -0
  10. package/dist/VirtualPackageManager/index.d.ts.map +1 -0
  11. package/dist/VirtualPackageManager/index.js +676 -0
  12. package/dist/VirtualShell/index.d.ts +87 -12
  13. package/dist/VirtualShell/index.d.ts.map +1 -1
  14. package/dist/VirtualShell/index.js +83 -12
  15. package/dist/VirtualUserManager/index.d.ts +52 -20
  16. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  17. package/dist/VirtualUserManager/index.js +54 -20
  18. package/dist/commands/alias.d.ts +4 -0
  19. package/dist/commands/alias.d.ts.map +1 -0
  20. package/dist/commands/alias.js +58 -0
  21. package/dist/commands/apt.d.ts +4 -0
  22. package/dist/commands/apt.d.ts.map +1 -0
  23. package/dist/commands/apt.js +182 -0
  24. package/dist/commands/cat.d.ts.map +1 -1
  25. package/dist/commands/cat.js +27 -8
  26. package/dist/commands/chmod.d.ts.map +1 -1
  27. package/dist/commands/chmod.js +52 -3
  28. package/dist/commands/command-helpers.d.ts +78 -4
  29. package/dist/commands/command-helpers.d.ts.map +1 -1
  30. package/dist/commands/command-helpers.js +78 -4
  31. package/dist/commands/curl.d.ts.map +1 -1
  32. package/dist/commands/curl.js +81 -29
  33. package/dist/commands/dpkg.d.ts +4 -0
  34. package/dist/commands/dpkg.d.ts.map +1 -0
  35. package/dist/commands/dpkg.js +144 -0
  36. package/dist/commands/echo.d.ts.map +1 -1
  37. package/dist/commands/echo.js +24 -12
  38. package/dist/commands/free.d.ts +3 -0
  39. package/dist/commands/free.d.ts.map +1 -0
  40. package/dist/commands/free.js +38 -0
  41. package/dist/commands/helpers.d.ts +3 -0
  42. package/dist/commands/helpers.d.ts.map +1 -1
  43. package/dist/commands/helpers.js +3 -0
  44. package/dist/commands/history.d.ts +3 -0
  45. package/dist/commands/history.d.ts.map +1 -0
  46. package/dist/commands/history.js +21 -0
  47. package/dist/commands/index.d.ts +8 -1
  48. package/dist/commands/index.d.ts.map +1 -1
  49. package/dist/commands/index.js +120 -11
  50. package/dist/commands/ls.d.ts.map +1 -1
  51. package/dist/commands/ls.js +4 -3
  52. package/dist/commands/lsb-release.d.ts +3 -0
  53. package/dist/commands/lsb-release.d.ts.map +1 -0
  54. package/dist/commands/lsb-release.js +50 -0
  55. package/dist/commands/man.d.ts +3 -0
  56. package/dist/commands/man.d.ts.map +1 -0
  57. package/dist/commands/man.js +155 -0
  58. package/dist/commands/neofetch.d.ts.map +1 -1
  59. package/dist/commands/neofetch.js +5 -0
  60. package/dist/commands/ping.d.ts.map +1 -1
  61. package/dist/commands/ping.js +5 -2
  62. package/dist/commands/ps.d.ts.map +1 -1
  63. package/dist/commands/ps.js +27 -6
  64. package/dist/commands/sh.d.ts.map +1 -1
  65. package/dist/commands/sh.js +29 -11
  66. package/dist/commands/source.d.ts +3 -0
  67. package/dist/commands/source.d.ts.map +1 -0
  68. package/dist/commands/source.js +31 -0
  69. package/dist/commands/test.d.ts +3 -0
  70. package/dist/commands/test.d.ts.map +1 -0
  71. package/dist/commands/test.js +92 -0
  72. package/dist/commands/type.d.ts +3 -0
  73. package/dist/commands/type.d.ts.map +1 -0
  74. package/dist/commands/type.js +34 -0
  75. package/dist/commands/uptime.d.ts +3 -0
  76. package/dist/commands/uptime.d.ts.map +1 -0
  77. package/dist/commands/uptime.js +40 -0
  78. package/dist/commands/wget.d.ts.map +1 -1
  79. package/dist/commands/wget.js +71 -100
  80. package/dist/commands/which.d.ts +3 -0
  81. package/dist/commands/which.d.ts.map +1 -0
  82. package/dist/commands/which.js +32 -0
  83. package/dist/index.d.ts +5 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -1
  86. package/dist/modules/linuxRootfs.d.ts +24 -0
  87. package/dist/modules/linuxRootfs.d.ts.map +1 -0
  88. package/dist/modules/linuxRootfs.js +297 -0
  89. package/dist/modules/neofetch.d.ts.map +1 -1
  90. package/dist/modules/neofetch.js +1 -0
  91. package/dist/standalone.js +4 -1
  92. package/package.json +2 -1
  93. package/src/SSHMimic/executor.ts +3 -5
  94. package/src/VirtualFileSystem/binaryPack.ts +219 -0
  95. package/src/VirtualFileSystem/index.ts +21 -11
  96. package/src/VirtualPackageManager/index.ts +820 -0
  97. package/src/VirtualShell/index.ts +104 -13
  98. package/src/VirtualUserManager/index.ts +55 -20
  99. package/src/commands/alias.ts +60 -0
  100. package/src/commands/apt.ts +198 -0
  101. package/src/commands/cat.ts +32 -8
  102. package/src/commands/chmod.ts +48 -3
  103. package/src/commands/command-helpers.ts +78 -4
  104. package/src/commands/curl.ts +78 -37
  105. package/src/commands/dpkg.ts +158 -0
  106. package/src/commands/echo.ts +30 -14
  107. package/src/commands/free.ts +40 -0
  108. package/src/commands/helpers.ts +8 -0
  109. package/src/commands/history.ts +29 -0
  110. package/src/commands/index.ts +116 -11
  111. package/src/commands/ls.ts +5 -4
  112. package/src/commands/lsb-release.ts +52 -0
  113. package/src/commands/man.ts +166 -0
  114. package/src/commands/neofetch.ts +5 -0
  115. package/src/commands/ping.ts +5 -2
  116. package/src/commands/ps.ts +28 -6
  117. package/src/commands/sh.ts +33 -11
  118. package/src/commands/source.ts +35 -0
  119. package/src/commands/test.ts +100 -0
  120. package/src/commands/type.ts +40 -0
  121. package/src/commands/uptime.ts +46 -0
  122. package/src/commands/wget.ts +70 -123
  123. package/src/commands/which.ts +34 -0
  124. package/src/index.ts +10 -0
  125. package/src/modules/linuxRootfs.ts +439 -0
  126. package/src/modules/neofetch.ts +1 -0
  127. package/src/standalone.ts +4 -1
  128. package/standalone.js +418 -103
  129. package/standalone.js.map +4 -4
  130. package/tests/new-features.test.ts +626 -0
@@ -0,0 +1,100 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ /**
4
+ * Evaluate a POSIX test expression.
5
+ * Supports: -f, -d, -e, -r, -w, -x, -s, -z, -n,
6
+ * string =, !=, numeric -eq -ne -lt -le -gt -ge,
7
+ * ! (negate), -a (and), -o (or).
8
+ */
9
+ function evalTest(tokens: string[], shell: import("../VirtualShell").VirtualShell, cwd: string): boolean {
10
+ // When called via [ command, ] is the last arg — strip it
11
+ // When called via test command, no brackets present
12
+ if (tokens[tokens.length - 1] === "]") {
13
+ tokens = tokens.slice(0, -1);
14
+ }
15
+ // Also strip leading [ if present (shouldn't normally happen but be safe)
16
+ if (tokens[0] === "[") {
17
+ tokens = tokens.slice(1);
18
+ }
19
+
20
+ if (tokens.length === 0) return false;
21
+
22
+ // Negation
23
+ if (tokens[0] === "!") return !evalTest(tokens.slice(1), shell, cwd);
24
+
25
+ // Boolean -a / -o (simple left-right, no precedence)
26
+ const andIdx = tokens.indexOf("-a");
27
+ if (andIdx !== -1) {
28
+ return evalTest(tokens.slice(0, andIdx), shell, cwd) &&
29
+ evalTest(tokens.slice(andIdx + 1), shell, cwd);
30
+ }
31
+ const orIdx = tokens.indexOf("-o");
32
+ if (orIdx !== -1) {
33
+ return evalTest(tokens.slice(0, orIdx), shell, cwd) ||
34
+ evalTest(tokens.slice(orIdx + 1), shell, cwd);
35
+ }
36
+
37
+ // Unary file tests
38
+ if (tokens.length === 2) {
39
+ const [flag, operand = ""] = tokens;
40
+ const resolvePath = (p: string) => p.startsWith("/") ? p : `${cwd}/${p}`.replace(/\/+/g, "/");
41
+ const path = resolvePath(operand);
42
+
43
+ switch (flag) {
44
+ case "-e": return shell.vfs.exists(path);
45
+ case "-f": return shell.vfs.exists(path) && shell.vfs.stat(path).type === "file";
46
+ case "-d": return shell.vfs.exists(path) && shell.vfs.stat(path).type === "directory";
47
+ case "-r": return shell.vfs.exists(path); // all readable in virtual env
48
+ case "-w": return shell.vfs.exists(path);
49
+ case "-x": return shell.vfs.exists(path) && !!(shell.vfs.stat(path).mode & 0o111);
50
+ case "-s": return shell.vfs.exists(path) && shell.vfs.stat(path).type === "file" && (shell.vfs.stat(path) as import("../types/vfs").VfsFileNode).size > 0;
51
+ case "-z": return operand.length === 0;
52
+ case "-n": return operand.length > 0;
53
+ case "-L": return shell.vfs.isSymlink(path);
54
+ }
55
+ }
56
+
57
+ // Binary comparisons
58
+ if (tokens.length === 3) {
59
+ const [left = "", op, right = ""] = tokens;
60
+ const leftN = Number(left);
61
+ const rightN = Number(right);
62
+
63
+ switch (op) {
64
+ // String
65
+ case "=":
66
+ case "==": return left === right;
67
+ case "!=": return left !== right;
68
+ case "<": return left < right;
69
+ case ">": return left > right;
70
+ // Numeric
71
+ case "-eq": return leftN === rightN;
72
+ case "-ne": return leftN !== rightN;
73
+ case "-lt": return leftN < rightN;
74
+ case "-le": return leftN <= rightN;
75
+ case "-gt": return leftN > rightN;
76
+ case "-ge": return leftN >= rightN;
77
+ }
78
+ }
79
+
80
+ // Single string (truthy if non-empty)
81
+ if (tokens.length === 1) return (tokens[0] ?? "").length > 0;
82
+
83
+ return false;
84
+ }
85
+
86
+ export const testCommand: ShellModule = {
87
+ name: "test",
88
+ aliases: ["["],
89
+ description: "Evaluate conditional expression",
90
+ category: "shell",
91
+ params: ["<expression>"],
92
+ run: ({ args, shell, cwd }) => {
93
+ try {
94
+ const result = evalTest([...args], shell, cwd);
95
+ return { exitCode: result ? 0 : 1 };
96
+ } catch {
97
+ return { stderr: "test: malformed expression", exitCode: 2 };
98
+ }
99
+ },
100
+ };
@@ -0,0 +1,40 @@
1
+ import { resolveModule } from ".";
2
+ import type { ShellModule } from "../types/commands";
3
+
4
+ export const typeCommand: ShellModule = {
5
+ name: "type",
6
+ description: "Describe how a command would be interpreted",
7
+ category: "shell",
8
+ params: ["<command...>"],
9
+ run: ({ args, shell, env }) => {
10
+ if (args.length === 0) return { stderr: "type: missing argument", exitCode: 1 };
11
+
12
+ const pathDirs = (env?.vars?.PATH ?? "/usr/local/bin:/usr/bin:/bin").split(":");
13
+ const lines: string[] = [];
14
+ let exitCode = 0;
15
+
16
+ for (const name of args) {
17
+ if (resolveModule(name)) {
18
+ lines.push(`${name} is a shell builtin`);
19
+ continue;
20
+ }
21
+
22
+ let found = false;
23
+ for (const dir of pathDirs) {
24
+ const full = `${dir}/${name}`;
25
+ if (shell.vfs.exists(full)) {
26
+ lines.push(`${name} is ${full}`);
27
+ found = true;
28
+ break;
29
+ }
30
+ }
31
+
32
+ if (!found) {
33
+ lines.push(`${name}: not found`);
34
+ exitCode = 1;
35
+ }
36
+ }
37
+
38
+ return { stdout: lines.join("\n"), exitCode };
39
+ },
40
+ };
@@ -0,0 +1,46 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+
4
+ export const uptimeCommand: ShellModule = {
5
+ name: "uptime",
6
+ description: "Tell how long the system has been running",
7
+ category: "system",
8
+ params: ["[-p] [-s]"],
9
+ run: ({ args, shell }) => {
10
+ const pretty = ifFlag(args, ["-p"]);
11
+ const since = ifFlag(args, ["-s"]);
12
+
13
+ const uptimeSec = Math.floor((Date.now() - shell.startTime) / 1000);
14
+ const days = Math.floor(uptimeSec / 86400);
15
+ const hours = Math.floor((uptimeSec % 86400) / 3600);
16
+ const mins = Math.floor((uptimeSec % 3600) / 60);
17
+
18
+ if (since) {
19
+ return {
20
+ stdout: new Date(shell.startTime).toISOString().slice(0, 19).replace("T", " "),
21
+ exitCode: 0,
22
+ };
23
+ }
24
+
25
+ if (pretty) {
26
+ const parts: string[] = [];
27
+ if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`);
28
+ if (hours > 0) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
29
+ parts.push(`${mins} minute${mins !== 1 ? "s" : ""}`);
30
+ return { stdout: `up ${parts.join(", ")}`, exitCode: 0 };
31
+ }
32
+
33
+ const timeStr = new Date().toTimeString().slice(0, 8);
34
+ const uptimeStr =
35
+ days > 0
36
+ ? `${days} day${days > 1 ? "s" : ""}, ${String(hours).padStart(2)}:${String(mins).padStart(2, "0")}`
37
+ : `${String(hours).padStart(2)}:${String(mins).padStart(2, "0")}`;
38
+ const sessions = shell.users.listActiveSessions().length;
39
+ const load = (Math.random() * 0.5).toFixed(2);
40
+
41
+ return {
42
+ stdout: ` ${timeStr} up ${uptimeStr}, ${sessions} user${sessions !== 1 ? "s" : ""}, load average: ${load}, ${load}, ${load}`,
43
+ exitCode: 0,
44
+ };
45
+ },
46
+ };
@@ -1,146 +1,93 @@
1
- import { spawn } from "node:child_process";
2
- import { mkdtemp, readFile, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
1
  import type { ShellModule } from "../types/commands";
6
2
  import { ifFlag, parseArgs } from "./command-helpers";
7
- import {
8
- assertPathAccess,
9
- normalizeTerminalOutput,
10
- resolvePath,
11
- runHostCommand,
12
- stripUrlFilename,
13
- } from "./helpers";
3
+ import { assertPathAccess, resolvePath, stripUrlFilename } from "./helpers";
14
4
 
15
- function runHostWget(args: string[]): Promise<{
16
- stdout: string;
17
- stderr: string;
18
- exitCode: number;
19
- }> {
20
- return new Promise((resolve) => {
21
- let childProcess: ReturnType<typeof spawn>;
5
+ export const wgetCommand: ShellModule = {
6
+ name: "wget",
7
+ description: "File downloader (pure fetch)",
8
+ category: "network",
9
+ params: ["[options] <url>"],
10
+ run: async ({ authUser, cwd, args, shell }) => {
11
+ const { flagsWithValues, positionals } = parseArgs(args, {
12
+ flagsWithValue: ["-O", "--output-document", "-o", "--output-file", "-P", "--directory-prefix", "--tries", "--timeout"],
13
+ });
22
14
 
23
- try {
24
- childProcess = spawn("wget", args, {
25
- stdio: ["ignore", "pipe", "pipe"],
26
- });
27
- } catch (error) {
28
- resolve({
29
- stdout: "",
30
- stderr: `wget: ${error instanceof Error ? error.message : String(error)}`,
31
- exitCode: 1,
32
- });
33
- return;
15
+ if (ifFlag(args, ["-h", "--help"])) {
16
+ return {
17
+ stdout: [
18
+ "Usage: wget [option]... [URL]...",
19
+ " -O, --output-document=FILE Write to FILE ('-' for stdout)",
20
+ " -P, --directory-prefix=DIR Save files in DIR",
21
+ " -q, --quiet Quiet mode",
22
+ " -v, --verbose Verbose output (default)",
23
+ " -c, --continue Continue partial download",
24
+ " --tries=N Retry N times",
25
+ " --timeout=N Timeout in seconds",
26
+ ].join("\n"),
27
+ exitCode: 0,
28
+ };
34
29
  }
35
30
 
36
- let stdout = "";
37
- let stderr = "";
38
- const stdoutStream = childProcess.stdout;
39
- const stderrStream = childProcess.stderr;
40
-
41
- if (!stdoutStream || !stderrStream) {
42
- resolve({
43
- stdout: "",
44
- stderr: "wget: failed to capture process output",
45
- exitCode: 1,
46
- });
47
- return;
31
+ if (ifFlag(args, ["-V", "--version"])) {
32
+ return { stdout: "GNU Wget 1.21.3 (virtual) built on Fortune GNU/Linux.", exitCode: 0 };
48
33
  }
49
34
 
50
- stdoutStream.setEncoding("utf8");
51
- stderrStream.setEncoding("utf8");
35
+ const url = positionals[0];
36
+ if (!url) return { stderr: "wget: missing URL\nUsage: wget [OPTION]... [URL]...", exitCode: 1 };
52
37
 
53
- stdoutStream.on("data", (chunk: string) => {
54
- stdout += chunk;
55
- });
38
+ const outputArg = flagsWithValues.get("-O") ?? flagsWithValues.get("--output-document") ?? null;
39
+ const dirPrefix = flagsWithValues.get("-P") ?? flagsWithValues.get("--directory-prefix") ?? null;
40
+ const quiet = ifFlag(args, ["-q", "--quiet"]);
56
41
 
57
- stderrStream.on("data", (chunk: string) => {
58
- stderr += chunk;
59
- });
42
+ // Derive target filename
43
+ const filename = outputArg === "-" ? null : (outputArg ?? stripUrlFilename(url));
44
+ const targetPath = filename
45
+ ? resolvePath(cwd, dirPrefix ? `${dirPrefix}/${filename}` : filename)
46
+ : null;
60
47
 
61
- childProcess.on("error", (error) => {
62
- const errorCode =
63
- error instanceof Error && "code" in error
64
- ? String((error as NodeJS.ErrnoException).code ?? "")
65
- : "";
66
- resolve({
67
- stdout: "",
68
- stderr: `wget: ${error.message}`,
69
- exitCode: errorCode === "ENOENT" ? 127 : 1,
70
- });
71
- });
48
+ if (targetPath) assertPathAccess(authUser, targetPath, "wget");
72
49
 
73
- childProcess.on("close", (code) => {
74
- resolve({
75
- stdout,
76
- stderr,
77
- exitCode: code ?? 1,
78
- });
79
- });
80
- });
81
- }
82
-
83
- export const wgetCommand: ShellModule = {
84
- name: "wget",
85
- description: "File downloader",
86
- category: "network",
87
- params: ["[url]"],
88
- run: async ({ authUser, cwd, args, shell }) => {
89
- const { flagsWithValues, positionals } = parseArgs(args, {
90
- flagsWithValue: ["-o", "-O", "--output", "--output-document"],
91
- });
92
- const outputPath =
93
- flagsWithValues.get("-o") ||
94
- flagsWithValues.get("-O") ||
95
- flagsWithValues.get("--output") ||
96
- flagsWithValues.get("--output-document") ||
97
- null;
98
- const url = positionals[0];
99
-
100
- if (!url) {
101
- return { stderr: "wget: missing URL", exitCode: 1 };
50
+ const stderrLines: string[] = [];
51
+ if (!quiet) {
52
+ stderrLines.push(`--${new Date().toISOString()}-- ${url}`);
53
+ stderrLines.push(`Resolving ${new URL(url).host}...`);
54
+ stderrLines.push(`Connecting to ${new URL(url).host}...`);
102
55
  }
103
56
 
104
- const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
105
-
106
- if (isHelpLike) {
107
- const result = await runHostWget(args);
108
- return {
109
- stdout: normalizeTerminalOutput(result.stdout),
110
- stderr: result.stderr
111
- ? normalizeTerminalOutput(result.stderr)
112
- : undefined,
113
- exitCode: result.exitCode,
114
- };
57
+ let response: Response;
58
+ try {
59
+ response = await fetch(url, { headers: { "User-Agent": "Wget/1.21.3 (Fortune GNU/Linux)" } });
60
+ } catch (err) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ stderrLines.push(`wget: unable to resolve host: ${msg}`);
63
+ return { stderr: stderrLines.join("\n"), exitCode: 4 };
115
64
  }
116
65
 
117
- const tempDir = await mkdtemp(join(tmpdir(), "virtual-env-js-wget-"));
118
- const tempFile = join(tempDir, "download");
66
+ if (!response.ok) {
67
+ stderrLines.push(`ERROR ${response.status}: ${response.statusText}`);
68
+ return { stderr: stderrLines.join("\n"), exitCode: 8 };
69
+ }
119
70
 
120
- try {
121
- const hostArgs = [...positionals, "-O", tempFile];
122
- const result = await runHostCommand("wget", hostArgs);
71
+ let body: string;
72
+ try { body = await response.text(); } catch { return { stderr: "wget: failed to read response", exitCode: 1 }; }
123
73
 
124
- if (result.exitCode !== 0) {
125
- return {
126
- stderr: normalizeTerminalOutput(
127
- result.stderr || `wget: exited with code ${result.exitCode}`,
128
- ),
129
- exitCode: result.exitCode,
130
- };
131
- }
74
+ if (!quiet) {
75
+ const ct = response.headers.get("content-type") ?? "application/octet-stream";
76
+ stderrLines.push(`HTTP request sent, awaiting response... ${response.status} ${response.statusText}`);
77
+ stderrLines.push(`Length: ${body.length} [${ct}]`);
78
+ }
132
79
 
133
- const content = await readFile(tempFile, "utf8");
134
- const target = resolvePath(cwd, outputPath || stripUrlFilename(url));
135
- assertPathAccess(authUser, target, "wget");
136
- shell.writeFileAsUser(authUser, target, content);
80
+ // Output to stdout (pipe) or file
81
+ if (outputArg === "-") {
82
+ return { stdout: body, stderr: stderrLines.join("\n") || undefined, exitCode: 0 };
83
+ }
137
84
 
138
- return {
139
- stdout: `saved ${target}`,
140
- exitCode: 0,
141
- };
142
- } finally {
143
- await rm(tempDir, { recursive: true, force: true });
85
+ if (targetPath) {
86
+ shell.writeFileAsUser(authUser, targetPath, body);
87
+ if (!quiet) stderrLines.push(`Saving to: '${targetPath}'\n${targetPath} 100%[==================>] ${body.length} B`);
88
+ return { stderr: stderrLines.join("\n") || undefined, exitCode: 0 };
144
89
  }
90
+
91
+ return { stdout: body, exitCode: 0 };
145
92
  },
146
93
  };
@@ -0,0 +1,34 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ export const whichCommand: ShellModule = {
4
+ name: "which",
5
+ description: "Locate a command in PATH",
6
+ category: "shell",
7
+ params: ["<command...>"],
8
+ run: ({ args, shell, env }) => {
9
+ if (args.length === 0) return { stderr: "which: missing argument", exitCode: 1 };
10
+
11
+ const pathDirs = (env?.vars?.PATH ?? "/usr/local/bin:/usr/bin:/bin").split(":");
12
+ const lines: string[] = [];
13
+ let anyMissing = false;
14
+
15
+ for (const name of args) {
16
+ let found = false;
17
+ for (const dir of pathDirs) {
18
+ const full = `${dir}/${name}`;
19
+ if (shell.vfs.exists(full)) {
20
+ const st = shell.vfs.stat(full);
21
+ if (st.type === "file") {
22
+ lines.push(full);
23
+ found = true;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+ if (!found) anyMissing = true;
29
+ }
30
+
31
+ if (lines.length === 0) return { exitCode: 1 };
32
+ return { stdout: lines.join("\n"), exitCode: anyMissing ? 1 : 0 };
33
+ },
34
+ };
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { SftpMimic, SshMimic } from "./SSHMimic/index";
4
4
  import VirtualFileSystem from "./VirtualFileSystem/index";
5
5
  import { VirtualShell } from "./VirtualShell/index";
6
6
  import { VirtualUserManager } from "./VirtualUserManager/index";
7
+ import { VirtualPackageManager } from "./VirtualPackageManager/index";
7
8
 
8
9
  export type {
9
10
  AuditLogEntry,
@@ -15,9 +16,11 @@ export type {
15
16
  CommandOutcome,
16
17
  CommandResult,
17
18
  NanoEditorSession,
19
+ ShellEnv,
18
20
  ShellModule,
19
21
  SudoChallenge,
20
22
  } from "./types/commands";
23
+ export type { ShellProperties } from "./VirtualShell/index";
21
24
  export type { ExecStream, ShellStream } from "./types/streams";
22
25
  export type {
23
26
  RemoveOptions,
@@ -35,6 +38,12 @@ export type {
35
38
  } from "./types/vfs";
36
39
  export type { VfsOptions, VfsPersistenceMode } from "./VirtualFileSystem/index";
37
40
 
41
+ export type {
42
+ PackageDefinition,
43
+ PackageFile,
44
+ InstalledPackage,
45
+ } from "./VirtualPackageManager/index";
46
+
38
47
  export {
39
48
  HoneyPot,
40
49
  SshClient,
@@ -43,6 +52,7 @@ export {
43
52
  VirtualShell,
44
53
  SshMimic as VirtualSshServer,
45
54
  VirtualUserManager,
55
+ VirtualPackageManager,
46
56
  };
47
57
 
48
58
  export {