about-system 0.10.10 → 0.10.12

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 (58) hide show
  1. package/README.md +5 -3
  2. package/dist/about-system-cli.js +69 -54
  3. package/dist/about-system-cli.js.map +1 -1
  4. package/dist/index.d.ts +64 -18
  5. package/dist/index.js +1 -1
  6. package/dist/system-info-api-7iy1ZV2o.js +2475 -0
  7. package/dist/system-info-api-7iy1ZV2o.js.map +1 -0
  8. package/dist/system-info-api.d.ts +432 -62
  9. package/dist/system-info-api.js +1 -1
  10. package/dist/web/assets/index--HOsj3jw.js +14 -0
  11. package/dist/web/assets/index-BU_blZyu.css +1 -0
  12. package/dist/web/index.html +13 -0
  13. package/package.json +1 -1
  14. package/src/about-system-cli.ts +42 -3
  15. package/src/cache/cache-config.ts +3 -3
  16. package/src/info/hardware.test.ts +27 -12
  17. package/src/info/hardware.ts +25 -22
  18. package/src/info/network.ts +14 -47
  19. package/src/system-info-api.test.ts +106 -0
  20. package/src/system-info-api.ts +67 -28
  21. package/src/systeminfo-types.d.ts +16 -10
  22. package/src/utils/network.ts +113 -94
  23. package/src/utils/utils.test.ts +84 -75
  24. package/dist/about-system-cli.d.ts.map +0 -1
  25. package/dist/cache/cache-config.d.ts +0 -62
  26. package/dist/cache/cache-config.d.ts.map +0 -1
  27. package/dist/cache/cache.d.ts +0 -53
  28. package/dist/cache/cache.d.ts.map +0 -1
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/info/hardware.d.ts +0 -76
  31. package/dist/info/hardware.d.ts.map +0 -1
  32. package/dist/info/memory.d.ts +0 -59
  33. package/dist/info/memory.d.ts.map +0 -1
  34. package/dist/info/network.d.ts +0 -54
  35. package/dist/info/network.d.ts.map +0 -1
  36. package/dist/info/platform.d.ts +0 -36
  37. package/dist/info/platform.d.ts.map +0 -1
  38. package/dist/info/process.d.ts +0 -23
  39. package/dist/info/process.d.ts.map +0 -1
  40. package/dist/info/settings.d.ts +0 -185
  41. package/dist/info/settings.d.ts.map +0 -1
  42. package/dist/info/software.d.ts +0 -33
  43. package/dist/info/software.d.ts.map +0 -1
  44. package/dist/info/system-status.d.ts +0 -36
  45. package/dist/info/system-status.d.ts.map +0 -1
  46. package/dist/system-info-api-ByXoHI0m.js +0 -2464
  47. package/dist/system-info-api-ByXoHI0m.js.map +0 -1
  48. package/dist/system-info-api.d.ts.map +0 -1
  49. package/dist/types/internal-types.d.ts +0 -14
  50. package/dist/types/internal-types.d.ts.map +0 -1
  51. package/dist/utils/command.d.ts +0 -18
  52. package/dist/utils/command.d.ts.map +0 -1
  53. package/dist/utils/network.d.ts +0 -27
  54. package/dist/utils/network.d.ts.map +0 -1
  55. package/dist/utils/platform.d.ts +0 -11
  56. package/dist/utils/platform.d.ts.map +0 -1
  57. package/dist/web-server.d.ts +0 -21
  58. package/dist/web-server.d.ts.map +0 -1
@@ -12,8 +12,11 @@
12
12
  import os from "os";
13
13
  import fs from "fs";
14
14
  import { fetchIPInfo } from "./utils/network";
15
+ import type { IPInfo } from "./utils/network";
15
16
  import path from "path";
16
17
  import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types";
18
+ import type { Cache } from "./cache/cache";
19
+ import { isCacheValid } from "./cache/cache";
17
20
  import { CACHE_FILE } from "./info/settings"; // Using CACHE_FILE from settings to ensure consistency
18
21
 
19
22
  // Import info functions from modules
@@ -159,34 +162,78 @@ export const infoFunctions = {
159
162
  screen_resolution,
160
163
  };
161
164
 
