opencode-system-metrics-tui 0.1.3 → 0.1.5
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 +36 -9
- package/dist/install-hack-font.js +10 -4
- package/dist/tui.js +92 -59
- package/package.json +4 -2
- package/scripts/install-plugin.ts +30 -0
- package/scripts/plugin-installation.ts +107 -0
package/README.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# OpenCode System Metrics TUI
|
|
2
2
|
|
|
3
|
+
## Install globally in OpenCode
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
opencode plugin -g opencode-system-metrics-tui@0.1.4 --force
|
|
7
|
+
```
|
|
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:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
npm run install-plugin
|
|
13
|
+
npm run verify-installation
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`npm run install-plugin` runs the same global OpenCode installer command with the current `package.json` version. The explicit command above is provided for users installing without this checkout.
|
|
17
|
+
|
|
18
|
+
Restart OpenCode after installation. This installs the plugin globally for all projects. The OpenCode cache directory name, including `@latest`, is only a package-specifier label and does not prove the package version. Verification reads the nested package's own `package.json` and checks `dist/tui.js`; it reports stale entries without deleting anything.
|
|
19
|
+
|
|
3
20
|
OpenCode TUI plugin that adds live CPU, RAM, GPU, GPU VRAM, temperature, and network metrics to the `sidebar_content` slot.
|
|
4
21
|
|
|
5
22
|
The README is available in **Español**, **English**, and **Português**.
|
|
@@ -23,6 +40,8 @@ The README is available in **Español**, **English**, and **Português**.
|
|
|
23
40
|
- En Windows, la temperatura de CPU requiere LibreHardwareMonitor ejecutándose como administrador con **Remote Web Server** activo en `http://127.0.0.1:8085`.
|
|
24
41
|
- En Windows, la velocidad de red usa PowerShell y `Get-NetAdapterStatistics`; no agrega una dependencia npm.
|
|
25
42
|
- Las lecturas opcionales de Windows se hacen bajo demanda, con timeout y caché; no hay timers globales de PowerShell.
|
|
43
|
+
- La detección de Nerd Font en Linux/macOS está limitada a rutas conocidas, 2 niveles de profundidad y 256 entradas por directorio; una ruta ausente o inaccesible usa Unicode. Windows no escanea fuentes salvo con `OPENCODE_MONITOR_NERD_FONT=1`.
|
|
44
|
+
- Si una fuente de métricas no responde a tiempo, su intento en caché se abandona de forma segura para permitir reintentos sin cancelar APIs que no aceptan `AbortSignal`.
|
|
26
45
|
|
|
27
46
|
### Requisitos
|
|
28
47
|
|
|
@@ -72,7 +91,7 @@ npm publish --access public
|
|
|
72
91
|
En otro equipo:
|
|
73
92
|
|
|
74
93
|
```bash
|
|
75
|
-
opencode plugin opencode-system-metrics-tui
|
|
94
|
+
opencode plugin opencode-system-metrics-tui@0.1.4
|
|
76
95
|
```
|
|
77
96
|
|
|
78
97
|
El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
|
|
@@ -82,12 +101,14 @@ El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia Ope
|
|
|
82
101
|
Publicar en npm e instalar en OpenCode son pasos distintos:
|
|
83
102
|
|
|
84
103
|
- `npm publish --access public` publica el paquete en npm.
|
|
85
|
-
- `opencode plugin opencode-system-metrics-tui` lo instala solo en el proyecto actual.
|
|
86
|
-
- `opencode plugin -g opencode-system-metrics-tui` lo instala globalmente para todos los proyectos.
|
|
104
|
+
- `opencode plugin opencode-system-metrics-tui@0.1.4` lo instala solo en el proyecto actual.
|
|
105
|
+
- `opencode plugin -g opencode-system-metrics-tui@0.1.4 --force` lo instala globalmente para todos los proyectos.
|
|
87
106
|
|
|
88
107
|
### Verificación
|
|
89
108
|
|
|
90
109
|
```bash
|
|
110
|
+
npm run install-plugin
|
|
111
|
+
npm run verify-installation
|
|
91
112
|
npm test
|
|
92
113
|
npm run typecheck
|
|
93
114
|
npm run build
|
|
@@ -112,6 +133,8 @@ npm run build
|
|
|
112
133
|
- On Windows, CPU temperature requires LibreHardwareMonitor running as administrator with **Remote Web Server** enabled at `http://127.0.0.1:8085`.
|
|
113
134
|
- On Windows, network speed uses PowerShell and `Get-NetAdapterStatistics`; it adds no npm dependency.
|
|
114
135
|
- Optional Windows reads are on demand, timeout-bounded, and cached; no global PowerShell timers are used.
|
|
136
|
+
- Linux/macOS Nerd Font detection is limited to known paths, two directory levels, and 256 entries per directory; missing or inaccessible paths safely fall back to Unicode. Windows does not scan fonts unless `OPENCODE_MONITOR_NERD_FONT=1` is set.
|
|
137
|
+
- When a metric source does not respond in time, its cache attempt is safely abandoned so retries remain possible without cancelling APIs that do not accept `AbortSignal`.
|
|
115
138
|
|
|
116
139
|
### Requirements
|
|
117
140
|
|
|
@@ -161,7 +184,7 @@ npm publish --access public
|
|
|
161
184
|
On another machine:
|
|
162
185
|
|
|
163
186
|
```bash
|
|
164
|
-
opencode plugin opencode-system-metrics-tui
|
|
187
|
+
opencode plugin opencode-system-metrics-tui@0.1.4
|
|
165
188
|
```
|
|
166
189
|
|
|
167
190
|
The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
|
|
@@ -171,12 +194,14 @@ The OpenCode installer configures the TUI plugin automatically. Restart OpenCode
|
|
|
171
194
|
Publishing to npm and installing in OpenCode are separate steps:
|
|
172
195
|
|
|
173
196
|
- `npm publish --access public` publishes the package to npm.
|
|
174
|
-
- `opencode plugin opencode-system-metrics-tui` installs it for the current project only.
|
|
175
|
-
- `opencode plugin -g opencode-system-metrics-tui` installs it globally for all projects.
|
|
197
|
+
- `opencode plugin opencode-system-metrics-tui@0.1.4` installs it for the current project only.
|
|
198
|
+
- `opencode plugin -g opencode-system-metrics-tui@0.1.4 --force` installs it globally for all projects.
|
|
176
199
|
|
|
177
200
|
### Verification
|
|
178
201
|
|
|
179
202
|
```bash
|
|
203
|
+
npm run install-plugin
|
|
204
|
+
npm run verify-installation
|
|
180
205
|
npm test
|
|
181
206
|
npm run typecheck
|
|
182
207
|
npm run build
|
|
@@ -201,6 +226,8 @@ npm run build
|
|
|
201
226
|
- No Windows, a temperatura da CPU requer o LibreHardwareMonitor executado como administrador com o **Remote Web Server** ativo em `http://127.0.0.1:8085`.
|
|
202
227
|
- No Windows, a velocidade da rede usa o PowerShell e `Get-NetAdapterStatistics`; nenhuma dependência npm é adicionada.
|
|
203
228
|
- As leituras opcionais do Windows são sob demanda, têm timeout e cache; não há timers globais do PowerShell.
|
|
229
|
+
- A detecção de Nerd Font no Linux/macOS é limitada a caminhos conhecidos, dois níveis de profundidade e 256 entradas por diretório; caminhos ausentes ou inacessíveis usam Unicode. O Windows não verifica fontes sem `OPENCODE_MONITOR_NERD_FONT=1`.
|
|
230
|
+
- Quando uma fonte de métricas não responde a tempo, a tentativa em cache é abandonada com segurança para permitir novas tentativas sem cancelar APIs que não aceitam `AbortSignal`.
|
|
204
231
|
|
|
205
232
|
### Requisitos
|
|
206
233
|
|
|
@@ -250,7 +277,7 @@ npm publish --access public
|
|
|
250
277
|
Em outro computador:
|
|
251
278
|
|
|
252
279
|
```bash
|
|
253
|
-
opencode plugin opencode-system-metrics-tui
|
|
280
|
+
opencode plugin opencode-system-metrics-tui@0.1.4
|
|
254
281
|
```
|
|
255
282
|
|
|
256
283
|
O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
|
|
@@ -260,8 +287,8 @@ O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o Open
|
|
|
260
287
|
Publicar no npm e instalar no OpenCode são etapas diferentes:
|
|
261
288
|
|
|
262
289
|
- `npm publish --access public` publica o pacote no npm.
|
|
263
|
-
- `opencode plugin opencode-system-metrics-tui` instala o plugin apenas no projeto atual.
|
|
264
|
-
- `opencode plugin -g opencode-system-metrics-tui` instala o plugin globalmente para todos os projetos.
|
|
290
|
+
- `opencode plugin opencode-system-metrics-tui@0.1.4` instala o plugin apenas no projeto atual.
|
|
291
|
+
- `opencode plugin -g opencode-system-metrics-tui@0.1.4 --force` instala o plugin globalmente para todos os projetos.
|
|
265
292
|
|
|
266
293
|
### Verificação
|
|
267
294
|
|
|
@@ -10,6 +10,8 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join as posixJoin, win32 } from "node:path";
|
|
12
12
|
var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
|
|
13
|
+
var MAX_FONT_SCAN_DEPTH = 2;
|
|
14
|
+
var MAX_FONT_SCAN_ENTRIES = 256;
|
|
13
15
|
function getFontDirectories(platform, home = homedir(), env = process.env) {
|
|
14
16
|
if (platform === "darwin")
|
|
15
17
|
return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
|
|
@@ -25,19 +27,23 @@ function isNerdFontFile(fileName) {
|
|
|
25
27
|
}
|
|
26
28
|
var isHackNerdFontFile = isNerdFontFile;
|
|
27
29
|
function hasNerdFont(platform = process.platform, home = homedir(), env = process.env) {
|
|
28
|
-
|
|
30
|
+
if (platform === "win32" && env.OPENCODE_MONITOR_NERD_FONT !== "1")
|
|
31
|
+
return false;
|
|
32
|
+
const containsFont = (directory, depth) => {
|
|
33
|
+
if (depth > MAX_FONT_SCAN_DEPTH)
|
|
34
|
+
return false;
|
|
29
35
|
if (!existsSync(directory))
|
|
30
36
|
return false;
|
|
31
37
|
try {
|
|
32
|
-
return readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
38
|
+
return readdirSync(directory, { withFileTypes: true }).slice(0, MAX_FONT_SCAN_ENTRIES).some((entry) => {
|
|
33
39
|
const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
|
|
34
|
-
return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
|
|
40
|
+
return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path, depth + 1);
|
|
35
41
|
});
|
|
36
42
|
} catch {
|
|
37
43
|
return false;
|
|
38
44
|
}
|
|
39
45
|
};
|
|
40
|
-
return getFontDirectories(platform, home, env).some(containsFont);
|
|
46
|
+
return getFontDirectories(platform, home, env).some((directory) => containsFont(directory, 0));
|
|
41
47
|
}
|
|
42
48
|
var hasHackNerdFont = hasNerdFont;
|
|
43
49
|
var nerdFontIcons = {
|
package/dist/tui.js
CHANGED
|
@@ -52,22 +52,31 @@ async function readLibreHardwareMonitorTemperature() {
|
|
|
52
52
|
if (!response.ok)
|
|
53
53
|
return;
|
|
54
54
|
const root = await response.json();
|
|
55
|
+
if (root === null || typeof root !== "object")
|
|
56
|
+
return;
|
|
55
57
|
const sensors = [];
|
|
56
58
|
const visit = (node) => {
|
|
57
59
|
if (node.Type === "Temperature" && node.Text !== undefined)
|
|
58
60
|
sensors.push(node);
|
|
59
|
-
for (const child of node.Children
|
|
60
|
-
|
|
61
|
+
for (const child of Array.isArray(node.Children) ? node.Children : []) {
|
|
62
|
+
if (child !== null && typeof child === "object")
|
|
63
|
+
visit(child);
|
|
64
|
+
}
|
|
61
65
|
};
|
|
62
66
|
visit(root);
|
|
63
67
|
const cpuSensor = sensors.find((sensor) => sensor.Text === "CPU Package") ?? sensors.find((sensor) => sensor.Text === "Core Average") ?? sensors.find((sensor) => sensor.Text === "Core Max");
|
|
64
68
|
return parseMonitorTemperature(cpuSensor?.Value);
|
|
65
69
|
}
|
|
66
70
|
function parseWindowsNetworkOutput(stdout) {
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(stdout.trim());
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const rows = (Array.isArray(parsed) ? parsed : [parsed]).filter((item) => item !== null && typeof item === "object");
|
|
69
78
|
return rows.map((item) => ({
|
|
70
|
-
iface: item.Name,
|
|
79
|
+
iface: typeof item.Name === "string" ? item.Name : undefined,
|
|
71
80
|
rx_bytes: finite(item.ReceivedBytes) ?? 0,
|
|
72
81
|
tx_bytes: finite(item.SentBytes) ?? 0
|
|
73
82
|
}));
|
|
@@ -126,14 +135,15 @@ function selectGpuMetrics(graphics) {
|
|
|
126
135
|
gpuMemoryTotalBytes: null,
|
|
127
136
|
gpuMemoryPercent: null
|
|
128
137
|
};
|
|
129
|
-
if (graphics === undefined)
|
|
138
|
+
if (graphics === undefined || graphics === null || !Array.isArray(graphics.controllers))
|
|
130
139
|
return empty;
|
|
131
140
|
const controller = graphics.controllers.map((item) => {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
141
|
+
const source = item ?? {};
|
|
142
|
+
const utilization = finite(source.utilizationGpu);
|
|
143
|
+
const temperature = finite(source.temperatureGpu);
|
|
144
|
+
const memoryTotal = finite(source.memoryTotal);
|
|
145
|
+
const memoryFree = finite(source.memoryFree);
|
|
146
|
+
const memoryUsed = finite(source.memoryUsed) ?? (memoryTotal !== null && memoryFree !== null ? Math.max(0, memoryTotal - memoryFree) : null);
|
|
137
147
|
const hasMemory = memoryUsed !== null && memoryTotal !== null && memoryTotal > 0;
|
|
138
148
|
return { utilization, temperature, memoryUsed, memoryTotal, hasMemory };
|
|
139
149
|
}).filter(({ utilization, temperature, hasMemory }) => utilization !== null || temperature !== null || hasMemory).sort((left, right) => {
|
|
@@ -156,7 +166,7 @@ function selectGpuMetrics(graphics) {
|
|
|
156
166
|
}
|
|
157
167
|
return result;
|
|
158
168
|
}
|
|
159
|
-
function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = Date.now(), unavailableCacheMs = cacheMs) {
|
|
169
|
+
function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = Date.now(), unavailableCacheMs = cacheMs, timeoutMs) {
|
|
160
170
|
if (state.inFlight !== undefined)
|
|
161
171
|
return state.inFlight;
|
|
162
172
|
if (state.lastFailureAt !== undefined && now - state.lastFailureAt < unavailableCacheMs) {
|
|
@@ -165,17 +175,23 @@ function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = D
|
|
|
165
175
|
if (now - state.lastAttemptAt < cacheMs)
|
|
166
176
|
return Promise.resolve(state.value);
|
|
167
177
|
state.lastAttemptAt = now;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
let flight;
|
|
179
|
+
flight = (async () => {
|
|
180
|
+
try {
|
|
181
|
+
const value = timeoutMs === undefined ? await reader() : await withTimeout(reader(), timeoutMs);
|
|
182
|
+
state.value = value;
|
|
183
|
+
state.lastFailureAt = undefined;
|
|
184
|
+
return value;
|
|
185
|
+
} catch {
|
|
186
|
+
state.lastFailureAt = now;
|
|
187
|
+
return state.value;
|
|
188
|
+
} finally {
|
|
189
|
+
if (state.inFlight === flight)
|
|
190
|
+
state.inFlight = undefined;
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
state.inFlight = flight;
|
|
194
|
+
return flight;
|
|
179
195
|
}
|
|
180
196
|
var cachedCpuTemperature = {
|
|
181
197
|
value: undefined,
|
|
@@ -238,9 +254,9 @@ async function readMetrics() {
|
|
|
238
254
|
withTimeout(si.mem(), 1500)
|
|
239
255
|
]);
|
|
240
256
|
const [temperature, graphics, network] = await Promise.allSettled([
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
readCachedMetric(cachedCpuTemperature, isWindows ? readWindowsCpuTemperature : () => si.cpuTemperature(), TEMPERATURE_CACHE_MS, Date.now(), TEMPERATURE_CACHE_MS, 2500),
|
|
258
|
+
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)
|
|
244
260
|
]);
|
|
245
261
|
const cpuPercent = load.status === "fulfilled" ? finite(load.value.currentLoad) : null;
|
|
246
262
|
const memoryTotalBytes = memory.status === "fulfilled" ? finite(memory.value.total) : null;
|
|
@@ -251,7 +267,7 @@ async function readMetrics() {
|
|
|
251
267
|
let downloadBytesPerSecond = null;
|
|
252
268
|
let uploadBytesPerSecond = null;
|
|
253
269
|
if (network.status === "fulfilled" && network.value !== undefined) {
|
|
254
|
-
const samples = network.value;
|
|
270
|
+
const samples = Array.isArray(network.value) ? network.value : [];
|
|
255
271
|
const timestamp = Date.now();
|
|
256
272
|
const rx = samples.reduce((total, item) => total + (finite(item.rx_bytes) ?? 0), 0);
|
|
257
273
|
const tx = samples.reduce((total, item) => total + (finite(item.tx_bytes) ?? 0), 0);
|
|
@@ -290,6 +306,8 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
290
306
|
import { homedir } from "node:os";
|
|
291
307
|
import { join as posixJoin, win32 } from "node:path";
|
|
292
308
|
var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
|
|
309
|
+
var MAX_FONT_SCAN_DEPTH = 2;
|
|
310
|
+
var MAX_FONT_SCAN_ENTRIES = 256;
|
|
293
311
|
function getFontDirectories(platform, home = homedir(), env = process.env) {
|
|
294
312
|
if (platform === "darwin")
|
|
295
313
|
return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
|
|
@@ -305,19 +323,23 @@ function isNerdFontFile(fileName) {
|
|
|
305
323
|
}
|
|
306
324
|
var isHackNerdFontFile = isNerdFontFile;
|
|
307
325
|
function hasNerdFont(platform = process.platform, home = homedir(), env = process.env) {
|
|
308
|
-
|
|
326
|
+
if (platform === "win32" && env.OPENCODE_MONITOR_NERD_FONT !== "1")
|
|
327
|
+
return false;
|
|
328
|
+
const containsFont = (directory, depth) => {
|
|
329
|
+
if (depth > MAX_FONT_SCAN_DEPTH)
|
|
330
|
+
return false;
|
|
309
331
|
if (!existsSync(directory))
|
|
310
332
|
return false;
|
|
311
333
|
try {
|
|
312
|
-
return readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
334
|
+
return readdirSync(directory, { withFileTypes: true }).slice(0, MAX_FONT_SCAN_ENTRIES).some((entry) => {
|
|
313
335
|
const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
|
|
314
|
-
return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
|
|
336
|
+
return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path, depth + 1);
|
|
315
337
|
});
|
|
316
338
|
} catch {
|
|
317
339
|
return false;
|
|
318
340
|
}
|
|
319
341
|
};
|
|
320
|
-
return getFontDirectories(platform, home, env).some(containsFont);
|
|
342
|
+
return getFontDirectories(platform, home, env).some((directory) => containsFont(directory, 0));
|
|
321
343
|
}
|
|
322
344
|
var hasHackNerdFont = hasNerdFont;
|
|
323
345
|
var nerdFontIcons = {
|
|
@@ -341,7 +363,8 @@ function shouldUseNerdFont(platform, detected, env = process.env) {
|
|
|
341
363
|
|
|
342
364
|
// src/tui.tsx
|
|
343
365
|
var REFRESH_INTERVAL_MS = 2000;
|
|
344
|
-
var
|
|
366
|
+
var platform = process.platform;
|
|
367
|
+
var icons = getMetricIcons(shouldUseNerdFont(platform, platform === "win32" ? false : hasNerdFont()));
|
|
345
368
|
var initialMetrics = {
|
|
346
369
|
cpuPercent: null,
|
|
347
370
|
memoryUsedBytes: null,
|
|
@@ -356,33 +379,43 @@ var initialMetrics = {
|
|
|
356
379
|
downloadBytesPerSecond: null,
|
|
357
380
|
uploadBytesPerSecond: null
|
|
358
381
|
};
|
|
359
|
-
var
|
|
360
|
-
var
|
|
361
|
-
var
|
|
362
|
-
|
|
382
|
+
var METRICS_STATE_KEY = Symbol.for("opencode-monitor.system-metrics");
|
|
383
|
+
var globalMetrics = globalThis;
|
|
384
|
+
var metricsState = globalMetrics[METRICS_STATE_KEY] ?? (globalMetrics[METRICS_STATE_KEY] = {
|
|
385
|
+
...(() => {
|
|
386
|
+
const [sharedMetrics, setSharedMetrics] = createSignal(initialMetrics);
|
|
387
|
+
return {
|
|
388
|
+
sharedMetrics,
|
|
389
|
+
setSharedMetrics
|
|
390
|
+
};
|
|
391
|
+
})(),
|
|
392
|
+
metricsTimer: undefined,
|
|
393
|
+
metricsConsumers: 0,
|
|
394
|
+
refreshing: false
|
|
395
|
+
});
|
|
363
396
|
var refreshMetrics = async () => {
|
|
364
|
-
if (refreshing)
|
|
397
|
+
if (metricsState.refreshing)
|
|
365
398
|
return;
|
|
366
|
-
refreshing = true;
|
|
399
|
+
metricsState.refreshing = true;
|
|
367
400
|
try {
|
|
368
|
-
setSharedMetrics(await readMetrics());
|
|
401
|
+
metricsState.setSharedMetrics(await readMetrics());
|
|
369
402
|
} finally {
|
|
370
|
-
refreshing = false;
|
|
403
|
+
metricsState.refreshing = false;
|
|
371
404
|
}
|
|
372
405
|
};
|
|
373
406
|
function startMetricsPolling() {
|
|
374
|
-
metricsConsumers += 1;
|
|
375
|
-
if (metricsConsumers !== 1)
|
|
407
|
+
metricsState.metricsConsumers += 1;
|
|
408
|
+
if (metricsState.metricsConsumers !== 1)
|
|
376
409
|
return;
|
|
377
410
|
refreshMetrics();
|
|
378
|
-
metricsTimer = setInterval(() => void refreshMetrics(), REFRESH_INTERVAL_MS);
|
|
411
|
+
metricsState.metricsTimer = setInterval(() => void refreshMetrics(), REFRESH_INTERVAL_MS);
|
|
379
412
|
}
|
|
380
413
|
function stopMetricsPolling() {
|
|
381
|
-
metricsConsumers = Math.max(0, metricsConsumers - 1);
|
|
382
|
-
if (metricsConsumers !== 0 || metricsTimer === undefined)
|
|
414
|
+
metricsState.metricsConsumers = Math.max(0, metricsState.metricsConsumers - 1);
|
|
415
|
+
if (metricsState.metricsConsumers !== 0 || metricsState.metricsTimer === undefined)
|
|
383
416
|
return;
|
|
384
|
-
clearInterval(metricsTimer);
|
|
385
|
-
metricsTimer = undefined;
|
|
417
|
+
clearInterval(metricsState.metricsTimer);
|
|
418
|
+
metricsState.metricsTimer = undefined;
|
|
386
419
|
}
|
|
387
420
|
function systemMetricsTitle() {
|
|
388
421
|
const locale = process.env.LC_ALL ?? process.env.LANGUAGE?.split(":")[0] ?? process.env.LANG ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
@@ -420,9 +453,9 @@ function MetricsPanel(props) {
|
|
|
420
453
|
_$insertNode(_el$6, _$createTextNode(` CPU `));
|
|
421
454
|
_$insertNode(_el$8, _el$9);
|
|
422
455
|
_$insertNode(_el$8, _el$0);
|
|
423
|
-
_$insert(_el$8, () => formatPercent(sharedMetrics().cpuPercent), _el$9);
|
|
456
|
+
_$insert(_el$8, () => formatPercent(metricsState.sharedMetrics().cpuPercent), _el$9);
|
|
424
457
|
_$insert(_el$8, () => icons.thermometer, _el$0);
|
|
425
|
-
_$insert(_el$8, () => formatTemperature(sharedMetrics().cpuTemperatureCelsius), null);
|
|
458
|
+
_$insert(_el$8, () => formatTemperature(metricsState.sharedMetrics().cpuTemperatureCelsius), null);
|
|
426
459
|
_$insertNode(_el$1, _el$10);
|
|
427
460
|
_$insertNode(_el$1, _el$11);
|
|
428
461
|
_$insertNode(_el$1, _el$13);
|
|
@@ -432,9 +465,9 @@ function MetricsPanel(props) {
|
|
|
432
465
|
_$insertNode(_el$13, _el$14);
|
|
433
466
|
_$insertNode(_el$13, _el$15);
|
|
434
467
|
_$insertNode(_el$13, _el$16);
|
|
435
|
-
_$insert(_el$13, () => formatGiB(sharedMetrics().memoryUsedBytes), _el$14);
|
|
436
|
-
_$insert(_el$13, () => formatGiB(sharedMetrics().memoryTotalBytes), _el$15);
|
|
437
|
-
_$insert(_el$13, () => formatPercent(sharedMetrics().memoryPercent), _el$16);
|
|
468
|
+
_$insert(_el$13, () => formatGiB(metricsState.sharedMetrics().memoryUsedBytes), _el$14);
|
|
469
|
+
_$insert(_el$13, () => formatGiB(metricsState.sharedMetrics().memoryTotalBytes), _el$15);
|
|
470
|
+
_$insert(_el$13, () => formatPercent(metricsState.sharedMetrics().memoryPercent), _el$16);
|
|
438
471
|
_$insertNode(_el$17, _el$18);
|
|
439
472
|
_$insertNode(_el$17, _el$19);
|
|
440
473
|
_$insertNode(_el$17, _el$21);
|
|
@@ -443,9 +476,9 @@ function MetricsPanel(props) {
|
|
|
443
476
|
_$insertNode(_el$19, _$createTextNode(` GPU `));
|
|
444
477
|
_$insertNode(_el$21, _el$22);
|
|
445
478
|
_$insertNode(_el$21, _el$23);
|
|
446
|
-
_$insert(_el$21, () => formatPercent(sharedMetrics().gpuPercent), _el$22);
|
|
479
|
+
_$insert(_el$21, () => formatPercent(metricsState.sharedMetrics().gpuPercent), _el$22);
|
|
447
480
|
_$insert(_el$21, () => icons.thermometer, _el$23);
|
|
448
|
-
_$insert(_el$21, () => formatTemperature(sharedMetrics().gpuTemperatureCelsius), null);
|
|
481
|
+
_$insert(_el$21, () => formatTemperature(metricsState.sharedMetrics().gpuTemperatureCelsius), null);
|
|
449
482
|
_$insertNode(_el$24, _el$25);
|
|
450
483
|
_$insertNode(_el$24, _el$26);
|
|
451
484
|
_$insertNode(_el$24, _el$28);
|
|
@@ -455,9 +488,9 @@ function MetricsPanel(props) {
|
|
|
455
488
|
_$insertNode(_el$28, _el$29);
|
|
456
489
|
_$insertNode(_el$28, _el$30);
|
|
457
490
|
_$insertNode(_el$28, _el$31);
|
|
458
|
-
_$insert(_el$28, () => formatGiB(sharedMetrics().gpuMemoryUsedBytes), _el$29);
|
|
459
|
-
_$insert(_el$28, () => formatGiB(sharedMetrics().gpuMemoryTotalBytes), _el$30);
|
|
460
|
-
_$insert(_el$28, () => formatPercent(sharedMetrics().gpuMemoryPercent), _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);
|
|
461
494
|
_$insertNode(_el$32, _el$33);
|
|
462
495
|
_$insertNode(_el$32, _el$34);
|
|
463
496
|
_$insertNode(_el$32, _el$36);
|
|
@@ -466,8 +499,8 @@ function MetricsPanel(props) {
|
|
|
466
499
|
_$insertNode(_el$34, _$createTextNode(` NET `));
|
|
467
500
|
_$insertNode(_el$36, _el$37);
|
|
468
501
|
_$insertNode(_el$36, _el$38);
|
|
469
|
-
_$insert(_el$36, () => formatRate(sharedMetrics().downloadBytesPerSecond), _el$38);
|
|
470
|
-
_$insert(_el$36, () => formatRate(sharedMetrics().uploadBytesPerSecond), null);
|
|
502
|
+
_$insert(_el$36, () => formatRate(metricsState.sharedMetrics().downloadBytesPerSecond), _el$38);
|
|
503
|
+
_$insert(_el$36, () => formatRate(metricsState.sharedMetrics().uploadBytesPerSecond), null);
|
|
471
504
|
_$effect((_p$) => {
|
|
472
505
|
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;
|
|
473
506
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-system-metrics-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "OpenCode TUI sidebar plugin for live system metrics",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"build": "bun scripts/build.ts && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24
24
|
"test": "tsx --test test/*.test.ts",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"install-font": "node dist/install-hack-font.js"
|
|
26
|
+
"install-font": "node dist/install-hack-font.js",
|
|
27
|
+
"verify-installation": "tsx scripts/plugin-installation.ts",
|
|
28
|
+
"install-plugin": "tsx scripts/install-plugin.ts"
|
|
27
29
|
},
|
|
28
30
|
"engines": {
|
|
29
31
|
"node": ">=18"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import { promisify } from "node:util"
|
|
4
|
+
import { formatVerificationResult, verifyPluginInstallation } from "./plugin-installation.js"
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile)
|
|
7
|
+
const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
|
|
8
|
+
name: string
|
|
9
|
+
version: string
|
|
10
|
+
}
|
|
11
|
+
const packageSpec = `${manifest.name}@${manifest.version}`
|
|
12
|
+
|
|
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)
|
|
22
|
+
}
|
|
23
|
+
|
|
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
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { access, readFile, readdir } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { join, resolve } from "node:path"
|
|
5
|
+
|
|
6
|
+
export type PluginVerificationOptions = {
|
|
7
|
+
cacheDirectory?: string
|
|
8
|
+
packageName: string
|
|
9
|
+
expectedVersion: string
|
|
10
|
+
distPath?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PluginVerification = {
|
|
14
|
+
ok: boolean
|
|
15
|
+
packagePath?: string
|
|
16
|
+
actualVersion?: string
|
|
17
|
+
staleEntries: string[]
|
|
18
|
+
errors: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type PackageManifest = { name?: unknown; version?: unknown }
|
|
22
|
+
|
|
23
|
+
export function getDefaultOpenCodeCacheDirectory(env: NodeJS.ProcessEnv = process.env, home = homedir()): string {
|
|
24
|
+
const cacheRoot = env.OPENCODE_CACHE_DIR ?? env.XDG_CACHE_HOME ?? join(home, ".cache")
|
|
25
|
+
return join(cacheRoot, "opencode", "packages")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readManifest(path: string): Promise<PackageManifest | undefined> {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(await readFile(path, "utf8")) as PackageManifest
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function exists(path: string): Promise<boolean> {
|
|
37
|
+
try {
|
|
38
|
+
await access(path)
|
|
39
|
+
return true
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function verifyPluginInstallation(options: PluginVerificationOptions): Promise<PluginVerification> {
|
|
46
|
+
const cacheDirectory = resolve(options.cacheDirectory ?? getDefaultOpenCodeCacheDirectory())
|
|
47
|
+
const errors: string[] = []
|
|
48
|
+
const staleEntries: string[] = []
|
|
49
|
+
let packagePath: string | undefined
|
|
50
|
+
let actualVersion: string | undefined
|
|
51
|
+
|
|
52
|
+
let entries: string[]
|
|
53
|
+
try {
|
|
54
|
+
entries = (await readdir(cacheDirectory, { withFileTypes: true }))
|
|
55
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(`${options.packageName}@`))
|
|
56
|
+
.map((entry) => entry.name)
|
|
57
|
+
} catch {
|
|
58
|
+
return { ok: false, staleEntries, errors: [`OpenCode package cache not found: ${cacheDirectory}`] }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const candidatePath = join(cacheDirectory, entry, "node_modules", options.packageName)
|
|
63
|
+
const manifest = await readManifest(join(candidatePath, "package.json"))
|
|
64
|
+
if (manifest?.name !== options.packageName || typeof manifest.version !== "string") {
|
|
65
|
+
staleEntries.push(join(cacheDirectory, entry))
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (manifest.version === options.expectedVersion && packagePath === undefined) {
|
|
69
|
+
packagePath = candidatePath
|
|
70
|
+
actualVersion = manifest.version
|
|
71
|
+
} else if (manifest.version !== options.expectedVersion) {
|
|
72
|
+
staleEntries.push(join(cacheDirectory, entry))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (packagePath === undefined) {
|
|
77
|
+
errors.push(`Installed ${options.packageName}@${options.expectedVersion} was not found in ${cacheDirectory}`)
|
|
78
|
+
} else if (actualVersion !== options.expectedVersion) {
|
|
79
|
+
errors.push(`Installed version is ${actualVersion ?? "unknown"}; expected ${options.expectedVersion}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
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(", ")}`)
|
|
86
|
+
|
|
87
|
+
return { ok: errors.length === 0, packagePath, actualVersion, staleEntries, errors }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
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")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.argv[1] !== undefined && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
96
|
+
const packageManifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
|
|
97
|
+
name: string
|
|
98
|
+
version: string
|
|
99
|
+
}
|
|
100
|
+
const result = await verifyPluginInstallation({
|
|
101
|
+
packageName: packageManifest.name,
|
|
102
|
+
expectedVersion: packageManifest.version,
|
|
103
|
+
cacheDirectory: process.env.OPENCODE_CACHE_DIR,
|
|
104
|
+
})
|
|
105
|
+
console.log(formatVerificationResult(result))
|
|
106
|
+
if (!result.ok) process.exitCode = 1
|
|
107
|
+
}
|