@@ -12,7 +12,7 @@ import {
1212} from '../types/settings'
1313import { logger } from '../utils/logger'
1414import { opencodeServerManager } from '../services/opencode-single-server'
15- import { DEFAULT_AGENTS_MD } from '../index '
15+ import { DEFAULT_AGENTS_MD } from '../constants '
1616
1717function compareVersions ( v1 : string , v2 : string ) : number {
1818 const parts1 = v1 . split ( '.' ) . map ( s => Number ( s ) )
@@ -27,6 +27,22 @@ function compareVersions(v1: string, v2: string): number {
2727 return 0
2828}
2929
30+ function execWithTimeout ( command : string , timeoutMs : number ) : { output : string ; timedOut : boolean } {
31+ try {
32+ const output = execSync ( command , {
33+ encoding : 'utf8' ,
34+ timeout : timeoutMs ,
35+ killSignal : 'SIGKILL'
36+ } )
37+ return { output, timedOut : false }
38+ } catch ( error ) {
39+ if ( error && typeof error === 'object' && 'status' in error && ( error as { status : number } ) . status === null ) {
40+ return { output : '' , timedOut : true }
41+ }
42+ throw error
43+ }
44+ }
45+
3046const UpdateSettingsSchema = z . object ( {
3147 preferences : UserPreferencesSchema . partial ( ) ,
3248} )
@@ -376,20 +392,20 @@ export function createSettingsRoutes(db: Database) {
376392 } )
377393
378394 app . post ( '/opencode-upgrade' , async ( c ) => {
379- try {
380- logger . info ( 'OpenCode upgrade requested' )
381-
382- const oldVersion = opencodeServerManager . getVersion ( )
383- logger . info ( `Current OpenCode version: ${ oldVersion } ` )
395+ const oldVersion = opencodeServerManager . getVersion ( )
396+ logger . info ( `Current OpenCode version: ${ oldVersion } ` )
384397
385- logger . info ( 'Running opencode upgrade...' )
386- const upgradeOutput = execSync ( 'opencode upgrade 2>&1' , { encoding : 'utf8' } )
398+ try {
399+ logger . info ( 'Running opencode upgrade with 90s timeout...' )
400+ const { output : upgradeOutput , timedOut } = execWithTimeout ( 'opencode upgrade 2>&1' , 90000 )
387401 logger . info ( `Upgrade output: ${ upgradeOutput } ` )
388402
389- await new Promise ( r => setTimeout ( r , 2000 ) )
403+ if ( timedOut ) {
404+ logger . warn ( 'OpenCode upgrade timed out after 90 seconds' )
405+ throw new Error ( 'Upgrade command timed out after 90 seconds' )
406+ }
390407
391408 const newVersion = opencodeServerManager . getVersion ( ) || await opencodeServerManager . fetchVersion ( )
392-
393409 logger . info ( `New OpenCode version: ${ newVersion } ` )
394410
395411 const upgraded = oldVersion && newVersion && compareVersions ( newVersion , oldVersion ) > 0
@@ -419,16 +435,58 @@ export function createSettingsRoutes(db: Database) {
419435 success : true ,
420436 message : 'OpenCode is already up to date' ,
421437 oldVersion,
422- newVersion : oldVersion ,
438+ newVersion,
423439 upgraded : false
424440 } )
425441 }
426442 } catch ( error ) {
427443 logger . error ( 'Failed to upgrade OpenCode:' , error )
428- return c . json ( {
429- error : 'Failed to upgrade OpenCode' ,
430- details : error instanceof Error ? error . message : 'Unknown error'
431- } , 500 )
444+ logger . warn ( 'Attempting to recover OpenCode server...' )
445+
446+ let recovered = false
447+ let recoveryMessage = ''
448+
449+ opencodeServerManager . clearStartupError ( )
450+ try {
451+ await opencodeServerManager . restart ( )
452+ logger . warn ( 'OpenCode server restarted after upgrade failure' )
453+ recovered = true
454+ recoveryMessage = 'Server recovered'
455+ } catch ( recoveryError ) {
456+ logger . error ( 'Failed to recover OpenCode server:' , recoveryError )
457+ recovered = false
458+ recoveryMessage = recoveryError instanceof Error ? recoveryError . message : 'Unknown error'
459+ }
460+
461+ let currentVersion : string | null | undefined = oldVersion
462+ try {
463+ currentVersion = opencodeServerManager . getVersion ( ) || oldVersion
464+ } catch ( versionError ) {
465+ logger . error ( 'Failed to get version after recovery:' , versionError )
466+ currentVersion = oldVersion
467+ }
468+
469+ return c . json (
470+ recovered ? {
471+ success : false ,
472+ error : 'Upgrade failed but server recovered' ,
473+ details : error instanceof Error ? error . message : 'Unknown error' ,
474+ oldVersion,
475+ newVersion : currentVersion ,
476+ upgraded : false ,
477+ recovered : true ,
478+ recoveryMessage
479+ } : {
480+ error : 'Failed to upgrade OpenCode and could not recover' ,
481+ details : error instanceof Error ? error . message : 'Unknown error' ,
482+ oldVersion,
483+ newVersion : currentVersion ,
484+ upgraded : false ,
485+ recovered : false ,
486+ recoveryMessage
487+ } ,
488+ recovered ? 400 : 500
489+ )
432490 }
433491 } )
434492
@@ -479,30 +537,32 @@ export function createSettingsRoutes(db: Database) {
479537 } )
480538
481539 app . post ( '/opencode-install-version' , async ( c ) => {
540+ const oldVersion = opencodeServerManager . getVersion ( )
541+ logger . info ( `Current OpenCode version: ${ oldVersion } ` )
542+
482543 try {
483544 const body = await c . req . json ( )
484545 const { version } = z . object ( { version : z . string ( ) . min ( 1 ) } ) . parse ( body )
485-
546+
486547 logger . info ( `Installing OpenCode version: ${ version } ` )
487-
488- const oldVersion = opencodeServerManager . getVersion ( )
489- logger . info ( `Current OpenCode version: ${ oldVersion } ` )
490-
491548 const versionArg = version . startsWith ( 'v' ) ? version : `v${ version } `
492- logger . info ( `Running opencode upgrade ${ versionArg } ...` )
493-
494- const upgradeOutput = execSync ( `opencode upgrade ${ versionArg } 2>&1` , { encoding : 'utf8' } )
549+ logger . info ( `Running opencode upgrade ${ versionArg } with 90s timeout ...` )
550+
551+ const { output : upgradeOutput , timedOut } = execWithTimeout ( `opencode upgrade ${ versionArg } 2>&1` , 90000 )
495552 logger . info ( `Upgrade output: ${ upgradeOutput } ` )
496-
497- await new Promise ( r => setTimeout ( r , 2000 ) )
498-
553+
554+ if ( timedOut ) {
555+ logger . warn ( 'OpenCode version install timed out after 90 seconds' )
556+ throw new Error ( 'Version install command timed out after 90 seconds' )
557+ }
558+
499559 const newVersion = await opencodeServerManager . fetchVersion ( )
500560 logger . info ( `New OpenCode version: ${ newVersion } ` )
501-
561+
502562 opencodeServerManager . clearStartupError ( )
503563 await opencodeServerManager . restart ( )
504564 logger . info ( 'OpenCode server restarted after version change' )
505-
565+
506566 return c . json ( {
507567 success : true ,
508568 message : `OpenCode ${ oldVersion ? `changed from v${ oldVersion } to` : 'installed as' } v${ newVersion } ` ,
@@ -511,10 +571,44 @@ export function createSettingsRoutes(db: Database) {
511571 } )
512572 } catch ( error ) {
513573 logger . error ( 'Failed to install OpenCode version:' , error )
514- return c . json ( {
515- error : 'Failed to install OpenCode version' ,
516- details : error instanceof Error ? error . message : 'Unknown error'
517- } , 500 )
574+ logger . warn ( 'Attempting to recover OpenCode server...' )
575+
576+ let recovered = false
577+ let recoveryMessage = ''
578+
579+ opencodeServerManager . clearStartupError ( )
580+ try {
581+ await opencodeServerManager . restart ( )
582+ logger . warn ( 'OpenCode server restarted after install failure' )
583+ recovered = true
584+ recoveryMessage = 'Server recovered'
585+ } catch ( recoveryError ) {
586+ logger . error ( 'Failed to recover OpenCode server:' , recoveryError )
587+ recovered = false
588+ recoveryMessage = recoveryError instanceof Error ? recoveryError . message : 'Unknown error'
589+ }
590+
591+ const currentVersion = opencodeServerManager . getVersion ( ) || oldVersion
592+
593+ return c . json (
594+ recovered ? {
595+ success : false ,
596+ error : 'Version install failed but server recovered' ,
597+ details : error instanceof Error ? error . message : 'Unknown error' ,
598+ oldVersion,
599+ newVersion : currentVersion ,
600+ recovered : true ,
601+ recoveryMessage
602+ } : {
603+ error : 'Failed to install OpenCode version and could not recover' ,
604+ details : error instanceof Error ? error . message : 'Unknown error' ,
605+ oldVersion,
606+ newVersion : currentVersion ,
607+ recovered : false ,
608+ recoveryMessage
609+ } ,
610+ recovered ? 400 : 500
611+ )
518612 }
519613 } )
520614
0 commit comments