typescript-virtual-container 1.2.3 → 1.2.5

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 (69) hide show
  1. package/README.md +871 -1231
  2. package/benchmark-results.txt +21 -21
  3. package/biome.json +9 -0
  4. package/dist/SSHMimic/index.d.ts +19 -2
  5. package/dist/SSHMimic/index.d.ts.map +1 -1
  6. package/dist/SSHMimic/index.js +127 -15
  7. package/dist/VirtualFileSystem/index.d.ts +115 -88
  8. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  9. package/dist/VirtualFileSystem/index.js +406 -258
  10. package/dist/VirtualShell/index.d.ts +3 -4
  11. package/dist/VirtualShell/index.d.ts.map +1 -1
  12. package/dist/VirtualShell/index.js +5 -23
  13. package/dist/VirtualUserManager/index.d.ts +41 -3
  14. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  15. package/dist/VirtualUserManager/index.js +83 -21
  16. package/dist/commands/chmod.d.ts +3 -0
  17. package/dist/commands/chmod.d.ts.map +1 -0
  18. package/dist/commands/chmod.js +31 -0
  19. package/dist/commands/cp.d.ts +3 -0
  20. package/dist/commands/cp.d.ts.map +1 -0
  21. package/dist/commands/cp.js +68 -0
  22. package/dist/commands/find.d.ts +3 -0
  23. package/dist/commands/find.d.ts.map +1 -0
  24. package/dist/commands/find.js +48 -0
  25. package/dist/commands/grep.d.ts.map +1 -1
  26. package/dist/commands/grep.js +61 -35
  27. package/dist/commands/head.d.ts +3 -0
  28. package/dist/commands/head.d.ts.map +1 -0
  29. package/dist/commands/head.js +30 -0
  30. package/dist/commands/index.d.ts.map +1 -1
  31. package/dist/commands/index.js +25 -35
  32. package/dist/commands/ln.d.ts +3 -0
  33. package/dist/commands/ln.d.ts.map +1 -0
  34. package/dist/commands/ln.js +42 -0
  35. package/dist/commands/mv.d.ts +3 -0
  36. package/dist/commands/mv.d.ts.map +1 -0
  37. package/dist/commands/mv.js +35 -0
  38. package/dist/commands/tail.d.ts +3 -0
  39. package/dist/commands/tail.d.ts.map +1 -0
  40. package/dist/commands/tail.js +33 -0
  41. package/dist/commands/wc.d.ts +3 -0
  42. package/dist/commands/wc.d.ts.map +1 -0
  43. package/dist/commands/wc.js +48 -0
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/standalone.js +7 -9
  47. package/package.json +7 -3
  48. package/scripts/publish-package.sh +70 -0
  49. package/src/SSHMimic/index.ts +159 -17
  50. package/src/VirtualFileSystem/index.ts +500 -280
  51. package/src/VirtualShell/index.ts +5 -33
  52. package/src/VirtualUserManager/index.ts +92 -26
  53. package/src/commands/chmod.ts +33 -0
  54. package/src/commands/cp.ts +76 -0
  55. package/src/commands/find.ts +61 -0
  56. package/src/commands/grep.ts +54 -38
  57. package/src/commands/head.ts +35 -0
  58. package/src/commands/index.ts +25 -43
  59. package/src/commands/ln.ts +47 -0
  60. package/src/commands/mv.ts +43 -0
  61. package/src/commands/tail.ts +37 -0
  62. package/src/commands/wc.ts +48 -0
  63. package/src/index.ts +1 -0
  64. package/src/standalone.ts +12 -9
  65. package/standalone.js +102 -0
  66. package/standalone.js.map +7 -0
  67. package/tests/bun-test-shim.ts +1 -0
  68. package/tests/sftp.test.ts +115 -191
  69. package/tests/users.test.ts +66 -83
