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).
This commit is contained in:
Dax Raad
2026-04-26 21:05:42 -04:00
parent 216dd363e8
commit 60ebd074ac
+31 -34
View File
@@ -132,6 +132,7 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })), Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })),
) )
// Use the package manager's resolver so registries, mirrors, auth, proxies, and dist-tags match upgrade behavior.
const viewVersion = Effect.fnUntraced(function* (method: "npm" | "pnpm" | "bun", spec: string) { const viewVersion = Effect.fnUntraced(function* (method: "npm" | "pnpm" | "bun", spec: string) {
const args = method === "bun" ? ["pm", "view", spec, "version", "--json"] : ["view", spec, "version", "--json"] const args = method === "bun" ? ["pm", "view", spec, "version", "--json"] : ["view", spec, "version", "--json"]
const result = yield* run([method, ...args]) const result = yield* run([method, ...args])
@@ -173,7 +174,14 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
Effect.orDie, Effect.orDie,
) )
const methodImpl = Effect.fn("Installation.method")(function* () { const result: Interface = {
info: Effect.fn("Installation.info")(function* () {
return {
version: InstallationVersion,
latest: yield* result.latest(),
}
}),
method: Effect.fn("Installation.method")(function* () {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method
if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method
const exec = process.execPath.toLowerCase() const exec = process.execPath.toLowerCase()
@@ -206,10 +214,9 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
} }
return "unknown" as Method return "unknown" as Method
}) }),
latest: Effect.fn("Installation.latest")(function* (installMethod?: Method) {
const latestImpl = Effect.fn("Installation.latest")(function* (installMethod?: Method) { const detectedMethod = installMethod || (yield* result.method())
const detectedMethod = installMethod || (yield* methodImpl())
if (detectedMethod === "brew") { if (detectedMethod === "brew") {
const formula = yield* getBrewFormula() const formula = yield* getBrewFormula()
@@ -258,22 +265,21 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
) )
const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response) const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response)
return data.tag_name.replace(/^v/, "") return data.tag_name.replace(/^v/, "")
}, Effect.orDie) }, Effect.orDie),
upgrade: Effect.fn("Installation.upgrade")(function* (m: Method, target: string) {
const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) { let upgradeResult: { code: ChildProcessSpawner.ExitCode; stdout: string; stderr: string } | undefined
let result: { code: ChildProcessSpawner.ExitCode; stdout: string; stderr: string } | undefined
switch (m) { switch (m) {
case "curl": case "curl":
result = yield* upgradeCurl(target) upgradeResult = yield* upgradeCurl(target)
break break
case "npm": case "npm":
result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) upgradeResult = yield* run(["npm", "install", "-g", `opencode-ai@${target}`])
break break
case "pnpm": case "pnpm":
result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) upgradeResult = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`])
break break
case "bun": case "bun":
result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) upgradeResult = yield* run(["bun", "install", "-g", `opencode-ai@${target}`])
break break
case "brew": { case "brew": {
const formula = yield* getBrewFormula() const formula = yield* getBrewFormula()
@@ -281,7 +287,7 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
if (formula.includes("/")) { if (formula.includes("/")) {
const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env }) const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env })
if (tap.code !== 0) { if (tap.code !== 0) {
result = tap upgradeResult = tap
break break
} }
const repo = yield* text(["brew", "--repo", "anomalyco/tap"]) const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
@@ -289,47 +295,38 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
if (dir) { if (dir) {
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env }) const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
if (pull.code !== 0) { if (pull.code !== 0) {
result = pull upgradeResult = pull
break break
} }
} }
} }
result = yield* run(["brew", "upgrade", formula], { env }) upgradeResult = yield* run(["brew", "upgrade", formula], { env })
break break
} }
case "choco": case "choco":
result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) upgradeResult = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"])
break break
case "scoop": case "scoop":
result = yield* run(["scoop", "install", `opencode@${target}`]) upgradeResult = yield* run(["scoop", "install", `opencode@${target}`])
break break
default: default:
return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` }) return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` })
} }
if (!result || result.code !== 0) { if (!upgradeResult || upgradeResult.code !== 0) {
const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || "" const stderr = m === "choco" ? "not running from an elevated command shell" : upgradeResult?.stderr || ""
return yield* new UpgradeFailedError({ stderr }) return yield* new UpgradeFailedError({ stderr })
} }
log.info("upgraded", { log.info("upgraded", {
method: m, method: m,
target, target,
stdout: result.stdout, stdout: upgradeResult.stdout,
stderr: result.stderr, stderr: upgradeResult.stderr,
}) })
yield* text([process.execPath, "--version"]) yield* text([process.execPath, "--version"])
})
return Service.of({
info: Effect.fn("Installation.info")(function* () {
return {
version: InstallationVersion,
latest: yield* latestImpl(),
}
}), }),
method: methodImpl, }
latest: latestImpl,
upgrade: upgradeImpl, return Service.of(result)
})
}), }),
) )