typescript-virtual-container 1.5.11 → 1.6.1

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 (132) 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 +123 -0
  112. package/dist/modules/VirtualNetworkManager.js +201 -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 +1 -1
@@ -0,0 +1,123 @@
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
+ /**
9
+ * A virtual network interface, either loopback or ethernet,
10
+ * with MAC address, MTU, IPv4/IPv6 addresses, and link state.
11
+ */
12
+ export interface VirtualInterface {
13
+ name: string;
14
+ type: "loopback" | "ether";
15
+ mac: string;
16
+ mtu: number;
17
+ state: "UP" | "DOWN";
18
+ ipv4: string;
19
+ ipv4Mask: number;
20
+ ipv6: string;
21
+ }
22
+ /**
23
+ * A routing table entry mapping a destination subnet
24
+ * to a gateway and outbound device.
25
+ */
26
+ export interface VirtualRoute {
27
+ destination: string;
28
+ gateway: string;
29
+ netmask: string;
30
+ device: string;
31
+ flags: string;
32
+ }
33
+ /**
34
+ * An ARP cache entry mapping an IP address to a MAC address
35
+ * on a specific device, with neighbour reachability state.
36
+ */
37
+ export interface VirtualArpEntry {
38
+ ip: string;
39
+ mac: string;
40
+ device: string;
41
+ state: "REACHABLE" | "STALE" | "PERMANENT";
42
+ }
43
+ /**
44
+ * Virtual network stack with routing table, ARP cache, and interface management.
45
+ * Provides dynamic data for `ip`, `ping`, and `/proc/net/*`.
46
+ */
47
+ export declare class VirtualNetworkManager {
48
+ private interfaces;
49
+ private routes;
50
+ private arpCache;
51
+ /**
52
+ * Returns a copy of all configured interfaces.
53
+ * @returns Array of VirtualInterface objects.
54
+ */
55
+ getInterfaces(): VirtualInterface[];
56
+ /**
57
+ * Returns a copy of the routing table.
58
+ * @returns Array of VirtualRoute objects.
59
+ */
60
+ getRoutes(): VirtualRoute[];
61
+ /**
62
+ * Returns a copy of the ARP cache.
63
+ * @returns Array of VirtualArpEntry objects.
64
+ */
65
+ getArpCache(): VirtualArpEntry[];
66
+ /**
67
+ * Adds a new route to the routing table.
68
+ * @param dest Destination network or "default".
69
+ * @param gateway Gateway IP address.
70
+ * @param netmask Subnet mask (e.g. "255.255.255.0").
71
+ * @param device Outbound device name (e.g. "eth0").
72
+ */
73
+ addRoute(dest: string, gateway: string, netmask: string, device: string): void;
74
+ /**
75
+ * Removes a route by destination network.
76
+ * @param dest Destination network to remove.
77
+ * @returns True if a route was removed, false if no match.
78
+ */
79
+ delRoute(dest: string): boolean;
80
+ /**
81
+ * Sets the administrative state of an interface.
82
+ * @param name Interface name ("lo", "eth0", etc.).
83
+ * @param state Desired state: "UP" or "DOWN".
84
+ * @returns True if the interface was found and updated, false otherwise.
85
+ */
86
+ setInterfaceState(name: string, state: "UP" | "DOWN"): boolean;
87
+ /**
88
+ * Sets the IPv4 address and prefix length on an interface.
89
+ * @param name Interface name.
90
+ * @param ipv4 New IPv4 address.
91
+ * @param mask New subnet mask prefix length (e.g. 24).
92
+ * @returns True if the interface was found and updated, false otherwise.
93
+ */
94
+ setInterfaceIp(name: string, ipv4: string, mask: number): boolean;
95
+ /**
96
+ * Simulates an ICMP ping to the given host.
97
+ * @param host Target IP address or hostname.
98
+ * @returns Latency in milliseconds if reachable, -1 if unreachable.
99
+ */
100
+ ping(host: string): number;
101
+ /**
102
+ * Formats all interfaces as `ip addr` output.
103
+ * @returns Formatted string mimicking `ip addr` command.
104
+ */
105
+ formatIpAddr(): string;
106
+ /**
107
+ * Formats the routing table as `ip route` output.
108
+ * @returns Formatted string mimicking `ip route` command.
109
+ */
110
+ formatIpRoute(): string;
111
+ /**
112
+ * Formats all interfaces as `ip link` output.
113
+ * @returns Formatted string mimicking `ip link` command.
114
+ */
115
+ formatIpLink(): string;
116
+ /**
117
+ * Formats the ARP cache as `ip neigh` output.
118
+ * @returns Formatted string mimicking `ip neigh` command.
119
+ */
120
+ formatIpNeigh(): string;
121
+ private _maskToCidr;
122
+ private _ipForDevice;
123
+ }
@@ -0,0 +1,201 @@
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
+ /**
13
+ * Virtual network stack with routing table, ARP cache, and interface management.
14
+ * Provides dynamic data for `ip`, `ping`, and `/proc/net/*`.
15
+ */
16
+ export class VirtualNetworkManager {
17
+ interfaces = [
18
+ {
19
+ name: "lo",
20
+ type: "loopback",
21
+ mac: "00:00:00:00:00:00",
22
+ mtu: 65536,
23
+ state: "UP",
24
+ ipv4: "127.0.0.1",
25
+ ipv4Mask: 8,
26
+ ipv6: "::1",
27
+ },
28
+ {
29
+ name: "eth0",
30
+ type: "ether",
31
+ mac: randomMac(),
32
+ mtu: 1500,
33
+ state: "UP",
34
+ ipv4: "10.0.0.2",
35
+ ipv4Mask: 24,
36
+ ipv6: "fe80::42:aff:fe00:2",
37
+ },
38
+ ];
39
+ routes = [
40
+ { destination: "default", gateway: "10.0.0.1", netmask: "0.0.0.0", device: "eth0", flags: "UG" },
41
+ { destination: "10.0.0.0", gateway: "0.0.0.0", netmask: "255.255.255.0", device: "eth0", flags: "U" },
42
+ { destination: "127.0.0.0", gateway: "0.0.0.0", netmask: "255.0.0.0", device: "lo", flags: "U" },
43
+ ];
44
+ arpCache = [
45
+ { ip: "10.0.0.1", mac: "02:42:0a:00:00:01", device: "eth0", state: "REACHABLE" },
46
+ ];
47
+ /**
48
+ * Returns a copy of all configured interfaces.
49
+ * @returns Array of VirtualInterface objects.
50
+ */
51
+ getInterfaces() {
52
+ return [...this.interfaces];
53
+ }
54
+ /**
55
+ * Returns a copy of the routing table.
56
+ * @returns Array of VirtualRoute objects.
57
+ */
58
+ getRoutes() {
59
+ return [...this.routes];
60
+ }
61
+ /**
62
+ * Returns a copy of the ARP cache.
63
+ * @returns Array of VirtualArpEntry objects.
64
+ */
65
+ getArpCache() {
66
+ return [...this.arpCache];
67
+ }
68
+ /**
69
+ * Adds a new route to the routing table.
70
+ * @param dest Destination network or "default".
71
+ * @param gateway Gateway IP address.
72
+ * @param netmask Subnet mask (e.g. "255.255.255.0").
73
+ * @param device Outbound device name (e.g. "eth0").
74
+ */
75
+ addRoute(dest, gateway, netmask, device) {
76
+ this.routes.push({ destination: dest, gateway, netmask, device, flags: "UG" });
77
+ }
78
+ /**
79
+ * Removes a route by destination network.
80
+ * @param dest Destination network to remove.
81
+ * @returns True if a route was removed, false if no match.
82
+ */
83
+ delRoute(dest) {
84
+ const idx = this.routes.findIndex((r) => r.destination === dest);
85
+ if (idx === -1)
86
+ return false;
87
+ this.routes.splice(idx, 1);
88
+ return true;
89
+ }
90
+ /**
91
+ * Sets the administrative state of an interface.
92
+ * @param name Interface name ("lo", "eth0", etc.).
93
+ * @param state Desired state: "UP" or "DOWN".
94
+ * @returns True if the interface was found and updated, false otherwise.
95
+ */
96
+ setInterfaceState(name, state) {
97
+ const iface = this.interfaces.find((i) => i.name === name);
98
+ if (!iface)
99
+ return false;
100
+ iface.state = state;
101
+ return true;
102
+ }
103
+ /**
104
+ * Sets the IPv4 address and prefix length on an interface.
105
+ * @param name Interface name.
106
+ * @param ipv4 New IPv4 address.
107
+ * @param mask New subnet mask prefix length (e.g. 24).
108
+ * @returns True if the interface was found and updated, false otherwise.
109
+ */
110
+ setInterfaceIp(name, ipv4, mask) {
111
+ const iface = this.interfaces.find((i) => i.name === name);
112
+ if (!iface)
113
+ return false;
114
+ iface.ipv4 = ipv4;
115
+ iface.ipv4Mask = mask;
116
+ return true;
117
+ }
118
+ /**
119
+ * Simulates an ICMP ping to the given host.
120
+ * @param host Target IP address or hostname.
121
+ * @returns Latency in milliseconds if reachable, -1 if unreachable.
122
+ */
123
+ ping(host) {
124
+ // Loopback always works
125
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
126
+ return 0.05 + Math.random() * 0.1;
127
+ }
128
+ // Check ARP cache / routing
129
+ const arp = this.arpCache.find((e) => e.ip === host);
130
+ if (arp && arp.state === "REACHABLE") {
131
+ return 0.5 + Math.random() * 2;
132
+ }
133
+ // Simulate random packet loss (5%)
134
+ if (Math.random() < 0.05)
135
+ return -1;
136
+ return 0.8 + Math.random() * 5;
137
+ }
138
+ /**
139
+ * Formats all interfaces as `ip addr` output.
140
+ * @returns Formatted string mimicking `ip addr` command.
141
+ */
142
+ formatIpAddr() {
143
+ const lines = [];
144
+ let idx = 1;
145
+ for (const iface of this.interfaces) {
146
+ const flags = iface.state === "UP"
147
+ ? (iface.type === "loopback" ? "LOOPBACK,UP,LOWER_UP" : "BROADCAST,MULTICAST,UP,LOWER_UP")
148
+ : "DOWN";
149
+ lines.push(`${idx}: ${iface.name}: <${flags}> mtu ${iface.mtu} qdisc mq state ${iface.state === "UP" ? "UNKNOWN" : "DOWN"} group default qlen 1000`);
150
+ lines.push(` link/${iface.type === "loopback" ? "loopback" : "ether"} ${iface.mac} brd ff:ff:ff:ff:ff:ff`);
151
+ lines.push(` inet ${iface.ipv4}/${iface.ipv4Mask} scope global ${iface.name}`);
152
+ lines.push(` valid_lft forever preferred_lft forever`);
153
+ lines.push(` inet6 ${iface.ipv6}/64 scope link`);
154
+ lines.push(` valid_lft forever preferred_lft forever`);
155
+ idx++;
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+ /**
160
+ * Formats the routing table as `ip route` output.
161
+ * @returns Formatted string mimicking `ip route` command.
162
+ */
163
+ formatIpRoute() {
164
+ return this.routes.map((r) => {
165
+ if (r.destination === "default") {
166
+ return `default via ${r.gateway} dev ${r.device}`;
167
+ }
168
+ return `${r.destination}/${this._maskToCidr(r.netmask)} dev ${r.device} proto kernel scope link src ${this._ipForDevice(r.device)}`;
169
+ }).join("\n");
170
+ }
171
+ /**
172
+ * Formats all interfaces as `ip link` output.
173
+ * @returns Formatted string mimicking `ip link` command.
174
+ */
175
+ formatIpLink() {
176
+ const lines = [];
177
+ let idx = 1;
178
+ for (const iface of this.interfaces) {
179
+ const flags = iface.state === "UP"
180
+ ? (iface.type === "loopback" ? "LOOPBACK,UP,LOWER_UP" : "BROADCAST,MULTICAST,UP,LOWER_UP")
181
+ : "DOWN";
182
+ lines.push(`${idx}: ${iface.name}: <${flags}> mtu ${iface.mtu} qdisc mq state ${iface.state === "UP" ? "UNKNOWN" : "DOWN"} mode DEFAULT group default qlen 1000`);
183
+ lines.push(` link/${iface.type === "loopback" ? "loopback" : "ether"} ${iface.mac} brd ff:ff:ff:ff:ff:ff`);
184
+ idx++;
185
+ }
186
+ return lines.join("\n");
187
+ }
188
+ /**
189
+ * Formats the ARP cache as `ip neigh` output.
190
+ * @returns Formatted string mimicking `ip neigh` command.
191
+ */
192
+ formatIpNeigh() {
193
+ return this.arpCache.map((e) => `${e.ip} dev ${e.device} lladdr ${e.mac} ${e.state}`).join("\n");
194
+ }
195
+ _maskToCidr(mask) {
196
+ return mask.split(".").reduce((acc, oct) => acc + (parseInt(oct, 10) ? parseInt(oct, 10).toString(2).split("1").length - 1 : 0), 0);
197
+ }
198
+ _ipForDevice(device) {
199
+ return this.interfaces.find((i) => i.name === device)?.ipv4 ?? "0.0.0.0";
200
+ }
201
+ }
@@ -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);