@@ -8,19 +8,25 @@ import type {
8
8
  import { adduserCommand } from "./adduser";
9
9
  import { catCommand } from "./cat";
10
10
  import { cdCommand } from "./cd";
11
+ import { chmodCommand } from "./chmod";
11
12
  import { clearCommand } from "./clear";
13
+ import { cpCommand } from "./cp";
12
14
  import { curlCommand } from "./curl";
13
15
  import { deluserCommand } from "./deluser";
14
16
  import { echoCommand } from "./echo";
15
17
  import { envCommand } from "./env";
16
18
  import { exitCommand } from "./exit";
17
19
  import { exportCommand } from "./export";
20
+ import { findCommand } from "./find";
18
21
  import { grepCommand } from "./grep";
22
+ import { headCommand } from "./head";
19
23
  import { createHelpCommand } from "./help";
20
24
  import { hostnameCommand } from "./hostname";
21
25
  import { htopCommand } from "./htop";
26
+ import { lnCommand } from "./ln";
22
27
  import { lsCommand } from "./ls";
23
28
  import { mkdirCommand } from "./mkdir";
29
+ import { mvCommand } from "./mv";
24
30
  import { nanoCommand } from "./nano";
25
31
  import { neofetchCommand } from "./neofetch";
26
32
  import { passwdCommand } from "./passwd";
@@ -30,9 +36,11 @@ import { setCommand } from "./set";
30
36
  import { shCommand } from "./sh";
31
37
  import { suCommand } from "./su";
32
38
  import { sudoCommand } from "./sudo";
39
+ import { tailCommand } from "./tail";
33
40
  import { touchCommand } from "./touch";
34
41
  import { treeCommand } from "./tree";
35
42
  import { unsetCommand } from "./unset";
43
+ import { wcCommand } from "./wc";
36
44
  import { wgetCommand } from "./wget";
37
45
  import { whoCommand } from "./who";
38
46
  import { whoamiCommand } from "./whoami";
@@ -68,6 +76,14 @@ const BASE_COMMANDS: ShellModule[] = [
68
76
  shCommand,
69
77
  clearCommand,
70
78
  exitCommand,
79
+ cpCommand,
80
+ mvCommand,
81
+ lnCommand,
82
+ findCommand,
83
+ wcCommand,
84
+ headCommand,
85
+ tailCommand,
86
+ chmodCommand,
71
87
  ];
72
88
 
73
89
  const customCommands: ShellModule[] = [];
@@ -80,6 +96,7 @@ const commandRegistry = new Map<string, ShellModule>();
80
96
  let cachedCommandNames: string[] | null = null;
81
97
 
82
98
  function buildCache(): void {
99
+ commandRegistry.clear();
83
100
  for (const mod of getCommandModules()) {
84
101
  commandRegistry.set(mod.name, mod);
85
102
  for (const alias of mod.aliases ?? []) {
@@ -90,13 +107,6 @@ function buildCache(): void {
90
107
  }
91
108
 
92
109
  function getCommandModules(): ShellModule[] {
93
- // console.log("Loading command modules...");
94
- // console.log(
95
- // `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
96
- // );
97
- // console.log(
98
- // `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
99
- // );
100
110
  return [...BASE_COMMANDS, ...customCommands, helpCommand];
101
111
  }
102
112
 
@@ -114,13 +124,7 @@ export function registerCommand(module: ShellModule): void {
114
124
  );
115
125
  }
116
126
 
117
- for (const name of names) {
118
- if (commandRegistry.has(name)) {
119
- throw new Error(`Command '${name}' already exists`);
120
- }
121
- commandRegistry.set(name, normalized);
122
- }
123
-
127
+ customCommands.push(normalized);
124
128
  buildCache();
125
129
  }
126
130
 
@@ -129,24 +133,16 @@ export function createCustomCommand(
129
133
  params: string[],
130
134
  run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
131
135
  ): ShellModule {
132
- return {
133
- name,
134
- params,
135
- run,
136
- };
136
+ return { name, params, run };
137
137
  }
138
138
 
139
139
  export function getCommandNames(): string[] {
140
- if (!cachedCommandNames) {
141
- buildCache();
142
- }
140
+ if (!cachedCommandNames) buildCache();
143
141
  return cachedCommandNames!;
144
142
  }
145
143
 
146
144
  export function resolveModule(name: string): ShellModule | undefined {
147
- if (!cachedCommandNames) {
148
- buildCache();
149
- }
145
+ if (!cachedCommandNames) buildCache();
150
146
  return commandRegistry.get(name.toLowerCase());
151
147
  }
152
148
 
@@ -166,7 +162,6 @@ function splitArgsRespectingQuotes(input: string): string[] {
166
162
  quoteChar = ch;
167
163
  continue;
168
164
  }
169
-
170
165
  if (ch === quoteChar) {
171
166
  inQuotes = false;
172
167
  quoteChar = "";
@@ -185,10 +180,7 @@ function splitArgsRespectingQuotes(input: string): string[] {
185
180
  current += ch;
186
181
  }
187
182
 
188
- if (current.length > 0) {
189
- tokens.push(current);
190
- }
191
-
183
+ if (current.length > 0) tokens.push(current);
192
184
  return tokens;
193
185
  }
194
186
 
@@ -200,8 +192,6 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
200
192
  };
201
193
  }
202
194
 
