summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/auth/index.ts19
-rw-r--r--packages/opencode/src/cli/cmd/providers.ts50
-rw-r--r--packages/opencode/src/server/control/index.ts16
-rw-r--r--packages/opencode/src/session/llm.ts26
-rw-r--r--packages/opencode/test/auth/auth.test.ts132
-rw-r--r--packages/opencode/test/bus/bus-effect.test.ts6
-rw-r--r--packages/opencode/test/skill/skill.test.ts6
-rw-r--r--packages/opencode/test/tool/registry.test.ts6
-rw-r--r--packages/opencode/test/tool/skill.test.ts6
9 files changed, 158 insertions, 109 deletions
diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index 2e83fe287..b1502da78 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/packages/opencode/src/auth/index.ts
@@ -1,6 +1,5 @@
import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
-import { makeRuntime } from "@/effect/run-service"
import { zod } from "@/util/effect-zod"
import { Global } from "../global"
import { AppFileSystem } from "../filesystem"
@@ -89,22 +88,4 @@ export namespace Auth {
)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
-
- const { runPromise } = makeRuntime(Service, defaultLayer)
-
- export async function get(providerID: string) {
- return runPromise((service) => service.get(providerID))
- }
-
- export async function all(): Promise<Record<string, Info>> {
- return runPromise((service) => service.all())
- }
-
- export async function set(key: string, info: Info) {
- return runPromise((service) => service.set(key, info))
- }
-
- export async function remove(key: string) {
- return runPromise((service) => service.remove(key))
- }
}
diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts
index 52da44190..829e4e1b4 100644
--- a/packages/opencode/src/cli/cmd/providers.ts
+++ b/packages/opencode/src/cli/cmd/providers.ts
@@ -1,4 +1,5 @@
import { Auth } from "../../auth"
+import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
@@ -13,9 +14,18 @@ import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process"
import { text } from "node:stream/consumers"
+import { Effect } from "effect"
type PluginAuth = NonNullable<Hooks["auth"]>
+const put = (key: string, info: Auth.Info) =>
+ AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.set(key, info)
+ }),
+ )
+
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
let index = 0
if (methodName) {
@@ -93,7 +103,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const saveProvider = result.provider ?? provider
if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
- await Auth.set(saveProvider, {
+ await put(saveProvider, {
type: "oauth",
refresh,
access,
@@ -102,7 +112,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
})
}
if ("key" in result) {
- await Auth.set(saveProvider, {
+ await put(saveProvider, {
type: "api",
key: result.key,
})
@@ -125,7 +135,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const saveProvider = result.provider ?? provider
if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
- await Auth.set(saveProvider, {
+ await put(saveProvider, {
type: "oauth",
refresh,
access,
@@ -134,7 +144,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
})
}
if ("key" in result) {
- await Auth.set(saveProvider, {
+ await put(saveProvider, {
type: "api",
key: result.key,
})
@@ -161,7 +171,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
}
if (result.type === "success") {
const saveProvider = result.provider ?? provider
- await Auth.set(saveProvider, {
+ await put(saveProvider, {
type: "api",
key: result.key ?? key,
})
@@ -221,7 +231,12 @@ export const ProvidersListCommand = cmd({
const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
- const results = Object.entries(await Auth.all())
+ const results = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ return Object.entries(yield* auth.all())
+ }),
+ )
const database = await ModelsDev.get()
for (const [providerID, result] of results) {
@@ -300,7 +315,7 @@ export const ProvidersLoginCommand = cmd({
prompts.outro("Done")
return
}
- await Auth.set(url, {
+ await put(url, {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
@@ -447,7 +462,7 @@ export const ProvidersLoginCommand = cmd({
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(key)) throw new UI.CancelledError()
- await Auth.set(provider, {
+ await put(provider, {
type: "api",
key,
})
@@ -463,22 +478,33 @@ export const ProvidersLogoutCommand = cmd({
describe: "log out from a configured provider",
async handler(_args) {
UI.empty()
- const credentials = await Auth.all().then((x) => Object.entries(x))
+ const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ return Object.entries(yield* auth.all())
+ }),
+ )
prompts.intro("Remove credential")
if (credentials.length === 0) {
prompts.log.error("No credentials found")
return
}
const database = await ModelsDev.get()
- const providerID = await prompts.select({
+ const selected = await prompts.select({
message: "Select provider",
options: credentials.map(([key, value]) => ({
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
value: key,
})),
})
- if (prompts.isCancel(providerID)) throw new UI.CancelledError()
- await Auth.remove(providerID)
+ if (prompts.isCancel(selected)) throw new UI.CancelledError()
+ const providerID = selected as string
+ await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.remove(providerID)
+ }),
+ )
prompts.outro("Logout successful")
},
})
diff --git a/packages/opencode/src/server/control/index.ts b/packages/opencode/src/server/control/index.ts
index aae77f2f0..cf8949c95 100644
--- a/packages/opencode/src/server/control/index.ts
+++ b/packages/opencode/src/server/control/index.ts
@@ -1,5 +1,7 @@
import { Auth } from "@/auth"
+import { AppRuntime } from "@/effect/app-runtime"
import { Log } from "@/util/log"
+import { Effect } from "effect"
import { ProviderID } from "@/provider/schema"
import { Hono } from "hono"
import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
@@ -39,7 +41,12 @@ export function ControlPlaneRoutes(): Hono {
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
- await Auth.set(providerID, info)
+ await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.set(providerID, info)
+ }),
+ )
return c.json(true)
},
)
@@ -69,7 +76,12 @@ export function ControlPlaneRoutes(): Hono {
),
async (c) => {
const providerID = c.req.valid("param").providerID
- await Auth.remove(providerID)
+ await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.remove(providerID)
+ }),
+ )
return c.json(true)
},
)
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index f6e5c9a3f..c3607e177 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -94,14 +94,24 @@ export namespace LLM {
modelID: input.model.id,
providerID: input.model.providerID,
})
- const [language, cfg, provider, auth] = await Promise.all([
- Provider.getLanguage(input.model),
- Config.get(),
- Provider.getProvider(input.model.providerID),
- Auth.get(input.model.providerID),
- ])
+ const [language, cfg, provider, info] = await Effect.runPromise(
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ const cfg = yield* Config.Service
+ const provider = yield* Provider.Service
+ return yield* Effect.all(
+ [
+ provider.getLanguage(input.model),
+ cfg.get(),
+ provider.getProvider(input.model.providerID),
+ auth.get(input.model.providerID),
+ ],
+ { concurrency: "unbounded" },
+ )
+ }).pipe(Effect.provide(Layer.mergeAll(Auth.defaultLayer, Config.defaultLayer, Provider.defaultLayer))),
+ )
// TODO: move this to a proper hook
- const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth"
+ const isOpenaiOauth = provider.id === "openai" && info?.type === "oauth"
const system: string[] = []
system.push(
@@ -200,7 +210,7 @@ export namespace LLM {
},
)
- const tools = await resolveTools(input)
+ const tools = resolveTools(input)
// LiteLLM and some Anthropic proxies require the tools parameter to be present
// when message history contains tool calls, even if no tools are being used.
diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts
index a569c7113..864649d7a 100644
--- a/packages/opencode/test/auth/auth.test.ts
+++ b/packages/opencode/test/auth/auth.test.ts
@@ -1,58 +1,86 @@
-import { test, expect } from "bun:test"
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
import { Auth } from "../../src/auth"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
-test("set normalizes trailing slashes in keys", async () => {
- await Auth.set("https://example.com/", {
- type: "wellknown",
- key: "TOKEN",
- token: "abc",
- })
- const data = await Auth.all()
- expect(data["https://example.com"]).toBeDefined()
- expect(data["https://example.com/"]).toBeUndefined()
-})
+const node = CrossSpawnSpawner.defaultLayer
-test("set cleans up pre-existing trailing-slash entry", async () => {
- // Simulate a pre-fix entry with trailing slash
- await Auth.set("https://example.com/", {
- type: "wellknown",
- key: "TOKEN",
- token: "old",
- })
- // Re-login with normalized key (as the CLI does post-fix)
- await Auth.set("https://example.com", {
- type: "wellknown",
- key: "TOKEN",
- token: "new",
- })
- const data = await Auth.all()
- const keys = Object.keys(data).filter((k) => k.includes("example.com"))
- expect(keys).toEqual(["https://example.com"])
- const entry = data["https://example.com"]!
- expect(entry.type).toBe("wellknown")
- if (entry.type === "wellknown") expect(entry.token).toBe("new")
-})
+const it = testEffect(Layer.mergeAll(Auth.defaultLayer, node))
-test("remove deletes both trailing-slash and normalized keys", async () => {
- await Auth.set("https://example.com", {
- type: "wellknown",
- key: "TOKEN",
- token: "abc",
- })
- await Auth.remove("https://example.com/")
- const data = await Auth.all()
- expect(data["https://example.com"]).toBeUndefined()
- expect(data["https://example.com/"]).toBeUndefined()
-})
+describe("Auth", () => {
+ it.live("set normalizes trailing slashes in keys", () =>
+ provideTmpdirInstance(() =>
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.set("https://example.com/", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "abc",
+ })
+ const data = yield* auth.all()
+ expect(data["https://example.com"]).toBeDefined()
+ expect(data["https://example.com/"]).toBeUndefined()
+ }),
+ ),
+ )
+
+ it.live("set cleans up pre-existing trailing-slash entry", () =>
+ provideTmpdirInstance(() =>
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.set("https://example.com/", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "old",
+ })
+ yield* auth.set("https://example.com", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "new",
+ })
+ const data = yield* auth.all()
+ const keys = Object.keys(data).filter((key) => key.includes("example.com"))
+ expect(keys).toEqual(["https://example.com"])
+ const entry = data["https://example.com"]!
+ expect(entry.type).toBe("wellknown")
+ if (entry.type === "wellknown") expect(entry.token).toBe("new")
+ }),
+ ),
+ )
+
+ it.live("remove deletes both trailing-slash and normalized keys", () =>
+ provideTmpdirInstance(() =>
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.set("https://example.com", {
+ type: "wellknown",
+ key: "TOKEN",
+ token: "abc",
+ })
+ yield* auth.remove("https://example.com/")
+ const data = yield* auth.all()
+ expect(data["https://example.com"]).toBeUndefined()
+ expect(data["https://example.com/"]).toBeUndefined()
+ }),
+ ),
+ )
-test("set and remove are no-ops on keys without trailing slashes", async () => {
- await Auth.set("anthropic", {
- type: "api",
- key: "sk-test",
- })
- const data = await Auth.all()
- expect(data["anthropic"]).toBeDefined()
- await Auth.remove("anthropic")
- const after = await Auth.all()
- expect(after["anthropic"]).toBeUndefined()
+ it.live("set and remove are no-ops on keys without trailing slashes", () =>
+ provideTmpdirInstance(() =>
+ Effect.gen(function* () {
+ const auth = yield* Auth.Service
+ yield* auth.set("anthropic", {
+ type: "api",
+ key: "sk-test",
+ })
+ const data = yield* auth.all()
+ expect(data["anthropic"]).toBeDefined()
+ yield* auth.remove("anthropic")
+ const after = yield* auth.all()
+ expect(after["anthropic"]).toBeUndefined()
+ }),
+ ),
+ )
})
diff --git a/packages/opencode/test/bus/bus-effect.test.ts b/packages/opencode/test/bus/bus-effect.test.ts
index 6f3bcbcfa..6f96a89c8 100644
--- a/packages/opencode/test/bus/bus-effect.test.ts
+++ b/packages/opencode/test/bus/bus-effect.test.ts
@@ -1,10 +1,10 @@
-import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { describe, expect } from "bun:test"
import { Deferred, Effect, Layer, Stream } from "effect"
import z from "zod"
import { Bus } from "../../src/bus"
import { BusEvent } from "../../src/bus/bus-event"
import { Instance } from "../../src/project/instance"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -13,9 +13,7 @@ const TestEvent = {
Pong: BusEvent.define("test.effect.pong", z.object({ message: z.string() })),
}
-const node = NodeChildProcessSpawner.layer.pipe(
- Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
-)
+const node = CrossSpawnSpawner.defaultLayer
const live = Layer.mergeAll(Bus.layer, node)
diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts
index 0a14e30b7..21c6c7e65 100644
--- a/packages/opencode/test/skill/skill.test.ts
+++ b/packages/opencode/test/skill/skill.test.ts
@@ -1,15 +1,13 @@
-import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Skill } from "../../src/skill"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import path from "path"
import fs from "fs/promises"
-const node = NodeChildProcessSpawner.layer.pipe(
- Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
-)
+const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node))
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index 5b59e314e..dea84bdcd 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/packages/opencode/test/tool/registry.test.ts
@@ -1,16 +1,14 @@
-import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { afterEach, describe, expect } from "bun:test"
import path from "path"
import fs from "fs/promises"
import { Effect, Layer } from "effect"
import { Instance } from "../../src/project/instance"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { ToolRegistry } from "../../src/tool/registry"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
-const node = NodeChildProcessSpawner.layer.pipe(
- Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
-)
+const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts
index 1cebf342d..b8b1394ed 100644
--- a/packages/opencode/test/tool/skill.test.ts
+++ b/packages/opencode/test/tool/skill.test.ts
@@ -1,7 +1,7 @@
-import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { Effect, Layer, ManagedRuntime } from "effect"
import { Agent } from "../../src/agent/agent"
import { Skill } from "../../src/skill"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep"
import { Truncate } from "../../src/tool/truncate"
import { afterEach, describe, expect, test } from "bun:test"
@@ -30,9 +30,7 @@ afterEach(async () => {
await Instance.disposeAll()
})
-const node = NodeChildProcessSpawner.layer.pipe(
- Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
-)
+const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))