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 +15 -11
- package/dist/install-hack-font.js +4 -4
- package/dist/tui.js +124 -38
- package/package.json +1 -1
- package/scripts/install-plugin.ts +33 -17
- package/scripts/plugin-installation.ts +28 -13
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
|
+
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.
|
|
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.
|
|
105
|
-
- `opencode plugin -g opencode-system-metrics-tui@0.1.
|
|
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.
|
|
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.
|
|
198
|
-
- `opencode plugin -g opencode-system-metrics-tui@0.1.
|
|
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.
|
|
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.
|
|
291
|
-
- `opencode plugin -g opencode-system-metrics-tui@0.1.
|
|
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 {
|
|
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 [
|
|
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 [
|
|
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) :
|
|
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
|
|
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:
|
|
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(),
|
|
254
|
-
withTimeout(si.mem(),
|
|
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,
|
|
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 {
|
|
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 [
|
|
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 [
|
|
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) :
|
|
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$
|
|
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$
|
|
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$
|
|
568
|
+
_$insertNode(_el$24, _el$29);
|
|
485
569
|
_$setProp(_el$24, "flexDirection", "row");
|
|
486
570
|
_$insert(_el$25, () => icons.vram);
|
|
487
|
-
_$insertNode(_el$26,
|
|
488
|
-
_$insertNode(_el$
|
|
489
|
-
_$
|
|
490
|
-
_$insertNode(_el$
|
|
491
|
-
_$
|
|
492
|
-
_$
|
|
493
|
-
_$insert(_el$
|
|
494
|
-
_$
|
|
495
|
-
_$
|
|
496
|
-
_$insertNode(_el$
|
|
497
|
-
_$
|
|
498
|
-
_$
|
|
499
|
-
_$
|
|
500
|
-
_$
|
|
501
|
-
_$insertNode(_el$
|
|
502
|
-
_$
|
|
503
|
-
_$
|
|
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$
|
|
520
|
-
_v$13 !== _p$.m && (_p$.m = _$setProp(_el$
|
|
521
|
-
_v$14 !== _p$.f && (_p$.f = _$setProp(_el$
|
|
522
|
-
_v$15 !== _p$.y && (_p$.y = _$setProp(_el$
|
|
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,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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
const
|
|
20
|
-
console.
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 (
|
|
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 (
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
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
|
-
|
|
99
|
+
export function shouldInstallPlugin(result: PluginVerification): boolean {
|
|
100
|
+
return !result.ok
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
export function formatVerificationResult(result: PluginVerification): string {
|
|
91
|
-
|
|
92
|
-
|
|
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))) {
|