opencode-system-metrics-tui 0.1.6 → 0.1.8

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.
package/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  ## Install globally in OpenCode
4
4
 
5
5
  ```powershell
6
- opencode plugin -g opencode-system-metrics-tui@0.1.6 --force
6
+ opencode plugin -g opencode-system-metrics-tui@0.1.8 --force
7
7
  ```
8
8
 
9
- This is the primary installation path. The version must match `package.json`; `--force` asks OpenCode to refresh its package cache. Verify the actual installed artifact afterward:
9
+ This is the primary installation path. The version must match `package.json`; `--force` asks OpenCode to refresh its package cache. The helper below skips OpenCode when the expected artifact is already current. Verify the actual installed artifact afterward:
10
10
 
11
11
  ```powershell
12
12
  npm run install-plugin
@@ -31,12 +31,14 @@ The README is available in **Español**, **English**, and **Português**.
31
31
  - RAM: memoria usada, total y porcentaje.
32
32
  - GPU: utilización y temperatura.
33
33
  - GPU VRAM: memoria usada, total y porcentaje.
34
+ - En Apple Silicon, la fila se muestra como **Unified Memory**; no se presenta la memoria total del sistema como VRAM usada porque no existe una fuente fiable de uso dinámico.
34
35
  - Red: velocidad de descarga y subida en tiempo real.
35
36
  - CPU, RAM y red se consultan con una cadencia general de 2 segundos; GPU, VRAM y temperaturas usan una caché expirable de 10 segundos para reducir el coste sin congelar los sensores.
36
37
  - Colores compatibles con el tema activo de OpenCode.
37
38
  - Iconos Unicode por defecto en Windows (la detección de fuentes instaladas no cambia la selección); Linux/macOS usan Nerd Font solo si está instalada y mantienen fallback Unicode.
38
39
  - En Windows usa iconos Unicode visibles de forma predeterminada, porque OpenCode no puede detectar la fuente activa de la terminal.
39
40
  - Soporte para Linux, macOS y Windows cuando el sistema expone las métricas.
41
+ - En macOS, la utilización de GPU usa `ioreg` y las temperaturas usan el helper de Stats si está instalado; si cualquiera de ellos falta, esa métrica queda como no disponible sin interrumpir el plugin. En Intel se conserva el fallback de GPU discreta de `systeminformation`.
40
42
  - En Windows, la temperatura de CPU requiere LibreHardwareMonitor ejecutándose como administrador con **Remote Web Server** activo en `http://127.0.0.1:8085`.
41
43
  - En Windows, la velocidad de red usa PowerShell y `Get-NetAdapterStatistics`; no agrega una dependencia npm.
42
44
  - Las lecturas opcionales de Windows se hacen bajo demanda, con timeout y caché; no hay timers globales de PowerShell.
@@ -91,7 +93,7 @@ npm publish --access public
91
93
  En otro equipo:
92
94
 
93
95
  ```bash
94
- opencode plugin opencode-system-metrics-tui@0.1.6
96
+ opencode plugin opencode-system-metrics-tui@0.1.8
95
97
  ```
96
98
 
97
99
  El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
@@ -101,8 +103,8 @@ El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia Ope
101
103
  Publicar en npm e instalar en OpenCode son pasos distintos:
102
104
 
103
105
  - `npm publish --access public` publica el paquete en npm.
104
- - `opencode plugin opencode-system-metrics-tui@0.1.6` lo instala solo en el proyecto actual.
105
- - `opencode plugin -g opencode-system-metrics-tui@0.1.6 --force` lo instala globalmente para todos los proyectos.
106
+ - `opencode plugin opencode-system-metrics-tui@0.1.8` lo instala solo en el proyecto actual.
107
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.8 --force` lo instala globalmente para todos los proyectos.
106
108
 
107
109
  ### Verificación
108
110
 
@@ -124,12 +126,14 @@ npm run build
124
126
  - RAM: used memory, total memory, and percentage.
125
127
  - GPU: utilization and temperature.
126
128
  - GPU VRAM: used memory, total memory, and percentage.
129
+ - On Apple Silicon, the row is labeled **Unified Memory**; total system memory is never presented as live VRAM usage because no reliable dynamic source is available.
127
130
  - Network: live download and upload speeds.
128
131
  - CPU, RAM, and network use the 2-second general cadence; GPU, VRAM, and temperatures use an expiring 10-second cache to reduce cost without freezing sensors.
129
132
  - Uses the active OpenCode theme colors.
130
133
  - Windows uses Unicode icons by default (installed-font detection does not change selection); Linux/macOS use Nerd Font only when installed and retain Unicode fallback.
131
134
  - On Windows, visible Unicode icons are used by default because OpenCode cannot detect the terminal's active font.
132
135
  - Supports Linux, macOS, and Windows when the operating system exposes the metrics.
136
+ - On macOS, GPU utilization uses `ioreg` and temperatures use the Stats helper when installed; missing helpers leave only that metric unavailable without interrupting the plugin. Intel retains the discrete-GPU `systeminformation` fallback.
133
137
  - On Windows, CPU temperature requires LibreHardwareMonitor running as administrator with **Remote Web Server** enabled at `http://127.0.0.1:8085`.
