opencode-system-metrics-tui 0.1.2 → 0.1.4

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
@@ -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.3 --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**.
@@ -72,7 +89,7 @@ npm publish --access public
72
89
  En otro equipo:
73
90
 
74
91
  ```bash
75
- opencode plugin opencode-system-metrics-tui
92
+ opencode plugin opencode-system-metrics-tui@0.1.3
76
93
  ```
77
94
 
78
95
  El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
@@ -82,12 +99,14 @@ El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia Ope
82
99
  Publicar en npm e instalar en OpenCode son pasos distintos:
83
100
 
84
101
  - `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.
102
+ - `opencode plugin opencode-system-metrics-tui@0.1.3` lo instala solo en el proyecto actual.
103
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.3 --force` lo instala globalmente para todos los proyectos.
87
104
 
88
105
  ### Verificación
89
106
 
90
107
  ```bash
108
+ npm run install-plugin
109
+ npm run verify-installation
91
110
  npm test
92
111
  npm run typecheck
93
112
  npm run build
@@ -161,7 +180,7 @@ npm publish --access public
161
180
  On another machine:
162
181
 
163
182
  ```bash
164
- opencode plugin opencode-system-metrics-tui
183
+ opencode plugin opencode-system-metrics-tui@0.1.3
165
184
  ```
166
185
 
167
186
  The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
@@ -171,12 +190,14 @@ The OpenCode installer configures the TUI plugin automatically. Restart OpenCode
171
190
  Publishing to npm and installing in OpenCode are separate steps:
172
191
 
173
192
  - `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.
193
+ - `opencode plugin opencode-system-metrics-tui@0.1.3` installs it for the current project only.
194
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.3 --force` installs it globally for all projects.
176
195
 
177
196
  ### Verification
178
197
 
179
198
  ```bash
199
+ npm run install-plugin
200
+ npm run verify-installation
180
201
  npm test
181
202
  npm run typecheck
182
203
  npm run build
@@ -250,7 +271,7 @@ npm publish --access public
250
271
  Em outro computador:
251
272
 
252
273
  ```bash
253
- opencode plugin opencode-system-metrics-tui
274
+ opencode plugin opencode-system-metrics-tui@0.1.3
254
275
  ```
255
276
 
256
277
  O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
@@ -260,8 +281,8 @@ O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o Open
260
281
  Publicar no npm e instalar no OpenCode são etapas diferentes:
261
282
 
262
283
  - `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.
284
+ - `opencode plugin opencode-system-metrics-tui@0.1.3` instala o plugin apenas no projeto atual.
285
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.3 --force` instala o plugin globalmente para todos os projetos.
265
286
 
266
287
  ### Verificação
267
288
 
package/dist/tui.js CHANGED
@@ -40,6 +40,7 @@ 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
44
  function parseMonitorTemperature(value) {
44
45
  if (value === undefined)
45
46
  return;
@@ -77,7 +78,7 @@ async function readWindowsNetworkSample() {
77
78
  "-NonInteractive",
78
79
  "-Command",
79
80
  "Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress"
80
- ], { windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024 });
81
+ ], { windowsHide: true, timeout: WINDOWS_NETWORK_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
81
82
  return parseWindowsNetworkOutput(stdout);
82
83
  }
83
84
  var previousNetwork;
@@ -114,6 +115,7 @@ async function withTimeout(promise, timeoutMs) {
114
115
  }
115
116
  var SLOW_METRIC_CACHE_MS = 1e4;
116
117
  var TEMPERATURE_CACHE_MS = 1e4;
118
+ var OPTIONAL_SOURCE_BACKOFF_MS = 30000;
117
119
  var isWindows = process.platform === "win32";
118
120
  var NETWORK_CACHE_MS = 2000;
119
121
  function selectGpuMetrics(graphics) {
@@ -154,16 +156,23 @@ function selectGpuMetrics(graphics) {
154
156
  }
155
157
  return result;
156
158
  }
