@@ -11,7 +11,6 @@ import * as Log from "@opencode-ai/core/util/log"
1111import { makeRuntime } from "@opencode-ai/core/effect/runtime"
1212import semver from "semver"
1313import { InstallationChannel , InstallationVersion } from "@opencode-ai/core/installation/version"
14- import { NpmConfig } from "@opencode-ai/core/npm-config"
1514
1615const log = Log . create ( { service : "installation" } )
1716
@@ -69,17 +68,12 @@ export class UpgradeFailedError extends Schema.TaggedErrorClass<UpgradeFailedErr
6968 stderr : Schema . String ,
7069} ) { }
7170
72- // Response schemas for external version APIs
71+ // BrowserCode currently only ships the curl installer (https://bcode.sh/install).
72+ // npm/brew/scoop/choco branches are stubbed: `method()` only returns "curl"
73+ // or "unknown", and `latest()` / `upgrade()` short-circuit on "unknown".
74+ // When we add another distribution channel, restore the corresponding registry
75+ // schemas + branches from upstream `anomalyco/opencode`.
7376const GitHubRelease = Schema . Struct ( { tag_name : Schema . String } )
74- const NpmPackage = Schema . Struct ( { version : Schema . String } )
75- const BrewFormula = Schema . Struct ( { versions : Schema . Struct ( { stable : Schema . String } ) } )
76- const BrewInfoV2 = Schema . Struct ( {
77- formulae : Schema . Array ( Schema . Struct ( { versions : Schema . Struct ( { stable : Schema . String } ) } ) ) ,
78- } )
79- const ChocoPackage = Schema . Struct ( {
80- d : Schema . Struct ( { results : Schema . Array ( Schema . Struct ( { Version : Schema . String } ) ) } ) ,
81- } )
82- const ScoopManifest = NpmPackage
8377
8478export interface Interface {
8579 readonly info : ( ) => Effect . Effect < Info >
@@ -114,40 +108,15 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
114108 Effect . catch ( ( ) => Effect . succeed ( "" ) ) ,
115109 )
116110
117- const run = Effect . fnUntraced (
118- function * ( cmd : string [ ] , opts ?: { cwd ?: string ; env ?: Record < string , string > } ) {
119- const proc = ChildProcess . make ( cmd [ 0 ] , cmd . slice ( 1 ) , {
120- cwd : opts ?. cwd ,
121- env : opts ?. env ,
122- extendEnv : true ,
123- } )
124- const handle = yield * spawner . spawn ( proc )
125- const [ stdout , stderr ] = yield * Effect . all (
126- [ Stream . mkString ( Stream . decodeText ( handle . stdout ) ) , Stream . mkString ( Stream . decodeText ( handle . stderr ) ) ] ,
127- { concurrency : 2 } ,
128- )
129- const code = yield * handle . exitCode
130- return { code, stdout, stderr }
131- } ,
132- Effect . scoped ,
133- Effect . catch ( ( ) => Effect . succeed ( { code : ChildProcessSpawner . ExitCode ( 1 ) , stdout : "" , stderr : "" } ) ) ,
134- )
135-
136- const getBrewFormula = Effect . fnUntraced ( function * ( ) {
137- const tapFormula = yield * text ( [ "brew" , "list" , "--formula" , "anomalyco/tap/opencode" ] )
138- if ( tapFormula . includes ( "opencode" ) ) return "anomalyco/tap/opencode"
139- const coreFormula = yield * text ( [ "brew" , "list" , "--formula" , "opencode" ] )
140- if ( coreFormula . includes ( "opencode" ) ) return "opencode"
141- return "opencode"
142- } )
143-
111+ // Re-runs the hosted install script with VERSION=<target>, which writes
112+ // the new binary to ~/.bcode/bin/bcode in place. Same flow as the
113+ // user's original `curl https://bcode.sh/install | bash` install.
144114 const upgradeCurl = Effect . fnUntraced (
145115 function * ( target : string ) {
146116 const response = yield * httpOk . execute ( HttpClientRequest . get ( "https://bcode.sh/install" ) )
147117 const body = yield * response . text
148- const bodyBytes = new TextEncoder ( ) . encode ( body )
149118 const proc = ChildProcess . make ( "bash" , [ ] , {
150- stdin : Stream . make ( bodyBytes ) ,
119+ stdin : Stream . make ( new TextEncoder ( ) . encode ( body ) ) ,
151120 env : { VERSION : target } ,
152121 extendEnv : true ,
153122 } )
@@ -163,178 +132,48 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
163132 Effect . orDie ,
164133 )
165134
166- // BrowserCode self-upgrade is intentionally disabled. The upstream
167- // `Installation` service queries opencode-ai's npm/brew/scoop/choco
168- // registries and the anomalyco/opencode GitHub releases — running
169- // any of those against a `bcode` binary would replace it with
170- // `opencode`. Instead we report `latest === current` so the
171- // auto-upgrade in `cli/upgrade.ts` is a no-op, and `bcode upgrade`
172- // prints a friendly "not yet supported" error.
173- const BCODE_UPGRADE_DISABLED = true
174-
175135 const result : Interface = {
176136 info : Effect . fn ( "Installation.info" ) ( function * ( ) {
177137 return {
178138 version : InstallationVersion ,
179139 latest : yield * result . latest ( ) ,
180140 }
181141 } ) ,
142+ // Until BrowserCode ships beyond the curl installer, we only detect
143+ // the curl path. Anything else is "unknown" — `upgrade()` handles
144+ // that with a clear error pointing at the install script.
182145 method : Effect . fn ( "Installation.method" ) ( function * ( ) {
183146 if ( process . execPath . includes ( path . join ( ".bcode" , "bin" ) ) ) return "curl" as Method
184147 if ( process . execPath . includes ( path . join ( ".local" , "bin" ) ) ) return "curl" as Method
185- const exec = process . execPath . toLowerCase ( )
186-
187- const checks : Array < { name : Method ; command : ( ) => Effect . Effect < string > } > = [
188- { name : "npm" , command : ( ) => text ( [ "npm" , "list" , "-g" , "--depth=0" ] ) } ,
189- { name : "yarn" , command : ( ) => text ( [ "yarn" , "global" , "list" ] ) } ,
190- { name : "pnpm" , command : ( ) => text ( [ "pnpm" , "list" , "-g" , "--depth=0" ] ) } ,
191- { name : "bun" , command : ( ) => text ( [ "bun" , "pm" , "ls" , "-g" ] ) } ,
192- { name : "brew" , command : ( ) => text ( [ "brew" , "list" , "--formula" , "opencode" ] ) } ,
193- { name : "scoop" , command : ( ) => text ( [ "scoop" , "list" , "opencode" ] ) } ,
194- { name : "choco" , command : ( ) => text ( [ "choco" , "list" , "--limit-output" , "opencode" ] ) } ,
195- ]
196-
197- checks . sort ( ( a , b ) => {
198- const aMatches = exec . includes ( a . name )
199- const bMatches = exec . includes ( b . name )
200- if ( aMatches && ! bMatches ) return - 1
201- if ( ! aMatches && bMatches ) return 1
202- return 0
203- } )
204-
205- for ( const check of checks ) {
206- const output = yield * check . command ( )
207- const installedName =
208- check . name === "brew" || check . name === "choco" || check . name === "scoop" ? "opencode" : "opencode-ai"
209- if ( output . includes ( installedName ) ) {
210- return check . name
211- }
212- }
213-
214148 return "unknown" as Method
215149 } ) ,
216150 latest : Effect . fn ( "Installation.latest" ) ( function * ( installMethod ?: Method ) {
217- if ( BCODE_UPGRADE_DISABLED ) {
218- return InstallationVersion
219- }
220151 const detectedMethod = installMethod || ( yield * result . method ( ) )
221-
222- if ( detectedMethod === "brew" ) {
223- const formula = yield * getBrewFormula ( )
224- if ( formula . includes ( "/" ) ) {
225- const infoJson = yield * text ( [ "brew" , "info" , "--json=v2" , formula ] )
226- const info = yield * Schema . decodeUnknownEffect ( Schema . fromJsonString ( BrewInfoV2 ) ) ( infoJson )
227- return info . formulae [ 0 ] . versions . stable
228- }
229- const response = yield * httpOk . execute (
230- HttpClientRequest . get ( "https://formulae.brew.sh/api/formula/opencode.json" ) . pipe (
231- HttpClientRequest . acceptJson ,
232- ) ,
233- )
234- const data = yield * HttpClientResponse . schemaBodyJson ( BrewFormula ) ( response )
235- return data . versions . stable
236- }
237-
238- if ( detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm" ) {
239- const response = yield * httpOk . execute (
240- HttpClientRequest . get (
241- `${ yield * NpmConfig . registry ( process . cwd ( ) ) } /opencode-ai/${ InstallationChannel } ` ,
242- ) . pipe ( HttpClientRequest . acceptJson ) ,
243- )
244- const data = yield * HttpClientResponse . schemaBodyJson ( NpmPackage ) ( response )
245- return data . version
246- }
247-
248- if ( detectedMethod === "choco" ) {
249- const response = yield * httpOk . execute (
250- HttpClientRequest . get (
251- "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version" ,
252- ) . pipe ( HttpClientRequest . setHeaders ( { Accept : "application/json;odata=verbose" } ) ) ,
253- )
254- const data = yield * HttpClientResponse . schemaBodyJson ( ChocoPackage ) ( response )
255- return data . d . results [ 0 ] . Version
256- }
257-
258- if ( detectedMethod === "scoop" ) {
259- const response = yield * httpOk . execute (
260- HttpClientRequest . get (
261- "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json" ,
262- ) . pipe ( HttpClientRequest . setHeaders ( { Accept : "application/json" } ) ) ,
263- )
264- const data = yield * HttpClientResponse . schemaBodyJson ( ScoopManifest ) ( response )
265- return data . version
266- }
267-
152+ // No-op for unsupported methods so the TUI auto-upgrade check stays
153+ // silent for devs running from source.
154+ if ( detectedMethod !== "curl" ) return InstallationVersion
268155 const response = yield * httpOk . execute (
269- HttpClientRequest . get ( "https://api.github.com/repos/anomalyco/opencode /releases/latest" ) . pipe (
156+ HttpClientRequest . get ( "https://api.github.com/repos/browser-use/browsercode /releases/latest" ) . pipe (
270157 HttpClientRequest . acceptJson ,
271158 ) ,
272159 )
273160 const data = yield * HttpClientResponse . schemaBodyJson ( GitHubRelease ) ( response )
274161 return data . tag_name . replace ( / ^ v / , "" )
275162 } , Effect . orDie ) ,
276163 upgrade : Effect . fn ( "Installation.upgrade" ) ( function * ( m : Method , target : string ) {
277- if ( BCODE_UPGRADE_DISABLED ) {
164+ if ( m !== "curl" ) {
278165 return yield * new UpgradeFailedError ( {
279166 stderr :
280- "BrowserCode auto-upgrade is not yet supported. Download a release from https://github.com/browser-use/browsercode/releases or rebuild from source." ,
167+ "Auto-upgrade currently supports only curl-installed bcode. " +
168+ "Reinstall via https://bcode.sh/install or download a release " +
169+ "from https://github.com/browser-use/browsercode/releases." ,
281170 } )
282171 }
283- let upgradeResult : { code : ChildProcessSpawner . ExitCode ; stdout : string ; stderr : string } | undefined
284- switch ( m ) {
285- case "curl" :
286- upgradeResult = yield * upgradeCurl ( target )
287- break
288- case "npm" :
289- upgradeResult = yield * run ( [ "npm" , "install" , "-g" , `opencode-ai@${ target } ` ] )
290- break
291- case "pnpm" :
292- upgradeResult = yield * run ( [ "pnpm" , "install" , "-g" , `opencode-ai@${ target } ` ] )
293- break
294- case "bun" :
295- upgradeResult = yield * run ( [ "bun" , "install" , "-g" , `opencode-ai@${ target } ` ] )
296- break
297- case "brew" : {
298- const formula = yield * getBrewFormula ( )
299- const env = { HOMEBREW_NO_AUTO_UPDATE : "1" }
300- if ( formula . includes ( "/" ) ) {
301- const tap = yield * run ( [ "brew" , "tap" , "anomalyco/tap" ] , { env } )
302- if ( tap . code !== 0 ) {
303- upgradeResult = tap
304- break
305- }
306- const repo = yield * text ( [ "brew" , "--repo" , "anomalyco/tap" ] )
307- const dir = repo . trim ( )
308- if ( dir ) {
309- const pull = yield * run ( [ "git" , "pull" , "--ff-only" ] , { cwd : dir , env } )
310- if ( pull . code !== 0 ) {
311- upgradeResult = pull
312- break
313- }
314- }
315- }
316- upgradeResult = yield * run ( [ "brew" , "upgrade" , formula ] , { env } )
317- break
318- }
319- case "choco" :
320- upgradeResult = yield * run ( [ "choco" , "upgrade" , "opencode" , `--version=${ target } ` , "-y" ] )
321- break
322- case "scoop" :
323- upgradeResult = yield * run ( [ "scoop" , "install" , `opencode@${ target } ` ] )
324- break
325- default :
326- return yield * new UpgradeFailedError ( { stderr : `Unknown method: ${ m } ` } )
172+ const upgradeResult = yield * upgradeCurl ( target )
173+ if ( upgradeResult . code !== 0 ) {
174+ return yield * new UpgradeFailedError ( { stderr : upgradeResult . stderr } )
327175 }
328- if ( ! upgradeResult || upgradeResult . code !== 0 ) {
329- const stderr = m === "choco" ? "not running from an elevated command shell" : upgradeResult ?. stderr || ""
330- return yield * new UpgradeFailedError ( { stderr } )
331- }
332- log . info ( "upgraded" , {
333- method : m ,
334- target,
335- stdout : upgradeResult . stdout ,
336- stderr : upgradeResult . stderr ,
337- } )
176+ log . info ( "upgraded" , { method : m , target, stdout : upgradeResult . stdout , stderr : upgradeResult . stderr } )
338177 yield * text ( [ process . execPath , "--version" ] )
339178 } ) ,
340179 }
0 commit comments