134
138
  - On Windows, network speed uses PowerShell and `Get-NetAdapterStatistics`; it adds no npm dependency.
135
139
  - Optional Windows reads are on demand, timeout-bounded, and cached; no global PowerShell timers are used.
@@ -184,7 +188,7 @@ npm publish --access public
184
188
  On another machine:
185
189
 
186
190
  ```bash
187
- opencode plugin opencode-system-metrics-tui@0.1.6
191
+ opencode plugin opencode-system-metrics-tui@0.1.8
188
192
  ```
189
193
 
190
194
  The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
@@ -194,8 +198,8 @@ The OpenCode installer configures the TUI plugin automatically. Restart OpenCode
194
198
  Publishing to npm and installing in OpenCode are separate steps:
195
199
 
196
200
  - `npm publish --access public` publishes the package to npm.
197
- - `opencode plugin opencode-system-metrics-tui@0.1.6` installs it for the current project only.
198
- - `opencode plugin -g opencode-system-metrics-tui@0.1.6 --force` installs it globally for all projects.
201
+ - `opencode plugin opencode-system-metrics-tui@0.1.8` installs it for the current project only.
202
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.8 --force` installs it globally for all projects.
199
203
 
200
204
  ### Verification
201
205
 
@@ -277,7 +281,7 @@ npm publish --access public
277
281
  Em outro computador:
278
282
 
279
283
  ```bash
280
- opencode plugin opencode-system-metrics-tui@0.1.6
284
+ opencode plugin opencode-system-metrics-tui@0.1.8
281
285
  ```
282
286
 
283
287
  O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
@@ -287,8 +291,8 @@ O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o Open
287
291
  Publicar no npm e instalar no OpenCode são etapas diferentes:
288
292
 
289
293
  - `npm publish --access public` publica o pacote no npm.
290
- - `opencode plugin opencode-system-metrics-tui@0.1.6` instala o plugin apenas no projeto atual.
291
- - `opencode plugin -g opencode-system-metrics-tui@0.1.6 --force` instala o plugin globalmente para todos os projetos.
294
+ - `opencode plugin opencode-system-metrics-tui@0.1.8` instala o plugin apenas no projeto atual.
295
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.8 --force` instala o plugin globalmente para todos os projetos.
292
296
 
293
297
  ### Verificação
294
298
 
@@ -8,19 +8,19 @@ import { promisify } from "node:util";
8
8
  // src/font.ts