157
- function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = Date.now()) {
159
+ function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = Date.now(), unavailableCacheMs = cacheMs) {
158
160
  if (state.inFlight !== undefined)
159
161
  return state.inFlight;
162
+ if (state.lastFailureAt !== undefined && now - state.lastFailureAt < unavailableCacheMs) {
163
+ return Promise.resolve(state.value);
164
+ }
160
165
  if (now - state.lastAttemptAt < cacheMs)
161
166
  return Promise.resolve(state.value);
162
167
  state.lastAttemptAt = now;
163
168
  state.inFlight = reader().then((value) => {
164
169
  state.value = value;
170
+ state.lastFailureAt = undefined;
165
171
  return value;
166
- }).catch(() => state.value).finally(() => {
172
+ }).catch(() => {
173
+ state.lastFailureAt = now;
174
+ return state.value;
175
+ }).finally(() => {
167
176
  state.inFlight = undefined;
168
177
  });
169
178
  return state.inFlight;
@@ -173,6 +182,11 @@ var cachedCpuTemperature = {
173
182
  lastAttemptAt: 0,
174
183
  inFlight: undefined
175
184
  };
185
+ var cachedLibreHardwareMonitorTemperature = {
186
+ value: undefined,
187
+ lastAttemptAt: 0,
188
+ inFlight: undefined
189
+ };
176
190
  var cachedGraphics = {
177
191
  value: undefined,
178
192
  lastAttemptAt: 0,
@@ -200,15 +214,21 @@ async function readWindowsNetworkMetrics() {
200
214
  return result;
201
215
  }
202
216
  async function readWindowsCpuTemperature() {
203
- let value;
204
217
  try {
205
- value = await readLibreHardwareMonitorTemperature();
218
+ const native = await si.cpuTemperature();
219
+ const value = finite(native.main) ?? finite(native.max) ?? undefined;
220
+ if (value !== undefined)
221
+ return { main: value };
206
222
  } catch {}
207
- if (value === undefined) {
208
- try {
209
- const fallback = await si.cpuTemperature();
210
- value = finite(fallback.main) ?? finite(fallback.max) ?? undefined;
211
- } catch {}
223
+ const now = Date.now();
224
+ const value = await readCachedMetric(cachedLibreHardwareMonitorTemperature, async () => {
225
+ const temperature = await readLibreHardwareMonitorTemperature();
226
+ if (temperature === undefined)
227
+ throw new Error("LibreHardwareMonitor temperature unavailable");
228
+ return temperature;
229
+ }, TEMPERATURE_CACHE_MS, now, OPTIONAL_SOURCE_BACKOFF_MS);
230
+ if (cachedLibreHardwareMonitorTemperature.lastFailureAt !== undefined && now - cachedLibreHardwareMonitorTemperature.lastFailureAt < OPTIONAL_SOURCE_BACKOFF_MS) {
231
+ return;
212
232
  }
213
233
  return value === undefined ? undefined : { main: value };
214
234
  }
@@ -220,7 +240,7 @@ async function readMetrics() {
220
240
  const [temperature, graphics, network] = await Promise.allSettled([
221
241
  withTimeout(readCachedMetric(cachedCpuTemperature, isWindows ? readWindowsCpuTemperature : () => si.cpuTemperature(), TEMPERATURE_CACHE_MS), 2500),
222
242
  withTimeout(readCachedMetric(cachedGraphics, () => si.graphics(), SLOW_METRIC_CACHE_MS), 2500),
223
- withTimeout(readCachedMetric(cachedNetwork, isWindows ? readWindowsNetworkMetrics : () => si.networkStats("*"), isWindows ? NETWORK_CACHE_MS : 2000), 1500)
243
+ withTimeout(readCachedMetric(cachedNetwork, isWindows ? readWindowsNetworkMetrics : () => si.networkStats("*"), isWindows ? NETWORK_CACHE_MS : 2000, Date.now(), OPTIONAL_SOURCE_BACKOFF_MS), 1500)
224
244
  ]);
225
245
  const cpuPercent = load.status === "fulfilled" ? finite(load.value.currentLoad) : null;
226
246
  const memoryTotalBytes = memory.status === "fulfilled" ? finite(memory.value.total) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-system-metrics-tui",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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
+ }