about-system 0.10.15 → 0.10.17

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.
@@ -1,6 +1,6 @@
1
1
  import "os";
2
2
  import "fs";
3
- import { g as i, i as m, l as r, s as n } from "./system-info-api-7iy1ZV2o.js";
3
+ import { g as i, i as m, l as r, s as n } from "./system-info-api-fNqdECMN.js";
4
4
  import "path";
5
5
  export {
6
6
  i as getSystemInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "about-system",
3
- "version": "0.10.15",
3
+ "version": "0.10.17",
4
4
  "description": "A Node.js script to display key system information with emojis. Cross-platform support for Windows, macOS, and Linux with customizable output and caching.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,509 @@
1
+ /**
2
+ * @fileoverview End-to-end tests for the CLI entry: argument routing, the
3
+ * settings sub-commands, output formatting/wrapping, the shell-greeting
4
+ * installer and the web mode. `main()` runs on import, so each case sets
5
+ * `process.argv`, imports a fresh module and waits for it to settle.
6
+ */
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+
9
+ const getSystemInfo = vi.hoisted(() => vi.fn());
10
+ vi.mock("./system-info-api", () => ({ getSystemInfo }));
11
+
12
+ const startWebServer = vi.hoisted(() => vi.fn());
13
+ vi.mock("./web-server", () => ({
14
+ startWebServer,
15
+ DEFAULT_WEB_PORT: 7777,
16
+ DEFAULT_WEB_HOST: "127.0.0.1",
17
+ }));
18
+
19
+ const settingsIO = vi.hoisted(() => ({
20
+ loadSettings: vi.fn(),
21
+ saveSettings: vi.fn(),
22
+ }));
23
+ vi.mock("./info/settings", async (importOriginal) => {
24
+ const actual = await importOriginal<typeof import("./info/settings")>();
25
+ return {
26
+ ...actual,
27
+ loadSettings: settingsIO.loadSettings,
28
+ saveSettings: settingsIO.saveSettings,
29
+ };
30
+ });
31
+
32
+ const fsMock = vi.hoisted(() => ({
33
+ existsSync: vi.fn(() => false),
34
+ readFileSync: vi.fn(() => ""),
35
+ writeFileSync: vi.fn(),
36
+ mkdirSync: vi.fn(),
37
+ copyFileSync: vi.fn(),
38
+ chmodSync: vi.fn(),
39
+ appendFileSync: vi.fn(),
40
+ unlinkSync: vi.fn(),
41
+ }));
42
+ vi.mock("fs", () => ({ ...fsMock, default: fsMock }));
43
+
44
+ const osState = vi.hoisted(() => ({ platform: "linux" }));
45
+ vi.mock("os", () => {
46
+ const api = {
47
+ platform: () => osState.platform,
48
+ homedir: () => "/home/ada",
49
+ tmpdir: () => "/tmp",
50
+ };
51
+ return { ...api, default: api };
52
+ });
53
+
54
+ import { DEFAULT_SETTINGS } from "./info/settings";
55
+
56
+ let out: string[];
57
+ let errs: string[];
58
+ let exitCode: number | undefined;
59
+ let origArgv: string[];
60
+ let origEnvPort: string | undefined;
61
+
62
+ const fresh = () => JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
63
+ // eslint-disable-next-line no-control-regex
64
+ const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
65
+
66
+ async function run(args: string[], platform = "linux") {
67
+ osState.platform = platform;
68
+ process.argv = ["node", "about-system", ...args];
69
+ vi.resetModules();
70
+ const mod = await import("./about-system-cli");
71
+ await new Promise((r) => setTimeout(r, 15));
72
+ return mod;
73
+ }
74
+
75
+ beforeEach(() => {
76
+ out = [];
77
+ errs = [];
78
+ exitCode = undefined;
79
+ origArgv = process.argv;
80
+ origEnvPort = process.env.PORT;
81
+ delete process.env.PORT;
82
+ for (const f of Object.values(fsMock)) f.mockReset();
83
+ fsMock.existsSync.mockReturnValue(false);
84
+ fsMock.readFileSync.mockReturnValue("");
85
+ settingsIO.loadSettings.mockReset().mockImplementation(() => fresh());
86
+ settingsIO.saveSettings.mockReset().mockReturnValue(true);
87
+ getSystemInfo.mockReset().mockResolvedValue({
88
+ user: "ada",
89
+ hostname: "box",
90
+ cpu: "Ryzen",
91
+ battery: "80%+",
92
+ });
93
+ startWebServer.mockReset().mockResolvedValue({ url: "http://127.0.0.1:7777" });
94
+ vi.spyOn(console, "log").mockImplementation((...a) => void out.push(a.join(" ")));
95
+ vi.spyOn(console, "error").mockImplementation((...a) => void errs.push(a.join(" ")));
96
+ vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
97
+ exitCode = code;
98
+ }) as never);
99
+ });
100
+
101
+ afterEach(() => {
102
+ process.argv = origArgv;
103
+ if (origEnvPort === undefined) delete process.env.PORT;
104
+ else process.env.PORT = origEnvPort;
105
+ vi.restoreAllMocks();
106
+ });
107
+
108
+ describe("settings sub-commands", () => {
109
+ it("--settings-init writes the defaults", async () => {
110
+ await run(["--settings-init"]);
111
+ expect(settingsIO.saveSettings).toHaveBeenCalledWith(DEFAULT_SETTINGS);
112
+ expect(out.join("\n")).toContain("Settings initialized with defaults");
113
+ expect(getSystemInfo).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it("--settings-init / --settings-reset report a failed save", async () => {
117
+ settingsIO.saveSettings.mockReturnValue(false);
118
+ await run(["--settings-init"]);
119
+ expect(out.join("\n")).toContain("Failed to initialize settings");
120
+ out.length = 0;
121
+ await run(["--settings-reset"]);
122
+ expect(out.join("\n")).toContain("Failed to reset settings");
123
+ });
124
+
125
+ it("--settings-reset restores the defaults", async () => {
126
+ await run(["--settings-reset"]);
127
+ expect(out.join("\n")).toContain("Settings reset to defaults");
128
+ });
129
+
130
+ it("--settings-show prints the current settings as JSON", async () => {
131
+ await run(["--settings-show"]);
132
+ expect(out[0]).toBe("Current settings:");
133
+ expect(JSON.parse(out[1]).labels.user).toBe("User");
134
+ });
135
+
136
+ it("--refresh deletes the cache file when present", async () => {
137
+ fsMock.existsSync.mockReturnValue(true);
138
+ await run(["--refresh"]);
139
+ expect(fsMock.unlinkSync).toHaveBeenCalled();
140
+ expect(out.join("\n")).toContain("Cache cleared");
141
+ });
142
+
143
+ it("--refresh is quiet when there is no cache, and reports unlink errors", async () => {
144
+ await run(["--refresh"]);
145
+ expect(fsMock.unlinkSync).not.toHaveBeenCalled();
146
+ fsMock.existsSync.mockReturnValue(true);
147
+ fsMock.unlinkSync.mockImplementation(() => {
148
+ throw new Error("EPERM");
149
+ });
150
+ await run(["--refresh"]);
151
+ expect(errs.join("\n")).toContain("Error clearing cache: EPERM");
152
+ });
153
+
154
+ it("--set coerces booleans, JSON and strings and supports dotted keys", async () => {
155
+ await run(["--set", "display.show_emojis", "false"]);
156
+ expect(settingsIO.saveSettings.mock.calls[0][0].display.show_emojis).toBe(false);
157
+ expect(out.join("\n")).toContain("Setting display.show_emojis = false");
158
+
159
+ await run(["--set", "display.single_line", "true"]);
160
+ expect(settingsIO.saveSettings.mock.calls[1][0].display.single_line).toBe(true);
161
+
162
+ await run(["--set", "colors.user", "blue"]);
163
+ expect(settingsIO.saveSettings.mock.calls[2][0].colors.user).toBe("blue");
164
+
165
+ await run(["--set", "display_order", '[["user"],["cpu"]]']);
166
+ expect(settingsIO.saveSettings.mock.calls[3][0].display_order).toEqual([["user"], ["cpu"]]);
167
+ });
168
+
169
+ it("--set creates missing intermediate objects", async () => {
170
+ await run(["--set", "brand.new.key", "v"]);
171
+ expect(settingsIO.saveSettings.mock.calls[0][0].brand.new.key).toBe("v");
172
+ });
173
+
174
+ it("--set reports a failed save and invalid JSON", async () => {
175
+ settingsIO.saveSettings.mockReturnValue(false);
176
+ await run(["--set", "labels.cpu", "Processor"]);
177
+ expect(out.join("\n")).toContain("Failed to save settings");
178
+ await run(["--set", "labels.cpu", "{broken"]);
179
+ expect(errs.join("\n")).toContain("Error setting value");
180
+ });
181
+
182
+ it("--set without a value falls through to normal display", async () => {
183
+ await run(["--set", "only-key"]);
184
+ expect(settingsIO.saveSettings).not.toHaveBeenCalled();
185
+ });
186
+ });
187
+
188
+ describe("help", () => {
189
+ it.each([
190
+ ["linux", "Linux"],
191
+ ["darwin", "macOS"],
192
+ ["win32", "Windows"],
193
+ ["freebsd", "Unknown"],
194
+ ])("--help names the platform (%s)", async (plat, label) => {
195
+ await run(["--help"], plat);
196
+ const text = out.join("\n");
197
+ expect(text).toContain("System Info Script");
198
+ expect(text).toContain(`Platform: ${label}`);
199
+ expect(text).toContain("http://127.0.0.1:7777");
200
+ });
201
+
202
+ it("-h is an alias", async () => {
203
+ await run(["-h"]);
204
+ expect(out.join("\n")).toContain("Usage:");
205
+ });
206
+ });
207
+
208
+ describe("display", () => {
209
+ it("prints one formatted line by default", async () => {
210
+ await run([]);
211
+ expect(out).toHaveLength(1);
212
+ const text = strip(out[0]);
213
+ expect(text).toContain("ada");
214
+ expect(text).toContain("Ryzen");
215
+ expect(getSystemInfo).toHaveBeenCalledWith(
216
+ expect.objectContaining({ keys: expect.arrayContaining(["user", "cpu"]) }),
217
+ );
218
+ });
219
+
220
+ it("CLI mode shows just the requested parts (comma list)", async () => {
221
+ await run(["user,cpu, "]);
222
+ expect(getSystemInfo.mock.calls[0][0].keys).toEqual(["user", "cpu"]);
223
+ expect(strip(out[0])).toBe("👤 ada 📈 Ryzen");
224
+ });
225
+
226
+ it("CLI mode accepts a single part and ignores flags and key=value", async () => {
227
+ await run(["--verbose", "a=b", "hostname"]);
228
+ expect(getSystemInfo.mock.calls[0][0].keys).toEqual(["hostname"]);
229
+ });
230
+
231
+ it("omits emojis and backgrounds when disabled", async () => {
232
+ settingsIO.loadSettings.mockImplementation(() => {
233
+ const s = fresh();
234
+ s.display.show_emojis = false;
235
+ s.display.show_backgrounds = false;
236
+ return s;
237
+ });
238
+ await run(["user"]);
239
+ expect(out[0]).not.toContain("👤");
240
+ expect(out[0]).not.toContain("\x1b[48;");
241
+ });
242
+
243
+ it("swaps the battery emoji for charging state", async () => {
244
+ await run(["battery"]);
245
+ expect(strip(out[0])).toBe("🔌 80%+");
246
+ getSystemInfo.mockResolvedValue({ battery: "40%" });
247
+ await run(["battery"]);
248
+ expect(strip(out[1])).toBe("🔋 40%");
249
+ });
250
+
251
+ it("renders multicolor ports with backgrounds, or plain colors without", async () => {
252
+ getSystemInfo.mockResolvedValue({ ports: "3000node 8080ngin 22ssh 53dns 80web 443web 9000x" });
253
+ await run(["ports"]);
254
+ expect(out[0]).toContain("\x1b[41m\x1b[97m3000node");
255
+ expect(out[0]).toContain("9000x");
256
+
257
+ settingsIO.loadSettings.mockImplementation(() => {
258
+ const s = fresh();
259
+ s.display.show_backgrounds = false;
260
+ return s;
261
+ });
262
+ out.length = 0;
263
+ await run(["ports"]);
264
+ expect(out[0]).toContain("\x1b[31m3000node\x1b[0m");
265
+ });
266
+
267
+ it("renders multicolor pacman values", async () => {
268
+ getSystemInfo.mockResolvedValue({ pacman: "apt npm" });
269
+ await run(["pacman"]);
270
+ expect(strip(out[0])).toBe("🚀 apt npm");
271
+ });
272
+
273
+ it("skips blank values and prints nothing when everything is blank", async () => {
274
+ getSystemInfo.mockResolvedValue({ user: " ", cpu: "" });
275
+ await run(["user,cpu"]);
276
+ expect(out).toEqual([]);
277
+ });
278
+
279
+ it("wraps long output to line_wrap_length when single_line is off", async () => {
280
+ settingsIO.loadSettings.mockImplementation(() => {
281
+ const s = fresh();
282
+ s.display.single_line = false;
283
+ s.display.line_wrap_length = 10;
284
+ return s;
285
+ });
286
+ getSystemInfo.mockResolvedValue({ user: "abcdefghijklmnopqrstuvwxyz" });
287
+ await run(["user"]);
288
+ expect(out.length).toBeGreaterThan(2);
289
+ for (const line of out) expect(strip(line).length).toBeLessThanOrEqual(10);
290
+ expect(strip(out.join(""))).toContain("abcdefghijklmnopqrstuvwxyz".slice(0, 5));
291
+ });
292
+
293
+ it("re-applies the active color on each wrapped line and handles emoji width", async () => {
294
+ settingsIO.loadSettings.mockImplementation(() => {
295
+ const s = fresh();
296
+ s.display.single_line = false;
297
+ s.display.line_wrap_length = 6;
298
+ return s;
299
+ });
300
+ getSystemInfo.mockResolvedValue({ user: "aaaaaaaaaaaa" });
301
+ await run(["user"]);
302
+ expect(out.length).toBeGreaterThan(1);
303
+ // The continuation line starts with the style that was active at the break.
304
+ expect(out[1].startsWith("\x1b[")).toBe(true);
305
+ });
306
+
307
+ it("does not wrap when line_wrap_length is non-positive", async () => {
308
+ settingsIO.loadSettings.mockImplementation(() => {
309
+ const s = fresh();
310
+ s.display.single_line = false;
311
+ s.display.line_wrap_length = 0;
312
+ return s;
313
+ });
314
+ await run(["user"]);
315
+ expect(out).toHaveLength(1);
316
+ });
317
+
318
+ it("prints a debug message when wrapping yields nothing", async () => {
319
+ settingsIO.loadSettings.mockImplementation(() => {
320
+ const s = fresh();
321
+ s.display.single_line = false;
322
+ s.advanced.debug = true;
323
+ return s;
324
+ });
325
+ getSystemInfo.mockResolvedValue({});
326
+ await run(["user"]);
327
+ expect(out.join("\n")).toContain("No system information could be displayed");
328
+ });
329
+
330
+ it("--json prints the raw info object", async () => {
331
+ await run(["--json"]);
332
+ expect(JSON.parse(out[0])).toMatchObject({ user: "ada", cpu: "Ryzen" });
333
+ expect(getSystemInfo.mock.calls[0][0].keys).toBeUndefined();
334
+ });
335
+
336
+ it("reports unexpected failures and exits 1", async () => {
337
+ getSystemInfo.mockRejectedValue(new Error("probe exploded"));
338
+ await run([]);
339
+ expect(errs.join("\n")).toContain("Error: probe exploded");
340
+ expect(exitCode).toBe(1);
341
+ });
342
+ });
343
+
344
+ describe("interactive key handling", () => {
345
+ const stdin = process.stdin as unknown as Record<string, unknown>;
346
+ let handlers: Record<string, (b: Buffer) => void>;
347
+ const saved: Record<string, unknown> = {};
348
+
349
+ beforeEach(() => {
350
+ handlers = {};
351
+ for (const k of ["isTTY", "setRawMode", "on", "off", "resume", "pause"]) saved[k] = stdin[k];
352
+ stdin.isTTY = true;
353
+ stdin.setRawMode = vi.fn();
354
+ stdin.on = vi.fn((ev: string, fn: (b: Buffer) => void) => {
355
+ handlers[ev] = fn;
356
+ return stdin;
357
+ });
358
+ stdin.off = vi.fn();
359
+ stdin.resume = vi.fn();
360
+ stdin.pause = vi.fn();
361
+ });
362
+
363
+ afterEach(() => {
364
+ for (const [k, v] of Object.entries(saved)) stdin[k] = v;
365
+ });
366
+
367
+ it("puts a TTY stdin into raw mode while collecting and restores it", async () => {
368
+ await run([]);
369
+ expect(stdin.setRawMode).toHaveBeenCalledWith(true);
370
+ expect(stdin.setRawMode).toHaveBeenLastCalledWith(false);
371
+ expect(stdin.pause).toHaveBeenCalled();
372
+ });
373
+
374
+ it("Esc aborts the collection; Ctrl+C exits 130", async () => {
375
+ let signal: AbortSignal | undefined;
376
+ getSystemInfo.mockImplementation(async (opts: { signal: AbortSignal }) => {
377
+ signal = opts.signal;
378
+ handlers.data(Buffer.from([0x1b]));
379
+ return { user: "partial" };
380
+ });
381
+ await run(["user"]);
382
+ expect(signal?.aborted).toBe(true);
383
+
384
+ getSystemInfo.mockImplementation(async () => {
385
+ handlers.data(Buffer.from([0x03]));
386
+ return { user: "x" };
387
+ });
388
+ await run(["user"]);
389
+ expect(exitCode).toBe(130);
390
+ });
391
+
392
+ it("ignores other keys", async () => {
393
+ let signal: AbortSignal | undefined;
394
+ getSystemInfo.mockImplementation(async (opts: { signal: AbortSignal }) => {
395
+ signal = opts.signal;
396
+ handlers.data(Buffer.from("a"));
397
+ return { user: "x" };
398
+ });
399
+ await run(["user"]);
400
+ expect(signal?.aborted).toBe(false);
401
+ expect(exitCode).toBeUndefined();
402
+ });
403
+ });
404
+
405
+ describe("web mode", () => {
406
+ it("starts on the defaults and says it is localhost-only", async () => {
407
+ await run(["web"]);
408
+ expect(startWebServer).toHaveBeenCalledWith({ port: 7777, host: "127.0.0.1" });
409
+ const text = out.join("\n");
410
+ expect(text).toContain("About System web UI running at http://127.0.0.1:7777");
411
+ expect(text).toContain("localhost only");
412
+ expect(text).toContain("Press Ctrl+C");
413
+ });
414
+
415
+ it("honours --port/--host (both spellings) and warns on a public bind", async () => {
416
+ await run(["web", "--port", "8080", "--host=0.0.0.0"]);
417
+ expect(startWebServer).toHaveBeenCalledWith({ port: 8080, host: "0.0.0.0" });
418
+ expect(out.join("\n")).toContain("Warning: system details");
419
+ });
420
+
421
+ it("--web works anywhere and PORT env is used as a fallback", async () => {
422
+ process.env.PORT = "9090";
423
+ await run(["--web"]);
424
+ expect(startWebServer).toHaveBeenCalledWith({ port: 9090, host: "127.0.0.1" });
425
+ });
426
+
427
+ it.each(["abc", "-1", "70000", "1.5"])("rejects the invalid port %s", async (bad) => {
428
+ await run(["web", `--port=${bad}`]);
429
+ expect(errs.join("\n")).toContain(`Invalid port: ${bad}`);
430
+ expect(exitCode).toBe(1);
431
+ expect(startWebServer).not.toHaveBeenCalled();
432
+ errs.length = 0;
433
+ });
434
+ });
435
+
436
+ describe("--install (shell greeting)", () => {
437
+ const written = () => fsMock.writeFileSync.mock.calls.map((c) => String(c[0]));
438
+ const appended = () => fsMock.appendFileSync.mock.calls.map((c) => String(c[0]));
439
+
440
+ it("copies the script, hushes login and creates .bashrc when missing", async () => {
441
+ await run(["--install"]);
442
+ expect(fsMock.mkdirSync).toHaveBeenCalledWith("/home/ada/.config", { recursive: true });
443
+ expect(fsMock.copyFileSync).toHaveBeenCalled();
444
+ expect(fsMock.chmodSync).toHaveBeenCalledWith("/home/ada/.config/systeminfo", "755");
445
+ expect(written()).toContain("/home/ada/.hushlogin");
446
+ expect(fsMock.writeFileSync).toHaveBeenCalledWith(
447
+ "/home/ada/.bashrc",
448
+ "node /home/ada/.config/systeminfo\n",
449
+ );
450
+ expect(out.join("\n")).toContain("Shell greeting installation completed!");
451
+ });
452
+
453
+ it("appends to existing rc files that don't mention systeminfo yet", async () => {
454
+ fsMock.existsSync.mockReturnValue(true);
455
+ fsMock.readFileSync.mockReturnValue("# my rc\n");
456
+ await run(["--install"]);
457
+ const files = appended();
458
+ expect(files).toContain("/home/ada/.bashrc");
459
+ expect(files).toContain("/home/ada/.zshrc");
460
+ expect(files).toContain("/home/ada/.config/fish/config.fish");
461
+ expect(files).toContain("/home/ada/.config/nushell/config.nu");
462
+ const fish = fsMock.appendFileSync.mock.calls.find((c) => String(c[0]).includes("fish"));
463
+ expect(String(fish?.[1])).toContain('set -U fish_greeting ""');
464
+ const nu = fsMock.appendFileSync.mock.calls.find((c) => String(c[0]).includes("nushell"));
465
+ expect(String(nu?.[1])).toContain("show_banner = false");
466
+ expect(fsMock.mkdirSync).not.toHaveBeenCalled();
467
+ });
468
+
469
+ it("is idempotent when the rc files already call systeminfo", async () => {
470
+ fsMock.existsSync.mockReturnValue(true);
471
+ fsMock.readFileSync.mockReturnValue("node ~/.config/systeminfo\n");
472
+ await run(["--install"]);
473
+ expect(fsMock.appendFileSync).not.toHaveBeenCalled();
474
+ });
475
+
476
+ it("tolerates a failing .hushlogin write", async () => {
477
+ fsMock.writeFileSync.mockImplementation((p: string) => {
478
+ if (String(p).endsWith(".hushlogin")) throw new Error("EACCES");
479
+ });
480
+ await run(["--install"]);
481
+ expect(out.join("\n")).toContain("installation completed");
482
+ });
483
+
484
+ it("prints PowerShell instructions and writes a startup batch file on Windows", async () => {
485
+ await run(["--install"], "win32");
486
+ const text = out.join("\n");
487
+ expect(text).toContain("Windows installation:");
488
+ expect(text).toContain("Add-Content $PROFILE");
489
+ expect(written().some((p) => p.endsWith("systeminfo-startup.bat"))).toBe(true);
490
+ expect(fsMock.chmodSync).not.toHaveBeenCalled();
491
+ });
492
+
493
+ it("reports install errors and exits 1", async () => {
494
+ fsMock.copyFileSync.mockImplementation(() => {
495
+ throw new Error("disk full");
496
+ });
497
+ await run(["--install"]);
498
+ expect(errs.join("\n")).toContain("Error installing shell greeting: disk full");
499
+ expect(exitCode).toBe(1);
500
+ });
501
+ });
502
+
503
+ describe("exports", () => {
504
+ it("exposes displaySystemInfo and installShellGreeting", async () => {
505
+ const mod = await run(["--settings-show"]);
506
+ expect(typeof mod.displaySystemInfo).toBe("function");
507
+ expect(typeof mod.installShellGreeting).toBe("function");
508
+ });
509
+ });