203
- // Internal async function for pipeline execution
204
-
205
195
  export async function runCommand(
206
196
  rawInput: string,
207
197
  authUser: string,
@@ -213,9 +203,7 @@ export async function runCommand(
213
203
  ): Promise<CommandResult> {
214
204
  const trimmed = rawInput.trim();
215
205
 
216
- if (trimmed.length === 0) {
217
- return { exitCode: 0 };
218
- }
206
+ if (trimmed.length === 0) return { exitCode: 0 };
219
207
 
220
208
  if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
221
209
  const { parseShellPipeline } = await import("../VirtualShell/shellParser");
@@ -223,10 +211,7 @@ export async function runCommand(
223
211
 
224
212
  const pipeline = parseShellPipeline(trimmed);
225
213
  if (!pipeline.isValid) {
226
- return {
227
- stderr: pipeline.error || "Syntax error",
228
- exitCode: 1,
229
- };
214
+ return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
230
215
  }
231
216
 
232
217
  try {
@@ -249,10 +234,7 @@ export async function runCommand(
249
234
  const mod = resolveModule(commandName);
250
235
 
251
236
  if (!mod) {
252
- return {
253
- stderr: `Command '${trimmed}' not found`,
254
- exitCode: 127,
255
- };
237
+ return { stderr: `Command '${trimmed}' not found`, exitCode: 127 };
256
238
  }
257
239
 
258
240
  try {
@@ -0,0 +1,47 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+ import { assertPathAccess, resolvePath } from "./helpers";
4
+
5
+ export const lnCommand: ShellModule = {
6
+ name: "ln",
7
+ params: ["[-s] <target> <link_name>"],
8
+ run: ({ authUser, shell, cwd, args }) => {
9
+ const symbolic = ifFlag(args, ["-s", "--symbolic"]);
10
+ const positionals = args.filter((a) => !a.startsWith("-"));
11
+ const [targetArg, linkArg] = positionals;
12
+
13
+ if (!targetArg || !linkArg) {
14
+ return { stderr: "ln: missing operand", exitCode: 1 };
15
+ }
16
+
17
+ const linkPath = resolvePath(cwd, linkArg);
18
+ const targetPath = symbolic
19
+ ? targetArg // keep relative for symlinks
20
+ : resolvePath(cwd, targetArg);
21
+
22
+ try {
23
+ assertPathAccess(authUser, linkPath, "ln");
24
+
25
+ if (!symbolic) {
26
+ // Hard link — copy file contents
27
+ const srcPath = resolvePath(cwd, targetArg);
28
+ assertPathAccess(authUser, srcPath, "ln");
29
+ if (!shell.vfs.exists(srcPath)) {
30
+ return {
31
+ stderr: `ln: ${targetArg}: No such file or directory`,
32
+ exitCode: 1,
33
+ };
34
+ }
35
+ const content = shell.vfs.readFile(srcPath);
36
+ shell.writeFileAsUser(authUser, linkPath, content);
37
+ } else {
38
+ shell.vfs.symlink(targetPath, linkPath);
39
+ }
40
+
41
+ return { exitCode: 0 };
42
+ } catch (err) {
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ return { stderr: `ln: ${msg}`, exitCode: 1 };
45
+ }
46
+ },
47
+ };
@@ -0,0 +1,43 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { assertPathAccess, resolvePath } from "./helpers";
3
+
4
+ export const mvCommand: ShellModule = {
5
+ name: "mv",
6
+ params: ["<source> <dest>"],
7
+ run: ({ authUser, shell, cwd, args }) => {
8
+ const positionals = args.filter((a) => !a.startsWith("-"));
9
+ const [srcArg, destArg] = positionals;
10
+
11
+ if (!srcArg || !destArg) {
12
+ return { stderr: "mv: missing operand", exitCode: 1 };
13
+ }
14
+
15
+ const srcPath = resolvePath(cwd, srcArg);
16
+ const destPath = resolvePath(cwd, destArg);
17
+
18
+ try {
19
+ assertPathAccess(authUser, srcPath, "mv");
20
+ assertPathAccess(authUser, destPath, "mv");
21
+
22
+ if (!shell.vfs.exists(srcPath)) {
23
+ return {
24
+ stderr: `mv: ${srcArg}: No such file or directory`,
25
+ exitCode: 1,
26
+ };
27
+ }
28
+
29
+ // If dest is a directory, move into it
30
+ const finalDest =
31
+ shell.vfs.exists(destPath) &&
32
+ shell.vfs.stat(destPath).type === "directory"
33
+ ? `${destPath}/${srcArg.split("/").pop()}`
34
+ : destPath;
35
+
36
+ shell.vfs.move(srcPath, finalDest);
37
+ return { exitCode: 0 };
38
+ } catch (err) {
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ return { stderr: `mv: ${msg}`, exitCode: 1 };
41
+ }
42
+ },
43
+ };
@@ -0,0 +1,37 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { getFlag } from "./command-helpers";
3
+ import { assertPathAccess, resolvePath } from "./helpers";
4
+
5
+ export const tailCommand: ShellModule = {
6
+ name: "tail",
7
+ params: ["[-n <lines>] [file...]"],
8
+ run: ({ authUser, shell, cwd, args, stdin }) => {
9
+ const nArg = getFlag(args, ["-n"]);
10
+ const n = typeof nArg === "string" ? parseInt(nArg, 10) : 10;
11
+ const positionals = args.filter((a) => !a.startsWith("-") && a !== nArg);
12
+
13
+ const take = (content: string) => {
14
+ const lines = content.split("\n");
15
+ return lines.slice(Math.max(0, lines.length - n)).join("\n");
16
+ };
17
+
18
+ if (positionals.length === 0) {
19
+ return { stdout: take(stdin ?? ""), exitCode: 0 };
20
+ }
21
+
22
+ const results: string[] = [];
23
+ for (const file of positionals) {
24
+ const filePath = resolvePath(cwd, file);
25
+ try {
26
+ assertPathAccess(authUser, filePath, "tail");
27
+ results.push(take(shell.vfs.readFile(filePath)));
28
+ } catch {
29
+ return {
30
+ stderr: `tail: ${file}: No such file or directory`,
31
+ exitCode: 1,
32
+ };
33
+ }
34
+ }
35
+ return { stdout: results.join("\n"), exitCode: 0 };
36
+ },
37
+ };
@@ -0,0 +1,48 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+ import { assertPathAccess, resolvePath } from "./helpers";
4
+
5
+ export const wcCommand: ShellModule = {
6
+ name: "wc",
7
+ params: ["[-l] [-w] [-c] [file...]"],
8
+ run: ({ authUser, shell, cwd, args, stdin }) => {
9
+ const lines = ifFlag(args, ["-l"]);
10
+ const words = ifFlag(args, ["-w"]);
11
+ const bytes = ifFlag(args, ["-c"]);
12
+ const showAll = !lines && !words && !bytes;
13
+ const positionals = args.filter((a) => !a.startsWith("-"));
14
+
15
+ const count = (content: string, label: string): string => {
16
+ const l = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
17
+ const w = content.trim().split(/\s+/).filter(Boolean).length;
18
+ const c = Buffer.byteLength(content, "utf8");
19
+ const parts: string[] = [];
20
+ if (showAll || lines) parts.push(String(l).padStart(7));
21
+ if (showAll || words) parts.push(String(w).padStart(7));
22
+ if (showAll || bytes) parts.push(String(c).padStart(7));
23
+ if (label) parts.push(` ${label}`);
24
+ return parts.join("");
25
+ };
26
+
27
+ if (positionals.length === 0) {
28
+ const content = stdin ?? "";
29
+ return { stdout: count(content, ""), exitCode: 0 };
30
+ }
31
+
32
+ const results: string[] = [];
33
+ for (const file of positionals) {
34
+ const filePath = resolvePath(cwd, file);
35
+ try {
36
+ assertPathAccess(authUser, filePath, "wc");
37
+ const content = shell.vfs.readFile(filePath);
38
+ results.push(count(content, file));
39
+ } catch {
40
+ return {
41
+ stderr: `wc: ${file}: No such file or directory`,
42
+ exitCode: 1,
43
+ };
44
+ }
45
+ }
46
+ return { stdout: results.join("\n"), exitCode: 0 };
47
+ },
48
+ };
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export type {
33
33
  VfsSnapshotNode,
34
34
  WriteFileOptions,
35
35
  } from "./types/vfs";
36
+ export type { VfsOptions, VfsPersistenceMode } from "./VirtualFileSystem/index";
36
37
 
37
38
  export {
38
39
  HoneyPot,
package/src/standalone.ts CHANGED
@@ -16,12 +16,6 @@ new VirtualSshServer({
16
16
  shell: virtualShell,
17
17
  })
18
18
  .start()
19
- .then((port: number) => {
20
- // if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
21
- // else {
22
- console.log(`SSH Mimic initialized. Listening on port ${port}.`);
23
- // }
24
- })
25
19
  .catch((error: unknown) => {
26
20
  console.error("Failed to start SSH Mimic:", error);
27
21
  process.exit(1);
@@ -29,10 +23,19 @@ new VirtualSshServer({
29
23
 
30
24
  new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
31
25
  .start()
32
- .then((port: number) => {
33
- console.log(`SFTP Mimic initialized. Listening on port ${port}.`);
34
- })
35
26
  .catch((error: unknown) => {
36
27
  console.error("Failed to start SFTP Mimic:", error);
37
28
  process.exit(1);
38
29
  });
30
+
31
+ process.on("uncaughtException", (error) => {
32
+ console.log("Oh my god, something terrible happened: ", error);
33
+ });
34
+
35
+ process.on("unhandledRejection", (error, promise) => {
36
+ console.log(
37
+ " Oh Lord! We forgot to handle a promise rejection here: ",
38
+ promise,
39
+ );
40
+ console.log(" The error was: ", error);
41
+ });