typescript-virtual-container 1.1.1 → 1.1.3

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 (192) hide show
  1. package/README.md +1 -1
  2. package/dist/SSHClient/index.d.ts +138 -0
  3. package/dist/SSHClient/index.d.ts.map +1 -0
  4. package/dist/SSHClient/index.js +216 -0
  5. package/dist/SSHMimic/exec.d.ts +4 -0
  6. package/dist/SSHMimic/exec.d.ts.map +1 -0
  7. package/dist/SSHMimic/exec.js +21 -0
  8. package/dist/SSHMimic/executor.d.ts +9 -0
  9. package/dist/SSHMimic/executor.d.ts.map +1 -0
  10. package/dist/SSHMimic/executor.js +131 -0
  11. package/dist/SSHMimic/hostKey.d.ts +2 -0
  12. package/dist/SSHMimic/hostKey.d.ts.map +1 -0
  13. package/dist/SSHMimic/hostKey.js +17 -0
  14. package/dist/SSHMimic/index.d.ts +39 -0
  15. package/dist/SSHMimic/index.d.ts.map +1 -0
  16. package/dist/SSHMimic/index.js +113 -0
  17. package/dist/SSHMimic/loginFormat.d.ts +2 -0
  18. package/dist/SSHMimic/loginFormat.d.ts.map +1 -0
  19. package/dist/SSHMimic/loginFormat.js +10 -0
  20. package/dist/SSHMimic/prompt.d.ts +2 -0
  21. package/dist/SSHMimic/prompt.d.ts.map +1 -0
  22. package/dist/SSHMimic/prompt.js +9 -0
  23. package/dist/VirtualFileSystem/archive.d.ts +5 -0
  24. package/dist/VirtualFileSystem/archive.d.ts.map +1 -0
  25. package/dist/VirtualFileSystem/archive.js +56 -0
  26. package/dist/VirtualFileSystem/index.d.ts +131 -0
  27. package/dist/VirtualFileSystem/index.d.ts.map +1 -0
  28. package/dist/VirtualFileSystem/index.js +355 -0
  29. package/dist/VirtualFileSystem/internalTypes.d.ts +18 -0
  30. package/dist/VirtualFileSystem/internalTypes.d.ts.map +1 -0
  31. package/dist/VirtualFileSystem/internalTypes.js +0 -0
  32. package/dist/VirtualFileSystem/path.d.ts +9 -0
  33. package/dist/VirtualFileSystem/path.d.ts.map +1 -0
  34. package/dist/VirtualFileSystem/path.js +49 -0
  35. package/dist/VirtualFileSystem/snapshot.d.ts +5 -0
  36. package/dist/VirtualFileSystem/snapshot.d.ts.map +1 -0
  37. package/dist/VirtualFileSystem/snapshot.js +59 -0
  38. package/dist/VirtualFileSystem/tree.d.ts +3 -0
  39. package/dist/VirtualFileSystem/tree.d.ts.map +1 -0
  40. package/dist/VirtualFileSystem/tree.js +19 -0
  41. package/dist/VirtualShell/index.d.ts +86 -0
  42. package/dist/VirtualShell/index.d.ts.map +1 -0
  43. package/dist/VirtualShell/index.js +129 -0
  44. package/dist/VirtualShell/shell.d.ts +5 -0
  45. package/dist/VirtualShell/shell.d.ts.map +1 -0
  46. package/dist/VirtualShell/shell.js +473 -0
  47. package/dist/VirtualShell/shellParser.d.ts +4 -0
  48. package/dist/VirtualShell/shellParser.d.ts.map +1 -0
  49. package/dist/VirtualShell/shellParser.js +207 -0
  50. package/dist/VirtualUserManager/index.d.ts +175 -0
  51. package/dist/VirtualUserManager/index.d.ts.map +1 -0
  52. package/dist/VirtualUserManager/index.js +390 -0
  53. package/dist/commands/adduser.d.ts +3 -0
  54. package/dist/commands/adduser.d.ts.map +1 -0
  55. package/dist/commands/adduser.js +18 -0
  56. package/dist/commands/cat.d.ts +3 -0
  57. package/dist/commands/cat.d.ts.map +1 -0
  58. package/dist/commands/cat.js +15 -0
  59. package/dist/commands/cd.d.ts +3 -0
  60. package/dist/commands/cd.d.ts.map +1 -0
  61. package/dist/commands/cd.js +17 -0
  62. package/dist/commands/clear.d.ts +3 -0
  63. package/dist/commands/clear.d.ts.map +1 -0
  64. package/dist/commands/clear.js +5 -0
  65. package/dist/commands/command-helpers.d.ts +23 -0
  66. package/dist/commands/command-helpers.d.ts.map +1 -0
  67. package/dist/commands/command-helpers.js +139 -0
  68. package/dist/commands/curl.d.ts +3 -0
  69. package/dist/commands/curl.d.ts.map +1 -0
  70. package/dist/commands/curl.js +44 -0
  71. package/dist/commands/deluser.d.ts +3 -0
  72. package/dist/commands/deluser.d.ts.map +1 -0
  73. package/dist/commands/deluser.js +15 -0
  74. package/dist/commands/echo.d.ts +3 -0
  75. package/dist/commands/echo.d.ts.map +1 -0
  76. package/dist/commands/echo.js +22 -0
  77. package/dist/commands/env.d.ts +3 -0
  78. package/dist/commands/env.d.ts.map +1 -0
  79. package/dist/commands/env.js +18 -0
  80. package/dist/commands/exit.d.ts +3 -0
  81. package/dist/commands/exit.d.ts.map +1 -0
  82. package/dist/commands/exit.js +5 -0
  83. package/dist/commands/export.d.ts +3 -0
  84. package/dist/commands/export.d.ts.map +1 -0
  85. package/dist/commands/export.js +34 -0
  86. package/dist/commands/grep.d.ts +3 -0
  87. package/dist/commands/grep.d.ts.map +1 -0
  88. package/dist/commands/grep.js +69 -0
  89. package/dist/commands/help.d.ts +3 -0
  90. package/dist/commands/help.d.ts.map +1 -0
  91. package/dist/commands/help.js +7 -0
  92. package/dist/commands/helpers.d.ts +26 -0
  93. package/dist/commands/helpers.d.ts.map +1 -0
  94. package/dist/commands/helpers.js +160 -0
  95. package/dist/commands/hostname.d.ts +3 -0
  96. package/dist/commands/hostname.d.ts.map +1 -0
  97. package/dist/commands/hostname.js +5 -0
  98. package/dist/commands/htop.d.ts +3 -0
  99. package/dist/commands/htop.d.ts.map +1 -0
  100. package/dist/commands/htop.js +10 -0
  101. package/dist/commands/index.d.ts +8 -0
  102. package/dist/commands/index.d.ts.map +1 -0
  103. package/dist/commands/index.js +214 -0
  104. package/dist/commands/ls.d.ts +3 -0
  105. package/dist/commands/ls.d.ts.map +1 -0
  106. package/dist/commands/ls.js +47 -0
  107. package/dist/commands/mkdir.d.ts +3 -0
  108. package/dist/commands/mkdir.d.ts.map +1 -0
  109. package/dist/commands/mkdir.js +21 -0
  110. package/dist/commands/nano.d.ts +3 -0
  111. package/dist/commands/nano.d.ts.map +1 -0
  112. package/dist/commands/nano.js +27 -0
  113. package/dist/commands/neofetch.d.ts +3 -0
  114. package/dist/commands/neofetch.d.ts.map +1 -0
  115. package/dist/commands/neofetch.js +32 -0
  116. package/dist/commands/passwd.d.ts +3 -0
  117. package/dist/commands/passwd.d.ts.map +1 -0
  118. package/dist/commands/passwd.js +21 -0
  119. package/dist/commands/pwd.d.ts +3 -0
  120. package/dist/commands/pwd.d.ts.map +1 -0
  121. package/dist/commands/pwd.js +5 -0
  122. package/dist/commands/rm.d.ts +3 -0
  123. package/dist/commands/rm.d.ts.map +1 -0
  124. package/dist/commands/rm.js +29 -0
  125. package/dist/commands/set.d.ts +7 -0
  126. package/dist/commands/set.d.ts.map +1 -0
  127. package/dist/commands/set.js +64 -0
  128. package/dist/commands/sh.d.ts +4 -0
  129. package/dist/commands/sh.d.ts.map +1 -0
  130. package/dist/commands/sh.js +45 -0
  131. package/dist/commands/su.d.ts +3 -0
  132. package/dist/commands/su.d.ts.map +1 -0
  133. package/dist/commands/su.js +24 -0
  134. package/dist/commands/sudo.d.ts +3 -0
  135. package/dist/commands/sudo.d.ts.map +1 -0
  136. package/dist/commands/sudo.js +47 -0
  137. package/dist/commands/touch.d.ts +3 -0
  138. package/dist/commands/touch.d.ts.map +1 -0
  139. package/dist/commands/touch.js +18 -0
  140. package/dist/commands/tree.d.ts +3 -0
  141. package/dist/commands/tree.d.ts.map +1 -0
  142. package/dist/commands/tree.js +11 -0
  143. package/dist/commands/unset.d.ts +3 -0
  144. package/dist/commands/unset.d.ts.map +1 -0
  145. package/dist/commands/unset.js +15 -0
  146. package/dist/commands/wget.d.ts +3 -0
  147. package/dist/commands/wget.d.ts.map +1 -0
  148. package/dist/commands/wget.js +113 -0
  149. package/dist/commands/who.d.ts +3 -0
  150. package/dist/commands/who.d.ts.map +1 -0
  151. package/dist/commands/who.js +15 -0
  152. package/dist/commands/whoami.d.ts +3 -0
  153. package/dist/commands/whoami.d.ts.map +1 -0
  154. package/dist/commands/whoami.js +5 -0
  155. package/dist/index.d.ts +11 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +7 -0
  158. package/dist/modules/neofetch.d.ts +19 -0
  159. package/dist/modules/neofetch.d.ts.map +1 -0
  160. package/dist/modules/neofetch.js +283 -0
  161. package/dist/modules/shellInteractive.d.ts +6 -0
  162. package/dist/modules/shellInteractive.d.ts.map +1 -0
  163. package/dist/modules/shellInteractive.js +26 -0
  164. package/dist/modules/shellRuntime.d.ts +11 -0
  165. package/dist/modules/shellRuntime.d.ts.map +1 -0
  166. package/dist/modules/shellRuntime.js +52 -0
  167. package/dist/standalone.d.ts +2 -0
  168. package/dist/standalone.d.ts.map +1 -0
  169. package/dist/standalone.js +25 -0
  170. package/dist/types/commands.d.ts +89 -0
  171. package/dist/types/commands.d.ts.map +1 -0
  172. package/dist/types/commands.js +0 -0
  173. package/dist/types/pipeline.d.ts +23 -0
  174. package/dist/types/pipeline.d.ts.map +1 -0
  175. package/dist/types/pipeline.js +0 -0
  176. package/dist/types/streams.d.ts +32 -0
  177. package/dist/types/streams.d.ts.map +1 -0
  178. package/dist/types/streams.js +0 -0
  179. package/dist/types/vfs.d.ts +71 -0
  180. package/dist/types/vfs.d.ts.map +1 -0
  181. package/dist/types/vfs.js +0 -0
  182. package/package.json +4 -2
  183. package/src/VirtualShell/shell.ts +3 -3
  184. package/src/VirtualUserManager/index.ts +18 -0
  185. package/src/commands/index.ts +2 -0
  186. package/src/commands/neofetch.ts +1 -1
  187. package/src/commands/passwd.ts +25 -0
  188. package/{modules → src/modules}/neofetch.ts +55 -52
  189. package/{modules → src/modules}/shellInteractive.ts +16 -4
  190. package/tests/users.test.ts +13 -0
  191. package/tsconfig.json +20 -8
  192. /package/{modules → src/modules}/shellRuntime.ts +0 -0
@@ -0,0 +1,11 @@
1
+ export interface TerminalSize {
2
+ cols: number;
3
+ rows: number;
4
+ }
5
+ export declare function shellQuote(value: string): string;
6
+ export declare function toTtyLines(text: string): string;
7
+ export declare function withTerminalSize(command: string, terminalSize: TerminalSize): string;
8
+ export declare function resolvePath(base: string, inputPath: string): string;
9
+ export declare function collectChildPids(parentPid: number): Promise<number[]>;
10
+ export declare function getVisibleHtopPidList(rootPid?: number): Promise<string | null>;
11
+ //# sourceMappingURL=shellRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellRuntime.d.ts","sourceRoot":"","sources":["../../src/modules/shellRuntime.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,YAAY,GACxB,MAAM,CAUR;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoB3E;AAED,wBAAsB,qBAAqB,CAC1C,OAAO,SAAc,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB"}
@@ -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.1",
7
+ "version": "1.1.3",
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": {
@@ -2,17 +2,17 @@ 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";
5
6
  import {
6
7
  spawnHtopProcess,
7
8
  spawnNanoEditorProcess,
8
- } from "../../modules/shellInteractive";
9
+ } from "../modules/shellInteractive";
9
10
  import {
10
11
  getVisibleHtopPidList,
11
12
  resolvePath,
12
13
  type TerminalSize,
13
14
  toTtyLines,
14
- } from "../../modules/shellRuntime";
15
- import { getCommandNames, runCommand } from "../commands";
15
+ } from "../modules/shellRuntime";
16
16
  import { formatLoginDate } from "../SSHMimic/loginFormat";
