From 987e66156f4b38072f7976fd3e88d87ac9e134ee Mon Sep 17 00:00:00 2001 From: Ricky Date: Sat, 23 May 2026 19:14:45 +0800 Subject: [PATCH 1/3] feat: add non-blocking auto-update to OpenCode plugin On OpenCode startup, the plugin now automatically runs `npx -y superpowers-zh` in the background (delayed 2s, timeout 120s) to keep skills up to date. - Non-blocking: config hook returns immediately, auto-update runs asynchronously - Error isolation: exec and showToast failures handled independently - User feedback: success/warning toast via client.tui.showToast - Zero new dependencies: uses only Node.js built-in child_process + util --- .opencode/plugins/superpowers.js | 41 ++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/.opencode/plugins/superpowers.js b/.opencode/plugins/superpowers.js index 52d1a37..8fe7ff8 100644 --- a/.opencode/plugins/superpowers.js +++ b/.opencode/plugins/superpowers.js @@ -1,15 +1,20 @@ /** * Superpowers plugin for OpenCode.ai * - * Injects superpowers bootstrap context via user message transform. - * Auto-registers skills directory via config hook (no symlinks needed). + * Features: + * 1. Injects superpowers bootstrap context via user message transform. + * 2. Auto-registers skills directory via config hook (no symlinks needed). + * 3. Auto-updates superpowers-zh skills on startup (non-blocking). */ import path from 'path'; import fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const execAsync = promisify(exec); // Simple frontmatter extraction (avoid dependency on skills-core for bootstrap) const extractAndStripFrontmatter = (content) => { @@ -70,11 +75,43 @@ ${toolMapping} // This works because Config.get() returns a cached singleton — modifications // here are visible when skills are lazily discovered later. config: async (config) => { + // ---- (1) Register skills path ---- config.skills = config.skills || {}; config.skills.paths = config.skills.paths || []; if (!config.skills.paths.includes(superpowersSkillsDir)) { config.skills.paths.push(superpowersSkillsDir); } + + // ---- (2) Non-blocking auto-update ---- + // Delay 2 seconds then run npx -y superpowers-zh in the background. + // All errors are caught silently — never blocks OpenCode startup. + setTimeout(async () => { + try { + await execAsync('npx -y superpowers-zh', { + cwd: directory, + timeout: 120000, + }); + // Notify success (toast failure does NOT affect update result) + client.tui.showToast({ + body: { + variant: 'success', + title: 'superpowers-zh', + message: '中文 Skills 更新完成', + duration: 3000, + }, + }).catch(() => {}); + } catch { + // Notify failure (toast failure does NOT throw uncaught exception) + client.tui.showToast({ + body: { + variant: 'warning', + title: 'superpowers-zh 更新', + message: '自动更新未成功,可手动执行 npx superpowers-zh', + duration: 5000, + }, + }).catch(() => {}); + } + }, 2000); }, // Inject bootstrap into the first user message of each session. From 22cd26a3eb4ec064e6cb80db984aa41d39032ff9 Mon Sep 17 00:00:00 2001 From: Ricky Date: Sat, 23 May 2026 19:30:39 +0800 Subject: [PATCH 2/3] feat: only toast on actual changes, silence npx output --- .opencode/plugins/superpowers.js | 39 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/.opencode/plugins/superpowers.js b/.opencode/plugins/superpowers.js index 8fe7ff8..c866c62 100644 --- a/.opencode/plugins/superpowers.js +++ b/.opencode/plugins/superpowers.js @@ -84,31 +84,34 @@ ${toolMapping} // ---- (2) Non-blocking auto-update ---- // Delay 2 seconds then run npx -y superpowers-zh in the background. - // All errors are caught silently — never blocks OpenCode startup. + // Toast only when skills actually changed or update failed. setTimeout(async () => { + // Snapshot skills mtime before update + const skillsDir = path.join(directory, '.opencode', 'skills'); + const getMtime = () => { + try { + const files = fs.readdirSync(skillsDir, { recursive: true }); + return Math.max(0, ...files.map(f => { + try { return fs.statSync(path.join(skillsDir, f)).mtimeMs; } catch { return 0; } + })); + } catch { return 0; } + }; + const before = getMtime(); + try { - await execAsync('npx -y superpowers-zh', { + await execAsync('npx -y superpowers-zh > /dev/null 2>&1', { cwd: directory, timeout: 120000, }); - // Notify success (toast failure does NOT affect update result) - client.tui.showToast({ - body: { - variant: 'success', - title: 'superpowers-zh', - message: '中文 Skills 更新完成', - duration: 3000, - }, - }).catch(() => {}); + // Only toast when skills were actually updated + if (getMtime() > before) { + client.tui.showToast({ + body: { variant: 'success', title: 'superpowers-zh', message: '中文 Skills 更新完成', duration: 3000 }, + }).catch(() => {}); + } } catch { - // Notify failure (toast failure does NOT throw uncaught exception) client.tui.showToast({ - body: { - variant: 'warning', - title: 'superpowers-zh 更新', - message: '自动更新未成功,可手动执行 npx superpowers-zh', - duration: 5000, - }, + body: { variant: 'warning', title: 'superpowers-zh 更新', message: '自动更新未成功,可手动执行 npx superpowers-zh', duration: 5000 }, }).catch(() => {}); } }, 2000); From 0c637fede6cd6572586ef211dfc8a20356e16208 Mon Sep 17 00:00:00 2001 From: Ricky Date: Sat, 23 May 2026 19:34:09 +0800 Subject: [PATCH 3/3] fix: use version check instead of mtime to detect updates --- .opencode/plugins/superpowers.js | 42 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/.opencode/plugins/superpowers.js b/.opencode/plugins/superpowers.js index c866c62..a6dc7c4 100644 --- a/.opencode/plugins/superpowers.js +++ b/.opencode/plugins/superpowers.js @@ -83,32 +83,36 @@ ${toolMapping} } // ---- (2) Non-blocking auto-update ---- - // Delay 2 seconds then run npx -y superpowers-zh in the background. - // Toast only when skills actually changed or update failed. + // Check remote version, only run npx when a newer version is available. + // Toast only on actual update or failure — silent when already latest. setTimeout(async () => { - // Snapshot skills mtime before update - const skillsDir = path.join(directory, '.opencode', 'skills'); - const getMtime = () => { - try { - const files = fs.readdirSync(skillsDir, { recursive: true }); - return Math.max(0, ...files.map(f => { - try { return fs.statSync(path.join(skillsDir, f)).mtimeMs; } catch { return 0; } - })); - } catch { return 0; } - }; - const before = getMtime(); + const versionFile = path.join(directory, '.opencode', '.superpowers-version'); + // Get remote latest version (silently skip on network error) + let remoteVer; + try { + const { stdout } = await execAsync('npm view superpowers-zh version', { timeout: 10000 }); + remoteVer = stdout.trim(); + } catch { + return; // Network unavailable, skip silently + } + if (!remoteVer) return; + + // Compare with locally recorded version + let localVer = ''; + try { localVer = fs.readFileSync(versionFile, 'utf8').trim(); } catch {} + if (remoteVer === localVer) return; // Already latest, silent + + // Run update try { await execAsync('npx -y superpowers-zh > /dev/null 2>&1', { cwd: directory, timeout: 120000, }); - // Only toast when skills were actually updated - if (getMtime() > before) { - client.tui.showToast({ - body: { variant: 'success', title: 'superpowers-zh', message: '中文 Skills 更新完成', duration: 3000 }, - }).catch(() => {}); - } + fs.writeFileSync(versionFile, remoteVer); + client.tui.showToast({ + body: { variant: 'success', title: 'superpowers-zh', message: '中文 Skills 更新完成', duration: 3000 }, + }).catch(() => {}); } catch { client.tui.showToast({ body: { variant: 'warning', title: 'superpowers-zh 更新', message: '自动更新未成功,可手动执行 npx superpowers-zh', duration: 5000 },