typescript-virtual-container 1.5.10 → 1.6.0

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 (133) hide show
  1. package/README.md +236 -456
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/Honeypot/index.d.ts +9 -0
  4. package/dist/Honeypot/index.js +57 -0
  5. package/dist/SSHMimic/exec.d.ts +4 -0
  6. package/dist/SSHMimic/exec.js +4 -0
  7. package/dist/SSHMimic/executor.d.ts +10 -1
  8. package/dist/SSHMimic/executor.js +18 -8
  9. package/dist/SSHMimic/hostKey.d.ts +5 -0
  10. package/dist/SSHMimic/hostKey.js +5 -0
  11. package/dist/SSHMimic/loginBanner.d.ts +7 -0
  12. package/dist/SSHMimic/loginBanner.js +4 -0
  13. package/dist/SSHMimic/loginFormat.d.ts +4 -0
  14. package/dist/SSHMimic/loginFormat.js +4 -0
  15. package/dist/SSHMimic/prompt.d.ts +9 -0
  16. package/dist/SSHMimic/prompt.js +9 -0
  17. package/dist/SSHMimic/scp.d.ts +18 -0
  18. package/dist/SSHMimic/scp.js +14 -0
  19. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
  20. package/dist/VirtualFileSystem/binaryPack.js +32 -10
  21. package/dist/VirtualFileSystem/index.d.ts +29 -0
  22. package/dist/VirtualFileSystem/index.js +126 -5
  23. package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
  24. package/dist/VirtualFileSystem/journal.d.ts +10 -4
  25. package/dist/VirtualFileSystem/journal.js +12 -2
  26. package/dist/VirtualFileSystem/path.d.ts +23 -1
  27. package/dist/VirtualFileSystem/path.js +23 -3
  28. package/dist/VirtualPackageManager/index.js +1 -1
  29. package/dist/VirtualShell/index.d.ts +3 -0
  30. package/dist/VirtualShell/index.js +12 -3
  31. package/dist/VirtualUserManager/index.d.ts +20 -1
  32. package/dist/VirtualUserManager/index.js +52 -15
  33. package/dist/commands/bc.d.ts +5 -0
  34. package/dist/commands/bc.js +5 -0
  35. package/dist/commands/cat.js +2 -2
  36. package/dist/commands/chgrp.d.ts +7 -0
  37. package/dist/commands/chgrp.js +42 -0
  38. package/dist/commands/chown.d.ts +7 -0
  39. package/dist/commands/chown.js +79 -0
  40. package/dist/commands/cp.js +4 -3
  41. package/dist/commands/dd.d.ts +7 -0
  42. package/dist/commands/dd.js +60 -0
  43. package/dist/commands/declare.js +0 -2
  44. package/dist/commands/expr.d.ts +7 -0
  45. package/dist/commands/expr.js +63 -0
  46. package/dist/commands/fun.d.ts +5 -0
  47. package/dist/commands/fun.js +5 -1
  48. package/dist/commands/help.d.ts +5 -0
  49. package/dist/commands/help.js +5 -0
  50. package/dist/commands/helpers.d.ts +43 -0
  51. package/dist/commands/helpers.js +61 -0
  52. package/dist/commands/id.d.ts +5 -0
  53. package/dist/commands/id.js +5 -0
  54. package/dist/commands/ip.d.ts +1 -0
  55. package/dist/commands/ip.js +50 -23
  56. package/dist/commands/jobs.js +43 -9
  57. package/dist/commands/kill.d.ts +1 -0
  58. package/dist/commands/kill.js +13 -5
  59. package/dist/commands/last.js +1 -1
  60. package/dist/commands/ln.d.ts +5 -0
  61. package/dist/commands/ln.js +5 -0
  62. package/dist/commands/ls.d.ts +5 -0
  63. package/dist/commands/ls.js +19 -4
  64. package/dist/commands/lsb-release.js +1 -1
  65. package/dist/commands/man.d.ts +5 -0
  66. package/dist/commands/man.js +5 -0
  67. package/dist/commands/manuals-bundle.js +242 -0
  68. package/dist/commands/miscutils.d.ts +43 -0
  69. package/dist/commands/miscutils.js +233 -0
  70. package/dist/commands/mkdir.js +3 -2
  71. package/dist/commands/mv.js +4 -3
  72. package/dist/commands/netcat.d.ts +7 -0
  73. package/dist/commands/netcat.js +64 -0
  74. package/dist/commands/nice.d.ts +7 -0
  75. package/dist/commands/nice.js +22 -0
  76. package/dist/commands/nohup.d.ts +7 -0
  77. package/dist/commands/nohup.js +18 -0
  78. package/dist/commands/ping.d.ts +2 -1
  79. package/dist/commands/ping.js +46 -8
  80. package/dist/commands/procUtils.d.ts +13 -0
  81. package/dist/commands/procUtils.js +72 -0
  82. package/dist/commands/pwd.d.ts +5 -0
  83. package/dist/commands/pwd.js +5 -0
  84. package/dist/commands/python.js +0 -4
  85. package/dist/commands/read.js +0 -1
  86. package/dist/commands/registry.d.ts +37 -0
  87. package/dist/commands/registry.js +73 -0
  88. package/dist/commands/rm.js +3 -2
  89. package/dist/commands/runtime.d.ts +47 -1
  90. package/dist/commands/runtime.js +60 -5
  91. package/dist/commands/sh.d.ts +5 -0
  92. package/dist/commands/sh.js +5 -0
  93. package/dist/commands/stat.js +3 -2
  94. package/dist/commands/strace.js +0 -1
  95. package/dist/commands/sysinfo.d.ts +19 -0
  96. package/dist/commands/sysinfo.js +73 -0
  97. package/dist/commands/test.d.ts +5 -0
  98. package/dist/commands/test.js +5 -0
  99. package/dist/commands/textutils.d.ts +25 -0
  100. package/dist/commands/textutils.js +171 -0
  101. package/dist/commands/top.d.ts +7 -0
  102. package/dist/commands/top.js +54 -0
  103. package/dist/commands/touch.js +6 -2
  104. package/dist/commands/tr.d.ts +5 -0
  105. package/dist/commands/tr.js +5 -0
  106. package/dist/commands/w.js +1 -1
  107. package/dist/commands/which.d.ts +5 -0
  108. package/dist/commands/which.js +5 -0
  109. package/dist/index.d.ts +10 -2
  110. package/dist/index.js +4 -0
  111. package/dist/modules/VirtualNetworkManager.d.ts +54 -0
  112. package/dist/modules/VirtualNetworkManager.js +144 -0
  113. package/dist/modules/linuxRootfs.d.ts +4 -3
  114. package/dist/modules/linuxRootfs.js +115 -74
  115. package/dist/modules/neofetch.d.ts +2 -0
  116. package/dist/modules/neofetch.js +3 -2
  117. package/dist/modules/pacmanGame.d.ts +2 -0
  118. package/dist/modules/pacmanGame.js +1 -0
  119. package/dist/modules/shellInteractive.d.ts +2 -0
  120. package/dist/modules/shellInteractive.js +2 -0
  121. package/dist/modules/shellRuntime.d.ts +7 -0
  122. package/dist/modules/shellRuntime.js +6 -0
  123. package/dist/modules/webTermRenderer.js +0 -7
  124. package/dist/types/commands.d.ts +1 -1
  125. package/dist/types/vfs.d.ts +8 -0
  126. package/dist/utils/argv.d.ts +22 -3
  127. package/dist/utils/argv.js +22 -3
  128. package/dist/utils/perfLogger.d.ts +10 -2
  129. package/dist/utils/perfLogger.js +7 -14
  130. package/dist/utils/shellSession.d.ts +35 -0
  131. package/dist/utils/shellSession.js +35 -0
  132. package/package.json +3 -2
  133. package/scripts/postinstall.js +42 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * VirtualNetworkManager — a configurable virtual network stack.