9
9
  import { existsSync, readdirSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
- import { join as posixJoin, win32 } from "node:path";
11
+ import { posix, win32 } from "node:path";
12
12
  var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
13
13
  var MAX_FONT_SCAN_DEPTH = 2;
14
14
  var MAX_FONT_SCAN_ENTRIES = 256;
15
15
  function getFontDirectories(platform, home = homedir(), env = process.env) {
16
16
  if (platform === "darwin")
17
- return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
17
+ return [posix.join(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
18
18
  if (platform === "win32") {
19
19
  const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
20
20
  const windows = env.WINDIR ?? "C:\\Windows";
21
21
  return [win32.join(localAppData, "Microsoft", "Windows", "Fonts"), win32.join(windows, "Fonts")];
22
22
  }
23
- return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
23
+ return [posix.join(home, ".local", "share", "fonts"), posix.join(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
24
24
  }
25
25
  function isNerdFontFile(fileName) {
26
26
  return NERD_FONT_FILE_PATTERN.test(fileName);
@@ -36,7 +36,7 @@ function hasNerdFont(platform = process.platform, home = homedir(), env = proces
36
36
  return false;
37
37
  try {
38
38
  return readdirSync(directory, { withFileTypes: true }).slice(0, MAX_FONT_SCAN_ENTRIES).some((entry) => {
39
- const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
39
+ const path = platform === "win32" ? win32.join(directory, entry.name) : posix.join(directory, entry.name);
40
40
  return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path, depth + 1);
41
41
  });
42
42
  } catch {
package/dist/tui.js CHANGED
@@ -40,13 +40,71 @@ import { execFile } from "node:child_process";
40
40
  import { promisify } from "node:util";
41
41
  import si from "systeminformation";
42
42
  var execFileAsync = promisify(execFile);
43
- var WINDOWS_NETWORK_TIMEOUT_MS = 1500;
43
+ var DEFAULT_METRIC_TIMEOUT_MS = 1500;
44
+ var WINDOWS_MEMORY_TIMEOUT_MS = 5000;
45
+ var WINDOWS_NETWORK_PROCESS_TIMEOUT_MS = 4000;
46
+ var WINDOWS_NETWORK_OUTER_TIMEOUT_MS = 4500;
47
+ var MAC_METRIC_TIMEOUT_MS = 1500;
48
+ var MAC_STATS_HELPER = "/Applications/Stats.app/Contents/Resources/smc";
49
+ var runCommand = async (command, args, timeoutMs) => {
50
+ const { stdout } = await execFileAsync(command, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024 });
51
+ return stdout;
52
+ };
53
+ async function detectMacHardwareArchitecture(runner = runCommand, processArchitecture = process.arch) {
54
+ try {
55
+ const output = (await runner("sysctl", ["-in", "hw.optional.arm64"], MAC_METRIC_TIMEOUT_MS)).trim();
56
+ if (output === "1")
57
+ return "arm64";
58
+ if (output === "0")
59
+ return "x86_64";
60
+ } catch {}
61
+ return processArchitecture === "arm64" ? "arm64" : "x86_64";
62
+ }
63
+ function gpuMemoryLabel(architecture, platform = process.platform) {
64
+ if (platform !== "darwin")
65
+ return "GPU VRAM";
66
+ if (architecture === "arm64")
67
+ return "Unified Memory";
68
+ if (architecture === "x86_64")
69
+ return "GPU VRAM";
70
+ return "Memory";
71
+ }
44
72
  function parseMonitorTemperature(value) {
45
73
  if (value === undefined)
46
74
  return;
47
75
  const parsed = Number(value.replace(",", ".").replace(/[^\d.-]/g, ""));
48
76
  return Number.isFinite(parsed) ? parsed : undefined;
49
77
  }
78
+ function parseMacGpuUtilization(stdout) {
79
+ const match = stdout.match(/"Device Utilization %"\s*=\s*([\d]+(?:\.\d+)?)/);
80
+ if (match === null)
81
+ return;
82
+ const value = Number(match[1]);
83
+ return Number.isFinite(value) && value >= 0 && value <= 100 ? value : undefined;
84
+ }
85
+ function parseMacStatsTemperature(stdout, sensorPrefix) {
86
+ let maximum;
87
+ for (const line of stdout.split(/\r?\n/)) {
88
+ const fields = line.trim().split(/\s+/);
89
+ if (fields.length < 2 || !new RegExp(`^\\[${sensorPrefix}`).test(fields[0]))
90
+ continue;
91
+ const value = Number(fields[1].replace(",", ".").replace(/[°cC]/g, ""));
92
+ if (Number.isFinite(value) && value > 0 && (maximum === undefined || value > maximum))
93
+ maximum = value;
94
+ }
95
+ return maximum;
96
+ }
97
+ async function readMacMetrics(runner = runCommand) {
98
+ const [gpuOutput, temperatureOutput] = await Promise.all([
99
+ runner("ioreg", ["-r", "-d", "1", "-c", "IOAccelerator"], MAC_METRIC_TIMEOUT_MS).catch(() => ""),
100
+ runner(MAC_STATS_HELPER, ["list", "-t"], MAC_METRIC_TIMEOUT_MS).catch(() => "")
101
+ ]);
102
+ return {
103
+ gpuPercent: parseMacGpuUtilization(gpuOutput) ?? null,
104
+ cpuTemperatureCelsius: parseMacStatsTemperature(temperatureOutput, "Tp") ?? null,
105
+ gpuTemperatureCelsius: parseMacStatsTemperature(temperatureOutput, "Tg") ?? null
106
+ };
107
+ }
50
108
  async function readLibreHardwareMonitorTemperature() {
51
109
  const response = await fetch("http://127.0.0.1:8085/data.json", { signal: AbortSignal.timeout(2000) });
52
110
  if (!response.ok)
@@ -87,7 +145,7 @@ async function readWindowsNetworkSample() {
87
145
  "-NonInteractive",
88
146
  "-Command",
89
147
  "Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress"
90
- ], { windowsHide: true, timeout: WINDOWS_NETWORK_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
148
+ ], { windowsHide: true, timeout: WINDOWS_NETWORK_PROCESS_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
91
149
  return parseWindowsNetworkOutput(stdout);
92
150
  }
93
151
  var previousNetwork;
@@ -102,6 +160,7 @@ var unavailable = {
102
160
  gpuMemoryUsedBytes: null,
103
161
  gpuMemoryTotalBytes: null,
104
162
  gpuMemoryPercent: null,
163
+ gpuMemoryIsUnified: null,
105
164
  downloadBytesPerSecond: null,
106
165
  uploadBytesPerSecond: null
107
166
  };
@@ -126,7 +185,15 @@ var SLOW_METRIC_CACHE_MS = 1e4;
126
185
  var TEMPERATURE_CACHE_MS = 1e4;
127
186
  var OPTIONAL_SOURCE_BACKOFF_MS = 30000;
128
187
  var isWindows = process.platform === "win32";
188
+ var isMac = process.platform === "darwin";
129
189
  var NETWORK_CACHE_MS = 2000;
190
+ var macHardwareArchitecture;
191
+ function readMacHardwareArchitecture() {
192
+ if (!isMac)
193
+ return Promise.resolve("x86_64");
194
+ macHardwareArchitecture ??= detectMacHardwareArchitecture();
195
+ return macHardwareArchitecture;
196
+ }
130
197
  function selectGpuMetrics(graphics) {
131
198
  const empty = {
132
199
  gpuPercent: null,
@@ -208,6 +275,11 @@ var cachedGraphics = {
208
275
  lastAttemptAt: 0,
209
276
  inFlight: undefined
210
277
  };
278
+ var cachedMacMetrics = {
279
+ value: undefined,
280
+ lastAttemptAt: 0,
281
+ inFlight: undefined
282
+ };
211
283
  var cachedNetwork = {
212
284
  value: undefined,
213
285
  lastAttemptAt: 0,
@@ -249,14 +321,16 @@ async function readWindowsCpuTemperature() {
249
321
  return value === undefined ? undefined : { main: value };
250
322
  }
251
323
  async function readMetrics() {
252
- const [load, memory] = await Promise.allSettled([
253
- withTimeout(si.currentLoad(), 1500),
254
- withTimeout(si.mem(), 1500)
324
+ const [load, memory, hardwareArchitecture] = await Promise.allSettled([
325
+ withTimeout(si.currentLoad(), DEFAULT_METRIC_TIMEOUT_MS),
326
+ withTimeout(si.mem(), isWindows ? WINDOWS_MEMORY_TIMEOUT_MS : DEFAULT_METRIC_TIMEOUT_MS),
327
+ readMacHardwareArchitecture()
255
328
  ]);
256
- const [temperature, graphics, network] = await Promise.allSettled([
329
+ const [temperature, graphics, network, mac] = await Promise.allSettled([
257
330
  readCachedMetric(cachedCpuTemperature, isWindows ? readWindowsCpuTemperature : () => si.cpuTemperature(), TEMPERATURE_CACHE_MS, Date.now(), TEMPERATURE_CACHE_MS, 2500),
258
331
  readCachedMetric(cachedGraphics, () => si.graphics(), SLOW_METRIC_CACHE_MS, Date.now(), SLOW_METRIC_CACHE_MS, 2500),
259
- readCachedMetric(cachedNetwork, isWindows ? readWindowsNetworkMetrics : () => si.networkStats("*"), isWindows ? NETWORK_CACHE_MS : 2000, Date.now(), OPTIONAL_SOURCE_BACKOFF_MS, 1500)
332
+ readCachedMetric(cachedNetwork, isWindows ? readWindowsNetworkMetrics : () => si.networkStats("*"), isWindows ? NETWORK_CACHE_MS : 2000, Date.now(), OPTIONAL_SOURCE_BACKOFF_MS, isWindows ? WINDOWS_NETWORK_OUTER_TIMEOUT_MS : DEFAULT_METRIC_TIMEOUT_MS),
333
+ isMac ? readCachedMetric(cachedMacMetrics, () => readMacMetrics(), SLOW_METRIC_CACHE_MS, Date.now(), OPTIONAL_SOURCE_BACKOFF_MS, MAC_METRIC_TIMEOUT_MS) : Promise.resolve(undefined)
260
334
  ]);
261
335
  const cpuPercent = load.status === "fulfilled" ? finite(load.value.currentLoad) : null;
262
336
  const memoryTotalBytes = memory.status === "fulfilled" ? finite(memory.value.total) : null;
@@ -264,6 +338,14 @@ async function readMetrics() {
264
338
  const memoryUsedBytes = memoryTotalBytes !== null && memoryAvailableBytes !== null ? Math.max(0, memoryTotalBytes - memoryAvailableBytes) : memory.status === "fulfilled" ? finite(memory.value.used) : null;
265
339
  const memoryPercent = memoryUsedBytes !== null && memoryTotalBytes !== null && memoryTotalBytes > 0 ? memoryUsedBytes / memoryTotalBytes * 100 : null;
266
340
  const gpu = selectGpuMetrics(graphics.status === "fulfilled" ? graphics.value : undefined);
341
+ const macMetrics = mac.status === "fulfilled" ? mac.value : undefined;
342
+ const architecture = hardwareArchitecture.status === "fulfilled" ? hardwareArchitecture.value : undefined;
343
+ const isAppleSilicon = isMac && architecture === "arm64";
344
+ if (isAppleSilicon) {
345
+ gpu.gpuMemoryUsedBytes = null;
346
+ gpu.gpuMemoryTotalBytes = null;
347
+ gpu.gpuMemoryPercent = null;
348
+ }
267
349
  let downloadBytesPerSecond = null;
268
350
  let uploadBytesPerSecond = null;
269
351
  if (network.status === "fulfilled" && network.value !== undefined) {
@@ -290,12 +372,13 @@ async function readMetrics() {
290
372
  memoryUsedBytes,
291
373
  memoryTotalBytes,
292
374
  memoryPercent,
293
- gpuPercent: gpu.gpuPercent,
294
- cpuTemperatureCelsius: temperature.status === "fulfilled" && temperature.value !== undefined ? finite(temperature.value.main) ?? ("max" in temperature.value ? finite(temperature.value.max) : null) : null,
295
- gpuTemperatureCelsius: gpu.gpuTemperatureCelsius,
375
+ gpuPercent: macMetrics?.gpuPercent ?? gpu.gpuPercent,
376
+ cpuTemperatureCelsius: macMetrics?.cpuTemperatureCelsius ?? (temperature.status === "fulfilled" && temperature.value !== undefined ? finite(temperature.value.main) ?? ("max" in temperature.value ? finite(temperature.value.max) : null) : null),
377
+ gpuTemperatureCelsius: macMetrics?.gpuTemperatureCelsius ?? gpu.gpuTemperatureCelsius,
296
378
  gpuMemoryUsedBytes: gpu.gpuMemoryUsedBytes,
297
379
  gpuMemoryTotalBytes: gpu.gpuMemoryTotalBytes,
298
380
  gpuMemoryPercent: gpu.gpuMemoryPercent,
381
+ gpuMemoryIsUnified: isAppleSilicon ? true : isMac && architecture === "x86_64" ? false : null,
299
382
  downloadBytesPerSecond,
300
383
  uploadBytesPerSecond
301
384
  };
@@ -304,19 +387,19 @@ async function readMetrics() {
304
387
  // src/font.ts
305
388
  import { existsSync, readdirSync } from "node:fs";
306
389
  import { homedir } from "node:os";
307
- import { join as posixJoin, win32 } from "node:path";
390
+ import { posix, win32 } from "node:path";
308
391
  var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
309
392
  var MAX_FONT_SCAN_DEPTH = 2;
310
393
  var MAX_FONT_SCAN_ENTRIES = 256;
311
394
  function getFontDirectories(platform, home = homedir(), env = process.env) {
312
395
  if (platform === "darwin")
313
- return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
396
+ return [posix.join(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
314
397
  if (platform === "win32") {
315
398
  const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
316
399
  const windows = env.WINDIR ?? "C:\\Windows";
317
400
  return [win32.join(localAppData, "Microsoft", "Windows", "Fonts"), win32.join(windows, "Fonts")];
318
401
  }
319
- return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
402
+ return [posix.join(home, ".local", "share", "fonts"), posix.join(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
320
403
  }
321
404
  function isNerdFontFile(fileName) {
322
405
  return NERD_FONT_FILE_PATTERN.test(fileName);
@@ -332,7 +415,7 @@ function hasNerdFont(platform = process.platform, home = homedir(), env = proces
332
415
  return false;
333
416
  try {
334
417
  return readdirSync(directory, { withFileTypes: true }).slice(0, MAX_FONT_SCAN_ENTRIES).some((entry) => {
335
- const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
418
+ const path = platform === "win32" ? win32.join(directory, entry.name) : posix.join(directory, entry.name);
336
419
  return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path, depth + 1);
337
420
  });
338
421
  } catch {
@@ -376,6 +459,7 @@ var initialMetrics = {
376
459
  gpuMemoryUsedBytes: null,
377
460
  gpuMemoryTotalBytes: null,
378
461
  gpuMemoryPercent: null,
462
+ gpuMemoryIsUnified: null,
379
463
  downloadBytesPerSecond: null,
380
464
  uploadBytesPerSecond: null
381
465
  };
@@ -432,13 +516,13 @@ function MetricsPanel(props) {
432
516
  onMount(startMetricsPolling);
433
517
  onCleanup(stopMetricsPolling);
434
518
  return (() => {
435
- var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` · `), _el$0 = _$createTextNode(` `), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createTextNode(` / `), _el$15 = _$createTextNode(` (`), _el$16 = _$createTextNode(`)`), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` · `), _el$23 = _$createTextNode(` `), _el$24 = _$createElement("box"), _el$25 = _$createElement("text"), _el$26 = _$createElement("text"), _el$28 = _$createElement("text"), _el$29 = _$createTextNode(` / `), _el$30 = _$createTextNode(` (`), _el$31 = _$createTextNode(`)`), _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createTextNode(`↓ `), _el$38 = _$createTextNode(` ↑ `);
519
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` · `), _el$0 = _$createTextNode(` `), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createTextNode(` / `), _el$15 = _$createTextNode(` (`), _el$16 = _$createTextNode(`)`), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` · `), _el$23 = _$createTextNode(` `), _el$24 = _$createElement("box"), _el$25 = _$createElement("text"), _el$26 = _$createElement("text"), _el$27 = _$createTextNode(` `), _el$28 = _$createTextNode(` `), _el$29 = _$createElement("text"), _el$30 = _$createTextNode(` / `), _el$31 = _$createTextNode(` (`), _el$32 = _$createTextNode(`)`), _el$33 = _$createElement("box"), _el$34 = _$createElement("text"), _el$35 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createTextNode(`↓ `), _el$39 = _$createTextNode(` ↑ `);
436
520
  _$insertNode(_el$, _el$2);
437
521
  _$insertNode(_el$, _el$4);
438
522
  _$insertNode(_el$, _el$1);
439
523
  _$insertNode(_el$, _el$17);
440
524
  _$insertNode(_el$, _el$24);
441
- _$insertNode(_el$, _el$32);
525
+ _$insertNode(_el$, _el$33);
442
526
  _$setProp(_el$, "flexDirection", "column");
443
527
  _$setProp(_el$, "paddingLeft", 0);
444
528
  _$setProp(_el$, "paddingRight", 0);
@@ -481,26 +565,28 @@ function MetricsPanel(props) {
481
565
  _$insert(_el$21, () => formatTemperature(metricsState.sharedMetrics().gpuTemperatureCelsius), null);
482
566
  _$insertNode(_el$24, _el$25);
483
567
  _$insertNode(_el$24, _el$26);
484
- _$insertNode(_el$24, _el$28);
568
+ _$insertNode(_el$24, _el$29);
485
569
  _$setProp(_el$24, "flexDirection", "row");
486
570
  _$insert(_el$25, () => icons.vram);
487
- _$insertNode(_el$26, _$createTextNode(` GPU VRAM `));
488
- _$insertNode(_el$28, _el$29);
489
- _$insertNode(_el$28, _el$30);
490
- _$insertNode(_el$28, _el$31);
491
- _$insert(_el$28, () => formatGiB(metricsState.sharedMetrics().gpuMemoryUsedBytes), _el$29);
492
- _$insert(_el$28, () => formatGiB(metricsState.sharedMetrics().gpuMemoryTotalBytes), _el$30);
493
- _$insert(_el$28, () => formatPercent(metricsState.sharedMetrics().gpuMemoryPercent), _el$31);
494
- _$insertNode(_el$32, _el$33);
495
- _$insertNode(_el$32, _el$34);
496
- _$insertNode(_el$32, _el$36);
497
- _$setProp(_el$32, "flexDirection", "row");
498
- _$insert(_el$33, () => icons.network);
499
- _$insertNode(_el$34, _$createTextNode(` NET `));
500
- _$insertNode(_el$36, _el$37);
501
- _$insertNode(_el$36, _el$38);
502
- _$insert(_el$36, () => formatRate(metricsState.sharedMetrics().downloadBytesPerSecond), _el$38);
503
- _$insert(_el$36, () => formatRate(metricsState.sharedMetrics().uploadBytesPerSecond), null);
571
+ _$insertNode(_el$26, _el$27);
572
+ _$insertNode(_el$26, _el$28);
573
+ _$insert(_el$26, () => gpuMemoryLabel(metricsState.sharedMetrics().gpuMemoryIsUnified === true ? "arm64" : metricsState.sharedMetrics().gpuMemoryIsUnified === false ? "x86_64" : undefined), _el$28);
574
+ _$insertNode(_el$29, _el$30);
575
+ _$insertNode(_el$29, _el$31);
576
+ _$insertNode(_el$29, _el$32);
577
+ _$insert(_el$29, () => formatGiB(metricsState.sharedMetrics().gpuMemoryUsedBytes), _el$30);
578
+ _$insert(_el$29, () => formatGiB(metricsState.sharedMetrics().gpuMemoryTotalBytes), _el$31);
579
+ _$insert(_el$29, () => formatPercent(metricsState.sharedMetrics().gpuMemoryPercent), _el$32);
580
+ _$insertNode(_el$33, _el$34);
581
+ _$insertNode(_el$33, _el$35);
582
+ _$insertNode(_el$33, _el$37);
583
+ _$setProp(_el$33, "flexDirection", "row");
584
+ _$insert(_el$34, () => icons.network);
585
+ _$insertNode(_el$35, _$createTextNode(` NET `));
586
+ _$insertNode(_el$37, _el$38);
587
+ _$insertNode(_el$37, _el$39);
588
+ _$insert(_el$37, () => formatRate(metricsState.sharedMetrics().downloadBytesPerSecond), _el$39);
589
+ _$insert(_el$37, () => formatRate(metricsState.sharedMetrics().uploadBytesPerSecond), null);
504
590
  _$effect((_p$) => {
505
591
  var _v$ = props.theme.text, _v$2 = TextAttributes.BOLD, _v$3 = props.theme.success, _v$4 = props.theme.text, _v$5 = props.theme.textMuted, _v$6 = props.theme.success, _v$7 = props.theme.text, _v$8 = props.theme.textMuted, _v$9 = props.theme.success, _v$0 = props.theme.text, _v$1 = props.theme.textMuted, _v$10 = props.theme.success, _v$11 = props.theme.text, _v$12 = props.theme.textMuted, _v$13 = props.theme.success, _v$14 = props.theme.text, _v$15 = props.theme.textMuted;
506
592
  _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
@@ -516,10 +602,10 @@ function MetricsPanel(props) {
516
602
  _v$1 !== _p$.l && (_p$.l = _$setProp(_el$21, "fg", _v$1, _p$.l));
517
603
  _v$10 !== _p$.u && (_p$.u = _$setProp(_el$25, "fg", _v$10, _p$.u));
518
604
  _v$11 !== _p$.c && (_p$.c = _$setProp(_el$26, "fg", _v$11, _p$.c));
519
- _v$12 !== _p$.w && (_p$.w = _$setProp(_el$28, "fg", _v$12, _p$.w));
520
- _v$13 !== _p$.m && (_p$.m = _$setProp(_el$33, "fg", _v$13, _p$.m));
521
- _v$14 !== _p$.f && (_p$.f = _$setProp(_el$34, "fg", _v$14, _p$.f));
522
- _v$15 !== _p$.y && (_p$.y = _$setProp(_el$36, "fg", _v$15, _p$.y));
605
+ _v$12 !== _p$.w && (_p$.w = _$setProp(_el$29, "fg", _v$12, _p$.w));
606
+ _v$13 !== _p$.m && (_p$.m = _$setProp(_el$34, "fg", _v$13, _p$.m));
607
+ _v$14 !== _p$.f && (_p$.f = _$setProp(_el$35, "fg", _v$14, _p$.f));
608
+ _v$15 !== _p$.y && (_p$.y = _$setProp(_el$37, "fg", _v$15, _p$.y));
523
609
  return _p$;
524
610
  }, {
525
611
  e: undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-system-metrics-tui",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "OpenCode TUI sidebar plugin for live system metrics",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,7 +1,8 @@
1
1
  import { execFile } from "node:child_process"
2
2
  import { readFile } from "node:fs/promises"
3
+ import { fileURLToPath } from "node:url"
3
4
  import { promisify } from "node:util"
4
- import { formatVerificationResult, verifyPluginInstallation } from "./plugin-installation.js"
5
+ import { formatVerificationResult, shouldInstallPlugin, verifyPluginInstallation } from "./plugin-installation.js"
5
6
 
6
7
  const execFileAsync = promisify(execFile)
7
8
  const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
@@ -10,21 +11,36 @@ const manifest = JSON.parse(await readFile(new URL("../package.json", import.met
10
11
  }
11
12
  const packageSpec = `${manifest.name}@${manifest.version}`
12
13
 
13
- console.log(`Installing ${packageSpec} globally through OpenCode`)
14
- try {
15
- const result = await execFileAsync("opencode", ["plugin", "-g", packageSpec, "--force"], { windowsHide: true })
16
- if (result.stdout.trim()) console.log(result.stdout.trim())
17
- if (result.stderr.trim()) console.error(result.stderr.trim())
18
- } catch (error) {
19
- const message = error instanceof Error ? error.message : String(error)
20
- console.error(`OpenCode plugin installation failed: ${message}`)
21
- process.exit(1)
14
+ export async function installPlugin(): Promise<void> {
15
+ const verificationOptions = {
16
+ packageName: manifest.name,
17
+ expectedVersion: manifest.version,
18
+ cacheDirectory: process.env.OPENCODE_CACHE_DIR,
19
+ }
20
+ const current = await verifyPluginInstallation(verificationOptions)
21
+ console.log(formatVerificationResult(current))
22
+ if (!shouldInstallPlugin(current)) {
23
+ console.log(`Installation already current for ${packageSpec}; OpenCode was not invoked.`)
24
+ return
25
+ }
26
+
27
+ console.log(`Installing ${packageSpec} globally through OpenCode`)
28
+ try {
29
+ const result = await execFileAsync("opencode", ["plugin", "-g", packageSpec, "--force"], { windowsHide: true })
30
+ if (result.stdout.trim()) console.log(result.stdout.trim())
31
+ if (result.stderr.trim()) console.error(result.stderr.trim())
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error)
34
+ console.error(`OpenCode plugin installation failed: ${message}`)
35
+ process.exitCode = 1
36
+ return
37
+ }
38
+
39
+ const verification = await verifyPluginInstallation(verificationOptions)
40
+ console.log(formatVerificationResult(verification))
41
+ if (!verification.ok) process.exitCode = 1
22
42
  }
23
43
 
24
- const verification = await verifyPluginInstallation({
25
- packageName: manifest.name,
26
- expectedVersion: manifest.version,
27
- cacheDirectory: process.env.OPENCODE_CACHE_DIR,
28
- })
29
- console.log(formatVerificationResult(verification))
30
- if (!verification.ok) process.exitCode = 1
44
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]) {
45
+ await installPlugin()
46
+ }
@@ -15,6 +15,7 @@ export type PluginVerification = {
15
15
  packagePath?: string
16
16
  actualVersion?: string
17
17
  staleEntries: string[]
18
+ warnings: string[]
18
19
  errors: string[]
19
20
  }
20
21
 
@@ -46,8 +47,11 @@ export async function verifyPluginInstallation(options: PluginVerificationOption
46
47
  const cacheDirectory = resolve(options.cacheDirectory ?? getDefaultOpenCodeCacheDirectory())
47
48
  const errors: string[] = []
48
49
  const staleEntries: string[] = []
50
+ const warnings: string[] = []
49
51
  let packagePath: string | undefined
50
52
  let actualVersion: string | undefined
53
+ let expectedArtifactFound = false
54
+ let expectedPackagePath: string | undefined
51
55
 
52
56
  let entries: string[]
53
57
  try {
@@ -55,7 +59,7 @@ export async function verifyPluginInstallation(options: PluginVerificationOption
55
59
  .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${options.packageName}@`))
56
60
  .map((entry) => entry.name)
57
61
  } catch {
58
- return { ok: false, staleEntries, errors: [`OpenCode package cache not found: ${cacheDirectory}`] }
62
+ return { ok: false, staleEntries, warnings, errors: [`OpenCode package cache not found: ${cacheDirectory}`] }
59
63
  }
60
64
 
61
65
  for (const entry of entries) {
@@ -65,31 +69,42 @@ export async function verifyPluginInstallation(options: PluginVerificationOption
65
69
  staleEntries.push(join(cacheDirectory, entry))
66
70
  continue
67
71
  }
68
- if (manifest.version === options.expectedVersion && packagePath === undefined) {
69
- packagePath = candidatePath
70
- actualVersion = manifest.version
71
- } else if (manifest.version !== options.expectedVersion) {
72
+ if (manifest.version === options.expectedVersion) {
73
+ expectedArtifactFound = true
74
+ expectedPackagePath ??= candidatePath
75
+ if (await exists(join(candidatePath, options.distPath ?? "dist", "tui.js")) && packagePath === undefined) {
76
+ packagePath = candidatePath
77
+ actualVersion = manifest.version
78
+ }
79
+ } else {
72
80
  staleEntries.push(join(cacheDirectory, entry))
73
81
  }
74
82
  }
75
83
 
76
- if (packagePath === undefined) {
84
+ if (!expectedArtifactFound) {
77
85
  errors.push(`Installed ${options.packageName}@${options.expectedVersion} was not found in ${cacheDirectory}`)
86
+ } else if (packagePath === undefined) {
87
+ errors.push(`Required dist/tui.js is missing from an expected ${options.packageName}@${options.expectedVersion} artifact in ${cacheDirectory}`)
88
+ packagePath = expectedPackagePath
89
+ actualVersion = options.expectedVersion
78
90
  } else if (actualVersion !== options.expectedVersion) {
79
91
  errors.push(`Installed version is ${actualVersion ?? "unknown"}; expected ${options.expectedVersion}`)
80
92
  }
81
93
 
82
- if (packagePath !== undefined && !(await exists(join(packagePath, options.distPath ?? "dist", "tui.js")))) {
83
- errors.push(`Required dist/tui.js is missing from ${packagePath}`)
84
- }
85
- if (staleEntries.length > 0) errors.push(`Stale OpenCode cache entries detected: ${staleEntries.join(", ")}`)
94
+ if (staleEntries.length > 0) warnings.push(`Stale OpenCode cache entries detected: ${staleEntries.join(", ")}`)
95
+
96
+ return { ok: errors.length === 0, packagePath, actualVersion, staleEntries, warnings, errors }
97
+ }
86
98
 
87
- return { ok: errors.length === 0, packagePath, actualVersion, staleEntries, errors }
99
+ export function shouldInstallPlugin(result: PluginVerification): boolean {
100
+ return !result.ok
88
101
  }
89
102
 
90
103
  export function formatVerificationResult(result: PluginVerification): string {
91
- if (result.ok) return `Verified OpenCode plugin ${result.actualVersion} at ${result.packagePath}`
92
- return result.errors.join("\n")
104
+ const lines = result.ok
105
+ ? [`Verified OpenCode plugin ${result.actualVersion} at ${result.packagePath}`]
106
+ : result.errors
107
+ return [...lines, ...result.warnings].join("\n")
93
108
  }
94
109
 
95
110
  if (process.argv[1] !== undefined && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {