opencode-system-metrics-tui 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Install globally in OpenCode
4
4
 
5
5
  ```powershell
6
- opencode plugin -g opencode-system-metrics-tui@0.1.12 --force
6
+ opencode plugin -g opencode-system-metrics-tui@0.1.13 --force
7
7
  ```
8
8
 
9
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:
@@ -17,6 +17,19 @@ npm run verify-installation
17
17
 
18
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
19
 
20
+ ### Uninstall safely
21
+
22
+ This project provides a safe helper; OpenCode does **not** provide a native uninstall command. It defaults to both observed global configuration roots (`~/.config/opencode` and `~/.opencode`), shows a plan, and requires `--yes` before changing anything. It removes only this plugin from OpenCode plugin arrays and verified cache entries. It never removes OpenCode, Node, Bun, shared dependencies, or repository files.
23
+
24
+ ```powershell
25
+ npm run uninstall-plugin -- --dry-run
26
+ npm run uninstall-plugin -- --yes
27
+ npm run uninstall-plugin -- --purge-cache --yes # also remove all verified stale versions
28
+ npm run uninstall-plugin -- --local # current project's .opencode config
29
+ ```
30
+
31
+ Use `--dry-run` first. Without `--yes`, no changes are made. An unversioned package entry removes all verified cache entries for this package; `--purge-cache` explicitly removes all verified entries even when no package entry is configured.
32
+
20
33
  OpenCode TUI plugin that adds live CPU, RAM, GPU, GPU VRAM, temperature, and network metrics to the `sidebar_content` slot.
21
34
 
22
35
  The README is available in **Español**, **English**, and **Português**.
@@ -63,6 +76,19 @@ opencode
63
76
 
64
77
  `npm run install-font` ejecuta el instalador compilado incluido en `dist/`, instala **Hack Nerd Font** únicamente para el usuario actual, no se ejecuta automáticamente durante `npm install` y solicita confirmación antes de descargar la fuente oficial.
65
78
 
79
+ ### Desinstalación segura
80
+
81
+ Este proyecto incluye un asistente seguro; OpenCode **no** tiene un comando nativo de desinstalación. Por defecto actúa sobre ambas raíces globales observadas (`~/.config/opencode` y `~/.opencode`), muestra un informe y requiere `--yes` antes de cambiar archivos. Solo elimina este paquete de las listas de plugins y sus entradas de caché verificadas.
82
+
83
+ ```bash
84
+ npm run uninstall-plugin -- --dry-run
85
+ npm run uninstall-plugin -- --yes
86
+ npm run uninstall-plugin -- --purge-cache --yes # también elimina versiones antiguas verificadas
87
+ npm run uninstall-plugin -- --local # configuración .opencode del proyecto actual
88
+ ```
89
+
90
+ Usa primero `--dry-run`. Sin `--yes` no se realizan cambios. Una entrada de paquete sin versión elimina todas las entradas de caché verificadas de este paquete; `--purge-cache` las elimina explícitamente.
91
+
66
92
  Comandos adicionales:
67
93
 
68
94
  ```bash
@@ -94,7 +120,7 @@ npm publish --access public
94
120
  En otro equipo:
95
121
 
96
122
  ```bash
97
- opencode plugin opencode-system-metrics-tui@0.1.12
123
+ opencode plugin opencode-system-metrics-tui@0.1.13
98
124
  ```
99
125
 
100
126
  El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
@@ -104,8 +130,8 @@ El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia Ope
104
130
  Publicar en npm e instalar en OpenCode son pasos distintos:
105
131
 
106
132
  - `npm publish --access public` publica el paquete en npm.
107
- - `opencode plugin opencode-system-metrics-tui@0.1.12` lo instala solo en el proyecto actual.
108
- - `opencode plugin -g opencode-system-metrics-tui@0.1.12 --force` lo instala globalmente para todos los proyectos.
133
+ - `opencode plugin opencode-system-metrics-tui@0.1.13` lo instala solo en el proyecto actual.
134
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.13 --force` lo instala globalmente para todos los proyectos.
109
135
 
