diff --git a/packages/opencode/src/cli/cmd/run/runtime.boot.ts b/packages/opencode/src/cli/cmd/run/runtime.boot.ts index 50337c9121..149d6b7312 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.boot.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.boot.ts @@ -9,7 +9,6 @@ import { Context, Effect, Layer } from "effect" import { stringifyKeyStroke } from "@opentui/keymap" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { TuiKeybind } from "@/cli/cmd/tui/config/keybind" -import { AppRuntime } from "@/effect/app-runtime" import { makeRuntime } from "@/effect/run-service" import { reusePendingTask } from "./runtime.shared" import { resolveSession, sessionHistory } from "./session.shared" @@ -62,9 +61,9 @@ const configTask: { current?: Promise } = {} class Service extends Context.Service()("@opencode/RunBoot") {} // Exposed on `TuiConfigLoader` so tests can mock without depending on the -// TuiConfig namespace; production calls TuiConfig.Service via AppRuntime. +// TuiConfig namespace; production calls the TuiConfig per-service runtime. export const TuiConfigLoader = { - get: () => AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.get()).pipe(Effect.provide(TuiConfig.layer))), + get: () => TuiConfig.runPromise((svc) => svc.get()), } function loadConfig() { diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index e322ba85c5..02d402fbf2 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -2,8 +2,6 @@ import { cmd } from "../cmd" import { UI } from "@/cli/ui" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { TuiConfig } from "@/cli/cmd/tui/config/tui" -import { AppRuntime } from "@/effect/app-runtime" -import { Effect } from "effect" import { errorMessage } from "@/util/error" import { validateSession } from "./validate-session" import { ServerAuth } from "@/server/auth" @@ -68,9 +66,7 @@ export const AttachCommand = cmd({ } })() const headers = ServerAuth.headers({ password: args.password, username: args.username }) - const config = await AppRuntime.runPromise( - TuiConfig.Service.use((svc) => svc.get()).pipe(Effect.provide(TuiConfig.layer)), - ) + const config = await TuiConfig.runPromise((svc) => svc.get()) const { tui } = await import("./app") try { diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 47081d721e..56e3e20ef4 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -14,6 +14,7 @@ import { Global } from "@opencode-ai/core/global" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" +import { defineService } from "@/effect/run-service" import { TuiKeybind } from "./keybind" import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" import { Filesystem } from "@/util/filesystem" @@ -244,3 +245,5 @@ export const layer = Layer.effect( ) export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer)) + +export const { runPromise, runFork, runCallback } = defineService(Service, defaultLayer) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 70c2059a6e..669dd6d280 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -14,8 +14,6 @@ import { import path from "path" import { fileURLToPath } from "url" import { TuiConfig } from "@/cli/cmd/tui/config/tui" -import { AppRuntime } from "@/effect/app-runtime" -import { Effect } from "effect" import * as Log from "@opencode-ai/core/util/log" import { errorData, errorMessage } from "@/util/error" import { isRecord } from "@/util/record" @@ -45,12 +43,9 @@ import { createCommandShim } from "./command-shim" ensureRuntimePluginSupport({ additional: keymapRuntimeModules }) // Exposed as a separate function so tests can mock it without touching the -// TuiConfig service. Production callers wait on plugin install fibers via -// AppRuntime + TuiConfig.layer; tests replace this with a no-op. -export const waitForDependencies = () => - AppRuntime.runPromise( - TuiConfig.Service.use((svc) => svc.waitForDependencies()).pipe(Effect.provide(TuiConfig.layer)), - ) +// TuiConfig service. Production callers wait on plugin install fibers via the +// TuiConfig per-service runtime; tests replace this with a no-op. +export const waitForDependencies = () => TuiConfig.runPromise((svc) => svc.waitForDependencies()) type PluginLoad = { options: ConfigPlugin.Options | undefined diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index c46e2f2532..e3412e8bd2 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -14,8 +14,6 @@ import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" -import { AppRuntime } from "@/effect/app-runtime" -import { Effect } from "effect" import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, @@ -189,9 +187,7 @@ export const TuiThreadCommand = cmd({ } const prompt = await input(args.prompt) - const config = await AppRuntime.runPromise( - TuiConfig.Service.use((svc) => svc.get()).pipe(Effect.provide(TuiConfig.layer)), - ) + const config = await TuiConfig.runPromise((svc) => svc.get()) const network = resolveNetworkOptionsNoConfig(args) const external = diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 9dc35a9bd3..8dc5fb58ad 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -2,7 +2,6 @@ import type { Argv } from "yargs" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { Installation } from "../../installation" -import { AppRuntime } from "../../effect/app-runtime" import { Global } from "@opencode-ai/core/global" import fs from "fs/promises" import path from "path" @@ -58,7 +57,7 @@ export const UninstallCommand = { UI.empty() prompts.intro("Uninstall OpenCode") - const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) + const method = await Installation.runPromise((svc) => svc.method()) prompts.log.info(`Installation method: ${method}`) const targets = await collectRemovalTargets(args, method) diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index 26b3ba1867..ec2981c845 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -2,7 +2,6 @@ import type { Argv } from "yargs" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { Installation } from "../../installation" -import { AppRuntime } from "../../effect/app-runtime" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Effect } from "effect" @@ -27,7 +26,7 @@ export const UpgradeCommand = { UI.println(UI.logo(" ")) UI.empty() prompts.intro("Upgrade") - const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) + const detectedMethod = await Installation.runPromise((svc) => svc.method()) const method = (args.method as Installation.Method) ?? detectedMethod if (method === "unknown") { prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`) @@ -47,7 +46,7 @@ export const UpgradeCommand = { prompts.log.info("Using method: " + method) const target = args.target ? args.target.replace(/^v/, "") - : await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest())) + : await Installation.runPromise((svc) => svc.latest()) if (InstallationVersion === target) { prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`) @@ -58,14 +57,11 @@ export const UpgradeCommand = { prompts.log.info(`From ${InstallationVersion} → ${target}`) const spinner = prompts.spinner() spinner.start("Upgrading...") - const err = await AppRuntime.runPromise( - Effect.gen(function* () { - const installation = yield* Installation.Service - return yield* installation.upgrade(method, target).pipe( - Effect.map(() => undefined as Installation.UpgradeFailedError | Error | undefined), - Effect.catch((error) => Effect.succeed(error as Installation.UpgradeFailedError | Error | undefined)), - ) - }), + const err = await Installation.runPromise((installation) => + installation.upgrade(method, target).pipe( + Effect.map(() => undefined as Installation.UpgradeFailedError | Error | undefined), + Effect.catch((error) => Effect.succeed(error as Installation.UpgradeFailedError | Error | undefined)), + ), ).catch((err: Error) => err) if (err) { spinner.stop("Upgrade failed", 1) diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 1f3802e80c..e5260621e2 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -55,3 +55,16 @@ export function makeRuntime(service: Context.Service, layer: Laye getRuntime().runCallback(attach(service.use(fn))), } } + +/** + * Composition of `makeRuntime` for services that own their own runtime. Returns + * the service class, the supplied layer, and the per-service runtime methods + * (`runPromise`, `runFork`, etc.). Each runtime uses the shared `memoMap`, so + * dependencies are built once across all `defineService` runtimes. + * + * Use for services that don't belong in `AppLayer` (TUI-specific config, etc.) + * or for services where call sites want to bypass `AppRuntime` provisioning. + */ +export function defineService(service: Context.Service, layer: Layer.Layer) { + return { Service: service, layer, ...makeRuntime(service, layer) } +} diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 4a6bc3267c..1ad49767f8 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -10,6 +10,7 @@ import * as Log from "@opencode-ai/core/util/log" import semver from "semver" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { NpmConfig } from "@opencode-ai/core/npm-config" +import { defineService } from "@/effect/run-service" const log = Log.create({ service: "installation" }) @@ -324,4 +325,6 @@ export const defaultLayer = layer.pipe( Layer.provide(CrossSpawnSpawner.defaultLayer), ) +export const { runPromise, runFork, runCallback } = defineService(Service, defaultLayer) + export * as Installation from "."