From 5cfb70e93ab22e44f7fa5f143282cf30b3957a36 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 8 Aug 2026 21:53:11 -0500 Subject: [PATCH] refactor(plugin): own integration method types --- packages/core/src/plugin/host.ts | 6 +- packages/core/test/plugin/host.ts | 73 +++------------------- packages/plugin/src/effect/integration.ts | 44 ++++++++++--- packages/plugin/src/promise/integration.ts | 44 ++++++++++--- 4 files changed, 80 insertions(+), 87 deletions(-) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 524e8e621a..a563be0579 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -261,7 +261,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../p update: (id, update) => draft.update(Integration.ID.make(id), update), remove: (id) => draft.remove(Integration.ID.make(id)), method: { - list: (id) => mutable(draft.method.list(Integration.ID.make(id))), + list: (id) => draft.method.list(Integration.ID.make(id)), update: (input) => draft.method.update(methodImplementation(input)), remove: (id, method) => draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)), @@ -386,13 +386,13 @@ function methodImplementation(input: IntegrationMethodRegistration): Integration if (input.method.type === "env") { return { integrationID: Integration.ID.make(input.integrationID), - method: { type: "env", names: input.method.names }, + method: input.method, } } if (input.method.type === "command") { return { integrationID: Integration.ID.make(input.integrationID), - method: Schema.decodeUnknownSync(Integration.CommandMethod)(input.method), + method: { ...input.method, id: Integration.MethodID.make(input.method.id) }, } } return { diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 5ec83ad331..18ea39d3c3 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -1,5 +1,5 @@ import { Plugin } from "@opencode-ai/plugin/effect" -import type { IntegrationDraft, IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import type { IntegrationMethod, IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" @@ -10,13 +10,11 @@ import { Project } from "@opencode-ai/core/project" import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { WebSearch } from "@opencode-ai/core/websearch" -import { Effect, Schema, Stream } from "effect" +import { Effect, Stream } from "effect" type Overrides = Partial> & { readonly session?: Partial } -type IntegrationMethod = ReturnType[number] - export function host(overrides: Overrides = {}): Plugin.Context { return { app: overrides.app ?? { name: "test", version: "test", channel: "test" }, @@ -279,14 +277,14 @@ export function integrationHost(integration: Integration.Interface): Plugin.Cont update: (id, update) => draft.update(Integration.ID.make(id), update), remove: (id) => draft.remove(Integration.ID.make(id)), method: { - list: (id) => draft.method.list(Integration.ID.make(id)).map(method), + list: (id) => draft.method.list(Integration.ID.make(id)), update: (input) => { if ("authorize" in input) { const methodID = Integration.MethodID.make(input.method.id) const refresh = input.refresh draft.method.update({ integrationID: Integration.ID.make(input.integrationID), - method: oauthMethod(input.method, methodID), + method: { ...input.method, id: methodID }, authorize: (answer) => input.authorize(answer).pipe( Effect.map((authorization) => { @@ -337,7 +335,7 @@ export function integrationHost(integration: Integration.Interface): Plugin.Cont if (input.method.type === "env") { draft.method.update({ integrationID: Integration.ID.make(input.integrationID), - method: { ...input.method, names: [...input.method.names] }, + method: input.method, }) return } @@ -347,14 +345,13 @@ export function integrationHost(integration: Integration.Interface): Plugin.Cont method: { ...input.method, id: Integration.MethodID.make(input.method.id), - command: [...input.method.command], }, }) return } draft.method.update({ integrationID: Integration.ID.make(input.integrationID), - method: keyMethod(input.method), + method: input.method, }) }, remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)), @@ -402,63 +399,11 @@ function oauthCredential(value: Credential.OAuth) { return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) }) } -function method(value: Integration.Method): IntegrationMethod { - if (value.type === "env") return { type: value.type, names: [...value.names] } - if (value.type === "key") return { type: value.type, label: value.label, form: mutable(value.form) } - if (value.type === "command") return { ...value, command: [...value.command] } - return { - type: value.type, - id: value.id, - label: value.label, - form: mutable(value.form), - } -} - function internalMethod(value: IntegrationMethod): Integration.Method { - if (value.type === "env") return value - if (value.type === "key") return keyMethod(value) - if (value.type === "command") { - return { - ...value, - id: Integration.MethodID.make(value.id), - command: [...value.command], - } + if (value.type === "oauth" || value.type === "command") { + return { ...value, id: Integration.MethodID.make(value.id) } } - return oauthMethod(value, Integration.MethodID.make(value.id)) -} - -type Mutable = Value extends readonly [infer Head, ...infer Tail] - ? [Mutable, ...MutableTuple] - : Value extends ReadonlyArray - ? Array> - : Value extends object - ? { -readonly [Key in keyof Value]: Mutable } - : Value - -type MutableTuple> = { - -readonly [Key in keyof Value]: Mutable -} - -function mutable(value: Value): Mutable -function mutable(value: unknown): unknown { - return structuredClone(value) -} - -function keyMethod(value: { readonly label?: string; readonly form?: unknown }) { - return Schema.decodeUnknownSync(Integration.KeyMethod)({ - type: "key", - ...(value.label === undefined ? {} : { label: value.label }), - ...(value.form === undefined ? {} : { form: value.form }), - }) -} - -function oauthMethod(value: { readonly label: string; readonly form?: unknown }, id: Integration.MethodID) { - return Schema.decodeUnknownSync(Integration.OAuthMethod)({ - id, - type: "oauth", - label: value.label, - ...(value.form === undefined ? {} : { form: value.form }), - }) + return value } function agentInfo(value: Agent.Info) { diff --git a/packages/plugin/src/effect/integration.ts b/packages/plugin/src/effect/integration.ts index 4be4e4d89a..c56dfd8467 100644 --- a/packages/plugin/src/effect/integration.ts +++ b/packages/plugin/src/effect/integration.ts @@ -1,11 +1,4 @@ -import type { - ConnectionInfo, - IntegrationCommandMethod, - IntegrationEnvMethod, - IntegrationKeyMethod, - IntegrationMethod, - IntegrationOAuthMethod, -} from "@opencode-ai/client" +import type { ConnectionInfo } from "@opencode-ai/client" import type { IntegrationApi } from "@opencode-ai/client/effect/api" import { Credential } from "@opencode-ai/schema/credential" import { Form } from "@opencode-ai/schema/form" @@ -14,6 +7,37 @@ import type { Transform } from "./registration.js" type IntegrationRef = { id: string; name: string } +export interface IntegrationOAuthMethod { + readonly id: string + readonly type: "oauth" + readonly label: string + readonly form?: Form.Fields +} + +export interface IntegrationCommandMethod { + readonly id: string + readonly type: "command" + readonly label: string + readonly command: ReadonlyArray +} + +export interface IntegrationKeyMethod { + readonly type: "key" + readonly label?: string + readonly form?: Form.Fields +} + +export interface IntegrationEnvMethod { + readonly type: "env" + readonly names: ReadonlyArray +} + +export type IntegrationMethod = + | IntegrationOAuthMethod + | IntegrationCommandMethod + | IntegrationKeyMethod + | IntegrationEnvMethod + export type IntegrationOAuthAuthorization = { readonly url: string readonly instructions: string @@ -30,7 +54,7 @@ export type IntegrationOAuthAuthorization = { ) export type IntegrationOAuthMethodRegistration = { readonly integrationID: string - readonly method: Omit & { readonly form?: Form.Fields } + readonly method: IntegrationOAuthMethod readonly authorize: (answer: Form.Answer) => Effect.Effect readonly refresh?: (credential: Credential.OAuth) => Effect.Effect readonly label?: (credential: Credential.OAuth) => string | undefined @@ -43,7 +67,7 @@ export type IntegrationMethodRegistration = } | { readonly integrationID: string - readonly method: Omit & { readonly form?: Form.Fields } + readonly method: IntegrationKeyMethod } | { readonly integrationID: string diff --git a/packages/plugin/src/promise/integration.ts b/packages/plugin/src/promise/integration.ts index 730564f0f9..733b9fe2c8 100644 --- a/packages/plugin/src/promise/integration.ts +++ b/packages/plugin/src/promise/integration.ts @@ -1,11 +1,4 @@ -import type { - ConnectionInfo, - IntegrationCommandMethod, - IntegrationEnvMethod, - IntegrationKeyMethod, - IntegrationMethod, - IntegrationOAuthMethod, -} from "@opencode-ai/client" +import type { ConnectionInfo } from "@opencode-ai/client" import type { IntegrationApi } from "@opencode-ai/client/promise/api" import { Credential } from "@opencode-ai/schema/credential" import { Form } from "@opencode-ai/schema/form" @@ -13,6 +6,37 @@ import type { Transform } from "./registration.js" type IntegrationRef = { id: string; name: string } +export interface IntegrationOAuthMethod { + readonly id: string + readonly type: "oauth" + readonly label: string + readonly form?: Form.Fields +} + +export interface IntegrationCommandMethod { + readonly id: string + readonly type: "command" + readonly label: string + readonly command: ReadonlyArray +} + +export interface IntegrationKeyMethod { + readonly type: "key" + readonly label?: string + readonly form?: Form.Fields +} + +export interface IntegrationEnvMethod { + readonly type: "env" + readonly names: ReadonlyArray +} + +export type IntegrationMethod = + | IntegrationOAuthMethod + | IntegrationCommandMethod + | IntegrationKeyMethod + | IntegrationEnvMethod + export type IntegrationOAuthAuthorization = { readonly url: string readonly instructions: string @@ -30,7 +54,7 @@ export type IntegrationOAuthAuthorization = { export type IntegrationOAuthMethodRegistration = { readonly integrationID: string - readonly method: Omit & { readonly form?: Form.Fields } + readonly method: IntegrationOAuthMethod readonly authorize: (answer: Form.Answer) => Promise readonly refresh?: (credential: Credential.OAuth) => Promise readonly label?: (credential: Credential.OAuth) => string | undefined @@ -41,7 +65,7 @@ export type IntegrationMethodRegistration = | { readonly integrationID: string; readonly method: IntegrationCommandMethod } | { readonly integrationID: string - readonly method: Omit & { readonly form?: Form.Fields } + readonly method: IntegrationKeyMethod } | { readonly integrationID: string; readonly method: IntegrationEnvMethod }