110
136
  ### Verificación
111
137
 
@@ -161,6 +187,19 @@ opencode
161
187
 
162
188
  `npm run install-font` runs the compiled installer included in `dist/`, installs **Hack Nerd Font** for the current user only, is not executed automatically by `npm install`, and asks for confirmation before downloading the official font.
163
189
 
190
+ ### Safe uninstall
191
+
192
+ This project provides a safe helper; OpenCode does **not** provide a native uninstall command. It targets both observed global configuration roots (`~/.config/opencode` and `~/.opencode`) by default, prints a report, and requires `--yes` before changing anything. It removes only this package from plugin lists and its verified cache entries.
193
+
194
+ ```bash
195
+ npm run uninstall-plugin -- --dry-run
196
+ npm run uninstall-plugin -- --yes
197
+ npm run uninstall-plugin -- --purge-cache --yes # also remove all verified stale versions
198
+ npm run uninstall-plugin -- --local # current project's .opencode config
199
+ ```
200
+
201
+ Run `--dry-run` first. Without `--yes`, no changes are made. An unversioned package entry removes all verified cache entries for this package; `--purge-cache` explicitly removes all verified entries. Neither mode removes OpenCode, Node, Bun, shared dependencies, or repository files.
202
+
164
203
  Extra commands:
165
204
 
166
205
  ```bash
@@ -192,7 +231,7 @@ npm publish --access public
192
231
  On another machine:
193
232
 
194
233
  ```bash
195
- opencode plugin opencode-system-metrics-tui@0.1.12
234
+ opencode plugin opencode-system-metrics-tui@0.1.13
196
235
  ```
197
236
 
198
237
  The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
@@ -202,8 +241,8 @@ The OpenCode installer configures the TUI plugin automatically. Restart OpenCode
202
241
  Publishing to npm and installing in OpenCode are separate steps:
203
242
 
204
243
  - `npm publish --access public` publishes the package to npm.
205
- - `opencode plugin opencode-system-metrics-tui@0.1.12` installs it for the current project only.
206
- - `opencode plugin -g opencode-system-metrics-tui@0.1.12 --force` installs it globally for all projects.
244
+ - `opencode plugin opencode-system-metrics-tui@0.1.13` installs it for the current project only.
245
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.13 --force` installs it globally for all projects.
207
246
 
208
247
  ### Verification
209
248
 
@@ -256,6 +295,19 @@ opencode
256
295
 
257
296
  `npm run install-font` executa o instalador compilado incluído em `dist/`, instala a **Hack Nerd Font** somente para o usuário atual, não é executado automaticamente durante `npm install` e pede confirmação antes de baixar a fonte oficial.
258
297
 
298
+ ### Desinstalação segura
299
+
300
+ Este projeto inclui um assistente seguro; o OpenCode **não** oferece um comando nativo de desinstalação. Por padrão, ele usa ambas as raízes globais observadas (`~/.config/opencode` e `~/.opencode`), mostra um relatório e exige `--yes` antes de alterar arquivos. Remove apenas este pacote das listas de plugins e suas entradas de cache verificadas.
301
+
302
+ ```bash
303
+ npm run uninstall-plugin -- --dry-run
304
+ npm run uninstall-plugin -- --yes
305
+ npm run uninstall-plugin -- --purge-cache --yes # também remove versões antigas verificadas
306
+ npm run uninstall-plugin -- --local # configuração .opencode do projeto atual
307
+ ```
308
+
309
+ Execute primeiro com `--dry-run`. Sem `--yes`, nenhuma alteração é feita. Uma entrada de pacote sem versão remove todas as entradas de cache verificadas deste pacote; `--purge-cache` remove explicitamente todas as entradas verificadas.
310
+
259
311
  Comandos adicionais:
260
312
 
261
313
  ```bash
@@ -287,7 +339,7 @@ npm publish --access public
287
339
  Em outro computador:
288
340
 
289
341
  ```bash
