Skip to content

Commit 60ebd07

Browse files
committed
core: refactor Installation service to use a single consolidated result object
Reorganizes the Installation service implementation by grouping info, method, latest, and upgrade methods into a single result object. This improves code locality and makes the service interface more maintainable. Also adds a clarifying comment explaining why the package manager's resolver is used for version lookups (to ensure registries, mirrors, auth, proxies, and dist-tags match upgrade behavior).
1 parent 216dd36 commit 60ebd07

1 file changed

Lines changed: 137 additions & 140 deletions

File tree

  • packages/opencode/src/installation

packages/opencode/src/installation/index.ts

Lines changed: 137 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)