diff --git a/index.ts b/index.ts index 12702ba..b8d8083 100644 --- a/index.ts +++ b/index.ts @@ -53,10 +53,7 @@ const plugin: Plugin = (async (ctx) => { // Check for updates after a delay setTimeout(() => { - checkForUpdates(ctx.client, logger, { - showToast: config.showUpdateToasts ?? true, - autoUpdate: config.autoUpdate ?? true - }).catch(() => { }) + checkForUpdates(ctx.client, logger).catch(() => { }) }, 5000) // Show migration toast if there were config migrations diff --git a/lib/config.ts b/lib/config.ts index 6f8f4a6..73e7060 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -13,8 +13,6 @@ export interface PluginConfig { protectedTools: string[] model?: string showModelErrorToasts?: boolean - showUpdateToasts?: boolean - autoUpdate?: boolean strictModelSelection?: boolean pruning_summary: "off" | "minimal" | "detailed" nudge_freq: number @@ -34,8 +32,6 @@ const defaultConfig: PluginConfig = { debug: false, protectedTools: ['task', 'todowrite', 'todoread', 'prune', 'batch', 'edit', 'write'], showModelErrorToasts: true, - showUpdateToasts: true, - autoUpdate: true, strictModelSelection: false, pruning_summary: 'detailed', nudge_freq: 10, @@ -51,8 +47,6 @@ const VALID_CONFIG_KEYS = new Set([ 'protectedTools', 'model', 'showModelErrorToasts', - 'showUpdateToasts', - 'autoUpdate', 'strictModelSelection', 'pruning_summary', 'nudge_freq', @@ -116,10 +110,6 @@ function createDefaultConfig(): void { // "model": "anthropic/claude-haiku-4-5", // Show toast notifications when model selection fails "showModelErrorToasts": true, - // Show toast notifications when a new version is available - "showUpdateToasts": true, - // Automatically update to new versions (restart required to apply) - "autoUpdate": true, // Only run AI analysis with session model or configured model (disables fallback models) "strictModelSelection": false, // AI analysis strategies (deduplication runs automatically on every request) @@ -209,8 +199,6 @@ export function getConfig(ctx?: PluginInput): ConfigResult { protectedTools: [...new Set([...config.protectedTools, ...(globalConfig.protectedTools ?? [])])], model: globalConfig.model ?? config.model, showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts, - showUpdateToasts: globalConfig.showUpdateToasts ?? config.showUpdateToasts, - autoUpdate: globalConfig.autoUpdate ?? config.autoUpdate, strictModelSelection: globalConfig.strictModelSelection ?? config.strictModelSelection, strategies: mergeStrategies(config.strategies, globalConfig.strategies as any), pruning_summary: globalConfig.pruning_summary ?? config.pruning_summary, @@ -242,8 +230,6 @@ export function getConfig(ctx?: PluginInput): ConfigResult { protectedTools: [...new Set([...config.protectedTools, ...(projectConfig.protectedTools ?? [])])], model: projectConfig.model ?? config.model, showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts, - showUpdateToasts: projectConfig.showUpdateToasts ?? config.showUpdateToasts, - autoUpdate: projectConfig.autoUpdate ?? config.autoUpdate, strictModelSelection: projectConfig.strictModelSelection ?? config.strictModelSelection, strategies: mergeStrategies(config.strategies, projectConfig.strategies as any), pruning_summary: projectConfig.pruning_summary ?? config.pruning_summary, diff --git a/lib/version-checker.ts b/lib/version-checker.ts index 642edef..191a85c 100644 --- a/lib/version-checker.ts +++ b/lib/version-checker.ts @@ -1,7 +1,6 @@ -import { readFileSync } from 'fs' +import { readFileSync, writeFileSync, existsSync } from 'fs' import { join, dirname } from 'path' import { fileURLToPath } from 'url' -import { spawn } from 'child_process' import { homedir } from 'os' export const PACKAGE_NAME = '@tarquinen/opencode-dcp' @@ -12,14 +11,12 @@ const __dirname = dirname(__filename) export function getLocalVersion(): string { try { - // Walk up from the current module to find the project's package.json - // This works whether running from dist/lib/, lib/, or installed in node_modules let dir = __dirname for (let i = 0; i < 5; i++) { const pkgPath = join(dir, 'package.json') try { const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) - if (pkg.name === '@tarquinen/opencode-dcp') { + if (pkg.name === PACKAGE_NAME) { return pkg.version } } catch { @@ -65,75 +62,55 @@ export function isOutdated(local: string, remote: string): boolean { return false } -export async function performUpdate(targetVersion: string, logger?: { info: (component: string, message: string, data?: any) => void }): Promise { - const cacheDir = join(homedir(), '.cache', 'opencode') - const bunCacheDir = join(homedir(), '.bun', 'install', 'cache', '@tarquinen') - const packageSpec = `${PACKAGE_NAME}@${targetVersion}` +/** + * Updates config files to pin the new version. + * Checks both global and local project configs. + * Handles: "@tarquinen/opencode-dcp", "@tarquinen/opencode-dcp@latest", "@tarquinen/opencode-dcp@1.2.3" + */ +export function updateConfigVersion(newVersion: string, logger?: { info: (component: string, message: string, data?: any) => void }): boolean { + const configs = [ + join(homedir(), '.config', 'opencode', 'opencode.jsonc'), // Global + join(process.cwd(), '.opencode', 'opencode.jsonc') // Local project + ] - logger?.info("version", "Starting auto-update", { targetVersion, cacheDir }) + let anyUpdated = false - // Clear bun's install cache for this package to prevent stale versions - try { - const { rmSync } = await import('fs') - rmSync(bunCacheDir, { recursive: true, force: true }) - logger?.info("version", "Cleared bun cache", { bunCacheDir }) - } catch (err) { - logger?.info("version", "Could not clear bun cache", { error: (err as Error).message }) - } - - return new Promise((resolve) => { - let resolved = false + for (const configPath of configs) { + try { + if (!existsSync(configPath)) continue - // Use bun since opencode uses bun to manage its plugin dependencies - const proc = spawn('bun', ['add', packageSpec], { - cwd: cacheDir, - stdio: 'pipe' - }) + const content = readFileSync(configPath, 'utf-8') - let stderr = '' - proc.stderr?.on('data', (data) => { - stderr += data.toString() - }) + // Match @tarquinen/opencode-dcp with optional version suffix (latest, 1.2.3, etc) + // The regex matches: " @tarquinen/opencode-dcp (optional @anything) " + const regex = new RegExp(`"${PACKAGE_NAME}(@[^"]*)?"`,'g') + const newEntry = `"${PACKAGE_NAME}@${newVersion}"` - proc.on('close', (code) => { - if (resolved) return - resolved = true - clearTimeout(timeoutId) - if (code === 0) { - logger?.info("version", "Auto-update succeeded", { targetVersion }) - resolve(true) - } else { - logger?.info("version", "Auto-update failed", { targetVersion, code, stderr: stderr.slice(0, 500) }) - resolve(false) + if (!regex.test(content)) { + continue } - }) - proc.on('error', (err) => { - if (resolved) return - resolved = true - clearTimeout(timeoutId) - logger?.info("version", "Auto-update error", { targetVersion, error: err.message }) - resolve(false) - }) + // Reset regex state + regex.lastIndex = 0 + const updatedContent = content.replace(regex, newEntry) + + if (updatedContent !== content) { + writeFileSync(configPath, updatedContent, 'utf-8') + logger?.info("version", "Config updated", { configPath, newVersion }) + anyUpdated = true + } + } catch (err) { + logger?.info("version", "Failed to update config", { configPath, error: (err as Error).message }) + } + } - // Timeout after 60 seconds - const timeoutId = setTimeout(() => { - if (resolved) return - resolved = true - proc.kill() - logger?.info("version", "Auto-update timed out", { targetVersion }) - resolve(false) - }, 60000) - }) + return anyUpdated } export async function checkForUpdates( client: any, - logger?: { info: (component: string, message: string, data?: any) => void }, - options: { showToast?: boolean; autoUpdate?: boolean } = {} + logger?: { info: (component: string, message: string, data?: any) => void } ): Promise { - const { showToast = true, autoUpdate = false } = options - try { const local = getLocalVersion() const npm = await getNpmVersion() @@ -148,41 +125,32 @@ export async function checkForUpdates( return } - logger?.info("version", "Update available", { local, npm, autoUpdate }) - - if (autoUpdate) { - // Attempt auto-update - const success = await performUpdate(npm, logger) - - if (success && showToast) { - await client.tui.showToast({ - body: { - title: "DCP: Updated!", - message: `v${local} → v${npm}\nRestart OpenCode to apply`, - variant: "success", - duration: 6000 - } - }) - } else if (!success && showToast) { - await client.tui.showToast({ - body: { - title: "DCP: Update failed", - message: `v${local} → v${npm}\nManual: npm install ${PACKAGE_NAME}@${npm}`, - variant: "warning", - duration: 6000 - } - }) - } - } else if (showToast) { + logger?.info("version", "Update available", { local, npm }) + + // Update any configs found + const updated = updateConfigVersion(npm, logger) + + if (updated) { await client.tui.showToast({ body: { title: "DCP: Update available", - message: `v${local} → v${npm}`, + message: `v${local} → v${npm}\nRestart OpenCode to apply`, variant: "info", duration: 6000 } }) + } else { + // Config update failed or plugin not found in config, show manual instructions + await client.tui.showToast({ + body: { + title: "DCP: Update available", + message: `v${local} → v${npm}\nUpdate opencode.jsonc:\n"${PACKAGE_NAME}@${npm}"`, + variant: "info", + duration: 8000 + } + }) } } catch { + // Silently fail version checks } } diff --git a/package-lock.json b/package-lock.json index 2341bd4..218e35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.4.8", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.4.8", + "version": "0.4.9", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.28", diff --git a/package.json b/package.json index 8cedf2f..709930f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "0.4.8", + "version": "0.4.9", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js",