Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Instance } from "../project/instance"
import { InstanceRuntime } from "../project/instance-runtime"
import { WithInstance } from "../project/with-instance"

export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
return Instance.provide({
return WithInstance.provide({
directory,
fn: async () => {
try {
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/cmd/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import fs from "fs/promises"
import { Filesystem } from "@/util/filesystem"
import matter from "gray-matter"
import { Instance } from "../../project/instance"
import { WithInstance } from "../../project/with-instance"
import { EOL } from "os"
import type { Argv } from "yargs"

Expand Down Expand Up @@ -61,7 +62,7 @@ const AgentCreateCommand = cmd({
describe: "model to use in the format of provider/model",
}),
async handler(args) {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
const cliPath = args.path
Expand Down Expand Up @@ -236,7 +237,7 @@ const AgentListCommand = cmd({
command: "list",
describe: "list all available agents",
async handler() {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { UI } from "../ui"
import { cmd } from "./cmd"
import { ModelsDev } from "@/provider/models"
import { Instance } from "@/project/instance"
import { WithInstance } from "@/project/with-instance"
import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share/session"
import { Session } from "@/session/session"
Expand Down Expand Up @@ -203,7 +204,7 @@ export const GithubInstallCommand = cmd({
command: "install",
describe: "install the GitHub agent",
async handler() {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
{
Expand Down
13 changes: 7 additions & 6 deletions packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "@/config/config"
import { ConfigMCP } from "../../config/mcp"
import { Instance } from "../../project/instance"
import { WithInstance } from "../../project/with-instance"
import { Installation } from "../../installation"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "path"
Expand Down Expand Up @@ -114,7 +115,7 @@ export const McpListCommand = cmd({
aliases: ["ls"],
describe: "list MCP servers and their status",
async handler() {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down Expand Up @@ -186,7 +187,7 @@ export const McpAuthCommand = cmd({
})
.command(McpAuthListCommand),
async handler(args) {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down Expand Up @@ -318,7 +319,7 @@ export const McpAuthListCommand = cmd({
aliases: ["ls"],
describe: "list OAuth-capable MCP servers and their auth status",
async handler() {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down Expand Up @@ -357,7 +358,7 @@ export const McpLogoutCommand = cmd({
type: "string",
}),
async handler(args) {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down Expand Up @@ -448,7 +449,7 @@ export const McpAddCommand = cmd({
command: "add",
describe: "add an MCP server",
async handler() {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down Expand Up @@ -618,7 +619,7 @@ export const McpDebugCommand = cmd({
demandOption: true,
}),
async handler(args) {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import os from "os"
import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
import { WithInstance } from "../../project/with-instance"
import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "@/util/process"
import { text } from "node:stream/consumers"
Expand Down Expand Up @@ -303,7 +303,7 @@ export const ProvidersLoginCommand = cmd({
type: "string",
}),
async handler(args) {
await Instance.provide({
await WithInstance.provide({
directory: process.cwd(),
async fn() {
UI.empty()
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import * as Log from "@opencode-ai/core/util/log"
import { errorData, errorMessage } from "@/util/error"
import { isRecord } from "@/util/record"
import { Instance } from "@/project/instance"
import { WithInstance } from "@/project/with-instance"
import {
readPackageThemes,
readPluginId,
Expand Down Expand Up @@ -790,7 +790,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
state.pending.delete(spec)
return true
}
const ready = await Instance.provide({
const ready = await WithInstance.provide({
directory: state.directory,
fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
}).catch((error) => {
Expand Down Expand Up @@ -986,7 +986,7 @@ async function load(input: { api: Api; config: TuiConfig.Info }) {
}
runtime = next
try {
await Instance.provide({
await WithInstance.provide({
directory: cwd,
fn: async () => {
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/cli/cmd/tui/worker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Installation } from "@/installation"
import { Server } from "@/server/server"
import * as Log from "@opencode-ai/core/util/log"
import { Instance } from "@/project/instance"
import { InstanceRuntime } from "@/project/instance-runtime"
import { WithInstance } from "@/project/with-instance"
import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade"
import { Config } from "@/config/config"
Expand Down Expand Up @@ -77,7 +77,7 @@ export const rpc = {
return { url: server.url.toString() }
},
async checkUpgrade(input: { directory: string }) {
await Instance.provide({
await WithInstance.provide({
directory: input.directory,
fn: async () => {
await upgrade().catch(() => {})
Expand Down
7 changes: 3 additions & 4 deletions packages/opencode/src/effect/app-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { Command } from "@/command"
import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format"
import { InstanceRuntime } from "@/project/instance-runtime"
import { InstanceLayer } from "@/project/instance-layer"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Workspace } from "@/control-plane/workspace"
Expand Down Expand Up @@ -93,17 +93,16 @@ export const AppLayer = Layer.mergeAll(
Truncate.defaultLayer,
ToolRegistry.defaultLayer,
Format.defaultLayer,
InstanceRuntime.layer,
Project.defaultLayer,
Vcs.defaultLayer,
Workspace.defaultLayer,
Worktree.defaultLayer,
Worktree.appLayer,
Pty.defaultLayer,
Installation.defaultLayer,
ShareNext.defaultLayer,
SessionShare.defaultLayer,
SyncEvent.defaultLayer,
).pipe(Layer.provideMerge(Observability.layer))
).pipe(Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(Observability.layer))

const rt = ManagedRuntime.make(AppLayer, { memoMap })
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
Expand Down
11 changes: 11 additions & 0 deletions packages/opencode/src/project/instance-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Effect, Layer } from "effect"
import { InstanceStore } from "./instance-store"

export const layer = Layer.unwrap(
Effect.promise(async () => {
const { InstanceBootstrap } = await import("./bootstrap")
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
}),
)

export * as InstanceLayer from "./instance-layer"
31 changes: 10 additions & 21 deletions packages/opencode/src/project/instance-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import { makeRuntime } from "@/effect/run-service"
import { AppRuntime } from "@/effect/app-runtime"
import { type InstanceContext } from "./instance-context"
import { InstanceStore, type LoadInput } from "./instance-store"
import { Effect, Layer } from "effect"

// Production InstanceStore wiring plus a bridge for Promise/ALS callers that
// cannot yet yield InstanceStore.Service. This keeps InstanceStore itself
// low-level while still giving legacy Hono and CLI paths the production
// bootstrap implementation. Delete the Promise helpers once those callers are
// migrated to Effect boundaries that provide InstanceStore directly.
// Keep the bootstrap implementation import lazy: Instance is imported broadly,
// and importing the app bootstrap graph at module load can trigger ESM cycles.
export const layer = Layer.unwrap(
Effect.promise(async () => {
const { InstanceBootstrap } = await import("./bootstrap")
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
}),
)
// Bridge for Promise/ALS callers that cannot yet yield InstanceStore.Service.
// Delete this module once those callers are migrated to Effect boundaries that
// provide InstanceStore directly.

const runtime = makeRuntime(InstanceStore.Service, layer)

export const load = (input: LoadInput) => runtime.runPromise((store) => store.load(input))
export const disposeInstance = (ctx: InstanceContext) => runtime.runPromise((store) => store.dispose(ctx))
export const disposeAllInstances = () => runtime.runPromise((store) => store.disposeAll())
export const reloadInstance = (input: LoadInput) => runtime.runPromise((store) => store.reload(input))
export const load = (input: LoadInput) => AppRuntime.runPromise(InstanceStore.Service.use((store) => store.load(input)))
export const disposeInstance = (ctx: InstanceContext) =>
AppRuntime.runPromise(InstanceStore.Service.use((store) => store.dispose(ctx)))
export const disposeAllInstances = () => AppRuntime.runPromise(InstanceStore.Service.use((store) => store.disposeAll()))
export const reloadInstance = (input: LoadInput) =>
AppRuntime.runPromise(InstanceStore.Service.use((store) => store.reload(input)))

export * as InstanceRuntime from "./instance-runtime"
27 changes: 9 additions & 18 deletions packages/opencode/src/project/instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,18 @@ import { type InstanceContext } from "./instance-context"
import { InstanceBootstrap } from "./bootstrap-service"
import * as Project from "./project"

export interface LoadInput<R = never> {
export interface LoadInput {
directory: string
/**
* Additional setup to run after the default InstanceBootstrap.
* Mainly used by tests for env-var setup or file writes that need the instance ALS context.
*/
init?: Effect.Effect<void, never, R>
worktree?: string
project?: Project.Info
}

export interface Interface {
readonly load: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
readonly reload: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
readonly load: (input: LoadInput) => Effect.Effect<InstanceContext>
readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext>
readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
readonly disposeAll: () => Effect.Effect<void>
readonly provide: <A, E, R, R2 = never>(
input: LoadInput<R2>,
effect: Effect.Effect<A, E, R>,
) => Effect.Effect<A, E, R | R2>
readonly provide: <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {}
Expand All @@ -44,7 +36,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
const scope = yield* Scope.Scope
const cache = new Map<string, Entry>()

const boot = <R>(input: LoadInput<R> & { directory: string }) =>
const boot = (input: LoadInput & { directory: string }) =>
Effect.gen(function* () {
const ctx: InstanceContext =
input.project && input.worktree
Expand All @@ -61,7 +53,6 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
})),
)
yield* bootstrap.run.pipe(Effect.provideService(InstanceRef, ctx))
if (input.init) yield* input.init.pipe(Effect.provideService(InstanceRef, ctx))
return ctx
}).pipe(Effect.withSpan("InstanceStore.boot"))

Expand All @@ -72,7 +63,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
return true
})

const completeLoad = <R>(directory: string, input: LoadInput<R>, entry: Entry) =>
const completeLoad = (directory: string, input: LoadInput, entry: Entry) =>
Effect.gen(function* () {
const exit = yield* Effect.exit(boot({ ...input, directory }))
if (Exit.isFailure(exit)) yield* removeEntry(directory, entry)
Expand Down Expand Up @@ -108,7 +99,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
return true
})

const load = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
const load = (input: LoadInput): Effect.Effect<InstanceContext> => {
const directory = AppFileSystem.resolve(input.directory)
return Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
Expand All @@ -126,7 +117,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
).pipe(Effect.withSpan("InstanceStore.load"))
}

const reload = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
const reload = (input: LoadInput): Effect.Effect<InstanceContext> => {
const directory = AppFileSystem.resolve(input.directory)
return Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -180,7 +171,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
return yield* cachedDisposeAll
})

const provide = <A, E, R, R2>(input: LoadInput<R2>, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | R2> =>
const provide = <A, E, R>(input: LoadInput, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
load(input).pipe(Effect.flatMap((ctx) => effect.pipe(Effect.provideService(InstanceRef, ctx))))

yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))
Expand Down
7 changes: 0 additions & 7 deletions packages/opencode/src/project/instance.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { Effect } from "effect"
import { context, type InstanceContext } from "./instance-context"
import { InstanceRuntime } from "./instance-runtime"

export type { InstanceContext } from "./instance-context"
export type { LoadInput } from "./instance-store"

export const Instance = {
async provide<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }): Promise<R> {
const ctx = await InstanceRuntime.load({ directory: input.directory, init: input.init })
return context.provide(ctx, async () => input.fn())
},
get current() {
return context.use()
},
Expand Down
10 changes: 10 additions & 0 deletions packages/opencode/src/project/with-instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AppRuntime } from "@/effect/app-runtime"
import { context } from "./instance-context"
import { InstanceStore } from "./instance-store"

export async function provide<R>(input: { directory: string; fn: () => R }): Promise<R> {
const ctx = await AppRuntime.runPromise(InstanceStore.Service.use((store) => store.load({ directory: input.directory })))
return context.provide(ctx, () => input.fn())
}

export * as WithInstance from "./with-instance"
Loading
Loading