refactor(opencode): roll out serviceUse proxy across 14 services + tests (#28576)

This commit is contained in:
Kit Langton
2026-05-21 03:36:27 +00:00
committed by GitHub
parent 69eee26f30
commit 39ea816a72
48 changed files with 640 additions and 622 deletions
+3
View File
@@ -1,4 +1,5 @@
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect" import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
import { serviceUse } from "@/effect/service-use"
import { import {
FetchHttpClient, FetchHttpClient,
HttpClient, HttpClient,
@@ -181,6 +182,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect( export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { serviceUse } from "@/effect/service-use"
import { Effect, Layer, Option, Schema, Context } from "effect" import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
@@ -38,6 +39,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/AccountRepo") {} export class Service extends Context.Service<Service, Interface>()("@opencode/AccountRepo") {}
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service> = Layer.effect( export const layer: Layer.Layer<Service> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { serviceUse } from "@/effect/service-use"
import { Provider } from "@/provider/provider" import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema" import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai" import { generateObject, streamObject, type ModelMessage } from "ai"
@@ -76,6 +77,8 @@ type State = Omit<Interface, "generate">
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { serviceUse } from "@/effect/service-use"
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import os from "os" import os from "os"
@@ -319,6 +320,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
export const use = serviceUse(Service)
function globalConfigFile() { function globalConfigFile() {
const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) => const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
path.join(Global.Path.config, file), path.join(Global.Path.config, file),
+3
View File
@@ -1,4 +1,5 @@
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
type State = Record<string, string | undefined> type State = Record<string, string | undefined>
@@ -12,6 +13,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Env") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Env") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
@@ -326,6 +327,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/File") {} export class Service extends Context.Service<Service, Interface>()("@opencode/File") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import path from "path" import path from "path"
import { serviceUse } from "@/effect/service-use"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect" import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect"
import type { PlatformError } from "effect/PlatformError" import type { PlatformError } from "effect/PlatformError"
@@ -141,6 +142,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
export const use = serviceUse(Service)
function env() { function env() {
const env = sanitizedProcessEnv() const env = sanitizedProcessEnv()
delete env.RIPGREP_CONFIG_PATH delete env.RIPGREP_CONFIG_PATH
+3
View File
@@ -1,4 +1,5 @@
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context, Schema } from "effect"
import { serviceUse } from "@/effect/service-use"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
@@ -27,6 +28,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
@@ -1,4 +1,5 @@
import { Effect, Layer, Schema, Context, Stream } from "effect" import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
@@ -89,6 +90,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Installation") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Installation") {}
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import path from "path" import path from "path"
import { serviceUse } from "@/effect/service-use"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Effect, Layer, Context, Option, Schema } from "effect" import { Effect, Layer, Context, Option, Schema } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
@@ -51,6 +52,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {} export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
+3
View File
@@ -1,4 +1,5 @@
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai" import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
import { serviceUse } from "@/effect/service-use"
import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
@@ -264,6 +265,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {} export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
@@ -1,4 +1,5 @@
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
import { InstanceRef } from "@/effect/instance-ref" import { InstanceRef } from "@/effect/instance-ref"
import { disposeInstance as runDisposers } from "@/effect/instance-registry" import { disposeInstance as runDisposers } from "@/effect/instance-registry"
@@ -24,6 +25,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {} export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {}
export const use = serviceUse(Service)
interface Entry { interface Entry {
readonly deferred: Deferred.Deferred<InstanceContext> readonly deferred: Deferred.Deferred<InstanceContext>
} }
+3
View File
@@ -1,4 +1,5 @@
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { serviceUse } from "@/effect/service-use"
import { Auth } from "@/auth" import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { optionalOmitUndefined } from "@opencode-ai/core/schema" import { optionalOmitUndefined } from "@opencode-ai/core/schema"
@@ -102,6 +103,8 @@ interface State {
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {} export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
@@ -1,4 +1,5 @@
import type * as SDK from "@opencode-ai/sdk/v2" import type * as SDK from "@opencode-ai/sdk/v2"
import { serviceUse } from "@/effect/service-use"
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect" import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Account } from "@/account/account" import { Account } from "@/account/account"
@@ -76,6 +77,8 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/ShareNext") {} export class Service extends Context.Service<Service, Interface>()("@opencode/ShareNext") {}
export const use = serviceUse(Service)
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) => const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
Effect.sync(() => Database.use(fn)) Effect.sync(() => Database.use(fn))
+20 -20
View File
@@ -18,14 +18,14 @@ const it = testEffect(Layer.merge(AccountRepo.layer, truncate))
it.live("list returns empty when no accounts exist", () => it.live("list returns empty when no accounts exist", () =>
Effect.gen(function* () { Effect.gen(function* () {
const accounts = yield* AccountRepo.Service.use((r) => r.list()) const accounts = yield* AccountRepo.use.list()
expect(accounts).toEqual([]) expect(accounts).toEqual([])
}), }),
) )
it.live("active returns none when no accounts exist", () => it.live("active returns none when no accounts exist", () =>
Effect.gen(function* () { Effect.gen(function* () {
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
expect(Option.isNone(active)).toBe(true) expect(Option.isNone(active)).toBe(true)
}), }),
) )
@@ -45,13 +45,13 @@ it.live("persistAccount inserts and getRow retrieves", () =>
}), }),
) )
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
expect(Option.isSome(row)).toBe(true) expect(Option.isSome(row)).toBe(true)
const value = Option.getOrThrow(row) const value = Option.getOrThrow(row)
expect(value.id).toBe(AccountID.make("user-1")) expect(value.id).toBe(AccountID.make("user-1"))
expect(value.email).toBe("test@example.com") expect(value.email).toBe("test@example.com")
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-1")) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-1"))
}), }),
) )
@@ -72,9 +72,9 @@ it.live("persistAccount normalizes trailing slashes in stored server URLs", () =
}), }),
) )
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
const list = yield* AccountRepo.Service.use((r) => r.list()) const list = yield* AccountRepo.use.list()
expect(Option.getOrThrow(row).url).toBe("https://control.example.com") expect(Option.getOrThrow(row).url).toBe("https://control.example.com")
expect(Option.getOrThrow(active).url).toBe("https://control.example.com") expect(Option.getOrThrow(active).url).toBe("https://control.example.com")
@@ -112,7 +112,7 @@ it.live("persistAccount sets the active account and org", () =>
) )
// Last persisted account is active with its org // Last persisted account is active with its org
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
expect(Option.isSome(active)).toBe(true) expect(Option.isSome(active)).toBe(true)
expect(Option.getOrThrow(active).id).toBe(AccountID.make("user-2")) expect(Option.getOrThrow(active).id).toBe(AccountID.make("user-2"))
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2")) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2"))
@@ -148,7 +148,7 @@ it.live("list returns all accounts", () =>
}), }),
) )
const accounts = yield* AccountRepo.Service.use((r) => r.list()) const accounts = yield* AccountRepo.use.list()
expect(accounts.length).toBe(2) expect(accounts.length).toBe(2)
expect(accounts.map((a) => a.email).sort()).toEqual(["a@example.com", "b@example.com"]) expect(accounts.map((a) => a.email).sort()).toEqual(["a@example.com", "b@example.com"])
}), }),
@@ -170,9 +170,9 @@ it.live("remove deletes an account", () =>
}), }),
) )
yield* AccountRepo.Service.use((r) => r.remove(id)) yield* AccountRepo.use.remove(id)
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
expect(Option.isNone(row)).toBe(true) expect(Option.isNone(row)).toBe(true)
}), }),
) )
@@ -207,12 +207,12 @@ it.live("use stores the selected org and marks the account active", () =>
) )
yield* AccountRepo.Service.use((r) => r.use(id1, Option.some(OrgID.make("org-99")))) yield* AccountRepo.Service.use((r) => r.use(id1, Option.some(OrgID.make("org-99"))))
const active1 = yield* AccountRepo.Service.use((r) => r.active()) const active1 = yield* AccountRepo.use.active()
expect(Option.getOrThrow(active1).id).toBe(id1) expect(Option.getOrThrow(active1).id).toBe(id1)
expect(Option.getOrThrow(active1).active_org_id).toBe(OrgID.make("org-99")) expect(Option.getOrThrow(active1).active_org_id).toBe(OrgID.make("org-99"))
yield* AccountRepo.Service.use((r) => r.use(id1, Option.none())) yield* AccountRepo.Service.use((r) => r.use(id1, Option.none()))
const active2 = yield* AccountRepo.Service.use((r) => r.active()) const active2 = yield* AccountRepo.use.active()
expect(Option.getOrThrow(active2).active_org_id).toBeNull() expect(Option.getOrThrow(active2).active_org_id).toBeNull()
}), }),
) )
@@ -243,7 +243,7 @@ it.live("persistToken updates token fields", () =>
}), }),
) )
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
const value = Option.getOrThrow(row) const value = Option.getOrThrow(row)
expect(value.access_token).toBe(AccessToken.make("new_token")) expect(value.access_token).toBe(AccessToken.make("new_token"))
expect(value.refresh_token).toBe(RefreshToken.make("new_refresh")) expect(value.refresh_token).toBe(RefreshToken.make("new_refresh"))
@@ -276,7 +276,7 @@ it.live("persistToken with no expiry sets token_expiry to null", () =>
}), }),
) )
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
expect(Option.getOrThrow(row).token_expiry).toBeNull() expect(Option.getOrThrow(row).token_expiry).toBeNull()
}), }),
) )
@@ -309,14 +309,14 @@ it.live("persistAccount upserts on conflict", () =>
}), }),
) )
const accounts = yield* AccountRepo.Service.use((r) => r.list()) const accounts = yield* AccountRepo.use.list()
expect(accounts.length).toBe(1) expect(accounts.length).toBe(1)
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
const value = Option.getOrThrow(row) const value = Option.getOrThrow(row)
expect(value.access_token).toBe(AccessToken.make("at_v2")) expect(value.access_token).toBe(AccessToken.make("at_v2"))
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2")) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2"))
}), }),
) )
@@ -337,9 +337,9 @@ it.live("remove clears active state when deleting the active account", () =>
}), }),
) )
yield* AccountRepo.Service.use((r) => r.remove(id)) yield* AccountRepo.use.remove(id)
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
expect(Option.isNone(active)).toBe(true) expect(Option.isNone(active)).toBe(true)
}), }),
) )
@@ -88,7 +88,7 @@ it.live("login normalizes trailing slashes in the provided server URL", () =>
}), }),
) )
const result = yield* Account.Service.use((s) => s.login("https://one.example.com/")).pipe( const result = yield* Account.use.login("https://one.example.com/").pipe(
Effect.provide(live(client)), Effect.provide(live(client)),
) )
@@ -109,7 +109,7 @@ it.live("login maps transport failures to account transport errors", () =>
) )
const error = yield* Effect.flip( const error = yield* Effect.flip(
Account.Service.use((s) => s.login("https://one.example.com")).pipe(Effect.provide(live(client))), Account.use.login("https://one.example.com").pipe(Effect.provide(live(client))),
) )
expect(error).toBeInstanceOf(AccountTransportError) expect(error).toBeInstanceOf(AccountTransportError)
@@ -163,7 +163,7 @@ it.live("orgsByAccount groups orgs per account", () =>
}), }),
) )
const rows = yield* Account.Service.use((s) => s.orgsByAccount()).pipe(Effect.provide(live(client))) const rows = yield* Account.use.orgsByAccount().pipe(Effect.provide(live(client)))
expect(rows.map((row) => [row.account.id, row.orgs.map((org) => org.id)]).map(([id, orgs]) => [id, orgs])).toEqual([ expect(rows.map((row) => [row.account.id, row.orgs.map((org) => org.id)]).map(([id, orgs]) => [id, orgs])).toEqual([
[AccountID.make("user-1"), [OrgID.make("org-1")]], [AccountID.make("user-1"), [OrgID.make("org-1")]],
@@ -201,12 +201,12 @@ it.live("token refresh persists the new token", () =>
), ),
) )
const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client))) const token = yield* Account.use.token(id).pipe(Effect.provide(live(client)))
expect(Option.getOrThrow(token)).toBeDefined() expect(Option.getOrThrow(token)).toBeDefined()
expect(String(Option.getOrThrow(token))).toBe("at_new") expect(String(Option.getOrThrow(token))).toBe("at_new")
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
const value = Option.getOrThrow(row) const value = Option.getOrThrow(row)
expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.access_token).toBe(AccessToken.make("at_new"))
expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new"))
@@ -246,12 +246,12 @@ it.live("token refreshes before expiry when inside the eager refresh window", ()
}), }),
) )
const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client))) const token = yield* Account.use.token(id).pipe(Effect.provide(live(client)))
expect(String(Option.getOrThrow(token))).toBe("at_new") expect(String(Option.getOrThrow(token))).toBe("at_new")
expect(refreshCalls).toBe(1) expect(refreshCalls).toBe(1)
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
const value = Option.getOrThrow(row) const value = Option.getOrThrow(row)
expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.access_token).toBe(AccessToken.make("at_new"))
expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new"))
@@ -315,7 +315,7 @@ it.live("concurrent config and token requests coalesce token refresh", () =>
expect(String(Option.getOrThrow(token))).toBe("at_new") expect(String(Option.getOrThrow(token))).toBe("at_new")
expect(refreshCalls).toBe(1) expect(refreshCalls).toBe(1)
const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const row = yield* AccountRepo.use.getRow(id)
const value = Option.getOrThrow(row) const value = Option.getOrThrow(row)
expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.access_token).toBe(AccessToken.make("at_new"))
expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new"))
@@ -388,7 +388,7 @@ it.live("poll stores the account and first org on success", () =>
expect(res.email).toBe("user@example.com") expect(res.email).toBe("user@example.com")
} }
const active = yield* AccountRepo.Service.use((r) => r.active()) const active = yield* AccountRepo.use.active()
expect(Option.getOrThrow(active)).toEqual( expect(Option.getOrThrow(active)).toEqual(
expect.objectContaining({ expect.objectContaining({
id: "user-1", id: "user-1",
@@ -46,8 +46,8 @@ function testAgent(input: {
it.instance("[#26514] subagent spawned from plan mode inherits read-only restriction (edit denied)", () => it.instance("[#26514] subagent spawned from plan mode inherits read-only restriction (edit denied)", () =>
Effect.gen(function* () { Effect.gen(function* () {
const planAgent = yield* Agent.Service.use((svc) => svc.get("plan")) const planAgent = yield* Agent.use.get("plan")
const generalAgent = yield* Agent.Service.use((svc) => svc.get("general")) const generalAgent = yield* Agent.use.get("general")
expect(planAgent).toBeDefined() expect(planAgent).toBeDefined()
expect(generalAgent).toBeDefined() expect(generalAgent).toBeDefined()
@@ -83,8 +83,8 @@ it.instance("[#26514] explore subagent launched from plan mode also stays read-o
// should propagate the parent **agent** permissions, not just deny edit // should propagate the parent **agent** permissions, not just deny edit
// when the subagent happens to already deny it. // when the subagent happens to already deny it.
Effect.gen(function* () { Effect.gen(function* () {
const planAgent = yield* Agent.Service.use((svc) => svc.get("plan")) const planAgent = yield* Agent.use.get("plan")
const explore = yield* Agent.Service.use((svc) => svc.get("explore")) const explore = yield* Agent.use.get("explore")
expect(planAgent).toBeDefined() expect(planAgent).toBeDefined()
expect(explore).toBeDefined() expect(explore).toBeDefined()
@@ -108,8 +108,8 @@ it.instance(
// be able to edit when the parent agent is `plan`. // be able to edit when the parent agent is `plan`.
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const planAgent = yield* Agent.Service.use((svc) => svc.get("plan")) const planAgent = yield* Agent.use.get("plan")
const my = yield* Agent.Service.use((svc) => svc.get("my_subagent")) const my = yield* Agent.use.get("my_subagent")
expect(planAgent).toBeDefined() expect(planAgent).toBeDefined()
expect(my).toBeDefined() expect(my).toBeDefined()
@@ -51,7 +51,7 @@ it.instance(
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* Plugin.Service.use((p) => p.init()) yield* Plugin.Service.use((p) => p.init())
const agents = yield* Agent.Service.use((svc) => svc.list()) const agents = yield* Agent.use.list()
const added = agents.find((agent) => agent.name === PLUGIN_AGENT.name) const added = agents.find((agent) => agent.name === PLUGIN_AGENT.name)
expect(added?.description).toBe(PLUGIN_AGENT.description) expect(added?.description).toBe(PLUGIN_AGENT.description)
expect(added?.mode).toBe(PLUGIN_AGENT.mode) expect(added?.mode).toBe(PLUGIN_AGENT.mode)
@@ -11,7 +11,7 @@ it.instance(
"agent color parsed from project config", "agent color parsed from project config",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = yield* Config.Service.use((svc) => svc.get()) const cfg = yield* Config.use.get()
expect(cfg.agent?.["build"]?.color).toBe("#FFA500") expect(cfg.agent?.["build"]?.color).toBe("#FFA500")
expect(cfg.agent?.["plan"]?.color).toBe("primary") expect(cfg.agent?.["plan"]?.color).toBe("primary")
}), }),
@@ -30,9 +30,9 @@ it.instance(
"Agent.get includes color from config", "Agent.get includes color from config",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plan = yield* AgentSvc.Service.use((svc) => svc.get("plan")) const plan = yield* AgentSvc.use.get("plan")
expect(plan?.color).toBe("#A855F7") expect(plan?.color).toBe("#A855F7")
const build = yield* AgentSvc.Service.use((svc) => svc.get("build")) const build = yield* AgentSvc.use.get("build")
expect(build?.color).toBe("accent") expect(build?.color).toBe("accent")
}), }),
{ {
+43 -43
View File
@@ -70,14 +70,14 @@ const load = (ctx: InstanceContext) =>
) )
const saveGlobal = (config: Config.Info) => const saveGlobal = (config: Config.Info) =>
Effect.runPromise( Effect.runPromise(
Config.Service.use((svc) => svc.updateGlobal(config)).pipe( Config.use.updateGlobal(config).pipe(
Effect.map((result) => result.info), Effect.map((result) => result.info),
Effect.scoped, Effect.scoped,
Effect.provide(layer), Effect.provide(layer),
), ),
) )
const clear = async (wait = false) => { const clear = async (wait = false) => {
await Effect.runPromise(Config.Service.use((svc) => svc.invalidate()).pipe(Effect.scoped, Effect.provide(layer))) await Effect.runPromise(Config.use.invalidate().pipe(Effect.scoped, Effect.provide(layer)))
if (wait) await InstanceRuntime.disposeAllInstances() if (wait) await InstanceRuntime.disposeAllInstances()
} }
const listDirs = (ctx: InstanceContext) => const listDirs = (ctx: InstanceContext) =>
@@ -162,7 +162,7 @@ async function check(map: (dir: string) => string) {
it.instance("loads config with defaults when no files exist", () => it.instance("loads config with defaults when no files exist", () =>
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.username).toBeDefined() expect(config.username).toBeDefined()
}), }),
) )
@@ -218,7 +218,7 @@ test("does not create global config when OPENCODE_CONFIG_DIR is set", async () =
it.instance( it.instance(
"loads JSON config file", "loads JSON config file",
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.model).toBe("test/model") expect(config.model).toBe("test/model")
expect(config.username).toBe("testuser") expect(config.username).toBe("testuser")
}), }),
@@ -228,7 +228,7 @@ it.instance(
it.instance( it.instance(
"loads shell config field", "loads shell config field",
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.shell).toBe("bash") expect(config.shell).toBe("bash")
}), }),
{ config: { shell: "bash" } }, { config: { shell: "bash" } },
@@ -313,7 +313,7 @@ test("updates global config and omits empty shell key in jsonc", async () => {
it.instance( it.instance(
"loads formatter boolean config", "loads formatter boolean config",
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.formatter).toBe(true) expect(config.formatter).toBe(true)
}), }),
{ config: { formatter: true } }, { config: { formatter: true } },
@@ -322,7 +322,7 @@ it.instance(
it.instance( it.instance(
"loads lsp boolean config", "loads lsp boolean config",
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.lsp).toBe(true) expect(config.lsp).toBe(true)
}), }),
{ config: { lsp: true } }, { config: { lsp: true } },
@@ -355,7 +355,7 @@ it.instance("ignores legacy tui keys in opencode config", () =>
tui: { scroll_speed: 4 }, tui: { scroll_speed: 4 },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.model).toBe("test/model") expect(config.model).toBe("test/model")
expect((config as Record<string, unknown>).theme).toBeUndefined() expect((config as Record<string, unknown>).theme).toBeUndefined()
expect((config as Record<string, unknown>).tui).toBeUndefined() expect((config as Record<string, unknown>).tui).toBeUndefined()
@@ -376,7 +376,7 @@ it.instance("loads JSONC config file", () =>
}`, }`,
), ),
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.model).toBe("test/model") expect(config.model).toBe("test/model")
expect(config.username).toBe("testuser") expect(config.username).toBe("testuser")
}), }),
@@ -398,7 +398,7 @@ it.instance("jsonc overrides json in the same directory", () =>
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
model: "override", model: "override",
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.model).toBe("base") expect(config.model).toBe("base")
expect(config.username).toBe("base") expect(config.username).toBe("base")
}), }),
@@ -414,7 +414,7 @@ it.instance("handles environment variable substitution", () =>
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
username: "{env:TEST_VAR}", username: "{env:TEST_VAR}",
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.username).toBe("test-user") expect(config.username).toBe("test-user")
}), }),
), ),
@@ -435,7 +435,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
}), }),
), ),
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.username).toBe("secret_value") expect(config.username).toBe("secret_value")
// Read the file to verify the env variable was preserved // Read the file to verify the env variable was preserved
@@ -455,7 +455,7 @@ it.instance("handles file inclusion substitution", () =>
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
username: "{file:included.txt}", username: "{file:included.txt}",
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.username).toBe("test-user") expect(config.username).toBe("test-user")
}), }),
) )
@@ -470,7 +470,7 @@ it.instance("handles file inclusion with replacement tokens", () =>
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
username: "{file:included.md}", username: "{file:included.md}",
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.username).toBe("const out = await Bun.$`echo hi`") expect(config.username).toBe("const out = await Bun.$`echo hi`")
}), }),
) )
@@ -547,7 +547,7 @@ it.instance("validates config schema and throws on invalid fields", () =>
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
invalid_field: "should cause error", invalid_field: "should cause error",
}) })
const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit) const exit = yield* Config.use.get().pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true) expect(Exit.isFailure(exit)).toBe(true)
}), }),
) )
@@ -556,7 +556,7 @@ it.instance("throws error for invalid JSON", () =>
Effect.gen(function* () { Effect.gen(function* () {
const test = yield* TestInstance const test = yield* TestInstance
yield* Effect.promise(() => Filesystem.write(path.join(test.directory, "opencode.json"), "{ invalid json }")) yield* Effect.promise(() => Filesystem.write(path.join(test.directory, "opencode.json"), "{ invalid json }"))
const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit) const exit = yield* Config.use.get().pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true) expect(Exit.isFailure(exit)).toBe(true)
}), }),
) )
@@ -574,7 +574,7 @@ it.instance("handles agent configuration", () =>
}, },
}, },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test_agent"]).toEqual( expect(config.agent?.["test_agent"]).toEqual(
expect.objectContaining({ expect.objectContaining({
model: "test/model", model: "test/model",
@@ -598,7 +598,7 @@ it.instance("treats agent variant as model-scoped setting (not provider option)"
}, },
}, },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
const agent = config.agent?.["test_agent"] const agent = config.agent?.["test_agent"]
expect(agent?.variant).toBe("xhigh") expect(agent?.variant).toBe("xhigh")
@@ -622,7 +622,7 @@ it.instance("handles command configuration", () =>
}, },
}, },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.command?.["test_command"]).toEqual({ expect(config.command?.["test_command"]).toEqual({
template: "test template", template: "test template",
description: "test command", description: "test command",
@@ -638,7 +638,7 @@ it.instance("migrates autoshare to share field", () =>
$schema: "https://opencode.ai/config.json", $schema: "https://opencode.ai/config.json",
autoshare: true, autoshare: true,
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.share).toBe("auto") expect(config.share).toBe("auto")
expect(config.autoshare).toBe(true) expect(config.autoshare).toBe(true)
}), }),
@@ -656,7 +656,7 @@ it.instance("migrates mode field to agent field", () =>
}, },
}, },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test_mode"]).toEqual({ expect(config.agent?.["test_mode"]).toEqual({
model: "test/model", model: "test/model",
temperature: 0.5, temperature: 0.5,
@@ -679,7 +679,7 @@ model: test/model
Test agent prompt`, Test agent prompt`,
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]).toEqual( expect(config.agent?.["test"]).toEqual(
expect.objectContaining({ expect.objectContaining({
name: "test", name: "test",
@@ -705,7 +705,7 @@ permission:
Ordered permissions`, Ordered permissions`,
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"]) expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"])
}), }),
) )
@@ -732,7 +732,7 @@ mode: subagent
Nested agent prompt`, Nested agent prompt`,
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["helper"]).toMatchObject({ expect(config.agent?.["helper"]).toMatchObject({
name: "helper", name: "helper",
@@ -770,7 +770,7 @@ description: Nested command
Nested command template`, Nested command template`,
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.command?.["hello"]).toEqual({ expect(config.command?.["hello"]).toEqual({
description: "Test command", description: "Test command",
@@ -804,7 +804,7 @@ description: Nested command
Nested command template`, Nested command template`,
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.command?.["hello"]).toEqual({ expect(config.command?.["hello"]).toEqual({
description: "Test command", description: "Test command",
@@ -834,7 +834,7 @@ it.instance("updates config and writes to file", () =>
it.instance("gets config directories", () => it.instance("gets config directories", () =>
Effect.gen(function* () { Effect.gen(function* () {
const dirs = yield* Config.Service.use((svc) => svc.directories()) const dirs = yield* Config.use.directories()
expect(dirs.length).toBeGreaterThanOrEqual(1) expect(dirs.length).toBeGreaterThanOrEqual(1)
}), }),
) )
@@ -950,7 +950,7 @@ it.instance("resolves scoped npm plugins in config", () =>
yield* writeTextEffect(path.join(pluginDir, "index.js"), "export default {}\n") yield* writeTextEffect(path.join(pluginDir, "index.js"), "export default {}\n")
yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] }) yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.plugin ?? []).toContain("@scope/plugin") expect(config.plugin ?? []).toContain("@scope/plugin")
}), }),
) )
@@ -1014,7 +1014,7 @@ mode: subagent
Helper subagent prompt`, Helper subagent prompt`,
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["helper"]).toMatchObject({ expect(config.agent?.["helper"]).toMatchObject({
name: "helper", name: "helper",
model: "test/model", model: "test/model",
@@ -1212,7 +1212,7 @@ it.instance("migrates legacy tools config to permissions - allow", () =>
agent: { test: { tools: { bash: true, read: true } } }, agent: { test: { tools: { bash: true, read: true } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ expect(config.agent?.["test"]?.permission).toEqual({
bash: "allow", bash: "allow",
read: "allow", read: "allow",
@@ -1228,7 +1228,7 @@ it.instance("migrates legacy tools config to permissions - deny", () =>
agent: { test: { tools: { bash: false, webfetch: false } } }, agent: { test: { tools: { bash: false, webfetch: false } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ expect(config.agent?.["test"]?.permission).toEqual({
bash: "deny", bash: "deny",
webfetch: "deny", webfetch: "deny",
@@ -1244,7 +1244,7 @@ it.instance("migrates legacy write tool to edit permission", () =>
agent: { test: { tools: { write: true } } }, agent: { test: { tools: { write: true } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" }) expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" })
}), }),
) )
@@ -1261,7 +1261,7 @@ it.instance(
share: "disabled", share: "disabled",
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.model).toBe("managed/model") expect(config.model).toBe("managed/model")
expect(config.share).toBe("disabled") expect(config.share).toBe("disabled")
expect(config.username).toBe("testuser") expect(config.username).toBe("testuser")
@@ -1278,7 +1278,7 @@ it.instance(
disabled_providers: ["openai"], disabled_providers: ["openai"],
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.autoupdate).toBe(false) expect(config.autoupdate).toBe(false)
expect(config.disabled_providers).toEqual(["openai"]) expect(config.disabled_providers).toEqual(["openai"])
}), }),
@@ -1288,7 +1288,7 @@ it.instance(
it.instance( it.instance(
"missing managed settings file is not an error", "missing managed settings file is not an error",
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.model).toBe("user/model") expect(config.model).toBe("user/model")
}), }),
{ config: { model: "user/model" } }, { config: { model: "user/model" } },
@@ -1302,7 +1302,7 @@ it.instance("migrates legacy edit tool to edit permission", () =>
agent: { test: { tools: { edit: false } } }, agent: { test: { tools: { edit: false } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny" }) expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny" })
}), }),
) )
@@ -1315,7 +1315,7 @@ it.instance("migrates legacy patch tool to edit permission", () =>
agent: { test: { tools: { patch: true } } }, agent: { test: { tools: { patch: true } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" }) expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" })
}), }),
) )
@@ -1328,7 +1328,7 @@ it.instance("migrates mixed legacy tools config", () =>
agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true } } }, agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ expect(config.agent?.["test"]?.permission).toEqual({
bash: "allow", bash: "allow",
edit: "allow", edit: "allow",
@@ -1346,7 +1346,7 @@ it.instance("merges legacy tools with existing permission config", () =>
agent: { test: { permission: { glob: "allow" }, tools: { bash: true } } }, agent: { test: { permission: { glob: "allow" }, tools: { bash: true } } },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.agent?.["test"]?.permission).toEqual({ expect(config.agent?.["test"]?.permission).toEqual({
glob: "allow", glob: "allow",
bash: "allow", bash: "allow",
@@ -1375,7 +1375,7 @@ it.instance("permission config preserves user key order", () =>
}, },
}) })
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(Object.keys(config.permission!)).toEqual([ expect(Object.keys(config.permission!)).toEqual([
"*", "*",
"edit", "edit",
@@ -1451,7 +1451,7 @@ it.instance("project config can override MCP server enabled status", () =>
"opencode.jsonc", "opencode.jsonc",
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.mcp?.jira).toEqual({ expect(config.mcp?.jira).toEqual({
type: "remote", type: "remote",
url: "https://jira.example.com/mcp", url: "https://jira.example.com/mcp",
@@ -1496,7 +1496,7 @@ it.instance("MCP config deep merges preserving base config properties", () =>
"opencode.jsonc", "opencode.jsonc",
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.mcp?.myserver).toEqual({ expect(config.mcp?.myserver).toEqual({
type: "remote", type: "remote",
url: "https://myserver.example.com/mcp", url: "https://myserver.example.com/mcp",
@@ -1537,7 +1537,7 @@ it.instance("local .opencode config can override MCP from project config", () =>
"opencode.json", "opencode.json",
) )
const config = yield* Config.Service.use((svc) => svc.get()) const config = yield* Config.use.get()
expect(config.mcp?.docs?.enabled).toBe(true) expect(config.mcp?.docs?.enabled).toBe(true)
}), }),
) )
+1 -1
View File
@@ -93,7 +93,7 @@ it.instance("keeps server and tui plugin merge semantics aligned", () =>
plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"], plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"],
}) })
const server = yield* Config.Service.use((svc) => svc.get()) const server = yield* Config.use.get()
const tui = yield* getTuiConfig(test.directory) const tui = yield* getTuiConfig(test.directory)
const serverPlugins = (server.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item)) const serverPlugins = (server.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item))
const tuiPlugins = (tui.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item)) const tuiPlugins = (tui.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item))
File diff suppressed because it is too large Load Diff
@@ -36,7 +36,7 @@ describe("file fsmonitor", () => {
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow())
expect(before.exitCode).not.toBe(0) expect(before.exitCode).not.toBe(0)
yield* File.Service.use((svc) => svc.status()) yield* File.use.status()
const after = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) const after = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow())
expect(after.exitCode).not.toBe(0) expect(after.exitCode).not.toBe(0)
@@ -63,7 +63,7 @@ describe("file fsmonitor", () => {
const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow())
expect(before.exitCode).not.toBe(0) expect(before.exitCode).not.toBe(0)
yield* File.Service.use((svc) => svc.read("tracked.txt")) yield* File.use.read("tracked.txt")
const after = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) const after = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow())
expect(after.exitCode).not.toBe(0) expect(after.exitCode).not.toBe(0)
@@ -10,8 +10,8 @@ import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const it = testEffect(File.defaultLayer) const it = testEffect(File.defaultLayer)
const read = (file: string) => File.Service.use((svc) => svc.read(file)) const read = (file: string) => File.use.read(file)
const list = (dir?: string) => File.Service.use((svc) => svc.list(dir)) const list = (dir?: string) => File.use.list(dir)
const expectAccessDenied = <A, E, R>(effect: Effect.Effect<A, E, R>) => const expectAccessDenied = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.gen(function* () { Effect.gen(function* () {
const exit = yield* effect.pipe(Effect.exit) const exit = yield* effect.pipe(Effect.exit)
+6 -6
View File
@@ -85,7 +85,7 @@ describe("file.ripgrep", () => {
Effect.gen(function* () { Effect.gen(function* () {
const dir = yield* tmpdir((dir) => write(path.join(dir, "match.ts"), "const value = 'other'\n")) const dir = yield* tmpdir((dir) => write(path.join(dir, "match.ts"), "const value = 'other'\n"))
const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })) const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle" })
expect(result.partial).toBe(false) expect(result.partial).toBe(false)
expect(result.items).toEqual([]) expect(result.items).toEqual([])
}), }),
@@ -100,7 +100,7 @@ describe("file.ripgrep", () => {
}), }),
) )
const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })) const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle" })
expect(result.partial).toBe(false) expect(result.partial).toBe(false)
expect(result.items).toHaveLength(1) expect(result.items).toHaveLength(1)
expect(result.items[0]?.path.text).toBe(path.join("src", "match.ts")) expect(result.items[0]?.path.text).toBe(path.join("src", "match.ts"))
@@ -118,7 +118,7 @@ describe("file.ripgrep", () => {
}), }),
) )
const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle", glob: ["*.ts"] })) const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle", glob: ["*.ts"] })
expect(result.partial).toBe(false) expect(result.partial).toBe(false)
expect(result.items).toHaveLength(1) expect(result.items).toHaveLength(1)
expect(result.items[0]?.path.text).toContain("match.ts") expect(result.items[0]?.path.text).toContain("match.ts")
@@ -136,7 +136,7 @@ describe("file.ripgrep", () => {
) )
const file = path.join(dir, "match.ts") const file = path.join(dir, "match.ts")
const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle", file: [file] })) const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle", file: [file] })
expect(result.partial).toBe(false) expect(result.partial).toBe(false)
expect(result.items).toHaveLength(1) expect(result.items).toHaveLength(1)
expect(result.items[0]?.path.text).toBe(file) expect(result.items[0]?.path.text).toBe(file)
@@ -200,7 +200,7 @@ describe("file.ripgrep", () => {
const result = yield* withRipgrepConfig( const result = yield* withRipgrepConfig(
path.join(dir, "missing-ripgreprc"), path.join(dir, "missing-ripgreprc"),
Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })), Ripgrep.use.search({ cwd: dir, pattern: "needle" }),
) )
expect(result.items).toHaveLength(1) expect(result.items).toHaveLength(1)
}), }),
@@ -212,7 +212,7 @@ describe("file.ripgrep", () => {
const result = yield* withRipgrepConfig( const result = yield* withRipgrepConfig(
path.join(dir, "missing-ripgreprc"), path.join(dir, "missing-ripgreprc"),
Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })), Ripgrep.use.search({ cwd: dir, pattern: "needle" }),
) )
expect(result.items).toHaveLength(1) expect(result.items).toHaveLength(1)
}), }),
+3 -3
View File
@@ -133,7 +133,7 @@ describe("Format", () => {
const file = `${dir}/test.txt` const file = `${dir}/test.txt`
yield* Effect.promise(() => Bun.write(file, "x")) yield* Effect.promise(() => Bun.write(file, "x"))
const formatted = yield* Format.Service.use((fmt) => fmt.file(file)) const formatted = yield* Format.use.file(file)
expect(formatted).toBe(false) expect(formatted).toBe(false)
}), }),
{ {
@@ -146,10 +146,10 @@ describe("Format", () => {
it.live("status() initializes formatter state per directory", () => it.live("status() initializes formatter state per directory", () =>
Effect.gen(function* () { Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.Service.use((fmt) => fmt.status()), { const a = yield* provideTmpdirInstance(() => Format.use.status(), {
config: { formatter: false }, config: { formatter: false },
}) })
const b = yield* provideTmpdirInstance(() => Format.Service.use((fmt) => fmt.status()), { const b = yield* provideTmpdirInstance(() => Format.use.status(), {
config: { config: {
formatter: true, formatter: true,
}, },
@@ -58,7 +58,7 @@ describe("installation", () => {
"reads release version from GitHub releases", "reads release version from GitHub releases",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("unknown")) const result = yield* Installation.use.latest("unknown")
expect(result).toBe("1.2.3") expect(result).toBe("1.2.3")
}), }),
) )
@@ -67,7 +67,7 @@ describe("installation", () => {
"strips v prefix from GitHub release tag", "strips v prefix from GitHub release tag",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("curl")) const result = yield* Installation.use.latest("curl")
expect(result).toBe("4.0.0-beta.1") expect(result).toBe("4.0.0-beta.1")
}), }),
) )
@@ -80,7 +80,7 @@ describe("installation", () => {
}), }),
).effect("reads npm versions via registry", () => ).effect("reads npm versions via registry", () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("npm")) const result = yield* Installation.use.latest("npm")
expect(result).toBe("1.5.0") expect(result).toBe("1.5.0")
expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
}), }),
@@ -94,7 +94,7 @@ describe("installation", () => {
}), }),
).effect("reads bun versions via registry", () => ).effect("reads bun versions via registry", () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("bun")) const result = yield* Installation.use.latest("bun")
expect(result).toBe("1.6.0") expect(result).toBe("1.6.0")
expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
}), }),
@@ -108,7 +108,7 @@ describe("installation", () => {
}), }),
).effect("reads pnpm versions via registry", () => ).effect("reads pnpm versions via registry", () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("pnpm")) const result = yield* Installation.use.latest("pnpm")
expect(result).toBe("1.7.0") expect(result).toBe("1.7.0")
expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
}), }),
@@ -116,7 +116,7 @@ describe("installation", () => {
testEffect(testLayer(() => jsonResponse({ version: "2.3.4" }))).effect("reads scoop manifest versions", () => testEffect(testLayer(() => jsonResponse({ version: "2.3.4" }))).effect("reads scoop manifest versions", () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("scoop")) const result = yield* Installation.use.latest("scoop")
expect(result).toBe("2.3.4") expect(result).toBe("2.3.4")
}), }),
) )
@@ -125,7 +125,7 @@ describe("installation", () => {
"reads chocolatey feed versions", "reads chocolatey feed versions",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("choco")) const result = yield* Installation.use.latest("choco")
expect(result).toBe("3.4.5") expect(result).toBe("3.4.5")
}), }),
) )
@@ -142,7 +142,7 @@ describe("installation", () => {
), ),
).effect("reads brew formulae API versions", () => ).effect("reads brew formulae API versions", () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("brew")) const result = yield* Installation.use.latest("brew")
expect(result).toBe("2.0.0") expect(result).toBe("2.0.0")
}), }),
) )
@@ -161,7 +161,7 @@ describe("installation", () => {
), ),
).effect("reads brew tap info JSON via CLI", () => ).effect("reads brew tap info JSON via CLI", () =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* Installation.Service.use((svc) => svc.latest("brew")) const result = yield* Installation.use.latest("brew")
expect(result).toBe("2.1.0") expect(result).toBe("2.1.0")
}), }),
) )
@@ -175,7 +175,7 @@ mcpTest.instance("state() generates a new state when none is saved", () =>
auth, auth,
) )
const entryBefore = yield* McpAuth.Service.use((auth) => auth.get("test-state-gen")) const entryBefore = yield* McpAuth.use.get("test-state-gen")
expect(entryBefore?.oauthState).toBeUndefined() expect(entryBefore?.oauthState).toBeUndefined()
// state() should generate and return a new state, not throw // state() should generate and return a new state, not throw
@@ -184,7 +184,7 @@ mcpTest.instance("state() generates a new state when none is saved", () =>
expect(state.length).toBe(64) // 32 bytes as hex expect(state.length).toBe(64) // 32 bytes as hex
// The generated state should be persisted // The generated state should be persisted
const entryAfter = yield* McpAuth.Service.use((auth) => auth.get("test-state-gen")) const entryAfter = yield* McpAuth.use.get("test-state-gen")
expect(entryAfter?.oauthState).toBe(state) expect(entryAfter?.oauthState).toBe(state)
}), }),
) )
@@ -202,7 +202,7 @@ mcpTest.instance("state() returns existing state when one is saved", () =>
// Pre-save a state // Pre-save a state
const existingState = "pre-saved-state-value" const existingState = "pre-saved-state-value"
yield* McpAuth.Service.use((auth) => auth.updateOAuthState("test-state-existing", existingState)) yield* McpAuth.use.updateOAuthState("test-state-existing", existingState)
// state() should return the existing state // state() should return the existing state
const state = yield* Effect.promise(() => provider.state()) const state = yield* Effect.promise(() => provider.state())
@@ -6,7 +6,7 @@ import { testEffect } from "./lib/effect"
const it = testEffect(Config.defaultLayer) const it = testEffect(Config.defaultLayer)
const load = Config.Service.use((svc) => svc.get()) const load = Config.use.get()
describe("Permission.evaluate for permission.task", () => { describe("Permission.evaluate for permission.task", () => {
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): Permission.Ruleset => const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): Permission.Ruleset =>
@@ -72,10 +72,10 @@ describe("plugin.auth-override", () => {
const plain = yield* tmpdirScoped({ git: true }) const plain = yield* tmpdirScoped({ git: true })
const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href
const methods = yield* ProviderAuth.Service.use((svc) => svc.methods()).pipe( const methods = yield* ProviderAuth.use.methods().pipe(
Effect.provide(layer(tmp.directory, [plugin])), Effect.provide(layer(tmp.directory, [plugin])),
) )
const plainMethods = yield* ProviderAuth.Service.use((svc) => svc.methods()).pipe( const plainMethods = yield* ProviderAuth.use.methods().pipe(
Effect.provide(layer(plain, [])), Effect.provide(layer(plain, [])),
provideInstance(plain), provideInstance(plain),
) )
@@ -18,7 +18,7 @@ const set = (k: string, v: string) =>
Effect.gen(function* () { Effect.gen(function* () {
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k]) if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
process.env[k] = v process.env[k] = v
yield* Env.Service.use((svc) => svc.set(k, v)) yield* Env.use.set(k, v)
}) })
afterEach(async () => { afterEach(async () => {
@@ -30,7 +30,7 @@ afterEach(async () => {
await disposeAllInstances() await disposeAllInstances()
}) })
const list = Provider.Service.use((svc) => svc.list()) const list = Provider.use.list()
const withAuthJson = (contents: string) => const withAuthJson = (contents: string) =>
Effect.acquireRelease( Effect.acquireRelease(
@@ -35,14 +35,14 @@ const set = (k: string, v: string) =>
Effect.gen(function* () { Effect.gen(function* () {
rememberEnv(k) rememberEnv(k)
process.env[k] = v process.env[k] = v
yield* Env.Service.use((svc) => svc.set(k, v)) yield* Env.use.set(k, v)
}) })
const remove = (k: string) => const remove = (k: string) =>
Effect.gen(function* () { Effect.gen(function* () {
rememberEnv(k) rememberEnv(k)
delete process.env[k] delete process.env[k]
yield* Env.Service.use((svc) => svc.remove(k)) yield* Env.use.remove(k)
}) })
afterEach(async () => { afterEach(async () => {
@@ -332,7 +332,7 @@ test("parseModel handles model IDs with slashes", () => {
it.instance("defaultModel returns first available model when no config set", () => it.instance("defaultModel returns first available model when no config set", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.Service.use((provider) => provider.defaultModel()) const model = yield* Provider.use.defaultModel()
expect(model.providerID).toBeDefined() expect(model.providerID).toBeDefined()
expect(model.modelID).toBeDefined() expect(model.modelID).toBeDefined()
}), }),
@@ -342,7 +342,7 @@ it.instance(
"defaultModel respects config model setting", "defaultModel respects config model setting",
Effect.gen(function* () { Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.Service.use((provider) => provider.defaultModel()) const model = yield* Provider.use.defaultModel()
expect(String(model.providerID)).toBe("anthropic") expect(String(model.providerID)).toBe("anthropic")
expect(String(model.modelID)).toBe("claude-sonnet-4-20250514") expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
}), }),
@@ -1032,7 +1032,7 @@ it.instance("getProvider returns undefined for nonexistent provider", () =>
it.instance("getProvider returns provider info", () => it.instance("getProvider returns provider info", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key") yield* set("ANTHROPIC_API_KEY", "test-api-key")
const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderID.anthropic)) const provider = yield* Provider.use.getProvider(ProviderID.anthropic)
expect(provider).toBeDefined() expect(provider).toBeDefined()
expect(String(provider?.id)).toBe("anthropic") expect(String(provider?.id)).toBe("anthropic")
}), }),
@@ -1680,7 +1680,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", ()
}) })
const listIn = (directory: string) => const listIn = (directory: string) =>
Provider.Service.use((svc) => svc.list()) Provider.use.list()
.pipe(provideInstanceEffect(directory)) .pipe(provideInstanceEffect(directory))
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) .pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
@@ -1698,7 +1698,7 @@ it.effect("opencode loader keeps paid models when auth exists", () =>
const keyedDir = yield* tmpdirScoped() const keyedDir = yield* tmpdirScoped()
const listIn = (directory: string) => const listIn = (directory: string) =>
Provider.Service.use((svc) => svc.list()) Provider.use.list()
.pipe(provideInstanceEffect(directory)) .pipe(provideInstanceEffect(directory))
.pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) .pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer))
@@ -13,7 +13,7 @@ const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Project.defaultLaye
const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) => const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) =>
Effect.acquireRelease( Effect.acquireRelease(
SessionNs.Service.use((session) => session.create(input)), SessionNs.use.create(input),
(created) => SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)), (created) => SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)),
) )
@@ -34,8 +34,8 @@ describe("session.listGlobal", () => {
expect(ids).toContain(firstSession.id) expect(ids).toContain(firstSession.id)
expect(ids).toContain(secondSession.id) expect(ids).toContain(secondSession.id)
const firstProject = yield* Project.Service.use((project) => project.get(firstSession.projectID)) const firstProject = yield* Project.use.get(firstSession.projectID)
const secondProject = yield* Project.Service.use((project) => project.get(secondSession.projectID)) const secondProject = yield* Project.use.get(secondSession.projectID)
const firstItem = sessions.find((session) => session.id === firstSession.id) const firstItem = sessions.find((session) => session.id === firstSession.id)
const secondItem = sessions.find((session) => session.id === secondSession.id) const secondItem = sessions.find((session) => session.id === secondSession.id)
@@ -31,7 +31,7 @@ function request(path: string, directory: string, init: RequestInit = {}) {
} }
function createSession(input?: Session.CreateInput) { function createSession(input?: Session.CreateInput) {
return Session.Service.use((svc) => svc.create(input)) return Session.use.create(input)
} }
function json<T>(response: Response) { function json<T>(response: Response) {
@@ -74,7 +74,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri
projectID: input.projectID, projectID: input.projectID,
}) })
}), }),
(info) => Workspace.Service.use((workspace) => workspace.remove(info.id)).pipe(Effect.ignore), (info) => Workspace.use.remove(info.id).pipe(Effect.ignore),
) )
const probeInstanceContext = Effect.gen(function* () { const probeInstanceContext = Effect.gen(function* () {
@@ -52,7 +52,7 @@ function pathFor(path: string, params: Record<string, string>) {
} }
function createSession(input?: Session.CreateInput) { function createSession(input?: Session.CreateInput) {
return Session.Service.use((svc) => svc.create(input)) return Session.use.create(input)
} }
function createTextMessage(sessionID: SessionIDType, text: string) { function createTextMessage(sessionID: SessionIDType, text: string) {
@@ -101,7 +101,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri
}), }),
) )
}), }),
(info) => Workspace.Service.use((svc) => svc.remove(info.id)).pipe(Effect.ignore), (info) => Workspace.use.remove(info.id).pipe(Effect.ignore),
) )
const insertLegacyAssistantMessage = (sessionID: SessionIDType, time = 1) => const insertLegacyAssistantMessage = (sessionID: SessionIDType, time = 1) =>
@@ -36,7 +36,7 @@ describe("sync HttpApi", () => {
const tmp = yield* TestInstance const tmp = yield* TestInstance
const headers = { "x-opencode-directory": tmp.directory, "content-type": "application/json" } const headers = { "x-opencode-directory": tmp.directory, "content-type": "application/json" }
const info = spyOn(Log.create({ service: "server.sync" }), "info") const info = spyOn(Log.create({ service: "server.sync" }), "info")
const session = yield* Session.Service.use((svc) => svc.create({ title: "sync" })) const session = yield* Session.use.create({ title: "sync" })
const started = yield* Effect.promise(() => const started = yield* Effect.promise(() =>
Promise.resolve(app().request(SyncPaths.start, { method: "POST", headers })), Promise.resolve(app().request(SyncPaths.start, { method: "POST", headers })),
@@ -128,7 +128,7 @@ const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; a
projectID: input.projectID, projectID: input.projectID,
}) })
}), }),
(info) => Workspace.Service.use((workspace) => workspace.remove(info.id)).pipe(Effect.ignore), (info) => Workspace.use.remove(info.id).pipe(Effect.ignore),
) )
const createRemoteWorkspace = (input: { const createRemoteWorkspace = (input: {
@@ -208,7 +208,7 @@ describe("workspace HttpApi", () => {
const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info
expect(workspace).toMatchObject({ type: "local-test", name: "local-test" }) expect(workspace).toMatchObject({ type: "local-test", name: "local-test" })
const session = yield* Session.Service.use((svc) => svc.create({})).pipe(provideInstance(dir)) const session = yield* Session.use.create({}).pipe(provideInstance(dir))
const warped = yield* request(WorkspacePaths.warp, dir, { const warped = yield* request(WorkspacePaths.warp, dir, {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
@@ -424,7 +424,7 @@ describe("workspace HttpApi", () => {
body: JSON.stringify({ type: "remote-session-target", branch: null }), body: JSON.stringify({ type: "remote-session-target", branch: null }),
}) })
const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info
const session = yield* Session.Service.use((svc) => svc.create()).pipe( const session = yield* Session.use.create().pipe(
Effect.provideService(WorkspaceRef, workspace.id), Effect.provideService(WorkspaceRef, workspace.id),
provideInstance(dir), provideInstance(dir),
) )
@@ -83,7 +83,7 @@ describe("project.initGit endpoint", () => {
worktree: tmp.directory, worktree: tmp.directory,
}) })
const ctx = yield* InstanceStore.Service.use((store) => store.reload({ directory: tmp.directory })) const ctx = yield* InstanceStore.use.reload({ directory: tmp.directory })
const tracked = yield* Snapshot.Service.use((snapshot) => snapshot.track()).pipe( const tracked = yield* Snapshot.Service.use((snapshot) => snapshot.track()).pipe(
Effect.provideService(InstanceRef, ctx), Effect.provideService(InstanceRef, ctx),
) )
@@ -22,8 +22,8 @@ describe("session action routes", () => {
Effect.gen(function* () { Effect.gen(function* () {
const test = yield* TestInstance const test = yield* TestInstance
const session = yield* Effect.acquireRelease( const session = yield* Effect.acquireRelease(
SessionNs.Service.use((svc) => svc.create({})), SessionNs.use.create({}),
(created) => SessionNs.Service.use((svc) => svc.remove(created.id)).pipe(Effect.ignore), (created) => SessionNs.use.remove(created.id).pipe(Effect.ignore),
) )
const res = yield* Effect.promise(() => const res = yield* Effect.promise(() =>
@@ -35,8 +35,8 @@ function pathFor(template: string, params: Record<string, string>) {
const withSession = (input?: Parameters<Session.Interface["create"]>[0]) => const withSession = (input?: Parameters<Session.Interface["create"]>[0]) =>
Effect.acquireRelease( Effect.acquireRelease(
Session.Service.use((session) => session.create(input)), Session.use.create(input),
(created) => Session.Service.use((session) => session.remove(created.id)).pipe(Effect.ignore), (created) => Session.use.remove(created.id).pipe(Effect.ignore),
) )
describe("session diff with missing patch (#26574)", () => { describe("session diff with missing patch (#26574)", () => {
@@ -28,7 +28,7 @@ const it = testEffect(
const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) => const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) =>
Effect.acquireRelease( Effect.acquireRelease(
SessionNs.Service.use((session) => session.create(input)), SessionNs.use.create(input),
(created) => SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)), (created) => SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)),
) )
@@ -56,7 +56,7 @@ describe("session.list", () => {
provideInstance(path.join(test.directory, "packages", "app")), provideInstance(path.join(test.directory, "packages", "app")),
) )
const ids = (yield* SessionNs.Service.use((session) => session.list())).map((session) => session.id) const ids = (yield* SessionNs.use.list()).map((session) => session.id)
expect(ids).toContain(root.id) expect(ids).toContain(root.id)
expect(ids).toContain(parent.id) expect(ids).toContain(parent.id)
expect(ids).toContain(current.id) expect(ids).toContain(current.id)
@@ -179,7 +179,7 @@ describe("session.list", () => {
const root = yield* withSession({ title: "root-session" }) const root = yield* withSession({ title: "root-session" })
const child = yield* withSession({ title: "child-session", parentID: root.id }) const child = yield* withSession({ title: "child-session", parentID: root.id })
const sessions = yield* SessionNs.Service.use((session) => session.list({ roots: true })) const sessions = yield* SessionNs.use.list({ roots: true })
const ids = sessions.map((session) => session.id) const ids = sessions.map((session) => session.id)
expect(ids).toContain(root.id) expect(ids).toContain(root.id)
@@ -206,7 +206,7 @@ describe("session.list", () => {
yield* withSession({ title: "unique-search-term-abc" }) yield* withSession({ title: "unique-search-term-abc" })
yield* withSession({ title: "other-session-xyz" }) yield* withSession({ title: "other-session-xyz" })
const sessions = yield* SessionNs.Service.use((session) => session.list({ search: "unique-search" })) const sessions = yield* SessionNs.use.list({ search: "unique-search" })
const titles = sessions.map((session) => session.title) const titles = sessions.map((session) => session.title)
expect(titles).toContain("unique-search-term-abc") expect(titles).toContain("unique-search-term-abc")
@@ -223,7 +223,7 @@ describe("session.list", () => {
yield* withSession({ title: "session-2" }) yield* withSession({ title: "session-2" })
yield* withSession({ title: "session-3" }) yield* withSession({ title: "session-3" })
const sessions = yield* SessionNs.Service.use((session) => session.list({ limit: 2 })) const sessions = yield* SessionNs.use.list({ limit: 2 })
expect(sessions.length).toBe(2) expect(sessions.length).toBe(2)
}), }),
{ git: true }, { git: true },
@@ -40,8 +40,8 @@ const withoutWatcher = <A, E, R>(effect: Effect.Effect<A, E, R>) => {
} }
const sessionScoped = Effect.acquireRelease( const sessionScoped = Effect.acquireRelease(
SessionNs.Service.use((svc) => svc.create({})), SessionNs.use.create({}),
(session) => SessionNs.Service.use((svc) => svc.remove(session.id)).pipe(Effect.ignore), (session) => SessionNs.use.remove(session.id).pipe(Effect.ignore),
) )
const fill = Effect.fn("SessionMessagesTest.fill")(function* ( const fill = Effect.fn("SessionMessagesTest.fill")(function* (
@@ -16,7 +16,7 @@ describe("tui.selectSession endpoint", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const tmp = yield* TestInstance const tmp = yield* TestInstance
const session = yield* Session.Service.use((svc) => svc.create({})) const session = yield* Session.use.create({})
const app = Server.Default().app const app = Server.Default().app
const response = yield* Effect.promise(() => const response = yield* Effect.promise(() =>
@@ -292,7 +292,7 @@ function createSummaryCompaction(sessionID: SessionID) {
} }
function readCompactionPart(sessionID: SessionID) { function readCompactionPart(sessionID: SessionID) {
return SessionNs.Service.use((ssn) => ssn.messages({ sessionID })).pipe( return SessionNs.use.messages({ sessionID }).pipe(
Effect.map((messages) => Effect.map((messages) =>
messages.at(-2)?.parts.find((item): item is MessageV2.CompactionPart => item.type === "compaction"), messages.at(-2)?.parts.find((item): item is MessageV2.CompactionPart => item.type === "compaction"),
), ),
@@ -35,7 +35,7 @@ const awaitDeferred = <T>(deferred: Deferred.Deferred<T>, message: string) =>
Effect.sleep("2 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error(message)))), Effect.sleep("2 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error(message)))),
) )
const remove = (id: SessionID) => SessionNs.Service.use((svc) => svc.remove(id)) const remove = (id: SessionID) => SessionNs.use.remove(id)
const subscribeGlobal = (type: string, callback: (event: NonNullable<GlobalEvent["payload"]>) => void) => { const subscribeGlobal = (type: string, callback: (event: NonNullable<GlobalEvent["payload"]>) => void) => {
const listener = (event: GlobalEvent) => { const listener = (event: GlobalEvent) => {
@@ -128,7 +128,7 @@ describe("ShareNext", () => {
Effect.gen(function* () { Effect.gen(function* () {
yield* seed("https://control.example.com", "org-1") yield* seed("https://control.example.com", "org-1")
const req = yield* ShareNext.Service.use((svc) => svc.request()).pipe(Effect.provide(live(none))) const req = yield* ShareNext.use.request().pipe(Effect.provide(live(none)))
expect(req.api.create).toBe("/api/shares") expect(req.api.create).toBe("/api/shares")
expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync") expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync")
@@ -147,7 +147,7 @@ describe("ShareNext", () => {
provideTmpdirInstance( provideTmpdirInstance(
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* Session.Service.use((svc) => svc.create({ title: "test" })) const session = yield* Session.use.create({ title: "test" })
const seen: HttpClientRequest.HttpClientRequest[] = [] const seen: HttpClientRequest.HttpClientRequest[] = []
const client = HttpClient.make((req) => { const client = HttpClient.make((req) => {
seen.push(req) seen.push(req)
@@ -163,7 +163,7 @@ describe("ShareNext", () => {
return Effect.succeed(json(req, { ok: true })) return Effect.succeed(json(req, { ok: true }))
}) })
const result = yield* ShareNext.Service.use((svc) => svc.create(session.id)).pipe( const result = yield* ShareNext.use.create(session.id).pipe(
Effect.provide(live(client)), Effect.provide(live(client)),
) )
@@ -188,7 +188,7 @@ describe("ShareNext", () => {
provideTmpdirInstance( provideTmpdirInstance(
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* Session.Service.use((svc) => svc.create({ title: "test" })) const session = yield* Session.use.create({ title: "test" })
const seen: HttpClientRequest.HttpClientRequest[] = [] const seen: HttpClientRequest.HttpClientRequest[] = []
const client = HttpClient.make((req) => { const client = HttpClient.make((req) => {
seen.push(req) seen.push(req)
@@ -205,8 +205,8 @@ describe("ShareNext", () => {
}) })
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
yield* ShareNext.Service.use((svc) => svc.create(session.id)) yield* ShareNext.use.create(session.id)
yield* ShareNext.Service.use((svc) => svc.remove(session.id)) yield* ShareNext.use.remove(session.id)
}).pipe(Effect.provide(live(client))) }).pipe(Effect.provide(live(client)))
expect(share(session.id)).toBeUndefined() expect(share(session.id)).toBeUndefined()
@@ -222,7 +222,7 @@ describe("ShareNext", () => {
it.live("create fails on a non-ok response and does not persist a share", () => it.live("create fails on a non-ok response and does not persist a share", () =>
provideTmpdirInstance(() => provideTmpdirInstance(() =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* Session.Service.use((svc) => svc.create({ title: "test" })) const session = yield* Session.use.create({ title: "test" })
const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500))) const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500)))
const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))).pipe( const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))).pipe(