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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/client/src/promise/generated/client.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions packages/protocol/src/groups/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IntegrationMethodNotFoundError,
IntegrationNotFoundError,
InvalidRequestError,
ServiceUnavailableError,
} from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"

Expand All @@ -16,6 +17,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
HttpApiEndpoint.get("integration.list", "/api/integration", {
query: LocationQuery,
success: Location.response(Schema.Array(Integration.Info)),
error: ServiceUnavailableError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand All @@ -31,7 +33,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
params: { integrationID: Integration.ID },
query: LocationQuery,
success: Location.response(Integration.Info),
error: IntegrationNotFoundError,
error: [IntegrationNotFoundError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand Down Expand Up @@ -68,7 +70,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
label: Schema.optional(Schema.String),
}),
success: HttpApiSchema.NoContent,
error: [IntegrationNotFoundError, InvalidRequestError],
error: [IntegrationNotFoundError, InvalidRequestError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand All @@ -89,7 +91,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
label: Schema.optional(Schema.String),
}),
success: HttpApiSchema.NoContent,
error: [IntegrationNotFoundError, InvalidRequestError],
error: [IntegrationNotFoundError, InvalidRequestError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand All @@ -110,7 +112,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
label: Schema.optional(Schema.String),
}),
success: Location.response(Integration.Attempt),
error: InvalidRequestError,
error: [InvalidRequestError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand All @@ -126,7 +128,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
query: LocationQuery,
success: Location.response(Integration.AttemptStatus),
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError],
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand All @@ -146,7 +148,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
query: LocationQuery,
payload: Schema.Struct({ code: Schema.optional(Schema.String) }),
success: HttpApiSchema.NoContent,
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, InvalidRequestError],
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, InvalidRequestError, ServiceUnavailableError],
},
)
.annotateMerge(locationQueryOpenApi)
Expand Down Expand Up @@ -182,7 +184,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
label: Schema.optional(Schema.String),
}),
success: Location.response(Integration.CommandAttempt),
error: [IntegrationNotFoundError, IntegrationMethodNotFoundError, InvalidRequestError],
error: [IntegrationNotFoundError, IntegrationMethodNotFoundError, InvalidRequestError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand All @@ -198,7 +200,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
query: LocationQuery,
success: Location.response(Integration.CommandAttemptStatus),
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError],
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
Expand Down
13 changes: 10 additions & 3 deletions packages/server/src/handlers/integration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Integration } from "@opencode/core/integration"
import { Plugin } from "@opencode/core/plugin"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/http-api"
import { Api } from "../api"
Expand All @@ -9,7 +8,7 @@ import {
IntegrationNotFoundError,
InvalidRequestError,
} from "@opencode/protocol/errors"
import { response } from "../location"
import { awaitPlugins, response } from "../location"
import { WellKnown } from "@opencode/core/wellknown"
import { Config } from "@opencode/core/config"
import { ManagedPolicy } from "@opencode/core/managed-policy"
Expand Down Expand Up @@ -54,7 +53,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.list",
Effect.fn(function* () {
yield* Plugin.awaitActivation
yield* awaitPlugins
const service = yield* Integration.Service
const current = yield* statements()
return yield* response(
Expand All @@ -75,6 +74,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.get",
Effect.fn(function* (ctx) {
yield* awaitPlugins
const service = yield* Integration.Service
const integration = yield* service.get(ctx.params.integrationID)
if (!integration)
Expand Down Expand Up @@ -106,6 +106,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.connect.key",
Effect.fn(function* (ctx) {
yield* awaitPlugins
yield* requireConnectable(ctx.params.integrationID)
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
Expand All @@ -127,6 +128,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.connect.external",
Effect.fn(function* (ctx) {
yield* awaitPlugins
yield* requireConnectable(ctx.params.integrationID)
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
Expand All @@ -148,6 +150,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.oauth.connect",
Effect.fn(function* (ctx) {
yield* awaitPlugins
yield* requireConnectable(ctx.params.integrationID)
const service = yield* Integration.Service
return yield* response(
Expand All @@ -165,6 +168,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.oauth.status",
Effect.fn(function* (ctx) {
yield* awaitPlugins
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
Expand Down Expand Up @@ -193,6 +197,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.oauth.complete",
Effect.fn(function* (ctx) {
yield* awaitPlugins
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
Expand Down Expand Up @@ -242,6 +247,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.command.connect",
Effect.fn(function* (ctx) {
yield* awaitPlugins
yield* requireConnectable(ctx.params.integrationID)
const service = yield* Integration.Service
const integration = yield* service.get(ctx.params.integrationID)
Expand Down Expand Up @@ -270,6 +276,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
.handle(
"integration.command.status",
Effect.fn(function* (ctx) {
yield* awaitPlugins
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
Expand Down
34 changes: 32 additions & 2 deletions packages/server/test/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ it.live(
yield* sdk.register(
Plugin.define({
id: "slow-plugin",
effect: () =>
effect: (ctx) =>
Effect.gen(function* () {
yield* Deferred.succeed(started, undefined)
yield* Deferred.await(release)
yield* ctx.integration.transform((editor) =>
editor.update("late-integration", (integration) => {
integration.name = "Late integration"
}),
)
}),
}),
)
Expand All @@ -58,7 +63,11 @@ it.live(
return handler(
new Request(url, {
method,
headers: { authorization: `Basic ${btoa("opencode:secret")}` },
headers: {
authorization: `Basic ${btoa("opencode:secret")}`,
...(method === "POST" ? { "content-type": "application/json" } : {}),
},
...(method === "POST" ? { body: JSON.stringify({ methodID: "missing" }) } : {}),
signal,
}),
)
Expand All @@ -69,6 +78,15 @@ it.live(
const agents = yield* request("GET", "/api/agent").pipe(Effect.forkScoped)
yield* Deferred.await(started)
const integrations = yield* request("GET", "/api/integration").pipe(Effect.forkScoped)
const integration = yield* request("GET", "/api/integration/late-integration").pipe(
Effect.forkScoped({ startImmediately: true }),
)
const oauth = yield* request("GET", "/api/integration/late-integration/connect/oauth/missing").pipe(
Effect.forkScoped({ startImmediately: true }),
)
const command = yield* request("POST", "/api/integration/late-integration/connect/command").pipe(
Effect.forkScoped({ startImmediately: true }),
)
const get = yield* request("GET", "/api/provider/custom").pipe(Effect.timeout("2 seconds"))
expect(get.status).toBe(404)
expect(yield* Effect.promise(() => get.json())).toMatchObject({
Expand All @@ -77,6 +95,9 @@ it.live(
})
expect(providers.pollUnsafe()).toBeUndefined()
expect(agents.pollUnsafe()).toBeUndefined()
expect(integration.pollUnsafe()).toBeUndefined()
expect(oauth.pollUnsafe()).toBeUndefined()
expect(command.pollUnsafe()).toBeUndefined()
yield* Deferred.succeed(release, undefined)
const list = yield* Fiber.join(providers)
expect(list.status).toBe(200)
Expand All @@ -97,6 +118,15 @@ it.live(
expect(yield* Effect.promise(() => discovered.json())).toMatchObject({
data: expect.arrayContaining([expect.objectContaining({ id: "opencode" })]),
})
const item = yield* Fiber.join(integration)
expect(item.status).toBe(200)
expect(yield* Effect.promise(() => item.json())).toMatchObject({ data: { id: "late-integration" } })
const attempt = yield* Fiber.join(oauth)
expect(attempt.status).toBe(404)
expect(yield* Effect.promise(() => attempt.json())).toMatchObject({ _tag: "IntegrationAttemptNotFoundError" })
const commandResult = yield* Fiber.join(command)
expect(commandResult.status).toBe(404)
expect(yield* Effect.promise(() => commandResult.json())).toMatchObject({ _tag: "IntegrationMethodNotFoundError" })
}),
15_000,
)
Loading