typescript-virtual-container 1.1.0 → 1.1.1-c

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 (229) hide show
  1. package/.vscode/settings.json +18 -0
  2. package/README.md +45 -5
  3. package/dist/SSHClient/index.d.ts +138 -0
  4. package/dist/SSHClient/index.d.ts.map +1 -0
  5. package/dist/SSHClient/index.js +216 -0
  6. package/dist/SSHMimic/exec.d.ts +4 -0
  7. package/dist/SSHMimic/exec.d.ts.map +1 -0
  8. package/dist/SSHMimic/exec.js +21 -0
  9. package/dist/SSHMimic/executor.d.ts +9 -0
  10. package/dist/SSHMimic/executor.d.ts.map +1 -0
  11. package/dist/SSHMimic/executor.js +131 -0
  12. package/dist/SSHMimic/hostKey.d.ts +2 -0
  13. package/dist/SSHMimic/hostKey.d.ts.map +1 -0
  14. package/dist/SSHMimic/hostKey.js +17 -0
  15. package/dist/SSHMimic/index.d.ts +39 -0
  16. package/dist/SSHMimic/index.d.ts.map +1 -0
  17. package/dist/SSHMimic/index.js +113 -0
  18. package/dist/SSHMimic/loginFormat.d.ts +2 -0
  19. package/dist/SSHMimic/loginFormat.d.ts.map +1 -0
  20. package/dist/SSHMimic/loginFormat.js +10 -0
  21. package/dist/SSHMimic/prompt.d.ts +2 -0
  22. package/dist/SSHMimic/prompt.d.ts.map +1 -0
  23. package/dist/SSHMimic/prompt.js +9 -0
  24. package/dist/VirtualFileSystem/archive.d.ts +5 -0
  25. package/dist/VirtualFileSystem/archive.d.ts.map +1 -0
  26. package/dist/VirtualFileSystem/archive.js +56 -0
  27. package/dist/VirtualFileSystem/index.d.ts +131 -0
  28. package/dist/VirtualFileSystem/index.d.ts.map +1 -0
  29. package/dist/VirtualFileSystem/index.js +355 -0
  30. package/dist/VirtualFileSystem/internalTypes.d.ts +18 -0
  31. package/dist/VirtualFileSystem/internalTypes.d.ts.map +1 -0
  32. package/dist/VirtualFileSystem/internalTypes.js +0 -0
  33. package/dist/VirtualFileSystem/path.d.ts +9 -0
  34. package/dist/VirtualFileSystem/path.d.ts.map +1 -0
  35. package/dist/VirtualFileSystem/path.js +49 -0
  36. package/dist/VirtualFileSystem/snapshot.d.ts +5 -0
  37. package/dist/VirtualFileSystem/snapshot.d.ts.map +1 -0
  38. package/dist/VirtualFileSystem/snapshot.js +59 -0
  39. package/dist/VirtualFileSystem/tree.d.ts +3 -0
  40. package/dist/VirtualFileSystem/tree.d.ts.map +1 -0
  41. package/dist/VirtualFileSystem/tree.js +19 -0
  42. package/dist/VirtualShell/index.d.ts +86 -0
  43. package/dist/VirtualShell/index.d.ts.map +1 -0
  44. package/dist/VirtualShell/index.js +129 -0
  45. package/dist/VirtualShell/shell.d.ts +5 -0
  46. package/dist/VirtualShell/shell.d.ts.map +1 -0
  47. package/dist/VirtualShell/shell.js +473 -0
  48. package/dist/VirtualShell/shellParser.d.ts +4 -0
  49. package/dist/VirtualShell/shellParser.d.ts.map +1 -0
  50. package/dist/VirtualShell/shellParser.js +207 -0
  51. package/dist/VirtualUserManager/index.d.ts +168 -0
  52. package/dist/VirtualUserManager/index.d.ts.map +1 -0
  53. package/dist/VirtualUserManager/index.js +375 -0
  54. package/dist/commands/adduser.d.ts +3 -0
  55. package/dist/commands/adduser.d.ts.map +1 -0
  56. package/dist/commands/adduser.js +18 -0
  57. package/dist/commands/cat.d.ts +3 -0
  58. package/dist/commands/cat.d.ts.map +1 -0
  59. package/dist/commands/cat.js +15 -0
  60. package/dist/commands/cd.d.ts +3 -0
  61. package/dist/commands/cd.d.ts.map +1 -0
  62. package/dist/commands/cd.js +17 -0
  63. package/dist/commands/clear.d.ts +3 -0
  64. package/dist/commands/clear.d.ts.map +1 -0
  65. package/dist/commands/clear.js +5 -0
  66. package/dist/commands/command-helpers.d.ts +23 -0
  67. package/dist/commands/command-helpers.d.ts.map +1 -0
  68. package/dist/commands/command-helpers.js +139 -0
  69. package/dist/commands/curl.d.ts +3 -0
  70. package/dist/commands/curl.d.ts.map +1 -0
  71. package/dist/commands/curl.js +44 -0
  72. package/dist/commands/deluser.d.ts +3 -0
  73. package/dist/commands/deluser.d.ts.map +1 -0
  74. package/dist/commands/deluser.js +15 -0
  75. package/dist/commands/echo.d.ts +3 -0
  76. package/dist/commands/echo.d.ts.map +1 -0
  77. package/dist/commands/echo.js +22 -0
  78. package/dist/commands/env.d.ts +3 -0
  79. package/dist/commands/env.d.ts.map +1 -0
  80. package/dist/commands/env.js +18 -0
  81. package/dist/commands/exit.d.ts +3 -0
  82. package/dist/commands/exit.d.ts.map +1 -0
  83. package/dist/commands/exit.js +5 -0
  84. package/dist/commands/export.d.ts +3 -0
  85. package/dist/commands/export.d.ts.map +1 -0
  86. package/dist/commands/export.js +34 -0
  87. package/dist/commands/grep.d.ts +3 -0
  88. package/dist/commands/grep.d.ts.map +1 -0
  89. package/dist/commands/grep.js +69 -0
  90. package/dist/commands/help.d.ts +3 -0
  91. package/dist/commands/help.d.ts.map +1 -0
  92. package/dist/commands/help.js +7 -0
  93. package/dist/commands/helpers.d.ts +26 -0
  94. package/dist/commands/helpers.d.ts.map +1 -0
  95. package/dist/commands/helpers.js +160 -0
  96. package/dist/commands/hostname.d.ts +3 -0
  97. package/dist/commands/hostname.d.ts.map +1 -0
  98. package/dist/commands/hostname.js +5 -0
  99. package/dist/commands/htop.d.ts +3 -0
  100. package/dist/commands/htop.d.ts.map +1 -0
  101. package/dist/commands/htop.js +10 -0
  102. package/dist/commands/index.d.ts +8 -0
  103. package/dist/commands/index.d.ts.map +1 -0
  104. package/dist/commands/index.js +212 -0
  105. package/dist/commands/ls.d.ts +3 -0
  106. package/dist/commands/ls.d.ts.map +1 -0
  107. package/dist/commands/ls.js +47 -0
  108. package/dist/commands/mkdir.d.ts +3 -0
  109. package/dist/commands/mkdir.d.ts.map +1 -0
  110. package/dist/commands/mkdir.js +21 -0
  111. package/dist/commands/nano.d.ts +3 -0
  112. package/dist/commands/nano.d.ts.map +1 -0
  113. package/dist/commands/nano.js +27 -0
  114. package/dist/commands/neofetch.d.ts +3 -0
  115. package/dist/commands/neofetch.d.ts.map +1 -0
  116. package/dist/commands/neofetch.js +32 -0
  117. package/dist/commands/pwd.d.ts +3 -0
  118. package/dist/commands/pwd.d.ts.map +1 -0
  119. package/dist/commands/pwd.js +5 -0
  120. package/dist/commands/rm.d.ts +3 -0
  121. package/dist/commands/rm.d.ts.map +1 -0
  122. package/dist/commands/rm.js +29 -0
  123. package/dist/commands/set.d.ts +7 -0
  124. package/dist/commands/set.d.ts.map +1 -0
  125. package/dist/commands/set.js +64 -0
  126. package/dist/commands/sh.d.ts +4 -0
  127. package/dist/commands/sh.d.ts.map +1 -0
  128. package/dist/commands/sh.js +45 -0
  129. package/dist/commands/su.d.ts +3 -0
  130. package/dist/commands/su.d.ts.map +1 -0
  131. package/dist/commands/su.js +24 -0
  132. package/dist/commands/sudo.d.ts +3 -0
  133. package/dist/commands/sudo.d.ts.map +1 -0
  134. package/dist/commands/sudo.js +47 -0
  135. package/dist/commands/touch.d.ts +3 -0
  136. package/dist/commands/touch.d.ts.map +1 -0
  137. package/dist/commands/touch.js +18 -0
  138. package/dist/commands/tree.d.ts +3 -0
  139. package/dist/commands/tree.d.ts.map +1 -0
  140. package/dist/commands/tree.js +11 -0
  141. package/dist/commands/unset.d.ts +3 -0
  142. package/dist/commands/unset.d.ts.map +1 -0
  143. package/dist/commands/unset.js +15 -0
  144. package/dist/commands/wget.d.ts +3 -0
  145. package/dist/commands/wget.d.ts.map +1 -0
  146. package/dist/commands/wget.js +113 -0
  147. package/dist/commands/who.d.ts +3 -0
  148. package/dist/commands/who.d.ts.map +1 -0
  149. package/dist/commands/who.js +15 -0
  150. package/dist/commands/whoami.d.ts +3 -0
  151. package/dist/commands/whoami.d.ts.map +1 -0
  152. package/dist/commands/whoami.js +5 -0
  153. package/dist/index.d.ts +11 -0
  154. package/dist/index.d.ts.map +1 -0
  155. package/dist/index.js +7 -0
  156. package/dist/modules/neofetch.d.ts +19 -0
  157. package/dist/modules/neofetch.d.ts.map +1 -0
  158. package/dist/modules/neofetch.js +284 -0
  159. package/dist/modules/shellInteractive.d.ts +6 -0
  160. package/dist/modules/shellInteractive.d.ts.map +1 -0
  161. package/dist/modules/shellInteractive.js +26 -0
  162. package/dist/modules/shellRuntime.d.ts +11 -0
  163. package/dist/modules/shellRuntime.d.ts.map +1 -0
  164. package/dist/modules/shellRuntime.js +52 -0
  165. package/dist/standalone.d.ts +2 -0
  166. package/dist/standalone.d.ts.map +1 -0
  167. package/dist/standalone.js +25 -0
  168. package/dist/types/commands.d.ts +89 -0
  169. package/dist/types/commands.d.ts.map +1 -0
  170. package/dist/types/commands.js +0 -0
  171. package/dist/types/pipeline.d.ts +23 -0
  172. package/dist/types/pipeline.d.ts.map +1 -0
  173. package/dist/types/pipeline.js +0 -0
  174. package/dist/types/streams.d.ts +32 -0
  175. package/dist/types/streams.d.ts.map +1 -0
  176. package/dist/types/streams.js +0 -0
  177. package/dist/types/vfs.d.ts +71 -0
  178. package/dist/types/vfs.d.ts.map +1 -0
  179. package/dist/types/vfs.js +0 -0
  180. package/package.json +4 -2
  181. package/src/{SSHMimic/client.ts → SSHClient/index.ts} +2 -2
  182. package/src/SSHMimic/exec.ts +1 -1
  183. package/src/SSHMimic/executor.ts +8 -8
  184. package/src/VirtualFileSystem/index.ts +26 -0
  185. package/src/VirtualShell/index.ts +17 -1
  186. package/src/VirtualShell/shell.ts +19 -107
  187. package/src/VirtualShell/shellParser.ts +32 -7
  188. package/src/VirtualUserManager/index.ts +149 -0
  189. package/src/{VirtualShell/commands → commands}/adduser.ts +1 -1
  190. package/src/{VirtualShell/commands → commands}/cat.ts +1 -1
  191. package/src/{VirtualShell/commands → commands}/cd.ts +1 -1
  192. package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
  193. package/src/{VirtualShell/commands → commands}/curl.ts +2 -2
  194. package/src/{VirtualShell/commands → commands}/deluser.ts +1 -1
  195. package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
  196. package/src/{VirtualShell/commands → commands}/env.ts +1 -1
  197. package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
  198. package/src/{VirtualShell/commands → commands}/export.ts +1 -1
  199. package/src/{VirtualShell/commands → commands}/grep.ts +1 -1
  200. package/src/{VirtualShell/commands → commands}/help.ts +1 -1
  201. package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
  202. package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
  203. package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
  204. package/src/{VirtualShell/commands → commands}/index.ts +7 -4
  205. package/src/{VirtualShell/commands → commands}/ls.ts +1 -1
  206. package/src/{VirtualShell/commands → commands}/mkdir.ts +1 -1
  207. package/src/{VirtualShell/commands → commands}/nano.ts +1 -1
  208. package/src/{VirtualShell/commands → commands}/neofetch.ts +2 -2
  209. package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
  210. package/src/{VirtualShell/commands → commands}/rm.ts +1 -1
  211. package/src/{VirtualShell/commands → commands}/set.ts +1 -1
  212. package/src/{VirtualShell/commands → commands}/sh.ts +1 -1
  213. package/src/{VirtualShell/commands → commands}/su.ts +1 -1
  214. package/src/{VirtualShell/commands → commands}/sudo.ts +1 -1
  215. package/src/{VirtualShell/commands → commands}/touch.ts +2 -2
  216. package/src/{VirtualShell/commands → commands}/tree.ts +1 -1
  217. package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
  218. package/src/{VirtualShell/commands → commands}/wget.ts +2 -2
  219. package/src/{VirtualShell/commands → commands}/who.ts +2 -2
  220. package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
  221. package/src/index.ts +2 -2
  222. package/{modules → src/modules}/neofetch.ts +56 -51
  223. package/src/modules/shellInteractive.ts +57 -0
  224. package/src/modules/shellRuntime.ts +76 -0
  225. package/tests/command-helpers.test.ts +1 -1
  226. package/tests/helpers.test.ts +1 -1
  227. package/tests/users.test.ts +60 -0
  228. package/tsconfig.json +19 -8
  229. /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
