about-system 0.0.51 → 0.0.66

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.
@@ -2,7 +2,7 @@ import "os";
2
2
  import "fs";
3
3
  import "https";
4
4
  import "path";
5
- import { g as m, i as r, l as p, s as n } from "./system-info-api-CChFyamn.js";
5
+ import { g as m, i as r, l as p, s as n } from "./system-info-api-C_y-O4VW.js";
6
6
  export {
7
7
  m as getSystemInfo,
8
8
  r as infoFunctions,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "about-system",
3
- "version": "0.0.51",
3
+ "version": "0.0.66",
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",
7
7
  "bin": {
8
- "about-system": "./dist/about-system-cli.js"
8
+ "about-system": "dist/about-system-cli.js"
9
9
  },
10
10
  "type": "module",
11
11
  "exports": {
@@ -35,7 +35,13 @@
35
35
  "show-settings": "npm run build && node dist/about-system-cli.js --settings-show",
36
36
  "reset-settings": "npm run build && node dist/about-system-cli.js --settings-reset",
37
37
  "clear-cache": "npm run build && node dist/about-system-cli.js --refresh",
38
- "help": "npm run build && node dist/about-system-cli.js --help"
38
+ "help": "npm run build && node dist/about-system-cli.js --help",
39
+ "app:dev": "cd native && npm run dev",
40
+ "app:build": "cd native && npm run build:desktop",
41
+ "test": "vitest run",
42
+ "test:ci": "vitest run --reporter=junit --outputFile=./junit.xml --coverage --coverage.reporter=lcov",
43
+ "test:watch": "vitest",
44
+ "coverage": "vitest run --coverage"
39
45
  },
40
46
  "keywords": [
41
47
  "src/about-system",
@@ -50,13 +56,16 @@
50
56
  "shell",
51
57
  "greeting",
52
58
  "emoji",
53
- "system-monitoring"
59
+ "system-monitoring",
60
+ "desktop-app",
61
+ "tauri"
54
62
  ],
55
63
  "author": "vtempest",
56
64
  "license": "rights.institute/prosper",
57
65
  "repository": {
58
66
  "type": "git",
59
- "url": "https://github.com/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/about-system-info"
67
+ "url": "git+https://github.com/OpenSourceAGI/dev-tools-starter-agent.git",
68
+ "directory": "packages/about-system-info"
60
69
  },
61
70
  "homepage": "https://github.com/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/about-system-info",
62
71
  "files": [
@@ -81,8 +90,10 @@
81
90
  },
82
91
  "devDependencies": {
83
92
  "@types/node": "^25.0.3",
93
+ "@vitest/coverage-v8": "^4.1.0",
84
94
  "typescript": "^5.9.3",
85
95
  "vite": "^7.3.1",
86
- "vite-plugin-dts": "^4.5.4"
96
+ "vite-plugin-dts": "^4.5.4",
97
+ "vitest": "^4.1.0"
87
98
  }
88
99
  }