3
+ *
4
+ * Maintains a routing table, ARP cache, and interface list with simulated
5
+ * latency and packet loss. Used by the `ip`, `ping`, and `netstat` commands
6
+ * to produce dynamic, deterministic output instead of hardcoded strings.
7
+ */
8
+ export interface VirtualInterface {
9
+ name: string;
10
+ type: "loopback" | "ether";
11
+ mac: string;
12
+ mtu: number;
13
+ state: "UP" | "DOWN";
14
+ ipv4: string;
15
+ ipv4Mask: number;
16
+ ipv6: string;
17
+ }
18
+ export interface VirtualRoute {
19
+ destination: string;
20
+ gateway: string;
21
+ netmask: string;
22
+ device: string;
23
+ flags: string;
24
+ }
25
+ export interface VirtualArpEntry {
26
+ ip: string;
27
+ mac: string;
28
+ device: string;
29
+ state: "REACHABLE" | "STALE" | "PERMANENT";
30
+ }
31
+ export declare class VirtualNetworkManager {
32
+ private interfaces;
33
+ private routes;
34
+ private arpCache;
35
+ getInterfaces(): VirtualInterface[];
36
+ getRoutes(): VirtualRoute[];
37
+ getArpCache(): VirtualArpEntry[];
38
+ addRoute(dest: string, gateway: string, netmask: string, device: string): void;
39
+ delRoute(dest: string): boolean;
40
+ setInterfaceState(name: string, state: "UP" | "DOWN"): boolean;
41
+ setInterfaceIp(name: string, ipv4: string, mask: number): boolean;
42
+ /** Ping simulation: returns latency in ms, or -1 if unreachable. */
43
+ ping(host: string): number;
44
+ /** Get formatted output for `ip addr`. */
45
+ formatIpAddr(): string;
46
+ /** Get formatted output for `ip route`. */
47
+ formatIpRoute(): string;
48
+ /** Get formatted output for `ip link`. */
49
+ formatIpLink(): string;
50
+ /** Get formatted output for `ip neigh`. */
51
+ formatIpNeigh(): string;
52
+ private _maskToCidr;
53
+ private _ipForDevice;
54
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * VirtualNetworkManager — a configurable virtual network stack.
3
+ *
4
+ * Maintains a routing table, ARP cache, and interface list with simulated
5
+ * latency and packet loss. Used by the `ip`, `ping`, and `netstat` commands
6
+ * to produce dynamic, deterministic output instead of hardcoded strings.
7
+ */
8
+ function randomMac() {
9
+ const hex = () => Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
10
+ return `02:42:${hex()}:${hex()}:${hex()}:${hex()}`;
11
+ }
12
+ export class VirtualNetworkManager {
13
+ interfaces = [
14
+ {
15
+ name: "lo",
16
+ type: "loopback",
17
+ mac: "00:00:00:00:00:00",
18
+ mtu: 65536,
19
+ state: "UP",
20
+ ipv4: "127.0.0.1",
21
+ ipv4Mask: 8,
22
+ ipv6: "::1",
23
+ },
24
+ {
25
+ name: "eth0",
26
+ type: "ether",
27
+ mac: randomMac(),
28
+ mtu: 1500,
29
+ state: "UP",
30
+ ipv4: "10.0.0.2",
31
+ ipv4Mask: 24,
32
+ ipv6: "fe80::42:aff:fe00:2",
33
+ },
34
+ ];
35
+ routes = [
36
+ { destination: "default", gateway: "10.0.0.1", netmask: "0.0.0.0", device: "eth0", flags: "UG" },
37
+ { destination: "10.0.0.0", gateway: "0.0.0.0", netmask: "255.255.255.0", device: "eth0", flags: "U" },
38
+ { destination: "127.0.0.0", gateway: "0.0.0.0", netmask: "255.0.0.0", device: "lo", flags: "U" },
39
+ ];
40
+ arpCache = [
41
+ { ip: "10.0.0.1", mac: "02:42:0a:00:00:01", device: "eth0", state: "REACHABLE" },
42
+ ];
43
+ getInterfaces() {
44
+ return [...this.interfaces];
45
+ }
46
+ getRoutes() {
47
+ return [...this.routes];
48
+ }
49
+ getArpCache() {
50
+ return [...this.arpCache];
51
+ }
52
+ addRoute(dest, gateway, netmask, device) {
53
+ this.routes.push({ destination: dest, gateway, netmask, device, flags: "UG" });
54
+ }
55
+ delRoute(dest) {
56
+ const idx = this.routes.findIndex((r) => r.destination === dest);
57
+ if (idx === -1)
58
+ return false;
59
+ this.routes.splice(idx, 1);
60
+ return true;
61
+ }
62
+ setInterfaceState(name, state) {
63
+ const iface = this.interfaces.find((i) => i.name === name);
64
+ if (!iface)
65
+ return false;
66
+ iface.state = state;
67
+ return true;
68
+ }
69
+ setInterfaceIp(name, ipv4, mask) {
70
+ const iface = this.interfaces.find((i) => i.name === name);
71
+ if (!iface)
72
+ return false;
73
+ iface.ipv4 = ipv4;
74
+ iface.ipv4Mask = mask;
75
+ return true;
76
+ }
77
+ /** Ping simulation: returns latency in ms, or -1 if unreachable. */
78
+ ping(host) {
79
+ // Loopback always works
80
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
81
+ return 0.05 + Math.random() * 0.1;
82
+ }
83
+ // Check ARP cache / routing
84
+ const arp = this.arpCache.find((e) => e.ip === host);
85
+ if (arp && arp.state === "REACHABLE") {
86
+ return 0.5 + Math.random() * 2;
87
+ }
88
+ // Simulate random packet loss (5%)
89
+ if (Math.random() < 0.05)
90
+ return -1;
91
+ return 0.8 + Math.random() * 5;
92
+ }
93
+ /** Get formatted output for `ip addr`. */
94
+ formatIpAddr() {
95
+ const lines = [];
96
+ let idx = 1;
97
+ for (const iface of this.interfaces) {
98
+ const flags = iface.state === "UP"
99
+ ? (iface.type === "loopback" ? "LOOPBACK,UP,LOWER_UP" : "BROADCAST,MULTICAST,UP,LOWER_UP")
100
+ : "DOWN";
101
+ lines.push(`${idx}: ${iface.name}: <${flags}> mtu ${iface.mtu} qdisc mq state ${iface.state === "UP" ? "UNKNOWN" : "DOWN"} group default qlen 1000`);
102
+ lines.push(` link/${iface.type === "loopback" ? "loopback" : "ether"} ${iface.mac} brd ff:ff:ff:ff:ff:ff`);
103
+ lines.push(` inet ${iface.ipv4}/${iface.ipv4Mask} scope global ${iface.name}`);
104
+ lines.push(` valid_lft forever preferred_lft forever`);
105
+ lines.push(` inet6 ${iface.ipv6}/64 scope link`);
106
+ lines.push(` valid_lft forever preferred_lft forever`);
107
+ idx++;
108
+ }
109
+ return lines.join("\n");
110
+ }
111
+ /** Get formatted output for `ip route`. */
112
+ formatIpRoute() {
113
+ return this.routes.map((r) => {
114
+ if (r.destination === "default") {
115
+ return `default via ${r.gateway} dev ${r.device}`;
116
+ }
117
+ return `${r.destination}/${this._maskToCidr(r.netmask)} dev ${r.device} proto kernel scope link src ${this._ipForDevice(r.device)}`;
118
+ }).join("\n");
119
+ }
120
+ /** Get formatted output for `ip link`. */
121
+ formatIpLink() {
122
+ const lines = [];
123
+ let idx = 1;
124
+ for (const iface of this.interfaces) {
125
+ const flags = iface.state === "UP"
126
+ ? (iface.type === "loopback" ? "LOOPBACK,UP,LOWER_UP" : "BROADCAST,MULTICAST,UP,LOWER_UP")
127
+ : "DOWN";
128
+ lines.push(`${idx}: ${iface.name}: <${flags}> mtu ${iface.mtu} qdisc mq state ${iface.state === "UP" ? "UNKNOWN" : "DOWN"} mode DEFAULT group default qlen 1000`);
129
+ lines.push(` link/${iface.type === "loopback" ? "loopback" : "ether"} ${iface.mac} brd ff:ff:ff:ff:ff:ff`);
130
+ idx++;
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ /** Get formatted output for `ip neigh`. */
135
+ formatIpNeigh() {
136
+ return this.arpCache.map((e) => `${e.ip} dev ${e.device} lladdr ${e.mac} ${e.state}`).join("\n");
137
+ }
138
+ _maskToCidr(mask) {
139
+ return mask.split(".").reduce((acc, oct) => acc + (parseInt(oct, 10) ? parseInt(oct, 10).toString(2).split("1").length - 1 : 0), 0);
140
+ }
141
+ _ipForDevice(device) {
142
+ return this.interfaces.find((i) => i.name === device)?.ipv4 ?? "0.0.0.0";
143
+ }
144
+ }
@@ -20,6 +20,7 @@
20
20
  import VirtualFileSystem from "../VirtualFileSystem";
21
21
  import type { ShellProperties } from "../VirtualShell";
22
22
  import type { VirtualActiveSession, VirtualUserManager } from "../VirtualUserManager";
23
+ import type { VirtualNetworkManager } from "./VirtualNetworkManager";
23
24
  /**
24
25
  * Sync `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the
25
26
  * VirtualUserManager's current user list into the VFS.
@@ -32,7 +33,7 @@ export declare function syncEtcPasswd(vfs: VirtualFileSystem, users: VirtualUser
32
33
  *
33
34
  * Safe to call repeatedly — acts as a live kernel state snapshot.
34
35
  */
35
- export declare function refreshProc(vfs: VirtualFileSystem, props: ShellProperties, hostname: string, shellStartTime: number, sessions?: VirtualActiveSession[]): void;
36
+ export declare function refreshProc(vfs: VirtualFileSystem, props: ShellProperties, hostname: string, shellStartTime: number, sessions?: VirtualActiveSession[], network?: VirtualNetworkManager): void;
36
37
  /**
37
38
  * Build or retrieve the static rootfs VFSB snapshot for the given
38
39
  * hostname + ShellProperties combination.
@@ -51,7 +52,7 @@ export declare function getStaticRootfsSnapshot(hostname: string, props: ShellPr
51
52
  * @param shellStartTime Unix ms of shell creation (for uptime).
52
53
  * @param sessions Active sessions (for /proc/<pid> population).
53
54
  */
54
- export declare function bootstrapLinuxRootfs(vfs: VirtualFileSystem, users: VirtualUserManager, hostname: string, props: ShellProperties, shellStartTime: number, sessions?: VirtualActiveSession[]): void;
55
+ export declare function bootstrapLinuxRootfs(vfs: VirtualFileSystem, users: VirtualUserManager, hostname: string, props: ShellProperties, shellStartTime: number, sessions?: VirtualActiveSession[], network?: VirtualNetworkManager): void;
55
56
  /**
56
57
  * Engine for runtimes that want periodic /proc refresh (e.g. web shell
57
58
  * with live `top`/`ps` output). Call `.boot()` once, then `.tick()` on
@@ -63,7 +64,7 @@ export declare function bootstrapLinuxRootfs(vfs: VirtualFileSystem, users: Virt
63
64
  * setInterval(() => engine.tick(shell.listActiveSessions()), 5000);
64
65
  * ```
65
66
  */
66
- export declare function createLinuxRootfsEngine(vfs: VirtualFileSystem, props: ShellProperties, hostname: string, startTime: number): {
67
+ export declare function createLinuxRootfsEngine(vfs: VirtualFileSystem, props: ShellProperties, hostname: string, startTime: number, network?: VirtualNetworkManager): {
67
68
  boot(users: VirtualUserManager, sessions?: VirtualActiveSession[]): void;
68
69
  tick(sessions?: VirtualActiveSession[]): void;
69
70
  };
@@ -505,7 +505,7 @@ function bootProcLog(vfs, props) {
505
505
  *
506
506
  * Safe to call repeatedly — acts as a live kernel state snapshot.
507
507
  */
508
- export function refreshProc(vfs, props, hostname, shellStartTime, sessions = []) {
508
+ export function refreshProc(vfs, props, hostname, shellStartTime, sessions = [], network) {
509
509
  ensureDir(vfs, "/proc");
510
510
  const uptimeSec = Math.floor((Date.now() - shellStartTime) / 1000);
511
511
  const idleSec = Math.floor(uptimeSec * 0.9);
@@ -589,6 +589,59 @@ export function refreshProc(vfs, props, hostname, shellStartTime, sessions = [])
589
589
  const load = (Math.random() * 0.3).toFixed(2);
590
590
  const numProcs = 1 + sessions.length;
591
591
  write(vfs, "/proc/loadavg", `${load} ${load} ${load} ${numProcs}/${numProcs} 1\n`);
592
+ // /proc/stat — CPU statistics
593
+ const cpuCount = os.cpus().length;
594
+ const userJiffies = Math.floor(uptimeSec * 100);
595
+ const niceJiffies = Math.floor(uptimeSec * 2);
596
+ const systemJiffies = Math.floor(uptimeSec * 30);
597
+ const idleJiffies = Math.floor(uptimeSec * 800);
598
+ const iowaitJiffies = Math.floor(uptimeSec * 5);
599
+ const irqJiffies = Math.floor(uptimeSec * 1);
600
+ const softirqJiffies = Math.floor(uptimeSec * 2);
601
+ const stealJiffies = Math.floor(uptimeSec * 0);
602
+ const totalJiffies = userJiffies + niceJiffies + systemJiffies + idleJiffies + iowaitJiffies + irqJiffies + softirqJiffies + stealJiffies;
603
+ const cpuStats = `cpu ${userJiffies} ${niceJiffies} ${systemJiffies} ${idleJiffies} ${iowaitJiffies} ${irqJiffies} ${softirqJiffies} ${stealJiffies} 0 0\n`;
604
+ const perCpuStats = Array.from({ length: cpuCount }, (_, i) => `cpu${i} ${Math.floor(userJiffies / cpuCount)} ${Math.floor(niceJiffies / cpuCount)} ${Math.floor(systemJiffies / cpuCount)} ${Math.floor(idleJiffies / cpuCount)} ${Math.floor(iowaitJiffies / cpuCount)} ${Math.floor(irqJiffies / cpuCount)} ${Math.floor(softirqJiffies / cpuCount)} ${Math.floor(stealJiffies / cpuCount)} 0 0`).join("\n");
605
+ write(vfs, "/proc/stat", `${cpuStats}${perCpuStats}\nintr ${Math.floor(totalJiffies * 2)} 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\nctxt ${Math.floor(totalJiffies * 50)}\nbtime ${Math.floor(shellStartTime / 1000)}\nprocesses ${numProcs + 10}\nprocs_running 1\nprocs_blocked 0\n`);
606
+ // /proc/vmstat — virtual memory stats
607
+ const pgpgin = Math.floor(totalJiffies * 0.5);
608
+ const pgpgout = Math.floor(totalJiffies * 0.3);
609
+ const pswpin = 0;
610
+ const pswpout = 0;
611
+ const pgalloc = Math.floor(totalJiffies * 2);
612
+ const pgfault = pgalloc + Math.floor(totalJiffies * 0.5);
613
+ const pgmajfault = Math.floor(totalJiffies * 0.01);
614
+ write(vfs, "/proc/vmstat", `nr_free_pages ${Math.floor(freeMemKb / 4)}\nnr_zone_inactive_anon 0\nnr_zone_active_anon 0\nnr_zone_inactive_file ${Math.floor(cachedKb / 4)}\nnr_zone_active_file ${Math.floor(buffersKb / 4)}\nnr_zone_unevictable 0\nnr_zone_write_pending 0\nnr_mlock 0\nnr_page_table_pages ${pageTablesKb}\nnr_kernel_stack ${Math.floor(totalMemKb * 0.0005)}\nnr_bounce 0\nnr_zspages 0\nnr_free_cma 0\nnuma_hit ${Math.floor(totalJiffies * 3)}\nnuma_miss 0\nnuma_foreign 0\nnuma_interleave 0\nnuma_local ${Math.floor(totalJiffies * 3)}\nnuma_other 0\nnr_inactive_anon 0\nnr_active_anon 0\nnr_inactive_file ${Math.floor(cachedKb / 4)}\nnr_active_file ${Math.floor(buffersKb / 4)}\nnr_unevictable 0\nnr_slab_reclaimable ${Math.floor(slabKb * 0.6)}\nnr_slab_unreclaimable ${Math.floor(slabKb * 0.4)}\nnr_isolated_anon 0\nnr_isolated_file 0\nworkingset_nodes 0\nworkingset_refault 0\nworkingset_activate 0\nworkingset_restore 0\nworkingset_nodereclaim 0\nnr_anon_pages ${Math.floor(totalMemKb * 0.001)}\nnr_mapped ${Math.floor(cachedKb * 0.4)}\nnr_file_pages ${Math.floor(cachedKb * 0.8)}\nnr_dirty ${Math.floor(totalMemKb * 0.001)}\nnr_writeback 0\nnr_writeback_temp 0\nnr_shmem ${Math.floor(totalMemKb * 0.005)}\nnr_shmem_hugepages 0\nnr_shmem_pmdmapped 0\nnr_file_hugepages 0\nnr_file_pmdmapped 0\nnr_anon_transparent_hugepages 0\nnr_vmscan_write 0\nnr_vmscan_immediate_reclaim 0\nnr_dirtied ${Math.floor(totalJiffies * 2)}\nnr_written ${Math.floor(totalJiffies * 2)}\nnr_throttled_written 0\nnr_kernel_misc_reclaimable 0\nnr_reclaim_pages 0\nnr_zone_active_anon 0\nnr_zone_active_file ${Math.floor(buffersKb / 4)}\npgpgin ${pgpgin}\npgpgout ${pgpgout}\npswpin ${pswpin}\npswpout ${pswpout}\npgalloc_dma 0\npgalloc_dma32 ${Math.floor(pgalloc * 0.3)}\npgalloc_normal ${Math.floor(pgalloc * 0.7)}\npgalloc_movable 0\npgfree ${pgalloc}\npgactivate ${Math.floor(totalJiffies * 0.5)}\npgdeactivate 0\npgfault ${pgfault}\npgmajfault ${pgmajfault}\npglazyfree 0\npgrefill 0\npgsteal_kswapd 0\npgsteal_direct 0\npgscan_kswapd 0\npgscan_direct 0\npgskip_dma 0\npgskip_dma32 0\npgskip_normal 0\npgskip_movable 0\npgmigrate_success 0\npgmigrate_fail 0\ncompact_migrate_scanned 0\ncompact_free_scanned 0\ncompact_isolated 0\ncompact_stall 0\ncompact_fail 0\ncompact_success 0\nhtlb_buddy_alloc_success 0\nhtlb_buddy_alloc_fail 0\nunevictable_pgs_culled 0\nunevictable_pgs_scanned 0\nunevictable_pgs_rescued 0\nunevictable_pgs_mlocked 0\nunevictable_pgs_munlocked 0\nunevictable_pgs_cleared 0\nunevictable_pgs_stranded 0\nswap_ra 0\nswap_ra_hit 0\nnr_hugepages 0\nnr_hugepages_bootmem 0\n\n`);
615
+ // /proc/pressure — PSI (Pressure Stall Information)
616
+ ensureDir(vfs, "/proc/pressure");
617
+ const someAvg10 = (Math.random() * 0.3).toFixed(2);
618
+ const someAvg60 = (Math.random() * 0.2 + 0.1).toFixed(2);
619
+ const someAvg300 = (Math.random() * 0.1 + 0.05).toFixed(2);
620
+ const someTotal = Math.floor(totalJiffies * 10);
621
+ write(vfs, "/proc/pressure/cpu", `some avg10=${someAvg10} avg60=${someAvg60} avg300=${someAvg300} total=${someTotal}\n`);
622
+ write(vfs, "/proc/pressure/memory", `some avg10=${(Number(someAvg10) * 0.5).toFixed(2)} avg60=${(Number(someAvg60) * 0.3).toFixed(2)} avg300=${(Number(someAvg300) * 0.2).toFixed(2)} total=${Math.floor(someTotal * 0.3)}\n`);
623
+ write(vfs, "/proc/pressure/io", `some avg10=${(Number(someAvg10) * 0.7).toFixed(2)} avg60=${(Number(someAvg60) * 0.5).toFixed(2)} avg300=${(Number(someAvg300) * 0.3).toFixed(2)} total=${Math.floor(someTotal * 0.5)}\n`);
624
+ // /proc/modules
625
+ write(vfs, "/proc/modules", `${[
626
+ "virtio 163840 10 - Live 0x0000000000000000",
627
+ "virtio_ring 28672 10 virtio, Live 0x0000000000000000",
628
+ "virtio_blk 20480 10 - Live 0x0000000000000000",
629
+ "virtio_net 57344 10 - Live 0x0000000000000000",
630
+ "virtio_console 28672 10 - Live 0x0000000000000000",
631
+ "virtio_pci 24576 10 - Live 0x0000000000000000",
632
+ "virtio_pci_legacy_dev 12288 1 virtio_pci, Live 0x0000000000000000",
633
+ "virtio_pci_modern_dev 16384 1 virtio_pci, Live 0x0000000000000000",
634
+ "ext4 847872 10 - Live 0x0000000000000000",
635
+ "jbd2 131072 1 ext4, Live 0x0000000000000000",
636
+ "mbcache 16384 1 ext4, Live 0x0000000000000000",
637
+ "fuse 172032 10 - Live 0x0000000000000000",
638
+ "overlay 131072 10 - Live 0x0000000000000000",
639
+ "nf_tables 188416 10 - Live 0x0000000000000000",
640
+ "tun 49152 10 - Live 0x0000000000000000",
641
+ "bridge 286720 10 - Live 0x0000000000000000",
642
+ "dm_mod 155648 10 - Live 0x0000000000000000",
643
+ "crc32c_intel 24576 10 - Live 0x0000000000000000",
644
+ ].join("\n")}\n`);
592
645
  // /proc/cmdline — Firecracker boot args
593
646
  write(vfs, "/proc/cmdline", `console=ttyS0 reboot=k panic=1 nomodule random.trust_cpu=1 ipv6.disable=1 swiotlb=noforce rdinit=/process_api init_on_free=1 -- --firecracker-init --addr 0.0.0.0:2024 --max-ws-buffer-size 32768 --block-local-connections\n`);
594
647
  // /proc/filesystems — matching real container
@@ -651,26 +704,69 @@ export function refreshProc(vfs, props, hostname, shellStartTime, sessions = [])
651
704
  write(vfs, "/proc/mounts", mountsContent);
652
705
  ensureDir(vfs, "/proc/self");
653
706
  write(vfs, "/proc/self/mounts", mountsContent);
654
- // /proc/net
707
+ // /proc/net — dynamic when network manager is available
655
708
  ensureDir(vfs, "/proc/net");
656
- write(vfs, "/proc/net/dev", `${[
657
- "Inter-| Receive | Transmit",
658
- " face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed",
659
- " lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
660
- " eth0: 128628 1230 0 19 0 0 0 0 52027469 2045 0 0 0 0 0 0",
661
- ].join("\n")}\n`);
709
+ if (network) {
710
+ const ifaces = network.getInterfaces();
711
+ const routes = network.getRoutes();
712
+ const arpCache = network.getArpCache();
713
+ const ipToHex = (ip) => ip.split(".")
714
+ .reverse()
715
+ .map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
716
+ .join("")
717
+ .toUpperCase();
718
+ // /proc/net/dev
719
+ const devHeader = "Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed";
720
+ const devLines = ifaces.map((iface) => {
721
+ const name = iface.name.padStart(4);
722
+ if (iface.name === "lo") {
723
+ return `${name}: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0`;
724
+ }
725
+ const rxBytes = Math.floor(Math.random() * 200000);
726
+ const rxPkts = Math.floor(Math.random() * 2000);
727
+ const txBytes = Math.floor(Math.random() * 50000000);
728
+ const txPkts = Math.floor(Math.random() * 3000);
729
+ return `${name}: ${String(rxBytes).padStart(8)} ${String(rxPkts).padStart(7)} 0 0 0 0 0 0 ${String(txBytes).padStart(9)} ${String(txPkts).padStart(7)} 0 0 0 0 0 0`;
730
+ });
731
+ write(vfs, "/proc/net/dev", `${devHeader}\n${devLines.join("\n")}\n`);
732
+ // /proc/net/route
733
+ const routeLines = routes.map((r) => [
734
+ r.device,
735
+ ipToHex(r.destination === "default" ? "0.0.0.0" : r.destination),
736
+ ipToHex(r.gateway),
737
+ r.flags === "UG"
738
+ ? "0003"
739
+ : r.flags === "U"
740
+ ? "0001"
741
+ : "0000",
742
+ "0", "0", "100",
743
+ ipToHex(r.netmask),
744
+ "0", "0", "0",
745
+ ].join("\t"));
746
+ write(vfs, "/proc/net/route", `Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n${routeLines.join("\n")}\n`);
747
+ // /proc/net/arp
748
+ const arpLines = arpCache.map((e) => `${e.ip.padEnd(15)} 0x1 0x2 ${e.mac.padEnd(17)} * ${e.device}`);
749
+ write(vfs, "/proc/net/arp", `IP address HW type Flags HW address Mask Device\n${arpLines.join("\n")}\n`);
750
+ }
751
+ else {
752
+ write(vfs, "/proc/net/dev", "Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n eth0: 128628 1230 0 19 0 0 0 0 52027469 2045 0 0 0 0 0 0\n");
753
+ write(vfs, "/proc/net/route", "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\neth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\neth0\t0000A8C0\t00000000\t0001\t0\t0\t100\t00FFFFFF\t0\t0\t0\n");
754
+ write(vfs, "/proc/net/arp", "IP address HW type Flags HW address Mask Device\n");
755
+ }
662
756
  write(vfs, "/proc/net/if_inet6", "");
663
- write(vfs, "/proc/net/tcp", " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n");
757
+ // /proc/net/tcp fake listening sockets
758
+ const tcpListen = [
759
+ " 0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 10000 1 0000000000000000 100 0 0 10 0",
760
+ " 1: 00000000:022D 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 10001 1 0000000000000000 100 0 0 10 0",
761
+ " 2: 00000000:0A8C 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 10002 1 0000000000000000 100 0 0 10 0",
762
+ ].join("\n");
763
+ write(vfs, "/proc/net/tcp", ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n${tcpListen}\n`);
664
764
  write(vfs, "/proc/net/tcp6", " sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n");
665
765
  write(vfs, "/proc/net/udp", " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n");
666
- write(vfs, "/proc/net/route", "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n" +
667
- "eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\n" +
668
- "eth0\t0000A8C0\t00000000\t0001\t0\t0\t100\t00FFFFFF\t0\t0\t0\n");
669
- write(vfs, "/proc/net/arp", "IP address HW type Flags HW address Mask Device\n");
670
766
  write(vfs, "/proc/net/fib_trie", "Local:\n +-- 0.0.0.0/0 3 0 5\n");
671
767
  write(vfs, "/proc/net/unix", "Num RefCount Protocol Flags Type St Inode Path\n" +
672
768
  "0000000000000000: 00000002 00000000 00010000 0001 01 10000 /run/dbus/system_bus_socket\n");
673
- write(vfs, "/proc/net/sockstat", "sockets: used 8\nTCP: inuse 0 orphan 0 tw 0 alloc 0 mem 0\nUDP: inuse 0 mem 0\nUDPLITE: inuse 0\nRAW: inuse 0\nFRAG: inuse 0 memory 0\n");
769
+ write(vfs, "/proc/net/sockstat", "sockets: used 11\nTCP: inuse 3 orphan 0 tw 0 alloc 3 mem 1024\nUDP: inuse 0 mem 0\nUDPLITE: inuse 0\nRAW: inuse 0\nFRAG: inuse 0 memory 0\n");
674
770
  // /proc/partitions — virtio block devices
675
771
  write(vfs, "/proc/partitions", `${[
676
772
  "major minor #blocks name",
@@ -994,61 +1090,6 @@ function bootstrapUsr(vfs) {
994
1090
  "hostname", "uname", "ps", "kill", "df", "du", "curl", "wget",
995
1091
  "nano", "diff", "uniq", "xargs", "base64",
996
1092
  ];
997
- // From a real container
998
- // const builtins = [
999
- // // core
1000
- // "sh", "bash", "dash",
1001
- // "ls", "cat", "echo", "grep", "find", "sort",
1002
- // "head", "tail", "cut", "tr", "sed", "awk", "mawk", "gawk",
1003
- // "wc", "tee", "tar", "gzip", "gunzip", "bzip2", "xz",
1004
- // "touch", "mkdir", "rm", "mv", "cp", "ln", "pwd",
1005
- // "chmod", "chown", "chgrp", "env", "date", "sleep",
1006
- // "id", "whoami", "hostname", "uname", "ps", "kill",
1007
- // "df", "du", "dd", "stat", "file",
1008
- // // net
1009
- // "curl", "wget", "nc", "netcat", "ss", "ip",
1010
- // // editors
1011
- // "nano", "vi",
1012
- // // text
1013
- // "diff", "uniq", "xargs", "base64", "md5sum", "sha256sum",
1014
- // "strings", "hexdump", "od", "column", "fmt", "paste",
1015
- // "join", "comm", "split", "csplit", "fold", "expand",
1016
- // // archive
1017
- // "zip", "unzip",
1018
- // // process
1019
- // "top", "htop", "free", "uptime", "dmesg", "lsof",
1020
- // "strace", "ltrace", "pgrep", "pkill", "nohup", "nice",
1021
- // // fs
1022
- // "mount", "umount", "lsblk", "fdisk", "blkid", "e2fsck",
1023
- // // misc
1024
- // "bc", "expr", "seq", "yes", "true", "false", "test",
1025
- // "readlink", "realpath", "dirname", "basename", "mktemp",
1026
- // "install", "make",
1027
- // // dev tools
1028
- // "gcc", "gcc-13", "g++", "g++-13", "cpp", "as", "ld",
1029
- // "ar", "nm", "objdump", "objcopy", "strip", "size",
1030
- // "cc", "c++", "pkg-config",
1031
- // // package
1032
- // "apt", "apt-get", "apt-cache", "dpkg", "dpkg-query",
1033
- // "lsb_release", "add-apt-repository",
1034
- // // scripting
1035
- // "perl", "python3", "python3.12", "pipx",
1036
- // // node/npm
1037
- // "node", "npm", "npx",
1038
- // // java
1039
- // "java", "javac", "jar", "javadoc",
1040
- // // security
1041
- // "openssl", "gpg", "gpg2", "gpgv", "ssh", "ssh-keygen",
1042
- // "sudo", "su", "passwd", "adduser", "useradd",
1043
- // // misc system
1044
- // "systemctl", "journalctl", "loginctl",
1045
- // "timedatectl", "localectl",
1046
- // "lshw", "lscpu", "lsusb", "lspci",
1047
- // // text proc
1048
- // "jq", "xmllint", "pandoc",
1049
- // // multimedia
1050
- // "ffmpeg",
1051
- // ];
1052
1093
  for (const bin of builtins) {
1053
1094
  ensureFile(vfs, `/usr/bin/${bin}`, `#!/bin/sh\nexec builtin ${bin} "$@"\n`, 0o755);
1054
1095
  }
@@ -1547,7 +1588,7 @@ export function getStaticRootfsSnapshot(hostname, props) {
1547
1588
  * @param shellStartTime Unix ms of shell creation (for uptime).
1548
1589
  * @param sessions Active sessions (for /proc/<pid> population).
1549
1590
  */
1550
- export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime, sessions = []) {
1591
+ export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime, sessions = [], network) {
1551
1592
  const snapshot = getStaticRootfsSnapshot(hostname, props);
1552
1593
  const hasRestoredData = vfs.getMode() === "fs" && vfs.exists("/home");
1553
1594
  if (hasRestoredData) {
@@ -1557,7 +1598,7 @@ export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime
1557
1598
  vfs.importRootTree(decodeVfs(snapshot));
1558
1599
  }
1559
1600
  bootstrapRoot(vfs);
1560
- refreshProc(vfs, props, hostname, shellStartTime, sessions);
1601
+ refreshProc(vfs, props, hostname, shellStartTime, sessions, network);
1561
1602
  syncEtcPasswd(vfs, users);
1562
1603
  }
1563
1604
  // ─── optional live engine ─────────────────────────────────────────────────────
@@ -1572,13 +1613,13 @@ export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime
1572
1613
  * setInterval(() => engine.tick(shell.listActiveSessions()), 5000);
1573
1614
  * ```
1574
1615
  */
1575
- export function createLinuxRootfsEngine(vfs, props, hostname, startTime) {
1616
+ export function createLinuxRootfsEngine(vfs, props, hostname, startTime, network) {
1576
1617
  return {
1577
1618
  boot(users, sessions = []) {
1578
- bootstrapLinuxRootfs(vfs, users, hostname, props, startTime, sessions);
1619
+ bootstrapLinuxRootfs(vfs, users, hostname, props, startTime, sessions, network);
1579
1620
  },
1580
1621
  tick(sessions = []) {
1581
- refreshProc(vfs, props, hostname, startTime, sessions);
1622
+ refreshProc(vfs, props, hostname, startTime, sessions, network);
1582
1623
  },
1583
1624
  };
1584
1625
  }
@@ -1,4 +1,5 @@
1
1
  import type { ShellProperties } from "../VirtualShell";
2
+ /** System info fields used to render neofetch output. */
2
3
  export interface NeofetchInfo {
3
4
  user: string;
4
5
  host: string;
@@ -15,4 +16,5 @@ export interface NeofetchInfo {
15
16
  memoryUsedMiB?: number;
16
17
  memoryTotalMiB?: number;
17
18
  }
19
+ /** Builds the full neofetch ASCII art system-info string. */
18
20
  export declare function buildNeofetchOutput(info: NeofetchInfo): string;
@@ -133,7 +133,7 @@ function countDpkgPackages() {
133
133
  const matches = data.match(/^Package:\s+/gm);
134
134
  return matches?.length ?? 0;
135
135
  }
136
- catch { }
136
+ catch { /* dpkg status file may not exist */ }
137
137
  }
138
138
  return undefined;
139
139
  }
@@ -148,7 +148,7 @@ function countSnapPackages() {
148
148
  const count = entries.filter((entry) => entry.isDirectory()).length;
149
149
  return count;
150
150
  }
151
- catch { }
151
+ catch { /* snap directory may not be readable */ }
152
152
  }
153
153
  return undefined;
154
154
  }
@@ -216,6 +216,7 @@ function resolveDefaults(info) {
216
216
  memoryTotalMiB: info.memoryTotalMiB ?? toMiB(totalMem),
217
217
  };
218
218
  }
219
+ /** Builds the full neofetch ASCII art system-info string. */
219
220
  export function buildNeofetchOutput(info) {
220
221
  const fields = resolveDefaults(info);
221
222
  const uptime = formatUptime(fields.uptimeSeconds);
@@ -1,10 +1,12 @@
1
1
  import type { ShellStream } from "../types/streams";
2
2
  import type { TerminalSize } from "./shellRuntime";
3
+ /** Configuration options for creating a PacmanGame instance. */
3
4
  export interface PacmanGameOptions {
4
5
  stream: ShellStream;
5
6
  terminalSize: TerminalSize;
6
7
  onExit: () => void;
7
8
  }
9
+ /** Classic Pacman game that runs in the terminal with ANSI rendering. */
8
10
  export declare class PacmanGame {
9
11
  private stream;
10
12
  private onExit;
@@ -92,6 +92,7 @@ const DR = [0, 1, 0, -1];
92
92
  const DC = [1, 0, -1, 0];
93
93
  const OPP = [2, 3, 0, 1];
94
94
  // ── PacmanGame ────────────────────────────────────────────────────────────────
95
+ /** Classic Pacman game that runs in the terminal with ANSI rendering. */
95
96
  export class PacmanGame {
96
97
  stream;
97
98
  onExit;
@@ -1,5 +1,7 @@
1
1
  import { type ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import type { ShellStream } from "../types/streams";
3
3
  import { type TerminalSize } from "./shellRuntime";
4
+ /** Spawns the nano editor as an interactive subprocess. */
4
5
  export declare function spawnNanoEditorProcess(tempPath: string, terminalSize: TerminalSize, stream: ShellStream): ChildProcessWithoutNullStreams;
6
+ /** Spawns htop as an interactive subprocess, filtered to the given PIDs. */
5
7
  export declare function spawnHtopProcess(pidList: string, terminalSize: TerminalSize, stream: ShellStream): ChildProcessWithoutNullStreams;
@@ -18,9 +18,11 @@ function spawnScriptProcess(command, terminalSize, stream) {
18
18
  });
19
19
  return proc;
20
20
  }
21
+ /** Spawns the nano editor as an interactive subprocess. */
21
22
  export function spawnNanoEditorProcess(tempPath, terminalSize, stream) {
22
23
  return spawnScriptProcess(`nano -- ${shellQuote(tempPath)}`, terminalSize, stream);
23
24
  }
25
+ /** Spawns htop as an interactive subprocess, filtered to the given PIDs. */
24
26
  export function spawnHtopProcess(pidList, terminalSize, stream) {
25
27
  return spawnScriptProcess(`htop -p ${shellQuote(pidList)}`, terminalSize, stream);
26
28
  }
@@ -1,10 +1,17 @@
1
+ /** Terminal dimensions (columns × rows). */
1
2
  export interface TerminalSize {
2
3
  cols: number;
3
4
  rows: number;
4
5
  }
6
+ /** Shell-escapes a string for safe use in a command line. */
5
7
  export declare function shellQuote(value: string): string;
8
+ /** Converts line endings to CRLF for TTY output. */
6
9
  export declare function toTtyLines(text: string): string;
10
+ /** Wraps a command to run with a specific terminal size via stty. */
7
11
  export declare function withTerminalSize(command: string, terminalSize: TerminalSize): string;
12
+ /** Resolves a path relative to a base working directory. */
8
13
  export declare function resolvePath(base: string, inputPath: string): string;
14
+ /** Recursively collects all child PIDs of a given parent process. */
9
15
  export declare function collectChildPids(parentPid: number): Promise<number[]>;
16
+ /** Returns a comma-separated PID list visible in htop, or null if none. */
10
17
  export declare function getVisibleHtopPidList(rootPid?: number): Promise<string | null>;
@@ -1,14 +1,17 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ /** Shell-escapes a string for safe use in a command line. */
3
4
  export function shellQuote(value) {
4
5
  return `'${value.replace(/'/g, `'\\''`)}'`;
5
6
  }
7
+ /** Converts line endings to CRLF for TTY output. */
6
8
  export function toTtyLines(text) {
7
9
  return text
8
10
  .replace(/\r\n/g, "\n")
9
11
  .replace(/\r/g, "\n")
10
12
  .replace(/\n/g, "\r\n");
11
13
  }
14
+ /** Wraps a command to run with a specific terminal size via stty. */
12
15
  export function withTerminalSize(command, terminalSize) {
13
16
  const cols = Number.isFinite(terminalSize.cols) && terminalSize.cols > 0
14
17
  ? Math.floor(terminalSize.cols)
@@ -18,6 +21,7 @@ export function withTerminalSize(command, terminalSize) {
18
21
  : 24;
19
22
  return `stty cols ${cols} rows ${rows} 2>/dev/null; ${command}`;
20
23
  }
24
+ /** Resolves a path relative to a base working directory. */
21
25
  export function resolvePath(base, inputPath) {
22
26
  if (!inputPath || inputPath.trim() === "" || inputPath === ".") {
23
27
  return base;
@@ -26,6 +30,7 @@ export function resolvePath(base, inputPath) {
26
30
  ? path.posix.normalize(inputPath)
27
31
  : path.posix.normalize(path.posix.join(base, inputPath));
28
32
  }
33
+ /** Recursively collects all child PIDs of a given parent process. */
29
34
  export async function collectChildPids(parentPid) {
30
35
  try {
31
36
  const childrenRaw = await readFile(`/proc/${parentPid}/task/${parentPid}/children`, "utf8");
@@ -42,6 +47,7 @@ export async function collectChildPids(parentPid) {
42
47
  return [];
43
48
  }
44
49
  }
50
+ /** Returns a comma-separated PID list visible in htop, or null if none. */
45
51
  export async function getVisibleHtopPidList(rootPid = process.pid) {
46
52
  const descendants = await collectChildPids(rootPid);
47
53
  const unique = Array.from(new Set(descendants)).sort((a, b) => a - b);
@@ -392,13 +392,6 @@ export class WebTermRenderer {
392
392
  return html;
393
393
  }
394
394
  }
395
- // const ANSI_NORMAL_TO_BRIGHT: Record<string, string> = {
396
- // "#000": "#555", "#c00": "#f55", "#0c0": "#5f5", "#cc0": "#ff5",
397
- // "#00c": "#55f", "#c0c": "#f5f", "#0cc": "#5ff", "#ccc": "#fff",
398
- // };
399
- // function boldBright(fg: string): string {
400
- // return ANSI_NORMAL_TO_BRIGHT[fg] ?? fg;
401
- // }
402
395
  function escHtml(ch) {
403
396
  if (ch === "&")
404
397
  return "&amp;";