@@ -0,0 +1,52 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ export function shellQuote(value) {
4
+ return `'${value.replace(/'/g, `'\\''`)}'`;
5
+ }
6
+ export function toTtyLines(text) {
7
+ return text
8
+ .replace(/\r\n/g, "\n")
9
+ .replace(/\r/g, "\n")
10
+ .replace(/\n/g, "\r\n");
11
+ }
12
+ export function withTerminalSize(command, terminalSize) {
13
+ const cols = Number.isFinite(terminalSize.cols) && terminalSize.cols > 0
14
+ ? Math.floor(terminalSize.cols)
15
+ : 80;
16
+ const rows = Number.isFinite(terminalSize.rows) && terminalSize.rows > 0
17
+ ? Math.floor(terminalSize.rows)
18
+ : 24;
19
+ return `stty cols ${cols} rows ${rows} 2>/dev/null; ${command}`;
20
+ }
21
+ export function resolvePath(base, inputPath) {
22
+ if (!inputPath || inputPath.trim() === "" || inputPath === ".") {
23
+ return base;
24
+ }
25
+ return inputPath.startsWith("/")
26
+ ? path.posix.normalize(inputPath)
27
+ : path.posix.normalize(path.posix.join(base, inputPath));
28
+ }
29
+ export async function collectChildPids(parentPid) {
30
+ try {
31
+ const childrenRaw = await readFile(`/proc/${parentPid}/task/${parentPid}/children`, "utf8");
32
+ const directChildren = childrenRaw
33
+ .trim()
34
+ .split(/\s+/)
35
+ .filter(Boolean)
36
+ .map((value) => Number.parseInt(value, 10))
37
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
38
+ const nested = await Promise.all(directChildren.map((pid) => collectChildPids(pid)));
39
+ return [...directChildren, ...nested.flat()];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ export async function getVisibleHtopPidList(rootPid = process.pid) {
46
+ const descendants = await collectChildPids(rootPid);
47
+ const unique = Array.from(new Set(descendants)).sort((a, b) => a - b);
48
+ if (unique.length === 0) {
49
+ return null;
50
+ }
51
+ return unique.join(",");
52
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=standalone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import { VirtualShell, VirtualSshServer } from ".";
2
+ const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
3
+ const virtualShell = new VirtualShell(hostname);
4
+ virtualShell.addCommand("demo", [], () => {
5
+ return {
6
+ stdout: "This is a demo command. It does nothing useful.",
7
+ exitCode: 0,
8
+ };
9
+ });
10
+ new VirtualSshServer({
11
+ port: 2222,
12
+ hostname,
13
+ shell: virtualShell,
14
+ })
15
+ .start()
16
+ .then((port) => {
17
+ // if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
18
+ // else {
19
+ console.log(`SSH Mimic initialized. Listening on port ${port}.`);
20
+ // }
21
+ })
22
+ .catch((error) => {
23
+ console.error("Failed to start SSH Mimic:", error);
24
+ process.exit(1);
25
+ });
@@ -0,0 +1,89 @@
1
+ /** Command invocation mode used by shell runtime. */
2
+ export type CommandMode = "shell" | "exec";
3
+ import type { VirtualShell } from "../VirtualShell";
4
+ import type { VirtualActiveSession } from "../VirtualUserManager";
5
+ /**
6
+ * Normalized command execution output.
7
+ *
8
+ * A command can write text, control session lifecycle, request UI state
9
+ * transitions, and update active identity/cwd.
10
+ */
11
+ export interface CommandResult {
12
+ /** Standard output payload to append in terminal. */
13
+ stdout?: string;
14
+ /** Standard error payload to append in terminal. */
15
+ stderr?: string;
16
+ /** Request full terminal clear before next prompt. */
17
+ clearScreen?: boolean;
18
+ /** Request current shell/exec session close. */
19
+ closeSession?: boolean;
20
+ /** Optional exit code (default behavior handled by caller). */
21
+ exitCode?: number;
22
+ /** Optional cwd to apply for next prompt iteration. */
23
+ nextCwd?: string;
24
+ /** Optional user switch for current session state. */
25
+ switchUser?: string;
26
+ /** Request opening built-in nano editor workflow. */
27
+ openEditor?: NanoEditorSession;
28
+ /** Request opening built-in htop-like screen. */
29
+ openHtop?: boolean;
30
+ /** Request sudo password challenge flow. */
31
+ sudoChallenge?: SudoChallenge;
32
+ }
33
+ /** Deferred sudo challenge metadata returned by sudo command. */
34
+ export interface SudoChallenge {
35
+ /** User currently requesting elevation. */
36
+ username: string;
37
+ /** Target identity for elevated command. */
38
+ targetUser: string;
39
+ /** Command to execute after successful challenge; null for login shell. */
40
+ commandLine: string | null;
41
+ /** True when challenge targets interactive login shell. */
42
+ loginShell: boolean;
43
+ /** Prompt text shown before password input. */
44
+ prompt: string;
45
+ }
46
+ /** State payload used by nano command interactive editor flow. */
47
+ export interface NanoEditorSession {
48
+ /** Final destination path to write when save succeeds. */
49
+ targetPath: string;
50
+ /** Temporary scratch path used while editing. */
51
+ tempPath: string;
52
+ /** Initial editor content shown to user. */
53
+ initialContent: string;
54
+ }
55
+ /** Runtime context object passed to each command module. */
56
+ export interface CommandContext {
57
+ /** Authenticated user currently bound to stream. */
58
+ authUser: string;
59
+ /** Virtual hostname shown in prompt and banners. */
60
+ hostname: string;
61
+ /** Snapshot of currently active user sessions. */
62
+ activeSessions: VirtualActiveSession[];
63
+ /** Original unparsed command line input. */
64
+ rawInput: string;
65
+ /** Invocation mode (interactive shell or direct exec). */
66
+ mode: CommandMode;
67
+ /** Tokenized arguments excluding command name. */
68
+ args: string[];
69
+ /** Virtual shell instance. */
70
+ shell: VirtualShell;
71
+ /** Optional stdin payload (used by pipes/redirections). */
72
+ stdin?: string;
73
+ /** Current working directory for command execution. */
74
+ cwd: string;
75
+ }
76
+ /** Contract implemented by each shell command module. */
77
+ export interface ShellModule {
78
+ /** Primary command name used in CLI. */
79
+ name: string;
80
+ /** Parameter help snippets displayed by help command. */
81
+ params: string[];
82
+ /** Command handler implementation. */
83
+ run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
84
+ /** Optional alternative command names. */
85
+ aliases?: string[];
86
+ }
87
+ /** Command return union allowing sync or async handlers. */
88
+ export type CommandOutcome = CommandResult | Promise<CommandResult>;
89
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/types/commands.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC7B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC7B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2DAA2D;IAC3D,UAAU,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IACjC,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC9B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,cAAc,EAAE,oBAAoB,EAAE,CAAC;IACvC,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,WAAW,CAAC;IAClB,kDAAkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,8BAA8B;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,GAAG,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrE,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC"}
File without changes
@@ -0,0 +1,23 @@
1
+ /** Represents a single command in a pipeline. */
2
+ export interface PipelineCommand {
3
+ /** Command name */
4
+ name: string;
5
+ /** Command arguments */
6
+ args: string[];
7
+ /** Input redirection file path (< file) */
8
+ inputFile?: string;
9
+ /** Output redirection file path (> file) */
10
+ outputFile?: string;
11
+ /** Append to output file (>> file) */
12
+ appendOutput?: boolean;
13
+ }
14
+ /** Represents a parsed shell pipeline */
15
+ export interface Pipeline {
16
+ /** List of commands in the pipeline */
17
+ commands: PipelineCommand[];
18
+ /** Whether this is a valid pipeline */
19
+ isValid: boolean;
20
+ /** Error message if parsing failed */
21
+ error?: string;
22
+ }
23
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/types/pipeline.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC/B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,QAAQ;IACxB,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf"}
File without changes
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Minimal stream contract used by exec command handlers.
3
+ */
4
+ export interface ExecStream {
5
+ /** Writes text to stdout channel. */
6
+ write(data: string): void;
7
+ /** Signals output completion. */
8
+ end(): void;
9
+ /** Sets process-like exit code for exec response. */
10
+ exit(code: number): void;
11
+ /** Writable stderr channel. */
12
+ stderr: {
13
+ /** Writes text to stderr channel. */
14
+ write(data: string): void;
15
+ };
16
+ }
17
+ /**
18
+ * Minimal interactive stream contract used by shell mode.
19
+ */
20
+ export interface ShellStream {
21
+ /** Writes text to shell output channel. */
22
+ write(data: string): void;
23
+ /** Sets shell exit code on close. */
24
+ exit(code: number): void;
25
+ /** Ends shell stream. */
26
+ end(): void;
27
+ /** Subscribes to incoming user input chunks. */
28
+ on(event: "data", listener: (chunk: Buffer) => void): void;
29
+ /** Subscribes to stream close event. */
30
+ on(event: "close", listener: () => void): void;
31
+ }
32
+ //# sourceMappingURL=streams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../src/types/streams.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,qCAAqC;IACrC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iCAAiC;IACjC,GAAG,IAAI,IAAI,CAAC;IACZ,qDAAqD;IACrD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,+BAA+B;IAC/B,MAAM,EAAE;QACP,qCAAqC;QACrC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,2CAA2C;IAC3C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,qCAAqC;IACrC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yBAAyB;IACzB,GAAG,IAAI,IAAI,CAAC;IACZ,gDAAgD;IAChD,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,wCAAwC;IACxC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAC/C"}
File without changes
@@ -0,0 +1,71 @@
1
+ /** Supported virtual node kinds. */
2
+ export type VfsNodeType = "file" | "directory";
3
+ /** Shared metadata fields available on file and directory stats. */
4
+ export interface VfsBaseNode {
5
+ /** Node name without parent path. */
6
+ name: string;
7
+ /** Absolute normalized node path. */
8
+ path: string;
9
+ /** POSIX-like mode bits. */
10
+ mode: number;
11
+ /** Node creation timestamp. */
12
+ createdAt: Date;
13
+ /** Last update timestamp. */
14
+ updatedAt: Date;
15
+ }
16
+ /** Stat shape returned for file nodes. */
17
+ export interface VfsFileNode extends VfsBaseNode {
18
+ type: "file";
19
+ /** True when file content stored as gzip bytes. */
20
+ compressed: boolean;
21
+ /** Stored byte length (compressed when compressed=true). */
22
+ size: number;
23
+ }
24
+ /** Stat shape returned for directory nodes. */
25
+ export interface VfsDirectoryNode extends VfsBaseNode {
26
+ type: "directory";
27
+ /** Number of direct children in directory. */
28
+ childrenCount: number;
29
+ }
30
+ /** Union of file and directory stat responses. */
31
+ export type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
32
+ /** Optional behavior flags for writeFile operations. */
33
+ export interface WriteFileOptions {
34
+ /** POSIX-like mode to apply on create or overwrite. */
35
+ mode?: number;
36
+ /** Store content compressed with gzip. */
37
+ compress?: boolean;
38
+ }
39
+ /** Optional behavior flags for remove operations. */
40
+ export interface RemoveOptions {
41
+ /** Allow deleting non-empty directory trees. */
42
+ recursive?: boolean;
43
+ }
44
+ /** Base snapshot node schema used for archive serialization. */
45
+ export interface VfsSnapshotBaseNode {
46
+ name: string;
47
+ mode: number;
48
+ /** ISO-8601 creation timestamp. */
49
+ createdAt: string;
50
+ /** ISO-8601 update timestamp. */
51
+ updatedAt: string;
52
+ }
53
+ /** Serialized snapshot shape for file nodes. */
54
+ export interface VfsSnapshotFileNode extends VfsSnapshotBaseNode {
55
+ type: "file";
56
+ compressed: boolean;
57
+ /** Base64-encoded raw file bytes. */
58
+ contentBase64: string;
59
+ }
60
+ /** Serialized snapshot shape for directory nodes. */
61
+ export interface VfsSnapshotDirectoryNode extends VfsSnapshotBaseNode {
62
+ type: "directory";
63
+ children: VfsSnapshotNode[];
64
+ }
65
+ /** Union of serialized snapshot node variants. */
66
+ export type VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode;
67
+ /** Top-level serialized filesystem snapshot. */
68
+ export interface VfsSnapshot {
69
+ root: VfsSnapshotDirectoryNode;
70
+ }
71
+ //# sourceMappingURL=vfs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/types/vfs.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACpE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE7E,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,wBAAwB,CAAC;CAC/B"}
File without changes
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "typescript-virtual-container",
3
3
  "description": "In-memory SSH server with virtual filesystem and typed programmatic API",
4
- "module": "src/index.ts",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
5
6
  "type": "module",
6
- "version": "1.1.0",
7
+ "version": "1.1.1c",
7
8
  "license": "MIT",
8
9
  "keywords": [
9
10
  "ssh",
@@ -20,6 +21,7 @@
20
21
  "lint": "bunx --bun @biomejs/biome lint ./src",
21
22
  "lint:write": "bunx --bun @biomejs/biome lint --write ./src",
22
23
  "test": "bunx --bun @biomejs/biome test ./src",
24
+ "build": "tsc --project tsconfig.json",
23
25
  "deploy:npm": "npm publish --access public"
24
26
  },
25
27
  "devDependencies": {
@@ -1,6 +1,6 @@
1
+ import { runCommand } from "../commands";
1
2
  import type { CommandResult } from "../types/commands";
2
3
  import type { VirtualShell } from "../VirtualShell";
3
- import { runCommand } from "../VirtualShell/commands";
4
4
 
5
5
  /**
6
6
  * Programmatic client for executing shell commands against a virtual shell.
@@ -154,7 +154,7 @@ export class SshClient {
154
154
  }
155
155
 
156
156
  try {
157
- vfs.writeFile(path, content);
157
+ this.shell.writeFileAsUser(this.username, path, content);
158
158
  return { stdout: `File '${path}' written`, exitCode: 0 };
159
159
  } catch (error) {
160
160
  return {
@@ -1,6 +1,6 @@
1
+ import { runCommand } from "../commands";
1
2
  import type { ExecStream } from "../types/streams";
2
3
  import type { VirtualShell } from "../VirtualShell";
3
- import { runCommand } from "../VirtualShell/commands";
4
4
 
5
5
  function toTtyLines(text: string): string {
6
6
  return text
@@ -1,8 +1,8 @@
1
+ import { runCommand as runSingleCommand } from "../commands";
2
+ import { resolvePath } from "../commands/helpers";
1
3
  import type { CommandMode, CommandResult } from "../types/commands";
2
4
  import type { Pipeline, PipelineCommand } from "../types/pipeline";
3
5
  import type { VirtualShell } from "../VirtualShell";
4
- import { runCommand as runSingleCommand } from "../VirtualShell/commands";
5
- import { resolvePath } from "../VirtualShell/commands/helpers";
6
6
 
7
7
  /**
8
8
  * Execute a parsed pipeline, chaining commands and handling redirections.
@@ -90,12 +90,12 @@ async function executeSingleCommandWithRedirections(
90
90
  if (cmd.appendOutput) {
91
91
  try {
92
92
  const existing = shell.vfs.readFile(outputPath);
93
- shell.vfs.writeFile(outputPath, existing + output);
93
+ shell.writeFileAsUser(authUser, outputPath, existing + output);
94
94
  } catch {
95
- shell.vfs.writeFile(outputPath, output);
95
+ shell.writeFileAsUser(authUser, outputPath, output);
96
96
  }
97
97
  } else {
98
- shell.vfs.writeFile(outputPath, output);
98
+ shell.writeFileAsUser(authUser, outputPath, output);
99
99
  }
100
100
  return { ...result, stdout: "" };
101
101
  } catch {
@@ -165,12 +165,12 @@ async function executePipelineChain(
165
165
  if (cmd.appendOutput) {
166
166
  try {
167
167
  const existing = shell.vfs.readFile(outputPath);
168
- shell.vfs.writeFile(outputPath, existing + output);
168
+ shell.writeFileAsUser(authUser, outputPath, existing + output);
169
169
  } catch {
170
- shell.vfs.writeFile(outputPath, output);
170
+ shell.writeFileAsUser(authUser, outputPath, output);
171
171
  }
172
172
  } else {
173
- shell.vfs.writeFile(outputPath, output);
173
+ shell.writeFileAsUser(authUser, outputPath, output);
174
174
  }
175
175
  currentOutput = "";
176
176
  } catch {
@@ -24,6 +24,18 @@ class VirtualFileSystem {
24
24
  private readonly archivePath: string;
25
25
  private dirty = false;
26
26
 
27
+ private computeNodeUsageBytes(node: InternalNode): number {
28
+ if (node.type === "file") {
29
+ return node.content.length;
30
+ }
31
+
32
+ let total = 0;
33
+ for (const child of node.children.values()) {
34
+ total += this.computeNodeUsageBytes(child);
35
+ }
36
+ return total;
37
+ }
38
+
27
39
  /**
28
40
  * Creates a virtual filesystem instance.
29
41
  *
@@ -287,6 +299,20 @@ class VirtualFileSystem {
287
299
  return renderTree(node, rootLabel);
288
300
  }
289
301
 
302
+ /**
303
+ * Computes total stored file bytes under a path.
304
+ *
305
+ * File usage is based on in-memory stored bytes, including compressed
306
+ * payload size when files are marked as compressed.
307
+ *
308
+ * @param targetPath File or directory path to measure, defaults to root.
309
+ * @returns Total byte usage for file content under target path.
310
+ */
311
+ public getUsageBytes(targetPath: string = "/"): number {
312
+ const node = getNode(this.root, targetPath);
313
+ return this.computeNodeUsageBytes(node);
314
+ }
315
+
290
316
  /**
291
317
  * Compresses file content with gzip and flags node as compressed.
292
318
  *
@@ -1,9 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { createCustomCommand, registerCommand, runCommand } from "../commands";
2
3
  import type { CommandContext, CommandResult } from "../types/commands";
3
4
  import type { ShellStream } from "../types/streams";
4
5
  import VirtualFileSystem from "../VirtualFileSystem";
5
6
  import { VirtualUserManager } from "../VirtualUserManager";
6
- import { createCustomCommand, registerCommand, runCommand } from "./commands";
7
7
  import { startShell } from "./shell";
8
8
 
9
9
  export interface ShellProperties {
@@ -170,6 +170,22 @@ class VirtualShell {
170
170
  public getHostname(): string {
171
171
  return this?.hostname;
172
172
  }
173
+
174
+ /**
175
+ * Writes a file on behalf of a user with quota enforcement.
176
+ *
177
+ * @param authUser User performing the write.
178
+ * @param targetPath Destination path.
179
+ * @param content File content.
180
+ */
181
+ public writeFileAsUser(
182
+ authUser: string,
183
+ targetPath: string,
184
+ content: string | Buffer,
185
+ ): void {
186
+ this.users.assertWriteWithinQuota(authUser, targetPath, content);
187
+ this.vfs.writeFile(targetPath, content);
188
+ }
173
189
  }
174
190
 
175
191
  export { VirtualShell };
@@ -1,12 +1,22 @@
1
- import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
1
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import type { ShellProperties, VirtualShell } from ".";
5
+ import { getCommandNames, runCommand } from "../commands";
6
+ import {
7
+ spawnHtopProcess,
8
+ spawnNanoEditorProcess,
9
+ } from "../modules/shellInteractive";
10
+ import {
11
+ getVisibleHtopPidList,
12
+ resolvePath,
13
+ type TerminalSize,
14
+ toTtyLines,
15
+ } from "../modules/shellRuntime";
5
16
  import { formatLoginDate } from "../SSHMimic/loginFormat";
6
17
  import { buildPrompt } from "../SSHMimic/prompt";
7
18
  import type { ShellStream } from "../types/streams";
8
19
  import type VirtualFileSystem from "../VirtualFileSystem";
9
- import { getCommandNames, runCommand } from "./commands";
10
20
 
11
21
  interface NanoSession {
12
22
  kind: "nano" | "htop";
@@ -24,22 +34,6 @@ interface PendingSudo {
24
34
  buffer: string;
25
35
  }
26
36
 
27
- function shellQuote(value: string): string {
28
- return `'${value.replace(/'/g, `'\\''`)}'`;
29
- }
30
-
31
- interface TerminalSize {
32
- cols: number;
33
- rows: number;
34
- }
35
-
36
- function toTtyLines(text: string): string {
37
- return text
38
- .replace(/\r\n/g, "\n")
39
- .replace(/\r/g, "\n")
40
- .replace(/\n/g, "\r\n");
41
- }
42
-
43
37
  export function startShell(
44
38
  properties: ShellProperties,
45
39
  stream: ShellStream,
@@ -68,60 +62,6 @@ export function startShell(
68
62
  `[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`,
69
63
  );
70
64
 
71
- async function collectChildPids(parentPid: number): Promise<number[]> {
72
- try {
73
- const childrenRaw = await readFile(
74
- `/proc/${parentPid}/task/${parentPid}/children`,
75
- "utf8",
76
- );
77
- const directChildren = childrenRaw
78
- .trim()
79
- .split(/\s+/)
80
- .filter(Boolean)
81
- .map((value) => Number.parseInt(value, 10))
82
- .filter((pid) => Number.isInteger(pid) && pid > 0);
83
-
84
- const nested = await Promise.all(
85
- directChildren.map((pid) => collectChildPids(pid)),
86
- );
87
- return [...directChildren, ...nested.flat()];
88
- } catch {
89
- return [];
90
- }
91
- }
92
-
93
- async function getVisibleHtopPidList(): Promise<string | null> {
94
- const rootPid = process.pid;
95
- const descendants = await collectChildPids(rootPid);
96
- const unique = Array.from(new Set(descendants)).sort((a, b) => a - b);
97
- if (unique.length === 0) {
98
- return null;
99
- }
100
-
101
- return unique.join(",");
102
- }
103
-
104
- function withTerminalSize(command: string): string {
105
- const cols =
106
- Number.isFinite(terminalSize.cols) && terminalSize.cols > 0
107
- ? Math.floor(terminalSize.cols)
108
- : 80;
109
- const rows =
110
- Number.isFinite(terminalSize.rows) && terminalSize.rows > 0
111
- ? Math.floor(terminalSize.rows)
112
- : 24;
113
- return `stty cols ${cols} rows ${rows} 2>/dev/null; ${command}`;
114
- }
115
-
116
- function resolvePath(base: string, inputPath: string): string {
117
- if (!inputPath || inputPath.trim() === "" || inputPath === ".") {
118
- return base;
119
- }
120
- return inputPath.startsWith("/")
121
- ? path.posix.normalize(inputPath)
122
- : path.posix.normalize(path.posix.join(base, inputPath));
123
- }
124
-
125
65
  function renderLine(): void {
126
66
  const prompt = buildCurrentPrompt();
127
67
  stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
@@ -236,7 +176,11 @@ export function startShell(
236
176
  if (activeSession.kind === "nano") {
237
177
  try {
238
178
  const updatedContent = await readFile(activeSession.tempPath, "utf8");
239
- shell.vfs.writeFile(activeSession.targetPath, updatedContent);
179
+ shell.writeFileAsUser(
180
+ authUser,
181
+ activeSession.targetPath,
182
+ updatedContent,
183
+ );
240
184
  await shell.vfs.flushMirror();
241
185
  } catch {
242
186
  // If temp file does not exist, nano exited without writing.
@@ -261,23 +205,7 @@ export function startShell(
261
205
  await writeFile(tempPath, initialContent, "utf8");
262
206
  }
263
207
 
264
- const command = withTerminalSize(`nano -- ${shellQuote(tempPath)}`);
265
- const editor = spawn("script", ["-qfec", command, "/dev/null"], {
266
- stdio: ["pipe", "pipe", "pipe"],
267
- env: {
268
- ...process.env,
269
- // biome-ignore lint/style/useNamingConvention: TERM is an environment variable conventionally in uppercase
270
- TERM: process.env.TERM ?? "xterm-256color",
271
- },
272
- });
273
-
274
- editor.stdout.on("data", (data: Buffer) => {
275
- stream.write(data.toString("utf8"));
276
- });
277
-
278
- editor.stderr.on("data", (data: Buffer) => {
279
- stream.write(data.toString("utf8"));
280
- });
208
+ const editor = spawnNanoEditorProcess(tempPath, terminalSize, stream);
281
209
 
282
210
  editor.on("error", (error: Error) => {
283
211
  stream.write(`nano: ${error.message}\r\n`);
@@ -303,23 +231,7 @@ export function startShell(
303
231
  return;
304
232
  }
305
233
 
306
- const command = withTerminalSize(`htop -p ${shellQuote(pidList)}`);
307
- const monitor = spawn("script", ["-qfec", command, "/dev/null"], {
308
- stdio: ["pipe", "pipe", "pipe"],
309
- env: {
310
- ...process.env,
311
- // biome-ignore lint/style/useNamingConvention: TERM is an environment variable conventionally in uppercase
312
- TERM: process.env.TERM ?? "xterm-256color",
313
- },
314
- });
315
-
316
- monitor.stdout.on("data", (data: Buffer) => {
317
- stream.write(data.toString("utf8"));
318
- });
319
-
320
- monitor.stderr.on("data", (data: Buffer) => {
321
- stream.write(data.toString("utf8"));
322
- });
234
+ const monitor = spawnHtopProcess(pidList, terminalSize, stream);
323
235
 
324
236
  monitor.on("error", (error: Error) => {
325
237
  stream.write(`htop: ${error.message}\r\n`);