about-system 0.0.18 → 0.0.21

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.
@@ -7,53 +7,46 @@
7
7
  * @module system-info-api
8
8
  * @author vtempest
9
9
  * @license rights.institute/prosper
10
- *
11
- * @example
12
- * ```typescript
13
- * import { getSystemInfo } from 'about-system';
14
- *
15
- * const info = await getSystemInfo();
16
- * console.log(info.cpu); // "AMD Ryzen 9 5900HX"
17
- * console.log(info.ram_used); // "8/16GB"
18
- * console.log(info.platform); // "linux"
19
- * ```
20
10
  */
21
11
 
22
12
  import os from "os";
23
13
  import fs from "fs";
24
- import path from "path";
25
- import { execSync } from "child_process";
26
14
  import https from "https";
27
- import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types.js";
28
-
29
- /**
30
- * Cache file location in system's temporary directory
31
- * @constant
32
- */
33
- const CACHE_FILE = path.join(os.tmpdir(), "systeminfo-cache.json");
15
+ import path from "path";
16
+ import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types";
17
+ import { CACHE_FILE } from "./info/settings"; // Using CACHE_FILE from settings to ensure consistency
18
+
19
+ // Import info functions from modules
20
+ import { user, hostname, os_info, kernel, device } from "./info/platform";
21
+ import { cpu, gpu, screen_resolution } from "./info/hardware";
22
+ import {
23
+ disk_used,
24
+ ram_used,
25
+ memory_available,
26
+ swap_used,
27
+ mount_points,
28
+ } from "./info/memory";
29
+ import { top_process, uptime, users_logged_in } from "./info/process";
30
+ import {
31
+ ip,
32
+ iplocal,
33
+ city,
34
+ domain,
35
+ isp,
36
+ network_interfaces,
37
+ ports,
38
+ } from "./info/network";
39
+ import {
40
+ services_running,
41
+ temperature,
42
+ battery,
43
+ load_average,
44
+ } from "./info/system-status";
45
+ import { shell, packages, containers } from "./info/software";
34
46
 
35
47
  /**
36
48
  * Cache duration configuration for different system information types
37
49
  * Values are in milliseconds
38
- *
39
- * @constant
40
- * @property {number} ip - IP information cache (5 minutes)
41
- * @property {number} cpu - CPU information cache (24 hours)
42
- * @property {number} gpu - GPU information cache (24 hours)
43
- * @property {number} os - OS information cache (24 hours)
44
- * @property {number} device - Device information cache (24 hours)
45
- * @property {number} kernel - Kernel information cache (1 hour)
46
- * @property {number} pacman - Package managers cache (10 minutes)
47
- * @property {number} ports - Open ports cache (5 minutes)
48
- * @property {number} containers - Docker containers cache (5 minutes)
49
- * @property {number} top_process - Top process cache (5 seconds)
50
- * @property {number} disk_used - Disk usage cache (1 minute)
51
- * @property {number} ram_used - RAM usage cache (10 seconds)
52
- * @property {number} services_running - Services cache (5 minutes)
53
- * @property {number} temperature - Temperature cache (30 seconds)
54
- * @property {number} battery - Battery status cache (1 minute)
55
- * @property {number} network_interfaces - Network interfaces cache (5 minutes)
56
- * @property {number} mount_points - Mount points cache (10 minutes)
57
50
  */
