@@ -132,6 +132,7 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
132132 Effect . catch ( ( ) => Effect . succeed ( { code : ChildProcessSpawner . ExitCode ( 1 ) , stdout : "" , stderr : "" } ) ) ,
133133 )
134134
135+ // Use the package manager's resolver so registries, mirrors, auth, proxies, and dist-tags match upgrade behavior.
135136 const viewVersion = Effect . fnUntraced ( function * ( method : "npm" | "pnpm" | "bun" , spec : string ) {
136137 const args = method === "bun" ? [ "pm" , "view" , spec , "version" , "--json" ] : [ "view" , spec , "version" , "--json" ]
137138 const result = yield * run ( [ method , ...args ] )
@@ -173,163 +174,159 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
173174 Effect . orDie ,
174175 )
175176
176- const methodImpl = Effect . fn ( "Installation.method" ) ( function * ( ) {
177- if ( process . execPath . includes ( path . join ( ".opencode" , "bin" ) ) ) return "curl" as Method
178- if ( process . execPath . includes ( path . join ( ".local" , "bin" ) ) ) return "curl" as Method
179- const exec = process . execPath . toLowerCase ( )
180-
181- const checks : Array < { name : Method ; command : ( ) => Effect . Effect < string > } > = [
182- { name : "npm" , command : ( ) => text ( [ "npm" , "list" , "-g" , "--depth=0" ] ) } ,
183- { name : "yarn" , command : ( ) => text ( [ "yarn" , "global" , "list" ] ) } ,
184- { name : "pnpm" , command : ( ) => text ( [ "pnpm" , "list" , "-g" , "--depth=0" ] ) } ,
185- { name : "bun" , command : ( ) => text ( [ "bun" , "pm" , "ls" , "-g" ] ) } ,
186- { name : "brew" , command : ( ) => text ( [ "brew" , "list" , "--formula" , "opencode" ] ) } ,
187- { name : "scoop" , command : ( ) => text ( [ "scoop" , "list" , "opencode" ] ) } ,
188- { name : "choco" , command : ( ) => text ( [ "choco" , "list" , "--limit-output" , "opencode" ] ) } ,
189- ]
190-
191- checks . sort ( ( a , b ) => {
192- const aMatches = exec . includes ( a . name )
193- const bMatches = exec . includes ( b . name )
194- if ( aMatches && ! bMatches ) return - 1
195- if ( ! aMatches && bMatches ) return 1
196- return 0
197- } )
198-
199- for ( const check of checks ) {
200- const output = yield * check . command ( )
201- const installedName =
202- check . name === "brew" || check . name === "choco" || check . name === "scoop" ? "opencode" : "opencode-ai"
203- if ( output . includes ( installedName ) ) {
204- return check . name
177+ const result : Interface = {
178+ info : Effect . fn ( "Installation.info" ) ( function * ( ) {
179+ return {
180+ version : InstallationVersion ,
181+ latest : yield * result . latest ( ) ,
205182 }
206- }
183+ } ) ,
184+ method : Effect . fn ( "Installation.method" ) ( function * ( ) {
185+ if ( process . execPath . includes ( path . join ( ".opencode" , "bin" ) ) ) return "curl" as Method
186+ if ( process . execPath . includes ( path . join ( ".local" , "bin" ) ) ) return "curl" as Method
187+ const exec = process . execPath . toLowerCase ( )
188+
189+ const checks : Array < { name : Method ; command : ( ) => Effect . Effect < string > } > = [
190+ { name : "npm" , command : ( ) => text ( [ "npm" , "list" , "-g" , "--depth=0" ] ) } ,
191+ { name : "yarn" , command : ( ) => text ( [ "yarn" , "global" , "list" ] ) } ,
192+ { name : "pnpm" , command : ( ) => text ( [ "pnpm" , "list" , "-g" , "--depth=0" ] ) } ,
193+ { name : "bun" , command : ( ) => text ( [ "bun" , "pm" , "ls" , "-g" ] ) } ,
194+ { name : "brew" , command : ( ) => text ( [ "brew" , "list" , "--formula" , "opencode" ] ) } ,
195+ { name : "scoop" , command : ( ) => text ( [ "scoop" , "list" , "opencode" ] ) } ,
196+ { name : "choco" , command : ( ) => text ( [ "choco" , "list" , "--limit-output" , "opencode" ] ) } ,
197+ ]
198+
199+ checks . sort ( ( a , b ) => {
200+ const aMatches = exec . includes ( a . name )
201+ const bMatches = exec . includes ( b . name )
202+ if ( aMatches && ! bMatches ) return - 1
203+ if ( ! aMatches && bMatches ) return 1
204+ return 0
205+ } )
207206
208- return "unknown" as Method
209- } )
207+ for ( const check of checks ) {
208+ const output = yield * check . command ( )
209+ const installedName =
210+ check . name === "brew" || check . name === "choco" || check . name === "scoop" ? "opencode" : "opencode-ai"
211+ if ( output . includes ( installedName ) ) {
212+ return check . name
213+ }
214+ }
210215
211- const latestImpl = Effect . fn ( "Installation.latest" ) ( function * ( installMethod ?: Method ) {
212- const detectedMethod = installMethod || ( yield * methodImpl ( ) )
216+ return "unknown" as Method
217+ } ) ,
218+ latest : Effect . fn ( "Installation.latest" ) ( function * ( installMethod ?: Method ) {
219+ const detectedMethod = installMethod || ( yield * result . method ( ) )
213220
214- if ( detectedMethod === "brew" ) {
215- const formula = yield * getBrewFormula ( )
216- if ( formula . includes ( "/" ) ) {
217- const infoJson = yield * text ( [ "brew" , "info" , "--json=v2" , formula ] )
218- const info = yield * Schema . decodeUnknownEffect ( Schema . fromJsonString ( BrewInfoV2 ) ) ( infoJson )
219- return info . formulae [ 0 ] . versions . stable
221+ if ( detectedMethod === "brew" ) {
222+ const formula = yield * getBrewFormula ( )
223+ if ( formula . includes ( "/" ) ) {
224+ const infoJson = yield * text ( [ "brew" , "info" , "--json=v2" , formula ] )
225+ const info = yield * Schema . decodeUnknownEffect ( Schema . fromJsonString ( BrewInfoV2 ) ) ( infoJson )
226+ return info . formulae [ 0 ] . versions . stable
227+ }
228+ const response = yield * httpOk . execute (
229+ HttpClientRequest . get ( "https://formulae.brew.sh/api/formula/opencode.json" ) . pipe (
230+ HttpClientRequest . acceptJson ,
231+ ) ,
232+ )
233+ const data = yield * HttpClientResponse . schemaBodyJson ( BrewFormula ) ( response )
234+ return data . versions . stable
220235 }
221- const response = yield * httpOk . execute (
222- HttpClientRequest . get ( "https://formulae.brew.sh/api/formula/opencode.json" ) . pipe (
223- HttpClientRequest . acceptJson ,
224- ) ,
225- )
226- const data = yield * HttpClientResponse . schemaBodyJson ( BrewFormula ) ( response )
227- return data . versions . stable
228- }
229236
230- if ( detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm" ) {
231- return yield * viewVersion ( detectedMethod , `opencode-ai@${ InstallationChannel } ` )
232- }
237+ if ( detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm" ) {
238+ return yield * viewVersion ( detectedMethod , `opencode-ai@${ InstallationChannel } ` )
239+ }
233240
234- if ( detectedMethod === "choco" ) {
235- const response = yield * httpOk . execute (
236- HttpClientRequest . get (
237- "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version" ,
238- ) . pipe ( HttpClientRequest . setHeaders ( { Accept : "application/json;odata=verbose" } ) ) ,
239- )
240- const data = yield * HttpClientResponse . schemaBodyJson ( ChocoPackage ) ( response )
241- return data . d . results [ 0 ] . Version
242- }
241+ if ( detectedMethod === "choco" ) {
242+ const response = yield * httpOk . execute (
243+ HttpClientRequest . get (
244+ "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version" ,
245+ ) . pipe ( HttpClientRequest . setHeaders ( { Accept : "application/json;odata=verbose" } ) ) ,
246+ )
247+ const data = yield * HttpClientResponse . schemaBodyJson ( ChocoPackage ) ( response )
248+ return data . d . results [ 0 ] . Version
249+ }
250+
251+ if ( detectedMethod === "scoop" ) {
252+ const response = yield * httpOk . execute (
253+ HttpClientRequest . get (
254+ "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json" ,
255+ ) . pipe ( HttpClientRequest . setHeaders ( { Accept : "application/json" } ) ) ,
256+ )
257+ const data = yield * HttpClientResponse . schemaBodyJson ( ScoopManifest ) ( response )
258+ return data . version
259+ }
243260
244- if ( detectedMethod === "scoop" ) {
245261 const response = yield * httpOk . execute (
246- HttpClientRequest . get (
247- "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json" ,
248- ) . pipe ( HttpClientRequest . setHeaders ( { Accept : "application/json" } ) ) ,
262+ HttpClientRequest . get ( "https://api.github.com/repos/anomalyco/opencode/releases/latest" ) . pipe (
263+ HttpClientRequest . acceptJson ,
264+ ) ,
249265 )
250- const data = yield * HttpClientResponse . schemaBodyJson ( ScoopManifest ) ( response )
251- return data . version
252- }
253-
254- const response = yield * httpOk . execute (
255- HttpClientRequest . get ( "https://api.github.com/repos/anomalyco/opencode/releases/latest" ) . pipe (
256- HttpClientRequest . acceptJson ,
257- ) ,
258- )
259- const data = yield * HttpClientResponse . schemaBodyJson ( GitHubRelease ) ( response )
260- return data . tag_name . replace ( / ^ v / , "" )
261- } , Effect . orDie )
262-
263- const upgradeImpl = Effect . fn ( "Installation.upgrade" ) ( function * ( m : Method , target : string ) {
264- let result : { code : ChildProcessSpawner . ExitCode ; stdout : string ; stderr : string } | undefined
265- switch ( m ) {
266- case "curl" :
267- result = yield * upgradeCurl ( target )
268- break
269- case "npm" :
270- result = yield * run ( [ "npm" , "install" , "-g" , `opencode-ai@${ target } ` ] )
271- break
272- case "pnpm" :
273- result = yield * run ( [ "pnpm" , "install" , "-g" , `opencode-ai@${ target } ` ] )
274- break
275- case "bun" :
276- result = yield * run ( [ "bun" , "install" , "-g" , `opencode-ai@${ target } ` ] )
277- break
278- case "brew" : {
279- const formula = yield * getBrewFormula ( )
280- const env = { HOMEBREW_NO_AUTO_UPDATE : "1" }
281- if ( formula . includes ( "/" ) ) {
282- const tap = yield * run ( [ "brew" , "tap" , "anomalyco/tap" ] , { env } )
283- if ( tap . code !== 0 ) {
284- result = tap
285- break
286- }
287- const repo = yield * text ( [ "brew" , "--repo" , "anomalyco/tap" ] )
288- const dir = repo . trim ( )
289- if ( dir ) {
290- const pull = yield * run ( [ "git" , "pull" , "--ff-only" ] , { cwd : dir , env } )
291- if ( pull . code !== 0 ) {
292- result = pull
266+ const data = yield * HttpClientResponse . schemaBodyJson ( GitHubRelease ) ( response )
267+ return data . tag_name . replace ( / ^ v / , "" )
268+ } , Effect . orDie ) ,
269+ upgrade : Effect . fn ( "Installation.upgrade" ) ( function * ( m : Method , target : string ) {
270+ let upgradeResult : { code : ChildProcessSpawner . ExitCode ; stdout : string ; stderr : string } | undefined
271+ switch ( m ) {
272+ case "curl" :
273+ upgradeResult = yield * upgradeCurl ( target )
274+ break
275+ case "npm" :
276+ upgradeResult = yield * run ( [ "npm" , "install" , "-g" , `opencode-ai@${ target } ` ] )
277+ break
278+ case "pnpm" :
279+ upgradeResult = yield * run ( [ "pnpm" , "install" , "-g" , `opencode-ai@${ target } ` ] )
280+ break
281+ case "bun" :
282+ upgradeResult = yield * run ( [ "bun" , "install" , "-g" , `opencode-ai@${ target } ` ] )
283+ break
284+ case "brew" : {
285+ const formula = yield * getBrewFormula ( )
286+ const env = { HOMEBREW_NO_AUTO_UPDATE : "1" }
287+ if ( formula . includes ( "/" ) ) {
288+ const tap = yield * run ( [ "brew" , "tap" , "anomalyco/tap" ] , { env } )
289+ if ( tap . code !== 0 ) {
290+ upgradeResult = tap
293291 break
294292 }
293+ const repo = yield * text ( [ "brew" , "--repo" , "anomalyco/tap" ] )
294+ const dir = repo . trim ( )
295+ if ( dir ) {
296+ const pull = yield * run ( [ "git" , "pull" , "--ff-only" ] , { cwd : dir , env } )
297+ if ( pull . code !== 0 ) {
298+ upgradeResult = pull
299+ break
300+ }
301+ }
295302 }
303+ upgradeResult = yield * run ( [ "brew" , "upgrade" , formula ] , { env } )
304+ break
296305 }
297- result = yield * run ( [ "brew" , "upgrade" , formula ] , { env } )
298- break
306+ case "choco" :
307+ upgradeResult = yield * run ( [ "choco" , "upgrade" , "opencode" , `--version=${ target } ` , "-y" ] )
308+ break
309+ case "scoop" :
310+ upgradeResult = yield * run ( [ "scoop" , "install" , `opencode@${ target } ` ] )
311+ break
312+ default :
313+ return yield * new UpgradeFailedError ( { stderr : `Unknown method: ${ m } ` } )
299314 }
300- case "choco" :
301- result = yield * run ( [ "choco" , "upgrade" , "opencode" , `--version=${ target } ` , "-y" ] )
302- break
303- case "scoop" :
304- result = yield * run ( [ "scoop" , "install" , `opencode@${ target } ` ] )
305- break
306- default :
307- return yield * new UpgradeFailedError ( { stderr : `Unknown method: ${ m } ` } )
308- }
309- if ( ! result || result . code !== 0 ) {
310- const stderr = m === "choco" ? "not running from an elevated command shell" : result ?. stderr || ""
311- return yield * new UpgradeFailedError ( { stderr } )
312- }
313- log . info ( "upgraded" , {
314- method : m ,
315- target,
316- stdout : result . stdout ,
317- stderr : result . stderr ,
318- } )
319- yield * text ( [ process . execPath , "--version" ] )
320- } )
321-
322- return Service . of ( {
323- info : Effect . fn ( "Installation.info" ) ( function * ( ) {
324- return {
325- version : InstallationVersion ,
326- latest : yield * latestImpl ( ) ,
315+ if ( ! upgradeResult || upgradeResult . code !== 0 ) {
316+ const stderr = m === "choco" ? "not running from an elevated command shell" : upgradeResult ?. stderr || ""
317+ return yield * new UpgradeFailedError ( { stderr } )
327318 }
319+ log . info ( "upgraded" , {
320+ method : m ,
321+ target,
322+ stdout : upgradeResult . stdout ,
323+ stderr : upgradeResult . stderr ,
324+ } )
325+ yield * text ( [ process . execPath , "--version" ] )
328326 } ) ,
329- method : methodImpl ,
330- latest : latestImpl ,
331- upgrade : upgradeImpl ,
332- } )
327+ }
328+
329+ return Service . of ( result )
333330 } ) ,
334331 )
335332
0 commit comments