165
+ /** Blocks derived from the public IP lookup. */
166
+ const IP_BLOCKS = new Set(["ip", "city", "domain", "isp"]);
167
+
168
+ /** Lets pending I/O callbacks (keypresses, HTTP responses) run between blocks. */
169
+ const yieldToEventLoop = () => new Promise<void>((resolve) => setImmediate(resolve));
170
+
162
171
  /**
163
- * Get all system information as a clean JSON object
172
+ * Returns the cached public IP lookup, or fetches it when the cache entry is
173
+ * older than 10 minutes. A failed fetch keeps the previous value and is still
174
+ * timestamped, so an offline machine is not re-queried on every launch. An
175
+ * aborted fetch leaves the cache untouched.
176
+ */
177
+ async function getIPInfo(cache: Cache, signal?: AbortSignal): Promise<IPInfo> {
178
+ const entry = cache["ipInfo"];
179
+ if (entry && isCacheValid(entry, "ipInfo")) return entry.value || {};
180
+
181
+ const fresh = await fetchIPInfo(undefined, undefined, signal);
182
+ const value = fresh.ip ? fresh : entry?.value || {};
183
+ if (!signal?.aborted) {
184
+ cache["ipInfo"] = { value, timestamp: Date.now() };
185
+ }
186
+ return value;
187
+ }
188
+
189
+ /**
190
+ * Get system information as a clean JSON object.
191
+ *
192
+ * `options.keys` limits collection to those blocks (all by default).
193
+ * `options.signal` stops collection early: whatever was gathered before the
194
+ * abort is returned, and the remaining blocks are left out.
164
195
  */
