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 +30 -9
- package/dist/tui.js +31 -11
- 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.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:
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
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
|
+
}
|