1- import { readFileSync } from 'fs'
1+ import { readFileSync , writeFileSync , existsSync } from 'fs'
22import { join , dirname } from 'path'
33import { fileURLToPath } from 'url'
4- import { spawn } from 'child_process'
54import { homedir } from 'os'
65
76export const PACKAGE_NAME = '@tarquinen/opencode-dcp'
@@ -12,14 +11,12 @@ const __dirname = dirname(__filename)
1211
1312export function getLocalVersion ( ) : string {
1413 try {
15- // Walk up from the current module to find the project's package.json
16- // This works whether running from dist/lib/, lib/, or installed in node_modules
1714 let dir = __dirname
1815 for ( let i = 0 ; i < 5 ; i ++ ) {
1916 const pkgPath = join ( dir , 'package.json' )
2017 try {
2118 const pkg = JSON . parse ( readFileSync ( pkgPath , 'utf-8' ) )
22- if ( pkg . name === '@tarquinen/opencode-dcp' ) {
19+ if ( pkg . name === PACKAGE_NAME ) {
2320 return pkg . version
2421 }
2522 } catch {
@@ -65,75 +62,55 @@ export function isOutdated(local: string, remote: string): boolean {
6562 return false
6663}
6764
68- export async function performUpdate ( targetVersion : string , logger ?: { info : ( component : string , message : string , data ?: any ) => void } ) : Promise < boolean > {
69- const cacheDir = join ( homedir ( ) , '.cache' , 'opencode' )
70- const bunCacheDir = join ( homedir ( ) , '.bun' , 'install' , 'cache' , '@tarquinen' )
71- const packageSpec = `${ PACKAGE_NAME } @${ targetVersion } `
65+ /**
66+ * Updates config files to pin the new version.
67+ * Checks both global and local project configs.
68+ * Handles: "@tarquinen/opencode-dcp", "@tarquinen/opencode-dcp@latest", "@tarquinen/[email protected] " 69+ */
70+ export function updateConfigVersion ( newVersion : string , logger ?: { info : ( component : string , message : string , data ?: any ) => void } ) : boolean {
71+ const configs = [
72+ join ( homedir ( ) , '.config' , 'opencode' , 'opencode.jsonc' ) , // Global
73+ join ( process . cwd ( ) , '.opencode' , 'opencode.jsonc' ) // Local project
74+ ]
7275
73- logger ?. info ( "version" , "Starting auto-update" , { targetVersion , cacheDir } )
76+ let anyUpdated = false
7477
75- // Clear bun's install cache for this package to prevent stale versions
76- try {
77- const { rmSync } = await import ( 'fs' )
78- rmSync ( bunCacheDir , { recursive : true , force : true } )
79- logger ?. info ( "version" , "Cleared bun cache" , { bunCacheDir } )
80- } catch ( err ) {
81- logger ?. info ( "version" , "Could not clear bun cache" , { error : ( err as Error ) . message } )
82- }
83-
84- return new Promise ( ( resolve ) => {
85- let resolved = false
78+ for ( const configPath of configs ) {
79+ try {
80+ if ( ! existsSync ( configPath ) ) continue
8681
87- // Use bun since opencode uses bun to manage its plugin dependencies
88- const proc = spawn ( 'bun' , [ 'add' , packageSpec ] , {
89- cwd : cacheDir ,
90- stdio : 'pipe'
91- } )
82+ const content = readFileSync ( configPath , 'utf-8' )
9283
93- let stderr = ''
94- proc . stderr ?. on ( 'data' , ( data ) => {
95- stderr += data . toString ( )
96- } )
84+ // Match @tarquinen /opencode-dcp with optional version suffix (latest, 1.2.3, etc)
85+ // The regex matches: " @tarquinen/opencode-dcp (optional @anything) "
86+ const regex = new RegExp ( `" ${ PACKAGE_NAME } (@[^"]*)?"` , 'g' )
87+ const newEntry = `" ${ PACKAGE_NAME } @ ${ newVersion } "`
9788
98- proc . on ( 'close' , ( code ) => {
99- if ( resolved ) return
100- resolved = true
101- clearTimeout ( timeoutId )
102- if ( code === 0 ) {
103- logger ?. info ( "version" , "Auto-update succeeded" , { targetVersion } )
104- resolve ( true )
105- } else {
106- logger ?. info ( "version" , "Auto-update failed" , { targetVersion, code, stderr : stderr . slice ( 0 , 500 ) } )
107- resolve ( false )
89+ if ( ! regex . test ( content ) ) {
90+ continue
10891 }
109- } )
11092
111- proc . on ( 'error' , ( err ) => {
112- if ( resolved ) return
113- resolved = true
114- clearTimeout ( timeoutId )
115- logger ?. info ( "version" , "Auto-update error" , { targetVersion, error : err . message } )
116- resolve ( false )
117- } )
93+ // Reset regex state
94+ regex . lastIndex = 0
95+ const updatedContent = content . replace ( regex , newEntry )
96+
97+ if ( updatedContent !== content ) {
98+ writeFileSync ( configPath , updatedContent , 'utf-8' )
99+ logger ?. info ( "version" , "Config updated" , { configPath, newVersion } )
100+ anyUpdated = true
101+ }
102+ } catch ( err ) {
103+ logger ?. info ( "version" , "Failed to update config" , { configPath, error : ( err as Error ) . message } )
104+ }
105+ }
118106
119- // Timeout after 60 seconds
120- const timeoutId = setTimeout ( ( ) => {
121- if ( resolved ) return
122- resolved = true
123- proc . kill ( )
124- logger ?. info ( "version" , "Auto-update timed out" , { targetVersion } )
125- resolve ( false )
126- } , 60000 )
127- } )
107+ return anyUpdated
128108}
129109
130110export async function checkForUpdates (
131111 client : any ,
132- logger ?: { info : ( component : string , message : string , data ?: any ) => void } ,
133- options : { showToast ?: boolean ; autoUpdate ?: boolean } = { }
112+ logger ?: { info : ( component : string , message : string , data ?: any ) => void }
134113) : Promise < void > {
135- const { showToast = true , autoUpdate = false } = options
136-
137114 try {
138115 const local = getLocalVersion ( )
139116 const npm = await getNpmVersion ( )
@@ -148,41 +125,32 @@ export async function checkForUpdates(
148125 return
149126 }
150127
151- logger ?. info ( "version" , "Update available" , { local, npm, autoUpdate } )
152-
153- if ( autoUpdate ) {
154- // Attempt auto-update
155- const success = await performUpdate ( npm , logger )
156-
157- if ( success && showToast ) {
158- await client . tui . showToast ( {
159- body : {
160- title : "DCP: Updated!" ,
161- message : `v${ local } → v${ npm } \nRestart OpenCode to apply` ,
162- variant : "success" ,
163- duration : 6000
164- }
165- } )
166- } else if ( ! success && showToast ) {
167- await client . tui . showToast ( {
168- body : {
169- title : "DCP: Update failed" ,
170- message : `v${ local } → v${ npm } \nManual: npm install ${ PACKAGE_NAME } @${ npm } ` ,
171- variant : "warning" ,
172- duration : 6000
173- }
174- } )
175- }
176- } else if ( showToast ) {
128+ logger ?. info ( "version" , "Update available" , { local, npm } )
129+
130+ // Update any configs found
131+ const updated = updateConfigVersion ( npm , logger )
132+
133+ if ( updated ) {
177134 await client . tui . showToast ( {
178135 body : {
179136 title : "DCP: Update available" ,
180- message : `v${ local } → v${ npm } ` ,
137+ message : `v${ local } → v${ npm } \nRestart OpenCode to apply ` ,
181138 variant : "info" ,
182139 duration : 6000
183140 }
184141 } )
142+ } else {
143+ // Config update failed or plugin not found in config, show manual instructions
144+ await client . tui . showToast ( {
145+ body : {
146+ title : "DCP: Update available" ,
147+ message : `v${ local } → v${ npm } \nUpdate opencode.jsonc:\n"${ PACKAGE_NAME } @${ npm } "` ,
148+ variant : "info" ,
149+ duration : 8000
150+ }
151+ } )
185152 }
186153 } catch {
154+ // Silently fail version checks
187155 }
188156}
0 commit comments