@@ -12,6 +12,7 @@ import {
12
12
  Settings,
13
13
  DEFAULT_SETTINGS,
14
14
  colors,
15
+ backgrounds,
15
16
  SETTINGS_FILE,
16
17
  CACHE_FILE,
17
18
  loadSettings,
@@ -21,11 +22,25 @@ import {
21
22
  // Platform detection
22
23
  const IS_WINDOWS = os.platform() === "win32";
23
24
 
25
+ // Foreground code -> [background code, contrasting text code] for the
26
+ // legacy 8-color multicolor rotation (ports/pacman rainbow mode).
27
+ const MULTICOLOR_BG: Record<number, [number, number]> = {
28
+ 31: [41, 97], // red
29
+ 32: [42, 30], // green
30
+ 33: [43, 30], // yellow
31
+ 34: [44, 97], // blue
32
+ 35: [45, 97], // magenta
33
+ 36: [46, 30], // cyan
34
+ };
35
+
24
36
  function formatValue(key: string, value: string, settings: Settings): string {
25
37
  if (!value || value.trim() === "") return "";
26
38
 
27
- const color =
28
- colors[settings.colors[key] as keyof typeof colors] || colors.reset;
39
+ const showBackgrounds = settings.display.show_backgrounds !== false;
40
+ const colorName = settings.colors[key] as keyof typeof colors;
41
+ const bg = backgrounds[colorName as keyof typeof backgrounds];
42
+ const style = (showBackgrounds && bg) || colors[colorName] || colors.reset;
43
+ const suffix = showBackgrounds ? colors.reset : "";
29
44
  const emoji = settings.display.show_emojis ? settings.emojis[key] || "" : "";
30
45
 
31
46
  // Special handling for battery emoji
@@ -33,7 +48,7 @@ function formatValue(key: string, value: string, settings: Settings): string {
33
48
  const batteryEmoji = value.includes("+")
34
49
  ? settings.emojis.battery_charging
35
50
  : settings.emojis.battery;
36
- return `${color}${batteryEmoji}${value}`;
51
+ return `${style}${batteryEmoji}${value}${suffix}`;
37
52
  }
38
53
 
39
54
  // Multicolor handling for ports
@@ -44,7 +59,12 @@ function formatValue(key: string, value: string, settings: Settings): string {
44
59
  const colorCodes = [31, 32, 33, 34, 35, 36];
45
60
  ports.forEach((port, index) => {
46
61
  const colorCode = colorCodes[index % colorCodes.length];
47
- output += `\x1b[${colorCode}m${port}\x1b[0m `;
62
+ if (showBackgrounds) {
63
+ const [bgCode, textCode] = MULTICOLOR_BG[colorCode];
64
+ output += `\x1b[${bgCode}m\x1b[${textCode}m${port}\x1b[0m `;
65
+ } else {
66
+ output += `\x1b[${colorCode}m${port}\x1b[0m `;
67
+ }
48
68
  });
49
69
  return output.trim();
50
70
  }
@@ -52,10 +72,10 @@ function formatValue(key: string, value: string, settings: Settings): string {
52
72
  // Multicolor handling for pacman
53
73
  if (key === "pacman" && settings.colors[key] === "multicolor" && value) {
54
74
  const emoji = settings.display.show_emojis ? settings.emojis.pacman : "";
55
- return `${color}${emoji}${value}`;
75
+ return `${style}${emoji}${value}${suffix}`;
56
76
  }
57
77
 
58
- return `${color}${emoji}${value}`;
78
+ return `${style}${emoji}${value}${suffix}`;
59
79
  }
60
80
 
61
81
  /**
@@ -230,6 +250,10 @@ function handleSettingsCommand(args: string[]): boolean {
230
250
  const parsedValue =
231
251
  value.startsWith("{") || value.startsWith("[")
232
252
  ? JSON.parse(value)
253
+ : value === "true"
254
+ ? true
255
+ : value === "false"
256
+ ? false
233
257
  : value;
234
258
 
235
259
  const keys = key.split(".");
@@ -400,6 +424,7 @@ Examples:
400
424
  about-system disk_used # Show only disk usage
401
425
  about-system --install
402
426
  about-system --set display.show_emojis false
427
+ about-system --set display.show_backgrounds false
403
428
  about-system --set colors.user blue
404
429
  about-system --set emojis.cpu "🚀 "
405
430
  about-system --set labels.cpu "Processor"
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @fileoverview Covers the on-disk cache that keeps `about-system` fast: most
3
+ * of the facts it reports are expensive to collect (shelling out to `wmic`,
4
+ * `sw_vers`, `lspci`) and change rarely, so a stale-but-valid entry is the
5
+ * difference between an instant readout and a multi-second one.
6
+ *
7
+ * The behaviours worth pinning are the ones that decide whether a user sees
8
+ * anything at all: a corrupt cache file must not throw, an unwritable temp
9
+ * directory must not throw, and an expired entry must never be served.
10
+ */
11
+
12
+ import fs from "fs";
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
+
15
+ import {
16
+ type Cache,
17
+ getCachedValue,
18
+ isCacheValid,
19
+ loadCache,
20
+ saveCache,
21
+ setCachedValue,
22
+ } from "./cache";
23
+ import { CACHE_DURATION, CACHE_FILE } from "./cache-config";
24
+
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ describe("loadCache", () => {
30
+ it("returns an empty cache when no file exists yet", () => {
31
+ vi.spyOn(fs, "existsSync").mockReturnValue(false);
32
+ expect(loadCache()).toEqual({});
33
+ });
34
+
35
+ it("reads a previously saved cache back", () => {
36
+ const stored: Cache = { cpu: { value: "M2 Pro", timestamp: 123 } };
37
+ vi.spyOn(fs, "existsSync").mockReturnValue(true);
38
+ vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(stored));
39
+
40
+ expect(loadCache()).toEqual(stored);
41
+ });
42
+
43
+ it("reads from the temp-directory cache file", () => {
44
+ vi.spyOn(fs, "existsSync").mockReturnValue(true);
45
+ const read = vi.spyOn(fs, "readFileSync").mockReturnValue("{}");
46
+
47
+ loadCache();
48
+
49
+ expect(read).toHaveBeenCalledWith(CACHE_FILE, "utf8");
50
+ });
51
+
52
+ it("recovers from a corrupt cache file rather than throwing", () => {
53
+ vi.spyOn(fs, "existsSync").mockReturnValue(true);
54
+ vi.spyOn(fs, "readFileSync").mockReturnValue("{ not json");
55
+
56
+ expect(loadCache()).toEqual({});
57
+ });
58
+
59
+ it("recovers from an unreadable cache file", () => {
60
+ vi.spyOn(fs, "existsSync").mockReturnValue(true);
61
+ vi.spyOn(fs, "readFileSync").mockImplementation(() => {
62
+ throw new Error("EACCES");
63
+ });
64
+
65
+ expect(loadCache()).toEqual({});
66
+ });
67
+ });
68
+
69
+ describe("saveCache", () => {
70
+ it("writes the cache as readable json", () => {
71
+ const write = vi.spyOn(fs, "writeFileSync").mockImplementation(() => {});
72
+ const cache: Cache = { cpu: { value: "M2 Pro", timestamp: 1 } };
73
+
74
+ saveCache(cache);
75
+
76
+ expect(write).toHaveBeenCalledWith(
77
+ CACHE_FILE,
78
+ JSON.stringify(cache, null, 2),
79
+ );
80
+ });
81
+
82
+ it("stays silent when the cache file cannot be written", () => {
83
+ vi.spyOn(fs, "writeFileSync").mockImplementation(() => {
84
+ throw new Error("EROFS");
85
+ });
86
+
87
+ expect(() => saveCache({})).not.toThrow();
88
+ });
89
+ });
90
+
91
+ describe("isCacheValid", () => {
92
+ beforeEach(() => {
93
+ vi.useFakeTimers();
94
+ vi.setSystemTime(1_000_000);
95
+ });
96
+
97
+ afterEach(() => {
98
+ vi.useRealTimers();
99
+ });
100
+
101
+ it("accepts an entry written just now", () => {
102
+ expect(isCacheValid({ value: "x", timestamp: Date.now() }, "cpu")).toBe(true);
103
+ });
104
+
105
+ it("rejects an entry older than its key's duration", () => {
106
+ const timestamp = Date.now() - CACHE_DURATION.ram_used - 1;
107
+ expect(isCacheValid({ value: "x", timestamp }, "ram_used")).toBe(false);
108
+ });
109
+
110
+ it("uses each key's own duration, not one global one", () => {
111
+ // A minute-old entry is stale for RAM (10s) but fresh for the CPU (24h).
112
+ const timestamp = Date.now() - 60_000;
113
+ expect(isCacheValid({ value: "x", timestamp }, "ram_used")).toBe(false);
114
+ expect(isCacheValid({ value: "x", timestamp }, "cpu")).toBe(true);
115
+ });
116
+
117
+ it("falls back to a one-minute window for an unknown key", () => {
118
+ expect(
119
+ isCacheValid({ value: "x", timestamp: Date.now() - 59_000 }, "unknown"),
120
+ ).toBe(true);
121
+ expect(
122
+ isCacheValid({ value: "x", timestamp: Date.now() - 61_000 }, "unknown"),
123
+ ).toBe(false);
124
+ });
125
+
126
+ it.each([
127
+ [undefined, "missing"],
128
+ [null, "null"],
129
+ [{ value: "x" }, "timestamp-less"],
130
+ [{ value: "x", timestamp: 0 }, "zero-timestamped"],
131
+ ])("rejects a %s entry", (entry) => {
132
+ expect(isCacheValid(entry as never, "cpu")).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe("getCachedValue", () => {
137
+ beforeEach(() => {
138
+ vi.useFakeTimers();
139
+ vi.setSystemTime(1_000_000);
140
+ });
141
+
142
+ afterEach(() => {
143
+ vi.useRealTimers();
144
+ });
145
+
146
+ it("returns null for a key that was never cached", () => {
147
+ expect(getCachedValue({}, "cpu")).toBeNull();
148
+ });
149
+
150
+ it("returns a fresh value", () => {
151
+ const cache: Cache = { cpu: { value: "M2 Pro", timestamp: Date.now() } };
152
+ expect(getCachedValue(cache, "cpu")).toBe("M2 Pro");
153
+ });
154
+
155
+ it("returns a cached falsy value rather than mistaking it for a miss", () => {
156
+ const cache: Cache = { battery: { value: 0, timestamp: Date.now() } };
157
+ expect(getCachedValue(cache, "battery")).toBe(0);
158
+ });
159
+
160
+ it("returns null for an expired value", () => {
161
+ const cache: Cache = {
162
+ ram_used: { value: "8 GB", timestamp: Date.now() - 60_000 },
163
+ };
164
+ expect(getCachedValue(cache, "ram_used")).toBeNull();
165
+ });
166
+
167
+ it("evicts an expired entry so it is not re-checked", () => {
168
+ const cache: Cache = {
169
+ ram_used: { value: "8 GB", timestamp: Date.now() - 60_000 },
170
+ };
171
+
172
+ getCachedValue(cache, "ram_used");
173
+
174
+ expect(cache).not.toHaveProperty("ram_used");
175
+ });
176
+
177
+ it("keeps a fresh entry in place", () => {
178
+ const cache: Cache = { cpu: { value: "M2 Pro", timestamp: Date.now() } };
179
+ getCachedValue(cache, "cpu");
180
+ expect(cache).toHaveProperty("cpu");
181
+ });
182
+ });
183
+
184
+ describe("setCachedValue", () => {
185
+ beforeEach(() => {
186
+ vi.useFakeTimers();
187
+ vi.setSystemTime(1_000_000);
188
+ });
189
+
190
+ afterEach(() => {
191
+ vi.useRealTimers();
192
+ });
193
+
194
+ it("stamps the value with the time it was cached", () => {
195
+ const cache: Cache = {};
196
+ setCachedValue(cache, "cpu", "M2 Pro");
197
+
198
+ expect(cache.cpu).toEqual({ value: "M2 Pro", timestamp: 1_000_000 });
199
+ });
200
+
201
+ it("round-trips through getCachedValue", () => {
202
+ const cache: Cache = {};
203
+ setCachedValue(cache, "cpu", "M2 Pro");
204
+ expect(getCachedValue(cache, "cpu")).toBe("M2 Pro");
205
+ });
206
+
207
+ it("replaces an existing entry and refreshes its timestamp", () => {
208
+ const cache: Cache = { cpu: { value: "old", timestamp: 1 } };
209
+ setCachedValue(cache, "cpu", "new");
210
+
211
+ expect(cache.cpu).toEqual({ value: "new", timestamp: 1_000_000 });
212
+ });
213
+
214
+ it("stores structured values, not just strings", () => {
215
+ const cache: Cache = {};
216
+ setCachedValue(cache, "network_interfaces", [{ name: "en0" }]);
217
+
218
+ expect(getCachedValue(cache, "network_interfaces")).toEqual([
219
+ { name: "en0" },
220
+ ]);
221
+ });
222
+ });
223
+
224
+ describe("CACHE_DURATION", () => {
225
+ it("gives every cached key a positive window", () => {
226
+ for (const [key, duration] of Object.entries(CACHE_DURATION)) {
227
+ expect(duration, key).toBeGreaterThan(0);
228
+ }
229
+ });
230
+
231
+ it("caches volatile readings for less time than fixed hardware facts", () => {
232
+ expect(CACHE_DURATION.ram_used).toBeLessThan(CACHE_DURATION.cpu);
233
+ expect(CACHE_DURATION.top_process).toBeLessThan(CACHE_DURATION.os);
234
+ });
235
+ });
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @fileoverview Covers the platform facts `about-system` opens its readout
3
+ * with: user, hostname, OS name, kernel and device model.
4
+ *
5
+ * Each of these shells out to a different tool per platform and parses its
6
+ * output, so the tests drive the Linux path (the one CI runs on) end to end and
7
+ * pin the two behaviours that hold everywhere: the cache is consulted before
8
+ * any command runs, and a missing tool degrades to a fallback rather than
9
+ * throwing.
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+
14
+ // These modules bind their imports by name, so the dependencies have to be
15
+ // mocked outright rather than spied on through a namespace object.
16
+ const execCommand = vi.hoisted(() => vi.fn());
17
+ const commandExists = vi.hoisted(() => vi.fn());
18
+ vi.mock("../utils/command", () => ({ execCommand, commandExists }));
19
+
20
+ const osMock = vi.hoisted(() => ({
21
+ platform: vi.fn(() => "linux"),
22
+ release: vi.fn(() => "6.1.0-generic"),
23
+ hostname: vi.fn(() => "workstation"),
24
+ userInfo: vi.fn(() => ({ username: "ada" })),
25
+ // cache-config resolves the cache file under the temp directory at import time.
26
+ tmpdir: vi.fn(() => "/tmp"),
27
+ }));
28
+ vi.mock("os", () => ({ ...osMock, default: osMock }));
29
+
30
+ const fsMock = vi.hoisted(() => ({
31
+ existsSync: vi.fn(() => false),
32
+ readFileSync: vi.fn(() => ""),
33
+ }));
34
+ vi.mock("fs", () => ({ ...fsMock, default: fsMock }));
35
+
36
+ const { device, hostname, kernel, os_info, user } = await import("./platform");
37
+ const { IS_LINUX } = await import("../utils/platform");
38
+ import type { InfoContext } from "../types/internal-types";
39
+
40
+ const context = (): InfoContext => ({ cache: {} });
41
+
42
+ beforeEach(() => {
43
+ execCommand.mockReturnValue("");
44
+ commandExists.mockReturnValue(false);
45
+ fsMock.existsSync.mockReturnValue(false);
46
+ fsMock.readFileSync.mockReturnValue("");
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.clearAllMocks();
51
+ });
52
+
53
+ describe("user and hostname", () => {
54
+ it("reads the current username", () => {
55
+ expect(user()).toBe("ada");
56
+ });
57
+
58
+ it("reads the machine's network name", () => {
59
+ expect(hostname()).toBe("workstation");
60
+ });
61
+ });
62
+
63
+ describe("kernel", () => {
64
+ it("reports the kernel release", () => {
65
+ expect(kernel(context())).toBe("6.1.0-generic");
66
+ });
67
+
68
+ it("caches the result", () => {
69
+ const ctx = context();
70
+ kernel(ctx);
71
+ expect(ctx.cache.kernel?.value).toBe("6.1.0-generic");
72
+ });
73
+
74
+ it("serves a cached kernel without asking the OS again", () => {
75
+ const ctx = context();
76
+ ctx.cache.kernel = { value: "cached-kernel", timestamp: Date.now() };
77
+
78
+ expect(kernel(ctx)).toBe("cached-kernel");
79
+ expect(osMock.release).not.toHaveBeenCalled();
80
+ });
81
+ });
82
+
83
+ describe("os_info", () => {
84
+ it("serves a cached value without running any command", () => {
85
+ const ctx = context();
86
+ ctx.cache.os = { value: "Cached OS 1.0", timestamp: Date.now() };
87
+
88
+ expect(os_info(ctx)).toBe("Cached OS 1.0");
89
+ expect(execCommand).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it("caches whatever it resolves", () => {
93
+ const ctx = context();
94
+ const resolved = os_info(ctx);
95
+ expect(ctx.cache.os?.value).toBe(resolved);
96
+ });
97
+
98
+ it("never returns an empty string, whatever the platform", () => {
99
+ expect(os_info(context())).toBeTruthy();
100
+ });
101
+
102
+ it.runIf(IS_LINUX)("reads the distribution from /etc/os-release", () => {
103
+ fsMock.existsSync.mockReturnValue(true);
104
+ fsMock.readFileSync.mockReturnValue(
105
+ 'NAME="Ubuntu"\nVERSION_ID="22.04"\nID=ubuntu\n',
106
+ );
107
+
108
+ expect(os_info(context())).toBe("Ubuntu 22.04");
109
+ });
110
+
111
+ it.runIf(IS_LINUX)("uses the name alone when there is no version id", () => {
112
+ fsMock.readFileSync.mockReturnValue('NAME="Arch Linux"\n');
113
+ expect(os_info(context())).toBe("Arch Linux");
114
+ });
115
+
116
+ it.runIf(IS_LINUX)("falls back to the kernel release with no os-release file", () => {
117
+ fsMock.readFileSync.mockImplementation(() => {
118
+ throw new Error("ENOENT");
119
+ });
120
+
121
+ expect(os_info(context())).toBe("Linux 6.1.0-generic");
122
+ });
123
+
124
+ it.runIf(IS_LINUX)("falls back to a bare Linux when the file has no NAME", () => {
125
+ fsMock.readFileSync.mockReturnValue("ID=unknown\n");
126
+ expect(os_info(context())).toBe("Linux");
127
+ });
128
+ });
129
+
130
+ describe("device", () => {
131
+ it("serves a cached model without probing", () => {
132
+ const ctx = context();
133
+ ctx.cache.device = { value: "Cached Box", timestamp: Date.now() };
134
+
135
+ expect(device(ctx)).toBe("Cached Box");
136
+ expect(execCommand).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("caches an empty result too, so the probe is not repeated", () => {
140
+ const ctx = context();
141
+ device(ctx);
142
+ expect(ctx.cache.device).toBeDefined();
143
+ });
144
+
145
+ it("returns an empty string when nothing identifies the machine", () => {
146
+ expect(device(context())).toBe("");
147
+ });
148
+
149
+ it.runIf(IS_LINUX)("reads the model from the DMI product name", () => {
150
+ fsMock.existsSync.mockImplementation(
151
+ (path: string) => path === "/sys/devices/virtual/dmi/id/product_name",
152
+ );
153
+ fsMock.readFileSync.mockReturnValue("OptiPlex 7090\n");
154
+
155
+ expect(device(context())).toBe("OptiPlex 7090");
156
+ });
157
+
158
+ it.runIf(IS_LINUX)("prefers Android's getprop when it is available", () => {
159
+ commandExists.mockImplementation((cmd: string) => cmd === "getprop");
160
+ execCommand.mockReturnValue("Pixel 8");
161
+
162
+ expect(device(context())).toBe("Pixel 8");
163
+ expect(execCommand).toHaveBeenCalledWith("getprop ro.product.model");
164
+ });
165
+
166
+ it.runIf(IS_LINUX)("falls through to DMI when getprop answers nothing", () => {
167
+ commandExists.mockImplementation((cmd: string) => cmd === "getprop");
168
+ execCommand.mockReturnValue("");
169
+ fsMock.existsSync.mockReturnValue(true);
170
+ fsMock.readFileSync.mockReturnValue("Steam Deck\n");
171
+
172
+ expect(device(context())).toBe("Steam Deck");
173
+ });
174
+
175
+ it.runIf(IS_LINUX)("ignores a blank DMI product name", () => {
176
+ fsMock.existsSync.mockReturnValue(true);
177
+ fsMock.readFileSync.mockReturnValue(" \n");
178
+
179
+ expect(device(context())).toBe("");
180
+ });
181
+
182
+ it.runIf(IS_LINUX)("survives an unreadable DMI file", () => {
183
+ fsMock.existsSync.mockReturnValue(true);
184
+ fsMock.readFileSync.mockImplementation(() => {
185
+ throw new Error("EACCES");
186
+ });
187
+
188
+ expect(device(context())).toBe("");
189
+ });
190
+ });
@@ -17,18 +17,43 @@ export const SETTINGS_FILE = path.join(
17
17
  export const CACHE_FILE = path.join(os.tmpdir(), "systeminfo-cache.json");
18
18
 
19
19
  // Color codes
20
+ //
21
+ // These are deliberately darker/more saturated than a typical terminal
22
+ // palette. The original bright variants (e.g. yellow 226, cyan 51, gray 250)
23
+ // have very high luminance and are nearly unreadable on a light/white
24
+ // terminal background. `lightblue` was also mislabeled: it pointed at gold
25
+ // (220), not a blue at all. Every value below reads clearly on both light
26
+ // and dark backgrounds.
20
27
  export const colors = {
21
28
  reset: "\x1b[0m",
22
- red: "\x1b[38;5;196m",
23
- orange: "\x1b[38;5;208m",
24
- yellow: "\x1b[38;5;226m",
25
- green: "\x1b[38;5;46m",
26
- blue: "\x1b[38;5;39m",
27
- cyan: "\x1b[38;5;51m",
28
- purple: "\x1b[38;5;171m",
29
- magenta: "\x1b[38;5;213m",
30
- gray: "\x1b[38;5;250m",
31
- lightblue: "\x1b[38;5;220m",
29
+ red: "\x1b[38;5;160m",
30
+ orange: "\x1b[38;5;130m",
31
+ yellow: "\x1b[38;5;58m",
32
+ green: "\x1b[38;5;22m",
33
+ blue: "\x1b[38;5;25m",
34
+ cyan: "\x1b[38;5;23m",
35
+ purple: "\x1b[38;5;91m",
36
+ magenta: "\x1b[38;5;125m",
37
+ gray: "\x1b[38;5;238m",
38
+ lightblue: "\x1b[38;5;24m",
39
+ };
40
+
41
+ // Background variants, keyed the same as `colors`, used when
42
+ // `display.show_backgrounds` is enabled. Each entry sets its own background
43
+ // block plus a foreground text color chosen for contrast against it, so
44
+ // every item is legible on its own regardless of what the terminal's
45
+ // background actually is.
46
+ export const backgrounds = {
47
+ red: "\x1b[48;5;88m\x1b[38;5;255m",
48
+ orange: "\x1b[48;5;166m\x1b[38;5;232m",
49
+ yellow: "\x1b[48;5;178m\x1b[38;5;232m",
50
+ green: "\x1b[48;5;22m\x1b[38;5;255m",
51
+ blue: "\x1b[48;5;18m\x1b[38;5;255m",
52
+ cyan: "\x1b[48;5;23m\x1b[38;5;255m",
53
+ purple: "\x1b[48;5;54m\x1b[38;5;255m",
54
+ magenta: "\x1b[48;5;53m\x1b[38;5;255m",
55
+ gray: "\x1b[48;5;236m\x1b[38;5;255m",
56
+ lightblue: "\x1b[48;5;26m\x1b[38;5;255m",
32
57
  };
33
58
 
34
59
  // Default settings
@@ -154,6 +179,7 @@ export const DEFAULT_SETTINGS = {
154
179
  },
155
180
  display: {
156
181
  show_emojis: true,
182
+ show_backgrounds: true,
157
183
  single_line: true,
158
184
  line_wrap_length: process?.stdout?.columns || 100,
159
185
  },
@@ -172,6 +198,8 @@ export interface Settings {
172
198
  labels: Record<string, string>;
173
199
  display: {
174
200
  show_emojis: boolean;
201
+ /** true (default): give every item its own background color block, so it stays legible regardless of the terminal's background */
202
+ show_backgrounds: boolean;
175
203
  /** true: print one continuous line and let the terminal soft-wrap it; false: hard-wrap at line_wrap_length */
176
204
  single_line: boolean;
177
205
  line_wrap_length: number;