Przeglądaj źródła

refactor(auth): remove async auth facade exports (#22306)

Kit Langton 4 dni temu
rodzic
commit
c22e34853d

+ 0 - 19
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))
-  }
 }

+ 38 - 12
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")
   },
 })

+ 14 - 2
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)
       },
     )

+ 18 - 8
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.

+ 80 - 52
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()
+      }),
+    ),
+  )
 })

+ 2 - 4
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)
 

+ 2 - 4
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))
 

+ 2 - 4
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))
 

+ 2 - 4
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))