290
- opencode plugin opencode-system-metrics-tui@0.1.12
342
+ opencode plugin opencode-system-metrics-tui@0.1.13
291
343
  ```
292
344
 
293
345
  O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
@@ -297,8 +349,8 @@ O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o Open
297
349
  Publicar no npm e instalar no OpenCode são etapas diferentes:
298
350
 
299
351
  - `npm publish --access public` publica o pacote no npm.
300
- - `opencode plugin opencode-system-metrics-tui@0.1.12` instala o plugin apenas no projeto atual.
301
- - `opencode plugin -g opencode-system-metrics-tui@0.1.12 --force` instala o plugin globalmente para todos os projetos.
352
+ - `opencode plugin opencode-system-metrics-tui@0.1.13` instala o plugin apenas no projeto atual.
353
+ - `opencode plugin -g opencode-system-metrics-tui@0.1.13 --force` instala o plugin globalmente para todos os projetos.
302
354
 
303
355
  ### Verificação
304
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-system-metrics-tui",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "OpenCode TUI sidebar plugin for live system metrics",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "typecheck": "tsc --noEmit",
26
26
  "install-font": "node dist/install-hack-font.js",
27
27
  "verify-installation": "tsx scripts/plugin-installation.ts",
28
- "install-plugin": "tsx scripts/install-plugin.ts"
28
+ "install-plugin": "tsx scripts/install-plugin.ts",
29
+ "uninstall-plugin": "tsx scripts/uninstall-plugin.ts"
29
30
  },
30
31
  "engines": {
31
32
  "node": ">=18"
@@ -0,0 +1,202 @@
1
+ import { lstat, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
2
+ import { homedir } from "node:os"
3
+ import { basename, isAbsolute, join, relative, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ export type UninstallScope = "global" | "local"
7
+
8
+ export type ConfigChange = {
9
+ path: string
10
+ removed: string[]
11
+ content: string
12
+ }
13
+
14
+ export type CacheCandidate = {
15
+ path: string
16
+ version: string
17
+ }
18
+
19
+ export type UninstallPlan = {
20
+ configChanges: ConfigChange[]
21
+ cacheCandidates: CacheCandidate[]
22
+ warnings: string[]
23
+ }
24
+
25
+ type PackageManifest = { name?: unknown; version?: unknown }
26
+
27
+ const packageManifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as { name: string }
28
+ const packageName = packageManifest.name
29
+
30
+ function isPackageSpec(value: unknown, name: string): value is string {
31
+ return typeof value === "string" && (value === name || value.startsWith(`${name}@`))
32
+ }
33
+
34
+ function filterPluginArrays(value: unknown, name: string, removed: string[]): unknown {
35
+ if (Array.isArray(value)) return value
36
+ if (value === null || typeof value !== "object") return value
37
+
38
+ const result: Record<string, unknown> = {}
39
+ for (const [key, child] of Object.entries(value)) {
40
+ if ((key === "plugin" || key === "plugins") && Array.isArray(child)) {
41
+ result[key] = child.filter((entry) => {
42
+ const spec = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : undefined
43
+ if (!isPackageSpec(spec, name)) return true
44
+ removed.push(spec)
45
+ return false
46
+ })
47
+ } else {
48
+ result[key] = filterPluginArrays(child, name, removed)
49
+ }
50
+ }
51
+ return result
52
+ }
53
+
54
+ export function filterPluginConfig(content: string, name = packageName): { content: string; removed: string[] } {
55
+ const parsed: unknown = JSON.parse(content)
56
+ const removed: string[] = []
57
+ const filtered = filterPluginArrays(parsed, name, removed)
58
+ const newline = content.includes("\r\n") ? "\r\n" : "\n"
59
+ const serialized = JSON.stringify(filtered, null, 2)
60
+ return { content: `${serialized.replaceAll("\n", newline)}${newline}`, removed }
61
+ }
62
+
63
+ export function getConfigPaths(scope: UninstallScope, cwd = process.cwd(), home = homedir(), env: NodeJS.ProcessEnv = process.env): string[] {
64
+ if (scope === "local") return [join(cwd, ".opencode", "tui.json"), join(cwd, ".opencode", "opencode.json"), join(cwd, "opencode.json")]
65
+ const configRoot = env.XDG_CONFIG_HOME ?? join(home, ".config")
66
+ const globalRoots = [join(configRoot, "opencode"), join(home, ".config", "opencode"), join(home, ".opencode")]
67
+ return [...new Set([
68
+ ...globalRoots.flatMap((root) => [join(root, "tui.json"), join(root, "opencode.json")]),
69
+ join(configRoot, "opencode.json"),
70
+ ])]
71
+ }
72
+
73
+ export function getCachePackagesRoot(home = homedir(), env: NodeJS.ProcessEnv = process.env): string {
74
+ const cacheRoot = env.OPENCODE_CACHE_DIR ?? env.XDG_CACHE_HOME ?? join(home, ".cache")
75
+ return join(cacheRoot, "opencode", "packages")
76
+ }
77
+
78
+ function isWithin(root: string, candidate: string): boolean {
79
+ const fromRoot = relative(resolve(root), resolve(candidate))
80
+ return fromRoot !== "" && !fromRoot.startsWith("..") && !isAbsolute(fromRoot)
81
+ }
82
+
83
+ async function readPackageManifest(path: string): Promise<PackageManifest | undefined> {
84
+ try {
85
+ return JSON.parse(await readFile(path, "utf8")) as PackageManifest
86
+ } catch {
87
+ return undefined
88
+ }
89
+ }
90
+
91
+ export async function findCacheCandidates(
92
+ cacheRoot: string,
93
+ name = packageName,
94
+ versions: Set<string> | undefined = undefined,
95
+ purgeCache = false,
96
+ ): Promise<{ candidates: CacheCandidate[]; warnings: string[] }> {
97
+ const root = resolve(cacheRoot)
98
+ const candidates: CacheCandidate[] = []
99
+ const warnings: string[] = []
100
+ let entries
101
+ try {
102
+ if ((await lstat(root)).isSymbolicLink()) throw new Error(`Unexpected symbolic link cache root: ${root}`)
103
+ entries = await readdir(root, { withFileTypes: true })
104
+ } catch (error) {
105
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { candidates, warnings }
106
+ if (error instanceof Error && error.message.startsWith("Unexpected symbolic link cache root:")) throw error
107
+ throw new Error(`Cannot inspect OpenCode cache root: ${root}`)
108
+ }
109
+
110
+ for (const entry of entries) {
111
+ if (!entry.isDirectory() || !entry.name.startsWith(`${name}@`)) continue
112
+ const candidatePath = join(root, entry.name)
113
+ if (!isWithin(root, candidatePath) || basename(candidatePath) !== entry.name) throw new Error(`Unexpected cache path: ${candidatePath}`)
114
+ const manifest = await readPackageManifest(join(candidatePath, "node_modules", name, "package.json"))
115
+ if (manifest?.name !== name || typeof manifest.version !== "string") {
116
+ warnings.push(`Skipped unverified cache entry: ${candidatePath}`)
117
+ continue
118
+ }
119
+ if (purgeCache || versions?.has(manifest.version)) candidates.push({ path: candidatePath, version: manifest.version })
120
+ }
121
+ candidates.sort((left, right) => left.path.localeCompare(right.path))
122
+ return { candidates, warnings }
123
+ }
124
+
125
+ export async function buildUninstallPlan(options: {
126
+ scope: UninstallScope
127
+ configPaths?: string[]
128
+ cacheRoot?: string
129
+ packageName?: string
130
+ purgeCache?: boolean
131
+ }): Promise<UninstallPlan> {
132
+ const name = options.packageName ?? packageName
133
+ const configChanges: ConfigChange[] = []
134
+ const versions = new Set<string>()
135
+ const warnings: string[] = []
136
+ let hasUnversionedPackage = false
137
+
138
+ for (const path of options.configPaths ?? getConfigPaths(options.scope)) {
139
+ let content: string
140
+ try {
141
+ if ((await lstat(path)).isSymbolicLink()) throw new Error(`Unexpected symbolic link config path: ${path}`)
142
+ content = await readFile(path, "utf8")
143
+ }
144
+ catch (error) {
145
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue
146
+ throw new Error(`Cannot read OpenCode config: ${path}`)
147
+ }
148
+ let filtered: { content: string; removed: string[] }
149
+ try { filtered = filterPluginConfig(content, name) }
150
+ catch { throw new Error(`Malformed OpenCode config: ${path}`) }
151
+ if (filtered.removed.length === 0) continue
152
+ for (const spec of filtered.removed) {
153
+ if (spec === name) hasUnversionedPackage = true
154
+ const version = spec.slice(name.length + 1)
155
+ if (version && version !== "latest") versions.add(version)
156
+ }
157
+ configChanges.push({ path, removed: filtered.removed, content: filtered.content })
158
+ }
159
+
160
+ const cache = await findCacheCandidates(options.cacheRoot ?? getCachePackagesRoot(), name, versions, options.purgeCache || hasUnversionedPackage)
161
+ return { configChanges, cacheCandidates: cache.candidates, warnings: [...warnings, ...cache.warnings] }
162
+ }
163
+
164
+ function printPlan(plan: UninstallPlan, dryRun: boolean): void {
165
+ console.log(dryRun ? "Uninstall dry run (no changes will be made):" : "Uninstall plan:")
166
+ for (const change of plan.configChanges) console.log(`Config ${dryRun ? "would remove" : "remove"} ${change.removed.join(", ")} from ${change.path}`)
167
+ for (const candidate of plan.cacheCandidates) console.log(`Cache ${dryRun ? "would remove" : "remove"} ${candidate.path} (${candidate.version})`)
168
+ if (plan.configChanges.length === 0 && plan.cacheCandidates.length === 0) console.log("No matching plugin configuration or verified cache entries found.")
169
+ for (const warning of plan.warnings) console.warn(`Warning: ${warning}`)
170
+ }
171
+
172
+ export async function uninstallPlugin(args = process.argv.slice(2)): Promise<void> {
173
+ const scope: UninstallScope = args.includes("--local") ? "local" : "global"
174
+ if (args.includes("--local") && args.includes("--global")) throw new Error("Choose only one scope: --local or --global")
175
+ const purgeCache = args.includes("--purge-cache")
176
+ const dryRun = args.includes("--dry-run") || args.includes("--report")
177
+ const yes = args.includes("--yes")
178
+ const plan = await buildUninstallPlan({ scope, purgeCache })
179
+ printPlan(plan, dryRun)
180
+ if (dryRun || (plan.configChanges.length === 0 && plan.cacheCandidates.length === 0)) return
181
+ if (!yes) {
182
+ console.log("Destructive uninstall requires --yes; no changes were made.")
183
+ return
184
+ }
185
+
186
+ for (const change of plan.configChanges) {
187
+ if (!isAbsolute(change.path) || (await lstat(change.path)).isSymbolicLink()) throw new Error(`Unexpected config path: ${change.path}`)
188
+ const mode = (await stat(change.path)).mode
189
+ await writeFile(change.path, change.content, { mode })
190
+ }
191
+ const cacheRoot = resolve(getCachePackagesRoot())
192
+ for (const candidate of plan.cacheCandidates) {
193
+ if (!isWithin(cacheRoot, candidate.path) || !basename(candidate.path).startsWith(`${packageName}@`) || (await lstat(candidate.path)).isSymbolicLink()) throw new Error(`Unexpected cache path: ${candidate.path}`)
194
+ await rm(candidate.path, { recursive: true, force: false })
195
+ }
196
+ console.log("Uninstall completed. Restart OpenCode if it was running.")
197
+ }
198
+
199
+ if (process.argv[1] !== undefined && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
200
+ try { await uninstallPlugin() }
201
+ catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1 }
202
+ }