From ae9a6966075042863cd061f798c0ce09fea3ff64 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 16:35:28 -0400 Subject: [PATCH] refactor: collapse installation barrel into installation/index.ts (#22910) --- packages/opencode/src/installation/index.ts | 339 +++++++++++++++++- .../opencode/src/installation/installation.ts | 336 ----------------- 2 files changed, 338 insertions(+), 337 deletions(-) delete mode 100644 packages/opencode/src/installation/installation.ts diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 4e48fcd6a0..babde9dc47 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1 +1,338 @@ -export * as Installation from "./installation" +import { Effect, Layer, Schema, Context, Stream } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { withTransientReadRetry } from "@/util/effect-http-client" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import path from "path" +import z from "zod" +import { BusEvent } from "@/bus/bus-event" +import { Flag } from "../flag/flag" +import { Log } from "../util" + +import semver from "semver" +import { InstallationChannel, InstallationVersion } from "./version" + +const log = Log.create({ service: "installation" }) + +export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" + +export type ReleaseType = "patch" | "minor" | "major" + +export const Event = { + Updated: BusEvent.define( + "installation.updated", + z.object({ + version: z.string(), + }), + ), + UpdateAvailable: BusEvent.define( + "installation.update-available", + z.object({ + version: z.string(), + }), + ), +} + +export function getReleaseType(current: string, latest: string): ReleaseType { + const currMajor = semver.major(current) + const currMinor = semver.minor(current) + const newMajor = semver.major(latest) + const newMinor = semver.minor(latest) + + if (newMajor > currMajor) return "major" + if (newMinor > currMinor) return "minor" + return "patch" +} + +export const Info = z + .object({ + version: z.string(), + latest: z.string(), + }) + .meta({ + ref: "InstallationInfo", + }) +export type Info = z.infer + +export const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` + +export function isPreview() { + return InstallationChannel !== "latest" +} + +export function isLocal() { + return InstallationChannel === "local" +} + +export class UpgradeFailedError extends Schema.TaggedErrorClass()("UpgradeFailedError", { + stderr: Schema.String, +}) {} + +// Response schemas for external version APIs +const GitHubRelease = Schema.Struct({ tag_name: Schema.String }) +const NpmPackage = Schema.Struct({ version: Schema.String }) +const BrewFormula = Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) }) +const BrewInfoV2 = Schema.Struct({ + formulae: Schema.Array(Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) })), +}) +const ChocoPackage = Schema.Struct({ + d: Schema.Struct({ results: Schema.Array(Schema.Struct({ Version: Schema.String })) }), +}) +const ScoopManifest = NpmPackage + +export interface Interface { + readonly info: () => Effect.Effect + readonly method: () => Effect.Effect + readonly latest: (method?: Method) => Effect.Effect + readonly upgrade: (method: Method, target: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Installation") {} + +export const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const http = yield* HttpClient.HttpClient + const httpOk = HttpClient.filterStatusOk(withTransientReadRetry(http)) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + const text = Effect.fnUntraced( + function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { + const proc = ChildProcess.make(cmd[0], cmd.slice(1), { + cwd: opts?.cwd, + env: opts?.env, + extendEnv: true, + }) + const handle = yield* spawner.spawn(proc) + const out = yield* Stream.mkString(Stream.decodeText(handle.stdout)) + yield* handle.exitCode + return out + }, + Effect.scoped, + Effect.catch(() => Effect.succeed("")), + ) + + const run = Effect.fnUntraced( + function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { + const proc = ChildProcess.make(cmd[0], cmd.slice(1), { + cwd: opts?.cwd, + env: opts?.env, + 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.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })), + ) + + const getBrewFormula = Effect.fnUntraced(function* () { + const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"]) + if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode" + const coreFormula = yield* text(["brew", "list", "--formula", "opencode"]) + if (coreFormula.includes("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 }> = [ + { 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 + }) + + const latestImpl = Effect.fn("Installation.latest")(function* (installMethod?: Method) { + const detectedMethod = installMethod || (yield* methodImpl()) + + if (detectedMethod === "brew") { + const formula = yield* getBrewFormula() + if (formula.includes("/")) { + const infoJson = yield* text(["brew", "info", "--json=v2", formula]) + const info = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(BrewInfoV2))(infoJson) + return info.formulae[0].versions.stable + } + const response = yield* httpOk.execute( + HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe( + HttpClientRequest.acceptJson, + ), + ) + const data = yield* HttpClientResponse.schemaBodyJson(BrewFormula)(response) + return data.versions.stable + } + + if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { + const r = (yield* text(["npm", "config", "get", "registry"])).trim() + const reg = r || "https://registry.npmjs.org" + const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg + const channel = InstallationChannel + const response = yield* httpOk.execute( + HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson), + ) + const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response) + return data.version + } + + if (detectedMethod === "choco") { + const response = yield* httpOk.execute( + HttpClientRequest.get( + "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version", + ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })), + ) + const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response) + return data.d.results[0].Version + } + + if (detectedMethod === "scoop") { + const response = yield* httpOk.execute( + HttpClientRequest.get( + "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", + ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })), + ) + const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response) + return data.version + } + + const response = yield* httpOk.execute( + HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe( + HttpClientRequest.acceptJson, + ), + ) + const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response) + return data.tag_name.replace(/^v/, "") + }, Effect.orDie) + + const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) { + let result: { code: ChildProcessSpawner.ExitCode; stdout: string; stderr: string } | undefined + switch (m) { + case "curl": + result = yield* upgradeCurl(target) + break + case "npm": + result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) + break + case "pnpm": + result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) + break + case "bun": + result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) + break + case "brew": { + const formula = yield* getBrewFormula() + const env = { HOMEBREW_NO_AUTO_UPDATE: "1" } + if (formula.includes("/")) { + const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env }) + if (tap.code !== 0) { + 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 + } + } + } + result = yield* run(["brew", "upgrade", formula], { env }) + break + } + case "choco": + result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) + break + case "scoop": + result = yield* run(["scoop", "install", `opencode@${target}`]) + break + default: + return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` }) + } + if (!result || result.code !== 0) { + const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || "" + 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({ + info: Effect.fn("Installation.info")(function* () { + return { + version: InstallationVersion, + latest: yield* latestImpl(), + } + }), + method: methodImpl, + latest: latestImpl, + upgrade: upgradeImpl, + }) + }), + ) + +export const defaultLayer = layer.pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(CrossSpawnSpawner.defaultLayer), +) + +export * as Installation from "." diff --git a/packages/opencode/src/installation/installation.ts b/packages/opencode/src/installation/installation.ts deleted file mode 100644 index 96a99b77a3..0000000000 --- a/packages/opencode/src/installation/installation.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { Effect, Layer, Schema, Context, Stream } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { withTransientReadRetry } from "@/util/effect-http-client" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import path from "path" -import z from "zod" -import { BusEvent } from "@/bus/bus-event" -import { Flag } from "../flag/flag" -import { Log } from "../util" - -import semver from "semver" -import { InstallationChannel, InstallationVersion } from "./version" - -const log = Log.create({ service: "installation" }) - -export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" - -export type ReleaseType = "patch" | "minor" | "major" - -export const Event = { - Updated: BusEvent.define( - "installation.updated", - z.object({ - version: z.string(), - }), - ), - UpdateAvailable: BusEvent.define( - "installation.update-available", - z.object({ - version: z.string(), - }), - ), -} - -export function getReleaseType(current: string, latest: string): ReleaseType { - const currMajor = semver.major(current) - const currMinor = semver.minor(current) - const newMajor = semver.major(latest) - const newMinor = semver.minor(latest) - - if (newMajor > currMajor) return "major" - if (newMinor > currMinor) return "minor" - return "patch" -} - -export const Info = z - .object({ - version: z.string(), - latest: z.string(), - }) - .meta({ - ref: "InstallationInfo", - }) -export type Info = z.infer - -export const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` - -export function isPreview() { - return InstallationChannel !== "latest" -} - -export function isLocal() { - return InstallationChannel === "local" -} - -export class UpgradeFailedError extends Schema.TaggedErrorClass()("UpgradeFailedError", { - stderr: Schema.String, -}) {} - -// Response schemas for external version APIs -const GitHubRelease = Schema.Struct({ tag_name: Schema.String }) -const NpmPackage = Schema.Struct({ version: Schema.String }) -const BrewFormula = Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) }) -const BrewInfoV2 = Schema.Struct({ - formulae: Schema.Array(Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) })), -}) -const ChocoPackage = Schema.Struct({ - d: Schema.Struct({ results: Schema.Array(Schema.Struct({ Version: Schema.String })) }), -}) -const ScoopManifest = NpmPackage - -export interface Interface { - readonly info: () => Effect.Effect - readonly method: () => Effect.Effect - readonly latest: (method?: Method) => Effect.Effect - readonly upgrade: (method: Method, target: string) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/Installation") {} - -export const layer: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const http = yield* HttpClient.HttpClient - const httpOk = HttpClient.filterStatusOk(withTransientReadRetry(http)) - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - - const text = Effect.fnUntraced( - function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { - const proc = ChildProcess.make(cmd[0], cmd.slice(1), { - cwd: opts?.cwd, - env: opts?.env, - extendEnv: true, - }) - const handle = yield* spawner.spawn(proc) - const out = yield* Stream.mkString(Stream.decodeText(handle.stdout)) - yield* handle.exitCode - return out - }, - Effect.scoped, - Effect.catch(() => Effect.succeed("")), - ) - - const run = Effect.fnUntraced( - function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { - const proc = ChildProcess.make(cmd[0], cmd.slice(1), { - cwd: opts?.cwd, - env: opts?.env, - 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.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })), - ) - - const getBrewFormula = Effect.fnUntraced(function* () { - const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"]) - if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode" - const coreFormula = yield* text(["brew", "list", "--formula", "opencode"]) - if (coreFormula.includes("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 }> = [ - { 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 - }) - - const latestImpl = Effect.fn("Installation.latest")(function* (installMethod?: Method) { - const detectedMethod = installMethod || (yield* methodImpl()) - - if (detectedMethod === "brew") { - const formula = yield* getBrewFormula() - if (formula.includes("/")) { - const infoJson = yield* text(["brew", "info", "--json=v2", formula]) - const info = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(BrewInfoV2))(infoJson) - return info.formulae[0].versions.stable - } - const response = yield* httpOk.execute( - HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe( - HttpClientRequest.acceptJson, - ), - ) - const data = yield* HttpClientResponse.schemaBodyJson(BrewFormula)(response) - return data.versions.stable - } - - if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { - const r = (yield* text(["npm", "config", "get", "registry"])).trim() - const reg = r || "https://registry.npmjs.org" - const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg - const channel = InstallationChannel - const response = yield* httpOk.execute( - HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson), - ) - const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response) - return data.version - } - - if (detectedMethod === "choco") { - const response = yield* httpOk.execute( - HttpClientRequest.get( - "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version", - ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })), - ) - const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response) - return data.d.results[0].Version - } - - if (detectedMethod === "scoop") { - const response = yield* httpOk.execute( - HttpClientRequest.get( - "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", - ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })), - ) - const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response) - return data.version - } - - const response = yield* httpOk.execute( - HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe( - HttpClientRequest.acceptJson, - ), - ) - const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response) - return data.tag_name.replace(/^v/, "") - }, Effect.orDie) - - const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) { - let result: { code: ChildProcessSpawner.ExitCode; stdout: string; stderr: string } | undefined - switch (m) { - case "curl": - result = yield* upgradeCurl(target) - break - case "npm": - result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) - break - case "pnpm": - result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) - break - case "bun": - result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) - break - case "brew": { - const formula = yield* getBrewFormula() - const env = { HOMEBREW_NO_AUTO_UPDATE: "1" } - if (formula.includes("/")) { - const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env }) - if (tap.code !== 0) { - 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 - } - } - } - result = yield* run(["brew", "upgrade", formula], { env }) - break - } - case "choco": - result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) - break - case "scoop": - result = yield* run(["scoop", "install", `opencode@${target}`]) - break - default: - return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` }) - } - if (!result || result.code !== 0) { - const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || "" - 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({ - info: Effect.fn("Installation.info")(function* () { - return { - version: InstallationVersion, - latest: yield* latestImpl(), - } - }), - method: methodImpl, - latest: latestImpl, - upgrade: upgradeImpl, - }) - }), - ) - -export const defaultLayer = layer.pipe( - Layer.provide(FetchHttpClient.layer), - Layer.provide(CrossSpawnSpawner.defaultLayer), -)