58
51
  const CACHE_DURATION = {
59
52
  ip: 5 * 60 * 1000,
@@ -84,30 +77,24 @@ const IS_LINUX = os.platform() === "linux";
84
77
 
85
78
  /**
86
79
  * Default IPInfo.io API token for geolocation
87
- * @constant
88
80
  */
89
81
  const DEFAULT_IPINFO_TOKEN = "da2d6cc4baa5d1";
90
82
 
91
83
  /**
92
84
  * Default network request timeout in milliseconds
93
- * @constant
94
85
  */
95
86
  const DEFAULT_NETWORK_TIMEOUT = 5000;
96
87
 
97
88
  /**
98
89
  * Represents a cached value with timestamp
99
- * @interface CacheEntry
100
90
  */
101
91
  interface CacheEntry {
102
- /** The cached value */
103
92
  value: any;
104
- /** Unix timestamp when the value was cached */
105
93
  timestamp: number;
106
94
  }
107
95
 
108
96
  /**
109
97
  * Cache storage structure
110
- * @interface Cache
111
98
  */
112
99
  interface Cache {
113
100
  [key: string]: CacheEntry;
@@ -115,33 +102,24 @@ interface Cache {
115
102
 
116
103
  /**
117
104
  * IP information from ipinfo.io API
118
- * @interface IPInfo
119
105
  */
120
106
  interface IPInfo {
121
- /** Public IP address */
122
107
  ip?: string;
123
- /** City location */
124
108
  city?: string;
125
- /** Reverse DNS hostname */
126
109
  hostname?: string;
127
- /** ISP organization string (includes AS number) */
128
110
  org?: string;
129
111
  }
130
112
 
131
113
  /**
132
114
  * Context object passed to info collection functions
133
- * @interface InfoContext
134
115
  */
135
116
  interface InfoContext {
136
- /** Cache storage */
137
117
  cache: Cache;
138
- /** IP information from external API */
139
118
  ipInfo?: IPInfo;
140
119
  }
141
120
 
142
121
  /**
143
122
  * Loads cache from disk
144
- * @returns {Cache} Cached data or empty object if cache doesn't exist or is corrupted
145
123
  */
146
124
  function loadCache(): Cache {
147
125
  try {
@@ -156,103 +134,21 @@ function loadCache(): Cache {
156
134
 
157
135
  /**
158
136
  * Saves cache to disk
159
- * @param {Cache} cache - Cache object to save
160
137
  */
161
138
  function saveCache(cache: Cache): void {
162
139
  try {
140
+ const cacheDir = path.dirname(CACHE_FILE);
141
+ if (!fs.existsSync(cacheDir)) {
142
+ fs.mkdirSync(cacheDir, { recursive: true });
143
+ }
163
144
  fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
164
145
  } catch (error) {
165
146
  // Silently fail if can't write cache
166
147
  }
167
148
  }
168
149
 
169
- /**
170
- * Checks if a cache entry is still valid based on configured duration
171
- * @param {CacheEntry} cacheEntry - The cache entry to validate
172
- * @param {string} key - The key name to determine cache duration
173
- * @returns {boolean} True if cache is still valid
174
- */
175
- function isCacheValid(cacheEntry: CacheEntry, key: string): boolean {
176
- if (!cacheEntry || !cacheEntry.timestamp) return false;
177
- const age = Date.now() - cacheEntry.timestamp;
178
- const maxAge = CACHE_DURATION[key as keyof typeof CACHE_DURATION] || 60000;
179
- return age < maxAge;
180
- }
181
-
182
- /**
183
- * Retrieves a value from cache if valid
184
- * @param {Cache} cache - Cache object
185
- * @param {string} key - Key to retrieve
186
- * @returns {any} Cached value or null if not found/expired
187
- */
188
- function getCachedValue(cache: Cache, key: string): any {
189
- if (!cache[key]) return null;
190
- const cacheEntry = cache[key];
191
- if (!isCacheValid(cacheEntry, key)) {
192
- delete cache[key];
193
- return null;
194
- }
195
- return cacheEntry.value;
196
- }
197
-
198
- /**
199
- * Stores a value in cache with current timestamp
200
- * @param {Cache} cache - Cache object
201
- * @param {string} key - Key to store under
202
- * @param {any} value - Value to cache
203
- */
204
- function setCachedValue(cache: Cache, key: string, value: any): void {
205
- cache[key] = {
206
- value,
207
- timestamp: Date.now(),
208
- };
209
- }
210
-
211
- /**
212
- * Executes a shell command safely with timeout
213
- * @param {string} command - Command to execute
214
- * @param {object} options - Additional options for execSync
215
- * @returns {string} Command output or empty string on error
216
- */
217
- function execCommand(command: string, options = {}): string {
218
- try {
219
- const cmd = IS_WINDOWS ? `cmd /c ${command}` : command;
220
- return execSync(cmd, {
221
- encoding: "utf8",
222
- timeout: 10000,
223
- stdio: ["pipe", "pipe", "ignore"],
224
- ...options,
225
- })
226
- .toString()
227
- .trim();
228
- } catch (error) {
229
- return "";
230
- }
231
- }
232
-
233
- /**
234
- * Checks if a command exists in the system PATH
235
- * @param {string} command - Command name to check
236
- * @returns {boolean} True if command exists
237
- */
238
- function commandExists(command: string): boolean {
239
- try {
240
- if (IS_WINDOWS) {
241
- execSync(`where ${command}`, { stdio: "ignore" });
242
- } else {
243
- execSync(`which ${command}`, { stdio: "ignore" });
244
- }
245
- return true;
246
- } catch {
247
- return false;
248
- }
249
- }
250
-
251
150
  /**
252
151
  * Fetches IP geolocation information from ipinfo.io API
253
- * @param {string} token - IPInfo.io API token
254
- * @param {number} timeout - Request timeout in milliseconds
255
- * @returns {Promise<IPInfo>} IP information object or empty object on error
256
152
  */
257
153
  async function fetchIPInfo(
258
154
  token: string = DEFAULT_IPINFO_TOKEN,
@@ -282,988 +178,43 @@ async function fetchIPInfo(
282
178
  }
283
179
 
284
180
  /**
285
- * System information collection functions
286
- * Each function returns a clean string value without formatting
287
- * @namespace infoFunctions
181
+ * System information collection functions map
288
182
  */
289
183
  export const infoFunctions = {
290
- /**
291
- * Gets the current username
292
- * @returns Current system username
293
- */
294
- user(): string {
295
- return os.userInfo().username;
296
- },
297
-
298
- /**
299
- * Gets the system hostname
300
- * @returns Computer hostname/network name
301
- */
302
- hostname(): string {
303
- return os.hostname();
304
- },
305
-
306
- /**
307
- * Gets public IP address from ipinfo.io
308
- * @param context - Info context with cache and IP info
309
- * @returns Public IPv4 address or empty string
310
- * @example "203.0.113.42"
311
- */
312
- async ip(context: InfoContext): Promise<string> {
313
- const cached = getCachedValue(context.cache, "ip");
314
- if (cached) return cached;
315
-
316
- if (!context.ipInfo) {
317
- setCachedValue(context.cache, "ip", "");
318
- return "";
319
- }
320
- const ip = context.ipInfo.ip || "";
321
- setCachedValue(context.cache, "ip", ip);
322
- return ip;
323
- },
324
-
325
- /**
326
- * Gets local/private IP address(es)
327
- * Tries ifconfig and ip commands on Linux, falls back to Node.js API
328
- * @returns Space-separated local IP addresses (RFC 1918 ranges)
329
- * @example "192.168.1.100" or "10.0.0.50 192.168.1.100"
330
- */
331
- iplocal(): string {
332
- if (IS_LINUX) {
333
- try {
334
- const ifconfig = execCommand("ifconfig 2>/dev/null");
335
- const wlanMatch = ifconfig.match(
336
- /wlan0[\s\S]*?inet (\d+\.\d+\.\d+\.\d+)/
337
- );
338
- if (wlanMatch) {
339
- return wlanMatch[1];
340
- }
341
- } catch {}
342
-
343
- try {
344
- const ipAddr = execCommand("ip addr show 2>/dev/null");
345
- const addresses: string[] = [];
346
- const matches = ipAddr.matchAll(/inet (\d+\.\d+\.\d+\.\d+)\/\d+/g);
347
- for (const match of matches) {
348
- if (!match[1].startsWith("127.")) {
349
- addresses.push(match[1]);
350
- }
351
- }
352
- if (addresses.length > 0) {
353
- return addresses.join(" ");
354
- }
355
- } catch {}
356
- }
357
-
358
- const interfaces = os.networkInterfaces();
359
- const addresses: string[] = [];
360
-
361
- for (const name of Object.keys(interfaces)) {
362
- for (const device of interfaces[name] || []) {
363
- if (device.family === "IPv4" && !device.internal) {
364
- addresses.push(device.address);
365
- }
366
- }
367
- }
368
-
369
- return addresses.join(" ");
370
- },
371
-
372
- /**
373
- * Gets geographic city based on public IP
374
- * @param context - Info context with IP geolocation data
375
- * @returns City name or empty string
376
- * @example "San Francisco", "New York", "London"
377
- */
378
- async city(context: InfoContext): Promise<string> {
379
- const cached = getCachedValue(context.cache, "city");
380
- if (cached !== null) return cached;
381
-
382
- if (!context.ipInfo || !context.ipInfo.city) {
383
- setCachedValue(context.cache, "city", "");
384
- return "";
385
- }
386
- const city = context.ipInfo.city;
387
- setCachedValue(context.cache, "city", city);
388
- return city;
389
- },
390
-
391
- /**
392
- * Gets reverse DNS hostname with HTTP prefix
393
- * @param context - Info context with IP info
394
- * @returns Domain with http:// prefix or empty string
395
- * @example "http://example.com", "http://host-203-0-113-42.example.net"
396
- */
397
- async domain(context: InfoContext): Promise<string> {
398
- const cached = getCachedValue(context.cache, "domain");
399
- if (cached !== null) return cached;
400
-
401
- if (!context.ipInfo || !context.ipInfo.hostname) {
402
- setCachedValue(context.cache, "domain", "");
403
- return "";
404
- }
405
- const domain = `http://${context.ipInfo.hostname}`;
406
- setCachedValue(context.cache, "domain", domain);
407
- return domain;
408
- },
409
-
410
- /**
411
- * Gets Internet Service Provider name
412
- * Strips AS number prefix from organization string
413
- * @param context - Info context with IP info
414
- * @returns ISP name or empty string
415
- * @example "Comcast Cable", "Verizon Business", "Cloudflare Inc"
416
- */
417
- async isp(context: InfoContext): Promise<string> {
418
- const cached = getCachedValue(context.cache, "isp");
419
- if (cached !== null) return cached;
420
-
421
- if (!context.ipInfo || !context.ipInfo.org) {
422
- setCachedValue(context.cache, "isp", "");
423
- return "";
424
- }
425
- const isp = context.ipInfo.org.split(" ").slice(1).join(" ");
426
- setCachedValue(context.cache, "isp", isp);
427
- return isp;
428
- },
429
-
430
- /**
431
- * Gets operating system name and version
432
- * Platform-specific detection for Windows, macOS, and Linux
433
- * @param context - Info context with cache
434
- * @returns OS name and version string
435
- * @example "Windows 11 Pro", "macOS Ventura 13.2.1", "Ubuntu 22.04.3 LTS"
436
- */
437
- os(context: InfoContext): string {
438
- const cached = getCachedValue(context.cache, "os");
439
- if (cached) return cached;
440
-
441
- const platform = os.platform();
442
- const release = os.release();
443
- let osName = "";
444
-
445
- if (IS_WINDOWS) {
446
- try {
447
- const version = execCommand("ver");
448
- const match = version.match(/Microsoft Windows \[Version ([^\]]+)\]/);
449
- osName = match ? `Windows ${match[1]}` : `Windows ${release}`;
450
- } catch {
451
- osName = `Windows ${release}`;
452
- }
453
- } else if (IS_MAC) {
454
- osName = `macOS ${release}`;
455
- } else if (IS_LINUX) {
456
- try {
457
- const osRelease = fs.readFileSync("/etc/os-release", "utf8");
458
- const nameMatch = osRelease.match(/^NAME="([^"]+)"/m);
459
- const versionMatch = osRelease.match(/^VERSION_ID="([^"]+)"/m);
460
- osName = nameMatch ? nameMatch[1] : "Linux";
461
- if (versionMatch) osName += ` ${versionMatch[1]}`;
462
- } catch {
463
- osName = `Linux ${release}`;
464
- }
465
- } else {
466
- osName = `${platform} ${release}`;
467
- }
468
-
469
- setCachedValue(context.cache, "os", osName);
470
- return osName;
471
- },
472
-
473
- /**
474
- * Gets CPU model name and specifications
475
- * Uses platform-specific commands (wmic/lscpu/cpuinfo)
476
- * Automatically strips "with Radeon Graphics" and similar suffixes
477
- * @param context - Info context with cache
478
- * @returns CPU model string or empty string
479
- * @example "Intel Core i7-12700K", "AMD Ryzen 9 5900HX", "Apple M2 Pro"
480
- */
481
- cpu(context: InfoContext): string {
482
- const cached = getCachedValue(context.cache, "cpu");
483
- if (cached) return cached;
484
-
485
- let cpuName = "";
486
-
487
- if (IS_WINDOWS) {
488
- try {
489
- const wmic = execCommand("wmic cpu get name /format:list");
490
- const nameMatch = wmic.match(/Name=(.+)/);
491
- if (nameMatch) {
492
- cpuName = nameMatch[1].trim();
493
- }
494
- } catch {}
495
-
496
- if (!cpuName) {
497
- try {
498
- const ps = execCommand(
499
- 'powershell.exe -Command "Get-WmiObject -Class Win32_Processor | Select-Object -ExpandProperty Name"'
500
- );
501
- if (ps.trim()) {
502
- cpuName = ps.trim();
503
- }
504
- } catch {}
505
- }
506
- } else if (IS_LINUX) {
507
- try {
508
- const lscpu = execCommand("lscpu");
509
- const modelMatch = lscpu.match(/Model name:\s*([^\n,]+)/);
510
- if (modelMatch) {
511
- cpuName = modelMatch[1].trim();
512
- }
513
- } catch {}
514
-
515
- if (!cpuName) {
516
- try {
517
- const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
518
- const modelMatch = cpuInfo.match(/model name\s*:\s*([^\n]+)/);
519
- const hardwareMatch = cpuInfo.match(/Hardware\s*:\s*([^\n]+)/);
520
-
521
- if (modelMatch) {
522
- cpuName = modelMatch[1].trim();
523
- } else if (hardwareMatch) {
524
- cpuName = hardwareMatch[1].trim();
525
- }
526
- } catch {}
527
- }
528
- } else {
529
- const cpus = os.cpus();
530
- if (cpus.length > 0) {
531
- cpuName = cpus[0].model.trim().replace(/[\r\n]+/g, " ");
532
- }
533
- }
534
-
535
- if (!cpuName) {
536
- setCachedValue(context.cache, "cpu", "");
537
- return "";
538
- }
539
-
540
- cpuName = cpuName.trim().replace(/with .*/, "");
541
-
542
- setCachedValue(context.cache, "cpu", cpuName);
543
- return cpuName;
544
- },
545
-
546
- /**
547
- * Gets graphics card model name
548
- * Extracts GPU info from lspci (Linux) or WMI (Windows)
549
- * Filters out basic/generic display adapters
550
- * @param context - Info context with cache
551
- * @returns GPU model string or empty string
552
- * @example "NVIDIA GeForce RTX 4070", "AMD Radeon RX 6800 XT"
553
- */
554
- gpu(context: InfoContext): string {
555
- const cached = getCachedValue(context.cache, "gpu");
556
- if (cached !== null) return cached;
557
-
558
- if (IS_WINDOWS) {
559
- try {
560
- const wmic = execCommand(
561
- "wmic path win32_VideoController get name /format:list"
562
- );
563
- const nameMatch = wmic.match(/Name=(.+)/);
564
- if (nameMatch) {
565
- const gpu = nameMatch[1].trim();
566
- if (gpu && gpu !== "" && !gpu.includes("Microsoft Basic")) {
567
- setCachedValue(context.cache, "gpu", gpu);
568
- return gpu;
569
- }
570
- }
571
- } catch {}
572
-
573
- try {
574
- const ps = execCommand(
575
- "powershell.exe -Command \"Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike '*Microsoft Basic*'} | Select-Object -First 1 -ExpandProperty Name\""
576
- );
577
- if (ps.trim()) {
578
- const gpu = ps.trim();
579
- setCachedValue(context.cache, "gpu", gpu);
580
- return gpu;
581
- }
582
- } catch {}
583
- } else if (IS_LINUX) {
584
- try {
585
- const lspci = execCommand("lspci");
586
- const gpuMatch = lspci.match(
587
- /VGA.*?(RTX|GeForce|AMD|Intel|NVIDIA)[^\n]*/i
588
- );
589
- if (gpuMatch) {
590
- let gpu = gpuMatch[0];
591
-
592
- const bracketMatch = gpu.match(/\[([^\]]+)\]/);
593
- if (bracketMatch) {
594
- gpu = bracketMatch[1];
595
- } else {
596
- gpu = gpu
597
- .replace(/^.*VGA[^:]*:\s*/, "")
598
- .replace(/\s*\(.*\)$/, "")
599
- .trim();
600
- }
601
-
602
- if (gpu) {
603
- setCachedValue(context.cache, "gpu", gpu);
604
- return gpu;
605
- }
606
- }
607
- } catch {}
608
- }
609
-
610
- setCachedValue(context.cache, "gpu", "");
611
- return "";
612
- },
613
-
614
- /**
615
- * Gets root filesystem disk usage percentage
616
- * Uses df command on Linux/Android
617
- * @param context - Info context with cache
618
- * @returns Percentage string or empty string
619
- * @example "45%", "78%"
620
- */
621
- disk_used(context: InfoContext): string {
622
- const cached = getCachedValue(context.cache, "disk_used");
623
- if (cached !== null) return cached;
624
-
625
- if (IS_LINUX) {
626
- try {
627
- const df = execCommand("df -h");
628
- let diskUsage = "";
629
-
630
- if (df.includes("/storage/emulated")) {
631
- const match = df.match(/\s+(\d+%)\s+\/storage\/emulated/);
632
- diskUsage = match ? match[1] : "";
633
- } else {
634
- const lines = df.split("\n");
635
- for (const line of lines) {
636
- if (line.trim().endsWith(" /")) {
637
- const parts = line.trim().split(/\s+/);
638
- const percentIndex = parts.findIndex((part) =>
639
- part.includes("%")
640
- );
641
- if (percentIndex !== -1) {
642
- diskUsage = parts[percentIndex];
643
- break;
644
- }
645
- }
646
- }
647
-
648
- if (!diskUsage) {
649
- const rootMatch = df.match(/(\d+%)\s+\/\s*$/m);
650
- diskUsage = rootMatch ? rootMatch[1] : "";
651
- }
652
- }
653
-
654
- setCachedValue(context.cache, "disk_used", diskUsage);
655
- return diskUsage;
656
- } catch {}
657
- }
658
-
659
- setCachedValue(context.cache, "disk_used", "");
660
- return "";
661
- },
662
-
663
- /**
664
- * Gets memory usage in gigabytes
665
- * Reads from /proc/meminfo on Linux, falls back to os.totalmem()
666
- * @param context - Info context with cache
667
- * @returns Memory usage as "used/total GB"
668
- * @example "12/32GB", "6/16GB"
669
- */
670
- ram_used(context: InfoContext): string {
671
- const cached = getCachedValue(context.cache, "ram_used");
672
- if (cached) return cached;
673
-
674
- if (IS_LINUX) {
675
- try {
676
- const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
677
- const totalMatch = meminfo.match(/MemTotal:\s+(\d+) kB/);
678
- const freeMatch = meminfo.match(/MemFree:\s+(\d+) kB/);
679
-
680
- if (totalMatch && freeMatch) {
681
- const totalMB = Math.round(parseInt(totalMatch[1]) / 1024);
682
- const freeMB = Math.round(parseInt(freeMatch[1]) / 1024);
683
- const usedMB = totalMB - freeMB;
684
-
685
- const totalGB = Math.round(totalMB / 1024);
686
- const usedGB = Math.round(usedMB / 1024);
687
-
688
- const result = `${usedGB}/${totalGB}GB`;
689
- setCachedValue(context.cache, "ram_used", result);
690
- return result;
691
- }
692
- } catch {}
693
- }
694
-
695
- const totalMem = os.totalmem();
696
- const freeMem = os.freemem();
697
- const usedMem = totalMem - freeMem;
698
-
699
- const totalGB = Math.round(totalMem / (1024 * 1024 * 1024));
700
- const usedGB = Math.round(usedMem / (1024 * 1024 * 1024));
701
-
702
- const result = `${usedGB}/${totalGB}GB`;
703
- setCachedValue(context.cache, "ram_used", result);
704
- return result;
705
- },
706
-
707
- /**
708
- * Gets the highest CPU-consuming process
709
- * Uses ps command to find top process by CPU usage (Linux only)
710
- * @param context - Info context with cache
711
- * @returns Process info as "percentage processname" or empty string
712
- * @example "8% firefox", "15% chrome", "3% systemd"
713
- */
714
- top_process(context: InfoContext): string {
715
- const cached = getCachedValue(context.cache, "top_process");
716
- if (cached !== null) return cached;
717
-
718
- if (IS_LINUX) {
719
- try {
720
- const ps = execCommand("ps -eo pcpu,comm --sort=-%cpu --no-headers");
721
- const lines = ps.split("\n");
722
- if (lines.length > 0) {
723
- const topProcess = lines[0].trim().replace(/\s+/, " ").split(" ");
724
- const cpu = topProcess[0].replace(/\.\d+/, "%");
725
- const process = topProcess[1].split("/").pop();
726
- const result = `${cpu} ${process}`;
727
- setCachedValue(context.cache, "top_process", result);
728
- return result;
729
- }
730
- } catch {}
731
- }
732
-
733
- setCachedValue(context.cache, "top_process", "");
734
- return "";
735
- },
736
-
737
- /**
738
- * Gets system uptime since last boot
739
- * @returns Uptime formatted as "Xd Yh Zm"
740
- * @example "2d 14h 23m", "0d 3h 45m"
741
- */
742
- uptime(): string {
743
- const uptimeSeconds = os.uptime();
744
- const days = Math.floor(uptimeSeconds / 86400);
745
- const hours = Math.floor((uptimeSeconds % 86400) / 3600);
746
- const minutes = Math.floor((uptimeSeconds % 3600) / 60);
747
- return `${days}d ${hours}h ${minutes}m`;
748
- },
749
-
750
- /**
751
- * Gets device or computer model name
752
- * Uses WMI on Windows, DMI on Linux, getprop on Android
753
- * @param context - Info context with cache
754
- * @returns Device model name or empty string
755
- * @example "Dell OptiPlex 7090", "MacBook Pro 16-inch", "Valve Steam Deck"
756
- */
757
- device(context: InfoContext): string {
758
- const cached = getCachedValue(context.cache, "device");
759
- if (cached !== null) return cached;
760
-
761
- if (IS_WINDOWS) {
762
- try {
763
- const wmic = execCommand("wmic csproduct get name /format:list");
764
- const nameMatch = wmic.match(/Name=(.+)/);
765
- if (nameMatch) {
766
- const device = nameMatch[1].trim();
767
- if (device && device !== "") {
768
- setCachedValue(context.cache, "device", device);
769
- return device;
770
- }
771
- }
772
- } catch {}
773
-
774
- try {
775
- const ps = execCommand(
776
- 'powershell.exe -Command "Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty Model"'
777
- );
778
- if (ps.trim()) {
779
- const device = ps.trim();
780
- setCachedValue(context.cache, "device", device);
781
- return device;
782
- }
783
- } catch {}
784
- } else if (IS_LINUX) {
785
- try {
786
- if (commandExists("getprop")) {
787
- const device = execCommand("getprop ro.product.model");
788
- if (device) {
789
- setCachedValue(context.cache, "device", device);
790
- return device;
791
- }
792
- }
793
-
794
- const dmiPath = "/sys/devices/virtual/dmi/id/product_name";
795
- if (fs.existsSync(dmiPath)) {
796
- const device = fs.readFileSync(dmiPath, "utf8").trim();
797
- if (device) {
798
- setCachedValue(context.cache, "device", device);
799
- return device;
800
- }
801
- }
802
- } catch {}
803
- }
804
-
805
- setCachedValue(context.cache, "device", "");
806
- return "";
807
- },
808
-
809
- /**
810
- * Gets kernel version string
811
- * Returns the operating system kernel version from os.release()
812
- * @param context - Info context with cache
813
- * @returns Kernel version string
814
- * @example "5.15.0-56-generic", "6.11.11-valve12-1-neptune"
815
- */
816
- kernel(context: InfoContext): string {
817
- const cached = getCachedValue(context.cache, "kernel");
818
- if (cached) return cached;
819
-
820
- const kernel = os.release();
821
- setCachedValue(context.cache, "kernel", kernel);
822
- return kernel;
823
- },
824
-
825
- /**
826
- * Gets the current shell name
827
- * Uses ps command to find parent process shell (Linux/Unix only)
828
- * @returns Shell name or empty string on Windows
829
- * @example "bash", "zsh", "fish", "nu"
830
- */
831
- shell(): string {
832
- if (IS_LINUX) {
833
- try {
834
- const ppid = process.ppid;
835
- const shell = execCommand(`ps -p ${ppid} -o comm=`).split("/").pop();
836
- return shell || "";
837
- } catch {}
838
- }
839
- return "";
840
- },
841
-
842
- /**
843
- * Gets available package managers and development tools
844
- * Checks for package managers (apt, yum, npm, etc.) and editors (nvim, hx)
845
- * @param context - Info context with cache
846
- * @returns Space-separated list of available commands
847
- * @example "apt npm docker nvim", "yay pacman bun hx"
848
- */
849
- pacman(context: InfoContext): string {
850
- const cached = getCachedValue(context.cache, "pacman");
851
- if (cached !== null) return cached;
852
-
853
- const commands = [
854
- "apt",
855
- "npm",
856
- "uv",
857
- "docker",
858
- "hx",
859
- "nvim",
860
- "bun",
861
- "yay",
862
- "pacman",
863
- "yum",
864
- "dnf",
865
- "zypper",
866
- "emerge",
867
- "apk",
868
- "snap",
869
- "flatpak",
870
- ];
871
- const available = commands.filter((cmd) => commandExists(cmd));
872
-
873
- const result = available.join(" ");
874
- setCachedValue(context.cache, "pacman", result);
875
- return result;
876
- },
877
-
878
- /**
879
- * Gets open TCP ports with service names
880
- * Uses lsof to find listening TCP ports (Linux only)
881
- * @param context - Info context with cache
882
- * @returns Space-separated port+process pairs or empty string
883
- * @example "80http 443http 22ssh", "3000node 5432post"
884
- */
885
- ports(context: InfoContext): string {
886
- const cached = getCachedValue(context.cache, "ports");
887
- if (cached !== null) return cached;
888
-
889
- if (IS_LINUX) {
890
- try {
891
- const lsof = execCommand("lsof -nP -iTCP -sTCP:LISTEN");
892
- const lines = lsof.split("\n").slice(1);
893
- const ports = new Set<string>();
894
-
895
- lines.forEach((line) => {
896
- const parts = line.split(/\s+/);
897
- if (parts.length >= 9) {
898
- const address = parts[8];
899
- const portMatch = address.match(/:(\d+)$/);
900
- if (portMatch) {
901
- const port = portMatch[1];
902
- const process = parts[0].substring(0, 4);
903
- ports.add(`${port}${process}`);
904
- }
905
- }
906
- });
907
-
908
- const result = Array.from(ports).join(" ");
909
- setCachedValue(context.cache, "ports", result);
910
- return result;
911
- } catch {}
912
- }
913
-
914
- setCachedValue(context.cache, "ports", "");
915
- return "";
916
- },
917
-
918
- /**
919
- * Gets running Docker container names
920
- * Lists active Docker containers with their names
921
- * @param context - Info context with cache
922
- * @returns Space-separated container names or empty string
923
- * @example "nginx redis postgres", "web-app db-server"
924
- */
925
- containers(context: InfoContext): string {
926
- const cached = getCachedValue(context.cache, "containers");
927
- if (cached !== null) return cached;
928
-
929
- if (!commandExists("docker")) {
930
- setCachedValue(context.cache, "containers", "");
931
- return "";
932
- }
933
-
934
- try {
935
- const containerCount = execCommand("docker ps -q")
936
- .split("\n")
937
- .filter((line) => line.trim()).length;
938
- if (containerCount === 0) {
939
- setCachedValue(context.cache, "containers", "");
940
- return "";
941
- }
942
-
943
- const containers = execCommand(
944
- 'docker ps --format "{{.Names}}\t{{.Ports}}"'
945
- );
946
- const lines = containers.split("\n").filter((line) => line.trim());
947
-
948
- const containerInfo: string[] = [];
949
-
950
- lines.forEach((line) => {
951
- const [name, ports] = line.split("\t");
952
- if (name) {
953
- containerInfo.push(name);
954
-
955
- if (ports) {
956
- const portMatches = ports.match(/->(\d+(-\d+)?)\//g);
957
- if (portMatches) {
958
- const uniquePorts = [
959
- ...new Set(
960
- portMatches.map((p) => p.replace(/->\d+(-\d+)?\//, ""))
961
- ),
962
- ];
963
- containerInfo.push(...uniquePorts);
964
- }
965
- }
966
- }
967
- });
968
-
969
- const result = containerInfo.join(" ");
970
- setCachedValue(context.cache, "containers", result);
971
- return result;
972
- } catch {}
973
-
974
- setCachedValue(context.cache, "containers", "");
975
- return "";
976
- },
977
-
978
- /**
979
- * Gets available memory in gigabytes
980
- * Reads MemAvailable from /proc/meminfo (Linux only)
981
- * @returns Available memory with "GB available" suffix or empty string
982
- * @example "12GB available", "4GB available"
983
- */
984
- memory_available(): string {
985
- if (!IS_LINUX) return "";
986
-
987
- try {
988
- const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
989
- const availableMatch = meminfo.match(/MemAvailable:\s+(\d+) kB/);
990
- if (availableMatch) {
991
- const availableGB = Math.round(
992
- parseInt(availableMatch[1]) / 1024 / 1024
993
- );
994
- return `${availableGB}GB available`;
995
- }
996
- } catch {}
997
- return "";
998
- },
999
-
1000
- /**
1001
- * Gets swap memory usage
1002
- * Calculates swap usage from /proc/meminfo (Linux only)
1003
- * @returns Swap usage as "percentage (size MB) swap" or empty string
1004
- * @example "15% (512MB) swap", "0% (0MB) swap"
1005
- */
1006
- swap_used(): string {
1007
- if (!IS_LINUX) return "";
1008
-
1009
- try {
1010
- const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
1011
- const swapTotalMatch = meminfo.match(/SwapTotal:\s+(\d+) kB/);
1012
- const swapFreeMatch = meminfo.match(/SwapFree:\s+(\d+) kB/);
1013
-
1014
- if (swapTotalMatch && swapFreeMatch) {
1015
- const swapTotal = parseInt(swapTotalMatch[1]);
1016
- const swapFree = parseInt(swapFreeMatch[1]);
1017
- const swapUsed = swapTotal - swapFree;
1018
-
1019
- if (swapTotal > 0) {
1020
- const swapUsedPercent = Math.round((swapUsed / swapTotal) * 100);
1021
- const swapUsedMB = Math.round(swapUsed / 1024);
1022
- return `${swapUsedPercent}% (${swapUsedMB}MB) swap`;
1023
- }
1024
- }
1025
- } catch {}
1026
- return "";
1027
- },
1028
-
1029
- /**
1030
- * Gets system load averages
1031
- * Reads 1, 5, and 15 minute load averages from /proc/loadavg (Linux only)
1032
- * @returns Space-separated load averages (1m 5m 15m) or empty string
1033
- * @example "0.52 0.58 0.59", "2.10 1.95 1.88"
1034
- */
1035
- load_average(): string {
1036
- if (!IS_LINUX) return "";
1037
-
1038
- try {
1039
- const loadavg = fs.readFileSync("/proc/loadavg", "utf8");
1040
- const loads = loadavg.split(" ").slice(0, 3);
1041
- return loads.join(" ");
1042
- } catch {}
1043
- return "";
1044
- },
1045
-
1046
- /**
1047
- * Gets number of logged in users
1048
- * Uses who command to count active user sessions (Linux only)
1049
- * @returns User count with "users" suffix or empty string
1050
- * @example "3 users", "1 users"
1051
- */
1052
- users_logged_in(): string {
1053
- if (!IS_LINUX) return "";
1054
-
1055
- try {
1056
- const who = execCommand("who");
1057
- const users = who.split("\n").filter((line) => line.trim()).length;
1058
- if (users > 0) {
1059
- return `${users} users`;
1060
- }
1061
- } catch {}
1062
- return "";
1063
- },
1064
-
1065
- /**
1066
- * Gets active network interface names
1067
- * Lists non-loopback interfaces with IPv4 addresses (Linux only)
1068
- * @param context - Info context with cache
1069
- * @returns Space-separated interface names or empty string
1070
- * @example "eth0 wlan0", "enp0s3"
1071
- */
1072
- network_interfaces(context: InfoContext): string {
1073
- const cached = getCachedValue(context.cache, "network_interfaces");
1074
- if (cached !== null) return cached;
1075
-
1076
- if (!IS_LINUX) {
1077
- setCachedValue(context.cache, "network_interfaces", "");
1078
- return "";
1079
- }
1080
-
1081
- try {
1082
- const interfaces = os.networkInterfaces();
1083
- const activeInterfaces: string[] = [];
1084
-
1085
- for (const [name, addrs] of Object.entries(interfaces)) {
1086
- if (name !== "lo") {
1087
- const ipv4Addr = addrs?.find(
1088
- (addr) => addr.family === "IPv4" && !addr.internal
1089
- );
1090
- if (ipv4Addr) {
1091
- activeInterfaces.push(name);
1092
- }
1093
- }
1094
- }
1095
-
1096
- const result = activeInterfaces.join(" ");
1097
- setCachedValue(context.cache, "network_interfaces", result);
1098
- return result;
1099
- } catch {}
1100
-
1101
- setCachedValue(context.cache, "network_interfaces", "");
1102
- return "";
1103
- },
1104
-
1105
- mount_points(context: InfoContext): string {
1106
- const cached = getCachedValue(context.cache, "mount_points");
1107
- if (cached !== null) return cached;
1108
-
1109
- if (!IS_LINUX) {
1110
- setCachedValue(context.cache, "mount_points", "");
1111
- return "";
1112
- }
1113
-
1114
- try {
1115
- const df = execCommand("df -h");
1116
- const lines = df.split("\n").slice(1);
1117
- const mountPoints: string[] = [];
1118
-
1119
- lines.forEach((line) => {
1120
- const parts = line.trim().split(/\s+/);
1121
- if (parts.length >= 6) {
1122
- const mountPoint = parts[5];
1123
- const usage = parts[4];
1124
- if (
1125
- !mountPoint.startsWith("/dev") &&
1126
- !mountPoint.startsWith("/proc") &&
1127
- !mountPoint.startsWith("/sys") &&
1128
- mountPoint !== "/"
1129
- ) {
1130
- mountPoints.push(`${mountPoint}(${usage})`);
1131
- }
1132
- }
1133
- });
1134
-
1135
- const result = mountPoints.slice(0, 3).join(" ");
1136
- setCachedValue(context.cache, "mount_points", result);
1137
- return result;
1138
- } catch {}
1139
-
1140
- setCachedValue(context.cache, "mount_points", "");
1141
- return "";
1142
- },
1143
-
1144
- services_running(context: InfoContext): string {
1145
- const cached = getCachedValue(context.cache, "services_running");
1146
- if (cached !== null) return cached;
1147
-
1148
- if (!IS_LINUX) {
1149
- setCachedValue(context.cache, "services_running", "");
1150
- return "";
1151
- }
1152
-
1153
- try {
1154
- let serviceCount = 0;
1155
-
1156
- if (commandExists("systemctl")) {
1157
- const services = execCommand(
1158
- "systemctl list-units --type=service --state=running --no-pager"
1159
- );
1160
- serviceCount = services
1161
- .split("\n")
1162
- .filter((line) => line.includes(".service")).length;
1163
- } else if (commandExists("service")) {
1164
- const services = execCommand("service --status-all");
1165
- serviceCount = services
1166
- .split("\n")
1167
- .filter((line) => line.includes("+")).length;
1168
- }
1169
-
1170
- if (serviceCount > 0) {
1171
- const result = `${serviceCount} services`;
1172
- setCachedValue(context.cache, "services_running", result);
1173
- return result;
1174
- }
1175
- } catch {}
1176
-
1177
- setCachedValue(context.cache, "services_running", "");
1178
- return "";
1179
- },
1180
-
1181
- temperature(context: InfoContext): string {
1182
- const cached = getCachedValue(context.cache, "temperature");
1183
- if (cached !== null) return cached;
1184
-
1185
- if (!IS_LINUX) {
1186
- setCachedValue(context.cache, "temperature", "");
1187
- return "";
1188
- }
1189
-
1190
- try {
1191
- const tempSources = [
1192
- "/sys/class/thermal/thermal_zone0/temp",
1193
- "/sys/class/hwmon/hwmon0/temp1_input",
1194
- "/sys/class/hwmon/hwmon1/temp1_input",
1195
- ];
1196
-
1197
- for (const source of tempSources) {
1198
- if (fs.existsSync(source)) {
1199
- const temp = fs.readFileSync(source, "utf8").trim();
1200
- const tempC = Math.round(parseInt(temp) / 1000);
1201
-
1202
- if (tempC > 0 && tempC < 150) {
1203
- const result = `${tempC}°C`;
1204
- setCachedValue(context.cache, "temperature", result);
1205
- return result;
1206
- }
1207
- }
1208
- }
1209
- } catch {}
1210
-
1211
- setCachedValue(context.cache, "temperature", "");
1212
- return "";
1213
- },
1214
-
1215
- battery(context: InfoContext): string {
1216
- const cached = getCachedValue(context.cache, "battery");
1217
- if (cached !== null) return cached;
1218
-
1219
- if (!IS_LINUX) {
1220
- setCachedValue(context.cache, "battery", "");
1221
- return "";
1222
- }
1223
-
1224
- try {
1225
- const batteryPath = "/sys/class/power_supply/BAT0";
1226
- const capacityPath = `${batteryPath}/capacity`;
1227
- const statusPath = `${batteryPath}/status`;
1228
-
1229
- if (fs.existsSync(capacityPath)) {
1230
- const capacity = fs.readFileSync(capacityPath, "utf8").trim();
1231
- const status = fs.existsSync(statusPath)
1232
- ? fs.readFileSync(statusPath, "utf8").trim()
1233
- : "Unknown";
1234
-
1235
- const batteryPercent = parseInt(capacity);
1236
- const isCharging = status === "Charging";
1237
- const result = `${batteryPercent}%${isCharging ? "+" : ""}`;
1238
- setCachedValue(context.cache, "battery", result);
1239
- return result;
1240
- }
1241
- } catch {}
1242
-
1243
- setCachedValue(context.cache, "battery", "");
1244
- return "";
1245
- },
1246
-
1247
- screen_resolution(): string {
1248
- if (!IS_LINUX) return "";
1249
-
1250
- try {
1251
- if (process.env.DISPLAY) {
1252
- const xrandr = execCommand("xrandr");
1253
- const resolutionMatch = xrandr.match(/(\d+x\d+)\+\d+\+\d+/);
1254
- if (resolutionMatch) {
1255
- return resolutionMatch[1];
1256
- }
1257
- }
1258
- } catch {}
1259
- return "";
1260
- },
184
+ user,
185
+ hostname,
186
+ ip,
187
+ iplocal,
188
+ city,
189
+ domain,
190
+ isp,
191
+ os: os_info, // Mapped from os_info
192
+ cpu,
193
+ gpu,
194
+ disk_used,
195
+ ram_used,
196
+ top_process,
197
+ uptime,
198
+ device,
199
+ kernel,
200
+ shell,
201
+ pacman: packages, // Mapped from packages
202
+ ports,
203
+ containers,
204
+ memory_available,
205
+ swap_used,
206
+ load_average,
207
+ users_logged_in,
208
+ network_interfaces,
209
+ mount_points,
210
+ services_running,
211
+ temperature,
212
+ battery,
213
+ screen_resolution,
1261
214
  };
1262
215
 
1263
216
  /**
1264
217
  * Get all system information as a clean JSON object
1265
- * @param options Configuration options
1266
- * @returns Promise resolving to SystemInfo object
1267
218
  */
1268
219
  export async function getSystemInfo(
1269
220
  options: SystemInfoOptions = {}
@@ -1272,12 +223,21 @@ export async function getSystemInfo(
1272
223
  const context: InfoContext = { cache };
1273
224
 
1274
225
  // Check if we need IP info
1275
- const cachedIPInfo = getCachedValue(cache, "ipInfo");
226
+ // Some functions need IP info, so we fetch it once if needed by any function
227
+ // For now we always try to fetch it if not cached, as ip/city/etc depend on it
228
+ // Optimization: check if we actually need to run those functions based on options if provided
229
+ // But current implementation fetches it always if missing from cache
230
+
231
+ const cachedIPInfo = cache["ipInfo"]?.value;
1276
232
  if (cachedIPInfo) {
1277
233
  context.ipInfo = cachedIPInfo;
1278
234
  } else {
1279
235
  context.ipInfo = await fetchIPInfo();
1280
- setCachedValue(cache, "ipInfo", context.ipInfo);
236
+ // Cache IP info itself
237
+ cache["ipInfo"] = {
238
+ value: context.ipInfo,
239
+ timestamp: Date.now(),
240
+ };
1281
241
  }
1282
242
 
1283
243
  // Collect all system information
@@ -1295,6 +255,7 @@ export async function getSystemInfo(
1295
255
  // Call all info functions
1296
256
  for (const [key, fn] of Object.entries(infoFunctions)) {
1297
257
  try {
258
+ // most functions are sync but some are async (ip related)
1298
259
  const value = await fn(context);
1299
260
  info[key as keyof SystemInfo] = value as any;
1300
261
  } catch (error) {