Skip to content

Commit 132e0bb

Browse files
committed
Add auto-update feature
1 parent 64b6510 commit 132e0bb

File tree

4 files changed

+107
-15
lines changed

4 files changed

+107
-15
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Add to your OpenCode config:
1313
```jsonc
1414
// opencode.jsonc
1515
{
16-
"plugin": ["@tarquinen/opencode-dcp@0.4.2"],
16+
"plugin": ["@tarquinen/opencode-dcp"],
1717
"experimental": {
1818
"primary_tools": ["prune"]
1919
}
@@ -22,7 +22,7 @@ Add to your OpenCode config:
2222

2323
The `experimental.primary_tools` setting ensures the `prune` tool is only available to the primary agent (not subagents).
2424

25-
When a new version is available, DCP will show a toast notification. Update by changing the version number in your config.
25+
DCP automatically updates itself in the background when new versions are available. You'll see a toast notification when an update is downloaded—just restart OpenCode to apply it. To disable auto-updates, set `"autoUpdate": false` in your DCP config.
2626

2727
Restart OpenCode. The plugin will automatically start optimizing your sessions.
2828

@@ -63,6 +63,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
6363
| `model` | (session) | Model for analysis (e.g., `"anthropic/claude-haiku-4-5"`) |
6464
| `showModelErrorToasts` | `true` | Show notifications on model fallback |
6565
| `showUpdateToasts` | `true` | Show notifications when a new version is available |
66+
| `autoUpdate` | `true` | Automatically download new versions (restart to apply) |
6667
| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) |
6768
| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
6869
| `nudge_freq` | `10` | How often to remind AI to prune (lower = more frequent) |

index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ const plugin: Plugin = (async (ctx) => {
5353

5454
// Check for updates after a delay
5555
setTimeout(() => {
56-
checkForUpdates(ctx.client, logger, config.showUpdateToasts ?? true).catch(() => { })
56+
checkForUpdates(ctx.client, logger, {
57+
showToast: config.showUpdateToasts ?? true,
58+
autoUpdate: config.autoUpdate ?? true
59+
}).catch(() => { })
5760
}, 5000)
5861

5962
// Show migration toast if there were config migrations

lib/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface PluginConfig {
1414
model?: string
1515
showModelErrorToasts?: boolean
1616
showUpdateToasts?: boolean
17+
autoUpdate?: boolean
1718
strictModelSelection?: boolean
1819
pruning_summary: "off" | "minimal" | "detailed"
1920
nudge_freq: number
@@ -34,6 +35,7 @@ const defaultConfig: PluginConfig = {
3435
protectedTools: ['task', 'todowrite', 'todoread', 'prune', 'batch', 'edit', 'write'],
3536
showModelErrorToasts: true,
3637
showUpdateToasts: true,
38+
autoUpdate: true,
3739
strictModelSelection: false,
3840
pruning_summary: 'detailed',
3941
nudge_freq: 10,
@@ -50,6 +52,7 @@ const VALID_CONFIG_KEYS = new Set([
5052
'model',
5153
'showModelErrorToasts',
5254
'showUpdateToasts',
55+
'autoUpdate',
5356
'strictModelSelection',
5457
'pruning_summary',
5558
'nudge_freq',
@@ -115,6 +118,8 @@ function createDefaultConfig(): void {
115118
"showModelErrorToasts": true,
116119
// Show toast notifications when a new version is available
117120
"showUpdateToasts": true,
121+
// Automatically update to new versions (restart required to apply)
122+
"autoUpdate": true,
118123
// Only run AI analysis with session model or configured model (disables fallback models)
119124
"strictModelSelection": false,
120125
// AI analysis strategies (deduplication runs automatically on every request)
@@ -205,6 +210,7 @@ export function getConfig(ctx?: PluginInput): ConfigResult {
205210
model: globalConfig.model ?? config.model,
206211
showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts,
207212
showUpdateToasts: globalConfig.showUpdateToasts ?? config.showUpdateToasts,
213+
autoUpdate: globalConfig.autoUpdate ?? config.autoUpdate,
208214
strictModelSelection: globalConfig.strictModelSelection ?? config.strictModelSelection,
209215
strategies: mergeStrategies(config.strategies, globalConfig.strategies as any),
210216
pruning_summary: globalConfig.pruning_summary ?? config.pruning_summary,
@@ -237,6 +243,7 @@ export function getConfig(ctx?: PluginInput): ConfigResult {
237243
model: projectConfig.model ?? config.model,
238244
showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts,
239245
showUpdateToasts: projectConfig.showUpdateToasts ?? config.showUpdateToasts,
246+
autoUpdate: projectConfig.autoUpdate ?? config.autoUpdate,
240247
strictModelSelection: projectConfig.strictModelSelection ?? config.strictModelSelection,
241248
strategies: mergeStrategies(config.strategies, projectConfig.strategies as any),
242249
pruning_summary: projectConfig.pruning_summary ?? config.pruning_summary,

lib/version-checker.ts

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { readFileSync } from 'fs'
22
import { join, dirname } from 'path'
33
import { fileURLToPath } from 'url'
4+
import { spawn } from 'child_process'
5+
import { homedir } from 'os'
46

57
export const PACKAGE_NAME = '@tarquinen/opencode-dcp'
68
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
@@ -50,7 +52,65 @@ export function isOutdated(local: string, remote: string): boolean {
5052
return false
5153
}
5254

53-
export async function checkForUpdates(client: any, logger?: { info: (component: string, message: string, data?: any) => void }, showToast: boolean = true): Promise<void> {
55+
export async function performUpdate(targetVersion: string, logger?: { info: (component: string, message: string, data?: any) => void }): Promise<boolean> {
56+
// OpenCode installs packages to ~/.cache/opencode/node_modules/
57+
const cacheDir = join(homedir(), '.cache', 'opencode')
58+
const packageSpec = `${PACKAGE_NAME}@${targetVersion}`
59+
60+
logger?.info("version", "Starting auto-update", { targetVersion, cacheDir })
61+
62+
return new Promise((resolve) => {
63+
let resolved = false
64+
65+
const proc = spawn('npm', ['install', '--legacy-peer-deps', packageSpec], {
66+
cwd: cacheDir,
67+
stdio: 'pipe'
68+
})
69+
70+
let stderr = ''
71+
proc.stderr?.on('data', (data) => {
72+
stderr += data.toString()
73+
})
74+
75+
proc.on('close', (code) => {
76+
if (resolved) return
77+
resolved = true
78+
clearTimeout(timeoutId)
79+
if (code === 0) {
80+
logger?.info("version", "Auto-update succeeded", { targetVersion })
81+
resolve(true)
82+
} else {
83+
logger?.info("version", "Auto-update failed", { targetVersion, code, stderr: stderr.slice(0, 500) })
84+
resolve(false)
85+
}
86+
})
87+
88+
proc.on('error', (err) => {
89+
if (resolved) return
90+
resolved = true
91+
clearTimeout(timeoutId)
92+
logger?.info("version", "Auto-update error", { targetVersion, error: err.message })
93+
resolve(false)
94+
})
95+
96+
// Timeout after 60 seconds
97+
const timeoutId = setTimeout(() => {
98+
if (resolved) return
99+
resolved = true
100+
proc.kill()
101+
logger?.info("version", "Auto-update timed out", { targetVersion })
102+
resolve(false)
103+
}, 60000)
104+
})
105+
}
106+
107+
export async function checkForUpdates(
108+
client: any,
109+
logger?: { info: (component: string, message: string, data?: any) => void },
110+
options: { showToast?: boolean; autoUpdate?: boolean } = {}
111+
): Promise<void> {
112+
const { showToast = true, autoUpdate = false } = options
113+
54114
try {
55115
const local = getLocalVersion()
56116
const npm = await getNpmVersion()
@@ -65,20 +125,41 @@ export async function checkForUpdates(client: any, logger?: { info: (component:
65125
return
66126
}
67127

68-
logger?.info("version", "Update available", { local, npm })
128+
logger?.info("version", "Update available", { local, npm, autoUpdate })
69129

70-
if (!showToast) {
71-
return
72-
}
130+
if (autoUpdate) {
131+
// Attempt auto-update
132+
const success = await performUpdate(npm, logger)
73133

74-
await client.tui.showToast({
75-
body: {
76-
title: "DCP: Update available",
77-
message: `v${local} → v${npm}\nUpdate opencode.jsonc: ${PACKAGE_NAME}@${npm}`,
78-
variant: "info",
79-
duration: 6000
134+
if (success && showToast) {
135+
await client.tui.showToast({
136+
body: {
137+
title: "DCP: Updated!",
138+
message: `v${local} → v${npm}\nRestart OpenCode to apply`,
139+
variant: "success",
140+
duration: 6000
141+
}
142+
})
143+
} else if (!success && showToast) {
144+
await client.tui.showToast({
145+
body: {
146+
title: "DCP: Update failed",
147+
message: `v${local} → v${npm}\nManual: npm install ${PACKAGE_NAME}@${npm}`,
148+
variant: "warning",
149+
duration: 6000
150+
}
151+
})
80152
}
81-
})
153+
} else if (showToast) {
154+
await client.tui.showToast({
155+
body: {
156+
title: "DCP: Update available",
157+
message: `v${local} → v${npm}`,
158+
variant: "info",
159+
duration: 6000
160+
}
161+
})
162+
}
82163
} catch {
83164
}
84165
}

0 commit comments

Comments
 (0)