17
17
  import { buildPrompt } from "../SSHMimic/prompt";
18
18
  import type { ShellStream } from "../types/streams";
@@ -222,6 +222,24 @@ export class VirtualUserManager {
222
222
  await this.persist();
223
223
  }
224
224
 
225
+ /**
226
+ * Updates password for an existing user account.
227
+ *
228
+ * @param username Username to update.
229
+ * @param password New plaintext password.
230
+ */
231
+ public async setPassword(username: string, password: string): Promise<void> {
232
+ this.validateUsername(username);
233
+ this.validatePassword(password);
234
+
235
+ if (!this.users.has(username)) {
236
+ throw new Error(`passwd: user '${username}' does not exist`);
237
+ }
238
+
239
+ this.users.set(username, this.createRecord(username, password));
240
+ await this.persist();
241
+ }
242
+
225
243
  /**
226
244
  * Deletes existing non-root user account.
227
245
  *
@@ -23,6 +23,7 @@ import { lsCommand } from "./ls";
23
23
  import { mkdirCommand } from "./mkdir";
24
24
  import { nanoCommand } from "./nano";
25
25
  import { neofetchCommand } from "./neofetch";
26
+ import { passwdCommand } from "./passwd";
26
27
  import { pwdCommand } from "./pwd";
27
28
  import { rmCommand } from "./rm";
28
29
  import { setCommand } from "./set";
@@ -53,6 +54,7 @@ const BASE_COMMANDS: ShellModule[] = [
53
54
  neofetchCommand,
54
55
  htopCommand,
55
56
  adduserCommand,
57
+ passwdCommand,
56
58
  deluserCommand,
57
59
  sudoCommand,
58
60
  suCommand,
@@ -1,4 +1,4 @@
1
- import { buildNeofetchOutput } from "../../modules/neofetch";
1
+ import { buildNeofetchOutput } from "../modules/neofetch";
2
2
  import type { ShellModule } from "../types/commands";
3
3
  import { ifFlag } from "./command-helpers";
4
4
  import { getAllEnvVars } from "./set";
@@ -0,0 +1,25 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ export const passwdCommand: ShellModule = {
4
+ name: "passwd",
5
+ params: ["<username> <password>"],
6
+ run: async ({ authUser, args, shell }) => {
7
+ const [username, password] = args;
8
+ if (!username || !password) {
9
+ return {
10
+ stderr: "passwd: usage: passwd <username> <password>",
11
+ exitCode: 1,
12
+ };
13
+ }
14
+
15
+ if (authUser !== "root" && authUser !== username) {
16
+ return { stderr: "passwd: permission denied", exitCode: 1 };
17
+ }
18
+
19
+ await shell.users.setPassword(username, password);
20
+ return {
21
+ stdout: `passwd: password updated for '${username}'`,
22
+ exitCode: 0,
23
+ };
24
+ },
25
+ };
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { ShellProperties } from "../src/VirtualShell";
4
+ import type { ShellProperties } from "../VirtualShell";
5
5
 
6
6
  function formatUptime(seconds: number): string {
7
7
  const totalMinutes = Math.max(1, Math.floor(seconds / 60));
@@ -56,11 +56,11 @@ function colorizeDetailLine(line: string): string {
56
56
  return line;
57
57
  }
58
58
 
59
- const colonIndex = line.indexOf(':');
59
+ const colonIndex = line.indexOf(":");
60
60
 
61
61
  if (colonIndex === -1) {
62
62
  // Pas de ':', chercher '@' pour identifier user@host
63
- if (line.includes('@')) {
63
+ if (line.includes("@")) {
64
64
  // C'est user@host, appliquer dégradé horizontal
65
65
  return applyHorizontalGradient(line);
66
66
  }
@@ -79,8 +79,8 @@ function colorizeDetailLine(line: string): string {
79
79
 
80
80
  function applyHorizontalGradient(text: string): string {
81
81
  // Nettoyer les codes ANSI existants
82
- const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*m`, 'g');
83
- const cleaned = text.replace(ansiRegex, '');
82
+ const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*m`, "g");
83
+ const cleaned = text.replace(ansiRegex, "");
84
84
 
85
85
  if (cleaned.trim().length === 0) {
86
86
  return text;
@@ -88,7 +88,7 @@ function applyHorizontalGradient(text: string): string {
88
88
 
89
89
  const start = { r: 255, g: 255, b: 255 };
90
90
  const end = { r: 168, g: 85, b: 247 };
91
- let result = '';
91
+ let result = "";
92
92
 
93
93
  for (let i = 0; i < cleaned.length; i += 1) {
94
94
  const ratio = cleaned.length <= 1 ? 0 : i / (cleaned.length - 1);
@@ -111,7 +111,7 @@ export interface NeofetchInfo {
111
111
  uptimeSeconds?: number;
112
112
  packages?: string;
113
113
  shell?: string;
114
- shellProps?: ShellProperties;
114
+ shellProps?: ShellProperties;
115
115
  resolution?: string;
116
116
  terminal?: string;
117
117
  cpu?: string;
@@ -180,8 +180,7 @@ function countDpkgPackages(): number | undefined {
180
180
  const data = readFileSync(filePath, "utf8");
181
181
  const matches = data.match(/^Package:\s+/gm);
182
182
  return matches?.length ?? 0;
183
- } catch {
184
- }
183
+ } catch {}
185
184
  }
186
185
 
187
186
  return undefined;
@@ -199,8 +198,7 @@ function countSnapPackages(): number | undefined {
199
198
  const entries = readdirSync(dirPath, { withFileTypes: true });
200
199
  const count = entries.filter((entry) => entry.isDirectory()).length;
201
200
  return count;
202
- } catch {
203
- }
201
+ } catch {}
204
202
  }
205
203
 
206
204
  return undefined;
@@ -250,28 +248,29 @@ function resolveDefaults(info: NeofetchInfo): Required<NeofetchInfo> {
250
248
  const totalMem = os.totalmem();
251
249
  const freeMem = os.freemem();
252
250
  const usedMem = Math.max(0, totalMem - freeMem);
253
- const shellProps = info.shellProps;
254
-
255
- const processUptime = process.uptime();
256
- if (info.uptimeSeconds === undefined) {
257
- info.uptimeSeconds = Math.round(processUptime);
258
- }
251
+ const shellProps = info.shellProps;
259
252
 
260
- console.log("Resolving neofetch info with shellProps:", shellProps);
253
+ const processUptime = process.uptime();
254
+ if (info.uptimeSeconds === undefined) {
255
+ info.uptimeSeconds = Math.round(processUptime);
256
+ }
261
257
 
262
258
  return {
263
259
  user: info.user,
264
260
  host: info.host,
265
- osName: shellProps?.os ?? info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
261
+ osName:
262
+ shellProps?.os ??
263
+ info.osName ??
264
+ `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
266
265
  kernel: shellProps?.kernel ?? info.kernel ?? os.release(),
267
266
  uptimeSeconds: info.uptimeSeconds ?? os.uptime(),
268
267
  packages: info.packages ?? resolvePackagesLabel(),
269
268
  shell: resolveShellLabel(info.shell),
270
- shellProps: info.shellProps as ShellProperties ?? {
271
- kernel: info.kernel ?? os.release(),
272
- os: info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
273
- arch: os.arch(),
274
- },
269
+ shellProps: (info.shellProps as ShellProperties) ?? {
270
+ kernel: info.kernel ?? os.release(),
271
+ os: info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
272
+ arch: os.arch(),
273
+ },
275
274
  resolution: info.resolution ?? "n/a (ssh)",
276
275
  terminal: info.terminal ?? "unknown",
277
276
  cpu: info.cpu ?? resolveCpuLabel(),
@@ -287,32 +286,32 @@ export function buildNeofetchOutput(info: NeofetchInfo): string {
287
286
  const colorBars = buildColorBars();
288
287
 
289
288
  const distroLogo = [
290
- " .. .:. ",
291
- " .::.. .. .. ",
292
- ". .... ... .. ",
293
- ": .... .:. .. ",
294
- ": .:.:........:. .. ",
295
- ": .. ",
296
- ". : ",
297
- ". : ",
298
- ".. : ",
299
- " :. .. ",
300
- " .. .. ",
301
- " :-. :: ",
302
- " .:. :. ",
303
- " ..: ... ",
304
- " ..: :.. ",
305
- " :... :....",
306
- " .. ....",
307
- " . .. ",
308
- " .:. .: ",
309
- " :. .. ",
310
- " ::. .. ",
311
- "..... ..:... ",
312
- "...:. .. ",
313
- ".:...:. ::. .. ",
314
- " ... ..:::::.. ..:::::::.. ",
315
- ]
289
+ " .. .:. ",
290
+ " .::.. .. .. ",
291
+ ". .... ... .. ",
292
+ ": .... .:. .. ",
293
+ ": .:.:........:. .. ",
294
+ ": .. ",
295
+ ". : ",
296
+ ". : ",
297
+ ".. : ",
298
+ " :. .. ",
299
+ " .. .. ",
300
+ " :-. :: ",
301
+ " .:. :. ",
302
+ " ..: ... ",
303
+ " ..: :.. ",
304
+ " :... :....",
305
+ " .. ....",
306
+ " . .. ",
307
+ " .:. .: ",
308
+ " :. .. ",
309
+ " ::. .. ",
310
+ "..... ..:... ",
311
+ "...:. .. ",
312
+ ".:...:. ::. .. ",
313
+ " ... ..:::::.. ..:::::::.. ",
314
+ ];
316
315
 
317
316
  const details = [
318
317
  `${fields.user}@${fields.host}`,
@@ -341,7 +340,11 @@ export function buildNeofetchOutput(info: NeofetchInfo): string {
341
340
  const rawLeft = distroLogo[i] ?? "";
342
341
  const right = details[i] ?? "";
343
342
  if (right.length > 0) {
344
- const left = colorizeLogoLine(rawLeft.padEnd(31, " "), i, distroLogo.length);
343
+ const left = colorizeLogoLine(
344
+ rawLeft.padEnd(31, " "),
345
+ i,
346
+ distroLogo.length,
347
+ );
345
348
  const coloredRight = colorizeDetailLine(right);
346
349
  lines.push(`${left} ${coloredRight}`);
347
350
  continue;
@@ -351,4 +354,4 @@ export function buildNeofetchOutput(info: NeofetchInfo): string {
351
354
  }
352
355
 
353
356
  return lines.join("\n");
354
- }
357
+ }