chore: generate

This commit is contained in:
opencode-agent[bot]
2026-03-20 16:11:21 +00:00
parent e78944e9a4
commit 51618e9cef
3 changed files with 170 additions and 182 deletions

View File

@@ -81,119 +81,115 @@ export namespace Installation {
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Installation") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Installation") {}
export const layer: Layer.Layer< export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildProcessSpawner.ChildProcessSpawner> =
Service, Layer.effect(
never, Service,
HttpClient.HttpClient | ChildProcessSpawner.ChildProcessSpawner Effect.gen(function* () {
> = Layer.effect( const http = yield* HttpClient.HttpClient
Service, const httpOk = HttpClient.filterStatusOk(withTransientReadRetry(http))
Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const http = yield* HttpClient.HttpClient
const httpOk = HttpClient.filterStatusOk(withTransientReadRetry(http))
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const text = Effect.fnUntraced( const text = Effect.fnUntraced(
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) { function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) {
const proc = ChildProcess.make(cmd[0], cmd.slice(1), { const proc = ChildProcess.make(cmd[0], cmd.slice(1), {
cwd: opts?.cwd, cwd: opts?.cwd,
env: opts?.env, env: opts?.env,
extendEnv: true, extendEnv: true,
}) })
const handle = yield* spawner.spawn(proc) const handle = yield* spawner.spawn(proc)
const out = yield* Stream.mkString(Stream.decodeText(handle.stdout)) const out = yield* Stream.mkString(Stream.decodeText(handle.stdout))
yield* handle.exitCode yield* handle.exitCode
return out return out
}, },
Effect.scoped, Effect.scoped,
Effect.catch(() => Effect.succeed("")), Effect.catch(() => Effect.succeed("")),
) )
const run = Effect.fnUntraced( const run = Effect.fnUntraced(
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) { function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) {
const proc = ChildProcess.make(cmd[0], cmd.slice(1), { const proc = ChildProcess.make(cmd[0], cmd.slice(1), {
cwd: opts?.cwd, cwd: opts?.cwd,
env: opts?.env, env: opts?.env,
extendEnv: true, extendEnv: true,
}) })
const handle = yield* spawner.spawn(proc) const handle = yield* spawner.spawn(proc)
const [stdout, stderr] = yield* Effect.all( const [stdout, stderr] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
{ concurrency: 2 }, { concurrency: 2 },
) )
const code = yield* handle.exitCode const code = yield* handle.exitCode
return { code, stdout, stderr } return { code, stdout, stderr }
}, },
Effect.scoped, Effect.scoped,
Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })), Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })),
) )
const getBrewFormula = Effect.fnUntraced(function* () { const getBrewFormula = Effect.fnUntraced(function* () {
const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"]) const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"])
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode" if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
const coreFormula = yield* text(["brew", "list", "--formula", "opencode"]) const coreFormula = yield* text(["brew", "list", "--formula", "opencode"])
if (coreFormula.includes("opencode")) return "opencode" if (coreFormula.includes("opencode")) return "opencode"
return "opencode" return "opencode"
})
const upgradeCurl = Effect.fnUntraced(
function* (target: string) {
const response = yield* httpOk.execute(HttpClientRequest.get("https://opencode.ai/install"))
const body = yield* response.text
const bodyBytes = new TextEncoder().encode(body)
const proc = ChildProcess.make("bash", [], {
stdin: Stream.make(bodyBytes),
env: { VERSION: target },
extendEnv: true,
})
const handle = yield* spawner.spawn(proc)
const [stdout, stderr] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
{ concurrency: 2 },
)
const code = yield* handle.exitCode
return { code, stdout, stderr }
},
Effect.scoped,
Effect.orDie,
)
const methodImpl = Effect.fn("Installation.method")(function* () {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method
if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method
const exec = process.execPath.toLowerCase()
const checks: Array<{ name: Method; command: () => Effect.Effect<string> }> = [
{ name: "npm", command: () => text(["npm", "list", "-g", "--depth=0"]) },
{ name: "yarn", command: () => text(["yarn", "global", "list"]) },
{ name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) },
{ name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) },
{ name: "scoop", command: () => text(["scoop", "list", "opencode"]) },
{ name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) },
]
checks.sort((a, b) => {
const aMatches = exec.includes(a.name)
const bMatches = exec.includes(b.name)
if (aMatches && !bMatches) return -1
if (!aMatches && bMatches) return 1
return 0
}) })
for (const check of checks) { const upgradeCurl = Effect.fnUntraced(
const output = yield* check.command() function* (target: string) {
const installedName = const response = yield* httpOk.execute(HttpClientRequest.get("https://opencode.ai/install"))
check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "opencode" : "opencode-ai" const body = yield* response.text
if (output.includes(installedName)) { const bodyBytes = new TextEncoder().encode(body)
return check.name const proc = ChildProcess.make("bash", [], {
stdin: Stream.make(bodyBytes),
env: { VERSION: target },
extendEnv: true,
})
const handle = yield* spawner.spawn(proc)
const [stdout, stderr] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
{ concurrency: 2 },
)
const code = yield* handle.exitCode
return { code, stdout, stderr }
},
Effect.scoped,
Effect.orDie,
)
const methodImpl = Effect.fn("Installation.method")(function* () {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method
if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method
const exec = process.execPath.toLowerCase()
const checks: Array<{ name: Method; command: () => Effect.Effect<string> }> = [
{ name: "npm", command: () => text(["npm", "list", "-g", "--depth=0"]) },
{ name: "yarn", command: () => text(["yarn", "global", "list"]) },
{ name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) },
{ name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) },
{ name: "scoop", command: () => text(["scoop", "list", "opencode"]) },
{ name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) },
]
checks.sort((a, b) => {
const aMatches = exec.includes(a.name)
const bMatches = exec.includes(b.name)
if (aMatches && !bMatches) return -1
if (!aMatches && bMatches) return 1
return 0
})
for (const check of checks) {
const output = yield* check.command()
const installedName =
check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "opencode" : "opencode-ai"
if (output.includes(installedName)) {
return check.name
}
} }
}
return "unknown" as Method return "unknown" as Method
}) })
const latestImpl = Effect.fn("Installation.latest")( const latestImpl = Effect.fn("Installation.latest")(function* (installMethod?: Method) {
function* (installMethod?: Method) {
const detectedMethod = installMethod || (yield* methodImpl()) const detectedMethod = installMethod || (yield* methodImpl())
if (detectedMethod === "brew") { if (detectedMethod === "brew") {
@@ -251,82 +247,80 @@ export namespace Installation {
) )
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,
)
const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) { const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) {
let result: { 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) result = yield* upgradeCurl(target)
break break
case "npm": case "npm":
result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`])
break break
case "pnpm": case "pnpm":
result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`])
break break
case "bun": case "bun":
result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`])
break break
case "brew": { case "brew": {
const formula = yield* getBrewFormula() const formula = yield* getBrewFormula()
const env = { HOMEBREW_NO_AUTO_UPDATE: "1" } const env = { HOMEBREW_NO_AUTO_UPDATE: "1" }
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 result = tap
break
}
const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
const dir = repo.trim()
if (dir) {
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
if (pull.code !== 0) {
result = pull
break break
} }
const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
const dir = repo.trim()
if (dir) {
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
if (pull.code !== 0) {
result = pull
break
}
}
} }
result = yield* run(["brew", "upgrade", formula], { env })
break
} }
result = yield* run(["brew", "upgrade", formula], { env }) case "choco":
break result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"])
break
case "scoop":
result = yield* run(["scoop", "install", `opencode@${target}`])
break
default:
throw new Error(`Unknown method: ${m}`)
} }
case "choco": if (!result || result.code !== 0) {
result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || ""
break return yield* new UpgradeFailedError({ stderr })
case "scoop": }
result = yield* run(["scoop", "install", `opencode@${target}`]) log.info("upgraded", {
break method: m,
default: target,
throw new Error(`Unknown method: ${m}`) stdout: result.stdout,
} stderr: result.stderr,
if (!result || result.code !== 0) { })
const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || "" yield* text([process.execPath, "--version"])
return yield* new UpgradeFailedError({ stderr })
}
log.info("upgraded", {
method: m,
target,
stdout: result.stdout,
stderr: result.stderr,
}) })
yield* text([process.execPath, "--version"])
})
return Service.of({ return Service.of({
info: Effect.fn("Installation.info")(function* () { info: Effect.fn("Installation.info")(function* () {
return { return {
version: VERSION, version: VERSION,
latest: yield* latestImpl(), latest: yield* latestImpl(),
} }
}), }),
method: methodImpl, method: methodImpl,
latest: latestImpl, latest: latestImpl,
upgrade: upgradeImpl, upgrade: upgradeImpl,
}) })
}), }),
) )
export const defaultLayer = layer.pipe( export const defaultLayer = layer.pipe(
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),

View File

@@ -178,9 +178,7 @@ it.effect("config sends the selected org header", () =>
}), }),
) )
const cfg = yield* Account.Service.use((s) => s.config(id, OrgID.make("org-9"))).pipe( const cfg = yield* Account.Service.use((s) => s.config(id, OrgID.make("org-9"))).pipe(Effect.provide(live(client)))
Effect.provide(live(client)),
)
expect(Option.getOrThrow(cfg)).toEqual({ theme: "light", seats: 5 }) expect(Option.getOrThrow(cfg)).toEqual({ theme: "light", seats: 5 })
expect(seen).toEqual({ expect(seen).toEqual({

View File

@@ -44,10 +44,7 @@ function testLayer(
httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response, httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
spawnHandler?: (cmd: string, args: readonly string[]) => string, spawnHandler?: (cmd: string, args: readonly string[]) => string,
) { ) {
return Installation.layer.pipe( return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(mockSpawner(spawnHandler)))
Layer.provide(mockHttpClient(httpHandler)),
Layer.provide(mockSpawner(spawnHandler)),
)
} }
describe("installation", () => { describe("installation", () => {
@@ -139,8 +136,7 @@ describe("installation", () => {
const layer = testLayer( const layer = testLayer(
() => jsonResponse({}), // HTTP not used for tap formula () => jsonResponse({}), // HTTP not used for tap formula
(cmd, args) => { (cmd, args) => {
if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode"
return "opencode"
if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson
return "" return ""
}, },