165
196
  export async function getSystemInfo(
166
197
  options: SystemInfoOptions = {}
167
198
  ): Promise<SystemInfo> {
199
+ const { keys, signal } = options;
168
200
  const cache = loadCache();
169
201
  const context: InfoContext = { cache };
170
202
 
171
- // Check if we need IP info
172
- // Some functions need IP info, so we fetch it once if needed by any function
173
- // For now we always try to fetch it if not cached, as ip/city/etc depend on it
174
- // Optimization: check if we actually need to run those functions based on options if provided
175
- // But current implementation fetches it always if missing from cache
176
-
177
- const cachedIPInfo = cache["ipInfo"]?.value;
178
- if (cachedIPInfo) {
179
- context.ipInfo = cachedIPInfo;
180
- } else {
181
- context.ipInfo = await fetchIPInfo();
182
- // Cache IP info itself
183
- cache["ipInfo"] = {
184
- value: context.ipInfo,
185
- timestamp: Date.now(),
186
- };
203
+ const wanted = Object.keys(infoFunctions).filter(
204
+ (key) => !keys || keys.includes(key)
205
+ );
206
+
207
+ // Start the IP lookup first so it overlaps with the local blocks.
208
+ const ipInfoPromise = wanted.some((key) => IP_BLOCKS.has(key))
209
+ ? getIPInfo(cache, signal)
210
+ : undefined;
211
+
212
+ const collected: Record<string, unknown> = {};
213
+ const run = async (key: string) => {
214
+ try {
215
+ collected[key] = await infoFunctions[key as keyof typeof infoFunctions](context);
216
+ } catch {
217
+ collected[key] = "";
218
+ }
219
+ };
220
+
221
+ for (const key of wanted) {
222
+ if (IP_BLOCKS.has(key)) continue;
223
+ await yieldToEventLoop();
224
+ if (signal?.aborted) break;
225
+ await run(key);
226
+ }
227
+
228
+ // On abort the lookup resolves at once with whatever was cached.
229
+ if (ipInfoPromise) {
230
+ context.ipInfo = await ipInfoPromise;
231
+ for (const key of wanted) {
232
+ if (IP_BLOCKS.has(key)) await run(key);
233
+ }
187
234
  }
188
235
 
189
- // Collect all system information
236
+ // Keep the infoFunctions key order regardless of collection order.
190
237
  const info: Partial<SystemInfo> = {
191
238
  timestamp: new Date().toISOString(),
192
239
  platform: IS_WINDOWS
@@ -197,16 +244,8 @@ export async function getSystemInfo(
197
244
  ? "linux"
198
245
  : "unknown",
199
246
  };
200
-
201
- // Call all info functions
202
- for (const [key, fn] of Object.entries(infoFunctions)) {
203
- try {
204
- // most functions are sync but some are async (ip related)
205
- const value = await fn(context);
206
- info[key as keyof SystemInfo] = value as any;
207
- } catch (error) {
208
- info[key as keyof SystemInfo] = "" as any;
209
- }
247
+ for (const key of wanted) {
248
+ if (key in collected) info[key as keyof SystemInfo] = collected[key] as any;
210
249
  }
211
250
 
212
251
  saveCache(cache);
@@ -125,12 +125,12 @@ export interface SystemInfo {
125
125
  /**
126
126
  * CPU benchmark score from Geekbench 6 multi-core
127
127
  * Shows performance score (in thousands) and ranking position
128
- * Uses fuzzy matching to find similar CPU models from top 1000 list
128
+ * Uses fuzzy matching to find similar CPU models from Top 1000 list
129
129
  * Empty string if CPU not found or benchmark data unavailable
130
130
  *
131
131
  * @example
132
- * - "38k #1 top 1%" (top performing CPU)
133
- * - "21k #68 top 7%" (mid-range CPU)
132
+ * - "38k #1 Top 1%" (top performing CPU)
133
+ * - "21k #68 Top 7%" (mid-range CPU)
134
134
  * - "" (CPU not in benchmark database)
135
135
  */
136
136
  bench: string;
@@ -142,9 +142,9 @@ export interface SystemInfo {
142
142
  * Empty string if CPU not found or benchmark data unavailable
143
143
  *
144
144
  * @example
145
- * - "Geekbench 6: 38k (Rank #1, top 1%) - ARM" (Apple M4 Ultra)
146
- * - "Geekbench 6: 21k (Rank #68, top 7%) - Intel" (Intel Core i9)
147
- * - "Geekbench 6: 19k (Rank #91, top 10%) - AMD" (AMD Ryzen 9 7950X)
145
+ * - "Geekbench 6: 38k (Rank #1, Top 1%) - ARM" (Apple M4 Ultra)
146
+ * - "Geekbench 6: 21k (Rank #68, Top 7%) - Intel" (Intel Core i9)
147
+ * - "Geekbench 6: 19k (Rank #91, Top 10%) - AMD" (AMD Ryzen 9 7950X)
148
148
  * - "" (CPU not in benchmark database)
149
149
  */
150
150
  cpu_bench_info: string;
@@ -156,8 +156,8 @@ export interface SystemInfo {
156
156
  * Empty string if GPU not found or benchmark data unavailable
157
157
  *
158
158
  * @example
159
- * - "38k #1 top 1%" (top performing GPU)
160
- * - "21k #68 top 7%" (mid-range GPU)
159
+ * - "38k #1 Top 1%" (top performing GPU)
160
+ * - "21k #68 Top 7%" (mid-range GPU)
161
161
  * - "" (GPU not in benchmark database)
162
162
  */
163
163
  gpu_bench: string;
@@ -169,8 +169,8 @@ export interface SystemInfo {
169
169
  * Empty string if GPU not found or benchmark data unavailable
170
170
  *
171
171
  * @example
172
- * - "Geekbench 6: 38k (Rank #1, top 1%) - NVIDIA" (NVIDIA GPU)
173
- * - "Geekbench 6: 21k (Rank #68, top 7%) - AMD" (AMD GPU)
172
+ * - "Geekbench 6: 38k (Rank #1, Top 1%) - NVIDIA" (NVIDIA GPU)
173
+ * - "Geekbench 6: 21k (Rank #68, Top 7%) - AMD" (AMD GPU)
174
174
  * - "" (GPU not in benchmark database)
175
175
  */
176
176
  gpu_bench_info: string;
@@ -475,6 +475,12 @@ export interface SystemInfoOptions {
475
475
 
476
476
  /** Return data object instead of printing to console (module usage) */
477
477
  returnData?: boolean;
478
+
479
+ /** Only collect these blocks (e.g. ["cpu", "ip"]); all blocks when omitted */
480
+ keys?: string[];
481
+
482
+ /** Abort to stop collecting early and return only the blocks gathered so far */
483
+ signal?: AbortSignal;
478
484
  }
479
485
 
480
486
  /**
@@ -3,6 +3,7 @@
3
3
  * @module network
4
4
  */
5
5
 
6
+ import http from "http";
6
7
  import https from "https";
7
8
  import {
8
9
  DEFAULT_IPINFO_TOKEN,
@@ -29,6 +30,7 @@ export interface IPInfo {
29
30
  * @interface IPAPIInfo
30
31
  */
31
32
  interface IPAPIInfo {
33
+ status?: string;
32
34
  query?: string;
33
35
  city?: string;
34
36
  isp?: string;
@@ -37,122 +39,139 @@ interface IPAPIInfo {
37
39
  }
38
40
 
39
41
  /**
40
- * Fetches IP geolocation information from ipinfo.io lite API
41
- * @param {string} token - IPInfo.io API token
42
- * @param {number} timeout - Request timeout in milliseconds
43
- * @returns {Promise<IPInfo>} IP information object or empty object on error
42
+ * GETs a URL and parses the body as JSON. Never rejects: resolves `null` on
43
+ * any error, on abort, or once `timeout` ms have passed in total — a hard
44
+ * deadline for the whole request, not a socket-idle timeout.
44
45
  */
45
- async function fetchFromIPInfo(
46
- token: string = DEFAULT_IPINFO_TOKEN,
47
- timeout: number = DEFAULT_NETWORK_TIMEOUT
48
- ): Promise<IPInfo> {
46
+ function getJSON(
47
+ url: string,
48
+ timeout: number,
49
+ signal?: AbortSignal
50
+ ): Promise<any> {
49
51
  return new Promise((resolve) => {
50
- const url = `https://api.ipinfo.io/lite/8.8.8.8?token=${token}`;
52
+ if (signal?.aborted) return resolve(null);
51
53
 
52
- const req = https.get(url, (res) => {
53
- let data = "";
54
- res.on("data", (chunk) => (data += chunk));
55
- res.on("end", () => {
56
- try {
57
- const parsed = JSON.parse(data);
58
- // ipinfo lite returns: { ip, city, region, country, org }
59
- // Map to our standard IPInfo format
60
- resolve({
61
- ip: parsed.ip,
62
- city: parsed.city,
63
- hostname: parsed.hostname,
64
- org: parsed.org,
65
- });
66
- } catch {
67
- resolve({});
68
- }
69
- });
70
- });
54
+ let settled = false;
55
+ let req: http.ClientRequest | undefined;
56
+ const finish = (value: any) => {
57
+ if (settled) return;
58
+ settled = true;
59
+ clearTimeout(timer);
60
+ signal?.removeEventListener("abort", onAbort);
61
+ resolve(value);
62
+ };
63
+ const onAbort = () => {
64
+ req?.destroy();
65
+ finish(null);
66
+ };
67
+ const timer = setTimeout(onAbort, timeout);
68
+ signal?.addEventListener("abort", onAbort);
71
69
 
72
- req.on("error", () => resolve({}));
73
- req.setTimeout(timeout, () => {
74
- req.destroy();
75
- resolve({});
76
- });
70
+ try {
71
+ const get = url.startsWith("https:") ? https.get : http.get;
72
+ req = get(url, (res) => {
73
+ let data = "";
74
+ res.on("data", (chunk) => (data += chunk));
75
+ res.on("end", () => {
76
+ try {
77
+ finish(JSON.parse(data));
78
+ } catch {
79
+ finish(null);
80
+ }
81
+ });
82
+ res.on("error", () => finish(null));
83
+ });
84
+ req.on("error", () => finish(null));
85
+ } catch {
86
+ finish(null);
87
+ }
77
88
  });
78
89
  }
79
90
 
80
91
  /**
81
- * Fetches IP geolocation information from ip-api.com (fallback)
82
- * @param {number} timeout - Request timeout in milliseconds
92
+ * Fetches IP information for this machine from the ipinfo.io lite API
93
+ * @param {string} token - IPInfo.io API token
94
+ * @param {number} timeout - Total request deadline in milliseconds
95
+ * @param {AbortSignal} signal - Abandons the request when aborted
83
96
  * @returns {Promise<IPInfo>} IP information object or empty object on error
84
97
  */
85
- async function fetchFromIPAPI(
86
- timeout: number = DEFAULT_NETWORK_TIMEOUT
98
+ export async function fetchFromIPInfo(
99
+ token: string = DEFAULT_IPINFO_TOKEN,
100
+ timeout: number = DEFAULT_NETWORK_TIMEOUT,
101
+ signal?: AbortSignal
87
102
  ): Promise<IPInfo> {
88
- return new Promise((resolve) => {
89
- const url = "http://ip-api.com/json/?fields=status,message,query,city,isp,org,as";
90
-
91
- const req = https.get(url, (res) => {
92
- let data = "";
93
- res.on("data", (chunk) => (data += chunk));
94
- res.on("end", () => {
95
- try {
96
- const parsed: IPAPIInfo = JSON.parse(data);
97
- if (parsed.status === "success") {
98
- // Map ip-api response to our standard IPInfo format
99
- resolve({
100
- ip: parsed.query,
101
- city: parsed.city,
102
- hostname: undefined,
103
- org: parsed.org || parsed.isp || parsed.as,
104
- });
105
- } else {
106
- resolve({});
107
- }
108
- } catch {
109
- resolve({});
110
- }
111
- });
112
- });
103
+ const parsed = await getJSON(
104
+ `https://api.ipinfo.io/lite/me?token=${token}`,
105
+ timeout,
106
+ signal
107
+ );
108
+ if (!parsed || typeof parsed !== "object") return {};
109
+ // ipinfo lite returns { ip, asn, as_name, country, ... } — no city.
110
+ const org =
111
+ parsed.org ?? (parsed.asn && parsed.as_name ? `${parsed.asn} ${parsed.as_name}` : undefined);
112
+ return {
113
+ ip: parsed.ip,
114
+ city: parsed.city,
115
+ hostname: parsed.hostname,
116
+ org,
117
+ };
118
+ }
113
119
 
114
- req.on("error", () => resolve({}));
115
- req.setTimeout(timeout, () => {
116
- req.destroy();
117
- resolve({});
118
- });
119
- });
120
+ /**
121
+ * Fetches IP geolocation information from ip-api.com (fallback). The free
122
+ * tier is plain HTTP only.
123
+ * @param {number} timeout - Total request deadline in milliseconds
124
+ * @param {AbortSignal} signal - Abandons the request when aborted
125
+ * @returns {Promise<IPInfo>} IP information object or empty object on error
126
+ */
127
+ export async function fetchFromIPAPI(
128
+ timeout: number = DEFAULT_NETWORK_TIMEOUT,
129
+ signal?: AbortSignal
130
+ ): Promise<IPInfo> {
131
+ const parsed: IPAPIInfo | null = await getJSON(
132
+ "http://ip-api.com/json/?fields=status,message,query,city,isp,org,as",
133
+ timeout,
134
+ signal
135
+ );
136
+ if (!parsed || parsed.status !== "success") return {};
137
+ return {
138
+ ip: parsed.query,
139
+ city: parsed.city,
140
+ hostname: undefined,
141
+ org: parsed.org || parsed.isp || parsed.as,
142
+ };
120
143
  }
121
144
 
122
145
  /**
123
- * Fetches IP geolocation information using race between ipinfo.io and ip-api.com
124
- * Uses ipinfo.io lite endpoint first, falls back to ip-api.com if slower or fails
146
+ * Fetches IP information from ipinfo.io and ip-api.com in parallel and merges
147
+ * them: ipinfo.io wins per field, ip-api.com fills the gaps (ipinfo lite has
148
+ * no city). Both requests share the same deadline, so this never takes longer
149
+ * than `timeout`.
125
150
  * @param {string} token - IPInfo.io API token
126
- * @param {number} timeout - Request timeout in milliseconds
151
+ * @param {number} timeout - Total deadline in milliseconds
152
+ * @param {AbortSignal} signal - Abandons both requests when aborted
127
153
  * @returns {Promise<IPInfo>} IP information object or empty object on error
128
154
  */
129
155
  export async function fetchIPInfo(
130
156
  token: string = DEFAULT_IPINFO_TOKEN,
131
- timeout: number = DEFAULT_NETWORK_TIMEOUT
157
+ timeout: number = DEFAULT_NETWORK_TIMEOUT,
158
+ signal?: AbortSignal
132
159
  ): Promise<IPInfo> {
133
- // Race both services, use whichever responds first
134
- const [ipinfoResult, ipapiResult] = await Promise.allSettled([
135
- fetchFromIPInfo(token, timeout),
136
- fetchFromIPAPI(timeout),
160
+ const [ipinfo, ipapi] = await Promise.all([
161
+ fetchFromIPInfo(token, timeout, signal),
162
+ fetchFromIPAPI(timeout, signal),
137
163
  ]);
138
164
 
139
- // Prefer ipinfo.io result if successful and has data
140
- if (ipinfoResult.status === "fulfilled" && ipinfoResult.value && ipinfoResult.value.ip) {
141
- return ipinfoResult.value;
165
+ const merged: IPInfo = {};
166
+ for (const source of [ipapi, ipinfo]) {
167
+ for (const [key, value] of Object.entries(source)) {
168
+ if (value) merged[key as keyof IPInfo] = value;
169
+ }
142
170
  }
143
-
144
- // Fall back to ip-api.com result if successful and has data
145
- if (ipapiResult.status === "fulfilled" && ipapiResult.value && ipapiResult.value.ip) {
146
- return ipapiResult.value;
147
- }
148
-
149
- // Return whichever has data, or empty object
150
- if (ipinfoResult.status === "fulfilled" && ipinfoResult.value) {
151
- return ipinfoResult.value;
171
+ // ipinfo answers over IPv6 when the machine has it; the short IPv4 address
172
+ // reads better in a one-line greeting, so keep ip-api's when it has one.
173
+ if (merged.ip?.includes(":") && ipapi.ip && !ipapi.ip.includes(":")) {
174
+ merged.ip = ipapi.ip;
152
175
  }
153
- if (ipapiResult.status === "fulfilled" && ipapiResult.value) {
154
- return ipapiResult.value;
155
- }
156
-
157
- return {};
158
- }
176
+ return merged;
177
+ }